核心数据结构对比:规划模块的轨迹、路径点与速度曲线

做自动驾驶系统集成,说白了就是在跟数据打交道。我这些年折腾Apollo和Autoware,最深的体会就是——数据结构不一致,才是集成路上最大的坑。你想想看,两个系统各自定义了一套“语言”,你要做的就是把它们翻译成彼此能懂的信号。

今天咱们就聚焦规划模块,看看轨迹、路径点、速度曲线这三个核心数据结构,在Apollo和Autoware里到底差在哪。

1. 轨迹(Trajectory)的整体设计差异

先看宏观层面。轨迹是什么?就是车辆未来要走的“时空路径”。

Apollo这边,轨迹定义在planning.proto里,核心是ADCTrajectory。它包含一个trajectory_point的列表,每个点都绑定了时间戳、位置、速度、加速度,甚至还有方向盘转角。说白了,Apollo把轨迹当成一个完整的时空序列,每个点都自带“时间轴”。

Autoware呢?它的轨迹定义在Trajectory.msg里,核心是Trajectory消息。它也有一个点列表,但每个点叫TrajectoryPoint。这里有个关键区别——Autoware的轨迹点不强制带时间。它更关注“路径”本身,速度信息是单独通过twist字段表达的。

我个人的习惯是:做集成时,先把Apollo的轨迹转成Autoware的格式,重点处理时间戳的映射。如果Autoware那边不需要时间,我就把Apollo的时间戳存到header.stamp里,至少保留原始信息。

来看个代码对比,更直观:

// Apollo的轨迹点结构(简化版)
message TrajectoryPoint {
  double timestamp_sec;      // 绝对时间戳
  PathPoint path_point;      // 路径点(x, y, z, theta, kappa)
  double v;                  // 纵向速度 m/s
  double a;                  // 纵向加速度 m/s^2
  double steer;              // 方向盘转角 rad
}

// Autoware的轨迹点结构(简化版)
TrajectoryPoint.msg:
  geometry_msgs/Pose pose        // 位置+朝向
  geometry_msgs/Twist twist      // 线速度+角速度
  float32 acceleration           // 加速度(可选)
  float32 curvature              // 曲率(可选)

看到了吗?Apollo把时间戳放在每个点里,Autoware则依赖消息头的stamp。这个差异,我在第一次做接口适配时就踩过坑。

2. 路径点(PathPoint)的细节差异

路径点是轨迹的基本单元。两个系统都定义了路径点,但字段含义有微妙差别。

Apollo的PathPoint包含:

  • x, y, z:位置坐标
  • theta:朝向角(弧度)
  • kappa:曲率(1/m)
  • s:沿路径的累积距离(m)
  • dkappa:曲率变化率
  • ddkappa:曲率二阶变化率

Autoware的TrajectoryPoint里,路径信息藏在pose里:

  • pose.position:位置
  • pose.orientation:四元数表示的朝向
  • twist.linear.x:纵向速度
  • twist.angular.z:横摆角速度

我曾经在这里栽过跟头:Apollo用theta(欧拉角)表示朝向,Autoware用四元数。直接赋值会导致车辆方向错乱。正确的做法是:用tf::createQuaternionFromYaw(theta)做转换。别问我怎么知道的,问就是实车测试时车在原地转圈...

还有一个细节:Apollo的s字段(累积距离)在Autoware里没有直接对应。如果你需要这个信息,得自己算——遍历路径点,累加相邻点的欧氏距离。我一般会在适配层加一个computeCumulativeDistance()函数。

3. 速度曲线(Speed Profile)的表达方式

速度曲线,说白了就是告诉车“什么时候该跑多快”。

Apollo的做法很直接:每个轨迹点都带v(速度)和a(加速度)。你拿到轨迹点列表,按时间戳排序,就得到了完整的速度曲线。Apollo还会在ADCTrajectory里额外提供一个speed_profile字段,是一个SpeedPoint列表,每个点包含t(时间)和s(距离)。

Autoware呢?它的速度信息分散在twist.linear.x里。没有独立的速度曲线结构。如果你想获取“时间-速度”关系,得自己从轨迹点的时间戳和速度值里提取。

特性 Apollo Autoware
速度存储位置 每个轨迹点的 v 字段 每个点的 twist.linear.x
加速度存储 每个轨迹点的 a 字段 acceleration 字段(可选)
独立速度曲线 有 SpeedProfile 结构 无,需自行提取
时间基准 每个点有绝对时间戳 消息头时间戳 + 点索引

我建议:做接口适配时,统一用“时间戳 + 速度值”的键值对来传递速度曲线。这样不管对接哪个系统,都能保证信息不丢失。我在项目中写过一个SpeedProfileAdapter类,专门处理这种转换。

4. 核心差异总结与适配策略

啰嗦了这么多,咱们总结一下。两个系统在规划模块的数据结构上,主要有三个差异:

  1. 时间戳粒度:Apollo每个点都带时间,Autoware依赖消息头
  2. 朝向表示:Apollo用欧拉角,Autoware用四元数
  3. 速度曲线独立性:Apollo有独立的速度曲线结构,Autoware没有

适配策略其实不复杂:

  • 写一个转换层,专门处理字段映射
  • 对缺失的字段(如s),用计算补齐
  • 对格式不同的字段(如朝向),做显式转换
  • 保留原始数据,别丢信息

下面这张图,是我做适配时画的整体思路,你看一眼就明白了:

Apollo 规划模块 ADCTrajectory 每个点: timestamp + v + a 朝向: theta (欧拉角) Autoware 规划模块 Trajectory.msg 每个点: twist.linear.x 朝向: quaternion (四元数) 接口适配层 字段映射 | 格式转换 | 数据补齐 核心转换逻辑 1. 时间戳: Apollo点时间 → header.stamp + 点索引 2. 朝向: theta → tf::createQuaternionFromYaw(theta) 3. 速度: v → twist.linear.x (保留加速度到acceleration字段)

嗯,这张图基本概括了适配层的核心工作。你实际做的时候,可能还会遇到一些边界情况,比如轨迹点数量不一致、时间戳精度不同等等。但掌握了这三个核心差异,剩下的就是体力活了。

最后说一句:别想着一次性搞定所有适配。我通常的做法是,先跑通一个最简单的场景(比如直线行驶),验证数据转换正确,再逐步增加复杂度。这样出了问题,排查范围也小。

可可精品资料库