4. 空间域滤波:均值滤波、高斯滤波、中值滤波的原理与实现
空间域滤波,说白了就是直接在图像像素上做操作。你想想看,ToF传感器拿到的深度图,每个像素都是一个距离值。这些值里难免有噪声——有的是传感器本身的电子噪声,有的是多路径反射造成的异常点。
我刚开始做ToF深度图处理时,第一反应就是:能不能像处理普通图像那样,直接对深度图做平滑?后来发现,嗯,可以,但得小心。深度图跟普通RGB图不一样,它的边缘往往对应物体的真实边界,一旦模糊了,后续的三维重建就全乱套了。
今天咱们就聊三种最基础的空间域滤波方法:均值滤波、高斯滤波、中值滤波。这三种方法,我几乎在每个ToF项目里都会用到,但用法完全不同。
4.1 均值滤波:简单但粗暴
均值滤波的原理,说白了就是取一个窗口内所有像素的平均值,用这个平均值替代中心像素的值。
公式很简单:
g(x,y) = (1/(m×n)) × Σ f(i,j)
其中m×n是窗口大小,f(i,j)是窗口内的像素值。
我在项目中遇到过一个问题:用3×3的均值滤波去处理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,效果就好很多——墙面噪声被抑制了,墙角线还保留着。
高斯核的计算:
// 生成高斯核
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深度图里经常会出现一些孤立的异常点(比如因为反射率太低导致的无效点),中值滤波能干净利落地把它们去掉。
代码实现:
// 中值滤波实现
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窗口,一遍下来,异常点基本消失,手指轮廓还很清楚。
4.5 本章知识体系
下面这张图展示了三种空间域滤波方法的核心逻辑和关系:
嗯,这三种方法各有各的脾气。均值滤波最简单,但副作用也最大;高斯滤波是个折中方案,适合噪声比较均匀的场景;中值滤波虽然计算慢一点,但在ToF深度图上效果最好,尤其是处理那些孤立的异常点。
我个人建议,刚开始做ToF深度图处理时,先从中值滤波入手。等你对数据的特点摸透了,再根据实际情况选择或者组合使用不同的滤波方法。