4. 中值滤波:原理与实现,对椒盐噪声的处理
各位同学,咱们今天聊聊中值滤波。
说实话,在传感器数据处理这块,椒盐噪声是最让人头疼的问题之一。你想想看,本来一个好好的信号,突然蹦出几个离谱的毛刺,像盐撒在画面上一样,看着就烦。我当年做超声波测距模块的时候,就吃过这玩意儿的亏——明明距离没变,数据却忽大忽小,差点让我怀疑传感器坏了。
后来我才意识到,对付这种噪声,均值滤波其实不太灵光。为什么?因为均值滤波会把噪声“平均”到整个窗口里,结果就是噪声虽然被削弱了,但正常信号也被拉偏了。那怎么办?中值滤波就是专门干这个的。
4.1 中值滤波的核心思想
中值滤波的原理,说白了就一句话:用窗口内所有数据的中间值,代替当前点的值。
举个例子。假设你有一个数据序列:
原始数据:[3, 5, 2, 100, 4, 6, 3]
这里面的100明显是个异常值(椒盐噪声)。如果我们用窗口大小为3的中值滤波:
- 处理第3个点(值为2)时,窗口覆盖 [5, 2, 100],排序后为 [2, 5, 100],中位数是5,所以输出5。
- 处理第4个点(值为100)时,窗口覆盖 [2, 100, 4],排序后为 [2, 4, 100],中位数是4,所以输出4。
- 处理第5个点(值为4)时,窗口覆盖 [100, 4, 6],排序后为 [4, 6, 100],中位数是6,所以输出6。
你看,那个100的毛刺,直接被替换成了4或6,完全被剔除了。而正常的数据点,比如3、5、2这些,变化并不大。这就是中值滤波的厉害之处——对异常值有天然的免疫力。
核心要点:中值滤波不依赖平均值,而是依赖排序后的中间值。所以它不怕单个或少数几个极端值。
4.2 中值滤波 vs 均值滤波
我经常被问到:什么时候用均值,什么时候用中值?
我的经验是这样的:
| 噪声类型 | 推荐滤波方式 | 原因 |
|---|---|---|
| 椒盐噪声(随机毛刺) | 中值滤波 | 毛刺被直接替换,不影响其他点 |
| 高斯噪声(随机小波动) | 均值滤波 | 平滑效果好,计算简单 |
| 混合噪声 | 先中值后均值 | 先剔除毛刺,再平滑波动 |
嗯,这里要注意:中值滤波也不是万能的。如果窗口里的异常值太多(比如超过一半),中值滤波也会失效。不过在实际项目中,椒盐噪声通常都是稀疏的,所以中值滤波的效果往往很好。
4.3 中值滤波的嵌入式实现
好了,理论说完了,咱们直接上代码。我个人习惯用C语言写嵌入式算法,因为移植性好,跑在单片机上也没问题。
// 中值滤波函数
// data: 输入数据数组
// size: 数据长度
// window_size: 窗口大小(必须是奇数)
void median_filter(float *data, int size, int window_size) {
if (window_size % 2 == 0) {
// 窗口大小必须是奇数,否则中位数不唯一
return;
}
int half_window = window_size / 2;
float *temp = (float *)malloc(window_size * sizeof(float));
float *output = (float *)malloc(size * sizeof(float));
for (int i = 0; i < size; i++) {
// 收集窗口内的数据
int count = 0;
for (int j = -half_window; j <= half_window; j++) {
int index = i + j;
// 边界处理:超出范围时用边界值填充
if (index < 0) index = 0;
if (index >= size) index = size - 1;
temp[count++] = data[index];
}
// 对窗口内数据排序(冒泡排序,简单够用)
for (int m = 0; m < window_size - 1; m++) {
for (int n = 0; n < window_size - 1 - m; n++) {
if (temp[n] > temp[n + 1]) {
float t = temp[n];
temp[n] = temp[n + 1];
temp[n + 1] = t;
}
}
}
// 取中位数
output[i] = temp[half_window];
}
// 将结果复制回原数组
for (int i = 0; i < size; i++) {
data[i] = output[i];
}
free(temp);
free(output);
}
小技巧:在实际项目中,如果数据量不大(比如窗口大小3或5),可以用插入排序代替冒泡排序。插入排序在数据基本有序时效率更高,而中值滤波的窗口数据往往变化不大。
4.4 避坑指南
我曾经在一个项目中,用中值滤波处理温度传感器数据。窗口大小设成了5,结果发现数据变得特别“迟钝”——明明温度已经变了,输出却要等好几拍才跟上。后来我才意识到,窗口越大,延迟越大。
所以,这里有几个坑你得注意:
- 窗口大小选择:窗口越大,滤波效果越强,但延迟也越大。一般建议窗口大小取3或5,最多7。再大就不太适合实时系统了。
- 边界处理:数据开头和结尾的窗口会超出范围。我习惯用边界值填充,也有人用镜像填充。效果差别不大,但边界值填充实现更简单。
- 排序算法选择:如果窗口大小固定且很小(比如3或5),直接用if-else硬编码排序,比任何通用排序都快。我写过窗口大小为3的中值滤波,只用3次比较就搞定了。
警告:千万不要在中断服务函数里直接跑中值滤波!尤其是排序部分,耗时不可控。我建议在中断里只做数据采集,滤波放在主循环或后台任务里处理。
4.5 中值滤波的流程图
下面这张图,是我自己画的中值滤波处理流程。你看一遍就能明白整个逻辑:
你看,整个流程其实很简单:读数据 → 建窗口 → 排序 → 取中位数。但就是这简单的几步,能把椒盐噪声治得服服帖帖。
4.6 实际项目中的经验
我记得有一次做工业现场的压力传感器采集,现场环境特别恶劣,电磁干扰严重。数据里时不时就蹦出几个离谱的值,用均值滤波根本压不住。后来我换成了中值滤波,窗口大小设成5,效果立竿见影——那些毛刺全没了,而且正常信号几乎没有失真。
不过我也踩过坑。有一次我把窗口大小设成了9,结果发现数据延迟太大了,系统响应跟不上。后来我改成窗口大小为3,虽然滤波效果稍微弱了一点,但响应速度上来了。所以,窗口大小不是越大越好,得根据实际需求来权衡。
我的建议:如果你不确定窗口大小该设多少,先从3开始试。如果噪声还有残留,再慢慢加大。别一上来就设个7或9,容易翻车。
好了,中值滤波的原理和实现就讲到这里。你回去可以自己写个demo试试,用一组带椒盐噪声的数据跑一下,看看效果。相信我,当你看到那些毛刺被一个个“拔掉”的时候,你会爱上这个算法的。
公众号:蓝海资料掘金营,微信deep3321