📝 本文将带你从零开始,用几行代码实现自动识别发票轮廓 + 透视矫正,把歪歪扭扭的扫描件变成规整的电子文档,全程无门槛,复制粘贴就能跑!


🎯 你能学到什么?

  • 图像预处理:缩小、灰度化、边缘检测
  • 轮廓识别:自动找到发票的最大外轮廓
  • 坐标排序:给四边形顶点排好顺序,避免变换错乱
  • 透视变换:把倾斜的发票 “掰正”,输出规整图像
  • 完整代码:直接复制运行,小白也能出效果

🧰 准备工作

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 * peri0.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 文字识别的扩展教程,把矫正后的发票直接转成可复制的文字?这样就能实现 “拍照→矫正→提取文字” 的全自动流程了!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐