9、数据过滤与平滑:原始数据噪声问题,低通滤波算法实现
做光线传感器开发,你很快会遇到一个头疼的问题——原始数据抖得像筛子。明明环境光没变,数值却在几十到几百之间来回跳。这不是硬件坏了,是噪声在捣乱。
我刚开始做这个的时候,被这个现象坑过好几次。有一次做自动亮度调节,用户反馈屏幕忽明忽暗,查了半天才发现是原始数据没做滤波处理。嗯,从那以后,我再也不敢跳过数据平滑这一步了。
9.1 噪声从哪里来?
光线传感器的噪声来源其实挺多的。我简单列几个常见的:
- 环境光本身不稳定——日光灯有50Hz/60Hz频闪,LED灯也有PWM调光
- 传感器自身噪声——ADC量化误差、热噪声
- 电磁干扰——手机射频、电机启停都会影响
- 采样抖动——系统调度延迟导致采样间隔不均匀
你想想看,这些噪声叠加在一起,原始数据能稳定才怪。所以我们需要一个「缓冲器」,把突变的尖峰磨平,把高频抖动滤掉。
核心思路:低通滤波。说白了就是让变化慢的信号通过,让变化快的信号衰减。光线变化本身是缓慢的(除非你突然开关灯),而噪声是高频的,正好一刀切。
9.2 移动平均滤波(Moving Average)
这是最直观的方法,也是我最早用的方案。原理很简单:维护一个固定长度的窗口,每次取窗口内所有数据的平均值。
举个例子,窗口大小设为5:
原始数据: 100, 120, 90, 110, 130, 95, 105, ...
窗口1: [100, 120, 90, 110, 130] → 平均 = 110
窗口2: [120, 90, 110, 130, 95] → 平均 = 109
窗口3: [90, 110, 130, 95, 105] → 平均 = 106
...
代码实现也很简单:
public class MovingAverageFilter {
private float[] window;
private int index = 0;
private int count = 0;
private float sum = 0f;
public MovingAverageFilter(int windowSize) {
window = new float[windowSize];
}
public float filter(float newValue) {
// 如果窗口还没填满,直接累加
if (count < window.length) {
sum += newValue;
window[index] = newValue;
index = (index + 1) % window.length;
count++;
return sum / count;
}
// 窗口满了,替换最旧的值
float oldest = window[index];
sum = sum - oldest + newValue;
window[index] = newValue;
index = (index + 1) % window.length;
return sum / window.length;
}
}
我的经验:窗口大小选3~10比较合适。太小了滤波效果差,太大了延迟严重。我曾经在一个自动亮度项目里用了20的窗口,结果用户从暗处走到亮处,屏幕要等3秒才反应过来——体验极差。
9.3 指数加权移动平均(EWMA)
移动平均有个问题:它平等对待窗口内的所有数据。但直觉告诉我们,越新的数据应该越重要。指数加权移动平均就是干这个的。
公式很简单:
filteredValue = α × newValue + (1 - α) × previousFilteredValue
其中α(alpha)是平滑因子,取值范围0~1。α越大,对新数据响应越快,但滤波效果差;α越小,滤波效果越好,但响应变慢。
代码实现:
public class EwmaFilter {
private float alpha;
private float filteredValue;
private boolean initialized = false;
public EwmaFilter(float alpha) {
// alpha 通常取 0.1 ~ 0.3
this.alpha = alpha;
}
public float filter(float newValue) {
if (!initialized) {
filteredValue = newValue;
initialized = true;
return filteredValue;
}
filteredValue = alpha * newValue + (1 - alpha) * filteredValue;
return filteredValue;
}
}
为什么EWMA更常用?因为它只需要存一个值(上一次的滤波结果),内存占用极小。而移动平均要维护整个窗口。在嵌入式设备上,这点差别很关键。
9.4 两种算法对比
| 特性 | 移动平均 | 指数加权移动平均 |
|---|---|---|
| 内存占用 | O(N),需要存储整个窗口 | O(1),只存一个值 |
| 计算复杂度 | O(1),但需要维护窗口 | O(1),极简单 |
| 响应速度 | 窗口内所有数据权重相同 | 新数据权重更高 |
| 适用场景 | 数据量小,需要严格平滑 | 实时性要求高,资源受限 |
| 参数调优 | 调整窗口大小 | 调整alpha值 |
9.5 避坑指南
我曾经踩过几个坑,写出来给你提个醒:
- 初始化阶段不要输出0——如果滤波器的初始值是0,前几次输出会严重拉低真实值。我习惯用第一个采样值初始化,或者直接丢弃前N个输出。
- α值不要选太大——α=0.5以上基本等于没滤波。我一般用0.1~0.3,具体看你的传感器噪声水平。
- 注意数据类型——用float做累加,别用int,否则精度丢失严重。
- 窗口大小和采样率要匹配——采样率100Hz时窗口10可能还行,采样率10Hz时窗口10就太长了。
特别注意:滤波不是万能的。如果原始数据噪声大到离谱(比如跳变幅度超过50%),先检查硬件电路和电源稳定性,别指望软件滤波能解决一切。
9.6 知识体系总览
下面这张图帮你理清本章的核心逻辑:
这张图把整个流程串起来了。从问题出发,到解决方案,再到两种具体算法,最后落到参数选择。你写代码的时候,脑子里要有这张图,就知道每一步在干什么了。
我的建议:刚开始做的时候,先用移动平均把功能跑通,再换成EWMA优化性能。别一上来就追求完美,先让系统动起来再说。
好了,数据滤波这块就讲到这里。代码我已经贴出来了,你直接复制到项目里就能用。记住,滤波参数要根据你的实际传感器和场景去调,没有万能参数。