4. 特征匹配与对极几何:2D-2D位姿估计

各位同学,欢迎来到第四章。今天咱们聊聊视觉SLAM里一个非常经典的问题——2D-2D位姿估计

说白了,就是给你两张图像,你只知道图像里的像素坐标,不知道任何3D信息,怎么把相机的运动给算出来?

嗯,这其实是很多SLAM系统的起点。我记得刚入行那会儿,第一次跑通ORB-SLAM,看到它从两张模糊的图片里算出相机转了多少度,平移了多少米,那种感觉确实挺震撼的。

4.1 特征匹配:我们得先找到“共同点”

要估计两帧之间的运动,第一步是什么?

你得先知道两张图里哪些点是同一个东西。对吧?

这就涉及到特征匹配。我个人习惯把特征匹配分成三步走:

  1. 特征提取:在每张图里找到那些“与众不同”的点(角点、斑点等)。
  2. 特征描述:给每个特征点算一个“身份证号”(描述子),比如ORB描述子就是一个256位的二进制串。
  3. 特征匹配:根据描述子的相似度,把两帧里的点配对起来。

核心要点:匹配的质量直接决定了位姿估计的精度。如果匹配错了,后面算出来的运动基本就是错的。

我在项目中遇到过一个问题:在高速公路上,车辆快速行驶,图像模糊得厉害。ORB特征点提取出来数量少,匹配质量也差。后来我换成了光流法(LK光流)来跟踪特征点,效果反而更好。所以,不要死磕一种方法,场景变了,策略也得变。

4.1.1 常用的特征点对比

特征类型 速度 旋转不变性 尺度不变性 典型应用场景
SIFT 高精度建图、离线处理
SURF 中等 实时性要求不高的场景
ORB 中等 中等 实时SLAM(如ORB-SLAM)
AKAZE 中等 非线性尺度空间、边缘场景

我的建议:自动驾驶场景下,优先考虑ORB或AKAZE。SIFT虽然精度高,但算力消耗太大,车载芯片扛不住。

4.2 对极几何:从2D到3D的桥梁

好,假设我们已经有了匹配好的特征点对。接下来怎么算运动?

这里就要请出对极几何了。

你想想看,两个相机在不同位置拍同一个点,这个点、两个相机的光心,这三个点天然就在一个平面上。这个平面叫极平面

极平面与两个成像平面的交线,就是极线

这个约束关系,用数学表达就是:

p2^T * F * p1 = 0

其中,p1p2是匹配点的归一化坐标,F就是基础矩阵

如果我们用的是归一化坐标(即去掉了内参的影响),那公式就变成:

p2^T * E * p1 = 0

这里的E就是本质矩阵

关键点:本质矩阵E包含了相机的旋转R和平移t的信息。我们只要能解出E,就能从E里分解出R和t。

4.3 八点法:求解本质矩阵

怎么解E?最经典的方法就是八点法

为什么叫八点法?因为E是一个3x3的矩阵,有9个元素。但尺度等价性(乘以任意非零常数不影响结果),所以实际上只有8个自由度。理论上,8对匹配点就能解出E。

代码实现其实不复杂:

// 伪代码:八点法求解本质矩阵
// 输入:匹配点对 (p1, p2),每对点都是归一化坐标
// 输出:本质矩阵 E

Matrix3x3 computeEssentialMatrix(vector<Point2f>& p1, vector<Point2f>& p2) {
    // 1. 构建线性方程组 A * e = 0
    // 每对点提供一个方程
    Matrix A(8, 9); // 至少8个点
    for (int i = 0; i < 8; i++) {
        float x1 = p1[i].x, y1 = p1[i].y;
        float x2 = p2[i].x, y2 = p2[i].y;
        A(i, 0) = x2 * x1;
        A(i, 1) = x2 * y1;
        A(i, 2) = x2;
        A(i, 3) = y2 * x1;
        A(i, 4) = y2 * y1;
        A(i, 5) = y2;
        A(i, 6) = x1;
        A(i, 7) = y1;
        A(i, 8) = 1.0;
    }

    // 2. SVD分解,取最小奇异值对应的右奇异向量
    SVD svd(A);
    Vector9f e_vec = svd.matrixV().col(8);

    // 3. 重组成3x3矩阵
    Matrix3x3 E;
    E << e_vec(0), e_vec(1), e_vec(2),
         e_vec(3), e_vec(4), e_vec(5),
         e_vec(6), e_vec(7), e_vec(8);

    // 4. 强制奇异值约束:E的奇异值应为 (sigma, sigma, 0)
    SVD svd_E(E);
    Matrix3x3 U = svd_E.matrixU();
    Vector3f S = svd_E.singularValues();
    S(0) = (S(0) + S(1)) / 2.0;
    S(1) = S(0);
    S(2) = 0;
    Matrix3x3 Sigma = S.asDiagonal();
    E = U * Sigma * svd_E.matrixV().transpose();

    return E;
}

注意:八点法对噪声非常敏感。我曾经在实车测试中,因为路面颠簸导致图像抖动,匹配点对里混入了几个外点,结果解出来的E完全不对,分解出的平移向量指向了天上。

所以,实际工程中一定要配合RANSAC(随机采样一致性)来使用。先随机采样8个点,算一个E,然后统计有多少内点符合这个E。重复多次,选内点最多的那个E。

4.4 从本质矩阵分解R和t

解出E之后,怎么得到R和t?

嗯,这里有个小技巧。E的SVD分解形式是:

E = U * Sigma * V^T

然后,R和t有四种可能的组合:

t1 = U * (0, 0, 1)^T
t2 = -U * (0, 0, 1)^T
R1 = U * W * V^T
R2 = U * W^T * V^T

其中 W = [0 -1 0; 1 0 0; 0 0 1]

四种组合里,只有一种是正确的——那就是三角化之后,点在两个相机前方的那一组。

避坑指南:我曾经在分解E之后,忘了做“正深度检验”。结果算出来的R和t看起来挺合理,但实际轨迹是反的——车在往前开,我算出来它在往后倒。后来加了一个简单的深度检查,问题就解决了。

4.5 单应矩阵:当场景近似平面时

除了本质矩阵,还有一种情况很常见:场景近似在一个平面上

比如,自动驾驶中,车辆在平坦的道路上行驶,路面上的纹理基本都在一个平面上。这时候,用单应矩阵H来建模会更稳定。

单应矩阵描述的是两个平面之间的映射关系:

p2 = H * p1

求解H只需要4对匹配点(因为H有8个自由度)。

怎么判断该用E还是H?

我个人习惯的做法是:同时计算E和H,然后看哪个模型的内点更多。如果H的内点明显多于E,说明场景近似平面,用H;否则用E。

场景 推荐模型 最少匹配点数 特点
一般场景(非平面) 本质矩阵E 8对 需要去畸变、归一化坐标
近似平面场景 单应矩阵H 4对 对纯旋转场景也有效
纯旋转(无平移) 单应矩阵H 4对 本质矩阵会退化

4.6 本章知识体系

为了让大家更直观地理解整个流程,我画了一张流程图:

2D-2D位姿估计核心流程 输入:两帧图像 步骤1:特征提取与匹配 场景判断 单应矩阵H 本质矩阵E 分解H得R,t 分解E得R,t 输出:旋转R + 平移t

4.7 工程落地中的几个坑

最后,分享几个我在实际项目中踩过的坑:

  • 尺度不确定性:单目视觉估计的平移t只有方向,没有尺度。你算出来车移动了1米,但实际可能是10米。所以单目SLAM必须配合其他传感器(如轮速计、IMU)来恢复尺度。
  • 纯旋转退化:如果相机只有旋转没有平移,本质矩阵E会变成零矩阵,无法求解。这时候只能用单应矩阵H。
  • 特征点分布:匹配点最好均匀分布在图像上。如果所有点都集中在图像的一个角落,解出来的E会非常不稳定。

总结一下:2D-2D位姿估计是视觉SLAM的基石。核心就是三步:特征匹配 → 求解E或H → 分解出R和t。每一步都有坑,但掌握了原理,配合RANSAC和正深度检验,基本就能稳定工作了。

一个小建议:刚开始做的时候,别急着上复杂的优化。先用八点法+RANSAC跑通一个最小系统,看看效果。等基础打牢了,再慢慢加BA(光束法平差)之类的优化。


公众号:蓝海资料掘金营,微信deep3321