16、传感器数据采集与处理:采样率设置、数据平滑滤波(低通/高通/滑动平均)、异常值剔除、数据校准
传感器开发做到一定深度,你会发现一个真相:原始数据基本没法直接用。
我刚开始做计步器那会儿,直接从加速度计拿数据,结果手机放桌上不动,步数还在涨。嗯,这就是典型的「原始数据陷阱」。今天我们就来系统聊聊,怎么把传感器那些「毛刺」数据,变成真正可用的信号。
核心观点:传感器数据处理的本质,是从「物理世界的模拟信号」到「数字世界的可用信息」的桥梁。采样率、滤波、异常值、校准,缺一不可。
16.1 采样率设置:不是越快越好
很多人一上来就设最高采样率,觉得数据越多越准。其实这是个误区。
采样率的选择,取决于你的应用场景和信号频率。根据奈奎斯特定理,采样率至少要是信号最高频率的2倍。但实际工程中,我一般取5-10倍。
| 应用场景 | 推荐采样率 | 说明 |
|---|---|---|
| 计步器 | 20-50 Hz | 人体步行频率约1-3 Hz,20 Hz足够 |
| 手势识别 | 50-100 Hz | 手势动作较快,需要更高采样率 |
| 车辆碰撞检测 | 200-400 Hz | 冲击信号持续时间极短 |
| 姿态追踪 | 100-200 Hz | 需要平衡精度和功耗 |
我的经验:在Android中设置采样率时,用 SENSOR_DELAY_GAME(约50 Hz)作为起点。如果发现数据不够平滑,再逐步提高。千万别一上来就 SENSOR_DELAY_FASTEST,那会让你的CPU和电池一起哭。
// 设置采样率的正确姿势
SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// 推荐:使用SENSOR_DELAY_GAME作为起点
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
// 如果需要自定义采样率(微秒)
// 10000微秒 = 100 Hz
sensorManager.registerListener(this, accelerometer, 10000);
16.2 数据平滑滤波:让信号「安静」下来
原始传感器数据就像个多动症的孩子,上蹿下跳。滤波的目的,就是让它「安静」下来,同时保留有用的信息。
16.2.1 低通滤波:保留低频,滤除高频噪声
低通滤波是最常用的。说白了,就是让变化慢的信号通过,把变化快的噪声滤掉。
// 一阶低通滤波实现
public class LowPassFilter {
private float alpha = 0.8f; // 滤波系数,越大越平滑
private float lastValue = 0f;
public float filter(float input) {
float output = alpha * lastValue + (1 - alpha) * input;
lastValue = output;
return output;
}
// 调整平滑程度
public void setAlpha(float alpha) {
this.alpha = alpha;
}
}
注意:alpha值的选择很关键。alpha越大,平滑效果越好,但延迟也越大。我一般从0.8开始调,根据实际效果微调。曾经有个项目,我设了0.99,结果数据延迟了半秒,用户说「手机反应慢」,其实是被我「平滑」掉了。
16.2.2 高通滤波:提取变化,滤除直流分量
高通滤波和低通相反,它保留变化快的信号,滤掉缓慢变化的部分。比如你想检测「拍打手机」的动作,高通滤波就很合适。
// 一阶高通滤波实现
public class HighPassFilter {
private float alpha = 0.1f;
private float lastOutput = 0f;
private float lastInput = 0f;
public float filter(float input) {
float output = alpha * (lastOutput + input - lastInput);
lastOutput = output;
lastInput = input;
return output;
}
}
16.2.3 滑动平均滤波:简单粗暴但有效
滑动平均是我个人最喜欢的滤波方式。它不需要复杂的数学,效果却出奇的好。
// 滑动平均滤波实现
public class MovingAverageFilter {
private float[] buffer;
private int index = 0;
private int count = 0;
private float sum = 0f;
public MovingAverageFilter(int windowSize) {
buffer = new float[windowSize];
}
public float filter(float input) {
sum -= buffer[index];
buffer[index] = input;
sum += input;
index = (index + 1) % buffer.length;
if (count < buffer.length) count++;
return sum / count;
}
}
经验之谈:窗口大小选多少?对于50 Hz的加速度数据,我通常用5-10的窗口。窗口越大越平滑,但延迟也越大。你想想看,如果窗口设到50,那延迟就是1秒,实时性就没了。
16.3 异常值剔除:别让「野点」毁了你的数据
传感器偶尔会抽风,突然跳出一个离谱的值。比如加速度计突然显示100 m/s²,这明显是异常。如果不处理,后续计算全完蛋。
我常用的方法是3σ原则和中值滤波。
// 基于3σ原则的异常值剔除
public class OutlierRemover {
private float mean = 0f;
private float std = 0f;
private List<Float> history = new ArrayList<>();
private int maxHistory = 100;
public boolean isOutlier(float value) {
if (history.size() < 10) {
history.add(value);
return false; // 数据太少,先不判断
}
// 计算均值和标准差
calculateStats();
// 如果超出3σ范围,视为异常
if (Math.abs(value - mean) > 3 * std) {
return true;
}
// 更新历史数据
history.add(value);
if (history.size() > maxHistory) {
history.remove(0);
}
return false;
}
private void calculateStats() {
// 计算均值和标准差的代码
// 这里省略具体实现
}
}
我曾经踩过的坑:有一次做跌倒检测,我把异常值直接丢弃了。结果用户真的摔倒了,加速度数据被当成「异常」扔掉了。后来我改成:异常值不丢弃,而是用上一个正常值替代。这样既去除了噪声,又保留了连续性。
16.4 数据校准:让传感器「认识」真实世界
每个传感器出厂时都有偏差。同一个型号的手机,放在同一个位置,读出的数据可能不一样。校准的目的,就是消除这些偏差。
16.4.1 零偏校准
传感器静止时,理论上应该输出0(或重力加速度)。但实际上总有偏差。校准方法很简单:采集静止时的数据,取平均值作为偏移量。
// 零偏校准示例
public class Calibration {
private float offsetX = 0f;
private float offsetY = 0f;
private float offsetZ = 0f;
private boolean calibrated = false;
public void calibrate(List<SensorEvent> staticData) {
float sumX = 0, sumY = 0, sumZ = 0;
for (SensorEvent event : staticData) {
sumX += event.values[0];
sumY += event.values[1];
sumZ += event.values[2];
}
int count = staticData.size();
offsetX = sumX / count;
offsetY = sumY / count;
offsetZ = sumZ / count;
calibrated = true;
}
public float[] applyCalibration(float[] rawValues) {
if (!calibrated) return rawValues;
return new float[]{
rawValues[0] - offsetX,
rawValues[1] - offsetY,
rawValues[2] - offsetZ
};
}
}
16.4.2 灵敏度校准
除了零偏,传感器还有灵敏度误差。比如理论上1g对应9.8 m/s²,但实际可能对应9.5或10.1。这需要已知的参考值来校准。
我的做法:让手机平放,采集Z轴数据(此时应为9.8 m/s²),然后计算缩放因子。这样至少能保证一个轴是准的。对于精度要求高的场景,建议用六面法校准,但那个太复杂,一般App用不上。
16.5 知识体系总览
下面这张图,把整个传感器数据处理的流程串起来了。你可以把它当作一个「检查清单」,每次处理数据时对照着走一遍。
这张图展示了从原始数据到可用数据的完整链路。每一步都有它的意义,跳过任何一步,都可能让你的应用表现异常。
总结一下:
- 采样率不是越高越好,够用就行
- 滤波要平衡平滑度和延迟
- 异常值要处理,但不能简单丢弃
- 校准是「最后一公里」,决定了数据的绝对精度
好了,这一章的内容就到这里。传感器数据处理是个实践性很强的工作,光看理论没用,建议你打开Android Studio,拿真实数据跑一遍上面的代码。你会发现,原来那些「不听话」的数据,其实是有规律可循的。