目录
[1. 透视变换:让倾斜发票 "正过来"](#1. 透视变换:让倾斜发票 “正过来”)
[(2)透视变换的 5 个关键步骤](#(2)透视变换的 5 个关键步骤)
[2. 轮廓检测:精准定位发票区域](#2. 轮廓检测:精准定位发票区域)
[(2)轮廓检测的 5 个执行步骤](#(2)轮廓检测的 5 个执行步骤)
[二、项目实战:OpenCV 发票识别全流程代码](#二、项目实战:OpenCV 发票识别全流程代码)
[1. 环境准备](#1. 环境准备)
[2. 工具函数定义](#2. 工具函数定义)
[3. 发票识别全流程执行](#3. 发票识别全流程执行)
[(1)步骤 1:读取原图并缩放](#(1)步骤 1:读取原图并缩放)
[(2)步骤 2:图像预处理与轮廓检测](#(2)步骤 2:图像预处理与轮廓检测)
[(3)步骤 3:筛选最大轮廓(定位发票区域)](#(3)步骤 3:筛选最大轮廓(定位发票区域))
[(4)步骤 4:透视变换校正发票](#(4)步骤 4:透视变换校正发票)
[(5)步骤 5:二值化与形态学优化(为 OCR 准备)](#(5)步骤 5:二值化与形态学优化(为 OCR 准备))
前言
在办公自动化与计算机视觉结合的场景中,发票识别是典型的落地需求 ------ 实际拍摄的发票常因角度倾斜、背景杂乱导致文字提取困难,而基于 OpenCV 的透视变换与轮廓检测技术,能快速将倾斜发票校正为正视角、高对比度的规整图像,为后续 OCR 文字识别奠定基础。本文将从核心技术原理出发,结合完整代码与实战效果,拆解发票识别的全流程,适合 OpenCV 入门者与计算机视觉爱好者学习。
一、核心技术原理:透视变换与轮廓检测
在开始项目前,需先理解两个关键技术的核心逻辑 ------ 它们是发票校正与目标提取的基础。
1. 透视变换:让倾斜发票 "正过来"
(1)什么是透视变换?
透视变换是一种将三维空间中倾斜的平面映射到二维平面的几何变换,它能模拟人眼的透视效果,解决 "拍摄角度倾斜导致发票边缘不规整" 的问题。例如,从斜上方拍摄的发票,其四个角会呈现梯形或不规则四边形,通过透视变换可将其校正为标准矩形,还原发票的真实比例。
透视变换的数学本质是通过4 组对应点(源图像 4 个角点与目标图像 4 个角点)计算变换矩阵,再用该矩阵对源图像进行像素映射。核心特点是:
- 平行线可能在变换后相交(符合真实透视规律);
- 能保留图像的细节信息,仅改变视角。
(2)透视变换的 5 个关键步骤
- 确定源图像与目标图像:源图像是拍摄的倾斜发票,目标图像是期望得到的 "正矩形发票";
- 选取 4 组对应关键点 :需在源图像中找到发票的 4 个顶点(左上、右上、右下、左下),并定义目标图像中对应的 4 个顶点(如
(0,0)
、(width,0)
、(width,height)
、(0,height)
); - 计算变换矩阵 :使用 OpenCV 的
cv2.getPerspectiveTransform()
,通过 4 组对应点生成 3x3 的透视变换矩阵M
; - 执行透视变换 :用
cv2.warpPerspective()
加载变换矩阵M
,将源图像映射为目标图像; - 插值处理 :由于变换后像素可能映射到非整数坐标,需通过插值(如
INTER_LINEAR
)补充像素值,保证图像清晰度。
2. 轮廓检测:精准定位发票区域
(1)什么是轮廓检测?
轮廓是图像中连续的、具有相同灰度或颜色的像素边缘,轮廓检测本质是从图像中提取目标物体的边界,在发票识别中用于 "从复杂背景中精准框选出发票区域"。
与边缘检测(如 Canny)不同,轮廓检测不仅能找到离散的边缘点,还能将边缘点连接成连续的闭合曲线,便于后续计算目标的面积、周长、顶点等特征。常用的轮廓检测算法依赖图像的二值化结果(黑白对比),因此预处理步骤尤为重要。
(2)轮廓检测的 5 个执行步骤
- 图像预处理 :将彩色图像转为灰度图(减少计算量),再通过高斯滤波(
cv2.GaussianBlur
)去除噪声,避免噪声干扰轮廓提取; - 边缘检测 :用二值化(如
cv2.threshold
+THRESH_OTSU
自动阈值)将灰度图转为黑白图像,突出发票与背景的对比; - 轮廓提取 :通过
cv2.findContours()
提取图像中所有轮廓,指定轮廓检索模式(如RETR_LIST
提取所有轮廓)与逼近方法(如CHAIN_APPROX_SIMPLE
简化轮廓点); - 轮廓筛选 :根据轮廓的面积(
cv2.contourArea
)、周长(cv2.arcLength
)等特征筛选出 "发票轮廓"------ 通常是面积最大的闭合轮廓; - 轮廓绘制与验证 :用
cv2.drawContours()
将筛选后的轮廓绘制在原图上,验证是否准确框选出发票区域。
二、项目实战:OpenCV 发票识别全流程代码
1. 环境准备
- 编程语言:Python 3.7+
- 依赖库:OpenCV(
pip install opencv-python
)、NumPy(pip install numpy
) - 测试数据:拍摄的倾斜发票图像(命名为
fapiao.jpg
,建议分辨率不低于 1000x800)
2. 工具函数定义
先封装 4 个核心工具函数,提高代码复用性与可读性。
(1)图像展示函数
用于快速展示处理过程中的图像,避免重复编写cv2.imshow
与cv2.waitKey
:
python
import cv2
import numpy as np
def cv_show(name, img):
"""
展示图像的通用函数
:param name: 窗口名称
:param img: 输入图像(numpy数组)
"""
cv2.imshow(name, img)
cv2.waitKey(0) # 等待按键关闭窗口
cv2.destroyWindow(name) # 关闭指定窗口
(2)图像自动缩放函数
解决 "原图过大导致窗口无法完整显示" 的问题,保持图像宽高比不变:
python
def resize_image(image, width=None, height=None, inter=cv2.INTER_AREA):
"""
保持宽高比的图像缩放函数
:param image: 输入图像
:param width: 目标宽度(None则按高度计算)
:param height: 目标高度(None则按宽度计算)
:param inter: 插值方式(默认INTER_AREA,适合缩放)
:return: 缩放后的图像
"""
dim = None # 存储目标尺寸(宽,高)
h, w = image.shape[:2] # 获取原图高、宽
# 若未指定宽和高,直接返回原图
if width is None and height is None:
return image
# 仅指定高度:按高度比例计算宽度
if width is None:
ratio = height / float(h)
dim = (int(w * ratio), height)
# 仅指定宽度:按宽度比例计算高度
else:
ratio = width / float(w)
dim = (width, int(h * ratio))
# 执行缩放
resized = cv2.resize(image, dim, interpolation=inter)
return resized
(3)轮廓点排序函数
透视变换需要 4 个 "有序的顶点"(左上→右上→右下→左下),该函数通过计算点的坐标和与差值实现排序:
python
def order_points(pts):
"""
对轮廓的4个顶点按"左上→右上→右下→左下"排序
:param pts: 输入的4个顶点(shape为(4,2)的numpy数组)
:return: 排序后的顶点数组
"""
rect = np.zeros((4, 2), dtype="float32") # 初始化排序后的数组
# 步骤1:按"x+y"的和排序(左上和最小,右下和最大)
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # 左上点(x+y最小)
rect[2] = pts[np.argmax(s)] # 右下点(x+y最大)
# 步骤2:按"y-x"的差值排序(右上差值最小,左下差值最大)
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # 右上点(y-x最小)
rect[3] = pts[np.argmax(diff)] # 左下点(y-x最大)
return rect
(4)透视变换函数
输入图像与 4 个顶点,返回校正后的规整图像:
python
def four_point_transform(image, pts):
"""
基于4个顶点的透视变换函数
:param image: 源图像(原始倾斜发票)
:param pts: 源图像中发票的4个顶点
:return: 校正后的图像
"""
# 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. 定义目标图像的4个顶点(标准矩形)
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
], dtype="float32")
# 4. 计算透视变换矩阵并执行变换
M = cv2.getPerspectiveTransform(rect, dst) # 生成3x3变换矩阵
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 执行变换
return warped
3. 发票识别全流程执行
(1)步骤 1:读取原图并缩放
先加载原始发票图像,按固定高度缩放(避免原图过大),同时保存缩放比例(后续用于还原轮廓坐标):
python
# 读取原始发票图像
orig = cv2.imread("fapiao.jpg")
if orig is None:
raise ValueError("未找到图像文件,请检查路径是否正确!")
# 缩放图像(固定高度为500,保持宽高比)
ratio = orig.shape[0] / 500.0 # 缩放比例(原图高 / 缩放后高)
image = resize_image(orig, height=500)
# 展示原图与缩放图
cv_show("原始发票", orig)
cv_show("缩放后发票", image)
以下是缩放后的发票图:

(2)步骤 2:图像预处理与轮廓检测
通过灰度化、二值化突出发票边缘,再提取所有轮廓:
python
# 1. 灰度化(减少通道数,降低计算量)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 2. 二值化(自动阈值,突出发票与背景对比)
# THRESH_OTSU:自动计算最优阈值,适合明暗对比明显的图像
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
# 3. 提取所有轮廓
# RETR_LIST:提取所有轮廓,不建立层次关系;CHAIN_APPROX_SIMPLE:简化轮廓点
cnts, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# 4. 绘制所有轮廓(红色,线宽1),验证提取效果
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show("所有轮廓", image_contours)
所有轮廓如下:

(3)步骤 3:筛选最大轮廓(定位发票区域)
发票通常是图像中面积最大的闭合区域,通过轮廓面积排序筛选出目标轮廓,并进行多边形近似(减少轮廓点数量):
python
# 1. 按轮廓面积降序排序,取面积最大的轮廓(即发票轮廓)
screen_cnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# 2. 轮廓近似(将不规则轮廓近似为多边形)
# arcLength:计算轮廓周长(True表示闭合轮廓)
peri = cv2.arcLength(screen_cnt, True)
# approxPolyDP:轮廓近似,epsilon=0.02*peri(控制近似精度)
screen_cnt = cv2.approxPolyDP(screen_cnt, 0.02 * peri, True)
# 3. 验证轮廓是否为4个顶点(发票是四边形,需4个顶点)
if len(screen_cnt) != 4:
raise ValueError("未检测到发票的4个顶点,请调整拍摄角度或图像质量!")
# 4. 绘制最大轮廓(绿色,线宽2)
image_max_contour = cv2.drawContours(image.copy(), [screen_cnt], -1, (0, 255, 0), 2)
cv_show("发票最大轮廓", image_max_contour)
最大轮廓如下:

(4)步骤 4:透视变换校正发票
用筛选出的 4 个顶点执行透视变换,将倾斜发票校正为正矩形:
python
# 1. 还原轮廓坐标(缩放后的坐标 * 缩放比例 = 原图坐标)
screen_cnt_org = screen_cnt.reshape(4, 2) * ratio
# 2. 执行透视变换(输入原图,避免缩放导致的细节丢失)
warped = four_point_transform(orig, screen_cnt_org)
# 3. 保存并展示校正后的发票
cv2.imwrite("fapiao_corrected.jpg", warped)
cv_show("校正后发票", warped)
校正后的发票如下:

(5)步骤 5:二值化与形态学优化(为 OCR 准备)
校正后的图像需进一步处理为 "白底黑字",减少噪声干扰,便于后续 OCR 文字识别:
python
# 1. 灰度化校正后的图像
warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
# 2. 二值化(转为黑白图像,THRESH_BINARY_INV表示黑底白字→白底黑字)
ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
# 3. 形态学闭运算(先膨胀再腐蚀,填充文字内部的小孔)
kernel = np.ones((2, 2), np.uint8) # 2x2结构元素
ref_processed = cv2.morphologyEx(ref, cv2.MORPH_CLOSE, kernel)
# 4. 缩放并展示最终结果(宽度固定为800,便于查看)
final_result = resize_image(ref_processed, width=800)
cv_show("最终白底黑字效果", final_result)
# 保存最终结果
cv2.imwrite("fapiao_final.jpg", final_result)
cv2.destroyAllWindows() # 关闭所有窗口
运行结果如下:
