在计算机视觉领域,图像的几何变换是基础中的基础。无论是文档矫正、车牌识别还是全景拼接,都离不开两种核心变换:仿射变换 与透视变换 。很多初学者容易混淆二者的区别,本文将从矩阵原理 、计算过程 、自由度 、接口调用 到实战示例,全方位拆解二者的差异,帮你彻底搞懂!
一、先看效果:平行四边形 vs 梯形
在开始枯燥的数学推导前,我们先直观感受一下两种变换的效果差异:
| 变换类型 | 核心特性 | 典型效果示意图 |
|---|---|---|
| 仿射变换 | 保持平行性 | 矩形 → 平行四边形(如平移后的卡片) |
| 透视变换 | 打破平行性,模拟近大远小 | 矩形 → 梯形(如斜拍文档矫正为正视图) |
二、数学本质:有没有"透视除法"?
1. 仿射变换:线性运算,无除法
仿射变换本质是二维平面内的线性变换 (平移+旋转+缩放+错切),其核心公式为:
{x′=a⋅x+b⋅y+cy′=d⋅x+e⋅y+f\begin{cases} x' = a \cdot x + b \cdot y + c \\ y' = d \cdot x + e \cdot y + f \end{cases}{x′=a⋅x+b⋅y+cy′=d⋅x+e⋅y+f
对应的矩阵形式为(2×32 \times 32×3 矩阵):
x′y′\]=\[abcdef\]⋅\[xy1\]\\begin{bmatrix} x' \\\\ y' \\end{bmatrix} = \\begin{bmatrix} a \& b \& c \\\\ d \& e \& f \\end{bmatrix} \\cdot \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix}\[x′y′\]=\[adbecf\]⋅ xy1 **通俗理解**:想象你在桌面上推一张扑克牌------它可以平移、旋转、拉伸,但始终是"平的",原来平行的边(如上下边)变换后依然平行。 #### 2. 透视变换:非线性运算,必须除法 透视变换模拟**三维空间的视角变换** (近大远小),其核心公式为: {x′=a⋅x+b⋅y+cg⋅x+h⋅y+1y′=d⋅x+e⋅y+fg⋅x+h⋅y+1\\begin{cases} x' = \\frac{a \\cdot x + b \\cdot y + c}{g \\cdot x + h \\cdot y + 1} \\\\ y' = \\frac{d \\cdot x + e \\cdot y + f}{g \\cdot x + h \\cdot y + 1} \\end{cases}{x′=g⋅x+h⋅y+1a⋅x+b⋅y+cy′=g⋅x+h⋅y+1d⋅x+e⋅y+f 对应的矩阵形式为(3×33 \\times 33×3 矩阵): \[uvw\]=\[abcdefgh1\]⋅\[xy1\]\\begin{bmatrix} u \\\\ v \\\\ w \\end{bmatrix} = \\begin{bmatrix} a \& b \& c \\\\ d \& e \& f \\\\ g \& h \& 1 \\end{bmatrix} \\cdot \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} uvw = adgbehcf1 ⋅ xy1 最终坐标需通过**透视除法** 得到:x′=uw, y′=vwx' = \\frac{u}{w},\\ y' = \\frac{v}{w}x′=wu, y′=wv。 **通俗理解**:想象你把扑克牌拿起来对着眼睛看------离你近的边显得宽,远的边显得窄,原来平行的边(如上下边)会在远处相交于一点(消失点)。 ### 三、自由度与未知数:为什么仿射要3点,透视要4点? 很多人会问:"自由度是不是就是未知数的个数?"------**不完全是**!自由度是描述变换复杂度的指标,而未知数可能因"冗余约束"(如尺度不变性)多于自由度。 #### 1. 仿射变换:6个自由度,需3个点 仿射变换由\*\*平移(2自由度)+旋转(1自由度)+缩放(2自由度)+错切(1自由度)\*\*组成,共 **6个自由度** 。 每个对应点 (x,y)→(x′,y′)(x,y) \\to (x',y')(x,y)→(x′,y′) 提供2个方程(x方向和y方向),因此需要 6÷2=36 \\div 2 = 36÷2=3 个点才能解出全部参数。 #### 2. 透视变换:8个自由度,需4个点 透视变换的 3×33 \\times 33×3 矩阵本应有9个未知数,但因**尺度不变性** (矩阵乘以任意非零常数效果相同),可固定最后一个参数为1,剩余 **8个自由度** 。 同理,每个点提供2个方程,因此需要 8÷2=48 \\div 2 = 48÷2=4 个点。 ### 四、OpenCV实战:接口调用与代码示例 搞清楚原理后,我们用OpenCV实际跑一遍两种变换,看看代码层面的差异。 #### 1. 仿射变换:`cv2.warpAffine` ##### 接口原型: ```python dst = cv2.warpAffine( src, # 输入图像 M, # 2×3仿射矩阵 dsize, # 输出图像尺寸 (width, height) flags=cv2.INTER_LINEAR, # 插值方式 borderMode=cv2.BORDER_CONSTANT, # 边界填充模式 borderValue=(0,0,0) # 边界填充颜色 ) ``` ##### 如何获取矩阵 `M`? * **旋转/平移** :直接用 `cv2.getRotationMatrix2D` ```python # 绕中心点(200,200)旋转45°,缩放1.0倍 M = cv2.getRotationMatrix2D(center=(200,200), angle=45, scale=1.0) ``` * **三点映射** :用 `cv2.getAffineTransform`(需3组对应点) ```python src_pts = np.float32([[0,0], [100,0], [0,100]]) # 原图3点 dst_pts = np.float32([[10,20], [110,30], [5,120]]) # 目标3点 M = cv2.getAffineTransform(src_pts, dst_pts) ``` ##### 完整示例:平移+旋转 ```python import cv2 import numpy as np img = cv2.imread("card.jpg") rows, cols = img.shape[:2] # 1. 定义仿射矩阵:向右平移100,向下平移50 M = np.float32([[1, 0, 100], [0, 1, 50]]) # 2. 执行变换 dst = cv2.warpAffine(img, M, (cols, rows)) # dsize保持原图尺寸 cv2.imshow("Affine Result", dst) cv2.waitKey(0) ``` #### 2. 透视变换:`cv2.warpPerspective` ##### 接口原型: ```python dst = cv2.warpPerspective( src, # 输入图像 M, # 3×3透视矩阵(单应性矩阵) dsize, # 输出图像尺寸 (width, height) flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(0,0,0) ) ``` ##### 如何获取矩阵 `M`? 必须用 `cv2.getPerspectiveTransform`,且需**4组对应点**: ```python # 原图4个角点(斜拍的文档) src_pts = np.float32([[56,65], [368,52], [28,387], [389,390]]) # 目标4个角点(正视图的文档,宽300,高300) dst_pts = np.float32([[0,0], [300,0], [0,300], [300,300]]) M = cv2.getPerspectiveTransform(src_pts, dst_pts) ``` ##### 完整示例:斜拍文档矫正 ```python import cv2 import numpy as np img = cv2.imread("document_skewed.jpg") rows, cols = img.shape[:2] # 1. 定义4组对应点(手动标注或用特征点检测) src_pts = np.float32([[56,65], [368,52], [28,387], [389,390]]) dst_pts = np.float32([[0,0], [300,0], [0,300], [300,300]]) # 2. 计算透视矩阵 M = cv2.getPerspectiveTransform(src_pts, dst_pts) # 3. 执行变换(输出300×300的正视图) dst = cv2.warpPerspective(img, M, (300, 300)) cv2.imshow("Perspective Result", dst) cv2.waitKey(0) ``` ### 五、核心区别总结表 | 对比维度 | 仿射变换 | 透视变换 | |--------------|------------------|-----------------------| | **数学本质** | 线性变换(无除法) | 非线性变换(需透视除法) | | **矩阵形状** | 2×3 | 3×3 | | **核心特性** | 保持平行性 | 打破平行性,模拟近大远小 | | **自由度** | 6个 | 8个 | | **所需对应点** | 3个 | 4个 | | **典型应用** | 平移、旋转、缩放、错切 | 文档矫正、鸟瞰图、车牌校正 | | **OpenCV接口** | `cv2.warpAffine` | `cv2.warpPerspective` | ### 六、常见问题解答(FAQ) #### Q1:为什么透视变换的矩阵是3×3,但OpenCV里有时看到2×4? A:3×3是数学上的完整矩阵,矩阵最右下角的值默认为1,2×4是是OpenCV为了计算方便而设计的"计算中间体",是计算时中间步骤的"紧凑存储格式"(将8个自由度展开为2行4列),实际使用时仍需reshape为3×3。 #### Q2:仿射变换能实现"近大远小"吗? A:不能。仿射变换是平面内的线性变换,无法模拟三维视角的深度变化,只能产生平行四边形变形。 #### Q3:如何判断该用哪种变换? A:若变换后需要**保持平行边** (如旋转后的图片),用仿射;若需要**矫正视角**(如斜拍的文档变正),用透视。 ### 七、总结 仿射变换与透视变换的本质区别在于**是否引入"透视除法"**: * 仿射是"平面游戏",只有乘法和加法,适合简单的平移旋转; * 透视是"立体游戏",多了除法步骤,能模拟真实世界的近大远小。 掌握二者的区别,不仅能帮你写出正确的代码,更能让你在实际项目中灵活选择变换方式------毕竟,选对工具才能事半功倍! 如果觉得本文有帮助,欢迎点赞收藏,有问题欢迎在评论区留言讨论~ 🚀 **原创不易,转载请注明出处!**