使用RGB-D相机或者双目立体匹配得到的深度图,经常会出现边缘锯齿明显、深度值跳变剧烈的问题。原本应该平滑过渡的物体轮廓,在深度图上却像楼梯一样一格一格的,直接拿去做点云重建或者背景虚化,出来的效果就会大打折扣。这篇文章就来聊聊深度图锯齿是怎么产生的,以及如何选择合适的平滑滤波和Blur算法来处理它。

深度图锯齿是怎么产生的
首先要理解深度图和普通彩色图的本质区别。彩色图的像素值反映的是物体表面的颜色信息,相邻像素之间通常有较强的连续性;而深度图的像素值代表物体到相机的距离,是一个物理测量值。当画面中存在前景和背景的遮挡关系时,深度值会在物体边缘处发生突兀的跳变,前景可能是2米,背景一步跨到5米。
这种跳变本身是正常的物理现象,但问题在于深度传感器的测量精度有限。无论是结构光方案还是ToF方案,在物体边缘处都会受到多路径反射、阴影、半透明物体等因素干扰,导致边缘附近的深度值出现严重失真。再加上立体匹配算法在弱纹理区域的视差计算不稳定,最终得到的深度图在边缘处就呈现出锯齿状的不规则毛刺。
p>另外,量化误差也是不可忽视的因素。很多深度图以16位整数存储,单位是毫米。当物体表面与相机光线呈较大倾角时,相邻像素的真实深度差可能小于1毫米,量化之后就会形成台阶状的伪轮廓,看起来也像锯齿。常见平滑滤波方案对比
针对深度图的锯齿问题,最常见的处理思路就是滤波。但不同的滤波算法特性差异很大,选错了不仅去不掉锯齿,还可能把本来正确的深度边缘也抹平了。下面逐个分析几种主流方案。
高斯滤波
高斯滤波是最经典的Blur算法,它用一个二维高斯核与图像做卷积,距离中心越近的像素权重越大。它的优点是实现简单、速度快,对随机噪声的抑制效果很好。可以用下面这段OpenCV代码快速验证:
import cv2
import numpy as np
# 读取16位深度图,单位为毫米
depth = cv2.imread("depth.png", cv2.IMREAD_ANYDEPTH)
# 先转成浮点类型再滤波,避免整数截断误差
depth_f = depth.astype(np.float32)
# 高斯滤波,核大小必须是正奇数,标准差控制平滑强度
smoothed = cv2.GaussianBlur(depth_f, (5, 5), 1.5)
# 转回16位整型保存
result = smoothed.astype(np.uint16)
cv2.imwrite("depth_gauss.png", result)
高斯滤波的致命弱点是各向同性,它不区分物体内部的小噪点和物体边缘的深度跳变,一律平均处理。核开得小了锯齿去不干净,核开大了物体轮廓就会被严重侵蚀,前景和背景的深度值在边缘处互相渗透,出现所谓的深度混叠。所以高斯滤波比较适合锯齿轻微、对边缘精度要求不高的场景,比如仅用于可视化预览。
中值滤波
中值滤波用邻域像素的中值代替中心像素值,对孤立的黑点、白点这类脉冲噪声效果特别好。深度图中经常出现的零值空洞和飞点,用中值滤波处理往往比高斯更有效:
import cv2
import numpy as np
depth = cv2.imread("depth.png", cv2.IMREAD_ANYDEPTH).astype(np.float32)
# 中值滤波,ksize为5表示5x5邻域
median = cv2.medianBlur(depth, 5)
cv2.imwrite("depth_median.png", median.astype(np.uint16))
中值滤波的优势在于能保住阶跃边缘,因为它取的是中值而不是均值,只要邻域内大多数像素属于同一侧深度,边缘位置就不会被明显移动。它的缺点是对连续的锯齿状伪轮廓改善有限,而且核大小增大后计算量上升明显。实践中常用3x3或5x5的小核做一到两次迭代,效果比一次大核更好。
双边滤波
双边滤波可以理解为加强了边缘保护能力的高斯滤波。它在计算权重时同时考虑空间距离和像素值差异:空间上离得近的像素权重大,同时深度值相近的像素权重大。这样在物体边缘两侧,因为深度值差异巨大,互相之间的权重接近于零,滤波只在同侧像素内部进行,边缘得以保留。
import cv2
import numpy as np
depth = cv2.imread("depth.png", cv2.IMREAD_ANYDEPTH).astype(np.float32)
# d为邻域直径,sigmaColor为值域标准差,sigmaSpace为空域标准差
bilateral = cv2.bilateralFilter(depth, d=9, sigmaColor=75, sigmaSpace=75)
cv2.imwrite("depth_bilateral.png", bilateral.astype(np.uint16))
这里的关键参数是sigmaColor,它决定了多大的深度差异会被视为边缘。如果深度以毫米为单位,前景背景差3000毫米,那么sigmaColor设成75意味着只有深度差在几十毫米以内的像素才互相融合,边缘自然得到了保护。需要注意的是,如果深度值范围很大,sigmaColor也要相应放大,否则滤波几乎不起作用。双边滤波的速度比高斯慢不少,对实时性要求高的应用要评估性能开销。
引导滤波
引导滤波是更现代的边缘保持滤波算法,它以一张引导图为基础对输入图做滤波。深度图处理的典型用法是用对齐的彩色图作为引导,利用彩色图像中清晰的物体边缘来指导深度图的平滑,效果往往比双边滤波更稳定,而且计算复杂度与核大小无关:
import cv2
import numpy as np
depth = cv2.imread("depth.png", cv2.IMREAD_ANYDEPTH).astype(np.float32) / 1000.0
guide = cv2.imread("color.png", cv2.COLOR_BGR2GRAY) / 255.0
guide = cv2.imread("color.png")[:, :, 0] / 255.0
# eps越大平滑越强,边缘保持能力越弱
filtered = cv2.ximgproc.jointBilateralFilter(
guide, depth, d=9, sigmaColor=0.1, sigmaSpace=75
)
# 也可以使用引导滤波
filtered = cv2.ximgproc.guidedFilter(
guide, depth, radius=8, eps=1e-4
)
cv2.imwrite("depth_guided.png", (filtered * 1000).astype(np.uint16))
引导滤波的前提是彩色图和深度图已经做好了配准对齐,如果两者存在几像素的偏差,边缘反而会出现彩色图边缘与深度边缘不一致的伪影。使用前建议先做一次配准或者裁剪掉边缘区域。
参数调整与方案选择建议
选滤波算法不能脱离具体应用。如果你的下游任务是三维重建、SLAM这类对几何精度敏感的场景,建议优先用中值滤波去除飞点和空洞,再配合双边滤波或引导滤波做轻度平滑,全程避免大核高斯。如果只是做人像背景虚化,边缘轻微的深度渗透反而有助于虚化过渡自然,这种情况下高斯滤波加一次形态学闭运算填充空洞就够用了。
参数调整上有一个通用技巧:先从小核小标准差开始,逐步增大直到锯齿消失,一旦发现物体轮廓开始模糊就回退一档。还可以把处理后的深度图和彩色图叠加显示,肉眼检查边缘是否有渗透现象。另外强烈建议在滤波前先把深度图转成float32类型处理,直接对uint16整型滤波会引入截断误差,让深度精度白白损失。
最后提一个进阶方案:联合双边上采样。如果原始深度图分辨率低于彩色图,与其先上采样再滤波,不如用高分辨率彩色图作为引导,直接对低分辨率深度图做上采样加滤波,一步到位同时解决分辨率低和锯齿两个问题。OpenCV的ximgproc模块和很多深度学习框架都提供了现成实现,值得在实际项目中尝试。