2、坐标系与坐标变换:世界坐标系、车辆坐标系、传感器坐标系、坐标变换矩阵、四元数与欧拉角、工程中的坐标系管理

说实话,坐标系这个话题,我见过太多人栽跟头了。

刚入行那会儿,我觉得坐标系不就是个数学概念嘛,有啥好讲的?直到有一次,我调试的感知模块死活跟规划对不上——明明前方有个障碍物,规划路径却直接穿过去了。查了三天,最后发现是激光雷达坐标系和车辆坐标系差了90度没转过来。嗯,从那以后,我再也不敢小看坐标系了。

2.1 世界坐标系:我们到底在哪儿?

世界坐标系,说白了就是给整个物理世界定一个“绝对位置”。

在自动驾驶里,我们通常用UTM坐标系(通用横轴墨卡托投影)或者自定义的局部坐标系。我个人习惯用UTM,因为它能直接跟GPS数据对接,省去很多麻烦。

关键点:世界坐标系是静止的,所有动态物体(车、人、障碍物)的位置都相对于它来描述。

举个例子:

  • 车辆当前位置: (x=452301.2, y=4421897.3, z=0.0) 米
  • 前方障碍物: (x=452310.5, y=4421905.8, z=0.0) 米

你看,这些坐标都是相对于世界原点的绝对位置。

2.2 车辆坐标系:车自己的小世界

车辆坐标系是跟着车走的。一般定义:

  • X轴:车头方向
  • Y轴:车身左侧
  • Z轴:车顶方向(右手系)

为什么需要这个坐标系?因为传感器都装在车上,它们看到的数据天然就是相对于车的。比如激光雷达说“前方5米有障碍物”,这个“前方”就是车辆坐标系下的前方。

我的经验:车辆坐标系的原点通常选在后轴中心。为什么?因为车辆的动力学模型(自行车模型)就是基于后轴中心建立的。你选别的位置,后面做运动预测时还得再转一次,麻烦。

2.3 传感器坐标系:每个传感器都有自己的视角

车上装了那么多传感器——激光雷达、摄像头、毫米波雷达、IMU……每个都有自己的坐标系。

比如:

  • 激光雷达:原点在雷达中心,X轴指向雷达正前方
  • 摄像头:原点在光心,Z轴指向镜头前方(注意,不同厂家定义可能不同)
  • IMU:原点在IMU芯片中心,通常跟车辆坐标系对齐

我记得有一次,供应商给的激光雷达标定文件里,坐标系定义跟我们默认的差了180度。结果融合出来的点云全反了。从那以后,我拿到新传感器第一件事就是确认坐标系定义。

避坑指南:不同传感器厂商的坐标系定义可能不一样。比如有的摄像头把Y轴朝下,有的朝上。拿到标定文件后,一定要先画个图验证一下,别直接往代码里塞。

2.4 坐标变换矩阵:怎么从一个坐标系跳到另一个?

坐标变换,说白了就是“旋转+平移”。

数学上,我们用4x4的齐次变换矩阵来表示:

| R11 R12 R13 tx |
| R21 R22 R23 ty |
| R31 R32 R33 tz |
| 0   0   0   1  |

其中:

  • R:3x3旋转矩阵
  • t:3x1平移向量

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

// 伪代码
Point3D point_in_vehicle = T_lidar_to_vehicle * point_in_lidar;

这里T_lidar_to_vehicle就是激光雷达到车辆坐标系的变换矩阵。

重要:变换矩阵是可逆的。从车辆到激光雷达,就是求逆矩阵。但注意,旋转矩阵的逆等于它的转置(因为旋转矩阵是正交矩阵),平移部分要重新计算。

2.5 四元数与欧拉角:旋转的两种表达方式

说到旋转,就绕不开欧拉角和四元数。

欧拉角:用三个角度(roll, pitch, yaw)表示旋转。直观,但有个大问题——万向锁。

什么叫万向锁?就是当pitch达到±90度时,roll和yaw会变得无法区分。我在做坡道场景时遇到过这个问题,车辆俯仰角一大,欧拉角直接崩了。

四元数:用四个数(w, x, y, z)表示旋转。没有万向锁,插值平滑,计算效率高。

我个人强烈建议:内部计算全部用四元数,只在可视化或人机交互时转成欧拉角

四元数转欧拉角的公式(以ZYX顺序为例):

roll  = atan2(2*(w*x + y*z), 1 - 2*(x*x + y*y))
pitch = asin(2*(w*y - z*x))
yaw   = atan2(2*(w*z + x*y), 1 - 2*(y*y + z*z))

小技巧:用Eigen库或者ROS的tf库,这些转换都有现成函数。别自己手写,容易出bug。我见过有人手写四元数乘法写反了顺序,结果旋转方向全反了。

2.6 工程中的坐标系管理:别让坐标系成为你的噩梦

在实际工程中,坐标系管理是个大问题。车上有十几个传感器,每个都要标定到车辆坐标系。如果管理不好,数据流就乱了。

我推荐的做法:

  1. 统一命名规范:比如 lidar_front, camera_left, imu_center
  2. 用树状结构管理:所有坐标系都挂在车辆坐标系下,形成一棵树
  3. 缓存变换矩阵:标定完成后,把所有变换矩阵存成静态数据,运行时直接查表
  4. 加断言检查:每次变换后,检查结果是否在合理范围内(比如距离不能突变)

举个例子,坐标系树可能长这样:

world
└── vehicle
    ├── lidar_front
    ├── lidar_rear
    ├── camera_front
    ├── camera_left
    ├── camera_right
    ├── radar_front
    └── imu

每个节点都存着到父节点的变换矩阵。要转换数据时,沿着树往上走到公共祖先,再往下走到目标节点。

我曾经踩过的坑:有一次,两个传感器都标定到了车辆坐标系,但一个用的是后轴中心,另一个用的是车辆质心。结果差了0.5米,融合出来的障碍物位置全偏了。所以,标定前一定要确认基准点一致。

2.7 实战:一个完整的坐标变换流程

假设我们要把激光雷达检测到的障碍物位置,转换到世界坐标系下:

  1. 激光雷达检测到障碍物:p_lidar = (x, y, z)
  2. 用标定矩阵转到车辆坐标系:p_vehicle = T_lidar_to_vehicle * p_lidar
  3. 用车辆定位信息(GPS+IMU)得到车辆在世界坐标系下的位姿:T_world_to_vehicle
  4. 转到世界坐标系:p_world = T_world_to_vehicle * p_vehicle

代码实现(伪代码):

// 1. 激光雷达检测
Point3D p_lidar = lidar.detect();

// 2. 转到车辆坐标系
Point3D p_vehicle = T_lidar_to_vehicle * p_lidar;

// 3. 获取车辆位姿(来自定位模块)
Pose vehicle_pose = localization.getPose();

// 4. 转到世界坐标系
Matrix4 T_world_to_vehicle = vehicle_pose.toMatrix();
Point3D p_world = T_world_to_vehicle * p_vehicle;

// 5. 输出结果
std::cout << "障碍物在世界坐标系下的位置: " << p_world << std::endl;

你看,整个过程就是一连串的矩阵乘法。只要标定数据准确,代码逻辑清晰,就不会出错。

总结一下:

  • 世界坐标系是绝对参考
  • 车辆坐标系是动态参考
  • 传感器坐标系是原始数据来源
  • 变换矩阵是连接它们的桥梁
  • 四元数比欧拉角更可靠
  • 工程管理比数学本身更重要

嗯,坐标系这块就讲这么多。下一章我们聊聊路径规划的核心算法,到时候你会看到,坐标系搞不清楚,后面的规划算法再牛也白搭。