4、数据预处理之去噪:移动平均滤波、中值滤波、高斯滤波、卡尔曼滤波入门
各位同学,欢迎来到第四讲。
传感器数据,说白了就是一堆带毛刺的原始信号。你拿到的数据,十有八九是「脏」的。噪声、尖峰、漂移,什么妖魔鬼怪都有。我刚开始做嵌入式项目时,就吃过这个亏——传感器读数直接拿去用,结果控制逻辑乱跳,电机抖得像筛糠。
所以,去噪是数据预处理的第一道关。今天咱们就聊聊四种最常用的滤波方法:移动平均、中值、高斯,还有卡尔曼。别被名字吓到,其实都不难。
4.1 移动平均滤波:最简单,也最常用
移动平均滤波,思路很直白:取最近 N 个采样点的平均值,作为当前输出。
举个例子,你测温度,每秒读一次。最近5秒的读数是 [25.1, 25.3, 25.0, 25.2, 25.4],那当前输出就是它们的平均值 25.2。每来一个新数据,就踢掉最老的那个,重新算一遍。
y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N
代码实现也很简单:
// 移动平均滤波,窗口大小 N=5
#define N 5
float buffer[N] = {0};
int index = 0;
float moving_average(float new_sample) {
float sum = 0;
buffer[index] = new_sample;
index = (index + 1) % N;
for (int i = 0; i < N; i++) {
sum += buffer[i];
}
return sum / N;
}
嗯,这里要注意:窗口 N 越大,平滑效果越好,但延迟也越大。我做过一个手势识别项目,用了 N=10 的移动平均,结果手都挥完了,输出才跟上。后来改成 N=4,实时性就好了很多。
4.2 中值滤波:专治「野值」
移动平均有个致命弱点——它对所有数据一视同仁。如果某个采样点突然跳出一个离谱的值(比如温度传感器被手摸了一下),平均值会被严重拉偏。
中值滤波就是来治这个毛病的。它取窗口内所有数据的中位数,而不是平均值。你想想看,就算混进来一个 100 度的异常值,只要其他数据正常,中位数基本不受影响。
// 中值滤波,窗口大小 N=5
#define N 5
float buffer[N] = {0};
int index = 0;
float median_filter(float new_sample) {
float temp[N];
buffer[index] = new_sample;
index = (index + 1) % N;
// 拷贝并排序
for (int i = 0; i < N; i++) temp[i] = buffer[i];
// 冒泡排序(简单示意,实际可用更高效算法)
for (int i = 0; i < N-1; i++) {
for (int j = 0; j < N-i-1; j++) {
if (temp[j] > temp[j+1]) {
float t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
return temp[N/2]; // 取中位数
}
我曾经在一个超声波测距项目里,遇到强烈的电机干扰,每隔几十个点就蹦出一个 0 值。移动平均根本压不住,换成中值滤波后,数据瞬间干净了。说白了,中值滤波就是「去噪界的清道夫」。
4.3 高斯滤波:更平滑,更自然
移动平均给每个数据点相同的权重,这其实不太合理。离当前时刻越近的数据,应该越重要才对。高斯滤波就是基于这个想法——它用高斯函数给数据分配权重,越近的权重越大。
说白了,高斯滤波是「加权移动平均」的一种特殊形式。权重系数由高斯分布决定:
w[i] = exp(-i² / (2σ²))
其中 i 是距离当前点的偏移量,σ 控制平滑程度。
代码实现:
// 高斯滤波,窗口大小 N=5,sigma=1.0
#define N 5
float kernel[N] = {0.061, 0.242, 0.383, 0.242, 0.061}; // 预计算的高斯权重
float buffer[N] = {0};
int index = 0;
float gaussian_filter(float new_sample) {
buffer[index] = new_sample;
index = (index + 1) % N;
float result = 0;
int pos = index;
for (int i = 0; i < N; i++) {
result += buffer[pos] * kernel[i];
pos = (pos + 1) % N;
}
return result;
}
我个人的习惯是,在需要保留信号边缘特征时(比如加速度计检测步数),用高斯滤波比移动平均好很多。因为高斯滤波的权重分布更自然,不会把陡峭的上升沿磨得太平。
4.4 卡尔曼滤波入门:状态估计的「黄金标准」
前面三种滤波,本质上都是「数据平滑」。但卡尔曼滤波不一样——它做的是「状态估计」。什么意思呢?它不只考虑当前观测值,还结合了系统的运动模型,来预测下一个状态。
举个例子,你用一个噪声很大的超声波传感器测距离。卡尔曼滤波会这样工作:
- 预测: 根据上一时刻的距离和速度,估算当前距离应该是多少。
- 更新: 把预测值和实际测量值加权平均,得到最优估计。
这个「加权」的权重,就是卡尔曼增益。它会根据噪声大小自动调整——测量噪声大,就多相信预测值;预测噪声大,就多相信测量值。
核心公式(一维情况):
// 一维卡尔曼滤波
typedef struct {
float x; // 状态估计值
float P; // 估计误差协方差
float Q; // 过程噪声协方差
float R; // 测量噪声协方差
} KalmanFilter;
void kalman_init(KalmanFilter *kf, float init_x, float init_P, float Q, float R) {
kf->x = init_x;
kf->P = init_P;
kf->Q = Q;
kf->R = R;
}
float kalman_update(KalmanFilter *kf, float measurement) {
// 预测
float P_pred = kf->P + kf->Q;
// 卡尔曼增益
float K = P_pred / (P_pred + kf->R);
// 更新
kf->x = kf->x + K * (measurement - kf->x);
kf->P = (1 - K) * P_pred;
return kf->x;
}
说实话,卡尔曼滤波刚接触时有点绕。我记得第一次调参,Q 和 R 设得不对,滤波结果比原始数据还乱。后来才明白,Q 和 R 的比值决定了滤波器的「性格」:
| 参数关系 | 效果 | 适用场景 |
|---|---|---|
| Q 大,R 小 | 更相信测量值,响应快,但噪声多 | 传感器精度高,系统变化快 |
| Q 小,R 大 | 更相信预测值,平滑好,但延迟大 | 传感器噪声大,系统变化慢 |
| Q ≈ R | 折中,兼顾平滑和响应 | 大多数通用场景 |
4.5 怎么选?一张表说清楚
| 滤波方法 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|
| 移动平均 | 简单、计算快 | 延迟大、对野值敏感 | 温度、湿度等慢变信号 |
| 中值滤波 | 抗脉冲噪声强 | 对高斯噪声效果一般 | 超声波、红外测距 |
| 高斯滤波 | 平滑自然、边缘保留好 | 需要预计算权重 | 加速度计、陀螺仪 |
| 卡尔曼滤波 | 最优估计、自适应 | 调参复杂、计算量大 | GPS+IMU融合、目标跟踪 |
最后说一句:没有万能的滤波器。我一般会先在 PC 上用 Python 快速验证,确定哪种方法合适,再移植到嵌入式平台。毕竟 MCU 的资源有限,能省则省。
好了,这一讲就到这里。下一讲咱们聊聊「特征提取」——从干净的数据里挖出真正有用的信息。
公众号:蓝海资料掘金营,微信deep3321