17、传感器坐标系与旋转矩阵:Android设备坐标系、世界坐标系、旋转矩阵计算、坐标变换应用
说到传感器,很多人第一反应就是「读数据」。加速度计返回三个数,陀螺仪返回三个数,然后呢?
嗯,如果你只是拿这些原始数据做做计步器,那确实够用了。但一旦涉及到AR、VR、游戏手柄、姿态识别,你就绕不开一个核心问题——坐标系。
我个人习惯把坐标系比作「语言的语法」。数据本身是单词,坐标系就是语法规则。不懂语法,单词堆在一起也看不懂。
17.1 两个坐标系:设备坐标系 vs 世界坐标系
先搞清楚我们在跟谁打交道。
17.1.1 Android设备坐标系
这是传感器数据默认的「母语」。你从SensorEvent.values里拿到的x、y、z,都是基于这个坐标系。
- X轴:设备屏幕水平方向,向右为正
- Y轴:设备屏幕垂直方向,向上为正
- Z轴:垂直于屏幕向外,指向屏幕正面为正
举个例子。你把手机平放在桌上,屏幕朝上。这时候:
- 加速度计的Z轴读数 ≈ 9.8 m/s²(重力方向向下,但传感器感知的是「支撑力」向上,所以是正值)
- X轴和Y轴读数 ≈ 0
我在项目中遇到过一个问题:有同事把手机竖屏拿着,发现Y轴加速度接近9.8,X轴接近0。他以为传感器坏了。其实没坏——手机竖着拿,重力方向沿着Y轴负方向,传感器感知的是Y轴正方向的支撑力,所以Y轴读数是正的9.8。
17.1.2 世界坐标系
世界坐标系是「绝对参考系」。它不随手机转动而改变。
- X轴:指向东方
- Y轴:指向北方(地磁北极)
- Z轴:指向天空(垂直于地面向上)
说白了,世界坐标系就是「东、北、天」三个方向。你拿着手机原地转一圈,设备坐标系在变,但世界坐标系纹丝不动。
为什么要区分这两个坐标系?因为很多应用场景需要「绝对方向」。比如:
- AR导航:虚拟箭头要指向真正的北方,而不是手机顶部
- 游戏:玩家转动手机,角色应该朝世界坐标系里的固定方向移动
- 姿态检测:判断手机是否「水平」,需要参考重力方向(世界坐标系的Z轴)
17.2 旋转矩阵:连接两个坐标系的桥梁
现在问题来了:设备坐标系的数据,怎么转换到世界坐标系?
答案就是旋转矩阵。
旋转矩阵是一个3×3的矩阵,它描述了「从设备坐标系到世界坐标系」的旋转关系。说白了,你拿着手机转了个角度,旋转矩阵就是那个「转了多少、往哪转」的数学表达。
17.2.1 旋转矩阵的数学含义
假设你在设备坐标系里有一个向量 v_device(比如加速度计的读数),你想知道它在世界坐标系里长什么样 v_world。公式很简单:
v_world = R * v_device
其中R就是旋转矩阵。反过来,如果你知道世界坐标系里的向量,想转回设备坐标系:
v_device = R⁻¹ * v_world
因为旋转矩阵是正交矩阵,它的逆矩阵等于转置矩阵,所以 R⁻¹ = Rᵀ。这个性质在代码里很实用——不用算逆矩阵,直接转置就行。
17.2.2 Android里怎么拿到旋转矩阵
Android SDK提供了现成的方法,不用你自己手算。核心API是SensorManager.getRotationMatrix()。
float[] R = new float[9];
boolean success = SensorManager.getRotationMatrix(
R, // 输出:旋转矩阵
null, // 输出:倾斜矩阵(我们暂时不用)
gravityValues, // 加速度计数据
geomagneticValues // 地磁传感器数据
);
这个方法需要两个输入:重力方向(加速度计)和地磁方向(磁力计)。有了这两个向量,系统就能算出一个完整的旋转矩阵。
getRotationMatrix()要求加速度计和磁力计的采样时间尽量接近。我曾经踩过坑——两个传感器回调频率不一致,导致算出来的旋转矩阵「抖动」得很厉害。后来我加了一个同步队列,确保两个数据的时间戳差不超过10ms,问题才解决。
17.3 坐标变换实战:从设备坐标到世界坐标
理论说完了,来点实际的。假设你要做一个「电子水平仪」——不管手机怎么转,界面上始终显示一个指向真正北方的箭头。
步骤很简单:
- 从磁力计拿到设备坐标系下的磁场向量
- 从加速度计拿到重力向量
- 调用
getRotationMatrix()得到旋转矩阵R - 用R把磁场向量变换到世界坐标系
- 取变换后向量的Y分量(指向北方),画箭头
代码实现:
// 假设已经拿到了 gravity 和 geomagnetic
float[] R = new float[9];
float[] I = new float[9];
SensorManager.getRotationMatrix(R, I, gravity, geomagnetic);
// 设备坐标系下的磁场向量
float[] magneticDevice = new float[]{magX, magY, magZ};
// 变换到世界坐标系
float[] magneticWorld = new float[3];
SensorManager.getOrientation(R, magneticWorld);
// 注意:getOrientation 返回的是角度,不是向量
// 正确的做法是用矩阵乘法
float[] magneticWorld = new float[3];
for (int i = 0; i < 3; i++) {
magneticWorld[i] = R[i*3] * magneticDevice[0]
+ R[i*3+1] * magneticDevice[1]
+ R[i*3+2] * magneticDevice[2];
}
// 现在 magneticWorld[1] 就是指向北方的分量
嗯,这里要注意:getOrientation()返回的是欧拉角(方位角、俯仰角、横滚角),不是变换后的向量。如果你需要的是向量本身,老老实实做矩阵乘法。
17.4 旋转矩阵的另一种玩法:欧拉角
有时候你不需要完整的向量变换,只需要知道手机「转了多少度」。这时候可以用欧拉角。
Android提供了SensorManager.getOrientation(),从旋转矩阵里提取三个角度:
float[] orientation = new float[3];
SensorManager.getOrientation(R, orientation);
// orientation[0]:方位角(绕Z轴,0=北,顺时针为正)
// orientation[1]:俯仰角(绕X轴,抬头为正)
// orientation[2]:横滚角(绕Y轴,右侧抬起为正)
这三个角度的单位是弧度,记得转成角度制:Math.toDegrees()。
我在做AR导航项目时,就用方位角来判断手机指向的方向。但有个坑——当手机接近水平时,方位角会变得很不稳定。这是因为「万向锁」问题:俯仰角接近±90°时,方位角和横滚角无法唯一确定。
17.5 知识体系总览
下面这张图总结了本章的核心逻辑:
17.6 避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间。
- 传感器数据要滤波:原始数据噪声很大,直接算旋转矩阵会抖得你怀疑人生。我习惯用低通滤波,或者直接上
Sensor.TYPE_ROTATION_VECTOR——这个虚拟传感器内部已经做了融合和滤波。 - 地磁传感器容易受干扰:靠近音箱、金属桌腿、甚至某些手机壳,磁场都会偏。我在办公室调试时发现方位角差了20°,后来发现是桌子下面有个铁质文件柜。
- getRotationMatrix()不是万能的:它要求设备不能处于自由落体状态(加速度计读不到重力),也不能处于强磁场环境。如果遇到这些极端情况,可以考虑用陀螺仪积分来辅助。
- 坐标系方向别搞反:Android设备坐标系和OpenGL的坐标系不一样。OpenGL是右手系,Y轴向上;Android设备坐标系是Y轴向上,但Z轴指向屏幕外。如果你在做OpenGL渲染,记得做一次坐标系转换。
嗯,坐标系和旋转矩阵这块内容,说难不难,说简单也不简单。关键是要理解「为什么需要它」以及「怎么用它」。下次你拿到传感器数据,别急着用,先想想——这个数据是在哪个坐标系下的?我要把它变到哪去?想清楚了,代码自然就写对了。
公众号:蓝海资料掘金营,微信deep3321