在日常的图像处理场景中,我们经常需要对倾斜、变形的票据 / 文档图像进行矫正,使其变成规整的正射视角,方便后续的文字识别、信息提取等操作。本文将基于 Python+OpenCV 实现完整的票据图像矫正流程,核心包含轮廓检测、四点透视变换、形态学处理等关键技术。
一、核心原理介绍
1.1 整体流程
整个票据矫正流程可以总结为以下步骤:
- 图像预处理(缩放、灰度化、二值化)
- 轮廓检测与筛选(找到票据的四个角点)
- 四点透视变换(将不规则四边形转换为矩形)
- 后处理(二值化、形态学操作、旋转)
1.2 关键技术点
- 轮廓检测:通过寻找图像中面积最大的轮廓,定位票据的物理边界
- 四点排序:对检测到的四个角点进行有序排列(左上、右上、右下、左下)
- 透视变换 :利用
cv2.getPerspectiveTransform和cv2.warpPerspective实现图像的视角矫正
python
import numpy as np
import cv2
def order_points(pts):
"""
对检测到的4个角点进行排序
:param pts: 原始4个角点坐标,形状为(4,2)
:return: 排序后的坐标,顺序为[左上, 右上, 右下, 左下]
"""
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
def four_point_transform(image, pts):
"""
四点透视变换,将不规则四边形转换为矩形
:param image: 原始图像
:param pts: 四边形的4个角点
:return: 矫正后的图像
"""
# 获取排序后的坐标
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):
"""
保持比例的图像缩放
:param image: 输入图像
:param width: 目标宽度
:param height: 目标高度
:param inter: 插值方法
:return: 缩放后的图像
"""
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__":
# 1. 读取并显示原始图像
image = cv2.imread('fapiao.jpg')
cv2.imshow('原始图像', image)
# 2. 图像缩放(避免原图过大影响处理速度)
ratio = image.shape[0] / 500.0 # 计算缩放比例
orig = image.copy()
image = resize(orig, height=500)
cv2.imshow('缩放后图像', image)
# 3. 轮廓检测
print("STEP 1: 轮廓检测")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度化
# 自动阈值二值化(OTSU算法)
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, (0, 0, 255), 1)
cv2.imshow('所有轮廓', image_contours)
# 4. 获取票据的最大轮廓
print("STEP 2: 获取最大轮廓")
# 按轮廓面积排序,取最大的轮廓
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# 轮廓近似(减少轮廓点数量)
peri = cv2.arcLength(screenCnt, True) # 计算轮廓周长
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, True) # 轮廓近似
print(f"轮廓近似后的角点数量: {screenCnt.shape}")
# 绘制票据轮廓
image_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 255, 0), 2)
cv2.imshow("票据轮廓", image_contour)
# 5. 透视变换矫正
# 还原轮廓坐标到原始图像尺寸
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
cv2.imwrite('invoice_new.jpg', warped)
cv2.namedWindow('透视变换后', cv2.WINDOW_NORMAL)
cv2.imshow("透视变换后", warped)
# 6. 后处理(二值化+形态学操作+旋转)
warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
# 二值化
ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv2.imshow('二值化结果', ref)
# 形态学闭运算(先膨胀后腐蚀),消除小的孔洞
kernel = np.ones((2, 2), np.uint8)
ref_new = cv2.morphologyEx(ref, cv2.MORPH_CLOSE, kernel)
# 缩放便于查看
ref_new = resize(ref_new.copy(), width=500)
cv2.imshow('形态学处理后', ref_new)
# 旋转90度(根据实际需求调整)
rotated_image = cv2.rotate(ref_new, cv2.ROTATE_90_COUNTERCLOCKWISE)
cv2.imshow("最终矫正结果", rotated_image)
# 等待按键退出
cv2.waitKey(0)
cv2.destroyAllWindows()

三、代码详细解析
3.1 核心函数说明
1. order_points(pts)
该函数的核心作用是对检测到的 4 个角点进行有序排列,确保顺序为左上、右上、右下、左下:
- 利用
x+y的和排序:和最小的是左上,和最大的是右下 - 利用
y-x的差排序:差最小的是右上,差最大的是左下
2. four_point_transform(image, pts)
实现透视变换的核心函数:
- 计算变换后图像的宽度和高度(取两组对边的最大值,保证完整性)
- 定义目标坐标(规整的矩形)
- 通过
cv2.getPerspectiveTransform计算透视变换矩阵 - 通过
cv2.warpPerspective应用变换,得到矫正后的图像
3. resize(image, width=None, height=None)
保持图像比例的缩放函数,避免缩放后图像变形。
3.2 主流程解析
-
图像预处理:
- 读取图像并缩放(解决大图处理慢的问题)
- 灰度化 + OTSU 自动阈值二值化,突出轮廓
-
轮廓检测:
- 使用
cv2.findContours查找所有轮廓 - 按面积排序,筛选出票据的最大轮廓
- 用
cv2.approxPolyDP进行轮廓近似,得到 4 个角点
- 使用
-
透视变换:
- 将检测到的角点还原到原始图像尺寸
- 应用四点透视变换,得到正射视角的票据图像
-
后处理:
- 二值化增强对比度
- 形态学闭运算消除小噪声
- 旋转调整方向(根据实际需求)