4. 数据采集与预处理:采样频率选择、滤波算法与异常值剔除

数据采集和预处理,说白了就是标定工作的“地基”。地基没打好,后面算法再漂亮也是白搭。我见过太多项目,花了几周调参数,最后发现是传感器数据本身就有问题——采样率不够、噪声没滤干净、或者混进了几个离谱的异常点。

这一章,我把这些年踩过的坑和总结的方法,掰开揉碎了讲给你听。

4.1 采样频率怎么选?不是越快越好

很多人觉得采样频率越高越好,其实不然。采样频率选错了,要么丢信息,要么白占资源。

核心原则:满足奈奎斯特采样定理

信号最高频率的2倍以上,这是底线。但实际工程中,我一般留3~5倍的余量。

经验法则:

  • 发动机转速信号:100 Hz 够用
  • 爆震传感器:10 kHz 起步
  • 氧传感器:10~20 Hz
  • 油门踏板位置:50 Hz 足够

我记得有一次做柴油机轨压标定,采样频率设了200 Hz,结果压力波动的高频成分全丢了。后来改成1 kHz,问题才解决。你想想看,轨压波动频率其实挺高的,200 Hz根本抓不住细节。

避坑指南:

我曾经在某个项目中,采样频率设得太高(20 kHz),结果数据量爆炸,存储和处理的压力都很大。后来发现,信号的有效带宽只有500 Hz,完全没必要。所以,采样频率不是越高越好,够用就行。

4.2 滤波算法:三种常用方法对比

传感器信号里总有噪声。滤波就是把这些噪声去掉,留下有用的信号。我常用的有三种:均值滤波、中值滤波、卡尔曼滤波。各有各的脾气。

滤波算法 适用场景 优点 缺点
均值滤波 平稳信号,噪声为高斯白噪声 简单、计算快 对突变不敏感,会模糊细节
中值滤波 有脉冲噪声(毛刺)的信号 能有效剔除孤立异常点 对连续噪声效果一般
卡尔曼滤波 动态系统,有模型可用的场景 实时性好,精度高 需要调参,计算量稍大

4.2.1 均值滤波

最简单,也最常用。把最近N个点取平均。适合噪声比较均匀的场景。

// 均值滤波示例
#define FILTER_WINDOW 5
float buffer[FILTER_WINDOW];
int index = 0;

float mean_filter(float new_value) {
    buffer[index] = new_value;
    index = (index + 1) % FILTER_WINDOW;
    
    float sum = 0;
    for (int i = 0; i < FILTER_WINDOW; i++) {
        sum += buffer[i];
    }
    return sum / FILTER_WINDOW;
}

小技巧:

窗口大小N怎么选?我一般先看信号的噪声频率。噪声频率高,N可以小一点(3~5);噪声频率低,N可以大一点(10~20)。但N太大,信号延迟也会变大,要注意。

4.2.2 中值滤波

对付毛刺信号特别好用。比如氧传感器信号,经常会有瞬间的跳变,用中值滤波一滤就干净了。

// 中值滤波示例
#define FILTER_WINDOW 5
float sort_buffer[FILTER_WINDOW];

float median_filter(float new_value) {
    // 将新值插入排序缓冲区
    for (int i = 0; i < FILTER_WINDOW - 1; i++) {
        sort_buffer[i] = sort_buffer[i + 1];
    }
    sort_buffer[FILTER_WINDOW - 1] = new_value;
    
    // 冒泡排序(窗口小,够用)
    for (int i = 0; i < FILTER_WINDOW - 1; i++) {
        for (int j = 0; j < FILTER_WINDOW - i - 1; j++) {
            if (sort_buffer[j] > sort_buffer[j + 1]) {
                float temp = sort_buffer[j];
                sort_buffer[j] = sort_buffer[j + 1];
                sort_buffer[j + 1] = temp;
            }
        }
    }
    return sort_buffer[FILTER_WINDOW / 2];
}

我个人习惯,在标定数据预处理阶段,先用中值滤波跑一遍,把明显的毛刺干掉,再用均值滤波平滑一下。效果很好。

4.2.3 卡尔曼滤波

这个稍微复杂一点,但效果是真的好。适合有模型可用的动态系统,比如车速、发动机转速这些。

// 一维卡尔曼滤波示例
typedef struct {
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
    float x;  // 估计值
    float P;  // 估计误差协方差
    float K;  // 卡尔曼增益
} KalmanFilter;

void kalman_init(KalmanFilter* kf, float init_x, float init_P) {
    kf->x = init_x;
    kf->P = init_P;
    kf->Q = 0.01;  // 需要根据实际调参
    kf->R = 0.1;   // 需要根据实际调参
}

float kalman_update(KalmanFilter* kf, float measurement) {
    // 预测
    kf->P = kf->P + kf->Q;
    
    // 更新
    kf->K = kf->P / (kf->P + kf->R);
    kf->x = kf->x + kf->K * (measurement - kf->x);
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->x;
}

注意:

卡尔曼滤波的Q和R参数需要仔细调。Q太大,滤波结果会跟着测量值乱跳;Q太小,结果会滞后。我一般先根据传感器的噪声水平估算R,然后手动调Q,直到效果满意为止。

4.3 异常值剔除:别让一个坏点毁了整个标定

异常值,就是那些明显偏离正常范围的数据点。可能是传感器故障、通信干扰、或者操作失误造成的。如果不剔除,标定出来的MAP图会很难看,甚至导致控制策略误判。

常用的方法:

  • 3σ原则:假设数据服从正态分布,超出均值±3倍标准差的数据视为异常值。
  • 箱线图法:基于四分位数,超出1.5倍IQR(四分位距)的数据视为异常值。
  • 物理限值法:根据传感器量程或物理常识,设定上下限,超出则剔除。

我的做法:

先做物理限值法,把明显不合理的数据(比如负的转速、超过量程的压力)直接扔掉。然后再用3σ原则或箱线图法,剔除统计上的异常点。两层保险,基本不会漏。

我曾经在一个项目中,发现某个传感器的数据总是有间歇性的跳变。一开始以为是噪声,后来用箱线图一分析,发现是传感器本身的问题。换了传感器之后,数据就正常了。所以,异常值剔除不只是数据处理,有时候还能帮你发现硬件问题。

4.4 知识结构图:数据采集与预处理流程

下面这张图,把整个流程串起来了。从传感器原始信号,到最终可用的标定数据,每一步都不能少。

数据采集与预处理流程 数据采集与预处理流程 传感器原始信号 采样频率选择 滤波处理(均值/中值/卡尔曼) 异常值剔除 可用标定数据 每一步都影响最终标定精度,缺一不可

4.5 总结一下

数据采集与预处理,看起来是基础工作,但恰恰是决定标定成败的关键。采样频率选对了,滤波方法用对了,异常值剔干净了,后面的工作才能顺利推进。

我个人习惯,每次开始标定之前,都会花点时间检查数据质量。看看采样频率够不够,滤波效果好不好,有没有异常点。磨刀不误砍柴工,这个时间花得值。