2. 数学基础(上):三维空间中的坐标系定义

做多传感器融合,第一件事不是调算法,而是把坐标系理清楚。我见过太多项目,标定参数算得再准,最后因为坐标系定义不一致,点云投影出来全是歪的。说白了,坐标系就是所有传感器的「共同语言」。你想想看,激光雷达说「前方 10 米」,IMU 说「加速度朝东」,如果它们不在同一个坐标系里说话,那融合出来的结果就是一团浆糊。

这一节,我们先把三维空间里的几个核心坐标系讲透。包括世界系、车身系、传感器系,以及贯穿始终的右手定则。嗯,这里要注意,每个坐标系的选择都有它的物理意义和工程习惯,不是随便定的。

2.1 右手定则:三维空间的「交通规则」

三维坐标系的方向怎么定?右手定则就是答案。伸出你的右手,拇指、食指、中指互相垂直——拇指指向 X 轴正方向,食指指向 Y 轴正方向,中指指向 Z 轴正方向。这就是右手坐标系。

为什么一定要用右手系?因为旋转方向也是由右手定则定义的:四指弯曲的方向就是正旋转方向。如果团队里有人用了左手系,有人用了右手系,那旋转矩阵算出来符号全是反的。我曾经在一个项目里接手过别人的代码,发现点云绕 Z 轴旋转时方向总是不对,查了两天才发现是坐标系习惯不一致——从那以后,我要求团队所有传感器驱动里必须显式声明坐标系手性。

核心原则:自动驾驶领域几乎全部使用右手坐标系。ROS、Apollo、Autoware 等主流框架均默认右手系。如果你看到左手系代码,大概率是某个老旧传感器驱动没更新。

旋转正方向也遵循右手定则:

  • 绕 X 轴正旋转:从 Y 轴转向 Z 轴(右手拇指指向 X 正方向)
  • 绕 Y 轴正旋转:从 Z 轴转向 X 轴
  • 绕 Z 轴正旋转:从 X 轴转向 Y 轴

这个规则在后续章节计算旋转矩阵、四元数、欧拉角时都会反复用到。记牢它,能省掉一半的调试时间。

2.2 世界坐标系:一切测量的「参考」

世界坐标系,也叫全局坐标系,是描述车辆和传感器在真实世界中位置的基准。它固定在大地上,不随车辆移动而改变。

在自动驾驶中,世界坐标系通常这样定义:

  • X 轴:指向东(或指向北,取决于具体约定)
  • Y 轴:指向北(或指向东,与 X 轴垂直)
  • Z 轴:指向天(垂直于地面向上)

常见的世界坐标系有几种:

坐标系 X 轴 Y 轴 Z 轴 典型应用
ENU(东-北-天) 东 北 天 GPS/IMU 融合、Apollo
NED(北-东-地) 北 东 地 航空、惯性导航
UTM 投影 东向 北向 高程 高精地图、全局路径规划

我个人习惯在量产项目里统一使用 ENU 坐标系。为什么?因为 ROS 的 map 和 odom 帧默认就是 ENU 约定,和 GPS 输出的经纬高转换过来也最直接。如果你用 NED,那 Z 轴朝下,激光雷达点云的高度值全是负数,看着就别扭。

小技巧:世界坐标系的原点通常选在车辆启动位置、地图原点或某个固定基站。在测试场里,我一般把原点设在场地中心,这样所有坐标值都不会太大,调试时一眼就能看出数据是否合理。

2.3 车身坐标系:车辆的「本体感知」

车身坐标系固定在车辆上,随车移动。它描述的是「车自己怎么看世界」。定义方式如下:

  • 原点:通常在后轴中心(或车辆质心)
  • X 轴:指向车辆前进方向(车头方向)
  • Y 轴:指向车辆左侧(符合右手系,Z 轴朝上时 Y 轴指向左)
  • Z 轴:指向车顶(垂直于地面向上)

这里有个容易搞混的地方:有些车厂把 Y 轴定义为指向右侧。如果你用右手定则,Z 轴朝上时 Y 轴指向左才是正方向。指向右的话,就变成左手系了。我记得有一次集成某供应商的车辆动力学模型,他们输出的侧向速度符号总是反的,最后发现是车身坐标系 Y 轴方向定义不同。从那以后,我要求所有外协模块必须提供坐标系定义文档。

车身坐标系在以下场景中直接使用:

  • 车辆动力学控制(速度、加速度、横摆角速度)
  • 底盘 CAN 信号解析(方向盘转角、轮速)
  • IMU 原始数据(加速度、角速度)
  • 障碍物相对位置(前向碰撞预警、盲区监测)

2.4 传感器坐标系:每个传感器的「小世界」

每个传感器都有自己的坐标系。激光雷达、摄像头、毫米波雷达、IMU,它们的坐标系定义各不相同。做融合的第一步,就是把它们统一到车身坐标系下。

常见的传感器坐标系定义:

传感器 X 轴 Y 轴 Z 轴 说明
激光雷达 正前方 左侧 上方 多数 LiDAR 遵循 ROS REP-105
摄像头 右方 下方 前方 图像坐标系(像素 u-v)需额外转换
毫米波雷达 正前方 左侧 上方 与 LiDAR 类似,但原点在雷达中心
IMU 正前方 左侧 上方 加速度/角速度输出在此坐标系下

摄像头比较特殊。它的原始数据是像素坐标 (u, v),需要先转换到相机坐标系(3D 空间),再转到车身坐标系。相机坐标系通常定义 Z 轴指向拍摄方向,X 轴向右,Y 轴向下——这和右手系不太一样,转换时要特别注意。

避坑指南:我曾经遇到过一款激光雷达,它的官方 ROS 驱动里把 Y 轴定义为指向右侧。这意味着它的点云数据在 Rviz 里显示是镜像的。如果你不做坐标系转换直接使用,那融合出来的障碍物位置会左右颠倒。解决办法:在驱动层加一个绕 Z 轴旋转 180° 的变换,或者手动修改坐标系定义。

2.5 坐标系关系图:从传感器到世界

下面这张图展示了四个坐标系之间的层级关系。理解这个层级,是后续做坐标变换的基础。

坐标系层级关系图 坐标系层级关系 世界坐标系 (World) 车身坐标系 (Body) LiDAR 坐标系 Camera 坐标系 IMU 坐标系 T_world_to_body T_body_to_lidar T_body_to_camera T_body_to_imu 说明: 每个传感器坐标系通过外参矩阵 T 变换到车身坐标系,车身坐标系再通过定位信息变换到世界坐标系。 T 矩阵包含旋转和平移,是后续章节的核心内容。

从图中可以看出,所有传感器数据最终都要变换到世界坐标系下,才能进行融合和决策。中间经过车身坐标系作为「」。这样做的好处是:每个传感器的外参标定只针对车身坐标系,不直接与世界坐标系耦合。如果车辆移动了,只需要更新车身到世界的变换,传感器到车身的变换保持不变。

2.6 坐标系变换的数学表示

坐标系之间的变换,用齐次变换矩阵 T 表示。它是一个 4×4 矩阵:

T = [ R   t ]
    [ 0   1 ]

其中:
R:3×3 旋转矩阵
t:3×1 平移向量
0:1×3 零向量

举个例子,把激光雷达坐标系下的点 p_lidar 变换到车身坐标系:

p_body = T_body_to_lidar * p_lidar

这里的 T_body_to_lidar 就是激光雷达的外参矩阵。它描述了激光雷达坐标系相对于车身坐标系的位姿。标定的过程,就是求解这个矩阵的过程。

重要概念:变换矩阵的逆矩阵表示反向变换。如果你有 T_body_to_lidar,那么 T_lidar_to_body = inv(T_body_to_lidar)。在代码里,我习惯用 Eigen 库的 .inverse() 方法,但要注意数值稳定性——如果矩阵接近奇异,逆矩阵会很不准。

2.7 坐标系定义检查清单

在实际项目中,我总结了一份检查清单。每次接入新传感器或新算法模块时,我都会逐条核对:

  1. 手性确认:所有坐标系都是右手系吗?有没有混入左手系?
  2. 轴向定义:每个坐标系的 X、Y、Z 正方向是否明确?有没有文档?
  3. 原点位置:传感器坐标系的原点在传感器物理中心的哪个位置?是光学中心还是安装面?
  4. 旋转正方向:绕各轴的正旋转方向是否遵循右手定则?
  5. 单位统一:所有坐标系使用米(m)还是毫米(mm)?角度用弧度(rad)还是度(deg)?
  6. 时间戳对齐:坐标系变换时,对应的数据时间戳是否一致?

嗯,这六条看起来简单,但每一条都踩过坑。尤其是第 5 条,单位问题——我见过有人把毫米当成米传进来,结果障碍物距离直接差了 1000 倍。还好是在仿真阶段发现的,不然实车测试就危险了。

个人习惯:我在代码里统一使用 double 类型存储坐标值,单位固定为米和弧度。所有传感器驱动层做一次单位转换,后续模块不再关心单位问题。这样能避免很多低级错误。