20、多传感器协同:同时处理加速度、陀螺仪、磁力计数据

说实话,单看加速度传感器,它能做的事挺有限的。你想想看,手机平放桌上,它只能告诉你「重力朝下」。但手机是转着的还是静止的?它分不清。这就是为什么我们需要把加速度、陀螺仪、磁力计三个传感器凑在一起干活。

我最早做的一个计步器项目,只用加速度传感器。结果用户把手机放裤兜里晃着走路,步数直接翻了三倍。后来加上陀螺仪和磁力计做姿态融合,才把误判压下去。嗯,这就是多传感器协同的价值。

为什么需要三个传感器?

每个传感器都有短板,说白了就是「一个传感器搞不定所有事」。

传感器 擅长什么 短板在哪
加速度计 测量线性加速度、重力方向 无法区分重力与运动加速度;噪声大
陀螺仪 测量角速度,响应快 有零偏漂移,长时间积分会飘
磁力计 测量地磁场,提供绝对航向 易受铁磁干扰,响应慢

你看,加速度计和陀螺仪互补性很强。加速度计能提供长期稳定的姿态参考(重力方向),但短期噪声大。陀螺仪短期精度高,但长期会漂。磁力计则像指南针,帮你锁定绝对方向。三个凑一起,才能得到稳定又准确的姿态数据。

核心思路:用加速度计和磁力计校正陀螺仪的漂移,用陀螺仪弥补加速度计和磁力计的响应延迟。这就是传感器融合的本质。

传感器融合的基本流程

我个人习惯把融合流程分成三步:

  1. 数据采集与预处理——拿到原始数据,先做滤波和校准
  2. 姿态表示——用四元数或旋转矩阵描述手机朝向
  3. 融合算法——用互补滤波或卡尔曼滤波把三个数据揉在一起

下面这张图可以帮你理清整体逻辑:

加速度计 线性加速度 + 重力 陀螺仪 角速度 磁力计 地磁场方向 预处理:低通滤波 + 去噪 + 校准 传感器融合算法 互补滤波 / 卡尔曼滤波 输出:稳定姿态(四元数/欧拉角)

互补滤波:最简单实用的融合方法

如果你刚入门,我建议先从互补滤波开始。它计算量小,效果也够用。核心思想就一句话:相信陀螺仪的短期变化,相信加速度计和磁力计的长期稳定

公式其实很简单:

姿态 = α × (陀螺仪积分结果) + (1 - α) × (加速度计+磁力计算出的姿态)

这里的 α 通常取 0.95 到 0.98 之间。α 越大,越信任陀螺仪,响应快但可能漂移。α 越小,越信任加速度计,稳定但响应慢。

我的经验:α 取 0.97 是个不错的起点。如果发现姿态有高频抖动,适当减小 α。如果发现姿态跟不上快速转动,适当增大 α。调这个参数就像调收音机,找到那个平衡点就行。

Android 中的 SensorManager 协同

在 Android 里注册多个传感器很简单。我一般这样写:

SensorManager sensorManager = getSystemService(SensorManager.class);

Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
Sensor magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

// 注册监听器,采样频率设为 SENSOR_DELAY_GAME
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_GAME);

注意,三个传感器的回调是独立的。你需要在 onSensorChanged 里根据传感器类型分别处理数据,然后统一交给融合算法。

我曾经踩过的坑:三个传感器的采样频率不一致!加速度计和磁力计通常比陀螺仪慢。如果你直接拿最新数据做融合,时间戳对不上,姿态会跳变。解决办法是给每个数据打时间戳,融合时做插值对齐。

一个简单的互补滤波实现

下面是我常用的互补滤波代码片段,用四元数表示姿态:

public class ComplementaryFilter {
    private float[] quaternion = new float[]{1, 0, 0, 0}; // w, x, y, z
    private static final float ALPHA = 0.97f;
    private long lastTimestamp = 0;

    public void updateWithGyroscope(float[] gyro, long timestamp) {
        if (lastTimestamp == 0) {
            lastTimestamp = timestamp;
            return;
        }
        float dt = (timestamp - lastTimestamp) / 1000.0f; // 秒
        lastTimestamp = timestamp;

        // 陀螺仪积分
        float[] deltaQ = new float[4];
        float halfDt = dt * 0.5f;
        deltaQ[0] = 1;
        deltaQ[1] = gyro[0] * halfDt;
        deltaQ[2] = gyro[1] * halfDt;
        deltaQ[3] = gyro[2] * halfDt;

        quaternion = quaternionMultiply(quaternion, deltaQ);
        normalizeQuaternion(quaternion);
    }

    public void updateWithAccelMag(float[] accel, float[] mag) {
        // 用加速度计和磁力计计算姿态
        float[] accelMagQuat = computeQuaternionFromAccelMag(accel, mag);

        // 互补融合
        quaternion[0] = ALPHA * quaternion[0] + (1 - ALPHA) * accelMagQuat[0];
        quaternion[1] = ALPHA * quaternion[1] + (1 - ALPHA) * accelMagQuat[1];
        quaternion[2] = ALPHA * quaternion[2] + (1 - ALPHA) * accelMagQuat[2];
        quaternion[3] = ALPHA * quaternion[3] + (1 - ALPHA) * accelMagQuat[3];
        normalizeQuaternion(quaternion);
    }

    // 省略四元数乘法和归一化辅助函数
}

这段代码里,陀螺仪数据以较高频率更新姿态,加速度计和磁力计数据则用来做「纠偏」。说白了,陀螺仪负责快,加速度计和磁力计负责准。

实际项目中的注意事项

  • 传感器校准:磁力计特别容易受干扰。我习惯在应用启动时让用户画「8」字校准。加速度计和陀螺仪一般出厂校准好了,但如果你发现数据有固定偏移,可以做个零偏校准。
  • 坐标系统一:Android 的传感器坐标系是固定的(手机平放,X向右,Y向上,Z朝外)。但不同手机厂商可能有差异。我建议用 SensorManager.getRotationMatrix() 把数据转到世界坐标系。
  • 性能优化:三个传感器同时高频采样,CPU 开销不小。如果只是做简单的方向检测,把采样率降到 SENSOR_DELAY_UI 就够了。做 VR/AR 才需要 SENSOR_DELAY_FASTEST

一句话总结:加速度计给方向,陀螺仪给速度,磁力计给航向。三个传感器各司其职,互补滤波把它们拧成一股绳,你就能得到稳定可靠的姿态数据。

好了,多传感器协同的核心思路就这些。你动手写代码的时候,记得先跑通单个传感器,再慢慢加融合逻辑。别一上来就搞卡尔曼滤波——那个水太深,先拿互补滤波练手,够用很久了。


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