4、深度图预处理:多帧平均降噪、双边滤波、时域滤波、飞点去除
各位同学,咱们今天聊点实在的。深度图拿到手,别急着往重建算法里扔。我见过太多人,原始数据直接上,结果重建出来一堆坑坑洼洼,还以为是算法不行。其实啊,预处理这一步做好了,后面能省80%的麻烦。
ToF相机出来的深度图,说白了就是一堆带噪声的测量值。有随机噪声、有飞点、有边缘拖影。咱们今天就把这些脏东西一个个清掉。
4.1 多帧平均降噪:最朴素的暴力美学
多帧平均,听起来很土对吧?但效果是真的稳。我早期做项目时,为了省时间只取单帧,结果重建出来的模型跟月球表面似的。后来老老实实做了多帧平均,世界瞬间清净了。
原理很简单:对同一场景连续采集N帧深度图,每个像素取均值。
def multi_frame_average(depth_frames):
"""
depth_frames: list of depth maps, shape (N, H, W)
returns: averaged depth map, shape (H, W)
"""
# 堆叠后取中位数,比均值更抗噪
stacked = np.stack(depth_frames, axis=0)
median_map = np.median(stacked, axis=0)
return median_map
这里有个坑:如果场景里有运动物体,多帧平均会把运动区域搞糊。怎么办?加个运动检测,只对静态区域做平均。嗯,这个后面讲时域滤波时会提到。
4.2 双边滤波:保边去噪的利器
普通高斯滤波会把边缘也磨平了。你想想看,深度图里边缘就是物体的轮廓,磨平了重建出来就是一坨。双边滤波就是来解决这个问题的。
它的核心思想:不仅看空间距离,还看像素值差异。空间上离得近的像素权重高,深度值相近的像素权重也高。这样边缘两侧的像素不会互相污染。
def bilateral_filter(depth_map, d=5, sigma_color=0.1, sigma_space=5):
"""
双边滤波实现
d: 窗口直径
sigma_color: 深度值差异的方差
sigma_space: 空间距离的方差
"""
filtered = cv2.bilateralFilter(
depth_map.astype(np.float32),
d,
sigma_color,
sigma_space
)
return filtered
我记得有一次做室内重建,墙面和家具的深度差只有几厘米。双边滤波参数没调好,直接把墙面和桌子糊一起了。后来把sigma_color压到0.03,边缘才清晰起来。
4.3 时域滤波:让深度图不再闪烁
多帧平均是空间上的,时域滤波是时间上的。说白了就是让每一帧的深度值,跟历史帧做个加权平均。这样深度图就不会一帧一个样,看着跟鬼片似的。
最常用的是指数移动平均(EMA):
class TemporalFilter:
def __init__(self, alpha=0.3):
self.alpha = alpha
self.prev_depth = None
def update(self, depth_map):
if self.prev_depth is None:
self.prev_depth = depth_map.copy()
return depth_map
# 指数加权平均
filtered = self.alpha * depth_map + (1 - self.alpha) * self.prev_depth
self.prev_depth = filtered.copy()
return filtered
为什么会拖影?你想想看,如果物体快速移动,历史帧的深度值还留在原地,当前帧已经跑到新位置了。加权平均后,旧位置的深度值会残留几帧。这就是拖影。
我曾经做过一个项目,机械臂抓取,深度图必须实时响应。alpha设到0.6才勉强跟上,但噪声又大了。后来加了个运动检测:运动快的区域alpha调高,静态区域alpha调低。效果不错。
4.4 飞点去除:干掉那些离谱的噪声
飞点(Flying Pixel)是ToF相机的老大难问题。说白了就是深度值突然跳变到离谱的值。比如墙面深度1米,突然冒出一个0.5米的点,或者3米的点。这些点就是飞点。
飞点产生的原因:边缘处红外光反射混乱,或者多路径干扰。嗯,这里不展开讲物理原理,咱们直接说怎么干掉它。
方法一:基于邻域一致性
检查每个像素,看它和周围8邻域的深度值是否一致。如果差异太大,就标记为飞点。
def remove_flying_pixels(depth_map, threshold=0.1):
"""
基于邻域一致性的飞点去除
threshold: 深度差异阈值(米)
"""
# 计算每个像素与邻域均值的差异
kernel = np.ones((3, 3), np.float32) / 9
local_mean = cv2.filter2D(depth_map, -1, kernel)
diff = np.abs(depth_map - local_mean)
# 标记飞点并置为无效值
mask = diff > threshold
depth_map_clean = depth_map.copy()
depth_map_clean[mask] = 0 # 或者 np.nan
return depth_map_clean, mask
方法二:基于边缘检测
飞点往往出现在深度边缘附近。先检测边缘,然后在边缘区域做更严格的检查。
def edge_aware_flying_pixel_removal(depth_map, edge_thresh=0.2, flying_thresh=0.3):
# 计算深度梯度
grad_x = cv2.Sobel(depth_map, cv2.CV_32F, 1, 0, ksize=3)
grad_y = cv2.Sobel(depth_map, cv2.CV_32F, 0, 1, ksize=3)
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
# 边缘区域
edge_mask = grad_mag > edge_thresh
# 在边缘区域做更严格的飞点检测
local_median = cv2.medianBlur(depth_map, 5)
diff = np.abs(depth_map - local_median)
# 边缘区域阈值更宽松,非边缘区域更严格
adaptive_thresh = np.where(edge_mask, flying_thresh * 1.5, flying_thresh)
flying_mask = diff > adaptive_thresh
depth_map_clean = depth_map.copy()
depth_map_clean[flying_mask] = 0
return depth_map_clean
4.5 预处理流程总结
说了这么多,咱们捋一下完整的预处理流程。我一般按这个顺序来:
- 多帧中位数平均:干掉随机噪声,保留边缘
- 飞点去除:先干掉离谱的异常值,免得后面滤波时污染邻域
- 双边滤波:进一步平滑,同时保护边缘
- 时域滤波:让深度图在时间上稳定,不闪烁
这个顺序是我踩过坑之后总结的。你想想看,如果先做时域滤波,飞点会残留在历史帧里,后面怎么都去不掉。所以飞点去除一定要放在前面。
最后说一句:预处理没有银弹。不同的ToF相机、不同的场景,参数都得调。我建议你准备一套标定数据,把各种参数组合跑一遍,看看效果。嗯,这个习惯帮我省了不少返工的麻烦。