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 知识体系总览

下面这张图,把整个传感器数据处理的流程串起来了。你可以把它当作一个「检查清单」,每次处理数据时对照着走一遍。

传感器数据处理流程 原始传感器数据 采样率设置(20-200 Hz) 异常值剔除(3σ/中值滤波) 数据平滑滤波(低通/高通/滑动平均) 数据校准(零偏/灵敏度) 可用数据

这张图展示了从原始数据到可用数据的完整链路。每一步都有它的意义,跳过任何一步,都可能让你的应用表现异常。

总结一下:

  • 采样率不是越高越好,够用就行
  • 滤波要平衡平滑度和延迟
  • 异常值要处理,但不能简单丢弃
  • 校准是「最后一公里」,决定了数据的绝对精度

好了,这一章的内容就到这里。传感器数据处理是个实践性很强的工作,光看理论没用,建议你打开Android Studio,拿真实数据跑一遍上面的代码。你会发现,原来那些「不听话」的数据,其实是有规律可循的。

公众号:蓝海资料掘金营,微信deep3321