1. SGM算法为什么能成为立体匹配的标杆我第一次接触SGM算法是在2016年做自动驾驶项目时当时试遍了各种立体匹配方法最后发现还是SGM的效果最稳。这个由Hirschmüller在2005年提出的算法至今仍是工业界的首选方案特别是在车载摄像头、机器人导航这些对实时性要求高的场景。SGM的核心优势在于它巧妙平衡了精度和效率。传统局部算法像SAD、SSD虽然计算快但在弱纹理区域就抓瞎全局算法如Graph Cut精度高但计算复杂度直接劝退。SGM用了个聪明的办法——多路径代价聚合把二维优化问题拆解成多个一维路径的优化组合。实测下来8路径聚合的版本在1080p图像上能做到30fps错误率比局部方法低60%以上。这里有个容易混淆的概念很多人以为Semi-Global是指算法只处理部分区域。其实它指的是用一维路径近似二维全局优化。我画个示意图就明白了左图像素P的聚合代价 路径1代价 路径2代价 ... 路径8代价这种做法的妙处在于每条路径用动态规划独立计算复杂度从O(WHd²)降到O(WHd)多路径叠加相当于隐式考虑了二维邻域约束不同方向的路径能抑制条纹噪声这是传统动态规划的顽疾2. 代价聚合的魔鬼细节2.1 互信息MI代价计算SGM最初采用互信息作为匹配代价这是它抗光照变化的关键。互信息公式看着吓人MI(I₁,I₂) H(I₁) H(I₂) - H(I₁,I₂)其实理解起来很简单H(I₁)是左图的熵信息量H(I₁,I₂)是联合熵。如果两张图相似联合熵会接近单图熵MI值就大。实际操作时我们会用泰勒展开近似计算# 实际代码中的简化计算 def mutual_info(patch1, patch2): hist_2d np.histogram2d(patch1.flatten(), patch2.flatten(), bins32)[0] joint_prob hist_2d / np.sum(hist_2d) marginal_prob1 np.sum(joint_prob, axis1) marginal_prob2 np.sum(joint_prob, axis0) mi 0 for i in range(joint_prob.shape[0]): for j in range(joint_prob.shape[1]): if joint_prob[i,j] 0: mi joint_prob[i,j] * np.log(joint_prob[i,j] / (marginal_prob1[i] * marginal_prob2[j])) return mi不过现在更常用的其实是Census变换Hamming距离的组合因为完全不用考虑光照归一化硬件友好适合FPGA加速在弱纹理区域表现更好2.2 惩罚参数P1/P2的调参玄学代价聚合的核心公式里有俩关键参数L(p,d) C(p,d) min(L(p-r,d), L(p-r,d-1) P1, L(p-r,d1) P1, min_k L(p-r,k) P2) - min_k L(p-r,k)P1控制小视差变化的惩罚P2控制大视差变化的惩罚。根据经验P1通常在10-50之间与图像梯度相关P2应该是P1的3-5倍更好的做法是让P2自适应图像梯度P2 P2_base / (1 |I(p) - I(q)|)我在KITTI数据集上做过参数敏感性测试参数组合错误率(%)耗时(ms)P110,P21005.2120P120,P2804.8125P130,P21504.5135P150,P22004.3150实际项目中建议先用网格搜索确定大致范围再微调。有个小技巧观察视差图的边缘如果出现锯齿说明P1太小如果边缘模糊说明P2太大。3. 视差优化的实战技巧3.1 左右一致性检查的坑理论上左右一致性检查很简单计算左视差图D_left计算右视差图D_right检查|D_left(x,y) - D_right(x-D_left,y)| threshold但实际会遇到这些问题边界溢出当x-D_left0时要特殊处理亚像素精度直接取整会造成精度损失遮挡区判定建议增加视差符号检查改进版的代码应该这样写def lr_check(D_left, D_right, threshold1): H,W D_left.shape mask np.ones((H,W), dtypebool) for y in range(H): for x in range(W): d int(round(D_left[y,x])) x_right x - d if x_right 0: mask[y,x] False continue # 亚像素插值 if x_right 1 and x_right W-1: d_right interpolate(D_right[y], x_right) else: d_right D_right[y, x_right] if abs(d - d_right) threshold: mask[y,x] False return mask3.2 空洞填充的工程经验视差图的空洞主要来自遮挡区域Occlusion误匹配Mismatch弱纹理Textureless填充策略要区分情况背景空洞取同列最近有效像素的最小视差前景空洞取邻域中值避免背景渗透小区域噪声形态学开运算实测效果最好的组合是先用3x3中值滤波去噪再用5x5最小滤波填充背景最后用颜色引导滤波边缘保特// OpenCV实现示例 cv::medianBlur(disparity, disparity, 3); cv::ximgproc::fastGlobalSmootherFilter( left_image, disparity, disparity, lambda1000, sigma_color0.05);4. 现代优化技巧4.1 多尺度处理加速原始SGM在4K图像上直接跑会非常慢。我们可以构建图像金字塔通常2-3层从最粗尺度开始计算将上一尺度结果作为下一尺度的视差范围约束这样能减少70%计算量代码框架def pyramid_sgm(images, levels3): pyramids [cv2.resize(images, None, fx1/2**i, fy1/2**i) for i in range(levels)] disparity None for i in reversed(range(levels)): if disparity is not None: # 上采样并扩大搜索范围 disparity cv2.resize(disparity, pyramids[i].shape[:2][::-1]) disparity disparity * 2 d_range (disparity-3, disparity3) else: d_range (0, 64) disparity compute_sgm(pyramids[i], d_range) return disparity4.2 硬件加速方案要让SGM达到实时性能必须用硬件加速。常见方案对比方案性能(fps1080p)功耗(W)开发难度CPU多线程8-1045★★GPU(CUDA)30-4090★★★FPGA6015★★★★ASIC1005★★★★★我在Xilinx Zynq上的实现关键点将代价计算拆解为流水线用HLS优化路径聚合的递归计算代价缓存采用行缓冲设计资源占用示例8路径聚合128视差级别1920宽度消耗BRAM 36%、DSP 58%5. 实际项目中的调优记录去年在无人机避障项目里遇到个典型问题在高度纹理重复的场景比如草坪SGM会产生大量误匹配。我们的解决方案预处理增强使用CLAHE增强局部对比度提取Sobel边缘作为额外代价参数动态调整def adaptive_p2(grad): base_p2 100 grad_norm np.clip(grad/50.0, 0, 3) return base_p2 * (1 grad_norm)后处理优化用语义分割结果约束视差范围对天空区域直接置为最大视差调整前后指标对比指标调整前调整后错误率15.2%6.7%运行时间68ms82ms空洞比例12%5%这个案例说明要想用好SGM必须根据场景特点进行定制化优化。现成的OpenCV实现虽然方便但往往达不到项目要求的精度。