4、对极几何与基础矩阵:对极几何约束、本质矩阵与基础矩阵、八点法求解基础矩阵、极线校正、三角化原理、线性三角化方法
4.1 对极几何约束——两张照片之间的“隐藏连线”
我们先从最直观的问题入手:给你两张不同角度拍摄的照片,你怎么知道同一个空间点在这两张图上的对应关系?
对极几何就是干这个的。它描述的是两个相机视图之间的内在几何关系。说白了,就是当你看到左图上有一个点,它在右图上可能出现在哪条线上——而不是漫无目的地全图搜索。
我刚开始做双目重建时,总觉得对极几何是个理论玩具。直到有一次,我在处理一个室外大场景的稀疏重建,特征点匹配错得一塌糊涂。后来用对极几何约束一筛,错误匹配直接砍掉了一半以上。嗯,从那以后我再也不敢小看它了。
核心概念:
- 基线:两个相机光心的连线
- 对极点:基线与成像平面的交点
- 对极平面:空间点与两个光心确定的平面
- 对极线:对极平面与成像平面的交线
关键约束就一句话:左图上的点,在右图上的匹配点一定位于对应的对极线上。这就是对极几何约束,也叫对极线约束。
你想想看,这个约束把二维搜索降到了一维搜索。原来你要在整张图上找匹配,现在只需要沿着一条线找。效率提升是数量级的。
4.2 本质矩阵与基础矩阵——数学上的“对极约束”
对极几何约束用数学怎么表达?这就引出了两个核心矩阵:本质矩阵 E 和基础矩阵 F。
本质矩阵 E:它编码了两个相机之间的相对旋转和平移。注意,它工作在归一化图像坐标下,也就是说它假设你已经知道了相机的内参。
基础矩阵 F:它更通用,直接工作在像素坐标下。它包含了相机的内参信息,所以不需要事先知道内参。
两者的关系很简单:F = K-T E K-1,其中 K 是相机内参矩阵。
我的经验:在实际项目中,如果你已经标定了相机,直接用本质矩阵 E 会更稳定。如果相机内参未知或者不准确,那就老老实实用基础矩阵 F。我曾经在一个项目中偷懒用了未标定的相机,结果用 E 算出来的结果一塌糊涂,换成 F 就好多了。
对极约束的数学形式:对于一对匹配点 x(左图)和 x'(右图),有 x'T F x = 0。这个方程就是整个对极几何的数学核心。
4.3 八点法求解基础矩阵——经典算法
怎么从一堆匹配点中算出 F 矩阵?最经典的方法就是八点法。
为什么是八个点?因为 F 矩阵有 9 个元素,但尺度是自由的(乘以任意非零常数不影响约束),所以实际上有 8 个自由度。理论上 8 对匹配点就够了。
算法步骤很简单:
- 对每一对匹配点 (x, y) 和 (x', y'),构造一个方程
- 把所有方程堆起来,得到一个 8×9 的矩阵(或者更多点就是 n×9)
- 用 SVD 分解求解,最小特征值对应的特征向量就是 F 矩阵的元素
- 强制 F 矩阵的秩为 2(因为本质矩阵的秩就是 2)
# 八点法的核心代码(Python 伪代码)
def compute_fundamental_matrix(pts1, pts2):
# pts1, pts2: N×2 的匹配点坐标
N = pts1.shape[0]
A = np.zeros((N, 9))
for i in range(N):
x, y = pts1[i]
xp, yp = pts2[i]
A[i] = [x*xp, x*yp, x, y*xp, y*yp, y, xp, yp, 1]
# SVD 分解
U, S, Vt = np.linalg.svd(A)
F = Vt[-1].reshape(3, 3)
# 强制秩为 2
U, S, Vt = np.linalg.svd(F)
S[2] = 0
F = U @ np.diag(S) @ Vt
return F
注意:八点法对数据尺度非常敏感。我建议在求解前先对坐标做归一化处理——把坐标平移到原点,再缩放到均值为 sqrt(2)。这一步不做,结果可能差一个数量级。我曾经踩过这个坑,后来养成了归一化的习惯。
4.4 极线校正——让匹配变得简单
有了 F 矩阵,我们可以做一件很酷的事:极线校正。
极线校正的目的是:通过图像变换,让两幅图像的对极线变成水平的。这样一来,左图上的点,在右图上的匹配点就在同一行上。搜索从二维变成了一维,而且就是简单的行搜索。
校正的过程本质上是对两幅图像做单应性变换。算法有很多种,最常用的是 Bouguet 算法:
- 把两个相机的光心旋转到平行
- 再旋转使得对极线水平
- 最后调整尺度使得视差范围合理
校正后的图像,你可以直接做行匹配。这在立体视觉中几乎是标配操作。
我的习惯:在做极线校正时,我会先检查校正后的图像质量。如果校正后图像变形太大,说明原始图像质量或者匹配点有问题。这时候不要急着往下走,先回头检查数据。
4.5 三角化原理——从二维到三维
好了,现在我们有了匹配点,也知道相机位姿(从 E 或 F 可以分解出来)。怎么得到空间点的三维坐标?这就是三角化要解决的问题。
三角化的原理其实很简单:从两个相机光心分别发出射线,经过图像上的匹配点,这两条射线在空间中的交点就是空间点的位置。
但现实中没有完美的交点——因为噪声的存在,两条射线往往不会精确相交。所以我们需要找一个“最接近”的点,让它到两条射线的距离之和最小。
这就是三角化的核心:在噪声下寻找最优的三维点。
4.6 线性三角化方法——简单粗暴但有效
最直接的三角化方法是线性三角化,也叫 DLT 方法。
思路是这样的:对于空间点 X,它在两个相机上的投影方程可以写成:
- x = P X(左图投影)
- x' = P' X(右图投影)
其中 P 和 P' 是 3×4 的投影矩阵。每个方程给出两个独立约束(因为齐次坐标有尺度),总共四个方程,三个未知数。超定方程组,用最小二乘解。
# 线性三角化核心代码
def linear_triangulation(P1, P2, pt1, pt2):
# P1, P2: 3×4 投影矩阵
# pt1, pt2: 2D 齐次坐标
A = np.zeros((4, 4))
A[0] = pt1[0] * P1[2] - P1[0]
A[1] = pt1[1] * P1[2] - P1[1]
A[2] = pt2[0] * P2[2] - P2[0]
A[3] = pt2[1] * P2[2] - P2[1]
# SVD 求解
_, _, Vt = np.linalg.svd(A)
X = Vt[-1]
X = X / X[3] # 齐次转欧氏
return X[:3]
线性三角化的优缺点:
- 优点:简单、快速、不需要迭代
- 缺点:对噪声敏感,尤其是当相机接近平行时(视差小的情况)
我个人的经验是:线性三角化适合作为初始解。如果后续要做优化(比如 Bundle Adjustment),用它来初始化就够了。但如果你的匹配点噪声很大,或者相机位姿不太准,线性三角化的结果可能惨不忍睹。这时候我会改用迭代方法,比如基于重投影误差的非线性优化。
知识体系总览
下面这张图展示了本章的核心逻辑链条:
整个流程环环相扣:匹配点 → 八点法求 F → 极线校正 → 分解位姿 → 三角化得到三维点。每一步都依赖上一步的结果,所以每一步都要做扎实。
好了,对极几何和基础矩阵的内容就讲到这里。这些是稀疏重建的基石,理解了它们,后面的 Bundle Adjustment 和稠密重建才能顺利推进。