小白也能学会!用 Python+OpenCV 自动矫正发票 / 文档透视畸变
你只用了几十行代码,就实现了专业扫描仪级别的文档矫正!轮廓识别 → 坐标排序 → 透视变换适用场景:发票、身份证、合同、书本页面等所有四边形文档扩展玩法:可以结合 OCR 工具(如),直接把矫正后的图像转成可编辑文字!
📝 本文将带你从零开始,用几行代码实现自动识别发票轮廓 + 透视矫正,把歪歪扭扭的扫描件变成规整的电子文档,全程无门槛,复制粘贴就能跑!
🎯 你能学到什么?
- 图像预处理:缩小、灰度化、边缘检测
- 轮廓识别:自动找到发票的最大外轮廓
- 坐标排序:给四边形顶点排好顺序,避免变换错乱
- 透视变换:把倾斜的发票 “掰正”,输出规整图像
- 完整代码:直接复制运行,小白也能出效果
🧰 准备工作
1. 安装依赖
打开终端 / 命令提示符,执行下面这行命令:
bash
运行
pip install opencv-python numpy matplotlib
opencv-python:处理图像的核心库numpy:处理数组和坐标计算matplotlib:可选,用来画直方图
2. 准备素材
找一张倾斜的发票 / 文档照片(比如fapiao.jpg),放在和代码同一个文件夹里。
🧩 核心函数拆解(小白友好版)
1. 图像显示工具函数
为了避免重复写cv2.imshow+cv2.waitKey,我们先封装一个显示函数:
python
运行
import cv2
import numpy as np
def cv_show(name, img):
"""显示图像,按任意键关闭窗口"""
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
✅ 用法:cv_show("窗口名", 图像变量),调试时超方便!
2. 四边形坐标排序函数
透视变换需要固定顺序的四个顶点(左上→右上→右下→左下),否则图像会乱掉:
python
运行
def order_points(pts):
# 初始化4个坐标点的容器
rect = np.zeros((4, 2), dtype="float32")
# 按x+y的和排序:最小=左上,最大=右下
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # 左上
rect[2] = pts[np.argmax(s)] # 右下
# 按y-x的差排序:最小=右上,最大=左下
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # 右上
rect[3] = pts[np.argmax(diff)] # 左下
return rect
💡 原理:利用坐标的和与差,自动给四个顶点排好队,不用手动指定顺序!
3. 四点透视变换函数
这是 “掰正” 图像的核心:
python
运行
def four_point_transform(image, pts):
# 1. 先把坐标排好序
rect = order_points(pts)
tl, tr, br, bl = rect # 拆成四个点
# 2. 计算矫正后图像的宽和高
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB)) # 取最大宽度
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB)) # 取最大高度
# 3. 定义矫正后图像的目标坐标
dst = np.array([
[0, 0], # 新左上
[maxWidth - 1, 0], # 新右上
[maxWidth - 1, maxHeight - 1], # 新右下
[0, maxHeight - 1] # 新左下
], dtype="float32")
# 4. 计算透视变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
# 5. 执行变换,输出矫正后的图像
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped
✅ 一句话总结:输入原图 + 四个顶点,输出 “掰正” 的规整图像!
4. 图像缩放函数
避免图片太大导致卡顿,先缩小再处理:
python
运行
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
dim = None
(h, w) = image.shape[:2] # 获取原图高和宽
if width is None and height is None:
return image # 没指定就返回原图
if width is None:
r = height / float(h) # 按高度等比例缩放
dim = (int(w * r), height)
else:
r = width / float(w) # 按宽度等比例缩放
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
💡 用法:resize(原图, height=500) → 把高度缩到 500 像素,宽度自动等比例变化。
🚀 完整流程:从倾斜发票到规整文档
步骤 1:读取并预处理图像
python
运行
# 1. 读取发票图片
image = cv2.imread("fapiao.jpg")
cv_show("原始图像", image)
# 2. 记录缩放比例(后面要还原坐标)
ratio = image.shape[0] / 500.0
orig = image.copy() # 保存原图,最后用它矫正
image = resize(orig, height=500) # 缩小到高度500像素
cv_show("缩小后的图像", image)
步骤 2:边缘检测 + 轮廓识别
python
运行
print("STEP 1: 轮廓检测")
# 1. 转灰度图(减少计算量)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 2. 二值化(自动找阈值,突出边缘)
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 3. 找所有轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 4. 画出所有轮廓(调试用)
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show("所有轮廓", image_contours)
步骤 3:找到最大轮廓(发票边缘)
python
运行
print("STEP 2: 获取最大轮廓")
# 1. 按面积排序,取最大的那个(就是发票的外轮廓)
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# 2. 轮廓近似(把复杂曲线变成四边形)
peri = cv2.arcLength(screenCnt, closed=True) # 计算轮廓周长
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, closed=True) # 近似成四边形
# 3. 画出最大轮廓(调试用)
image_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 255, 0), 2)
cv_show("最大轮廓(发票边缘)", image_contour)
💡 关键:approxPolyDP会把锯齿状的轮廓变成规整的四边形,正好是发票的四个角!
步骤 4:执行透视矫正
python
运行
print("STEP 3: 透视矫正")
# 1. 还原坐标(因为之前缩小了图像,坐标要乘回缩放比例)
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
# 2. 保存矫正后的图像
cv2.imwrite("invoice_new.jpg", warped)
# 3. 显示结果
cv2.namedWindow("矫正后的发票", cv2.WINDOW_NORMAL) # 可缩放窗口
cv2.imshow("矫正后的发票", warped)
cv2.waitKey(0)
cv2.destroyAllWindows()
✨ 运行效果
- 输入:一张倾斜、拍摄角度不正的发票照片
- 输出:
invoice_new.jpg→ 一张完全规整、水平的发票扫描件,和扫描仪扫出来的一样!
📌 小白常见问题解答
Q1:运行报错ModuleNotFoundError: No module named 'cv2'
A:没装 OpenCV,回到准备工作部分,执行pip install opencv-python。
Q2:找不到发票轮廓,画出的轮廓是乱的
A:
- 检查图片:确保发票在画面中占主要部分,背景不要太复杂
- 调整参数:把
0.05 * peri改成0.03 * peri或0.08 * peri,让轮廓更精确 / 更简化 - 光线问题:保证发票光线均匀,避免过暗 / 过曝
Q3:矫正后的图像是倒的 / 歪的
A:
- 检查
order_points函数是否正确复制,坐标顺序错了会导致图像翻转 - 确保最大轮廓是四边形(
screenCnt.shape应该是(4, 1, 2))
🎉 总结
你只用了几十行代码,就实现了专业扫描仪级别的文档矫正!
- 核心逻辑:轮廓识别 → 坐标排序 → 透视变换
- 适用场景:发票、身份证、合同、书本页面等所有四边形文档
- 扩展玩法:可以结合 OCR 工具(如
pytesseract),直接把矫正后的图像转成可编辑文字!
完整代码一键复制
把下面所有代码粘到invoice_correction.py里,放一张fapiao.jpg在同目录,直接运行即可:
python
运行
import cv2
import numpy as np
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
def four_point_transform(image, pts):
rect = order_points(pts)
tl, tr, br, bl = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
dst = np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
# 主流程
if __name__ == "__main__":
image = cv2.imread("fapiao.jpg")
ratio = image.shape[0] / 500.0
orig = image.copy()
image = resize(orig, height=500)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
peri = cv2.arcLength(screenCnt, closed=True)
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, closed=True)
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
cv2.imwrite("invoice_new.jpg", warped)
cv_show("矫正结果", warped)
要不要我再帮你写一篇OCR 文字识别的扩展教程,把矫正后的发票直接转成可复制的文字?这样就能实现 “拍照→矫正→提取文字” 的全自动流程了!
更多推荐
所有评论(0)