13、传感器数据滤波:原始数据噪声来源、简单移动平均滤波算法实现、卡尔曼滤波入门
各位好,我是你们的老朋友。今天咱们聊聊传感器数据滤波。
说实话,做距离传感器开发,最让人头疼的不是硬件本身,而是那堆“脏数据”。你明明把手放在传感器前面,距离值却像抽风一样跳来跳去。嗯,这就是噪声在作怪。
13.1 原始数据噪声来源
我刚开始做项目时,总以为传感器读回来的数据就是真实值。后来被坑了几次,才明白——原始数据里全是“杂质”。
噪声主要来自这几个地方:
- 硬件电路噪声:电源纹波、信号线串扰、PCB布局不合理。说白了,电路本身就不完美。
- 环境干扰:红外传感器容易受阳光、灯光影响。我记得有一次在强光下测试,距离值直接漂了5厘米。
- 传感器自身特性:每个传感器都有测量误差,尤其是低成本器件,线性度差、温漂严重。
- ADC量化误差:模拟信号转数字时,精度有限,会引入±1 LSB的误差。
核心观点:噪声是随机的、不可预测的。滤波的目的,就是从“脏数据”里提取出“干净信号”。
你想想看,如果直接把原始数据拿去做距离判断,结果会怎样?手还没靠近,距离值就乱跳,触发误操作。用户体验直接崩盘。
13.2 简单移动平均滤波算法实现
最简单的滤波方法,就是移动平均。说白了,就是把最近N次采样值加起来,除以N。
为什么有效?因为噪声是随机的,正负噪声在平均过程中会相互抵消。真实信号是缓慢变化的,平均后基本不变。
我个人的习惯是,先试N=5。效果不好再调大。N越大,曲线越平滑,但响应越慢。这是个取舍问题。
来看代码实现:
public class MovingAverageFilter {
private float[] buffer;
private int index = 0;
private int count = 0;
private int windowSize;
private float sum = 0;
public MovingAverageFilter(int windowSize) {
this.windowSize = windowSize;
this.buffer = new float[windowSize];
}
public float filter(float newValue) {
// 如果缓冲区满了,先减去最旧的值
if (count == windowSize) {
sum -= buffer[index];
} else {
count++;
}
// 存入新值
buffer[index] = newValue;
sum += newValue;
// 更新索引(循环覆盖)
index = (index + 1) % windowSize;
// 返回平均值
return sum / count;
}
}
用法很简单:
MovingAverageFilter filter = new MovingAverageFilter(5);
float filteredValue = filter.filter(rawValue);
小技巧:窗口大小建议选奇数。为什么?因为奇数窗口的延迟是整数个采样周期,计算方便。当然,这不是硬性要求。
我曾经在一个项目中,窗口设到20,结果手都贴上去了,距离值还在慢慢下降。用户反应“反应太慢”。后来改成8,效果刚刚好。
13.3 卡尔曼滤波入门
移动平均虽然简单,但有个硬伤——它假设所有历史数据权重相同。实际上,越新的数据越重要。卡尔曼滤波就是解决这个问题的。
卡尔曼滤波的核心思想:用上一时刻的估计值,结合当前测量值,算出最优估计。它有两个步骤:预测和更新。
我刚开始学卡尔曼滤波时,被那一堆矩阵公式吓到了。后来发现,对于一维距离传感器,公式可以简化到极致。
一维卡尔曼滤波的五个核心公式:
- 预测状态: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
其中:
- x_est:当前最优估计值
- z:当前测量值
- Q:过程噪声协方差(系统模型的不确定性)
- R:测量噪声协方差(传感器本身的噪声水平)
看代码实现:
public class KalmanFilter1D {
private float Q; // 过程噪声
private float R; // 测量噪声
private float x; // 估计值
private float P; // 误差协方差
private float K; // 卡尔曼增益
public KalmanFilter1D(float Q, float R, float initialValue) {
this.Q = Q;
this.R = R;
this.x = initialValue;
this.P = 1.0f; // 初始误差协方差,通常设大一点
}
public float filter(float measurement) {
// 预测
float xPred = x;
float PPred = P + Q;
// 更新
K = PPred / (PPred + R);
x = xPred + K * (measurement - xPred);
P = (1 - K) * PPred;
return x;
}
}
用法:
KalmanFilter1D kf = new KalmanFilter1D(0.01f, 0.1f, rawValue);
float filteredValue = kf.filter(rawValue);
避坑指南:Q和R的调参是关键。Q越大,滤波器越相信测量值,响应快但噪声大。R越大,滤波器越相信预测值,平滑但滞后。我曾经调了一整天,才找到合适的Q和R。
为什么卡尔曼滤波比移动平均好?因为它能自适应。当测量值突然变化时,卡尔曼增益会自动增大,快速跟踪。当测量值稳定时,增益变小,平滑输出。
来看一张对比图:
从图上能看出来:原始数据上下乱跳,移动平均虽然平滑了,但明显滞后。卡尔曼滤波既平滑了噪声,又保持了快速响应。这就是它的优势。
我的建议:如果项目对实时性要求不高,移动平均就够用了。如果要求高精度、低延迟,比如手势识别、接近感应,那就上卡尔曼滤波。
最后说一句:滤波不是万能的。如果硬件噪声太大,滤波也救不了。我建议先优化硬件设计,再考虑软件滤波。双管齐下,效果最好。
好了,这一章就到这里。记住:数据滤波是传感器开发的必修课,多练练,你也能成为高手。
公众号:蓝海资料掘金营,微信deep3321