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 中值滤波的流程图

下面这张图,是我自己画的中值滤波处理流程。你看一遍就能明白整个逻辑:

开始 读取传感器数据 构建大小为N的窗口 对窗口内数据排序 取中位数作为输出 结束

你看,整个流程其实很简单:读数据 → 建窗口 → 排序 → 取中位数。但就是这简单的几步,能把椒盐噪声治得服服帖帖。

4.6 实际项目中的经验

我记得有一次做工业现场的压力传感器采集,现场环境特别恶劣,电磁干扰严重。数据里时不时就蹦出几个离谱的值,用均值滤波根本压不住。后来我换成了中值滤波,窗口大小设成5,效果立竿见影——那些毛刺全没了,而且正常信号几乎没有失真。

不过我也踩过坑。有一次我把窗口大小设成了9,结果发现数据延迟太大了,系统响应跟不上。后来我改成窗口大小为3,虽然滤波效果稍微弱了一点,但响应速度上来了。所以,窗口大小不是越大越好,得根据实际需求来权衡

我的建议:如果你不确定窗口大小该设多少,先从3开始试。如果噪声还有残留,再慢慢加大。别一上来就设个7或9,容易翻车。

好了,中值滤波的原理和实现就讲到这里。你回去可以自己写个demo试试,用一组带椒盐噪声的数据跑一下,看看效果。相信我,当你看到那些毛刺被一个个“拔掉”的时候,你会爱上这个算法的。


公众号:蓝海资料掘金营,微信deep3321