OpenCV图像透视变换:自动矫正倾斜的发票

1. 引言

在日常生活中,用手机拍摄的文档照片往往因为拍摄角度而产生透视畸变 ------原本方正的纸张变成了不规则的四边形,导致文字歪斜,影响阅读和OCR识别。透视变换技术可以将图像中任意四边形区域"拉正"为矩形,完美解决这一问题。本文将基于你提供的完整Python代码,深入解析利用OpenCV实现自动文档矫正的每一步,从图像预处理、轮廓检测到透视变换,并详细讲解核心函数的原理与参数含义。

2. 透视变换核心原理

透视变换是一种将图像从一个平面投影到另一个平面的几何变换。它用一个 3×3 的单应性矩阵 描述,需要至少4对对应点才能求解。在文档矫正场景中,这4对点就是原始图像中文档的四个顶点和矫正后矩形的四个顶点。OpenCV提供了两个核心函数:

  • cv2.getPerspectiveTransform(src, dst):根据源点和目标点计算变换矩阵。

  • cv2.warpPerspective(src, M, dsize):应用变换矩阵到图像。

3. 环境准备

  • Python 3.x

  • OpenCV (pip install opencv-python)

  • NumPy (pip install numpy)

4. 代码逐段详解

4.1 导入库

复制代码
import numpy as np
import cv2
  • numpy:用于高效的数组运算和数学计算(如距离、求和)。

  • cv2:OpenCV库,提供图像处理、轮廓检测、透视变换等功能。

4.2 辅助函数

4.2.1 图像显示函数 cv_show
复制代码
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
  • 作用:方便在调试过程中随时显示图像,按下任意键后关闭窗口并释放资源。

  • cv2.waitKey(0) 等待按键,参数0表示无限等待。

  • cv2.destroyAllWindows() 关闭所有OpenCV创建的窗口。

4.2.2 图像等比例缩放函数 resize
复制代码
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
  • 作用:按指定宽度或高度等比例缩放图像,保持宽高比,避免图像变形。

  • 参数:

    • widthheight:指定目标宽度或高度,只需提供一个,另一个自动计算。

    • inter:插值方法,默认为cv2.INTER_AREA,适合缩小图像。

  • 原理:根据提供的尺寸计算缩放比例 r,然后生成新尺寸 dim,最后调用cv2.resize完成缩放。

4.2.3 顶点排序函数 order_points
复制代码
def order_points(pts):
    # 一共4个坐标点
    rect = np.zeros(shape=(4, 2), dtype="float32")  # 用来存储排序之后的坐标位置
    # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
    s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作。(x+y)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作。(y-x)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect
  • 为什么要排序? 透视变换要求源点和目标点的顺序必须一一对应(例如左上对左上)。而轮廓检测得到的四个点顺序是随机的,需要重新排序为 左上、右上、右下、左下

  • 排序原理

    • 左上角的点具有最小的 x+y 值(靠近原点)。

    • 右下角的点具有最大的 x+y 值。

    • 右上角的点具有最小的 y-x 值(因为y和x接近)。

    • 左下角的点具有最大的 y-x 值(x小y大)。

  • 代码解析:

    • pts.sum(axis=1):计算每个点的 (x, y) 和,得到一维数组。

    • np.argmin(s)np.argmax(s) 分别找出和最小及最大的点的索引,分别赋值给左上和右下。

    • np.diff(pts, axis=1):计算每个点的 y - x(因为diff是后减前,即 x[:,1] - x[:,0],注意这里实际是 y - x 取决于pts的列顺序,通常pts是 (x, y),所以diff得到 (y - x))。

    • 同理,找出差最小和最大的点作为右上和左下。

4.2.4 透视变换核心函数 four_point_transform
复制代码
def four_point_transform(image, pts):
    # 获取输入坐标点
    rect = order_points(pts)
    (tl, tr, br, bl) = rect
    # 计算输入的w和h值
    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, dsize=(maxWidth, maxHeight))
    # 返回变换后结果
    return warped
  • 步骤解析:

    1. 排序顶点 :调用order_points得到按顺序排列的四个点 (tl, tr, br, bl)

    2. 计算目标矩形尺寸:由于原始四边形可能不是严格的矩形,上下边长度可能不同,左右边长度也可能不同。为了确保矫正后的图像包含整个文档内容,取上下边宽度的最大值作为目标宽度,左右边长度的最大值作为目标高度。

      • widthA:底部边(bl→br)的欧氏距离。

      • widthB:顶部边(tl→tr)的欧氏距离。

      • maxWidth:取两者较大值并转为整数。

      • 同理计算高度。

    3. 定义目标点:目标矩形四个顶点按顺序为左上(0,0)、右上(maxWidth-1,0)、右下(maxWidth-1,maxHeight-1)、左下(0,maxHeight-1)。注意减1是因为像素索引从0开始。

    4. 计算变换矩阵cv2.getPerspectiveTransform(rect, dst) 返回3×3变换矩阵M。

    5. 应用变换cv2.warpPerspective(image, M, (maxWidth, maxHeight)) 输出矫正后的图像。

4.3 主程序流程

4.3.1 读取图像并显示原图
复制代码
image = cv2.imread('fapiao.jpg')
cv_show('image', image)
  • 读取图片 fapiao.jpg,并用自定义函数显示。
4.3.2 缩放图像以加快处理
复制代码
ratio = image.shape[0] / 500.0  # 计算缩小比率
orig = image.copy()
image = resize(orig, height=500)
cv_show('1', image)
  • ratio:原图高度除以500,得到缩放比例,后续用于将检测到的轮廓坐标映射回原图。

  • orig:保存原始图像副本,用于最终的高质量透视变换。

  • resize(orig, height=500):将图像高度缩放到500像素,宽度等比例缩放。处理缩小图能极大提高轮廓检测速度。

  • 显示缩放后的图像(标题'1')。

结果展示:

4.3.3 轮廓检测
复制代码
print("STEP 1: 轮廓检测")
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]
image_contours = cv2.drawContours(image.copy(), cnts, -1, color=(0, 0, 255), thickness=1)
cv_show('image_contours', image_contours)
  • 灰度化cv2.cvtColor 将BGR图像转为灰度图,便于二值化。

  • 二值化cv2.threshold 使用OTSU算法自动计算最优阈值,得到黑白二值图 edged。参数 cv2.THRESH_BINARY | cv2.THRESH_OTSU 表示采用OTSU自动阈值且输出为二值图像。

  • 轮廓检测cv2.findContours 查找所有轮廓。参数:

    • edged.copy():传入二值图副本,避免函数修改原图。

    • cv2.RETR_LIST:提取所有轮廓,不建立等级关系。

    • cv2.CHAIN_APPROX_SIMPLE:压缩轮廓,只保留端点。

    • [-2]:兼容不同OpenCV版本,确保返回轮廓列表。

  • 绘制轮廓:在图像副本上用红色(BGR=(0,0,255))绘制所有轮廓,便于观察。

  • 显示结果。

结果展示:

4.3.4 获取最大轮廓并近似为四边形
复制代码
print("STEP 2: 获取最大轮廓")
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]  # 获取面积最大的轮廓
print(screenCnt.shape)
peri = cv2.arcLength(screenCnt, closed=True)  # 计算轮廓周长
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, closed=True)  # 轮廓近似
print(screenCnt.shape)
image_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 255, 0), thickness=2)

cv2.imshow("image_contour", image_contour)
cv2.waitKey(0)
  • 筛选最大轮廓 :假设文档是图像中面积最大的物体,按面积降序排序,取第一个轮廓 screenCnt

  • 计算周长cv2.arcLength 获取轮廓周长,closed=True 表示轮廓闭合。

  • 多边形近似cv2.approxPolyDP 使用道格拉斯-普克算法简化轮廓。参数 0.05 * peri 是近似精度,值越小越精细,这里设为周长的5%,目的是将复杂轮廓简化为四边形。理想情况下,简化后轮廓应包含4个点。

  • 打印形状:查看简化前后的顶点数。

  • 绘制近似轮廓:在图像副本上用绿色((0,255,0))绘制该轮廓,线条粗细2。

  • 显示结果。

结果展示:

4.3.5 透视变换
复制代码
###############
# 透视变换
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
cv2.imwrite('invoice_new.jpg', warped)
cv2.namedWindow('xx', cv2.WINDOW_NORMAL)
cv2.imshow("xx", warped)
cv2.waitKey(0)
  • 坐标还原

    • screenCnt 当前形状可能为 (4, 1, 2),通过 reshape(4, 2) 转换为标准的 (4, 2) 数组。

    • 乘以缩放比例 ratio,将坐标从缩放后的图像空间映射回原始图像空间,确保透视变换在原始分辨率下进行,保留细节。

  • 调用透视变换four_point_transform(orig, ...) 对原始图像 orig 进行矫正,得到 warped

  • 保存结果cv2.imwrite 将矫正后的图像保存为 invoice_new.jpg

  • 显示结果 :创建一个可调整大小的窗口 xx 显示矫正后的图像,便于查看细节。

运行结果展示:

5. 运行结果

输入图像 fapiao.jpg 是一张倾斜拍摄的发票,程序将依次显示:

  • 原图

  • 缩放后的图像(高度500)

  • 所有轮廓(红色)

  • 检测到的最大轮廓近似结果(绿色四边形)

  • 最终矫正后的正面图像(保存为 invoice_new.jpg

矫正后的图像将呈现标准的矩形,文字方向变正,极大方便后续OCR识别。

6. 总结与注意事项

6.1 总结

本文通过一个完整的OpenCV案例,详细讲解了如何利用透视变换自动矫正倾斜的文档图像。核心步骤包括:

  • 图像缩放加速处理

  • 灰度化与OTSU二值化

  • 轮廓检测与筛选

  • 多边形近似获取四个顶点

  • 顶点排序与透视变换

代码结构清晰,函数封装良好,便于复用和扩展。

6.2 注意事项

  • 顶点近似精度cv2.approxPolyDPepsilon 参数(0.05 * peri)需要根据实际图像调整。若无法得到4个点,可适当减小或增大该值。

  • 最大轮廓假设:代码假设文档是面积最大的轮廓,如果图像中包含其他更大物体(如桌面、背景杂物),可能导致误检。可增加筛选条件(如轮廓长宽比、面积范围)提高鲁棒性。

  • 坐标映射screenCnt.reshape(4, 2) * ratio 必须在调用透视变换前执行,确保使用原始图像坐标。

  • 图像方向 :如果矫正后图像方向不对(例如旋转了90度),可用 cv2.rotate 进行修正。

通过理解和掌握这段代码,你可以轻松将其应用到车牌矫正、名片扫描、增强现实等更多场景中。如果在实践中遇到问题,欢迎留言交流!

相关推荐
霍格沃兹测试学院-小舟畅学1 小时前
LangChain + DeepSeek 实战拆解:从 LCEL 到智能体,如何真正“做出”一个可控 AI 系统?
java·人工智能·langchain
新缸中之脑1 小时前
TADA:零幻觉 TTS 模型
人工智能·语音识别
智算菩萨1 小时前
通往AGI之路:基于性能与通用性的等级划分框架深度解析
论文阅读·人工智能·深度学习·ai·agi
vivo互联网技术1 小时前
ICLR2026 | Ada-RefSR: 自适应隐式相关建模,开启“信而有证”的参考超分新范式
人工智能
GJGCY1 小时前
2026 RPA平台技术路径对比:AI融合、扩展性与信创适配实测
人工智能·ai·rpa·智能体·数字员工
乐迪信息2 小时前
乐迪信息:AI防爆摄像机识别船舶违规明火作业
大数据·人工智能·安全·计算机视觉·目标跟踪
Moshow郑锴2 小时前
2026.3 AWS连环事故原因分析-究竟是裁员导致还是AI失控?
人工智能·云计算·aws·incident
油泼辣子多加2 小时前
【ML】SVM算法原理
人工智能·算法·机器学习·支持向量机·数据挖掘
BFT白芙堂2 小时前
高精度仿真引领机器人学习:以 Franka Research 3 为核心的 PolaRiS 评估框架深度解析
人工智能·学习·机器人·人机交互·具身智能机器人·franka