3. 双目立体视觉基础:对极几何、视差与深度的关系、立体匹配的基本流程

说到深度估计,双目立体视觉是我个人觉得最直观、也最容易上手的方法。你想想看,人类天生就有两只眼睛,大脑就是靠这两只眼睛看到的微小差异来判断距离的。计算机视觉里,我们做的就是模拟这个过程。

这一节,我会带你从最核心的三个概念入手:对极几何视差与深度的关系,以及立体匹配的基本流程。搞懂了这些,你就能理解为什么两张图片能算出三维深度。

3.1 对极几何:两张图之间的约束关系

先问一个问题:给定左图上的一点,怎么在右图上找到对应的点?

最笨的办法是在整张右图上搜索。但这样太慢了,而且容易出错。对极几何告诉我们:左图上的一个点,在右图上的对应点一定在某一条直线上。这条线就叫「极线」。

核心概念:对极几何描述的是两个相机视图之间的几何关系。它不依赖场景内容,只依赖相机内参和两相机之间的相对位姿。

我刚开始做双目项目时,总觉得对极几何是纯理论,没什么用。直到有一次,我需要在嵌入式设备上做实时深度估计,搜索范围太大导致帧率上不去。后来我用了极线约束,把搜索从二维降到了一维,速度直接提升了十几倍。嗯,从那以后我再也不敢小看它了。

3.1.1 基本元素

  • 基线:两个相机光心的连线
  • 极平面:空间点与两个光心构成的平面
  • 极线:极平面与成像平面的交线
  • 极点:基线与成像平面的交点

说白了,对极几何就是告诉我们:左图上的点,它的匹配点一定在右图的某条极线上。反过来也一样。这个约束叫「极线约束」。

3.1.2 本质矩阵与基础矩阵

实际工程中,我们通常用矩阵来表达这种约束关系:

  • 本质矩阵 E:描述两个相机在归一化坐标系下的对极几何关系。它只包含旋转和平移信息。
  • 基础矩阵 F:描述两个相机在像素坐标系下的对极几何关系。它包含了内参和外参。

公式很简单:

// 极线约束公式
// 左图点 p1 和右图点 p2 满足:
p2^T * F * p1 = 0

// 如果相机已标定,可以用本质矩阵:
p2^T * E * p1 = 0

我的经验:实际项目中,我建议先做立体校正(Stereo Rectification),把两张图变成「行对齐」的状态。这样极线就变成了水平线,搜索时只需要在同一行上找匹配点。省事多了。

3.2 视差与深度的关系

好,现在假设我们已经找到了左右图上的匹配点。接下来怎么算深度?

这里有个非常直观的几何关系。你看,当物体离相机近时,它在左右图上的位置差异大;离得远时,差异小。这个差异就是「视差」。

3.2.1 三角测量原理

我画个图你就明白了:

双目三角测量原理 左相机 O₁ 右相机 O₂ 基线 B P (X, Y, Z) 左成像平面 右成像平面 x₁ x₂ 视差 d = x₁ - x₂ 深度 Z

从图上你能看到:

  • 空间点 P 在左图成像为 x₁,在右图成像为 x₂
  • 视差 d = x₁ - x₂(注意:这里 x₁ 和 x₂ 是像素坐标)
  • 深度 Z 与视差 d 成反比

3.2.2 深度计算公式

公式其实很简单:

Z = (f * B) / d

其中:
Z  = 深度(单位:毫米/米)
f  = 相机焦距(单位:像素)
B  = 基线长度(单位:毫米)
d  = 视差(单位:像素)

注意:这个公式成立的前提是两张图已经做了立体校正,即左右图的行完全对齐。如果没有校正,公式会更复杂,需要用到基础矩阵。

我曾经在一个项目中,直接用原始图像算深度,结果出来的深度图全是扭曲的。后来才发现是忘了做立体校正。嗯,这个坑我替你们踩过了。

3.2.3 视差与深度的关系曲线

视差(像素) 深度(米) 说明
1 100.0 远处物体,视差小
5 20.0 中等距离
10 10.0 常见场景
50 2.0 近处物体
100 1.0 非常近,视差大

(假设 f = 1000 像素,B = 0.1 米)

你看,视差和深度是反比关系。这意味着:近处的物体深度精度高,远处的物体深度精度低。这是双目视觉的固有限制。

3.3 立体匹配的基本流程

好,现在理论讲完了。我们来看看实际工程中怎么做。立体匹配,说白了就是「找对应点」的过程。

3.3.1 整体流程

我习惯把立体匹配分成四个步骤:

  1. 图像预处理:去噪、增强对比度、直方图均衡化
  2. 立体校正:让左右图行对齐,极线变成水平线
  3. 匹配代价计算:计算每个像素在不同视差下的相似度
  4. 视差优化:从代价中选出最优视差,做平滑和后处理

下面这张图能帮你理清思路:

图像预处理 去噪 / 增强 立体校正 行对齐 匹配代价计算 相似度度量 视差优化 平滑 / 后处理 输出:视差图 / 深度图

3.3.2 匹配代价计算

这一步是核心。我们要判断左图的一个像素和右图的一个像素是不是同一个点。常用的方法有:

  • SAD(绝对差和):计算窗口内像素差的绝对值之和。简单快速,但对光照敏感。
  • SSD(平方差和):类似SAD,但用平方放大差异。对噪声更敏感。
  • NCC(归一化互相关):对光照变化不敏感,但计算量大。
  • Census变换:用局部二值模式描述纹理,鲁棒性好。我项目中常用这个。
// SAD 匹配代价计算示例
// 左图窗口 L,右图窗口 R,窗口大小 w
float computeSAD(Image L, Image R, int u, int v, int d, int w) {
    float cost = 0.0;
    int half = w / 2;
    
    for (int i = -half; i <= half; i++) {
        for (int j = -half; j <= half; j++) {
            float l_val = L.getPixel(u + j, v + i);
            float r_val = R.getPixel(u + j - d, v + i);  // 注意:视差 d 导致水平偏移
            cost += fabs(l_val - r_val);
        }
    }
    return cost;
}

我的建议:如果场景光照变化大,别用SAD。我试过在户外用SAD做双目匹配,结果一塌糊涂。换成Census变换后,效果好了很多。

3.3.3 视差优化

计算完代价后,我们得到的是一个「代价立方体」。接下来要从中选出最优视差。常用的方法有:

  • 赢家通吃(WTA):直接选代价最小的视差。简单,但噪声大。
  • 动态规划(DP):在极线上做全局优化。效果好,但有条纹伪影。
  • 半全局匹配(SGM):在多个方向做DP,然后融合。这是目前工程中最常用的方法。
  • 图割(Graph Cut):全局优化,效果好但慢。

我个人最推荐SGM。它在精度和速度之间取得了很好的平衡。我在多个项目中都用它,效果稳定。

3.3.4 后处理

最后一步,对得到的视差图做后处理:

  • 左右一致性检查:从左到右和从右到左的视差应该一致。不一致的就是遮挡点,需要剔除。
  • 中值滤波:去除孤立噪声点。
  • 亚像素插值:用二次曲线拟合,得到亚像素精度的视差。

避坑指南:我曾经在项目中忽略了左右一致性检查,结果深度图上全是遮挡区域的错误值。后来加上这个检查,虽然丢掉了一些像素,但剩下的都是可靠的。记住:宁可少,不可错

3.4 小结

这一节我们讲了三个核心内容:

  • 对极几何:用极线约束把二维搜索降为一维
  • 视差与深度:Z = fB / d,反比关系,近处精度高
  • 立体匹配流程:预处理 → 校正 → 代价计算 → 优化 → 后处理

搞懂了这些,你就掌握了双目立体视觉的骨架。下一节我们会深入SGM算法,看看它到底是怎么做到又快又准的。


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