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 是滤波后的数据。
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 知识体系总览
下面这张图帮你理清均值滤波的核心逻辑:
3.7 写在最后
均值滤波虽然简单,但它是你理解更复杂滤波算法的基础。我建议你动手把上面的代码跑一遍,换几个不同的窗口大小看看效果。数据可以用串口打印出来,放到 Excel 里画个图,一目了然。
嗯,下一章我们会聊中值滤波。那个东西对付脉冲噪声特别管用,跟均值滤波正好互补。
公众号:蓝海资料掘金营,微信deep3321