§1.2.7
齐次坐标、刚体/相似/仿射/投影变换的自由度与可解最少点对?
核心概念
齐次坐标 (Homogeneous Coordinates) 是一种在 N 维空间中用 N+1 维向量表示点的方法。它通过增加一个额外的维度,将几何变换(尤其是平移)统一为线性变换(矩阵乘法),并能优雅地表示无穷远点。例如,2D 点 在齐次坐标中表示为 ,其中 ,通常取 得到 。
几何变换描述了物体或坐标系在空间中的移动、形变。从约束最强到最弱,常见的 2D 变换层级为:
- 刚体/欧几里得变换 (Rigid/Euclidean Transformation): 保持距离和角度,仅包含旋转和平移。
- 相似变换 (Similarity Transformation): 保持角度(形状),但距离按相同比例缩放。是刚体变换加上一个全局缩放。
- 仿射变换 (Affine Transformation): 保持平行性,但不保持角度和距离。可以看作是剪切、缩放、旋转、平移的组合。
- 投影变换/单应性 (Projective Transformation/Homography): 仅保持直线(共线性)。这是最一般的线性变换,能描述透视效果。
原理与推导
设 2D 点在欧几里得坐标系下为 ,其齐次坐标为 。变换后的点为 ,其齐次坐标为 。从齐次坐标到欧几里得坐标的转换是 。所有 2D 变换都可以表示为 矩阵 的乘法:。
1. 刚体变换 (Rigid/Euclidean)
- 原理:仅包含旋转和平移,保持长度和角度。
- 矩阵形式: 其中 是一个 的正交旋转矩阵 (), 是 的平移向量。
- 自由度 (DoF):3
- 旋转:1 个参数 ()
- 平移:2 个参数 ()
- 可解最少点对:2
- 推导:每个点对 提供 2 个方程。3 个自由度需要 3 个方程,理论上需要 1.5 个点对。
- 几何直观:
- 第 1 个点对 :可以用来对齐原点,消除 2 个平移自由度。
- 第 2 个点对 :此时系统只剩下旋转。点 绕原点 的向量 和点 绕原点 的向量 之间的夹角即为旋转角 。
- 因此,2 个点对足以确定一个刚体变换。
2. 相似变换 (Similarity)
- 原理:刚体变换 + 均匀缩放,保持角度(形状)。
- 矩阵形式: 其中 是缩放因子。
- 自由度 (DoF):4
- 缩放:1 个参数 ()
- 旋转:1 个参数 ()
- 平移:2 个参数 ()
- 可解最少点对:2
- 推导:4 个自由度需要 4 个方程,2 个点对正好提供 个方程。
- 几何直观:2 个点对定义了两条线段。相似变换由一条线段到另一条线段的映射唯一确定。这个映射包含了平移(将线段起点对齐)、旋转(将线段方向对齐)和缩放(将线段长度对齐)。
3. 仿射变换 (Affine)
- 原理:保持平行线,可以看作是更一般的线性变换加平移。
- 矩阵形式: 其中 是一个任意的 矩阵。
- 自由度 (DoF):6
- 线性变换矩阵 :4 个参数 ()
- 平移向量 :2 个参数 ()
- 可解最少点对:3
- 推导:6 个自由度需要 6 个方程,3 个点对提供 个方程。
- 几何直观:一个仿射变换由一个三角形到另一个三角形的映射唯一确定。3 个不共线的点构成一个三角形,这 3 个点对的映射关系足以解出 6 个参数。
4. 投影变换 (Projective / Homography)
- 原理:最一般的变换,仅保持共线性。用于描述中心投影(如相机成像)。
- 矩阵形式:
- 自由度 (DoF):8
- 推导:矩阵 有 9 个元素,但变换是齐次的,即 和 () 代表同一个变换。因为 和 在转换回欧几里得坐标后是同一点。这种尺度模糊性使得自由度减 1,即 。通常通过约束 或 来归一化。
- 可解最少点对:4
- 推导:8 个自由度需要 8 个方程,4 个点对提供 个方程。
- 几何直观:一个投影变换由一个任意四边形到另一个任意四边形的映射唯一确定,前提是两组四个点中都没有三点共线。
- 求解方法 (DLT):对于每个点对 ,我们有 。展开后可得到两个线性方程组,写成 的形式,其中 是 矩阵元素展开的 向量。4 个点对可以构建一个 的矩阵 ,通过奇异值分解 (SVD) 求解 , 对应 的最小奇异值对应的右奇异向量。
代码实现
下面的 Python 代码使用 OpenCV 来计算仿射变换和投影变换矩阵。
python
1import cv22import numpy as np34def solve_transformations():5 """6 演示如何使用 OpenCV 求解仿射变换和投影变换。7 """8 print("--- 演示 OpenCV 变换矩阵求解 ---\n")910 # 1. 仿射变换 (Affine Transformation)11 # 至少需要 3 个点对来求解 6 个自由度12 print("1. 求解仿射变换 (需要 3 个点对):")1314 # 定义源图像中的三个点 (例如,一个三角形的顶点)15 # 选择不共线的点是至关重要的16 pts_src_affine = np.array([[50, 50], [200, 50], [50, 200]], dtype=np.float32)1718 # 定义目标图像中对应的三个点19 # 这里模拟了旋转、平移和剪切20 pts_dst_affine = np.array([[100, 100], [250, 50], [150, 250]], dtype=np.float32)2122 # 使用 cv2.getAffineTransform 求解仿射变换矩阵23 # 这个函数内部实现了基于三点对的直接求解24 M_affine = cv2.getAffineTransform(pts_src_affine, pts_dst_affine)2526 print("源点 (3个):\n", pts_src_affine)27 print("目标点 (3个):\n", pts_dst_affine)28 print("计算得到的 2x3 仿射变换矩阵 M:\n", M_affine)29 # 注意:OpenCV 返回的是 2x3 矩阵,因为 [0, 0, 1] 这行是固定的30 print("其完整的 3x3 齐次矩阵形式为:\n", np.vstack([M_affine, [0, 0, 1]]))31 print("-" * 30)3233 # 2. 投影变换 (Projective Transformation / Homography)34 # 至少需要 4 个点对来求解 8 个自由度35 print("2. 求解投影变换 (需要 4 个点对):")3637 # 定义源图像中的四个点 (例如,一个矩形的顶点)38 # 选择没有三点共线的点是至关重要的39 pts_src_proj = np.array([[0, 0], [300, 0], [300, 200], [0, 200]], dtype=np.float32)4041 # 定义目标图像中对应的四个点,模拟透视效果42 pts_dst_proj = np.array([[50, 50], [280, 30], [310, 220], [20, 180]], dtype=np.float32)4344 # 使用 cv2.getPerspectiveTransform 求解投影变换矩阵45 # 这个函数内部实现了基于四点对的 DLT 算法46 M_proj = cv2.getPerspectiveTransform(pts_src_proj, pts_dst_proj)4748 print("源点 (4个):\n", pts_src_proj)49 print("目标点 (4个):\n", pts_dst_proj)50 print("计算得到的 3x3 投影变换矩阵 H:\n", M_proj)51 print("-" * 30)5253 # 3. 使用 findHomography 进行更鲁棒的求解54 # 当点对数量多于4个,且可能存在噪声或外点时,使用此方法55 print("3. 使用 findHomography 进行鲁棒求解:")56 # 假设我们有更多的点,其中一些是好的,一个是坏的(outlier)57 pts_src_robust = np.array([[0, 0], [300, 0], [300, 200], [0, 200], [150, 100]], dtype=np.float32)58 pts_dst_robust = np.array([[50, 50], [280, 30], [310, 220], [20, 180], [500, 500]], dtype=np.float32) # 最后一个是外点5960 # 使用 RANSAC 算法来抵抗外点的影响61 M_homography, mask = cv2.findHomography(pts_src_robust, pts_dst_robust, cv2.RANSAC, 5.0)6263 print("使用 RANSAC 从5个点对(含1个外点)中计算的单应矩阵 H:\n", M_homography)64 print("RANSAC 内点掩码 (1表示内点, 0表示外点):\n", mask.ravel())656667if __name__ == '__main__':68 solve_transformations()
工程实践
- 数据准备:选择的特征点应尽可能均匀地分布在整个图像/平面上,避免共线或集中在小区域。点的分布对变换矩阵的精度和稳定性至关重要。
- 鲁棒估计:在实际应用中,特征点匹配总会存在错误匹配(outliers)。直接使用最小点对求解对噪声和外点极其敏感。因此,必须使用鲁棒估计算法,如 RANSAC (随机抽样一致性) 或 LMedS (最小中值法)。
cv2.findHomography内部就封装了 RANSAC。 - 归一化:在应用 DLT 算法求解投影变换前,对点坐标进行归一化(平移到质心,并缩放到均方根距离为 )是提高数值稳定性的关键步骤。这可以有效改善矩阵 的条件数,得到更精确的解。
cv2.findHomography内部也包含了这一步骤。 - 模型选择:
- 如果场景是平面,或者相机只进行纯旋转,则场景点之间的变换是严格的投影变换(单应性)。
- 如果物体距离相机很远,透视效果不明显,可以用仿射变换近似,计算更简单,需要点更少。
- 在做文档扫描矫正时,纸张是平面,用投影变换。
- 在做目标跟踪时,如果目标小且形状变化不大,可以用相似变换甚至刚体变换来建模其运动。
- 部署与性能:这些变换矩阵的计算通常在预处理阶段完成。在推理时,应用变换(如
cv2.warpPerspective)是主要的计算开销,但对于现代 CPU/GPU 来说通常很快。变换矩阵本身(9个或6个浮点数)的存储和传输开销极小。
常见误区与边界情况
- 误区1:混淆自由度与矩阵元素个数
- 初学者常认为投影变换有 9 个自由度。必须理解齐次坐标的尺度不变性,从而将自由度减为 8。
- 误区2:用最少点对处理实际数据
- 理论上 4 个点对可解单应矩阵,但只要其中一个点有微小误差,结果就会偏差很大。工程上总是用远超于最小数量的点对,并结合 RANSAC 来保证鲁棒性。
- 边界情况:退化配置 (Degenerate Configuration)
- 当用于求解的点处于“退化”状态时,无法唯一确定变换。
- 仿射变换:若 3 个点共线,则无法确定仿射变换。因为共线的三个点无法定义一个平面上的唯一坐标系。
- 投影变换:若 4 个点中有 3 个或更多点共线,则无法唯一确定投影变换。
- 在 RANSAC 迭代中,如果随机抽样到的点集是退化的,该次迭代会失败或产生无意义的结果。
- 面试追问:
- 问:“为什么单应性(Homography)是 8 个自由度?”
- 答:一个 3x3 矩阵有 9 个元素,但由于齐次坐标的尺度等价性(即 和 是同一点),变换矩阵 和 产生相同的投影变换。为了消除这种冗余,我们可以除以一个非零元素(如 )或施加一个范数约束(如 ),从而减少一个自由度,剩下 8 个。
- 问:“给你上百个匹配点对,其中混杂着错误匹配,如何计算它们之间的单应矩阵?”
- 答:使用 RANSAC 算法。其流程是:1) 随机选择 4 个点对(求解单应矩阵的最小集);2) 用这 4 个点对计算一个单应矩阵 H;3) 用 H 变换所有源点,并计算与对应目标点的距离,统计距离小于阈值的“内点”数量;4) 重复以上步骤 N 次,选择拥有最多内点的那个 H 作为最终模型。最后,可以用所有被判定为内点的点对,重新计算一个更精确的 H。
- 问:“什么情况下仿射变换可以很好地近似投影变换?”
- 答:当场景中的物体基本在一个平面上,且该平面与相机成像平面近似平行时;或者当物体距离相机非常远,使得透视投影退化为近似正交投影时。在这种情况下,投影矩阵的最后一行
[h31, h32, h33]会非常接近[0, 0, 1],此时仿射变换是一个很好的近似。
- 答:当场景中的物体基本在一个平面上,且该平面与相机成像平面近似平行时;或者当物体距离相机非常远,使得透视投影退化为近似正交投影时。在这种情况下,投影矩阵的最后一行
- 问:“为什么单应性(Homography)是 8 个自由度?”