第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