我们经常需要将纸质文档转换为电子档,但拍摄的文档照片往往存在倾斜、透视畸变等问题,导致文档内容歪斜、不易识别。想要解决这个问题,就需要用到**图像透视变换。**它能够将不规则的四边形区域映射为规整的矩形,完美消除透视畸变,广泛应用于文档矫正、车牌识别、全景拼接等场景。
本文将通过一个完整案例,从图像读取、轮廓检测,到透视变换、结果后处理,逐步完成发票/文件的自动矫正。
图片准备:

透视变换的核心流程
透视变换的实现不需要手动推导复杂的矩阵公式,OpenCV 已经封装了核心函数,整个流程可以概括为 3 步:
-
从原始图像中精准提取目标区域的 4 个顶点坐标(文档的四个角点)。
-
定义变换后图像的4 个对应顶点坐标(规整矩形的四个角点)。
-
利用 OpenCV 计算透视变换矩阵,再通过该矩阵完成图像的透视映射,得到矫正后的图像。
最关键的步骤是提取准确的 4 个顶点坐标,这直接决定了透视变换的最终效果。
案例
1.导入相关库
python
import cv2
import numpy as np
2.图像显示工具函数
方便调试过程中查看每一步结果
python
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
3.图像按比例缩放工具函数
避免图像过大导致处理速度慢、轮廓检测不准确
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()函数:实现图像的等比例缩放,避免拉伸变形,核心是计算缩放比例r,再通过cv2.resize()完成缩放
4.图像读取与预处理
python
#读取原图
image = cv2.imread(r"C:\Users\LEGION\Desktop\83a7a5a67856e5690a7a34da17c4fda7.jpg")
cv_show('image', image)
# 缩小图像,便于处理
#高度缩放到500像素,宽度按比例缩放
ratio = image.shape[0] / 500
orig = image.copy()
image = resize(orig, height=500)
cv_show('1', image)
运行结果:

5.轮廓检测
python
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, (0, 0, 255), 1)
cv_show('image_contours', image_contours)
这里使用了cv2.threshold()函数的组合参数cv2.THRESH_BINARY | cv2.THRESH_OTSU,其中:
cv2.THRESH_BINARY:二值化模式,将超过阈值的像素设为 255(白色),低于阈值的像素设为 0(黑色)。
cv2.THRESH_OTSU:自动寻找最佳阈值,无需手动指定,适用于背景和目标区域对比度较为明显的图像(如文档照片),能够有效提升二值化效果。
cv2.findContours():用于寻找图像中的轮廓,返回值取[-2]是为了兼容不同版本的 OpenCV,避免因版本差异导致报错。
运行结果:

6.找到最大轮廓(逼近为四边形)
python
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) # 多边形逼近,保留主要拐点
image_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 0, 255), 2)
cv2.imshow("image_contour", image_contour)
cv2.waitKey(0)
cv2.approxPolyDP():用于多边形逼近,核心作用是简化轮廓。我们的目标是获取文档的 4 个角点,因此通过该函数将复杂的最大轮廓简化为四边形,逼近精度0.05 * peri可根据实际图像微调,过大会导致轮廓失真,过小则无法简化为四边形。
运行结果:

7.顶点坐标排序函数order_points()
python
def order_points(pts):
#一共4个坐标点
rect = np.zeros((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
和最小的为左上顶点
和最大的为右下顶点
差最小的为右上顶点
差最大的为左下顶点
8.透视变换核心函数four_point_transform()
调用order_points()函数,对输入顶点坐标进行排序。
计算变换后图像的宽度和高度(取两组对边的最大值,保证文档完整显示,不被裁剪)。
定义变换后图像的 4 个对应顶点坐标(规整矩形的四个角点)。
利用cv2.getPerspectiveTransform()计算透视变换矩阵M。
利用cv2.warpPerspective()执行透视变换,返回矫正后的图像。
python
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")
#图像透视变换 cv2.getPerspectiveTransform(src,dst[,solveMethod]) MP获得转换之间的关系
# src:变换前图像四边形顶点坐标
# dst:变换后图像四边形顶点坐标
# cv2.warpPerspective(src, MP, dsizel, dst[, flags[, borderNode[, borderValue]]]]) dst
#参数说明:
#src:原图
#MP:透视变换矩阵,3行3列
#dsize:输出图像的大小,二元元组(width,height)
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))#返回变换后结果
return warped
运行结果:

9.执行透视变换与结果优化
python
# 执行透视变换(将缩放后图像的顶点坐标还原到原始图像坐标体系)
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
# 对透视变换结果进行逆时针旋转90度(不覆盖原始矫正结果)
warped_rotated = cv2.rotate(warped, cv2.ROTATE_90_COUNTERCLOCKWISE)
# 保存并显示原始矫正结果(未旋转)
cv2.imwrite('invoice_new_original.jpg', warped)
cv2.namedWindow('xx_original(未旋转透视结果)', cv2.WINDOW_NORMAL)
cv2.imshow('xx_original(未旋转透视结果)', warped)
cv2.waitKey(0)
cv2.destroyWindow('xx_original(未旋转透视结果)')
# 保存并显示逆时针旋转90度后的矫正结果
cv2.imwrite('invoice_new_rotated.jpg', warped_rotated)
cv2.namedWindow('xx_rotated(逆时针旋转90度后结果)', cv2.WINDOW_NORMAL)
cv2.imshow('xx_rotated(逆时针旋转90度后结果)', warped_rotated)
cv2.waitKey(0)
# 释放所有窗口,清理内存资源
cv2.destroyAllWindows()
运行结果:

除此之外,还可以有如下的后续处理步骤:
-
二值化:提高文字对比度,便于后续OCR识别
-
腐蚀:去除孤立噪点,让边缘更干净