拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SGBM半全局立体匹配详解:从原理到OpenCV调参与工程避坑

SGBM半全局立体匹配详解:从原理到OpenCV调参与工程避坑

去年有一段时间,我在给一台室内服务机器人做视觉避障。双目相机的方案定了之后,我一开始图省事,直接调OpenCV的BM(Block Matching)把视差图跑出来,结果在办公室常见的光滑地板、白墙、大面积无纹理区域上,视差图惨不忍睹——黑窟窿一片,条纹状的错误视差到处都是。后来我把匹配算法换成SGBM(Semi-Global Block Matching,半全局块匹配),效果直接从“没法用”变成了“基本可用”。从那时起我就意识到,SGBM是传统立体匹配算法里性价比最高的一个——比局部匹配准得多,比全局匹配快得多,而且OpenCV直接内置,不需要自己从头实现。

这篇文章想用做项目而不是搞理论研究的视角,把SGBM的全链路讲清楚:它解决什么问题、核心思想是什么、OpenCV里的参数到底在控制什么、实际跑起来会遇到哪些坑、以及它和深度学习立体匹配怎么选。适合正好在搞双目视觉、深度估计、机器人避障、三维重建的工程师,也适合刚入门想搞懂SGBM到底怎么工作的同学。

1. 为什么会有SGBM这样一个“中间态”算法

1.1 局部匹配:快是真的快,糙也是真的糙

立体匹配的基本问题很简单:左右两张图像上的同一个物理点,在像素坐标上的水平偏移量叫视差(disparity)。只要知道视差d、相机焦距f和双目基线B,就能用 Z = fB / d 算出深度。问题在于,怎么确定左右图上哪个点和哪个点是同一个点。

最朴素的做法就是BM:在左图像素p周围取一个固定大小的窗口,在右图极线上滑动同样大小的窗口,计算窗口内像素灰度差的绝对值和(SAD)之类的代价,取代价最小的偏移量作为视差。这个思路实现起来简单,OpenCV里几十行就能跑通,速度飞快,但有两个致命问题。

第一个问题是无纹理区域。假设你在拍一面纯白墙壁,左右图上各自窗口里的灰度都差不多,那么不管视差取多少,代价都差不多,最后选出来的视差就是随机的,表现为视差图上大片的胡椒噪声或色块断层。

第二个问题是深度不连续处。物体边缘处,窗口内一部分像素属于前景、一部分属于背景,它们的真实视差不同。窗口硬生生把这俩平均到一起,结果前景边缘的视差向外“膨胀”,这就是常说的前景胖化(foreground fattening)现象。做物体识别时,这个现象会让目标的轮廓比实际大一圈,非常头疼。

1.2 全局匹配:准确但代价极高

为了对付这些难题,研究者把视差图看成一个马尔可夫随机场,定义了一个包含数据项和平滑项的能量函数:

E(d) = sum_p C(p, d_p) + sum_{(p,q) in N} V(d_p - d_q)

其中C是匹配代价,V是相邻像素视差不一致的惩罚。要直接最小化这个能量,就得做二维全局优化,典型方法有图割(Graph Cut)、置信传播(Belief Propagation)。精度确实比局部匹配高一个档次,但运算量极其巨大,在一张普通的VGA图上跑一次要数秒甚至几十秒,实时应用根本扛不住。我在实验室里用Middlebury数据集跑过类似算法,等结果的那几分钟里基本只能盯着进度条发呆。

1.3 半全局思想:把二维优化拆成一维路径

SGM的巧妙之处在于,它不追求严格求解二维全局能量最小化,而是把能量最小化近似成若干条一维路径上的动态规划,再把所有路径的结果加起来。

打个比方:二维全局优化像是在整块地毯上找最优路径,难度很大;一维动态规划像是沿着一根根纱线分别找最优,然后把所有纱线的结果“投票”汇总。每根纱线都有视野盲区,但多根纱线交叉缝合之后,整体精度已经非常接近全局优化,而速度却快了几个数量级。

这就是“半全局”三个字的由来:介于局部和全局之间,卡在了一个工程上极舒服的位置。OpenCV里的SGBM就是SGM思想在块匹配框架下落地的一个工程实现。

2. 拆开SGBM的三步走:代价计算、路径聚合、视差提取

2.1 匹配代价:为什么是Census和BT

SGBM的第一步是为左图每个像素在每一个候选视差下计算一个匹配代价。OpenCV默认组合了两种代价:Census变换和BT代价。

Census变换的思路非常工程化:取中心像素周围一个邻域(比如7x7),把每个邻域像素的灰度值和中心像素比较,比中心大记1,否则记0,得到一个二进制比特串。两幅图中对应像素的比特串算汉明距离,距离越小说明越相似。

这玩意儿最妙的地方是对光照变化不敏感。室内双目往往会出现左右镜头曝光不一致的情况,直接比较灰度绝对值很容易出问题,而Census比较的是相对大小,只要光照是单调变化的,比特串基本不变,所以鲁棒性很好。

BT代价(Birchfield-Tomasi)则用来弥补像素采样离散化带来的误差。它可以看作对灰度值做线性插值后计算的绝对值差,对边缘像素的匹配更宽容。OpenCV的SGBM里有一个preFilterCap参数控制预滤波截断值,默认63。它先把图像归一化,限制梯度入值,防止过强的边缘在代价计算时一家独大。如果场景光照很强烈,可以考虑适当调大这个值。

2.2 多路径代价聚合:SGBM的灵魂

单个像素的代价是没法直接用的,因为它没有考虑邻域里视差应该平滑变化这个先验。SGM的做法是把前面那个能量函数里的平滑项,拆成多条一维扫描线来迭代。

对于一条从方向r来的路径,递推式可以写成:

L_r(p, d) = C(p, d) + min( L_r(p-r, d), L_r(p-r, d-1) + P1, L_r(p-r, d+1) + P1, min_{i} L_r(p-r, i) + P2 ) - min_{i} L_r(p-r, i)

看着唬人,其实一句话就能概括:当前像素的聚合代价 = 自身匹配代价 + 前一个像素在各种视差情况下的最小累积代价,其中前一个像素视差和当前一样时不加惩罚,差一代收P1的税,跳变大了收P2的税。最后减去前一个像素的最小值,是为了防止数值越来越大,这步不改变相对大小,只是数值稳定。

P1和P2是这套算法里最重要的两个超参数。P1惩罚小幅视差变化,P2惩罚大幅视差跳变。P1设太小,视差图会充满各种细碎噪点;P2设太小,则没法强制倾斜表面连成片。通常P2要远大于P1,典型值是P1的4到8倍。

在OpenCV默认实现里,聚合方向一共有8条(上下左右加4个斜对角)。所有方向上这个递推跑完之后,把每条路径的L_r加起来,得到S(p, d)。每个像素取使S(p, d)最小的d就是初始视差。

2.3 从整数视差到亚像素,以及左右一致性检查

每个像素独立取最小之后,得到的还是整数精度的视差图。OpenCV会对最优视差和它的两个相邻视差做抛物线拟合,插值出亚像素级别的视差,这一步能让深度精度显著提升。如果对距离精度有要求,建议保留默认开启的功能。

还有一个关键步骤是左右一致性检查(L-R Check)。具体做法是把左右图互换再跑一遍匹配,得到右图的视差图。然后检查左图某个像素的视差d_L,是否等于右图中对应像素的视差d_R。如果两者之差超过disp12MaxDiff,就认为这个点的匹配不可靠,直接置为无效视差。

我每次跑SGBM都会先看一眼disp12MaxDiff=0时的无效区分布,这能很直观地暴露遮挡区域和误匹配区域。遮挡区域本来就没有对应的物理点,左右一致性检查会把这些“死区”标出来,后续要用插值或者干脆忽略它们。

3. StereoSGBM_create参数逐项拆解:每个旋钮到底在调什么

3.1 一眼看懂参数表

OpenCV里SGBM的接口非常简单,一个函数把所有参数都给了:

import cv2 stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=5, P1=8 * 3 * blockSize**2, P2=32 * 3 * blockSize**2, disp12MaxDiff=1, preFilterCap=63, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM ) disparity = stereo.compute(img_left, img_right).astype(np.float32) / 16.0

注意最后那个 /16.0。OpenCV为了省内存,内部用16位有符号整数存储视差,实际浮点视差是整数值除以16得到的。如果忘了除,后面算深度会差出整整16倍,这是我见过的最常见的低级错误。

下面这张表把每个参数的作用和调参方向列出来,方便对照:

参数作用调参方向
minDisparity最小视差值场景最近距离很大时设为正值
numDisparities最大视差-最小视差,必须16的倍数值越大能测越近的物体,但计算量线性增长
blockSize匹配窗口边长,奇数,不小于5越大对噪声越鲁棒,越容易丢失薄结构
P1/P2平滑惩罚越大视差越平滑,但边缘细节会模糊
disp12MaxDiff左右一致性检查阈值0~2,越大保留越多误匹配
preFilterCap输入梯度截断值光照强的场景可适当调大
uniquenessRatio最小代价与次小代价的最小差距比率越大误匹配越少,但有效视差也变少
speckleWindowSize小连通域滤除窗口0禁用,值太大会删除细小真实物体
speckleRange连通域内最大视差差通常取16~64之间

3.2 调参的先后顺序,不要一上来乱拧

很多人拿到SGBM就凭感觉乱调参数,调半天效果还是很烂。我的习惯是按顺序来,每一步都先确认前一步没问题。

第一步先保证输入图像已经完成了立体校正。如果左右图极线没对齐,后面的调参全白搭。判断方法很简单:找特征明显的点,看它在左右图里是否在同一行。

第二步确定numDisparities。这取决于你要测的最近距离。用Z = fB/d反推:最近距离Z_min已知,算一下对应的最大视差,再往大留些余量,取16的整数倍。比如焦距600像素、基线0.12米,想测最近0.5米,最大视差约144,取numDisparities=160。

第三步调blockSize。从3或5开始,观察噪点情况。如果视差图颗粒感强,可以逐渐加大到9、11。但blockSize一大,物体边缘细节就保不住了。一般室内场景5~7是比较均衡的选择。

第四步才是P1/P2。把P1固定在blockSize^2的一个基准量上,主要调P2。如果视差图出现很多破损的“碎片化”区域,增大P2让平滑约束更强;如果出现大片粘连、物体边缘糊成一团,说明P2太大了,往回降。

最后一步是后处理参数。uniquenessRatio、speckleWindowSize、speckleRange这几个对最终观感影响很大,但在精度要求高的项目里要谨慎使用,因为它们本质上是在“删除”不可靠数据,删多了有效范围就小了。

3.3 三种mode到底该选哪个

mode参数其实是在精度和速度之间做取舍:

  • STEREO_SGBM_MODE_SGBM:默认的8条路径半全局算法,最常用的选择。
  • STEREO_SGBM_MODE_SGBM_3WAY:只有3条路径,速度快很多,内存占用低,适合嵌入式板子。代价是视差图质量下降,特别是物体边缘。
  • STEREO_SGBM_MODE_HH:Hirschmüller在论文里的完整算法,在SGBM的基础上又增加了一些处理,质量最好但速度最慢。

我在x86工控机上一般用SGBM模式;在树莓派这类弱算力平台上,如果实时性要求高,先降分辨率再切3WAY模式,比硬跑HH模式划算得多。

4. 实拍实测:那些一定会遇到的坑和排查链路

4.1 全图错乱:先怀疑标定,不要怀疑算法

如果你跑出来的视差图是一片乱七八糟的色块,完全没有远近层次,第一步不是调参,而是回去检查立体标定和极线校正。

极线校正的本质是重投影,让左右图的对应点落在同一水平线上。如果校正矩阵不准,同一个物理点在左右图里的对应点不在同一行,SGM的搜索路径就会失效,结果必然是乱的。我见过好几次项目组抓算法调参调了一周,最后发现是标定板拍少了,反投影误差大得离谱。

验证方法很简单:校正后,把左右图并排画出来,用cv2.line随手画几条水平线,检查特征点是否都能落到对应的水平线上。这个目测检查花不了两分钟,但能省掉后面十几个小时的瞎调。

4.2 黑空洞:无纹理区域与遮挡区域的必然产物

跑SGBM时,下面两片区域最容易出现黑色无效视差:一是无纹理区域,如白墙、天空、地板;二是遮挡区域,如前景物体遮挡的背景区域。

无纹理区域本质上没有足够的图像信息做匹配,算法给出低置信度的估计还不如不给。左右一致性检查会把这种低置信度结果直接滤掉,变成黑色空洞。我见过有人为了填洞,把disp12MaxDiff设成很大的值,结果误匹配大片回归,整体质量反而更差。

我的做法是区分场景。如果是做三维重建,空洞留下来,后期用深度插值算法去补;如果是做避障,视差图上的空洞宁可保留,避免因为误匹配导致误判。不要为了“好看”而强行填洞。

4.3 条纹状视差:重复纹理和周期性结构惹的祸

百叶窗、格栅、工装裤、键盘这类周期性的结构是立体匹配的噩梦。左右两幅图里每一根竖条长得都差不多,匹配时会出现多个低代价候选,算法容易在相邻周期之间跳来跳去,视差图上就会呈现一条一条的条纹。

遇到这类场景,我一般把blockSize加大,让匹配窗口内能覆盖更多的纹理周期,从而区分出微小的错位差异。同时适当增大P2,让相邻像素的视差保持连贯,压制跳变。

如果还是压不住,可以试试在输入图像上做一个轻微的滤波,或者换光照角度。这里额外提醒一句:不要在SGBM之前乱加高斯模糊,模糊虽然能减少噪声,但也会抹掉边缘的细小结构,导致深度边缘变糟糕。

4.4 从视差图到深度图:别忘了单位换算和坐标系

SGBM输出的是视差图,不是深度图。很多初学者卡在这里。深度转换公式很简单:

Z = f * B / d

其中f是焦距(像素单位),B是双目基线长度(米),d是视差(像素,记得除以16)。比如f=600像素,B=0.12米,某像素视差d=16(实际浮点值是1.0像素),那么深度Z = 600 * 0.12 / 1.0 = 72米,这说明该点非常远,接近无穷。

所有像素都算一遍,再配合相机内参和相对位姿,就能生成3D点云。OpenCV里可以用reprojectImageTo3D配合Q矩阵一步到位,但前提是你有一份准确的Q矩阵,它来自立体校正头的输出。

5. 工程选型:SGBM的定位和它的替代者

5.1 传统SGBM vs 深度学习立体匹配

现在深度学习立体匹配已经很强了,像PSMNet、RAFT-Stereo这类网络精度能吊打传统方法,但SGBM在工程界仍然活得很好,原因有三个。

第一是稳定性。深度学习模型的泛化能力受训练集影响很大,换一套镜头、换一个光照环境,性能就可能崩。SGBM没有训练偏差,只要标定没问题,理论在任何场景下都能跑出一个稳定可预期的结果。

第二是算力需求。一张VGA分辨率图像,SGBM在普通CPU上都能跑到实时,而深度学习模型动辄需要GPU,在嵌入式双目相机和实时避障场景里不太现实。

第三是数据成本。SGBM不需要真值视差图,买双摄像头就能开干;深度学习需要一个带高精度真值的训练集,自己采集和标注成本极高。

所以在做选型时,我的经验是:实时性要求高、算力弱、场景变化大,选SGBM;离线处理、算力充裕、且能搞到足够多的领域数据,再考虑深度学习方案。

5.2 SGBM的提速三板斧

SGBM虽然比全局算法快,但在高分辨率图像上依然吃力。我的优化顺序是:先降分辨率,再限制视差范围,最后切3WAY模式。

降分辨率是最直接的。把图像从1080p缩到720p,SGBM耗时基本能降到原来的三分之一甚至更低,而且深度精度损失在接受范围内。原因是视差范围和搜索步长同时变小了,计算量是二次方级别下降。

限制视差范围不减少单点计算量,但能减少搜索的候选视差数,也就是动态规划里那个d的循环次数。对近距离场景,把maxDisparity从128降到64,耗时能省30%左右。

3WAY模式主要是给ARM平台准备的,牺牲一定的边缘质量换速度。如果连3WAY都跑不动,就要考虑异构加速,把SGBM放到GPU或者专用的双目深度计算芯片上去了。

5.3 进阶玩法:把SGBM当辅助去配合深度学习

还有一个思路值得提:在算力富裕但训练数据不足的场景,可以用SGBM生成伪标签来辅助深度学习模型的训练。先跑一批SGBM视差图,经过人工筛选和清洗,作为弱监督信号去蒸馏一个更小的学生网络。这样既保留了SGBM的无需真值特性,又获得了深度学习模型的推理速度和精细边缘建模能力。我在几个工业项目里试过这个路径,整体调优的性价比相当不错。


最后回到我那个避障项目。在实际运行中我把blockSize和P1/P2调到一个相对均衡的点之后,视差图质量已经很能打了,但真正让我收获最大的是另一个细节:左右镜头的自动曝光必须关闭,换成手动固定曝光。

这个坑平时不容易发现。在室内光照不均匀时,左右镜头的自动曝光各自调节,左右图亮度差很多。虽然Census对光照变化有一定鲁棒性,但如果亮度差过大,匹配代价还是会漂移。我后来在代码里加了一个曝光同步的步骤,让两个镜头用同一组曝光参数,视差图的空洞率立刻下降了10个百分点以上。

做双目视觉这么多年,我的感受是,SGBM这类传统算法最怕的不是算法本身不行,而是输入层面没伺候好——标定不准、曝光不稳、图像有抖动,这些问题在调参之前就该解决干净。算法只是最后那个把数学变成可用结果的环节。希望这篇内容能帮你在做SGBM的时候少走一些弯路。

返回列表