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 消息,右边是 Apollo 的 Protobuf 定义,中间是三种桥接方案。你只要把这张图记在脑子里,遇到具体问题时就知道该往哪个方向去查。
好了,数据结构这块就聊到这。记住一句话:数据不通,寸步难行;数据通了,万事皆顺。