立体块匹配的散斑滤波:连通域阈值如何误删细杆
从 SGBM/BM 代价聚合后的散斑统计出发,说明连通域面积阈值与视差连续性如何误伤细结构;对比 speckleFilter、左右一致性与置信度掩膜,用已知细杆场景的召回与空洞率验收。

1. 噪点少了,护栏也断了
双目视差图里最容易让人误判的改进,是背景上的彩色碎片明显减少,同时一根真实的细杆也从深度图中消失。点云看起来更干净,抓取或避障却开始漏检。若验收只看全图有效像素比例、平面均方误差,甚至可能把这次退化记成优化。
典型场景是两三像素宽的金属杆、货架横撑、线缆或远处护栏。它在校正后的左图中确实可见,原始 SGBM 视差也给出一段大致正确的响应;打开 speckleWindowSize=100 后,这段响应被写成无效值。问题不在「滤波太强」这句泛泛判断,而在滤波器采用的对象模型:它不知道哪里是杆,只把视差图解释成由四邻接边连接的连通域,再按像素面积一刀切。
细杆恰好触碰了这个模型的两个薄弱点。其一,它的投影面积小,即使长度很长,宽度只有少数像素时总面积仍可能低于阈值。其二,块匹配窗口跨过前景和背景,杆上的视差会被切成若干不连续短段;滤波器看到的不是一根长杆,而是多个面积更小的岛。要把问题调明白,必须从代价聚合、视差编码、连通规则一直追到验收掩膜,不能只在最终彩色图上拖动两个滑块。
2. 散斑是匹配不确定性的拓扑结果
StereoBM 对每个像素和候选视差计算块代价,并在窗口内聚合;StereoSGBM 则把局部匹配代价沿若干方向累积,用 和 惩罚视差变化。把简化后的路径递推写出来:
最终视差来自各方向路径代价之和的最小值,亚像素拟合后再以定点数输出。这个过程改善了弱纹理区域的连续性,却没有创造新的观测。遮挡区在右图没有对应点;重复纹理可能有多个相近极小值;低纹理块的代价曲线近乎平坦;物体边界处的支持窗口同时覆盖前景与背景。它们都可能产生局部错误视差。
这里的「散斑」不是图像亮度上的椒盐噪声,而是视差空间中面积较小、与周围不连续的区域。一个散斑内部可以有很多像素,亮度也可以很平滑;反过来,一个单像素亮度噪点若没有改变视差,根本不是该滤波器处理的对象。把散斑滤波当成中值滤波或形态学开运算,会误解参数的含义。
BM 与 SGBM 的上游参数还会改变散斑的统计形态。增大 BM 的块尺寸通常减少孤立点,却会让边界视差向两侧膨胀;增大 SGBM 的平滑惩罚会连接一些短裂缝,也可能把窄前景拉向背景。uniquenessRatio 提高后,模棱两可的像素更早变成无效值,剩余细杆可能被切得更碎。因此,散斑阈值不是独立于匹配器的「最后一步参数」,它作用于上游参数已经塑造过的拓扑。
3. filterSpeckles 实际执行的连通判据
设有效视差像素组成顶点集合 ,像素 的存储视差为 。OpenCV 的实现检查上、下、左、右四个邻居;若相邻像素都不是无效值,并且
就在它们之间连一条边。对得到的每个连通分量 ,若
便把整个分量写成 newVal。这里 对应 maxSpeckleSize,在 StereoMatcher 参数中对应名字容易误导的 speckleWindowSize;它是连通域最大像素数,不是边长为该数值的方形窗口。源码中的比较是小于等于,所以面积恰好等于阈值的区域也会被删。
另一个常被忽略的细节是, 约束的是传播过程中每一对相邻像素,而不是整个分量的最大值减最小值。假设斜面上一串视差为 20、21、22、23,只要相邻差都不超过 1,它们仍属于同一分量,即使首尾差为 3。这种「链式连通」能保留平滑斜面,也可能借一条视差缓坡把本来不同的区域接起来。
因此,滤波器只有两项证据:局部视差是否连续、连通面积是否足够大。它不看颜色边缘,不知道语义类别,不理解细长形状,也不判断视差是否满足极线几何。面积小的真实目标与面积小的错误匹配,在这个决策空间中完全同类。把阈值叫作「噪声大小」很方便,却掩盖了它其实是对最小可保留目标投影面积的硬约束。
4. 为什么细杆会被切成小岛
若杆的可见投影宽度为 ,连续可匹配长度为 ,理想连通面积近似为
其中 是遮挡、反光和低纹理造成的缺口。圆杆直径为 、深度为 时,其宽度量级约为 。同一根杆越远,宽度按距离反比缩小;面积阈值却仍以固定像素计数。一个在近处稳定保留的参数,换到远端货架就可能越过删除边界。
更麻烦的是,滤波器并不先把所有属于杆的像素相加。只要某一行出现无效视差,或者相邻两点的视差差值超过 ,杆就会在图上断开。设断裂后得到 段,每段面积为 。即使 远大于阈值,只要每个 ,这些段仍会逐个消失。真实目标的总面积并不能保护它,决定结果的是最大连续段面积分布。
块窗口正是断裂的重要来源。两像素宽的杆配五像素窗口时,窗口内大部分样本来自背景。沿杆移动时,前景与背景占比随边缘、纹理和遮挡变化,最优视差可能在前景峰与背景峰之间跳动。SGBM 的路径聚合可让部分位置连续,但交叉杆、端点和高光处仍容易留下无效带。若杆与背景的真实视差间隔为 ,还会出现两种相反情况:
- 当 时,杆会从大背景分量中分离;面积不足便被删除。
- 当 ,或中间存在逐步变化的错误视差桥时,杆可能并入背景大分量而幸存,但其视差未必仍代表杆。
第二种情况说明「滤波后杆还有值」也不等于杆被正确恢复。只统计有效像素会把背景泄漏算成成功,验收必须同时约束视差误差。
5. OpenCV 参数的单位契约
StereoBM 和 StereoSGBM 的常见输出是 CV_16S,使用四位小数的定点表示:
直接调用 cv::filterSpeckles 或 Python 的 cv2.filterSpeckles 时,maxDiff 与输入矩阵处于同一存储单位。希望允许相邻像素相差 2 个视差像素,就应对原始 CV_16S 图传入 ,而不是 2。先除以 16 转为浮点再调用也不对:该 API 的公开契约面向单通道有符号 16 位视差图。
StereoSGBM 构造参数 speckleRange 则按整数视差像素配置,OpenCV 文档明确说明内部会乘 16。例如 speckleRange=2 在 SGBM 的 CPU 路径中最终以 32 传给连通域实现。内置参数与独立函数的同义数值不等价,把同一个 YAML 数字同时喂给两条路径,会得到相差十六倍的连续性阈值。
还不应假设 BM 与 SGBM 的同名参数在所有版本、后端上都经过完全相同的缩放。以当前 OpenCV 4.x CPU 源码为例,SGBM 调用前显式乘 StereoMatcher::DISP_SCALE,BM 的过滤调用路径则直接转交其参数值。要公平比较 BM、SGBM 或不同 OpenCV 构建,稳妥做法是关闭匹配器内置散斑过滤,保留原始 CV_16S 输出,再统一调用一次独立 filterSpeckles;同时在实验记录中保存 cv2.getBuildInformation() 和版本号。
无效值也不能随手写成 0。OpenCV 匹配器通常以
表示无效视差。若 minDisparity 非零或为负,把 0 当无效值会误伤合法近零视差,或者漏掉真正的无效像素。独立过滤时应把这个值作为 newVal,并在所有阶段使用同一有效性判据。另一个版本相关细节是当前 SGBM CPU 路径在散斑过滤前还会对视差做 中值滤波;复现实验时不能把「纯聚合输出」与「compute 返回值」混为一谈。
6. 把删除掩膜单独保存
调参的第一步不是画更漂亮的伪彩色图,而是让每个后处理阶段留下布尔掩膜。下面的基线关闭 SGBM 内置散斑处理,再显式过滤。这样 removed_by_speckle 精确表示本阶段新增的无效像素,不会把搜索范围边界、唯一性检查造成的空洞归到散斑滤波头上。
import cv2 as cv
import numpy as np
min_disp = 0
sgbm = cv.StereoSGBM_create(
minDisparity=min_disp,
numDisparities=128,
blockSize=5,
P1=8 * 1 * 5**2,
P2=32 * 1 * 5**2,
disp12MaxDiff=-1,
uniquenessRatio=8,
speckleWindowSize=0,
speckleRange=0,
mode=cv.STEREO_SGBM_MODE_SGBM_3WAY,
)
raw16 = sgbm.compute(left_rect, right_rect)
assert raw16.dtype == np.int16
invalid16 = np.int16((min_disp - 1) * 16)
valid_raw = raw16 > invalid16
filtered16 = raw16.copy()
filtered16, _ = cv.filterSpeckles(
filtered16,
float(invalid16),
maxSpeckleSize=80,
maxDiff=float(2 * 16),
)
valid_filtered = filtered16 > invalid16
removed_by_speckle = valid_raw & ~valid_filtered
disp_px = np.where(valid_filtered, filtered16 / 16.0, np.nan)若 Python 绑定版本对关键字参数支持不同,可按位置传入,但不要省掉单位转换。filterSpeckles 会改写输入输出数组,必须先复制原始图;否则事后无法知道某个空洞是原本无效还是本阶段删除。比较参数时还要固定校正图、曝光、匹配器配置和有效视差 ROI。左侧搜索边界本就不能产生完整视差,把它计入空洞率会让 numDisparities 变化伪装成滤波退化。
建议至少落盘四张无损数据:raw16、filtered16、removed_by_speckle 和评测 ROI。PNG 伪彩色只用于查看,不应反向读回计算指标,因为归一化和颜色映射已经丢失无效值及亚像素信息。对删除掩膜再与杆标注求交,可以直接回答「哪些真实像素由散斑阶段删掉」,而不是从最终点云猜测。
7. 面积阈值与连续阈值不是单调旋钮
固定 时,提高 会让更多小分量被删除,这一方向是单调的。但调整 的效果并不单调。减小 会切断视差抖动处的边,把一根杆分成更多小段,删除率通常升高;增大 会合并相邻区域,可能让杆接入大分量而免删,也可能让错误视差借「桥」并入背景,从而逃过滤波。
因此,speckleRange 调大后空洞减少,不足以证明结果改善。它可能只是取消了删除边界,把原先能识别的小错误岛黏到大区域上。至少要同时观察:
- 连通分量的面积分布,尤其是杆内各段的最大面积和分位数;
- 分量内部相邻视差差值分布,而不只是全局极差;
- 杆与邻近背景的视差误差和边界泄漏;
- 每个阶段新增的无效像素,而不是最终无效总量。
分辨率变化也会破坏固定面积阈值。图像按比例缩放为原来的 倍时,同一表面理想投影面积约变为 倍,但细杆在接近采样极限时并不遵循平滑缩放:三像素宽缩到一点五像素后,插值、校正和块窗口可能直接让它断裂。按总像素数线性换算阈值只能作为起点,不能代替目标尺度分桶验收。
调参顺序应先稳定上游契约:极线校正残差、视差搜索范围、块尺寸、唯一性和 SGBM 平滑项;然后关闭所有后处理导出基线;最后在固定基线上扫描 二维网格。若一边改 blockSize 一边改面积阈值,连通域输入已经变化,无法判断召回损失来自哪一步。
8. 左右一致性检查解决的是另一类错误
左右一致性不按区域大小判断,而是检查同一三维点在两幅图上的几何闭环。为避免符号歧义,先定义左右视差都取正的幅值。左图像素 映射到右图 ,一致性条件可写成
实际实现应对亚像素位置插值,并先确认右匹配器输出的符号约定;ximgproc.createRightMatcher 生成的右视差常采用与左图不同的符号,不能把两个原始 CV_16S 数值直接相减。OpenCV 匹配器的 disp12MaxDiff 提供内置检查,参数按整数视差像素表达,但为了得到可审计的阶段掩膜,实验中常值得显式计算右视差和 lr_fail。
左右检查擅长拒绝偶然匹配和部分遮挡错误,却也不是细杆保护器。细杆两侧大多是深度不连续边界;从左相机可见的杆边缘,在右相机可能被背景或杆自身遮住。杆越细,可供两个视角共同观察的内部像素比例越低。把 设得很严,会让正确但一侧不可见的边缘成为空洞。
这与散斑过滤的误伤机制不同:散斑过滤因「小」而删,左右检查因「闭环不成立」而删。两者串联若直接取交集后的有效区域,错误会叠加。验收应分别记录 lr_fail、removed_by_speckle 及二者交集。若细杆召回主要在前者下降,应检查遮挡模型与采样;若主要在后者下降,才去调连通阈值。只看最终 mask 无法定位责任。
9. 置信度掩膜比面积多了什么,又缺什么
理想置信度应反映最佳视差与竞争候选之间的可分性、左右闭环、局部纹理和边界位置。原生 StereoBM/StereoSGBM 的 compute 不直接返回完整代价曲线或校准后的逐像素概率。uniquenessRatio 是对最佳代价相对优势的门限,它能提前拒绝模糊匹配,但不是可跨场景比较的概率;把「通过 uniqueness」等同于高置信度,会高估重复纹理上的结果。
使用 opencv_contrib 的 WLS 视差滤波链路时,DisparityWLSFilter.getConfidenceMap() 可以给出浮点置信度图,常用范围为 0 到 255。它综合左右视差等信息,适合生成 low_confidence 掩膜。若自有匹配器保留代价体,也可以使用最佳与次佳代价间隔、局部曲率或熵构造置信度,但需要在标注数据上校准,不能凭一个阈值把任意分数解释成概率。
置信度相对面积规则的优势,是两个同样大小的区域可以得到不同判断:一个在纹理清晰杆面上、代价峰尖锐;另一个在重复栅格上、多个候选接近。缺点则是细杆恰好经常处于混合窗口和遮挡边缘,天然低置信。若策略是「低置信全部删除」,它会用另一条理由重演同样的误删。
更合理的组合不是盲目串联三个硬阈值,而是保留各自证据:
这个交集可作为保守拒绝起点:小分量只有在置信度低且左右闭环也失败时才删除。它通常比「任一条件失败就删除」少制造空洞,但仍不能证明被保留值正确。对安全相关的细杆目标,还要把状态区分为可信、无效和未知;未知区域交给后续时序或其他传感器处理,而不是把保留操作包装成深度恢复。
10. 不靠无限降低阈值保护细结构
把 maxSpeckleSize 降到零当然能保住更多杆,也会放回大量孤立错误点。工程目标不是让某一帧的杆 mask 变满,而是在控制背景误匹配的同时保留可验证细结构。可以从三类额外证据入手。
第一类是形状。面积相同的紧凑噪声团与细长分量,在骨架长度、主轴长宽比、周长面积比上不同。可以对待删除分量计算二阶矩;若主轴明显、沿轴方向连续,转入「候选细结构」而非立即删除。不过块匹配散斑也可能呈条带状,所以形状只能降级判决,不能单独作为真值。
第二类是图像边缘与视差层次。真实杆通常在左图有成对边缘,内部视差应靠近前景层而非在整个搜索范围随机跳动。若连通分量位于稳定的双边缘之间,且其视差中位数与已知前景深度带一致,可以使用更小的最小保留面积。这里要防止用左图边缘直接「填造」视差:边缘只能保护已有候选,不能替无匹配像素发明深度。
第三类是时序。相机或目标运动后,遮挡关系和亚像素采样会变化。一帧中只有二十个像素的杆段,经过位姿补偿后若在多帧投影到同一三维细长结构,其可信度高于随机散斑。相反,错误匹配往往无法在三维中维持位置。时序融合必须使用原始观测置信和可见性模型,不能简单对视差做时间中值,否则运动边界会留下拖影。
若应用已经知道细杆可能出现的 ROI 或最小物理直径,还可以由 、深度范围和目标尺寸推算最坏投影宽度,在采集设计阶段判断它是否低于可辨识极限。若远端杆只有不到一个像素宽,任何后处理都无法稳定恢复;正确动作是提高分辨率、缩小工作距离、改变基线或增加主动深度,而不是继续寻找「不误删」的连通域阈值。
11. 用已知细杆场景做分层验收
验收集应包含已知几何的细杆,而不是只在自然场景里肉眼找成功样例。可在标定工作区放置不同直径、深度和朝向的杆,保存校正后的左图标注 ,并用测量模型或更可靠传感器提供杆上真值视差 。标注只覆盖左相机实际可见区域,并排除视差搜索无效 ROI,避免把不可观测像素算成算法失败。
首先定义只看有效性的空洞率:
再定义带视差正确性约束的细杆召回:
两者不能互相替代。关闭所有过滤通常降低空洞率,却可能让背景视差覆盖杆;这时 好看, 仍然很差。还应在杆外侧设置窄背景带 ,统计被错误赋成杆视差的边界泄漏率,防止通过增大 把杆黏入背景来「提升召回」。
对细长目标,仅有像素召回还不够。沿杆中心线按弧长采样,计算最长连续正确段占可见长度的比例:
避障和姿态估计往往需要连贯结构;相同的百分之七十召回,均匀散落的点与一段连续杆具有不同下游价值。把结果按投影宽度一到二、三到四、五像素以上分桶,再按杆与背景视差间隔分桶,才能看出阈值在哪个尺度开始失效。报告全场景一个平均数会被大平面淹没。
实验矩阵至少包含四条可归因链路:原始匹配;仅左右一致性;仅独立散斑过滤;左右一致性加置信度与散斑证据。每条链固定上游参数,扫描相同的 、有效 ROI 和标注。对每个阶段输出 、、、背景泄漏率和全图错误分量数量,才有资格讨论哪一种过滤「更好」。
12. 从现象回到可执行判断
看到细杆空洞时,可以按删除掩膜快速归因。原始视差已经缺失,先查校正残差、曝光同步、块尺寸、唯一性和搜索范围;原始视差存在而 removed_by_speckle 命中,查看各杆段面积是否落在 以下、断点处相邻视差差是否越过 ;主要由 lr_fail 命中,则检查右视差符号、亚像素采样和遮挡区;置信度独自拒绝,则检查分数是否经过场景校准。
参数最终应由最小目标而非背景观感决定。给定工作距离内最细杆的投影宽度和可连续匹配长度,先估算其连通面积下界,再让 与该下界保留安全间隔;随后用背景错误分量分布检查过滤能力是否足够。若两类面积高度重叠,说明单纯面积规则不可分,应增加左右、置信度、形状或时序证据,而不是继续把阈值调到小数点后的幻觉精度。
上线门槛也应写成场景指标:各投影宽度分桶的细杆召回不得低于基线容限,空洞率不得越界,最长连续段满足下游最小长度,同时背景泄漏和错误点数量不能因保杆策略失控。保存原始定点视差、版本、全部参数和阶段掩膜,任何一次 OpenCV 升级都重放同一组样本。
散斑滤波本身没有做错事;它忠实执行了「小且局部连续的视差岛可以丢弃」这一先验。真正的错误,是把该先验误当成普适噪声定义。细杆场景把这种假设暴露得最清楚:连通域面积只能回答区域有多大,左右一致性回答几何是否闭环,置信度回答匹配证据有多强。把三类证据分开记录,再用召回、空洞和连续长度共同验收,才能在点云变干净时确认真实结构没有一起被擦掉。
相关
也可以看看
johan's blog