尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
DP动态规划立体匹配:从原理到工程实战
如果你接触过双目视觉或者三维重建一定绕不开一个问题怎么在左右两张图里找到同一个点。立体匹配就是这个问题的统称而DP动态规划算法是其中思路最直接、也很容易上手的一种解法。它把整张视差图拆成一条条扫描线在每一行内用动态规划搜索最优视差路径结果快、逻辑清晰非常适合做入门选型、二次开发和关键帧初始化的预匹配。这篇内容我不打算按教科书的方式带你过一遍公式而是从实际工程的角度出发把DP立体匹配的原理、实现、参数调节、常见坑和调试经验一次讲透。网上讲DP立体匹配的帖子不少但大多停留在“扫描线搜索最优路径”这层至于代价项怎么设、平滑惩罚怎么调、为什么跑出来总是有横条纹、为什么边界一团糟很少有人讲透。这篇文章就把这些补上适合刚接触立体匹配的开发者也适合想快速搭一套预处理模块的老手参考。1. 先把立体匹配要做的事说清楚1.1 双目立体匹配到底在算什么立体匹配的输入是两张已校正的灰度图像左图和右图。所谓“校正”就是通过双目相机的标定参数做极线校正让同一个物理点在左右图像中落在同一条水平扫描线上。这样一来匹配问题就变成了一维搜索左图某个像素(x, y)只需要在右图的同一行y上去找它的对应像素(x - d, y)这个d就是视差。视差和深度之间存在反比关系。在基线固定的情况下视差越大说明物体距离相机越近。所以立体匹配的输出本质上就是给左图中的每个像素估计一个视差值形成一张视差图。这个问题的本质难度在于不同物体表面可能颜色相近纹理重复的区域更是容易匹配错。而真实的场景中还有遮挡、光照差异、噪声、前景边界断裂等等麻烦所以立体匹配在做的事一方面是“尽力找到最像的那个点”另一方面是“靠邻域一致性把找错的地方纠正回来”。这正是后面DP算法大显身手的地方。1.2 DP动态规划解决的是什么层面的问题裸地逐像素找最佳匹配得到的结果通常很乱。原因很简单每个像素独立选择视差完全没有考虑它和左右邻居之间的约束关系。真实世界里一张连续的物体表面它的视差应该是平滑变化的相邻像素的视差不会突然剧烈跳变。立体匹配的问题因此可以建模成一个能量函数最小化的过程能量函数中包含两项数据项某个像素取某个视差时匹配代价是多少代价越小越像。平滑项相邻像素之间的视差变化如果过大就施加惩罚。目标是在整张图上找一个视差组合让数据项和平滑项的加权总和最小。这个问题从原理上等价于一个二维网格上的马尔可夫随机场优化而这类问题在像素级约束下是NP-hard的不可能在多项式时间内求出全局最优解。于是就有了各种近似方法图割、置信度传播、Cooperative Optimization以及本文要说的DP动态规划。DP的方法很聪明它先把问题退化成一维的扫描线问题在每一行内部用动态规划求最优视差路径因为一维的动态规划是有标准解的。代价是行与行之间不再有关联但换来的是极快的速度和非常稳定的结果。你可以在心里把DP立体匹配理解成把一张地毯横向切成一条条细线在每条线上做局部最优规划最终拼回整张图。2. DP立体匹配的核心设计思路2.1 能量函数怎么定——数据项和平滑项缺一不可DP立体匹配里的能量函数并不复杂先看通用的定义形式E Σ D(x, d(x)) Σ V(d(x), d(x1))前一项是数据项D(x, d(x))表示左图x这个像素取视差d(x)时的匹配代价。后一项是平滑项V是相邻像素视差之间的惩罚。DP在一条扫描线上从左到右推进每一步选择当前像素的视差时都在“当前像素的匹配代价”和“与上一个像素视差变化带来的代价”之间取平衡最终得到整行代价最小的路径。这里有一个很关键的设计取舍平滑项的形式直接决定了结果的性格。如果平滑项非常强相邻像素必须视差接近那么整个行会变得很平物体边缘也会被糊掉。如果平滑项太弱便失去约束力结果跟独立匹配差不多噪声和毛刺满天飞。我实测下来常用的平滑项做法是双阈值模型与SGM里的做法很像视差变化小于某个小阈值(比如1)给一个小代价P1。视差变化超过这个阈值但小于一个更大的阈值给一个中等代价P2。视差发生大跳变时如果是在纹理浅的区域惩罚要弱一些否则边缘处很容易被压平。这个设计的物理含义是允许真实深度不连续的存在不能因为平滑项把背景和前景的边界弄糊。还有一个容易忽略的细节数据项如何选择。常见的有绝对灰度差简单但对光照和噪声极其敏感。SAD绝对差和在窗口内求和有一定抗噪能力。Census变换对光照变化不敏感保留纹理结构信息很适合作为DP的数据项。归一化互相关NCC对增益变化稳健但计算量偏大。我在做这个DP实现时一开始用灰度差结果在左右图亮度有差异的地方直接崩掉后来切到Census变换稳定性明显提升。2.2 在DP与图割等算法之间做选择为什么DP听起来“粗糙”却在很多场景中仍然被使用因为速度和效果的平衡点非常好。图割和置信度传播这类全局方法理论上结果更好尤其在有复杂遮挡关系的场景中它们能建模更大范围的结构约束。但在实际项目中这类算法的计算量往往呈指数级或高多项式级的增长。拿两张普通分辨率的图来说图割跑一次需要几十秒甚至几分钟置信度传播的迭代也要花很长时间。DP的复杂度则是O(W * D * 状态数)W是图像宽度D是视差范围状态数通常很小像基于行的DP本质上只有D个状态。这意味着DP的处理速度是毫秒级别的适合放到实时的流水线里也适合作为预处理步骤快速给出初始化视差之后再交给其他模块精修。所以我的建议很明确如果你的场景是实时或准实时的或者你只是想要一个高质量的初始视差图DP是首选。如果离线处理并且非常看重边界和遮挡区域的精度那么考虑SGM半全局匹配或者全局优化方法更好。值得注意的是SGM本身就可以理解成多方向扫描线DP的扩展掌握了DP再做SGM会顺畅得多。3. 完整实现细节与实操步骤3.1 基础结构代码怎么组织我用C写过一版也用Python写过快速原型。这里不限制语言核心逻辑是一样的。先把整个流程拆成这几个模块读取左右灰度图。设定视差搜索范围[dmin, dmax]。对每个像素计算所有视差下的匹配代价得到代价空间volume。按扫描线逐行做动态规划生成累计代价图。回溯或直接取每列最小累计代价对应的视差作为初始视差。事后处理左右一致性检查、遮挡填充、亚像素细化、中值滤波。规模上假设图像宽度为W高度为H视差范围为D那么代价空间的尺寸是W * H * D。以一张640x480的图、视差范围64来说代价空间的体积就是64048064约2000万个值用float存储需要80MB左右的内存。这个数字足够让你意识到紧凑的数据结构和内存连续性很重要。接着看动态规划的核心部分。对于每一行y创建一个二维累计表C[x][d]表示从左边界走到(x, d)的最小累计代价。递推公式为C(x, d) D(x, d) min( C(x-1, d), C(x-1, d-1) P1, C(x-1, d1) P1, min_k C(x-1, k) P2 )还要带上每个状态的前驱方向等整行填完之后回溯找到最小路径就是这一行的视差序列。下面给一个Python风格的核心伪代码方便看清每一步做了什么for y in range(H): # 每个像素的全视差代价 for x in range(W): for d in range(D): cost volume[y][x][d] best_prev min( C_prev[d], # 视差不变 C_prev[d-1] P1, # 小变化 C_prev[d1] P1, global_min_prev P2 # 大跳变 ) C_cur[d] cost best_prev # 记录前驱方向用于回溯 back_ptr[y][x][d] argmin(...) C_prev C_cur # 回溯获得视差这里有个计算细节值得注意global_min_prev不应该在循环内重复计算应该在进入x之前先对上一列的所有视差求一次min存下来直接调用。否则一个循环里多一层求min计算量直接翻几倍。3.2 匹配代价计算的工程选择DP对数据项的依赖非常强因为平滑项只能在“原本该是什么”和“邻居是什么”之间做调和。数据项如果错了平滑项会把错误传播到一整行。三种常用代价项我分别说下实际表现绝对灰度差AD|I_left(x) - I_right(x-d)|很容易受左右相机曝光差影响。如果左右图亮度均衡也不是不能用但是对噪声很敏感。窗口SAD把周围5x5或者9x9像素的绝对差加起来能显著抑制噪声代价是模糊了物体的边缘细节视差图的边界会变钝。Census变换取一个窗口内像素与中心像素的大小关系形成一个比特串再比较两个比特串的汉明距离。它天然抵抗亮度偏移且在局部保持纹理结构。缺点是如果窗口过小在纹理稀疏区域会失去分辨力。我一般用9x9窗口的Census结果作为数据项。这个变换的另一个好处是它的数值天然是整数且范围稳定(毕竟汉明距离不会超过比特位数)在DP的累计代价表里基本不会出现overflow问题。如果场景中噪声特别大还可以在Census距离外面套一个截断函数把过大的代价值压到固定上限这样可以避免一个离群点带偏整行的动态规划。3.3 平滑参数P1、P2与视差范围参数调整是DP立体匹配里最花时间的部分。P1和P2不是随意拍的需要理解它们的相对量级。先看数据项的尺度。Census的汉明距离在9x9窗口下最大值是81一般真实匹配处的距离可能在5-20之间。P1如果设置成100那意味着视差每变化1个单位就付出100的惩罚基本不给视差变化的空间结果就是整行太平。P1取3到10这个量级比较合适允许小幅度的视差渐变。P2是用于建模遮挡和物体边界的大跳变惩罚一般取P1的3到5倍即15-50之间。如果使用SAD窗口作为数据项代价尺度完全不同需要重新标定。最简单的做法是用一小段真实图跑一遍统计正确匹配区域和非正确匹配区域的代价分布再反过来给P1和P2定参考值。视差范围也不是越大越好。很多人直接设0到255结果不仅慢而且误匹配率上升。正确做法是根据双目配置估算最小和最大深度深度近视差大深度远视差小。比如近距离物体最近0.5米视差可能到60px那上限就设70。远距离5米以外不再关心就把下限设为5。定好范围之后再扩张10%-20%留点余量即可。3.4 一维动态规划的本质与边界问题DP的“每一行独立”是最大的优点也是最大的缺点。优点在于快缺点在于行与行之间没有任何约束。如果只是在每一行单独做DP得到的视差图会带有明显的水平条纹效应原因很好理解右侧像素的视差选择会受到左侧累计的影响却不会受到上方或下方像素的影响所以一个错误的视差决策会沿着扫描线一路传播。解决这类污染通常有两个方向事后处理中值滤波、双边滤波去横纹极其有效。算法扩展多方向扫描线也就是SGM。SGM在多个方向各做一次类似的DP递推然后把结果相加。这样条纹的传播方向被平均掉效果提升非常明显。如果你的目标是快速做一版先用DP加中值滤波是完全可行的。如果你的项目对精度有更高要求建议后面把DP升级成多方向扫描代码改动不大收益却非常直观。3.5 完整实现一次DP立体匹配我拿一个1920x1080的测试图跑了一版视差范围设置为0到96选用了9x9 Census代价项P1设为8P2设为40最后再做一次3x3中值滤波和亚像素抛物线拟合。流程的伪代码可以从代价空间开始逐步走# 1. Census变换 census_left CensusTransform(gray_left, 4) # 4是窗口半宽 census_right CensusTransform(gray_right, 4) # 2. 计算代价空间这里用汉明距离 volume np.zeros((D, H, W), dtypenp.float32) for d in range(dmin, dmax 1): shifted_right shift(census_right, d) volume[d - dmin] HammingDistance(census_left, shifted_right) # 3. 逐行DP volume volume.reshape(D, H, W) # 调整形状 disparity_map np.zeros((H, W), dtypenp.int16) for y in range(H): C_prev np.full(D, 0.0) back_ptr np.zeros((W, D), dtypenp.int16) global_min_prev np.min(C_prev) # 避免重复计算 for x in range(W): C_cur np.zeros(D) for d in range(D): # 一个很重要的细节d-1, d1要检查边界 cost_prev min( C_prev[d], C_prev[d - 1] P1 if d 0 else inf, C_prev[d 1] P1 if d D - 1 else inf, global_min_prev P2 ) C_cur[d] volume[d][y][x] cost_prev back_ptr[x] argmin(C_cur) C_prev C_cur global_min_prev np.min(C_prev) # 4. 回溯 d_opt int(np.argmin(C_prev)) for x in range(W - 1, -1, -1): disparity_map[y, x] d_opt dmin # 恢复真实视差 d_opt back_ptr[x, d_opt] # 沿前驱指针回溯跑完之后我看了一眼输出初期视差图确实是横纹明显边界区域倒是基本能保持。中值滤波之后横纹大部分消失了但边缘的细微层次还是差一些。这个阶段已经可以作为很多应用的输入了。这里还要提一个非常容易被忽略的坑回溯时视差索引别弄反。有些人习惯把视差范围dmin到dmax映射成0到D-1回溯过程中拿到的是索引要加回dmin才是真实视差值。很多人忘记这一步最终出来的视差图整体偏移一个常数看起来“好像对但全都是错的”。3.6 亚像素细化与遮挡处理得到整数视差图后精度通常在1个像素级别很多三维重建场景不够用。最轻量级的提升办法是抛物线拟合用当前最优视差及其相邻两个视差对应的累计代价拟合一条抛物线取抛物线的顶点作为亚像素位置。公式很简单d_sub d (0.5 * (C(d-1) - C(d1))) / (C(d-1) - 2*C(d) C(d1))注意分母不能为零如果接近零就保留整数视差。遮挡区域的识别也不难方法是做左右一致性的交叉检查从左图计算出来的视差图映射到右图后再反向计算右图视差如果左右视差相差超过1到2个像素就标记为遮挡或误匹配区域。这些区域一般出现在物体边界靠背景的一侧因为背景在左图里被前景挡住在右图里根本找不到对应点。遮挡区域的处理基本有两种思路用最近邻有效视差填充方向是从物体边界往外扩散。标记成无效值交给上层模块处理。整体来说我会推荐第二种因为第一眼填出来的视差虽然看着“满”但数值是猜的用在三维重建里会制造出一堆假点。4. 常见问题与调试心得4.1 条纹就这样消除先说为什么会有横纹DP的横纹效应是它最知名的缺陷。你会发现视差图上有明显的一层层横向的“瀑布”纹理越弱的区域越明显。原因是DP在单一行上做递推某个位置的错误决策会通过平滑项沿水平方向传播直到碰到一个纹理很强的锚点才会被纠正。所以在弱纹理区域、重复纹理区域这一错误会延伸到整条线。最简单的抑制方法是中值滤波窗口3x3或5x5效果立竿见影缺点是边界可能被磨糊一点。另一个经验是把P2调大强惩罚大跳变能在一定程度上减少横纹的“传染”代价是边界会被压平。所以不要把希望全寄托在参数上后处理是必须的。如果你受不了中值滤波带来的边界模糊可以试试导向滤波或者双边滤波它们能在保护边缘的前提下平滑弱纹理区域效果比普通中值滤波高一个档次。4.2 边界处一团糟是怎么回事物体前背景交界处的视差存在跳变DP的平滑项却不喜欢跳变。如果不给边界位置留余地DP会倾向于在边界处做一个渐变的缓坡从前景视差逐步过渡到背景视差表现在视差图上就是物体的边缘变厚、边界被“吃掉”一圈。这里一个比较实用的解法是利用颜色或梯度信息来调制平滑惩罚。在亮度图梯度比较大的地方往往是物体边界这时候可以减小P2的惩罚量允许视差发生大跳变。反过来在平坦区域惩罚加强把噪声压住。实现并不复杂在构造平滑项时根据相邻像素的灰度差动态调整P2P2_adaptive P2 * (1 - edge(x))edge(x)是edge map在x处的值可以用Sobel梯度的归一化结果。如果梯度很大edge接近1P2_adaptive就变小边界不会被压平。这个技巧在SGM里也常用确实能显著改善边缘效果。4.3 弱纹理和重复纹理区域怎么让它们不那么混乱弱纹理区域比如一堵白墙任何视差下的匹配代价都差不多DP会在这类区域里随机漂移。重复纹理区域比如百叶窗、密集栅栏则容易出现整数倍错位也就是视差跳了一个周期。弱纹理区域没什么灵丹妙药因为信息本来就少。唯一有效的做法是依赖平滑项把周围可靠区域的视差传播进去。这正好是DP的强项。参数上的直观体现是P2不能设太大否则判断好的区域不会扩散进去但也不能太小否则噪声也扩散进去了。经验值是P2相对数据项尺度来说要让它“略高于大多数误匹配的代价”但又“远低于真正大跳变的代价”。重复纹理区域建议配合唯一性检测如果某个像素在最优视差下的代价和第二优视差下的代价非常接近说明这个匹配不可靠可以直接标记为无效等待后续处理。4.4 代码性能优化的几个建议DP立体匹配其实非常适合工程优化几个关键点列出来内存连续访问是最重要的。一个很常见的做法是把volume存储为[D][H][W]或[H][W][D]不同排列访存模式完全不同。做逐行DP时当前行的数据应该是连续读取的所以建议按行进行重组避免在D维上跳着访问。提前计算global_min_prev。整个内循环里真正发生的数据比较是C_prev的最小值把它在循环外用向量化指令一次算出来内层只做加减和比较。视差方向展开。如果是针对嵌入式平台可以考虑将D维展开到固定循环里让编译器自动做循环展开和指令流水化。多线程按行并行很直接因为每行之间互相独立。开16个线程显著加速几乎无通信开销。代价是代价空间的构建也需要并行化否则会成为新瓶颈。Python代码里如果遇到性能瓶颈把内层循环向量化比如用numpy数组存储C_prev使用np.minimum数组操作替代for循环慢速下降会非常明显。我做过一次对比720p图像、视差128、C多线程实现DP核心部分单线程35毫秒16线程压到6-8毫秒。预处理和后处理另算但也都能在毫秒级完成。整个立体匹配模块跑在20毫秒以内没问题。4.5 从DP到SGM的升级路径如果你已经搭建好了DP立体匹配的框架升级到SGM是水到渠成的事。SGM本质上就是不只在一个方向上做DP而是在八个或者四个方向上各自做一遍动态规划然后把累计代价加起来。每个方向的DP公式和DP完全相同唯一的区别是起始位置和遍历方向不一样。比如从左上到右下方向就沿着对角线扫从右上到左下方向就沿着另一条对角线扫。实现时可以复用DP里的大部分函数for direction in directions: C_dir dp_pass(volume, direction, P1, P2) aggregated C_dir最后在aggregated里取最小代价对应的视差得出来的视差图在边界和弱纹理区域的表现远超单扫描线DP。这也是学术上和工业界都认可的标准路径。如果时间精力不太够我建议路线是这样先完整实现单行DP把数据项、平滑项、回溯、后处理都跑通理解每个模块为什么存在然后加方向循环这样最稳。反过来直接去抄SGM的开源实现一旦出了问题很难定位。4.6 调试数据与调参资源调试立体匹配算法去哪弄图合成图自己用渲染软件生成凹凸起伏的平面左右图做平移视差真值直接就是平移量调试方便。公开数据集经典的双目数据集非常适合跑通流程和对比精度里面的ground truth视差图可以直接算误差指标。自己拍的实拍图用双目相机或者手机夹在尺子上快速拍两幅注意保证平行移动不要在拍摄时旋转。调参的顺序我个人建议是先固定真实视差已知的合成图确认数据项正确。再固定数据项扫描P1、P2观察视差图的均方误差或误匹配率。加窗口大小调节重复观察。最后处理左右一致性阈值。每一步都保留上次参数下的结果图你会发现参数与参数之间是有交互的比如窗口变大之后P2可以适当调大因为窗口本身已经在局部平均了。5. 总结一段实操感受立体匹配里的DP算法很多人在学习时觉得“太简单不值得深究”但实际工程里我反而建议你扎实写好它。它能让你真正理解数据项、平滑项、路径搜索、遮挡处理这些立体匹配的核心模块理解清楚之后再上手SGM、端到端深度学习方案感受会完全不一样。我踩过的几个坑再强调一遍一是回溯索引容易弄错偏移二是global min的复用别写在循环内层三是平滑项中的边界惩罚需要配合边缘图调制四是遮挡区域最好标记为无效而不是强行填数。跑任何一张图之前先用几个已知平移量的合成图检验你的代码能帮你省下一大把调试时间。这套方法我目前还在用作为快速初始化模块嵌入到三维重建流水线里效果稳定、可控。如果你也在做双目相关的工作希望这篇内容能替你省点时间少走些弯路。
RELATED

相关推荐

叮当小宝CS接入:京东客服怎么配置?咚咚接待与工单联动

叮当小宝CS接入:京东客服怎么配置?咚咚接待与工单联动

新店在京东开起来的前几天,咨询量不大,一部手机就能盯完全程。等类目活动上过一轮、店铺权重起来,咚咚的咨询会在两个时段集中涌进来:上午开播前后和晚上八点以后。这时候没有配置好的接待规则,通常会出现两件事&#…

📅 2026/10/16 9:22:30
AI替代不了你的真相:价值、信任与定义问题的能力

AI替代不了你的真相:价值、信任与定义问题的能力

1. 先别急着恐慌:用“替代坐标系”判断AI到底能做什么1.1 我的真实经历:AI先替代了最没风险感的岗位几年前我带过一个数据标注团队,二十几号人,每天对着屏幕框选图片、标注文本。当时我以为这种工作需要耐心和细心,短期…

📅 2026/10/16 9:22:30
UVR 人声分离工具:4 步从伴奏里「拎」出干净人声

UVR 人声分离工具:4 步从伴奏里「拎」出干净人声

UVR 人声分离工具:4 步从伴奏里「拎」出干净人声 【免费下载链接】ultimatevocalremovergui GUI for a Vocal Remover that uses Deep Neural Networks. 项目地址: https://gitcode.com/GitHub_Trending/ul/ultimatevocalremovergui 手里有首歌,…

📅 2026/10/16 9:17:29
MORE NEWS

更多资讯

📰

Fast-LIO从原理到实战:ESKF与迭代卡尔曼的激光惯性里程计

简介:FAST-LIO 是基于 LiDAR 与 IMU 的高精度 SLAM 算法,这份资源是作者针对论文算法与开源代码的个人理解整理,适合正在学习多传感器融合、激光里程计或准备复现 SLAM 系统的研究者参考。文档先梳理基础概念:李群与李代数、流形、…

📰

Spring Boot + Vue绩效考核系统设计与实现:从流程状态机到权限控制

在企业里做绩效考核系统,最难的从来不是写代码,而是把HR那套"考核逻辑"翻译成系统逻辑。最近我刚完成一套基于Spring Boot Vue的企业绩效考核管理系统,从需求梳理到数据库建模,再到前后端联调和部署上线,整…

📰

2025代理公司高性能技术栈架构:五层全景与效率提升实战

1. 为什么代理公司需要"架构"技术栈,而不是拼工具 1.1 先聊聊我见到的效率黑洞 我见过不少数字代理团队,看起来什么工具都有:带客户用的沟通软件、内部协作平台、项目管理面板、代码仓库、好几种CMS、三套不同版本的前端框架、甚至…

📰

医疗供应链管理系统Java后端:微服务架构与库存设计实战

项目标题里这个“医疗供应链管理系统”,我最近半年至少被问过三回。一次是帮某位准备跳槽的兄弟过简历,他写的是通用电商项目;一次是模拟面试时候选人主动选的题目;还有一次是某大厂朋友内推后让我帮忙复盘面试题。共同点很一致&a…

📰

R语言terra包遥感数据处理进阶:栅格代数与分区统计实战

做遥感数据处理,前两篇我们把 terra 的基本读写、投影变换、裁剪和重采样都过了一遍。说实话,很多人玩到这一步就卡住了:数据能读、能裁、能看,可一旦需要从一堆栅格里真正“算出点结果”——比如提取植被指数、做变化检测、按行政…

📰

基于Boost.Signals2实现类Qt信号槽:C++模块解耦与线程安全实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬