第3章:时域滤波基础
各位同学,今天我们来聊聊时域滤波。说实话,这是我最喜欢讲的一章。为什么?因为时域滤波太直观了——你不需要懂傅里叶变换,不需要理解频域概念,直接在时间轴上对数据动手脚就行。
我刚开始做嵌入式开发那会儿,第一个项目就是处理风速传感器的信号。传感器装在楼顶,风一吹,数据跳得跟心电图似的。那时候我还不懂滤波,直接把原始数据送进控制器,结果系统一会儿开一会儿关,跟抽风一样。后来师傅告诉我:「你先做做平滑处理。」嗯,从那以后,我就跟时域滤波结下了不解之缘。
核心思想:时域滤波就是利用当前时刻及其前后时刻的采样值,通过某种数学运算,得到更平滑、更可靠的估计值。
3.1 滑动平均滤波
滑动平均,说白了就是取最近N个点的平均值。这是最基础、最常用的滤波方法。
数学表达式:
y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N
其中N是窗口大小。N越大,平滑效果越好,但延迟也越大。
代码实现(C语言):
#define WINDOW_SIZE 5
float buffer[WINDOW_SIZE] = {0};
int index = 0;
float sum = 0;
float moving_average(float new_sample) {
sum -= buffer[index];
buffer[index] = new_sample;
sum += new_sample;
index = (index + 1) % WINDOW_SIZE;
return sum / WINDOW_SIZE;
}
我的经验:窗口大小选3~7比较合适。我在处理风向标信号时,一般用5点滑动平均。太小了滤不干净,太大了反应太慢。你想想看,风向突然变了,你还在用10秒前的数据,那还叫实时吗?
3.2 中值滤波
中值滤波对付「野点」特别有效。什么是野点?就是那种突然跳得很离谱的数据。比如风速传感器被鸟撞了一下,瞬间输出100m/s——这明显是假的。
中值滤波的做法:取窗口内的所有数据,排序,取中间那个值。
#define WINDOW_SIZE 5
float median_filter(float *window) {
// 排序(这里用简单的冒泡)
float temp[WINDOW_SIZE];
for(int i = 0; i < WINDOW_SIZE; i++) temp[i] = window[i];
for(int i = 0; i < WINDOW_SIZE-1; i++) {
for(int j = 0; j < WINDOW_SIZE-1-i; j++) {
if(temp[j] > temp[j+1]) {
float t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
return temp[WINDOW_SIZE/2];
}
注意:中值滤波会丢失细节。如果你需要保留信号的快速变化特征,慎用。我曾经在一个项目中同时用了滑动平均和中值滤波,结果信号被「磨」得太平了,连风向突变都看不出来。
3.3 限幅滤波
限幅滤波的思路更简单:如果当前采样值和上一个有效值相差太大,就丢弃它。
#define MAX_DELTA 10.0f
float last_valid = 0;
float limit_filter(float new_sample) {
if(fabs(new_sample - last_valid) > MAX_DELTA) {
return last_valid; // 丢弃,保持上次值
}
last_valid = new_sample;
return new_sample;
}
限幅滤波的阈值怎么设?我一般看信号的物理意义。比如风向变化,每秒变化超过30度就不合理。风速变化,每秒超过5m/s也值得怀疑。这个阈值需要根据你的实际场景来调。
3.4 防脉冲干扰滤波
这个其实是限幅滤波的升级版。它结合了中值滤波和限幅滤波的思想:先判断是不是脉冲干扰,如果是,就用中值替代。
#define THRESHOLD 15.0f
float anti_pulse_filter(float new_sample, float *window, int size) {
// 计算当前值与中值的偏差
float median = median_filter(window);
if(fabs(new_sample - median) > THRESHOLD) {
return median; // 认为是脉冲,用中值替代
}
return new_sample;
}
避坑指南:我曾经在风力发电项目中用过这个滤波器。一开始阈值设得太小,结果正常的风速变化也被当成脉冲滤掉了。后来我把阈值调到信号标准差的3倍,效果就好多了。记住:阈值不是拍脑袋定的,要基于实际数据的统计分析。
3.5 加权滑动平均
普通滑动平均给每个点相同的权重。但直觉告诉我们:越新的数据应该越重要。加权滑动平均就是干这个的。
数学表达式:
y[n] = (w0*x[n] + w1*x[n-1] + ... + wN-1*x[n-N+1]) / sum(w)
权重怎么选?我常用的几种方案:
| 权重方案 | 权重序列 | 特点 |
|---|---|---|
| 线性递减 | [3,2,1] | 简单,效果适中 |
| 指数衰减 | [0.5, 0.25, 0.125, ...] | 对最新数据敏感 |
| 三角权重 | [1,2,3,2,1] | 中间数据权重最大 |
#define N 5
const float weights[N] = {0.1, 0.15, 0.2, 0.25, 0.3}; // 越新权重越大
float weighted_moving_average(float *buffer) {
float sum = 0;
float w_sum = 0;
for(int i = 0; i < N; i++) {
sum += buffer[i] * weights[i];
w_sum += weights[i];
}
return sum / w_sum;
}
我的建议:加权滑动平均是「性价比」最高的滤波器。它实现简单,计算量小,效果却比普通滑动平均好很多。我在处理风向标信号时,90%的情况都用它。只有遇到特别严重的脉冲干扰时,才会加上中值滤波做预处理。
知识体系总览
下面这张图总结了五种时域滤波方法的核心逻辑和适用场景:
好了,这一章的内容就到这里。五种时域滤波方法,各有各的脾气。在实际项目中,我通常不会只用一种,而是组合使用。比如先限幅去掉明显的野点,再用加权滑动平均做平滑。记住:没有最好的滤波器,只有最合适的滤波器。
公众号:蓝海资料掘金营,微信deep3321