第二章:坐标系与坐标变换

做自动驾驶规划控制,第一件事是什么?

不是调参数,不是写算法。而是搞清楚——车在哪,要去哪,周围的东西又在哪。

这三个问题,全得靠坐标系来回答。我刚开始入行时,觉得坐标系不就是个数学概念嘛,有啥好学的?结果第一次实车调试,就因为坐标系搞混,车直接往墙上怼……嗯,从那以后我再也不敢小看它了。

2.1 三种坐标系,你得门儿清

自动驾驶里,我们主要跟三种坐标系打交道。说白了,就是三种不同的“看世界”的角度。

2.1.1 全局坐标系

全局坐标系,也叫世界坐标系。它固定在地球上,不随车动。通常用经纬度(WGS84)或者UTM坐标来表示。

我个人习惯用UTM,因为它单位是米,算距离方便。高精地图里的车道线、红绿灯位置,存的都是全局坐标。

小提示: 全局坐标系的原点一般选在某个固定点,比如地图的西南角。千万别选在移动的物体上,否则你算出来的位置全是错的。

2.1.2 车辆坐标系

车辆坐标系是固定在车上的。原点一般在后轴中心,或者车辆质心。

标准定义是这样的:

  • x轴:车头方向为正
  • y轴:车身左侧为正(注意,有些标准用右侧为正,容易搞混)
  • z轴:车顶方向为正

我在项目中遇到过,不同传感器厂商对y轴正方向定义不一样。有的用左正,有的用右正。如果你不做统一,融合出来的数据就是乱的。

2.1.3 局部坐标系

局部坐标系是个“临时工”。它跟着车走,但原点不一定在车上。

比如,你规划一条路径,路径上的每个点,都是相对于路径起点来描述的。这个起点就是局部坐标系的原点。

为什么要用局部坐标系?因为全局坐标数值太大,动不动就几十万米,计算起来浮点误差大。局部坐标数值小,精度高。

2.2 坐标变换:平移和旋转

有了坐标系,接下来就是怎么把一个点从一个坐标系变到另一个坐标系。这就是坐标变换。

坐标变换就两件事:平移和旋转。

2.2.1 平移

平移最简单。就是两个坐标系原点之间的偏移。

比如,车辆坐标系原点相对于全局坐标系原点的偏移是 (dx, dy, dz)。那么车上的一个点 (x车, y车, z车) 在全局坐标系下的坐标就是:

// 平移变换
x全 = x车 + dx
y全 = y车 + dy
z全 = z车 + dz

就这么简单。但要注意,平移的前提是两个坐标系的朝向完全一致。如果朝向不同,就得先旋转。

2.2.2 旋转

旋转就稍微复杂一点。它描述的是两个坐标系之间的朝向差异。

旋转可以用旋转矩阵来表示。绕x轴、y轴、z轴旋转,各有各的矩阵。我直接给你代码:

// 绕z轴旋转角度θ
Rz = | cosθ  -sinθ  0 |
     | sinθ   cosθ  0 |
     | 0      0     1 |

// 绕x轴旋转角度φ
Rx = | 1   0      0    |
     | 0   cosφ  -sinφ |
     | 0   sinφ   cosφ |

// 绕y轴旋转角度ψ
Ry = |  cosψ  0   sinψ |
     |  0     1   0    |
     | -sinψ  0   cosψ |

实际应用中,我们通常先旋转,再平移。顺序不能反,反了结果就错了。我曾经因为搞反顺序,让车在停车场里画了半小时的圈……

2.3 欧拉角:直观但有个坑

欧拉角是描述旋转最直观的方式。它用三个角度来表示:横滚角(Roll)、俯仰角(Pitch)、偏航角(Yaw)。

  • Yaw(偏航):绕z轴转,就是车头左右摆
  • Pitch(俯仰):绕y轴转,就是车头上下翘
  • Roll(横滚):绕x轴转,就是车身左右侧倾

你想想看,这三个角度是不是很符合直觉?开车时你主要关心Yaw,上坡下坡看Pitch,过弯侧倾看Roll。

避坑指南: 欧拉角有个致命问题——万向锁。当Pitch接近±90度时,Roll和Yaw会变得无法区分。我曾经在模拟陡坡起步时,就因为万向锁,导致车辆姿态解算直接炸了。所以,如果你要做全姿态的规划控制,别只用欧拉角。

2.4 四元数:工程师的救星

为了解决万向锁,我们引入了四元数。

四元数听起来玄乎,其实就是一个四维向量 (w, x, y, z)。它表示绕某个轴旋转某个角度。没有奇点,插值平滑,计算效率高。

我个人建议:内部计算全用四元数,只在人机交互时转成欧拉角。

四元数和欧拉角的转换代码,我直接给你:

// 欧拉角转四元数 (ZYX顺序)
double cy = cos(yaw * 0.5);
double sy = sin(yaw * 0.5);
double cp = cos(pitch * 0.5);
double sp = sin(pitch * 0.5);
double cr = cos(roll * 0.5);
double sr = sin(roll * 0.5);

w = cy * cp * cr + sy * sp * sr;
x = cy * cp * sr - sy * sp * cr;
y = sy * cp * sr + cy * sp * cr;
z = sy * cp * cr - cy * sp * sr;
// 四元数转欧拉角
double sinr_cosp = 2 * (w * x + y * z);
double cosr_cosp = 1 - 2 * (x * x + y * y);
roll = atan2(sinr_cosp, cosr_cosp);

double sinp = 2 * (w * y - z * x);
if (abs(sinp) >= 1)
    pitch = copysign(M_PI / 2, sinp);
else
    pitch = asin(sinp);

double siny_cosp = 2 * (w * z + x * y);
double cosy_cosp = 1 - 2 * (y * y + z * z);
yaw = atan2(siny_cosp, cosy_cosp);

注意,上面这个转换假设旋转顺序是ZYX。不同顺序结果不一样,一定要跟你的传感器对齐。

2.5 知识体系总览

下面这张图,是我自己整理的坐标系与坐标变换知识结构。你照着这个学,不会乱。

坐标系与坐标变换知识体系 全局坐标系 车辆坐标系 局部坐标系 坐标变换 平移变换 旋转变换 欧拉角 四元数 ⚠ 万向锁风险 ✓ 无奇点 内部计算推荐使用四元数,人机交互时转欧拉角

2.6 实战中的坐标系管理

说了这么多理论,来点实际的。在代码里,你怎么管理这些坐标系?

我推荐用TF树(坐标变换树)。每个坐标系都是一个节点,节点之间的变换关系就是边。你只需要维护好这棵树,任何两个坐标系之间的变换都能自动算出来。

ROS里的TF库就是干这个的。我自己写代码时,也会模仿TF的思路,建一个轻量级的变换管理器。

核心原则:
  1. 每个传感器、每个模块,都要明确它工作在哪个坐标系下
  2. 所有数据在进入算法模块前,统一变换到同一个坐标系
  3. 变换参数(平移量、旋转量)要实时更新,尤其是车辆运动时

嗯,坐标系这块就讲这么多。内容不多,但都是基础中的基础。你把这些搞懂了,后面学路径规划、控制算法,才能站得稳。

我的习惯: 每次写代码前,先在纸上画出坐标系关系图。标清楚原点在哪、轴朝哪、谁相对于谁。画清楚了再动键盘,能省一半的调试时间。

可可精品资料库