4. 核心数据结构对比

做自动驾驶系统集成,最头疼的是什么?我个人觉得,不是算法调参,也不是传感器选型,而是——数据对不上。

Autoware 和 Apollo,两个系统各自有一套数据定义。Autoware 用 ROS2 的消息类型,Apollo 用 Protobuf。你想想看,如果两边的数据格式不统一,那协同开发就是一句空话。今天我们就来掰扯清楚,这两套数据结构到底差在哪,以及怎么把它们桥接起来。

核心观点:数据结构是系统的“语言”。语言不通,协同开发寸步难行。理解两者的差异,是打通 Autoware 和 Apollo 的第一步。

4.1 Autoware 的 ROS2 消息类型

Autoware 从 ROS1 迁移到 ROS2 之后,消息定义也做了大改。ROS2 的消息文件是 .msg 格式,说白了就是一个结构体定义。每个字段有类型和名字,简单直接。

举个例子,Autoware 里最常用的感知消息——PredictedObjects.msg,它长这样:

# PredictedObjects.msg
std_msgs/Header header
PredictedObject[] objects

而 PredictedObject 本身又包含:

# PredictedObject.msg
unique_identifier_msgs/UUID uuid
ObjectClassification classification
geometry_msgs/PoseWithCovariance kinematics
PredictedPath[] predicted_paths
Shape shape

我刚开始接触 Autoware 时,最不习惯的就是它的嵌套结构。一个消息套一个消息,层级很深。但好处是——可读性强。你打开一个 .msg 文件,基本能猜到它存了什么数据。

Autoware 的 ROS2 消息有几个特点:

  • 强类型:每个字段类型明确,编译时就能检查错误
  • 支持嵌套:消息可以包含消息,适合表达复杂结构
  • 自带时间戳:Header 里包含 stamp 和 frame_id,天然支持时间同步
  • 序列化效率一般:ROS2 的序列化基于 CDR,性能不如 Protobuf

小技巧:如果你在 Autoware 里自定义消息,记得遵循 package_name/msg/FileName.msg 的命名规范。我曾经因为文件名大小写问题,编译报错找了半天。

4.2 Apollo 的 Protobuf 定义

Apollo 走的是另一条路——Protobuf。Google 出品,性能怪兽。Apollo 里几乎所有数据都用 .proto 文件定义。

拿 Apollo 的感知结果来说,PerceptionObstacle 的定义是这样的:

// PerceptionObstacle.proto
message PerceptionObstacle {
  optional int32 id = 1;
  optional apollo.common.Point3D position = 2;
  optional double theta = 3;
  optional apollo.common.Point3D velocity = 4;
  optional double length = 5;
  optional double width = 6;
  optional double height = 7;
  optional Type type = 8;
  optional double timestamp = 9;
  repeated apollo.common.Point3D polygon_point = 10;
  // ... 还有更多字段
}

你看,Protobuf 的写法跟 ROS2 消息不太一样。每个字段都有编号(比如 = 1、= 2),这是为了二进制编码时节省空间。字段前面还有 optional、repeated 这样的修饰符。

Apollo 用 Protobuf 的原因很简单——性能。自动驾驶系统里,数据量巨大,尤其是激光雷达点云和图像数据。Protobuf 的序列化速度比 ROS2 的 CDR 快不少,而且生成的二进制数据更小。

但 Protobuf 也有缺点:

  • 可读性差:二进制数据,肉眼看不出来是什么
  • 调试麻烦:得用 protoc 工具转成 JSON 才能看
  • 版本管理要小心:字段编号一旦用了就不能改,否则前后不兼容

注意:Apollo 的 Protobuf 定义里,时间戳用的是 double 类型,单位是秒。而 Autoware 的 ROS2 消息用的是 builtin_interfaces/Time,包含秒和纳秒两部分。桥接时一定要做单位转换,否则时间对不上。

4.3 核心差异对比

我把两者的核心差异整理成了一张表,方便你对照:

对比维度 Autoware (ROS2) Apollo (Protobuf)
定义文件 .msg .proto
序列化方式 CDR (Common Data Representation) Protobuf 自研编码
性能 中等 高(编码紧凑、速度快)
可读性 高(文本可读) 低(二进制不可读)
时间戳格式 builtin_interfaces/Time(秒+纳秒) double(秒,浮点数)
坐标表示 geometry_msgs/Pose(位置+四元数) Point3D + Quaternion(分开定义)
路径/轨迹 PredictedPath(带概率) Trajectory(带时间戳)
扩展性 需重新编译 支持向前/向后兼容

为什么会这样?说白了,两个系统的设计哲学不同。Autoware 更注重开发体验和调试便利性,所以选了可读性强的 ROS2 消息。Apollo 更注重量产性能和稳定性,所以选了 Protobuf。

4.4 数据桥接方案

好,问题来了——两个系统要协同工作,数据怎么打通?

我做过几个项目,总结下来有三种桥接方案:

方案一:直接转换(点对点)

写一个转换节点,订阅 Autoware 的 ROS2 消息,转成 Apollo 的 Protobuf 格式,再通过共享内存或 Socket 发给 Apollo。反过来也一样。

这种方案最直接,但工作量不小。每个消息类型都要写一个转换函数。我记得有一次,光 PredictedObjects 到 PerceptionObstacles 的转换,就写了 200 多行代码。

// 伪代码示例:ROS2 转 Protobuf
PredictedObjects ros_msg = ...;
apollo::perception::PerceptionObstacles pb_msg;

for (auto &obj : ros_msg.objects) {
  auto *pb_obj = pb_msg.add_perception_obstacle();
  pb_obj->set_id(uuid_to_int(obj.uuid));
  pb_obj->set_theta(obj.kinematics.pose.orientation.z);
  // ... 继续转换其他字段
}

方案二:中间层抽象(推荐)

定义一个“中间数据格式”,两边都转成这个格式。这样,每增加一种消息类型,只需要写两个转换函数(ROS2→中间、中间→Protobuf),而不是 N 个。

我个人比较推荐这种方案。它虽然多了一层抽象,但维护成本低很多。你想想看,如果以后要对接第三个系统,只需要再写两个转换函数就行。

推荐做法:用 JSON 或 FlatBuffers 作为中间格式。JSON 调试方便,FlatBuffers 性能更好。我一般先用 JSON 做原型验证,稳定后再切到 FlatBuffers。

方案三:共享内存 + 协议适配

Autoware 和 Apollo 跑在不同的进程里,甚至不同的机器上。通过共享内存(比如 ROS2 的 shared_memory 或 Apollo 的 Cyber RT)交换数据,然后在各自进程内做协议适配。

这种方案性能最好,但复杂度也最高。你需要处理内存同步、锁竞争、数据生命周期等问题。嗯,这里要注意——千万别在共享内存里传指针,否则你会 debug 到怀疑人生。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 时间戳单位不一致:Autoware 用秒+纳秒,Apollo 用 double 秒。桥接时记得做 sec + nsec / 1e9 的转换。
  • 坐标系方向不同:Autoware 的坐标系是 REP 103(前-左-上),Apollo 是 ENU(东-北-上)。桥接时要做旋转矩阵变换。
  • ID 类型不同:Autoware 用 UUID(128位),Apollo 用 int32。我建议用哈希映射,把 UUID 映射到 int32,但要保证唯一性。
  • 枚举值不一致:比如障碍物类型,Autoware 的 CAR 可能是 1,Apollo 的 VEHICLE 可能是 3。桥接时一定要做枚举映射表。

我的经验:在项目初期,先花一天时间把两边的数据结构映射表画出来。别急着写代码。映射表画清楚了,后面写转换代码就是体力活。

4.6 知识体系总览

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

核心数据结构对比与桥接方案 Autoware (ROS2) • .msg 文件定义 • CDR 序列化 • 可读性强,调试方便 • 时间戳: builtin_interfaces/Time • 坐标: geometry_msgs/Pose Apollo (Protobuf) • .proto 文件定义 • Protobuf 自研编码 • 性能高,编码紧凑 • 时间戳: double 秒 • 坐标: Point3D + Quaternion 数据桥接方案 方案一:直接转换(点对点) 方案二:中间层抽象(推荐) 方案三:共享内存 + 协议适配 数据差异 核心思路:理解差异 → 选择桥接方案 → 实现数据互通

这张图把本章的核心逻辑串起来了。左边是 Autoware 的 ROS2 消息,右边是 Apollo 的 Protobuf 定义,中间是三种桥接方案。你只要把这张图记在脑子里,遇到具体问题时就知道该往哪个方向去查。

好了,数据结构这块就聊到这。记住一句话:数据不通,寸步难行;数据通了,万事皆顺。