3. 对极几何与基础矩阵:对极约束、本质矩阵E、基础矩阵F、八点法求解
各位同学,今天我们来啃一块硬骨头——对极几何。说实话,我刚入行做SLAM那会儿,被这些矩阵绕得晕头转向。但后来我发现,只要你理解了“对极约束”这个核心思想,剩下的就是套公式、解方程的事了。
咱们先想一个问题:单目相机拍了两张图,怎么知道两张图里同一个点在哪?你可能会说“特征匹配啊”。没错,但匹配完了呢?我们还需要知道这个点在三维空间中的位置。对极几何就是干这个的——它告诉我们两个相机之间的几何关系。
核心思想: 对极几何描述的是两个相机视图之间的投影几何关系。它不依赖场景结构,只依赖相机内参和两帧之间的相对运动。
3.1 对极约束——两张图之间的“锁链”
想象一下,你左眼看一个点,右眼看同一个点。这个点在左眼视网膜上的位置,和右眼视网膜上的位置之间,存在一个固定的几何关系。这个关系,就是对极约束。
数学上怎么表达?假设空间点P,在左相机坐标系下的坐标为 X,在右相机坐标系下的坐标为 X'。它们之间满足:
X' = R * X + t
其中R是旋转矩阵,t是平移向量。这个公式很直观吧?就是把左边的点旋转一下,再平移一下,就到了右边。
但我们要的是像素坐标。设左图的像素坐标为 x,右图为 x'。那么对极约束可以写成:
x'^T * E * x = 0
这个E就是本质矩阵。为什么等于0?因为两个向量和基线共面,混合积为0。嗯,这里不展开推导了,你记住这个形式就行。
我的经验: 在实际项目中,我经常用对极约束来剔除误匹配。如果一对匹配点不满足 x'^T * E * x ≈ 0,那基本就是错误匹配,直接扔掉。这比RANSAC还快。
3.2 本质矩阵E——只跟运动有关
本质矩阵E = t^ × R,其中t^是t的反对称矩阵。它只包含旋转和平移信息,跟相机内参无关。所以,如果你用的是归一化坐标(已经去掉了内参),那E就是你要的东西。
E有几个重要性质:
- 秩为2(因为t^的秩是2)
- 有5个自由度(3旋转 + 3平移,但尺度模糊去掉1个)
- 奇异值有两个相等,第三个为0
我记得有一次做视觉里程计,用E分解出来的R和t总是跳变。后来发现是忘了对E做奇异值约束——把奇异值强制设为(1,1,0)。这个小坑,我踩过。
3.3 基础矩阵F——带上内参一起玩
本质矩阵E只适用于归一化坐标。但实际中我们拿到的是像素坐标,怎么办?把内参加进去就行了。
基础矩阵F = K'^{-T} * E * K^{-1}
其中K和K'分别是左右相机的内参矩阵。如果两个相机相同(比如单目相机的连续两帧),那K = K'。
F的性质和E类似:
- 秩为2
- 有7个自由度(因为尺度模糊,且秩约束去掉1个)
- 满足 x'^T * F * x = 0
注意: 基础矩阵F对噪声非常敏感。我曾经用手机拍了一段视频,相邻帧之间运动太小,结果F矩阵几乎退化。后来我强制要求两帧之间的平移量大于某个阈值,才稳定下来。
3.4 八点法求解——最经典的线性方法
好了,理论说完了,怎么算?最经典的就是八点法(Eight-Point Algorithm)。
思路很简单:把 x'^T * F * x = 0 展开成线性方程。每个匹配点提供一个方程,8个点就能解出F的9个元素(去掉尺度模糊)。
具体步骤:
- 归一化坐标: 把像素坐标变换到[-1, 1]区间。这一步非常重要,不归一化的话数值稳定性很差。
- 构造矩阵A: 每个匹配点构造一行 [x'x, x'y, x', y'x, y'y, y', x, y, 1]
- SVD分解: 对A做SVD,取最小奇异值对应的右奇异向量,就是F的9个元素
- 秩约束: 对F再做一次SVD,把最小奇异值设为0,保证秩为2
- 反归一化: 把F变换回原始像素坐标空间
代码实现大概长这样:
def eight_point_algorithm(pts1, pts2):
# 1. 归一化
T1 = normalize_points(pts1)
T2 = normalize_points(pts2)
pts1_norm = (T1 @ pts1.T).T
pts2_norm = (T2 @ pts2.T).T
# 2. 构造A矩阵
A = []
for p1, p2 in zip(pts1_norm, pts2_norm):
x, y = p1[0], p1[1]
xp, yp = p2[0], p2[1]
A.append([xp*x, xp*y, xp, yp*x, yp*y, yp, x, y, 1])
A = np.array(A)
# 3. SVD求解
U, S, Vt = np.linalg.svd(A)
F = Vt[-1].reshape(3, 3)
# 4. 秩2约束
U, S, Vt = np.linalg.svd(F)
S[2] = 0
F = U @ np.diag(S) @ Vt
# 5. 反归一化
F = T2.T @ F @ T1
return F / F[2, 2]
避坑指南: 我曾经直接用原始像素坐标跑八点法,结果F矩阵全是NaN。后来才意识到,像素坐标动辄几百上千,直接构造A矩阵会导致数值溢出。归一化是必须的,别偷懒。
3.5 从F/E分解R和t
有了F或E,怎么得到R和t?对E做SVD分解:
U, S, Vt = np.linalg.svd(E)
W = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
# 两种可能的旋转
R1 = U @ W @ Vt
R2 = U @ W.T @ Vt
# 平移向量
t = U[:, 2]
这里会有4种可能的组合(R1/t, R1/-t, R2/t, R2/-t)。怎么选?用三角化出来的点深度来判断——所有点必须在相机前方(深度为正)。
关键点: 单目视觉存在尺度模糊。从E分解出来的t只有方向,没有大小。这就是为什么单目SLAM需要额外处理尺度问题——我们会在后面的章节详细讲。
3.6 本章知识体系
下面这张图总结了对极几何的核心逻辑:
说白了,整个流程就是:输入匹配点 → 构造约束方程 → 求解F/E → 分解出R和t。每一步都有坑,但每一步也都有成熟的解法。
好了,这一章的内容就到这。对极几何是单目SLAM的基石,后面的三角化、PnP、BA优化都建立在这个基础上。把F和E吃透,后面的路就好走了。