文章目录
在计算机视觉领域,我们经常会遇到拍摄角度不佳导致图像变形的问题,比如倾斜的发票、扭曲的文档等。这时候,图像透视变换就成了“救星”。它能将倾斜、变形的图像矫正为正视角,为后续的文字识别、信息提取等操作扫清障碍。今天,我们就从原理入手,结合实战案例,带大家全面掌握图像透视变换。
一、透视变换是什么?
透视变换(Perspective Transformation),也叫投影变换,是一种将图像从一个二维坐标系映射到另一个三维坐标系的非线性变换。简单来说,它能模拟人眼视角的变化,把倾斜拍摄的“斜视图”转换成正面拍摄的“正视图”。
比如我们拍摄一张放在桌面上的发票,由于拍摄角度不是正上方,得到的发票图像可能是梯形或不规则四边形;而通过透视变换,就能将其矫正为标准的矩形,让发票上的文字、数字恢复正常的比例和角度。
二、透视变换的核心原理
1. 关键概念:透视变换矩阵
透视变换的实现依赖于透视变换矩阵(3×3矩阵) ,通过这个矩阵,可以将图像中任意一个像素点的坐标 ( x , y ) (x,y) (x,y)映射到新的坐标 ( x ′ , y ′ ) (x',y') (x′,y′)。其数学表达式如下:
[ x ′ y ′ w ′ ] = [ a 00 a 01 a 02 a 10 a 11 a 12 a 20 a 21 a 22 ] [ x y 1 ] \begin{bmatrix} x' \\ y' \\ w' \end{bmatrix}= \begin{bmatrix} a_{00} & a_{01} & a_{02} \\ a_{10} & a_{11} & a_{12} \\ a_{20} & a_{21} & a_{22} \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
x′y′w′
=
a00a10a20a01a11a21a02a12a22
xy1
其中, w ′ w' w′是齐次坐标的缩放因子,最终的像素坐标需要通过 x ′ = x ′ / w ′ x'=x'/w' x′=x′/w′、 y ′ = y ′ / w ′ y'=y'/w' y′=y′/w′计算得到。
这个3×3矩阵包含了9个参数,但由于齐次坐标的特性,实际只需要8个独立参数就能确定变换关系——而这8个参数,恰好可以通过4对对应点(变换前图像的4个顶点和变换后图像的4个顶点)来求解。
2. 核心条件:4对对应点
透视变换的前提是找到变换前图像的4个顶点(通常是目标物体的边界角点) 和变换后图像的4个顶点(通常是标准的矩形顶点) 。这4对对应点必须满足:
- 变换前的4个点不共线(比如发票的4个角,不能在同一条直线上);
- 变换后的4个点通常构成标准矩形(方便后续处理,如文字识别)。
三、OpenCV实现透视变换的关键步骤
在OpenCV中,实现透视变换主要依赖两个核心函数,整个流程可分为4步:
步骤1:读取并预处理图像
首先读取原始图像,根据需求进行缩放(降低图像尺寸可提高后续操作的速度)、灰度化、边缘检测等预处理,为后续寻找目标物体的4个顶点做准备。
步骤2:寻找目标物体的4个顶点
通过轮廓检测找到目标物体(如发票)的轮廓,再通过轮廓近似,提取出目标物体的4个顶点。这一步是透视变换的关键——如果顶点找错,后续的矫正效果会大打折扣。
步骤3:计算透视变换矩阵
使用cv2.getPerspectiveTransform(src, dst)
函数计算变换矩阵。其中:
src
:变换前的4个顶点坐标(需按“左上、右上、右下、左下”的顺序排列);dst
:变换后的4个顶点坐标(通常是标准矩形的顶点,如[[0,0], [width,0], [width,height], [0,height]]
)。
步骤4:执行透视变换
使用cv2.warpPerspective(src, M, dsize)
函数完成透视变换。其中:
src
:原始图像;M
:步骤3计算得到的透视变换矩阵;dsize
:变换后输出图像的尺寸(通常由dst
的顶点计算得到)。
四、实战:用透视变换矫正发票图像
接下来,我们以“发票矫正”为例,结合代码详细讲解透视变换的实现过程。
1. 准备工作
- 环境:Python 3.x + OpenCV(版本3.4.18.65,安装命令:
pip install opencv-python==3.4.18.65
); - 素材:一张倾斜的发票图像(
fapiao.jpg
,图像内容包含发票的文字、数字和边界)。
2. 完整代码解析
import numpy as np
import cv2
# 1. 辅助函数定义
def cv_show(name, img):
"""显示图像,按任意键关闭窗口"""
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyWindow(name)
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
def four_point_transform(image, pts):
"""执行透视变换,返回矫正后的图像"""
# 获取排序后的4个顶点
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))
# 定义矫正后图像的4个顶点(标准矩形)
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, height=None, inter=cv2.INTER_AREA):
"""按高度缩放图像,保持宽高比"""
(h, w) = image.shape[:2]
if height is None:
return image
# 计算缩放比例
r = height / float(h)
dim = (int(w * r), height)
# 执行缩放
resized = cv2.resize(image, dim, interpolation=inter)
return resized
# 2. 读取并预处理图像
# 读取原始发票图像
image = cv2.imread('fapiao.jpg')
cv_show('原始图像', image)
# 按高度缩放为500像素(降低尺寸,提高处理速度)
ratio = image.shape[0] / 500 # 记录缩放比例(后续恢复原始尺寸用)
orig = image.copy() # 保存原始图像
image = resize(orig, height=500)
cv_show('缩放后图像', image)
# 3. 寻找发票的4个顶点
# 灰度化
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化(OTSU自动阈值,突出边缘)
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('二值化图像', edged)
# 轮廓检测(寻找图像中的所有轮廓)
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 绘制所有轮廓(红色,线宽1)
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show('所有轮廓', image_contours)
# 筛选出面积最大的轮廓(发票的轮廓)
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# 轮廓近似(将不规则轮廓近似为多边形,epsilon=0.05*周长,控制近似精度)
peri = cv2.arcLength(screenCnt, True) # 计算轮廓周长(True表示闭合轮廓)
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, True)
# 绘制发票的轮廓(红色,线宽2)
image_invoice_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 0, 255), 2)
cv_show('发票轮廓', image_invoice_contour)
# 4. 执行透视变换
# 恢复原始尺寸的顶点坐标(之前缩放了图像,需乘以缩放比例)
pts = screenCnt.reshape(4, 2) * ratio
# 执行透视变换
warped = four_point_transform(orig, pts)
# 5. 后处理(二值化,方便后续文字识别)
warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
# 二值化(白底黑字)
ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 按宽度缩放为900像素,方便查看
ref = resize(ref, width=900)
cv_show('矫正后的发票', ref)
# 6. 保存结果
cv2.imwrite('corrected_invoice.jpg', ref)
print("矫正完成,结果已保存为 corrected_invoice.jpg")
3. 效果对比
- 原始图像:发票倾斜,文字和数字有透视变形;
- 矫正后图像:发票变为标准矩形,文字、数字恢复正常比例,可直接用于 OCR 文字识别(如提取发票金额、编号等信息)。
五、常见问题与解决方案
1. 找不到目标物体的 4 个顶点?
- 原因:图像噪声过多、边缘检测不完整、轮廓筛选错误;
- 解决方案:
- 增加图像预处理步骤,如使用高斯滤波(
cv2.GaussianBlur
)去除噪声; - 调整二值化阈值(可手动设置阈值,而非依赖 OTSU 自动阈值);
- 优化轮廓近似的精度(调整
epsilon
参数,如0.02*peri
或0.08*peri
)。
- 增加图像预处理步骤,如使用高斯滤波(
2. 矫正后的图像有拉伸或变形?
- 原因:4 个顶点的顺序错误、
dst
(变换后顶点)的尺寸计算不准确; - 解决方案:
- 确保
order_points
函数正确排序顶点(按 “左上、右上、右下、左下”); - 重新计算
maxWidth
和maxHeight
,确保取到正确的宽度和高度最大值。
- 确保
3. 透视变换后图像边缘有黑边?
- 原因:变换矩阵计算时,部分像素超出了输出图像的范围;
- 解决方案:
- 在
cv2.warpPerspective
中添加borderMode=cv2.BORDER_CONSTANT
和borderValue=(255,255,255)
(白色填充黑边); - 示例:
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight), borderMode=cv2.BORDER_CONSTANT, borderValue=(255,255,255))
。
- 在
六、透视变换的应用场景
除了发票矫正,透视变换在计算机视觉中还有很多实用场景:
- 文档扫描:将倾斜的纸质文档矫正为正视图,提升扫描质量;
- 车牌识别:矫正倾斜拍摄的车牌,提高识别准确率;
- 图像拼接:在全景图拼接中,通过透视变换统一多幅图像的视角;
eight), borderMode=cv2.BORDER_CONSTANT, borderValue=(255,255,255))`。
六、透视变换的应用场景
除了发票矫正,透视变换在计算机视觉中还有很多实用场景:
- 文档扫描:将倾斜的纸质文档矫正为正视图,提升扫描质量;
- 车牌识别:矫正倾斜拍摄的车牌,提高识别准确率;
- 图像拼接:在全景图拼接中,通过透视变换统一多幅图像的视角;
- AR 增强现实:将虚拟物体映射到真实场景的指定平面(如将虚拟海报贴在真实墙壁上)。