16、指南针融合:加速度传感器与磁力传感器的互补

各位同学,欢迎来到第十六章。

前面我们分别聊了加速度传感器和磁力传感器。加速度计能告诉我们「哪边是下」,磁力计能告诉我们「哪边是北」。但你可能已经猜到了——单独用任何一个,都做不出一个靠谱的电子指南针。

为什么?

因为现实世界太「脏」了。加速度计会被运动干扰,磁力计会被磁场干扰。你拿着手机原地转一圈,磁力计的数据可能飘得像喝醉了酒。这时候,就需要把两个传感器的数据「融合」起来,互相取长补短。

说白了,这就是一个经典的传感器融合问题。我当年第一次做这个功能时,以为只是简单地把两个角度平均一下……结果被现实狠狠教育了一顿。今天我们就来把这个坑填上。

16.1 为什么需要融合?

先看一个场景:你拿着手机,水平放在桌面上。这时候磁力计可以正常工作,给出一个稳定的方位角。但如果你把手机倾斜一下呢?

磁力计测量的是三维磁场矢量。当手机倾斜时,这个矢量的水平分量和垂直分量会混在一起。如果不做任何处理,直接拿原始数据算方位角,结果会严重偏差。

反过来,加速度计可以告诉我们手机的倾斜角度(俯仰角和横滚角)。利用这个信息,我们可以把磁力计的数据「掰正」——也就是把三维磁场矢量投影到水平面上,然后再算方位角。

这就是融合的核心思想:用加速度计的姿态信息,去校正磁力计的测量坐标系

核心公式(理解即可):

先利用加速度计数据计算旋转矩阵 R,然后将磁力计原始数据 M 通过 R 变换到水平坐标系,最后用水平分量计算方位角:

// 伪代码示意
// 1. 从加速度计获取俯仰角 pitch 和横滚角 roll
// 2. 构建旋转矩阵 R
// 3. 将磁力计数据 M = [mx, my, mz] 旋转到水平面
// 4. 水平方位角 = atan2(my_rotated, mx_rotated)

嗯,这里要注意:这个旋转矩阵的推导,其实涉及一些线性代数。但作为应用开发者,你不需要手推公式——Android 的 SensorManager 已经提供了现成的 API。

16.2 Android 中的融合 API

我个人习惯直接使用 SensorManager.getRotationMatrix()SensorManager.getOrientation()。这两个方法内部已经完成了加速度计和磁力计的融合工作。

来看代码:

// 注册传感器监听
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor magnetometer = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

// 在 onSensorChanged 中
float[] gravity = new float[3];
float[] geomagnetic = new float[3];

@Override
public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
        gravity = event.values.clone();
    }
    if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
        geomagnetic = event.values.clone();
    }

    if (gravity != null && geomagnetic != null) {
        float[] R = new float[9];
        float[] I = new float[9];
        boolean success = SensorManager.getRotationMatrix(R, I, gravity, geomagnetic);

        if (success) {
            float[] orientation = new float[3];
            SensorManager.getOrientation(R, orientation);
            // orientation[0] 是方位角(弧度),范围 -π 到 π
            float azimuth = (float) Math.toDegrees(orientation[0]);
            if (azimuth < 0) azimuth += 360;
            // 现在 azimuth 就是 0~360 的指南针角度
        }
    }
}

这段代码看起来简单,但有几个关键点我要提醒你:

  • 两个传感器必须同时有数据,才能调用 getRotationMatrix。我建议等两个传感器都至少收到一次数据后再开始计算。
  • 返回的方位角是弧度,记得转成角度。而且范围是 -180° 到 180°,需要映射到 0°~360°。
  • 这个 API 在手机静止或慢速运动时效果很好,但如果你快速晃动手机,结果会抖动得很厉害。

小技巧: 如果你发现指南针在水平旋转时角度跳变(比如从 359° 突然跳到 0°),可以用一个简单的平滑处理:记录上一次的角度,如果当前角度与上一次的差值超过 180°,就认为发生了跳变,手动修正一下。

16.3 避坑指南:我踩过的三个大坑

这部分内容,是我用真金白银的加班时间换来的。你仔细看。

坑一:传感器数据的时间戳不同步

我曾经在项目里直接拿 onSensorChanged 回调里的最新值去算,结果发现指南针指针疯狂抖动。后来排查发现,加速度计和磁力计的回调频率不一样,导致我拿到的两个数据可能差了 50 毫秒。50 毫秒在快速运动时,足以让姿态计算产生明显误差。

解决办法: 在回调里记录时间戳,只有当两个传感器的时间戳差值小于某个阈值(比如 20ms)时,才进行融合计算。或者使用 SensorManager.registerListener() 的第四个参数,指定一个固定的采样频率。

坑二:硬铁干扰(Hard Iron Distortion)

手机内部的扬声器、马达、甚至 SIM 卡座,都会产生固定的磁场偏移。这个偏移是恒定的,会导致你的指南针永远偏一个固定角度。我第一次发现这个问题,是在一台原型机上——指南针永远指向东北方向,不管怎么转都回不来。

解决办法: 做一次「8 字校准」——让用户拿着手机在空中画一个 ∞ 字形。Android 系统其实内置了校准算法,但需要你调用 SensorManager.getRotationMatrix() 时传入正确的参数。另外,你也可以在应用里提示用户做校准动作。

坑三:软铁干扰(Soft Iron Distortion)

这个比硬铁更隐蔽。它是由手机附近的铁磁性材料引起的,会导致磁场在某些方向上被「拉伸」或「压缩」。表现就是:你转一圈,磁力计读数的最大值和最小值不一样。

解决办法: 需要做椭圆拟合校准。简单来说,就是让用户在各个方向旋转手机,收集足够多的磁力计数据点,然后拟合出一个椭球,再用这个椭球的参数去校正原始数据。这个算法比较复杂,但网上有现成的开源库可以用。

16.4 融合后的效果与局限

经过加速度计校正后的指南针,在手机静止或缓慢移动时,精度可以做到 ±2° 以内。这在大多数导航场景下已经够用了。

但你要知道它的局限:

  • 运动状态下精度下降: 如果你拿着手机跑步,加速度计会受到运动加速度的干扰,导致姿态计算错误,进而影响磁力计的校正。这时候指南针会「飘」。
  • 无法消除所有干扰: 如果手机靠近大块金属(比如汽车车身、铁质桌子),磁力计的数据会完全失真,融合算法也救不了。
  • 需要定期校准: 每次开机后,或者手机环境变化后,最好重新做一次校准。

一句话总结: 加速度计 + 磁力计的融合,是电子指南针的「及格线」。它能解决坐标系对齐的问题,但解决不了传感器本身的物理局限。如果你需要更高精度的航向,那就得上陀螺仪了——那是下一章的内容。

16.5 知识体系图:融合流程

下面这张图,展示了从原始传感器数据到最终方位角的完整流程。我建议你把它保存下来,写代码时对照着看。

加速度计与磁力计融合流程图 加速度计 原始数据 [ax, ay, az] 磁力计 原始数据 [mx, my, mz] 姿态计算 俯仰角 / 横滚角 旋转校正 投影到水平面 融合计算 getRotationMatrix() 方位角输出 0°~360° 指南针方向 两个传感器数据缺一不可,时间戳对齐是关键

16.6 小结

这一章我们讲清楚了:为什么需要融合、怎么用 Android API 做融合、以及实际开发中会遇到哪些坑。

我个人觉得,传感器融合是 Android 开发中「看起来简单,做起来全是细节」的典型代表。你照着官方文档写代码,可能也能跑,但只有踩过那些坑之后,你才会真正理解每一行代码背后的意义。

嗯,今天就到这里。代码不多,但背后的原理值得你多花点时间消化。


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