3. 数据回放验证概述

做自动驾驶回放闭环,第一关就是跟数据格式打交道。你想想看,车上那么多传感器——激光雷达、摄像头、毫米波雷达、IMU……每毫秒都在产生数据。怎么存、怎么读、怎么对齐时间戳,直接决定了你后面回放能不能「复现」当时的场景。

我个人习惯把数据格式分成两类:通用交换格式(比如 Rosbag、NuScenes、KITTI)和内部定制格式。今天咱们就把主流格式掰开揉碎聊一聊,再聊聊我自己设计自定义格式时踩过的坑。

数据存储与格式 · 核心知识结构 数据存储与格式 主流格式 Rosbag (ROS1/2) NuScenes (nuscenes) KITTI (原始格式) 自定义格式设计 列式存储 (Parquet) 时间片 + 索引 序列化 (Protobuf) 选择格式 = 平衡 兼容性 · 读写速度 · 存储效率

3.1 Rosbag —— 最熟悉的“老朋友”

Rosbag 是 ROS 生态下的标准容器。说白了,它就是按时间顺序把 topic 消息一股脑儿塞进一个文件。我在项目中用过几百个 bag,最深的体会是:rosbag 的索引机制很轻量,但如果你录了 200GB 的 bag,想随机跳到第 37 分 12 秒?嗯,那加载速度会让你想砸键盘。

💡 我的习惯: 录制时按场景切分 bag,每个 bag 不超过 30 分钟。配合 rosbag index 重建索引,回放时用 --clock 保持时间同步。
# 常用 rosbag 命令 (ROS1)
rosbag record -O my_scene.bag /camera/image_raw /lidar/points /imu/data
rosbag info my_scene.bag
rosbag play my_scene.bag --clock --rate 1.0

ROS2 的 bag 底层用了 SQLite3,元数据更丰富。我个人觉得 ROS2 bag 的随机访问比 ROS1 好不少,但跨版本兼容偶尔会出幺蛾子——我曾经因为 ROS2 的序列化 ID 不匹配,折腾了一下午才找到原因。

3.2 NuScenes 格式 —— 学术界的“标准答案”

NuScenes 数据集用的是 JSON + 二进制文件 的组合。它的核心是一张巨大的关系表:场景、样本、传感器、标注……全部用 token 关联。你想想看,这种设计对做算法训练非常友好,但直接拿来做回放?有点重。

我在做闭环验证时,曾经尝试把 NuScenes 格式直接喂给回放引擎。结果发现每次加载都要解析 JSON 并建立内存索引,启动时间比实际回放还长。后来我学乖了:只把 NuScenes 作为离线标注的中间格式,回放前转成更紧凑的二进制格式。

关键点: NuScenes 的坐标系定义(ego 坐标系、全局坐标系、传感器坐标系)非常严谨。如果你要自定义格式,建议参考它的坐标变换树设计。

3.3 KITTI 格式 —— 简单粗暴,但够用

KITTI 的原始数据就是一堆 png + txt + calib 文件。每个文件夹代表一个“drive”,里面按时间戳命名。说实话,这种格式在 2012 年很前卫,但现在看,没有内聚的时间同步机制,全靠文件名里的毫秒数对齐。

我记得有一次用 KITTI 格式做回放,发现某个摄像头图像和激光雷达点云的时间戳差了 40 毫秒。查了半天,原来是文件系统拷贝时时间戳精度丢了。所以后来我只要用 KITTI 风格,一定会额外生成一个 timestamps.txt 并校验完整性。

3.4 自定义数据格式 —— 什么时候该自己造轮子?

你可能会问:有 Rosbag 和 NuScenes 为什么还要自己设计格式?我遇到过三种典型场景:

  • 回放性能瓶颈: Rosbag 的序列化/反序列化开销大,高频传感器(如 128 线激光雷达 + 8 个摄像头)容易掉帧。
  • 需要列式查询: 比如我只想回放所有“行人横穿马路”的场景,通用格式没法直接按语义过滤。
  • 硬件在环(HIL)约束: 某些实时系统要求数据按微秒级时间片排列,且内存映射(mmap)友好。
⚠️ 避坑: 我曾经设计过一个“极致紧凑”的二进制格式,把所有传感器数据打包成一个 blob。结果后来想加一个 GPS 状态字段,不得不重写整个解析器。所以自定义格式一定要预留扩展头(version + schema hash)。

下面是我目前在项目中使用的自定义格式骨架(简化版):

// 自定义回放格式 v2 (protobuf 风格描述)
message Frame {
    uint64 timestamp_us          = 1;   // 全局时间戳
    uint32 sensor_count          = 2;
    repeated SensorData sensors  = 3;
    optional bytes extra_meta    = 4;   // JSON 元数据
}
message SensorData {
    uint32 sensor_id             = 1;
    bytes  raw_data              = 2;   // 按 sensor 类型编码
    uint32 data_len              = 3;
    // 可扩展字段
}

实际落地时,我会用 FlatBuffers 或 Cap'n Proto 做序列化,零拷贝反序列化对回放性能太重要了。你想想看,每毫秒要反序列化几十个传感器消息,如果每个都拷贝一次内存,CPU 很快就扛不住了。

3.5 格式选型对比

特性Rosbag (ROS1)NuScenesKITTI自定义 (Protobuf/FlatBuffers)
随机访问慢 (需扫描)中等 (JSON 索引)差 (依赖文件名)快 (时间戳索引)
序列化开销中等高 (JSON 解析)低 (原始文件)极低 (零拷贝)
跨平台依赖 ROS好好好
扩展性一般好 (schema 灵活)差优秀 (预留字段)
工具链丰富中等少需自研

我个人建议:原型阶段用 Rosbag 或 NuScenes,快速验证。进入量产回放 pipeline 后,逐步迁移到自定义格式。但不管选哪种,时间同步和坐标系一致性永远是第一位的。

📌 小经验: 设计自定义格式时,一定要把“数据版本”和“传感器标定参数”内嵌到文件头里。我曾经因为标定参数外挂,导致回放时用了错误的相机内参,花了三天才定位到 bug。