ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

SGBM半全局立体匹配详解:从原理到OpenCV调参与工程避坑 去年有一段时间我在给一台室内服务机器人做视觉避障。双目相机的方案定了之后我一开始图省事直接调OpenCV的BMBlock Matching把视差图跑出来结果在办公室常见的光滑地板、白墙、大面积无纹理区域上视差图惨不忍睹——黑窟窿一片条纹状的错误视差到处都是。后来我把匹配算法换成SGBMSemi-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和BTSGBM的第一步是为左图每个像素在每一个候选视差下计算一个匹配代价。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, d1) 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都会先看一眼disp12MaxDiff0时的无效区分布这能很直观地暴露遮挡区域和误匹配区域。遮挡区域本来就没有对应的物理点左右一致性检查会把这些“死区”标出来后续要用插值或者干脆忽略它们。3. StereoSGBM_create参数逐项拆解每个旋钮到底在调什么3.1 一眼看懂参数表OpenCV里SGBM的接口非常简单一个函数把所有参数都给了import cv2 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize5, P18 * 3 * blockSize**2, P232 * 3 * blockSize**2, disp12MaxDiff1, preFilterCap63, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.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取numDisparities160。第三步调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_HHHirschmü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。比如f600像素B0.12米某像素视差d16实际浮点值是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缩到720pSGBM耗时基本能降到原来的三分之一甚至更低而且深度精度损失在接受范围内。原因是视差范围和搜索步长同时变小了计算量是二次方级别下降。限制视差范围不减少单点计算量但能减少搜索的候选视差数也就是动态规划里那个d的循环次数。对近距离场景把maxDisparity从128降到64耗时能省30%左右。3WAY模式主要是给ARM平台准备的牺牲一定的边缘质量换速度。如果连3WAY都跑不动就要考虑异构加速把SGBM放到GPU或者专用的双目深度计算芯片上去了。5.3 进阶玩法把SGBM当辅助去配合深度学习还有一个思路值得提在算力富裕但训练数据不足的场景可以用SGBM生成伪标签来辅助深度学习模型的训练。先跑一批SGBM视差图经过人工筛选和清洗作为弱监督信号去蒸馏一个更小的学生网络。这样既保留了SGBM的无需真值特性又获得了深度学习模型的推理速度和精细边缘建模能力。我在几个工业项目里试过这个路径整体调优的性价比相当不错。最后回到我那个避障项目。在实际运行中我把blockSize和P1/P2调到一个相对均衡的点之后视差图质量已经很能打了但真正让我收获最大的是另一个细节左右镜头的自动曝光必须关闭换成手动固定曝光。这个坑平时不容易发现。在室内光照不均匀时左右镜头的自动曝光各自调节左右图亮度差很多。虽然Census对光照变化有一定鲁棒性但如果亮度差过大匹配代价还是会漂移。我后来在代码里加了一个曝光同步的步骤让两个镜头用同一组曝光参数视差图的空洞率立刻下降了10个百分点以上。做双目视觉这么多年我的感受是SGBM这类传统算法最怕的不是算法本身不行而是输入层面没伺候好——标定不准、曝光不稳、图像有抖动这些问题在调参之前就该解决干净。算法只是最后那个把数学变成可用结果的环节。希望这篇内容能帮你在做SGBM的时候少走一些弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进