31  双目立体视觉

人有两只眼睛,于是能一眼看出哪个物体更近——大脑把两眼视网膜上同一目标的微小位置差换算成了距离。机器视觉可以照搬这套机制:用两台相隔一段距离的相机同时拍摄,同一个空间点在两幅图像里会落在不同的横向位置,这个位置差就是视差(disparity),而视差直接编码了深度。双目立体视觉(binocular stereo vision)就是从一对图像反推出每个像素深度的技术,它是 章节 30 列举的几条三维成像路线中最接近人眼、也最”被动”的一条:不向场景投射任何能量,只靠环境光,因此低功耗、不互相干扰、可远距离工作。代价是它有一个绕不开的前提——被测表面必须自带纹理,因为找视差本质上是在两幅图里找同一块图案的对应,无纹理就无从对应。本章从对极几何讲起,经块匹配、可靠性校验、亚像素与窗口,最后落到深度重建,全程用一对合成的校正立体对(图 31.1)贯穿。

(a) 左图(左相机,原点)
(b) 右图(右相机,+X 60 mm)
图 31.1: 合成的校正立体对。场景含背景平面、抬升方盒、右侧斜坡、左下球冠,以及右下角一块刻意设计的无纹理灰块。右图整体内容相对左图左移,移动量即视差——近物(方盒、球冠)移得多,远的背景移得少。

31.1 对极几何与视差

两台相机看同一个点,几何上并非毫无约束。空间点、两个相机光心共同确定一个平面,它与两个像面相交得到一对对极线(epipolar line)——这就是对极几何(epipolar geometry)的核心结论:左图某像素在右图中的对应点,必定落在它对应的那条对极线上,而不会满图乱跑。这把二维搜索降成了一维搜索。工程上更进一步,通过校正(rectification)对两幅图做一次投影变换,使所有对极线都与图像行严格平行、且行号一一对齐。校正之后,左图第 \(y\) 行某像素的对应点,必在右图同一行 \(y\) 上,只在 \(x\) 方向上有偏移。这正是本章立体对的状态:寻找对应退化为沿同一行的一维滑动。

本例的立体对在合成阶段就已是校正状态(两相机仅沿 \(X\) 平移、内参相同),故可直接做行内搜索。真实系统需先做 章节 5 的双目标定,求出两相机的相对位姿与畸变,再据此计算校正映射——校正质量直接决定后续匹配的成败:哪怕残留半像素的垂直错位,块匹配也会系统性变差。

设两相机光轴平行、间距为基线(baseline) \(B\),焦距 \(f\)(像素),一个深度为 \(Z\) 的点在左右图的横坐标之差为视差 \(d\)。由相似三角形可得 \(d/f = B/Z\),即

\[ Z = \frac{fB}{d}. \tag{31.1}\]

视差与深度成反比:物体越近视差越大,越远越小。本例 \(f=600\) px、\(B=60\) mm,故 \(fB=36000\) mm·px,真值视差落在 36.0~51.4 px(对应 \(Z\) 从 1000 mm 到 700 mm),图 31.1 中近物左移明显、背景几乎不动,正是这个反比关系的直观体现。

反比关系还有一个影响深远的推论。对 式 31.1 求微分,得到一个像素视差误差对应的深度误差,即深度分辨率

\[ \Delta Z = \frac{Z^2}{fB}. \tag{31.2}\]

它随 \(Z^2\) 增长——这就是”双目远处更不准”的数学本质。本例在 \(Z=1000\) mm 处,每 1 px 视差对应 27.8 mm 深度;而到了 \(Z=800\) mm 处只有 17.8 mm,\(Z=700\) mm 处更降到 13.6 mm。同样的视差测量误差,在远处被放大成数倍的深度误差。这条公式也指明了提升精度的两个方向:增大 \(fB\)(更长焦或更长基线),或者把视差测准到亚像素——后者是 小节 31.4 的主题。

图 31.2 是本场景的真值视差场(由已知深度逐像素经 式 31.1 反算得到),后续所有匹配结果都将以它为标尺。注意方盒与球冠(近物)整体偏亮(视差大),背景偏暗(视差小),斜坡从近到远呈连续渐变——这正是 式 31.1 反比关系的图像化呈现,也是块匹配理应逼近的目标。

图 31.2: 由已知深度反算的真值视差场(亮 = 视差大 = 近)。它是评估后续所有匹配结果的标尺:方盒/球冠亮、背景暗,斜坡连续渐变。

基线 \(B\) 是一个典型的权衡量。由 式 31.2,基线越长深度分辨率越高;但基线越长,两相机视角差异越大,被前景挡住、只有一只眼能看见的遮挡(occlusion)区域也越大,且同一表面在两图中的透视形变更严重、匹配更难。工业双目的基线选取要在”精度”与”遮挡/匹配难度”之间折中,通常按工作距离的若干分之一来定。

31.2 块匹配

校正之后,求视差就是:对左图每个像素,沿右图同一行滑动,找到最相似的位置。“相似”需要一个代价函数。最朴素的是绝对差之和(sum of absolute differences,SAD)——取以像素为中心的一个小窗口,逐点算左右灰度差的绝对值再求和,代价越小越像;这与 章节 16 的模板匹配同源,只是模板换成了”左图的局部窗口”、搜索范围被对极几何约束在一行之内。对光照变化更稳健的是归一化互相关(NCC),但计算更重。本例用 SAD,并把搜索限制在视差区间 \([30, 56]\) px(由场景已知深度范围加余量得到)。

对每个像素、每个候选视差都算一遍窗口代价,结果是一个三维的代价体(cost volume) \(C(x,y,d)\)。沿 \(d\) 维取代价最小的位置,就得到该像素的整数视差——这一步称为”赢家通吃”。图 31.3 是 11×11 窗口的原始整数视差结果:方盒、斜坡、球冠的形状都正确浮现,近物亮(视差大)、背景暗(视差小)。但仔细看斜坡和球冠,会发现明显的量化条带——本该平滑过渡的深度被切成了一级级台阶,这正是整数视差的离散化痕迹:真值视差是连续的分数,而赢家通吃只能输出整数。

图 31.3: 11×11 SAD 块匹配的原始整数视差(近物亮、远处暗)。斜坡与球冠上可见阶梯状量化条带,是整数视差离散化的直接后果;右下无纹理块呈现杂乱的随机视差。

定量地看,这套 11×11 SAD 在全图上达到 91.8% 的有效率、平均绝对误差(MAE)0.362 px、坏点率(误差 > 1 px)3.93%。若只统计非遮挡区域,MAE 降到 0.299 px——印证了误差的一大来源是遮挡:被前景挡住的背景像素在右图里根本不存在对应,赢家通吃只能给它配一个错误的最小代价。本例真值遮挡占 0.92%,集中在近物左缘的背景带。如何把这些不可靠的视差识别出来,是下一节的任务。

31.3 失效与可靠性

赢家通吃永远会给出一个”最佳”视差,哪怕那块区域根本没有可靠对应。一个朴素而有效的甄别手段是左右一致性检查(left-right consistency check):以左图为参考求一次视差图 \(d_L\),再以右图为参考求一次 \(d_R\),然后核对——若左图像素 \(x\) 的视差为 \(d_L(x)\),它在右图的对应点是 \(x-d_L\),那么 \(d_R(x-d_L)\) 应当与 \(d_L(x)\) 几乎相等。两者不一致(本例阈值取 1 px)的像素被判为不可靠并剔除。遮挡区会被这一检查自然捕获:被遮挡像素的”对应点”其实属于前景,左右两次匹配各说各话,必然对不上。

图 31.4 是校验后的视差图:遮挡带(近物左缘)和无纹理块的大部分已被剔为黑色(无效),保留下来的视差更干净。代价是全图有效率从 91.8% 降到 88.6%,但保留像素的质量大幅提升——MAE 从 0.362 px 改善到 0.134 px、坏点率从 3.93% 压到 1.26%。可靠性检查不创造精度,它只是诚实地把”我不知道”的地方标出来,这在工业上往往比一个看似稠密、实则掺假的视差图有用得多。

图 31.4: 左右一致性检查后的视差图。遮挡带与无纹理块被剔为黑色(无效);保留像素的 MAE 从 0.362 px 降到 0.134 px。把”无法可靠测量”的区域如实标出,是立体视觉走向工程可用的关键一步。

最能说明问题的是右下角那块 90×90 px 的无纹理块——它是刻意设计的失效区。图 31.3 中它呈现一片杂乱的随机视差,而原始统计显示:这块区域原始有效率高达 100%、却有 82.2% 是坏点。原因很简单:均匀灰块上每个位置长得都一样,SAD 代价曲线在整个搜索范围内近乎平坦,赢家通吃挑出的”最小值”纯粹由传感噪声决定,是个彻头彻尾的随机解。更值得玩味的是左右检查之后:无纹理块仅 43.3% 的像素存活,而存活者中仍有 82.5% 是坏点。也就是说,即便两次随机匹配”碰巧”对上了,对上的也仍是错的——无纹理,无立体

无纹理块的教训直接解释了 章节 32 主动投纹理的动机:既然被动双目在无纹理表面无能为力,那就主动往场景上投射一片人造的随机散斑或编码条纹,人为制造出可供匹配的纹理。这也是”主动双目”传感器的核心思路——双目的几何框架不变,只是用投影仪补上了纹理这一前提。

31.4 亚像素与窗口

整数视差的量化条带(图 31.3)来自赢家通吃只能取整。补救办法与 章节 14章节 20 中的亚像素边缘定位如出一辙:在代价最小的整数视差 \(d_0\) 处,取它与左右邻 \(d_0\pm1\) 的三个代价值 \(C_{-1},C_0,C_{+1}\)抛物线插值,抛物线顶点相对 \(d_0\) 的偏移为

\[ \delta = \frac{C_{-1}-C_{+1}}{2\,(C_{-1}-2C_0+C_{+1})}, \tag{31.3}\]

亚像素视差即 \(d_0+\delta\)。三个采样值确定一条抛物线,顶点公式封闭、计算量可忽略——又一次几乎免费的精度。效果在斜坡区(真值是连续分数视差)最直观:那里整数视差 MAE 为 0.252 px,亚像素插值后骤降到 0.070 px。全图非遮挡误差直方图也随之搬家:\(|\text{err}|\le 0.125\) px 的像素占比从整数的 80.4% 升到亚像素的 91.6%。全图 MAE 从 0.362 改善到 0.342 px、非遮挡从 0.299 到 0.278——量化条带被抹平。

另一个必须按场景调的旋钮是窗口尺寸。窗口大,纳入的纹理多、对噪声的平均更充分,但默认了”整个窗口同属一个深度”,在深度不连续处(物体边缘)会把前景与背景糊在一起。下表是 5×5、11×11、21×21 三种窗口在非遮挡区(亚像素视差)的对照:

表 31.1: 窗口尺寸权衡:大窗整体更准但边缘恶化(非遮挡、亚像素视差)
窗口 MAE (px) 坏点率 边缘环坏点 无纹理坏点
5×5 0.336 3.69% 3.95% 90.5%
11×11 0.278 3.38% 4.26% 86.7%
21×21 0.251 3.24% 5.17% 65.5%

读这张表要分区域看。整体 MAE 随窗口增大而下降(0.336 → 0.251),因为大窗压噪更狠;可边缘环(方盒边界两侧各 12 px)的坏点率却反向恶化(3.95% → 5.17%)——这就是边缘流血(edge bleeding):大窗在前景边缘外侧把背景像素也算了进来,于是边缘被前景”撑胖”。图 31.5 对比 5×5 与 21×21 两个极端:win5 噪点多、边缘锐利,win21 平滑干净但方盒、斜坡的轮廓明显膨胀。有意思的是无纹理坏点率从 win5 的 90.5% 降到 65.5%——大窗把外部相邻的纹理也借了进来,给原本无解的灰块凑出了一点可匹配的内容,但这是借来的、不可信的。

(a) 5×5 窗口(噪点多、边缘锐利)
(b) 21×21 窗口(平滑、但前景增胖)
图 31.5: 窗口尺寸的两难。小窗保边缘但抗噪差;大窗平滑但边缘流血,方盒与斜坡轮廓可见膨胀。窗口尺寸没有万能值,需按”测量精度 vs 边缘保真”按场景折中。

边缘流血可以量到具体像素。在 \(y=195\) 行测方盒边缘的视差跨越位置:方盒左缘真值 \(x=155\),三种窗口测得 148/151/152——越往大窗越偏向左外侧(前景外扩);右缘真值 \(x=290\),测得 291/292/293——同样向外膨胀。窗口每大一圈,前景边缘大约外扩半个窗宽。这解释了为何工业上常对前景边缘附近的视差额外不信任,或干脆改用自适应窗口、引入全局平滑约束的算法。

31.5 深度重建

有了可靠的视差图,套用 式 31.1 逐像素 \(Z=fB/d\) 即得深度图(图 31.6)。这张图的本质是一幅 2.5D 距离图——它记录的是从相机看出去每个像素方向上最近表面的距离,而非完整的三维模型(背面、被遮挡处没有数据),与 章节 30 对 2.5D 的界定一致。要进一步得到度量三维点,还需用相机内参把每个 \((x,y,Z)\) 反投影成 \((X,Y,Z)\),那就进入了 章节 37 的点云领域。

图 31.6: 由视差图经 \(Z=fB/d\) 转换的深度图(近亮远暗)。这是一幅 2.5D 距离图:只记录每个视线方向上最近表面的距离。方盒重建高度 200.1 mm(真值 200)。

精度上,测量结果令人信服:背景中位深度 1000.0 mm(真值 1000)、方盒面 800.0 mm(真值 800),于是重建的方盒高度为 200.1 mm,真值 200 mm——零点几毫米的误差。这里恰好回收了 小节 31.4 的亚像素红利:由 式 31.2\(Z=1000\) mm 处深度分辨率是 27.8 mm/px,若只有整数视差,深度就被钉死在 ~28 mm 的台阶上,根本测不出 200 mm 的盒高到小数点后一位。而 0.1 px 的亚像素精度把有效深度分辨率提升到 2.78 mm(×10),盒高才能精确到 200.1 mm。亚像素不是锦上添花,它是双目能否做度量测量的分水岭。

31.6 SciVision 实现

需要坦诚说明:SciVision SDK 没有提供双目立体匹配库——没有校正、代价体、视差求解或左右检查的现成 API。因此本章的配套工程 code/stereo_vision/ 是一份自包含的 C++ 实现,SDK 仅用于图像读写(SciImage::SaveImage 输出 PNG)。下面是三段核心代码。

代价体的赢家通吃(SAD 用积分图做盒滤波加速,此处略去积分图细节,只示意求最优视差与亚像素):

int best = BIG, bi = -1;                  // 沿视差维找最小代价
for (int di = 0; di < ND; ++di) {
    int c = cost[((size_t)di * H + y) * W + x];
    if (c < best) { best = c; bi = di; }
}
m.id[i] = D0 + bi;  m.d[i] = (float)(D0 + bi);  m.valid[i] = 1;
if (bi > 0 && bi < ND - 1) {              // 抛物线亚像素插值
    double cm = cost[((size_t)(bi-1)*H + y)*W + x];
    double cp = cost[((size_t)(bi+1)*H + y)*W + x];
    double denom = cm - 2.0 * best + cp;
    if (denom > 1e-9) {
        double offd = 0.5 * (cm - cp) / denom;   // 即式 @eq-sv-subpix
        if (offd > 0.5) offd = 0.5; else if (offd < -0.5) offd = -0.5;
        m.d[i] = (float)(D0 + bi + offd);
    }
}

左右一致性检查(对左、右两次匹配结果交叉核对):

auto bmR = blockMatch(right, left, false, 11);   // 以右图为参考再匹配一次
for (int y = 0; y < H; ++y)
    for (int x = 0; x < W; ++x) {
        int i = y * W + x;
        if (!bm11.valid[i]) continue;
        int xr = x - (int)std::lround(bm11.d[i]);     // 左图像素在右图的对应点
        if (xr < 0 || xr >= W || !bmR.valid[y*W+xr]) continue;
        if (std::fabs((double)bm11.d[i] - bmR.d[y*W+xr]) <= 1.0)  // 一致才保留
            lrValid[i] = 1;
    }

blockMatch(left, right, true, 11)blockMatch(right, left, false, 11) 分别求 \(d_L\)\(d_R\)win=11 是窗口边长,D0=30D1=56 框定搜索区间。这种”算法在 SDK 之外”的格局并非偶然:工业双目对算力和延迟极敏感,真正的产品多把立体匹配下沉到专用传感器或 FPGA——视差求解是规则的、可大规模并行的逐像素运算,天然适合硬件流水线,与通用 CV SDK 的”调库”范式分属两层。SciVision 这类库的位置在视差/深度图产出之后:用其滤波、形态学、Blob、测量等工具处理深度图,完成尺寸、缺陷等下游任务。

工业案例:物流分拣的纹理依赖

物流分拣线常用双目相机在线测量包裹体积(长宽高),以计费和规划装箱。实践中成败几乎完全取决于表面纹理:牛皮纸箱表面有印刷、胶带、纤维纹理,视差稠密可靠,体积测得又快又准;可一旦遇上黑色塑料袋、反光封箱胶带或镜面包装,表面要么无纹理、要么镜面反射破坏了朗伯假设,视差大面积失效——测出的体积要么残缺要么乱跳。对策有二:一是改用主动双目,加投一片散斑给表面人为制造纹理(呼应 章节 32);二是多模态补洞,用飞行时间或结构光在双目失效处填补。教训很直接:被动双目的前提是”被测表面自带纹理”,上线前务必先验证目标表面的纹理特性,再决定用被动双目、主动双目还是换技术路线。

31.7 小结

  • 视差即深度:对极几何把对应点搜索约束到一条对极线,校正后退化为行内一维搜索;深度由 \(Z=fB/d\) 给出,视差与深度成反比。
  • 深度分辨率随 \(Z^2\) 恶化\(\Delta Z=Z^2/(fB)\)):本例 1000 mm 处 27.8 mm/px,远处更不准是双目的本质局限,靠增大 \(fB\) 或亚像素来缓解。
  • 块匹配 + 赢家通吃给出整数视差(11×11 SAD:MAE 0.362 px),但整数量化产生阶梯条带;抛物线亚像素插值几乎免费地抹平条带,斜坡区 MAE 从 0.252 降到 0.070 px,并把有效深度分辨率从 27.8 mm 提升到 2.78 mm——亚像素是双目做度量测量的分水岭。
  • 左右一致性检查诚实地剔除遮挡与无纹理区(有效率 91.8% → 88.6%,但保留像素 MAE 改善到 0.134 px);无纹理,无立体——无纹理块原始 100% 有效却 82% 是坏点,检查后仍 82.5% 坏,这正是结构光等主动投纹理方案的动机。
  • 窗口尺寸是精度与边缘保真的两难:大窗整体更准(MAE 0.251)却让前景边缘流血膨胀(边缘坏点 3.95% → 5.17%),方盒边缘随窗口外扩约半窗宽,工程上需按场景折中或改用自适应/全局方法。

关于对极几何与多视图几何的经典论述,可参阅 Hartley 与 Zisserman 的专著 (Hartley 和 Zisserman 2004);稠密双目匹配代价与算法的系统分类与评测见 Scharstein 与 Szeliski 的经典综述 (Scharstein 和 Szeliski 2002),兼顾精度与效率、成为工业主力的半全局匹配(SGM)则由 Hirschmüller 提出 (Hirschmüller 2008)。立体匹配在工业视觉中的工程实现与不确定度分析另可参考 Steger 等人的著作 (Steger, Ulrich, 和 Wiedemann 2018)