坐标系与坐标变换:无人车的“世界观”

做无人车算法这些年,我最大的感触就是——坐标系这东西,看着简单,但坑是真多。你想想看,一辆车在路上跑,它怎么知道自己在哪?怎么知道障碍物在哪?怎么决定往哪拐?

说白了,这一切都离不开坐标系。今天我们就来聊聊无人车的“世界观”,也就是全局坐标系、局部坐标系、车辆坐标系,以及它们之间怎么互相转换。

1. 三种坐标系,三种视角

我刚开始做路径跟踪时,总搞混坐标系。后来我总结了一句话:全局坐标系是地图,局部坐标系是眼睛,车辆坐标系是身体

1.1 全局坐标系(World Coordinate System)

全局坐标系是固定不变的。它通常以地球上的某个点为原点,比如经纬度原点,或者高精地图的某个参考点。X轴指向东,Y轴指向北,Z轴指向天。

这个坐标系用来描述:

  • 道路的绝对位置
  • 障碍物的绝对坐标
  • 车辆的绝对位姿

我在项目中遇到过一个问题:GPS信号飘移导致全局坐标不准,结果车辆以为自己已经在车道线上了,其实还差半米。嗯,这就是全局坐标系的局限性——它依赖外部传感器。

1.2 车辆坐标系(Vehicle Coordinate System)

车辆坐标系是跟着车走的。原点通常在车辆后轴中心,或者质心。X轴指向车头前方,Y轴指向左侧,Z轴指向上方。

这个坐标系用来描述:

  • 车辆自身的速度、加速度
  • 车辆自身的横摆角速度
  • 车辆自身的姿态(俯仰、侧倾)

我建议在做车辆动力学建模时,一定要用车辆坐标系。因为牛顿第二定律在车辆坐标系下写起来最顺手。

1.3 局部坐标系(Local Coordinate System)

局部坐标系是个“中间人”。它通常以车辆当前位置为原点,但坐标轴方向与全局坐标系平行。说白了,就是把全局坐标系平移到了车的位置。

这个坐标系用来描述:

  • 传感器检测到的障碍物相对位置
  • 路径跟踪时的横向误差、纵向误差
  • 局部路径规划

你想想看,如果直接用全局坐标做路径跟踪,计算量会很大。但用局部坐标,误差一目了然。

2. 坐标变换矩阵:数学的魔法

三种坐标系之间怎么转换?靠的就是坐标变换矩阵。我记得第一次手推这个矩阵时,推了整整一页纸,结果发现有个符号写反了……

2.1 二维旋转矩阵

假设车辆在全局坐标系中有一个航向角 θ(绕Z轴旋转),那么从车辆坐标系到全局坐标系的旋转矩阵是:

R = | cosθ  -sinθ |
    | sinθ   cosθ |

反过来,从全局坐标系到车辆坐标系,就是转置一下:

R_inv = | cosθ  sinθ |
        | -sinθ cosθ |

这里有个小技巧:旋转矩阵的逆等于它的转置。这个性质在代码里非常有用,不用真的去求逆矩阵。

2.2 平移变换

光旋转还不够,还得平移。假设车辆在全局坐标系中的位置是 (x_v, y_v),那么从车辆坐标系到全局坐标系的完整变换是:

X_global = R * X_vehicle + T

其中 T = [x_v, y_v]^T。

我的经验:在实际代码中,我习惯把旋转和平移分开写。这样调试时,可以单独验证旋转是否正确,再验证平移是否正确。一步到位反而容易出bug。

3. 齐次坐标:让变换更优雅

你发现没有?上面的变换里,旋转是矩阵乘法,平移是向量加法。两个操作混在一起,代码写起来很别扭。

齐次坐标就是来解决这个问题的。它给二维坐标加一个维度,变成 (x, y, 1)。这样,旋转和平移就可以合并成一个 3x3 的矩阵:

T = | cosθ  -sinθ  x_v |
    | sinθ   cosθ  y_v |
    | 0      0      1   |

那么从车辆坐标系到全局坐标系,就变成了一个矩阵乘法:

X_global_homo = T * X_vehicle_homo

是不是清爽多了?

核心要点:齐次坐标让坐标变换变得统一、简洁。在无人车算法中,几乎所有坐标变换都用齐次坐标实现。我建议你从一开始就养成这个习惯。

4. 实战:坐标系变换在路径跟踪中的应用

光说不练假把式。我们来看一个实际场景:路径跟踪。

假设我们有一条全局路径,由一系列全局坐标点组成。车辆当前在某个位置,我们要计算车辆到路径的横向误差。

步骤是这样的:

  1. 找到路径上离车辆最近的点(匹配点)
  2. 计算匹配点在车辆坐标系下的坐标
  3. 横向误差就是匹配点在车辆坐标系下的Y坐标

代码实现大概是这样的:

// 假设已知:
// vehicle_pose: 车辆在全局坐标系下的位姿 (x, y, theta)
// path_point: 路径上匹配点的全局坐标 (x_p, y_p)

// 1. 计算相对位置
double dx = path_point.x - vehicle_pose.x;
double dy = path_point.y - vehicle_pose.y;

// 2. 旋转到车辆坐标系
double local_x =  cos(theta) * dx + sin(theta) * dy;
double local_y = -sin(theta) * dx + cos(theta) * dy;

// 3. 横向误差就是 local_y
double lateral_error = local_y;

你看,代码就这么几行。但我在项目中踩过一个坑:航向角 theta 的定义。有的传感器输出的是弧度,有的是角度;有的定义是北偏东,有的是东偏北。一定要统一!

我曾经犯过的错:有一次,我把IMU输出的航向角直接拿来用,没注意它是以磁北为参考的。结果车辆在转弯时,横向误差符号反了,车辆直接往反方向打方向盘。幸好是在仿真环境里,不然就撞了。

5. 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

坐标系与坐标变换知识体系 全局坐标系 固定不变,描述绝对位置 原点:地球参考点 局部坐标系 随车平移,方向与全局一致 用于误差计算 车辆坐标系 随车旋转平移 用于动力学建模 坐标变换方法 旋转矩阵 + 平移向量 → 齐次坐标矩阵 典型应用 路径跟踪横向误差计算 · 障碍物相对位置估计 · 传感器数据融合

6. 避坑指南

最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:

  • 航向角定义要统一:我建议统一使用“从X轴正方向逆时针旋转”为正方向。所有传感器数据进来,先做一次转换。
  • 注意浮点精度:坐标变换涉及大量三角函数运算。我习惯用 double 而不是 float,尤其是在累积变换时。
  • 测试要覆盖边界:比如航向角为0度、90度、180度时,变换矩阵是否正常?我曾经在航向角为90度时,sin和cos写反了,结果车辆横着走。
  • 齐次坐标的最后一维:变换后记得归一化,把最后一维变回1。虽然大多数情况下它本来就是1,但养成习惯总没错。
一个小技巧:在调试坐标变换时,我习惯先写一个单元测试。输入一个已知点,手动计算期望输出,然后对比代码输出。这样能快速定位问题。

好了,坐标系与坐标变换就讲到这里。这些东西看似基础,但真的贯穿了整个无人车算法。路径跟踪、避障、传感器融合……哪哪都离不开它。希望你能把这些概念吃透,后面学起来会轻松很多。


公众号:蓝海资料掘金营,微信deep3321