4. 空间域滤波:均值滤波、高斯滤波、中值滤波的原理与实现

空间域滤波,说白了就是直接在图像像素上做操作。你想想看,ToF传感器拿到的深度图,每个像素都是一个距离值。这些值里难免有噪声——有的是传感器本身的电子噪声,有的是多路径反射造成的异常点。

我刚开始做ToF深度图处理时,第一反应就是:能不能像处理普通图像那样,直接对深度图做平滑?后来发现,嗯,可以,但得小心。深度图跟普通RGB图不一样,它的边缘往往对应物体的真实边界,一旦模糊了,后续的三维重建就全乱套了。

今天咱们就聊三种最基础的空间域滤波方法:均值滤波、高斯滤波、中值滤波。这三种方法,我几乎在每个ToF项目里都会用到,但用法完全不同。

4.1 均值滤波:简单但粗暴

均值滤波的原理,说白了就是取一个窗口内所有像素的平均值,用这个平均值替代中心像素的值。

公式很简单:

g(x,y) = (1/(m×n)) × Σ f(i,j)

其中m×n是窗口大小,f(i,j)是窗口内的像素值。

我在项目中遇到过一个问题:用3×3的均值滤波去处理ToF深度图,结果物体边缘全糊了。为什么?因为均值滤波对窗口内所有像素一视同仁,边缘两侧的深度值差异很大,一平均就把边缘抹平了。

注意:均值滤波会严重破坏深度图的边缘信息。在ToF应用中,除非你只关心大块平坦区域,否则慎用。

代码实现也很直接:

// 均值滤波实现
void meanFilter(float* depthMap, int width, int height, int kernelSize) {
    float* temp = new float[width * height];
    int half = kernelSize / 2;
    float scale = 1.0f / (kernelSize * kernelSize);
    
    for (int y = half; y < height - half; y++) {
        for (int x = half; x < width - half; x++) {
            float sum = 0.0f;
            for (int ky = -half; ky <= half; ky++) {
                for (int kx = -half; kx <= half; kx++) {
                    sum += depthMap[(y + ky) * width + (x + kx)];
                }
            }
            temp[y * width + x] = sum * scale;
        }
    }
    // 复制回原图
    memcpy(depthMap, temp, width * height * sizeof(float));
    delete[] temp;
}

4.2 高斯滤波:加权平均的艺术

高斯滤波比均值滤波聪明一点。它给窗口内的像素分配不同的权重——离中心越近,权重越大;越远,权重越小。权重由高斯函数决定。

二维高斯函数:

G(x,y) = (1/(2πσ²)) × exp(-(x²+y²)/(2σ²))

我个人习惯用σ来控制平滑程度。σ越大,高斯分布越平坦,平滑效果越强;σ越小,越集中在中心点,平滑效果越弱。

我记得有一次处理一个室内场景的ToF数据,墙面上的深度噪声特别明显。用均值滤波试了试,墙面是平滑了,但墙角线全没了。换成高斯滤波,σ选1.5,效果就好很多——墙面噪声被抑制了,墙角线还保留着。

经验之谈:ToF深度图的高斯滤波,σ建议取1.0~2.0之间。太小了没效果,太大了边缘也保不住。

高斯核的计算:

// 生成高斯核
void generateGaussianKernel(float* kernel, int kernelSize, float sigma) {
    int half = kernelSize / 2;
    float sum = 0.0f;
    
    for (int y = -half; y <= half; y++) {
        for (int x = -half; x <= half; x++) {
            float value = exp(-(x*x + y*y) / (2 * sigma * sigma));
            kernel[(y + half) * kernelSize + (x + half)] = value;
            sum += value;
        }
    }
    
    // 归一化
    for (int i = 0; i < kernelSize * kernelSize; i++) {
        kernel[i] /= sum;
    }
}

4.3 中值滤波:边缘保护的利器

中值滤波的思路完全不同。它不取平均,而是取窗口内所有像素值的中位数。

为什么中值滤波能保护边缘?你想想看,如果窗口跨越了物体边缘,一侧是近处的深度值(比如1米),另一侧是远处的深度值(比如5米)。均值滤波会把它们平均成3米,边缘就模糊了。但中值滤波会取中间的那个值——要么是1米附近的,要么是5米附近的,不会产生中间值。

中值滤波还有一个绝活:去除椒盐噪声。ToF深度图里经常会出现一些孤立的异常点(比如因为反射率太低导致的无效点),中值滤波能干净利落地把它们去掉。

核心优势:中值滤波在去除噪声的同时,能很好地保持边缘锐度。这是ToF深度图处理中最常用的滤波方法之一。

代码实现:

// 中值滤波实现
void medianFilter(float* depthMap, int width, int height, int kernelSize) {
    float* temp = new float[width * height];
    int half = kernelSize / 2;
    int windowSize = kernelSize * kernelSize;
    float* window = new float[windowSize];
    
    for (int y = half; y < height - half; y++) {
        for (int x = half; x < width - half; x++) {
            int idx = 0;
            for (int ky = -half; ky <= half; ky++) {
                for (int kx = -half; kx <= half; kx++) {
                    window[idx++] = depthMap[(y + ky) * width + (x + kx)];
                }
            }
            // 排序取中值
            std::sort(window, window + windowSize);
            temp[y * width + x] = window[windowSize / 2];
        }
    }
    
    memcpy(depthMap, temp, width * height * sizeof(float));
    delete[] window;
    delete[] temp;
}

4.4 三种滤波方法的对比

我把这三种方法放在一起对比一下,方便你根据实际场景选择:

方法 噪声抑制 边缘保持 计算速度 适用场景
均值滤波 一般 平坦区域、预处理
高斯滤波 较好 中等 中等 一般平滑、噪声均匀
中值滤波 好(尤其椒盐噪声) 慢(需排序) 边缘保护、异常点去除

我曾经在一个ToF手势识别项目里,深度图上有大量因为手指边缘反射产生的异常点。用高斯滤波试了,效果不理想——异常点还在,只是被模糊了。换成中值滤波,3×3窗口,一遍下来,异常点基本消失,手指轮廓还很清楚。

避坑指南:中值滤波的窗口大小不是越大越好。5×5以上会开始模糊细节,而且计算量成倍增加。我一般先用3×3试试,效果不够再上5×5。

4.5 本章知识体系

下面这张图展示了三种空间域滤波方法的核心逻辑和关系:

空间域滤波方法对比 ToF深度图 均值滤波 简单平均,边缘模糊 高斯滤波 加权平均,边缘中等 中值滤波 取中位数,边缘保持好 平滑但边缘模糊 平滑且边缘较好 去异常点且边缘清晰 选择建议:边缘重要→中值滤波;噪声均匀→高斯滤波;快速预览→均值滤波

嗯,这三种方法各有各的脾气。均值滤波最简单,但副作用也最大;高斯滤波是个折中方案,适合噪声比较均匀的场景;中值滤波虽然计算慢一点,但在ToF深度图上效果最好,尤其是处理那些孤立的异常点。

我个人建议,刚开始做ToF深度图处理时,先从中值滤波入手。等你对数据的特点摸透了,再根据实际情况选择或者组合使用不同的滤波方法。


专注资料整理