1. 组合导航坐标系实战:东北天与北东地的IMU数据转换解析
1.1 从一个真实的调试现场说起
去年帮一个做室内巡检机器人的团队排查定位漂移问题,现象很典型:机器人直线走三米,轨迹在rviz里画出来是一条弧线,偏了将近四十厘米。他们用的是某国产九轴IMU加轮式里程计做融合,算法跑的是开源的ESKF框架。我拿到数据包第一件事不是看滤波参数,而是把IMU的原始角速度和加速度单独拎出来,手动积分了十秒钟,发现重力分量落在了Y轴上——正常应该是Z轴。问题当场就清楚了:IMU固件输出的坐标系是北东地(NED),而他们的融合算法默认按东北天(ENU)处理,两个坐标系之间差了一个旋转,谁都没做转换。
这个坑在组合导航里太常见了。东北天和北东地这两个坐标系,一个来自测绘和机器人领域,一个来自航空航天领域,两拨人各用各的习惯,等到IMU、激光雷达、视觉、轮速计这些传感器凑到一起做融合的时候,坐标系不统一的问题就会以各种诡异的形式冒出来——可能是重力方向不对,可能是偏航角差90度,也可能是轨迹整体镜像翻转。更麻烦的是,这类问题往往不会让程序崩溃,只会让定位结果慢慢跑偏,排查起来非常费劲。
这篇内容就是把这个事情彻底讲透。我会从两个坐标系的定义差异讲起,把旋转矩阵的推导过程一步步写出来,然后给出可直接复用的Python转换代码,最后分享几个我在实际项目中踩过的坑和排查技巧。不管你是刚接触组合导航的学生,还是正在调试多传感器融合的工程师,看完应该都能直接上手操作。
1.2 谁需要关心这个转换
如果你在做以下几类事情,坐标系转换就是绕不过去的基本功:
- 用IMU做姿态解算或惯性导航,需要把加速度投影到正确的导航系下
- 做激光雷达与IMU的联合标定,外参矩阵里必然包含坐标系对齐
- 跑VINS、LIO-SAM、FAST-LIO这类融合定位算法,需要确认IMU输出坐标系与算法期望是否一致
- 做轮式里程计与IMU融合,需要把里程计的速度方向与IMU的航向对齐
- 使用PX4、ArduPilot等飞控的日志做二次开发,需要理解飞控内部的坐标系约定
这些场景的共同点是:只要有一个环节的坐标系搞错了,整个系统的输出就是错的,而且错误往往很隐蔽。
2. 东北天与北东地:两个坐标系到底差在哪里
2.1 东北天(ENU)的定义与使用场景
东北天坐标系,英文是East-North-Up,简称ENU。它的定义很直观:
- X轴指向正东方向
- Y轴指向正北方向
- Z轴指向天顶方向,也就是垂直于当地水平面向上
这个坐标系是右手系。你可以伸出右手,大拇指指向东,食指指向北,中指自然就指向上了。
ENU在机器人领域和测绘领域用得最多。ROS里面的地图坐标系、激光雷达的默认坐标系、大多数SLAM算法的世界坐标系,默认都是ENU或者与之等价的变体。原因也很简单:机器人在地面上跑,Z轴朝上符合直觉,重力加速度在Z轴上是负值(约-9.81 m/s²),俯仰角和横滚角的符号也跟人的直觉一致——抬头是正俯仰,右倾是正横滚。
测绘领域用ENU是因为它跟当地水平面绑定,做局部区域的高精度定位时,不需要考虑地球曲率带来的复杂变换。你打开QGIS或者ArcGIS,建一个局部投影坐标系,底层的数学基础就是ENU这一套。
2.2 北东地(NED)的定义与来源
北东地坐标系,英文是North-East-Down,简称NED。它的定义同样很清晰:
- X轴指向正北方向
- Y轴指向正东方向
- Z轴指向地心方向,也就是垂直于当地水平面向下
这也是一个右手系。右手大拇指指北,食指指东,中指自然指向下。
NED是航空航天领域的标准坐标系。飞机、导弹、无人机的姿态描述,几乎全部基于NED。原因在于航空器主要关心的是前向运动,把X轴定义为机头朝向(在导航系下就是北),Z轴向下则让重力加速度变成正值(+9.81 m/s²),在动力学方程里写起来更顺手。
大多数工业级IMU和航空级惯导系统,出厂默认的输出坐标系就是NED。你拿到一个IMU模块,手册上写着“输出符合右手坐标系,X朝前,Y朝右,Z朝下”,这其实就是机体坐标系下的NED约定。当这个IMU被安装到机器人上时,如果机器人本身用的是ENU导航系,就必须做转换。
2.3 两个坐标系的旋转关系推导
现在来推导ENU和NED之间的旋转矩阵。这是整篇内容最核心的部分,我会把每一步都写清楚。
假设我们站在地面上,头顶是天,脚下是地。ENU坐标系的三轴方向是:
- X_ENU = 东
- Y_ENU = 北
- Z_ENU = 天
NED坐标系的三轴方向是:
- X_NED = 北
- Y_NED = 东
- Z_NED = 地
观察一下对应关系:
- NED的X轴(北)等于ENU的Y轴
- NED的Y轴(东)等于ENU的X轴
- NED的Z轴(地)等于ENU的Z轴的反方向
所以,一个向量在ENU下的坐标 (x_e, y_e, z_e) 转换到NED下,应该是:
- x_n = y_e (北方向分量)
- y_n = x_e (东方向分量)
- z_n = -z_e (地方向分量,即天的反方向)
写成矩阵形式:
[x_n] [0 1 0] [x_e] [y_n] = [1 0 0] [y_e] [z_n] [0 0 -1][z_e]这个矩阵就是ENU到NED的旋转矩阵,记作 R_enu_to_ned。
反过来,NED到ENU的转换就是它的转置(因为旋转矩阵是正交矩阵,转置等于逆):
[x_e] [0 1 0] [x_n] [y_e] = [1 0 0] [y_n] [z_e] [0 0 -1][z_n]注意这个矩阵的行列式是-1,说明它不是一个纯旋转,而是旋转加镜像。这一点很重要,后面讲姿态转换的时候会用到。
2.4 为什么不能简单交换XY轴了事
很多人第一次接触这个转换,会觉得“不就是把XY换一下,Z取反吗”,然后写代码的时候只交换了位置分量,忘了处理姿态。结果位置看起来对了,但姿态角全乱套。
问题出在:姿态是一个旋转,旋转的变换规则和向量的变换规则不一样。向量在坐标系变换下是左乘旋转矩阵,而姿态(用旋转矩阵表示)的变换是相似变换:R_new = C * R_old * C^T,其中C是坐标系变换矩阵。
如果你只交换了位置分量,没有对姿态做相似变换,那么IMU输出的姿态角在ENU下就是错的。具体表现是:偏航角可能差了90度,横滚和俯仰的符号可能反了。这种错误在静止状态下可能看不出来(因为姿态角接近零),一旦开始运动就会暴露。
3. IMU数据转换的完整实操流程
3.1 确认IMU的原始坐标系
动手写转换代码之前,必须先确认IMU输出的到底是什么坐标系。这一步不能靠猜,要靠查手册和实测。
查手册是最直接的方法。工业IMU的数据手册里通常会有一节叫“Coordinate System Definition”或者“Axis Orientation”,里面会画一个箭头图,标明XYZ轴的方向。如果手册写的是“X forward, Y right, Z down”,那在机体坐标系下就是NED约定。如果写的是“X right, Y forward, Z up”,那可能是ENU的变体。
手册找不到或者写得含糊的时候,就做静态实测。把IMU静止水平放置,读取加速度计的三轴输出。重力加速度的方向永远指向地心,所以:
- 如果Z轴输出约+9.81,说明Z轴朝下,是NED
- 如果Z轴输出约-9.81,说明Z轴朝上,是ENU
- 如果X或Y轴输出约±9.81,说明IMU没有水平放置,或者坐标系定义比较特殊
这个测试我做过无数次,非常可靠。唯一需要注意的是,有些IMU的加速度计输出单位是g而不是m/s²,这时候读数应该是±1.0左右。
3.2 加速度与角速度的转换
确认了原始坐标系之后,加速度和角速度的转换就很简单了,直接套用前面推导的矩阵。
假设IMU输出的是NED下的数据,我们要转到ENU:
import numpy as np def ned_to_enu_vector(vec_ned): """ 将NED坐标系下的三维向量转换到ENU坐标系 输入: vec_ned = [x_n, y_n, z_n] 输出: vec_enu = [x_e, y_e, z_e] """ x_n, y_n, z_n = vec_ned x_e = y_n y_e = x_n z_e = -z_n return np.array([x_e, y_e, z_e]) # 示例:NED下的重力加速度 g_ned = np.array([0.0, 0.0, 9.81]) g_enu = ned_to_enu_vector(g_ned) print(f"ENU下的重力: {g_enu}") # 输出 [0, 0, -9.81]角速度的转换规则和加速度完全一样,因为角速度也是一个三维向量,在坐标系变换下遵循相同的规则。但这里有一个容易忽略的点:角速度的物理意义是绕某根轴的旋转速率,坐标系变换之后,旋转轴也跟着变了,所以直接套用向量变换矩阵是正确的。
注意:有些IMU输出的角速度是在机体坐标系下的,而加速度可能是在导航系下的,或者反过来。这种情况必须先统一到同一个坐标系,再做转换。查手册的时候一定要看清楚每个量的参考坐标系。
3.3 姿态角的转换:欧拉角、旋转矩阵与四元数
姿态的转换比向量复杂,因为姿态描述的是一个旋转关系,不是简单的三维向量。我分别讲三种常见表示方法的转换。
欧拉角转旋转矩阵再转换
欧拉角是最直观的姿态表示,但也是最容易出错的。不同厂商对欧拉角的定义不一样:有的是ZYX顺序,有的是XYZ顺序;有的偏航角以北为0,有的以东为0。转换之前必须确认清楚。
假设我们已经把欧拉角转成了旋转矩阵R_ned(表示机体在NED下的姿态),要转到ENU下:
def rotation_ned_to_enu(R_ned): """ 将NED下的旋转矩阵转换到ENU下 C是NED到ENU的坐标系变换矩阵 """ C = np.array([ [0, 1, 0], [1, 0, 0], [0, 0, -1] ]) R_enu = C @ R_ned @ C.T return R_enu这个公式的推导逻辑是:R_ned把机体坐标系下的向量转到NED导航系,我们想要一个矩阵把机体向量转到ENU导航系。中间插入C和C的逆,就完成了导航系的切换。
四元数的转换
四元数的转换稍微麻烦一点,因为四元数不能直接左乘右乘矩阵。有两种做法:
第一种是把四元数转成旋转矩阵,做完相似变换再转回四元数。这种方法最稳妥,不容易出错。
第二种是直接构造一个表示坐标系变换的四元数,然后做四元数乘法。NED到ENU的坐标系变换,可以理解为绕某个轴旋转180度再翻转。具体来说,这个变换等价于:先绕X轴旋转180度,再绕新的Z轴旋转90度。对应的四元数是:
def quaternion_ned_to_enu(q_ned): """ 将NED下的姿态四元数转换到ENU q_ned格式: [w, x, y, z] """ # 坐标系变换四元数: 绕X轴180度再绕Z轴90度 # 等价于四元数 [0, 1, 0, 0] 和 [0.707, 0, 0, 0.707] 的乘积 q_c = np.array([0.0, 0.70710678, 0.70710678, 0.0]) # 四元数乘法: q_enu = q_c * q_ned * q_c^(-1) q_c_inv = np.array([q_c[0], -q_c[1], -q_c[2], -q_c[3]]) def quat_multiply(q1, q2): w1, x1, y1, z1 = q1 w2, x2, y2, z2 = q2 return np.array([ w1*w2 - x1*x2 - y1*y2 - z1*z2, w1*x2 + x1*w2 + y1*z2 - z1*y2, w1*y2 - x1*z2 + y1*w2 + z1*x2, w1*z2 + x1*y2 - y1*x2 + z1*w2 ]) q_temp = quat_multiply(q_c, q_ned) q_enu = quat_multiply(q_temp, q_c_inv) return q_enu我个人的建议是:除非你对四元数乘法非常熟练,否则一律走“四元数转旋转矩阵,转换后再转回四元数”的路线。多几行代码,但出错概率低很多。
3.4 完整转换代码与验证方法
把上面的片段整合成一个完整的转换类:
import numpy as np class NED_ENU_Converter: """NED与ENU坐标系转换工具""" # NED到ENU的坐标系变换矩阵 C_ned_to_enu = np.array([ [0, 1, 0], [1, 0, 0], [0, 0, -1] ]) # ENU到NED的坐标系变换矩阵(转置) C_enu_to_ned = C_ned_to_enu.T @classmethod def vector_ned_to_enu(cls, vec): """向量从NED转到ENU""" return cls.C_ned_to_enu @ vec @classmethod def vector_enu_to_ned(cls, vec): """向量从ENU转到NED""" return cls.C_enu_to_ned @ vec @classmethod def rotation_ned_to_enu(cls, R): """旋转矩阵从NED转到ENU""" return cls.C_ned_to_enu @ R @ cls.C_ned_to_enu.T @classmethod def rotation_enu_to_ned(cls, R): """旋转矩阵从ENU转到NED""" return cls.C_enu_to_ned @ R @ cls.C_enu_to_ned.T @classmethod def euler_ned_to_enu(cls, roll, pitch, yaw): """ 欧拉角从NED转到ENU 输入: NED下的roll, pitch, yaw(弧度) 输出: ENU下的roll, pitch, yaw(弧度) """ # 先转旋转矩阵 R_ned = cls._euler_to_rotation(roll, pitch, yaw) # 转换 R_enu = cls.rotation_ned_to_enu(R_ned) # 转回欧拉角 return cls._rotation_to_euler(R_enu) @staticmethod def _euler_to_rotation(roll, pitch, yaw): """ZYX顺序欧拉角转旋转矩阵""" cr, sr = np.cos(roll), np.sin(roll) cp, sp = np.cos(pitch), np.sin(pitch) cy, sy = np.cos(yaw), np.sin(yaw) R = np.array([ [cy*cp, cy*sp*sr - sy*cr, cy*sp*cr + sy*sr], [sy*cp, sy*sp*sr + cy*cr, sy*sp*cr - cy*sr], [-sp, cp*sr, cp*cr] ]) return R @staticmethod def _rotation_to_euler(R): """旋转矩阵转ZYX顺序欧拉角""" pitch = -np.arcsin(R[2, 0]) if np.abs(np.cos(pitch)) > 1e-6: roll = np.arctan2(R[2, 1], R[2, 2]) yaw = np.arctan2(R[1, 0], R[0, 0]) else: roll = 0.0 yaw = np.arctan2(-R[0, 1], R[1, 1]) return roll, pitch, yaw验证转换是否正确,有一个简单的方法:取一个已知的向量,手动算一遍,看代码输出是否一致。比如NED下的北方向单位向量[1,0,0],转到ENU下应该是[0,1,0](东方向为X,北方向为Y)。再比如NED下的重力[0,0,9.81],转到ENU下应该是[0,0,-9.81]。
另一个验证方法是检查旋转矩阵的正交性:R @ R.T 应该等于单位矩阵,行列式应该等于1。如果转换后的旋转矩阵不满足这两个条件,说明转换过程有问题。
4. 实际项目中的典型问题与排查技巧
4.1 重力方向不对导致的高度漂移
这是最常见的症状。IMU在静止状态下,如果重力分量没有正确落在Z轴上,滤波器会认为系统有一个持续的水平加速度,导致速度和位置慢慢漂移。
排查方法:把IMU静止放置,采集一分钟的加速度数据,取平均。正常情况下,ENU下应该是[0, 0, -9.81]左右,NED下应该是[0, 0, 9.81]左右。如果X或Y方向有超过0.1 m/s²的分量,要么是IMU没有水平放置,要么是坐标系搞错了。
我遇到过一个案例:IMU的Z轴输出是-9.81,但算法期望的是+9.81,结果滤波器一直在补偿一个不存在的重力,高度估计直接发散。后来发现是IMU固件升级后,默认输出坐标系从NED变成了ENU,但配置文件没有更新。
4.2 偏航角差90度的诡异现象
如果发现机器人的航向总是差90度,而且怎么调参数都改不过来,大概率是坐标系转换的问题。
具体来说,ENU下的偏航角是以东为0度、逆时针为正,而NED下的偏航角是以北为0度、顺时针为正。两者之间差了一个90度的偏移和符号翻转。
转换公式是:yaw_enu = 90° - yaw_ned(角度制),或者 yaw_enu = π/2 - yaw_ned(弧度制)。
这个公式看起来简单,但实际用的时候要注意角度范围。NED下的偏航角通常在-180°到180°之间,转换后需要归一化到同样的范围。
4.3 多传感器融合时的坐标系对齐检查清单
做多传感器融合的时候,坐标系对齐是第一步,也是最容易出错的一步。我整理了一个检查清单,每次新项目都过一遍:
| 检查项 | 检查方法 | 常见问题 |
|---|---|---|
| IMU输出坐标系 | 查手册+静态重力测试 | 手册写NED,实际输出ENU |
| 激光雷达坐标系 | 查手册+观察点云方向 | 雷达坐标系与IMU不一致 |
| 相机坐标系 | 查手册+观察图像方向 | 光学坐标系Z朝前,与IMU不同 |
| 轮速计坐标系 | 查手册+手动转动轮子 | 速度方向定义与IMU相反 |
| 融合算法期望坐标系 | 查文档+看源码 | 不同算法默认坐标系不同 |
| 外参矩阵方向 | 检查标定结果 | T_imu_lidar和T_lidar_imu搞反 |
这个清单看起来简单,但每一条我都见过有人踩坑。特别是最后一条,外参矩阵的方向搞反,会导致标定结果完全错误,而且很难从数据上看出来。
4.4 用Python快速验证坐标系转换
我习惯在正式集成之前,先用Python写一个小脚本验证转换逻辑。这个脚本做三件事:
第一,生成一组模拟的IMU数据,包括静止状态和运动状态。第二,分别用NED和ENU的假设去处理这组数据。第三,对比两种处理方式的结果差异,确认转换逻辑正确。
import numpy as np import matplotlib.pyplot as plt def simulate_imu_data(duration=10.0, dt=0.01): """模拟IMU数据:静止5秒,然后绕Z轴旋转""" t = np.arange(0, duration, dt) n = len(t) # 真实角速度(ENU下) omega_enu = np.zeros((n, 3)) omega_enu[t > 5.0, 2] = 0.5 # 绕Z轴旋转 # 真实加速度(ENU下,包含重力) acc_enu = np.zeros((n, 3)) acc_enu[:, 2] = -9.81 # 重力 # 转换到NED C = np.array([[0, 1, 0], [1, 0, 0], [0, 0, -1]]) omega_ned = (C @ omega_enu.T).T acc_ned = (C @ acc_enu.T).T return t, omega_enu, acc_enu, omega_ned, acc_ned # 验证转换 t, omega_enu, acc_enu, omega_ned, acc_ned = simulate_imu_data() # 检查重力方向 print(f"ENU下重力: {acc_enu[0]}") # 应该是 [0, 0, -9.81] print(f"NED下重力: {acc_ned[0]}") # 应该是 [0, 0, 9.81] # 检查角速度 print(f"ENU下角速度: {omega_enu[600]}") # 应该是 [0, 0, 0.5] print(f"NED下角速度: {omega_ned[600]}") # 应该是 [0, 0, -0.5]运行这个脚本,如果输出符合预期,说明转换逻辑是对的。这个方法比直接上硬件调试快得多,也安全得多。
5. 进阶话题:从坐标系转换到多传感器标定
5.1 坐标系转换与IMU标定的关系
坐标系转换解决的是“同一个物理量在不同坐标系下的表示”问题,而IMU标定解决的是“IMU输出与真实值之间的偏差”问题。两者经常被混在一起,但实际上是两个独立的问题。
一个典型的场景:IMU安装到机器人上,安装角度有偏差,导致IMU的X轴与机器人的前进方向不重合。这个偏差角就是安装误差,需要通过标定来估计。标定出来的结果通常是一个旋转矩阵R_extrinsic,表示IMU坐标系到机器人坐标系的变换。
如果IMU输出的是NED,而机器人用的是ENU,那么完整的变换是:R_total = R_enu_to_ned * R_extrinsic。先做安装误差补偿,再做坐标系转换,顺序不能反。
5.2 激光雷达与IMU联合标定中的坐标系问题
激光雷达和IMU的联合标定,本质上是在估计两个传感器之间的外参,包括旋转和平移。这个过程中,坐标系转换是基础中的基础。
假设激光雷达输出的是ENU下的点云,IMU输出的是NED下的姿态,那么标定的时候必须先把两者统一到同一个坐标系。通常的做法是:把IMU数据转到ENU下,然后以ENU为基准做标定。
标定完成之后,得到的外参矩阵T_lidar_imu是在ENU下的。如果后续要用于NED下的算法,还需要把外参矩阵也做相应的转换。这个转换很容易被忽略,导致标定结果在另一个坐标系下不可用。
5.3 坐标系转换在VINS和LIO中的实际应用
VINS-Mono和LIO-SAM这类融合定位算法,对坐标系有明确的假设。VINS-Mono默认世界坐标系是ENU,重力方向是-Z。LIO-SAM默认世界坐标系也是ENU,但它的IMU预积分部分对重力方向的处理略有不同。
如果你用的IMU输出是NED,直接喂给这些算法,结果一定是错的。必须在数据进入算法之前做转换。转换的位置通常有两个选择:一是在IMU驱动层做,二是在算法前端做。
我个人的建议是在驱动层做。原因很简单:驱动层做一次转换,后面所有算法都不用再操心坐标系问题。如果在算法前端做,每个算法都要写一遍转换代码,容易遗漏,也容易写错。
6. 几个我踩过的坑和对应的解决方案
6.1 坑一:以为所有IMU都是NED
刚开始做组合导航的时候,我想当然地认为所有IMU都是NED输出,结果遇到一个国产IMU,手册上写的是“符合右手坐标系”,但没有明确说是NED还是ENU。实测发现Z轴输出是-9.81,才知道是ENU。
教训:永远不要假设IMU的坐标系,一定要查手册或者实测。手册写得不清楚的时候,静态重力测试是最可靠的方法。
6.2 坑二:转换矩阵写反了方向
有一次调试,发现转换后的数据完全不对,排查了半天才发现是转换矩阵写反了。NED到ENU的矩阵和ENU到NED的矩阵互为转置,但我在代码里把两个矩阵搞混了。
教训:转换矩阵的方向一定要标注清楚,变量命名要包含方向信息,比如C_ned_to_enu和C_enu_to_ned,不要用C1和C2这种含糊的命名。
6.3 坑三:忽略了角速度的坐标系
加速度的坐标系问题比较容易发现,因为重力方向很明显。角速度的坐标系问题就隐蔽多了,因为静止的时候角速度是零,看不出区别。
有一次做旋转测试,发现偏航角的积分结果与预期相反,排查后发现是角速度的坐标系没有转换。IMU输出的是NED下的角速度,我直接拿去积分ENU下的姿态,符号自然反了。
教训:加速度和角速度要一起转换,不要只转一个。测试的时候要包含旋转运动,不能只做静态测试。
6.4 坑四:四元数转换后没有归一化
四元数在多次乘法之后,数值精度会下降,导致不再是单位四元数。如果不做归一化,姿态解算会慢慢漂移。
教训:每次四元数运算之后都要归一化。这个操作开销很小,但能避免很多莫名其妙的问题。
def normalize_quaternion(q): """归一化四元数""" norm = np.linalg.norm(q) if norm < 1e-10: return np.array([1.0, 0.0, 0.0, 0.0]) return q / norm6.5 坑五:欧拉角的万向锁问题
欧拉角在俯仰角接近±90度的时候会出现万向锁,导致横滚和偏航无法区分。这个问题在坐标系转换中也会遇到:如果转换前的俯仰角接近90度,转换后的欧拉角可能完全错误。
解决方案是尽量用旋转矩阵或四元数做中间表示,只在最后输出的时候转成欧拉角。如果确实需要处理大俯仰角的场景,考虑用旋转向量或者直接输出旋转矩阵。
7. 工具与资源推荐
7.1 Python库
- numpy:矩阵运算的基础,所有转换都离不开
- scipy.spatial.transform:提供了Rotation类,支持欧拉角、旋转矩阵、四元数之间的相互转换,比手写代码可靠得多
- transforms3d:专门做三维坐标变换的库,API设计得很清晰
用scipy的Rotation类做转换,代码会简洁很多:
from scipy.spatial.transform import Rotation as R # NED下的姿态 r_ned = R.from_euler('ZYX', [yaw_ned, pitch_ned, roll_ned]) # 转换到ENU C = np.array([[0, 1, 0], [1, 0, 0], [0, 0, -1]]) R_ned_matrix = r_ned.as_matrix() R_enu_matrix = C @ R_ned_matrix @ C.T r_enu = R.from_matrix(R_enu_matrix) # 输出ENU下的欧拉角 yaw_enu, pitch_enu, roll_enu = r_enu.as_euler('ZYX')7.2 可视化工具
调试坐标系问题的时候,可视化能帮大忙。我常用的组合是:
- rviz:ROS的可视化工具,可以同时显示IMU姿态、点云、轨迹,直观对比不同坐标系下的数据
- matplotlib:画时间序列曲线,对比转换前后的加速度、角速度、姿态角
- PlotJuggler:专门做时间序列可视化的工具,支持实时数据流,适合调试在线系统
7.3 参考文档
- ROS REP-103:定义了ROS中的标准坐标系约定,包括ENU和机体坐标系
- PX4的坐标系文档:详细说明了飞控中使用的NED和FRD约定
- 各IMU厂商的数据手册:确认原始坐标系的第一手资料
8. 写在最后
坐标系转换这个事情,说起来简单,做起来坑多。核心就是那个3x3的矩阵,但围绕这个矩阵的确认、验证、调试,往往要花掉比预期多得多的时间。
我的经验是:新项目上手第一件事,就是把所有传感器的坐标系确认一遍,写一个最小的测试程序验证转换逻辑,然后再开始做融合。这个前期投入可能只要半天,但能省掉后面几天的调试时间。
另外,转换代码一定要写单元测试。取几个已知的输入输出对,验证转换函数的正确性。这样以后改代码的时候,不用担心不小心改坏了转换逻辑。
最后分享一个我常用的快速检查方法:把IMU静止放置,看加速度的Z分量。如果是-9.81,就是ENU;如果是+9.81,就是NED。这个方法五秒钟就能做完,但能避免很多低级错误。