3. 均值滤波:原理与实现,滑动窗口均值滤波

聊到传感器滤波,均值滤波绝对是最基础、最直观的一种。说白了,就是把连续采到的几个数据加起来,再除以个数,拿这个平均值去用。我刚开始做嵌入式开发那会儿,第一个接触的滤波算法就是它。

你可能会想:「就这么简单?」嗯,确实简单。但简单不代表没用。很多场景下,均值滤波能帮你解决大问题。

3.1 均值滤波的核心思想

假设你从传感器读到的数据是这样的:

原始数据:23, 25, 24, 26, 22, 100, 23, 24, 25

看到那个 100 了吗?明显是个噪声,可能是瞬间干扰。如果我们取最近 5 个数据做平均:

第5个点:(23+25+24+26+22)/5 = 24.0
第6个点:(25+24+26+22+100)/5 = 39.4  ← 被干扰了
第7个点:(24+26+22+100+23)/5 = 39.0  ← 还在受影响
...

你会发现,均值滤波能压制噪声,但也会把噪声「拖」进结果里。这就是它的代价。

核心公式:
y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N
其中 N 是窗口大小,x 是原始数据,y 是滤波后的数据。

3.2 滑动窗口均值滤波

实际工程中,我们不会每次都重新算一遍所有数据。那样太慢了,尤其是窗口大了以后。我习惯用「滑动窗口」的方式——新数据进来,旧数据出去,窗口一直在往前滑。

举个例子,窗口大小 N=3:

原始数据:  [10, 12, 11, 13, 15, 14]
窗口滑动过程:
  第1次:10, 12, 11 → 平均 = 11.0
  第2次:12, 11, 13 → 平均 = 12.0
  第3次:11, 13, 15 → 平均 = 13.0
  第4次:13, 15, 14 → 平均 = 14.0

你看,每次只移出去一个旧数据,移进来一个新数据。计算量小了很多。

3.3 代码实现:从零搭建

下面是我个人比较喜欢的一种实现方式。用环形缓冲区,避免数据搬移,效率很高。

#define WINDOW_SIZE  5

typedef struct {
    float buffer[WINDOW_SIZE];
    int index;      // 当前写入位置
    int count;      // 已存数据个数
    float sum;      // 窗口内数据总和
} MovingAverageFilter;

void MovingAverage_Init(MovingAverageFilter *f) {
    f->index = 0;
    f->count = 0;
    f->sum = 0.0f;
    for (int i = 0; i < WINDOW_SIZE; i++) {
        f->buffer[i] = 0.0f;
    }
}

float MovingAverage_Update(MovingAverageFilter *f, float new_data) {
    // 如果窗口还没满,直接加
    if (f->count < WINDOW_SIZE) {
        f->sum += new_data;
        f->buffer[f->index] = new_data;
        f->index = (f->index + 1) % WINDOW_SIZE;
        f->count++;
        return f->sum / f->count;
    }

    // 窗口满了,用新数据替换最旧的数据
    float oldest = f->buffer[f->index];
    f->sum = f->sum - oldest + new_data;
    f->buffer[f->index] = new_data;
    f->index = (f->index + 1) % WINDOW_SIZE;

    return f->sum / WINDOW_SIZE;
}
我的经验:用环形缓冲区 + 累加和的方式,每次更新只需要做一次减法、一次加法、一次除法。比每次都重新累加所有数据快得多。在 STM32F103 上测过,窗口大小 10 的时候,每次更新耗时不到 1 微秒。

3.4 窗口大小怎么选?

这个问题我经常被问到。窗口大小 N 决定了滤波效果和响应速度的平衡。

窗口大小 平滑效果 响应速度 适用场景
N=3~5 较弱 快速变化的信号,如按键消抖
N=8~16 中等 中等 温度、湿度等缓慢变化信号
N=32~64 很强 需要极度平滑,不关心延迟
注意:窗口越大,延迟越大。如果你做的是实时控制系统,比如四轴飞行器的姿态解算,窗口太大可能会导致系统响应跟不上。我曾经在一个项目中把窗口设到 20,结果飞控反应慢半拍,差点炸机。

3.5 均值滤波的局限性

说实话,均值滤波不是万能的。它有几个明显的短板:

  • 对脉冲噪声敏感:一个大的毛刺会污染整个窗口内的数据
  • 无法消除周期性噪声:如果噪声频率和采样频率有特定关系,均值滤波效果很差
  • 边缘效应:刚开始滤波时,窗口没填满,输出值会偏小

那怎么办?我一般会在均值滤波之前先做个简单的限幅滤波,把明显离谱的数据先干掉。比如这样:

float data = read_sensor();
if (abs(data - last_valid) > MAX_CHANGE) {
    data = last_valid;  // 丢弃突变
}
last_valid = data;
output = MovingAverage_Update(&filter, data);

这样组合使用,效果会好很多。

3.6 知识体系总览

下面这张图帮你理清均值滤波的核心逻辑:

均值滤波知识体系 原始传感器数据 滑动窗口均值滤波 环形缓冲区 + 累加和更新 平滑后的数据 窗口大小 N 决定平滑程度 响应延迟 N越大延迟越大 计算效率 O(1) 常数时间 局限性 • 对脉冲噪声敏感 • 无法消除周期性噪声 • 存在边缘效应 改进方案 • 限幅滤波 + 均值滤波 • 加权均值滤波 • 中值滤波(下一章)

3.7 写在最后

均值滤波虽然简单,但它是你理解更复杂滤波算法的基础。我建议你动手把上面的代码跑一遍,换几个不同的窗口大小看看效果。数据可以用串口打印出来,放到 Excel 里画个图,一目了然。

嗯,下一章我们会聊中值滤波。那个东西对付脉冲噪声特别管用,跟均值滤波正好互补。


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