3、信号预处理:滑动平均滤波、中值滤波、卡尔曼滤波入门

各位同学,欢迎来到信号预处理这一讲。

说实话,传感器采集回来的原始数据,基本没法直接用。为什么?因为噪声太严重了。你想想看,一个光敏传感器,哪怕环境光纹丝不动,它输出的数值也会上下跳动。这就是噪声,来自电源纹波、热噪声、量化误差等等。

我刚开始做智能灯具项目时,就吃过这个亏。传感器数据直接拿来控制PWM调光,结果灯光像呼吸一样忽明忽暗,用户投诉说“这灯是不是坏了”。从那以后,我养成了一个习惯:任何传感器数据,必须先过预处理这一关。

3.1 滑动平均滤波:最简单也最常用

滑动平均滤波的原理,说白了就是取最近N个采样值的平均值。每来一个新数据,就丢掉最老的那个,保持窗口长度不变。

为什么它有效?因为噪声通常是高频的、随机的,而真实信号变化相对缓慢。平均一下,噪声就被“抹平”了。

核心公式:

y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N

这里N是窗口长度。N越大,滤波效果越平滑,但延迟也越大。我个人习惯,对于光照传感器,N取4到8就够用了。如果用在人体红外传感器上,N可以取大一点,比如16,因为人体动作本身就不需要那么快响应。

看代码实现:

// 滑动平均滤波器
#define WINDOW_SIZE 8

typedef struct {
    float buffer[WINDOW_SIZE];
    uint8_t index;
    float sum;
} MovingAverage;

float moving_average_filter(MovingAverage* ma, float new_sample) {
    // 减去最老的数据
    ma->sum -= ma->buffer[ma->index];
    // 存入新数据
    ma->buffer[ma->index] = new_sample;
    // 累加新数据
    ma->sum += new_sample;
    // 更新索引
    ma->index = (ma->index + 1) % WINDOW_SIZE;
    // 返回平均值
    return ma->sum / WINDOW_SIZE;
}

嗯,这里要注意:初始化时要把buffer全部清零,sum也要清零。否则第一次滤波会得到一个错误值。

避坑指南:我曾经在项目里忘记初始化sum,结果前几次滤波输出全是垃圾数据,排查了半天才发现。初始化一定要做彻底。

3.2 中值滤波:对付脉冲噪声的利器

滑动平均滤波有个致命弱点:它对脉冲噪声非常敏感。什么叫脉冲噪声?就是突然冒出一个离谱的数值,比如光照传感器突然读到65535(满量程)。

为什么会这样?因为平均滤波会把那个异常值也平均进去,导致输出瞬间跳变。这时候,中值滤波就派上用场了。

中值滤波的做法:取窗口内的所有数据,排序后取中间那个值。这样一来,哪怕有一个异常值,只要它不在中间位置,就不会影响输出。

我举个例子:假设窗口大小是5,数据是[100, 102, 101, 65535, 103]。排序后是[100, 101, 102, 103, 65535],中值是102。你看,那个65535被完美过滤掉了。

代码实现:

// 中值滤波器
#define MEDIAN_WINDOW 5

float median_filter(float* buffer, float new_sample) {
    float temp[MEDIAN_WINDOW];
    
    // 移动窗口
    for(int i = 0; i < MEDIAN_WINDOW - 1; i++) {
        buffer[i] = buffer[i+1];
    }
    buffer[MEDIAN_WINDOW - 1] = new_sample;
    
    // 拷贝到临时数组并排序
    memcpy(temp, buffer, sizeof(float) * MEDIAN_WINDOW);
    
    // 冒泡排序(窗口小,够用)
    for(int i = 0; i < MEDIAN_WINDOW - 1; i++) {
        for(int j = 0; j < MEDIAN_WINDOW - 1 - i; j++) {
            if(temp[j] > temp[j+1]) {
                float t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    
    // 返回中值
    return temp[MEDIAN_WINDOW / 2];
}

注意:中值滤波的窗口大小必须是奇数,否则中值不好取。另外,排序算法在窗口很小时用冒泡没问题,窗口大了就得换快速排序。

我个人习惯,在智能灯具中,先做一次中值滤波剔除脉冲噪声,再做滑动平均滤波平滑数据。两者配合使用,效果非常好。

3.3 卡尔曼滤波入门:从“猜”到“准”

前面两种滤波方法,说白了都是“事后诸葛亮”——它们只根据历史数据做处理。卡尔曼滤波不一样,它有一个“预测-更新”的闭环过程。

我尽量用大白话讲清楚:卡尔曼滤波认为,系统的状态是随时间变化的,而且我们只能通过观测来估计它。它有两个步骤:

  1. 预测:根据上一时刻的状态,猜一下当前时刻的状态大概是多少。
  2. 更新:拿到当前时刻的观测值后,结合预测值和观测值,算出一个更准确的估计值。

你想想看,这就像你闭着眼睛走路。你根据上一秒的位置和速度,猜自己现在在哪(预测)。然后你睁开眼看一下实际位置(观测),把猜测和观测结合起来,得到更准确的位置估计。

对于智能灯具,我们通常用一维卡尔曼滤波就够了。所谓一维,就是只处理一个变量,比如光照强度。

核心公式如下:

步骤 公式 说明
预测 x_pred = x_est 状态不变(假设静止)
预测 P_pred = P_est + Q 预测误差协方差
更新 K = P_pred / (P_pred + R) 卡尔曼增益
更新 x_est = x_pred + K * (z - x_pred) 状态更新
更新 P_est = (1 - K) * P_pred 误差协方差更新

这里Q是过程噪声协方差,R是测量噪声协方差。Q越大,说明系统本身变化快;R越大,说明传感器噪声大。

看代码:

// 一维卡尔曼滤波器
typedef struct {
    float x_est;  // 估计值
    float P_est;  // 估计误差协方差
    float Q;      // 过程噪声
    float R;      // 测量噪声
} KalmanFilter1D;

void kalman_init(KalmanFilter1D* kf, float init_value) {
    kf->x_est = init_value;
    kf->P_est = 1.0f;  // 初始误差设大一点
    kf->Q = 0.01f;     // 根据实际情况调整
    kf->R = 0.1f;      // 根据传感器手册或实测调整
}

float kalman_update(KalmanFilter1D* kf, float measurement) {
    // 预测
    float x_pred = kf->x_est;
    float P_pred = kf->P_est + kf->Q;
    
    // 更新
    float K = P_pred / (P_pred + kf->R);
    kf->x_est = x_pred + K * (measurement - x_pred);
    kf->P_est = (1.0f - K) * P_pred;
    
    return kf->x_est;
}

关键点:Q和R的调参非常关键。Q设太大,滤波结果会跟着噪声跑;Q设太小,响应太慢。R设太大,滤波结果不信任传感器;R设太小,噪声滤不干净。

我记得第一次调卡尔曼滤波参数时,花了整整一个下午。后来总结出一个经验:先固定R(根据传感器数据手册的噪声指标),然后调Q,直到滤波结果既平滑又能跟上真实变化。

3.4 三种滤波器的对比与选择

我把三种滤波器的特点整理成了一张表,方便你对比:

滤波器 优点 缺点 适用场景
滑动平均 简单、计算量小 对脉冲噪声敏感、有延迟 光照、温度等缓慢变化信号
中值滤波 抗脉冲噪声能力强 计算量大(需排序)、平滑效果一般 有突发干扰的场景
卡尔曼滤波 自适应、精度高、延迟可控 需要调参、计算量稍大 动态变化、要求高精度的场景

我个人建议:

  • 如果MCU资源紧张,用滑动平均就够了。
  • 如果传感器经常受到电磁干扰(比如靠近电机),加上中值滤波。
  • 如果要做高精度调光或色温控制,上卡尔曼滤波。

实战经验:我在一个智能台灯项目中,用了“中值滤波+卡尔曼滤波”的组合。先中值滤波剔除异常值,再卡尔曼滤波平滑输出。最终调光效果非常顺滑,用户完全感觉不到灯光在变化。

好了,信号预处理的内容就讲到这里。下一章我们会进入传感器融合的核心——如何把多个传感器的数据结合起来,得到更准确的环境感知结果。

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