本节摘要:几何变换改变像素的位置而非颜色,解决"摆正、对齐、矫正"三类问题。本节从平移的最小例子出发,讲透 warpAffine 与 2×3 变换矩阵、rotate 与 flip 的快捷方式、getAffineTransform 与 getPerspectiveTransform 的三对点/四对点标定法,以及仿射与透视的分界线:平行线是否保持平行。读完你能校正倾斜的文档照、把任意四边形"拉平"成正视图。
阅读完本节,你应当能够:
平移。把图像向右移 50 像素、向下移 30 像素:
import cv2 import numpy as np img = cv2.imread('example.jpg') h, w = img.shape[:2] M = np.float32([[1, 0, 50], [0, 1, 30]]) # 2x3 平移矩阵 shifted = cv2.warpAffine(img, M, (w, h))
矩阵怎么读?[1, 0, 50] 的意思是:输出 x = 1×x + 0×y + 50——每个像素的横坐标加 50。第二行同理处理 y。变换矩阵就是一张"输出坐标从输入坐标怎么算"的公式表。
旋转。绕画面中心转 45 度,不缩放:
center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, 45, 1.0) # 中心、角度、缩放 rotated = cv2.warpAffine(img, M, (w, h))
透视矫正。拍歪的屏幕或文档,找到四个角点,把它"拉平":
src_pts = np.float32([[58, 92], [412, 60], [405, 350], [62, 388]]) # 原图四角 dst_pts = np.float32([[0, 0], [360, 0], [360, 420], [0, 420]]) # 目标矩形 M = cv2.getPerspectiveTransform(src_pts, dst_pts) warped = cv2.warpPerspective(img, M, (360, 420))
三段代码,一个共同结构:构造变换矩阵 → warp 系列函数执行。几何变换的全部工作就是"矩阵从哪来"。
几何变换可以按"自由度"排成一列台阶,每上一级能做的事更多、约束更少:
| 层级 | 变换 | 保持什么 | 自由度 | 典型用途 |
|---|---|---|---|---|
| 一级 | 平移 | 距离、角度、平行 | 2 | 目标居中 |
| 二级 | 欧氏(平移+旋转) | 距离、角度 | 3 | 图像摆正 |
| 三级 | 仿射 | 平行关系、直线 | 6 | 倾斜校正、剪切 |
| 四级 | 透视 | 仅直线仍是直线 | 8 | 文档/车牌矫正 |
平移加旋转再加各向异性的缩放与剪切,合起来是仿射变换,统一用 2×3 矩阵表示。它的核心性质是平行线变换后仍然平行——矩形会变成平行四边形,但不会变成梯形。透视变换用 3×3 矩阵(齐次坐标),允许平行线相交于消失点,正好对应"近大远小"的成像规律。手机斜拍一张 A4 纸,纸的四条边在照片里往往两两不平行(远端看起来更窄),这不是仿射能描述的形变,必须透视变换出马。
一个容易被忽略的实现细节:warpAffine 并不是把输入像素"推"到输出位置,而是对输出图的每个像素,用矩阵的逆变换"回查"它该来自输入的哪里。为什么?正向推会把多个输入像素挤到同一个输出格,另一些输出格却空着,出现麻点。逆向映射保证输出图的每个像素都有确定来源;来源坐标若落在非整数位置,就用插值(默认 INTER_LINEAR)从邻域估算。
这也解释了平移后"露出的黑边":输出像素回查到了输入图像之外,没有来源,只能填边界填充值。第五个参数 borderValue 控制这个填充色,默认黑色;处理白底文档时传 255 填白色,观感好得多。
getRotationMatrix2D 的第三个参数是缩放系数,设 1.0 表示不缩放。一个实用技巧:旋转 45 度时矩形的对角线会比边长更长,原图尺寸装不下旋转后的内容,四角被裁。解决方法是先算出旋转后的包围盒尺寸、把平移分量加大,或者简单粗暴地在旋转前把画布扩大一圈。做文档矫正时常用前者,做特效展示时后者即可。
大多数时候你不会手写矩阵,而是给"点对"让 OpenCV 反算。规则简单:仿射三对点,透视四对点。点从哪来?交互式用鼠标点击取坐标(用 mouse 回调写十行小程序),或用角点检测自动找(第 3 章)。标定的精度直接决定矫正质量——点选偏两三个像素,矫正结果就能看出歪斜。
warp 系列的插值参数沿用第 1 章的原则:缩小用 INTER_AREA,常规用 INTER_LINEAR,高质量放大用 INTER_CUBIC。边界填充推荐试一下 BORDER_REFLECT(反射填充,边缘内容镜像延伸),照片类处理比黑边自然;文档矫正则常用纯色填充配合 borderValue。
cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) 能做 90 度的三个方向旋转,它内部是内存转置级别的操作,比 warpAffine 快且零精度损失。相机偶发 90 度方向错误、手机竖拍横存这类问题,用它一步到位。任意角度仍走 getRotationMatrix2D。
getPerspectiveTransform 的两组点必须按同样顺序排列(比如都按左上、右上、右下、左下顺时针)。一组顺时针一组乱序,矩阵就废了,输出会像被拧麻花。团队协作时把点序写进注释,是廉价的保险。另一个坑:目标尺寸写错导致输出图比例失真,A4 纸是 210×297,矫正成 297×210 之外的比例,人眼立刻看出"被拉伸"。
⚠️ 常见坑:对索引坐标与几何坐标混淆。选点时鼠标回调给的是 (x, y),写进点对前确认单位与顺序一致;混用行列会让矫正矩阵"看起来对但就是歪"。调试时可先把点画到图上确认位置:circle 一圈再 imshow。
把一张网格图分别做旋转、仿射、透视,能直观看到三者的"形变能力"梯度。旋转后的网格仍是正交网格,只是整体转了个方向;仿射后的网格变成平行四边形网格——纵横线仍各自平行,但夹角变了;透视后的网格近处格大、远处格小,平行线在远处汇于一点。理解这个梯度的价值在于反向诊断:看到矫正不理想的结果图,先判断残留的形变属于哪一级(该用透视却只用了仿射,是文档矫正最常见的错误),再决定升级变换还是修点。
工程里还有一个高频细节:变换后图像的有效区域。平移 50 像素后,原图右侧 50 像素宽的内容会被推出画布,输出画布尺寸仍是原图尺寸时这部分直接丢失。处理"不许丢内容"的场合(印章提取、票据拼接),输出尺寸要按变换后的包围盒显式计算,再在矩阵里补偿平移量,让目标落进新画布中央。
两个方案。简单方案:旋转前把画布扩成"对角线长度"的正方形,中心对齐后再转,四角信息全保留;精细方案:用原尺寸与旋转角推算新的画布尺寸并修正矩阵里的平移分量,只补足需要的部分。文档矫正类任务用后者(避免无谓的白边),展示类任务用前者省事。
可以且推荐。仿射矩阵乘法封闭——先旋转再平移再缩放的三次 warp,等价于三个矩阵相乘后的一次 warp。每次 warp 都有插值损失,多次变换会累积模糊,合并成一次既快又保真。写法就是把矩阵按顺序相乘,最后调用一次 warpAffine。
用已知尺寸的参照物验证:A4 纸矫正后宽高比应接近 210 比 297。更通用的方法是矫正前后量目标上的同一段直线长度,比例异常说明四角点选偏或目标尺寸参数写错。测量类应用(零件尺寸检测)这个检查必做,比例错了后面全是白算。
仿射是透视的特例(3×3 矩阵最后一行固定为 0、0、1)。用 getPerspectiveTransform 标三对点会退化,用四对点标一个纯仿射形变,解出的矩阵最后一行自然回到仿射形态。工程上不必刻意转换,按"平行是否保持"选路线即可。
把本节内容组装成一个可复用的作业流程,以"手机拍的 A4 文档矫正"为例。
第一步取点。交互式取四角(鼠标回调记四组坐标)最稳;自动化路线用纸页检测(灰度化、阈值、找最大轮廓、逼近四边形顶点)适合批量处理。四点按固定顺序排好(左上、右上、右下、左下),本节 3.3 的点序铁律在此兑现。
第二步定目标尺寸。按 A4 的真实比例设定输出(比如 595 乘 842 的比例值),比例失真的话字会被拉伸,人眼对文字的拉伸极其敏感。
第三步算矩阵并矫正。getPerspectiveTransform 加 warpPerspective,插值用 INTER_CUBIC(文档要清晰、计算一次不差这点时间),边界填白色(borderValue 传 255)贴近纸面观感。
第四步质检。看四边是否笔直(残存曲率说明取点偏了)、文字行是否水平(残留旋转说明仿射成分没拉正)、对比抽查两处尺寸比例。批量化时把质检抽样比例定在十分之一左右,抽到不合格回溯该张的取点。
这套流程换到车牌、屏幕、包装盒的矫正,骨架完全不变——几何变换这节的实战价值就在于此:一次学会,处处复用。
getRotationMatrix2D 默认绕图像中心旋转且保持画布尺寸不变,旋转角度一大,四角内容就会转出画布被裁掉。工程上的做法是先算出旋转后外接矩形的宽高,再把平移分量拼进矩阵,让画布主动扩出去:先用 abs 值算出新的宽高,再在 M 的第三列加上新画布中心与原中心的偏移。矫正文档、摆正倾斜的牌照照片时,这一步几乎是必做的。
另一个高频疑问是"为什么我的旋转方向和想象相反"。OpenCV 的坐标系 y 轴向下,角度正方向是逆时针,而屏幕直觉常常相反;调不准时先拿 90 度这种已知答案的角度试一次,确认方向感再碰任意角度。同理,warpAffine 的 dsize 决定输出画布大小,与输入无关——把 dsize 设大一点再配合平移,就能实现"先腾地方再转"的宽容策略。
位置摆正了,接下来处理形状层面的毛病:小白点、断裂、空洞。下一节的形态学操作专治二值图上的这些"形状缺陷"。