ADTF 这个名字,搞 ADAS 实车采集的工程师肯定不陌生,但做算法开发的同事听到它,大概率会皱眉头。前阵子我们团队就卡在这儿:路测车队用 ADTF 辛苦跑了一个月,攒了几百 GB 的高速、城区、隧道各种场景的传感器数据,结果算法组的同事拿到手一看,只能挠头——这些 DAT 文件我们 ROS 节点根本读不进去,手里的感知模型和融合算法想拿真实数据验证,还得先解决格式和工具链问题。于是就有了这次从数据采集到回放验证的完整打通实践,把 ADTF 和 ROS 之间的适配层整个捋了一遍。这篇文章就把我的做法、踩过的坑和最终落地的方案整理出来,给同样被"数据孤岛"卡住的团队一些参考。
整个链路说简单也简单,无非是采集、转换、回放、验证四步。但每一步都有不少隐藏的细节,尤其是 ADTF 的数据模型、时间戳处理、坐标系对齐这些地方,稍不留神就会让后续的回放验证前功尽弃。所以这篇不只是讲配置和命令,更多的是讲讲我踩过的坑,以及每一步背后的原理和取舍逻辑。
1. ADTF 记录的数据,为什么非搬到 ROS 里不可
先解释一下背景。ADTF(Automotive Data and Time-Triggered Framework)在汽车电子工具链里是个老牌角色,尤其在整车厂和 Tier1 的数据采集、离线分析、ECU 验证场景里用得非常多。它天生支持多传感器同步采集,对 CAN、摄像头、雷达、激光雷达这些信号源有很成熟的接入方案,而且底层是时间触发的调度机制,时间戳精度和同步性比普通 PC 录屏强得多。我们自己路测车的采集系统就是基于 ADTF 搭的,一套配置跑下来,图像、点云、CAN 信号整整齐齐落在同一个时间基准里,这个优势在实车数据采集这个场景里几乎是刚需。
但问题也恰恰出在生态上。ADTF 再好用,它的一套数据格式(默认是 .dat 文件,配合 stream 描述文件)在开源算法生态里基本是"孤立"的。我们算法团队做感知、融合、规划验证,日常使用的框架是 ROS,训练好的模型、写的后处理脚本、可视化工具全部围绕 ROS 展开。从网上下载的预训练权重也好、开源感知算法也好,输入输出接口几乎清一色是 ROS topic,用的是 sensor_msgs/Image、sensor_msgs/PointCloud2 这类标准消息。两边数据语言不通,直接导致一个荒唐的局面:越是高质量的真实道路数据,越难被算法团队用起来;算法团队为了做验证,要么去网上找别人录的 ROS bag,要么自己开着车架一套全 ROS 采集系统再跑一遍,白白浪费了 ADTF 车队采集的现成数据。
这不是个别团队的痛点。行业内只要涉及"实车采集 + 算法验证"两拨人协作的项目,几乎都会遇到这个数据格式断层。打通 ADTF 和 ROS 的适配层,核心价值就一句话:让真实采集的数据能无缝进入 ROS 生态,让算法团队能在统一工具链里做回放、做评测、做调试。
2. ADTF 采集侧那些"看着不起眼、丢了会死"的细节
在做转换之前,必须先把你手上的 ADTF 数据模型搞清楚。ADTF 里的核心概念是 "Stream"(数据流)和 "Sample"(数据样本),一个 Stream 对应一路传感器或一路总线信号,里面是一串带时间戳的 Sample。你可以理解成 ROS 里的一个 topic 加一系列带 header.stamp 的消息。每个 Stream 都有明确的类型定义,比如 video/AUWV、CAN 总线消息、GPS NMEA 句子、雷达 list 等等。ADTF 的 .dat 文件会把这些流和样本按时间顺序组织在一起,同时保留一个描述文件(DADF 文件)记录流的元信息。
如果你只是拿 .dat 文件做转换,没有把采集端的配置一并归档,后面基本会踩坑踩到怀疑人生。拿我们的经历来说,有几次拿到来源不明的数据后才发现:摄像头内参、外参标定文件不在数据包里;CAN 的 DBC 数据库文件也没有随附;甚至不同路测车的触发源都不一样,有的用 GPS PPS 做硬同步,有的直接依赖 ADTF 自身的逻辑时钟。这些信息缺失,会让转换出来的 ROS bag 变成一个"半残废"包——图像能看,但无法做任何基于坐标系的感知验证。
所以我强烈建议在采集端就建立一套完整的"数据包"规范,不要只录 .dat 文件。我们现在的规范是每个采集任务输出一个目录,里面必须包含以下几类内容:
- ADTF 原始数据:.dat + DADF 描述文件
- 传感器标定文件夹:每个摄像头的内参、畸变系数、到车体坐标系的位姿变换,激光雷达到车体的变换
- CAN 信号数据库:对应的 DBC 文件,确保总线信号能解析成物理量
- 采集日志:包括采集设备 ID、传感器版本、触发模式、时区配置、软件版本
这一步虽然简单,但直接决定了后续回放验证能不能闭环。很多团队在数据适配上栽跟头,不是转换代码多难写,而是源头数据缺信息,后面再怎么补都补不齐。
采集端的另外几个细节也要留个心眼。一是采样频率要固定,尤其是摄像头这种大流量数据,ADTF 配置里如果帧率设置得忽高忽低,转换出来的 bag 里消息时间戳出现明显抖动,后续做时间同步算法时误判率会上升。二是录制时的分片策略,我们建议每 10~20 分钟切一个 .dat 文件,方便管理和并行转换,切片太大会导致单文件损坏后整体报废。三是存储介质必须用高速 SSD,ADTF 在写入大位流数据时对磁盘带宽非常敏感,机械盘或者网络存储几乎必然丢帧,丢了帧这个数据包基本就废了。
2.1 DAT 文件和 ROS bag 的数据组织差异
还有一个让老 ROS 用户最容易产生误区的地方:ADTF 的 .dat 文件和 ROS bag 在数据组织逻辑上是有差别的。ROS bag 里每条消息都完整记录了 header 的时间戳和 frame_id,而 ADTF 的 Sample 也有时间戳,但它的时间戳风格是"全局共享时间轴"——所有流上的样本都基于同一个时间源头(通常来自采集系统的主时钟),这一点和 ROS 的 wall time 概念很像,但和 ROS 中常见的"各节点各自挂钟"模式不同。转换的时候,一定要保证把 ADTF 的时间戳正确映射到 ROS 的 ros::Time 上,不能自作聪明做本地时间补偿,否则回放出来的数据时间轴是乱的。
另外,ROS bag 本身并不强制要求话题时间戳排序,但 rosbag play 回放时默认按消息 header 时间顺序发送,如果转换完的 bag 时间戳乱序,回放时会出现消息乱跳、插件抽风的问题。我们转换完成后会做一个全包的单调性检查,一旦发现某个 topic 的时间戳回退,基本能断定是转换器有 bug 或者源数据本身有问题。
3. DAT 转 ROS Bag:三种可行路径与自研转换器的关键实现
从工具链角度说,把 ADTF 数据搬进 ROS,有几种可以做,各有利弊,我分别说一下,你们可以根据自己的实际场景选。
方案一:直接用 EB 的原生导出工具。ADTF 自带一些数据导出插件,比如可以导成 CSV、图像序列,但说实话这些工具离 ROS bag 的需求还差很远。它能导出的信息格式比较死板,摄像头流、CAN 流能导,但要导成 topic 层级、带标准 ROS message 结构的 bag,必须二次加工。官方支持的格式往往也不是你要的,比如导出图像是 JPEG 文件序列而 ROS 期望的是 sensor_msgs/CompressedImage 或者原始图像流。所以这个方案适合快速验证、人工检查数据完整性,不适合做系统化的数据流水线。
方案二:实时桥接(live bridging)。在 ADTF 运行时,通过插件把数据直接转发到 ROS 网络,不走 .dat 文件。这个方案的好处是延迟低、实时性好,适合做 HIL 台架、实时联合调试。但缺点也很明显:需要把 RT 系统和 Linux 上跑的 ROS 节点打通,网络协议、数据序列化方式都要自己做,复杂度高,而且一旦断流数据就丢了,不如先落盘再做离线转换来得可靠。
方案三(我主推):自研离线转换器。使用 ADTF 提供的 File Library API(C++ SDK)直接读取 .dat 文件,解析出每个 Stream 的 Sample,再按映射表写到 ROS bag 里。这个方法前期工作量大些,但后期收益非常明显:批处理灵活、可配置、可复用,能按需筛选 topic、压缩图像、抽取帧,做得好了完全可以沉淀成团队的基础工具链。我们最终选的就是这条路。
3.1 路径取舍:为什么我最终选了自研转换器
选方案三不是因为它看起来高大上,而是基于我们实际场景的三个硬需求。第一,我们手里的数据量特别大,一个月的路测数据经常是几个 TB 的规模,需要一种能批量、无人值守运行的转换程序,显然不可能是动鼠标点 GUI 的工具能扛住的。第二,算法团队对 bag 格式有明确预期——哪些 topic、什么消息类型、什么 frame_id、哪些数据要保留哪些可以丢弃,必须由转换配置来灵活控制,而不是导出工具写死。第三,也是最要命的,ADTF 的 DAT 文件在读取的时候很容易踩到"描述文件缺失导致流信息不完整"的坑,转换器必须有容错能力,能跳过坏流并记录日志继续跑,这点原生工具做不到。
所以这个自研转换器本质上的定位是:一部把 ADTF 世界和 ROS 世界做词汇翻译的"同声传译机"。它不改变数据内容,只改变数据的编排方式和封装格式。
3.2 转换器核心实现:从 ADTF 流到 ROS topic 的映射
核心代码逻辑其实不复杂,关键是把映射关系理清楚。第一步是读 DAT 文件——用 ADTF File Library 打开文件后,可以拿到所有 stream 的元信息列表。每个 stream 有一个类型标识,比如adtf::stream_type_video、adtf::stream_type_can,或者自定义的adtf::stream_type_custom。第二步是写一个映射表,把这些类型对应到 ROS 的 message 类型:
| ADTF Stream 类型 | ROS 消息类型 | 映射说明 |
|---|---|---|
| Video(YUV/Bayer/RGB) | sensor_msgs/Image | 需指定 encoding、step、宽高 |
| Compressed Video | sensor_msgs/CompressedImage | 保留原始压缩格式 |
| CAN Frame | can_msgs/CanFrame 或自定 | 需结合 DBC 解析 |
| GPS/GNSS NMEA | sensor_msgs/NavSatFix | 需处理经纬度坐标 |
| Radar Object List | custom_msgs/RadarObject | 参考原厂协议提取目标列表 |
| Point Cloud(LAS/自定义) | sensor_msgs/PointCloud2 | 需处理点字段定义 |
第三步是遍历每个 stream 里的 sample,把样本的 buffer 拷贝、按消息类型序列化,同时把 ADTF sample 的时间戳转换成 ROS time,写进std_msgs/Header。
伪代码大概是这样的:
// 伪代码示意,重点说明流程 adtf::IFile* pFile = adtf::File::Open(sPath, adtf::File::OpenMode::Read); for (auto& streamInfo : pFile->GetStreams()) { auto streamReader = pFile->ReadStream(streamInfo); while (streamReader->NextSample(sample)) { auto rosTime = ToRosTime(sample->timeStamp); auto rosMsg = ConvertToRosMsg(streamInfo, sample); bag.write(topicName, rosTime, rosMsg); } }这里最重要的就是ConvertToRosMsg这个函数里对图像数据的处理。ADTF 里摄像头流常见的编码是 YUV422 或 Bayer 格式,而 ROS 的sensor_msgs/Image要求相对标准的 encoding 表示,比如yuv422、bayer_bggr8等。如果编码值写错,图像在 RViz 里显示要么颜色对不上,要么直接显示不了。建议转换时先把 ADTF 的编码枚举值映射成 ROS 的标准编码字符串,宁可多花点时间也要把这一步做对。
3.3 时间戳与坐标系:两个最容易翻车的细节
时间戳处理是转换器里最容易出 bug 的地方。ADTF 的时间戳单位是微秒,ROS 的ros::Time是秒加纳秒的结构体。如果直接强转而不做单位换算,回放时所有消息的时间戳都会被解析成 1970 年附近的"远古时间",rosbag play 默认按/clock或 wall-time 回放,就会直接不输出任何数据。我们最初就踩了这个坑,转换完生成的 bag 在 RViz 里根本打不开,查了整整一个下午才发现是时间单位问题。
另一个大坑是坐标系的信息丢失。ADTF 数据流本身不强制带 frame_id,很多采集配置里并不会在数据流里嵌入传感器坐标系名称。转换到 ROS bag 的时候如果所有 topic 的 frame_id 都是空的,后面做感知融合时 tf 树根本建不起来。我们现在的做法是:转换配置里明确一个传感器到车体的静态变换表,转换时自动为每个 topic 打上对应的 frame_id,并额外生成一个 TF 静态广播配置,确保在 ROS 环境里能直接构建出完整的坐标变换关系。
<!-- 静态变换示例:后置摄像头到车体坐标系 --> <node pkg="tf2_ros" type="static_transform_publisher" name="cam_rear_to_base" args="0.0 1.5 1.1 0 0.05 3.14 base_link cam_rear_link" />4. 回放验证链路搭建:让 Bag 里的数据真正驱动 ADAS 算法
转换出 ROS bag 只是第一步,真正的目标是让这些数据在 ROS 环境里能被算法节点消费,完成感知、融合、决策等环节的验证。我在搭建回放验证链路时,按照"先可视化确认,再评测指标确认,最后闭环联调"的顺序走,每一步都有对应的工具和方法。
4.1 回放环境初始化:从启动参数到 TF 树
回放之前先做两件准备工作。第一是检查 bag 文件的 topic 信息和消息统计,最简单的做法是用rosbag info命令查看:
rosbag info dataset_highway.bag path: dataset_highway.bag version: 2.0 duration: 32.4s start: Aug 05 2024 15:30:11.28 end: Aug 05 2024 15:32:03.68 size: 6.3 GB messages: 1584304 compression: none topics: - /cam_front/image_raw main_image 1004 msgs : sensor_msgs/Image - /cam_front/camera_info main_info 112 msgs : sensor_msgs/CameraInfo - /lidar_front/points main_lidar 322 msgs : sensor_msgs/PointCloud2 - /can/chassis_speed main_can 6543 msgs : custom_msgs/CanFrame这一步能让你快速确认 bag 的基本信息是否符合预期——话题是否存在、消息类型对不对、频率是否符合源数据的采集配置。我一般会写一个自动检查脚本,把 bag 里所有 topic 的 message type、消息数、时间范围输出成表格,然后对比采集端的配置清单,凡是出现类型不匹配、帧率掉线、时间轴长度不对的都第一时间发现。
第二件准备工作是 TF 树。如果你的感知算法依赖坐标变换(比如把激光雷达点云投影到图像上),回放前必须保证 TF 树完整。我通常把静态变换直接写进一个tf_static.launch文件,回放时一起启动,这样 RViz 不管看那个传感器数据都能有正确的坐标系语境。
4.2 可视化确认与主观感知评估
回放最直观的验证手段就是可视化。我会同时打开 RViz 和 rqt_bag 两个窗口。RViz 负责看图像、点云、检测框,rqt_bag 负责查看原始消息的内容和播放控制。
这个时候不必急着跑算法,先把数据本身的可视化质量确认清楚。看摄像头画面有没有花屏、卡顿、曝光异常,看激光雷达点云有没有明显的掉帧和丢线,看 CAN 信号速度曲线是否连续。很多时候数据质量问题在采集端并不显眼,但通过 ROS 回放很容易暴露,例如摄像头丢帧、雷达点云时间戳抖动、GPS 秒脉冲丢失导致的整段时间轴错乱等。可视化确认这个环节必须认真做,因为它是在为后面的算法评测建立"数据可用性"的基线。
4.3 驱动感知算法:以目标检测与融合验证为例
可视化确认没问题后,就可以让 bag 驱动算法节点了。以我们做的一个 FCW(前向碰撞预警)验证为例:bag 里有一个前视摄像头流和毫米波雷达目标列表流,算法侧需要做两件事,一是对图像跑目标检测,二是把毫米波雷达目标与视觉检测目标做融合输出。我在同一个 launch 文件里启动了以下节点:
/cam_front/image_raw订阅节点,经过一个检测模型推理输出带 3D 框的检测结果。/radar_front/objects订阅节点,把雷达目标列表叠加到图像上。- 一个后期融合节点,把视觉和雷达输出做关联匹配,并输出融合障碍物列表。
这种情况下,ROS 的消息过滤机制(message_filters)非常有用。因为 ADTF 转出来的 bag 里各 topic 虽然基于同一时间轴,但消息到达时间不可能完全整齐对齐,融合节点必须用时间同步策略——比如雷达 20Hz、视觉 10Hz,无法逐帧严格对齐,只能采用 approximateTime 同步策略,把一段时间窗口内最接近的视觉帧和雷达帧配对,然后做融合。但要注意窗口设置过大,会引入明显的延迟感,设置过小又会丢失匹配对,需要根据实车数据的传感器采样特性来调。
最后用 RViz 回放看一下融合效果,主观评估检测框是否稳定、目标是否容易出现跳变、雷达和视觉是否出现明显的重叠错误。如果发现问题,可以回头去查 bag 的原始数据质量,也可以查算法参数。这一步能非常有效地暴露"算法本身没问题,但数据适配出了问题"的情况——这种情况在数据驱动开发流程里太常见了。
4.4 客观评测:把 Ground Truth 和算法输出做对比
可视化评估只能说明"看起来还行",要做严格验证必须有可量化的评测指标。我们的做法是在回放链路里加入一个评测节点(evaluation node),它同时订阅算法的输出结果和一个预先标注好的 Ground Truth 话题(或者人为从采集数据中提取的参考位置),在回放结束后输出一系列指标。
以 ADAS 前车检测为例,评测节点会计算:
- 检出率:算法在每一帧是否成功检测到目标车。如果 ADTF 采集的输入视频里有大量暗光或遮挡场景,这一步能暴露训练数据和真实数据的 domain gap。
- 距离误差:毫米波雷达目标距离值与 Ground Truth 的误差(RMS、均值、MAX)。若误差偏大,需要回溯是雷达标定问题还是转换时坐标偏移问题。
- 时间同步质量:图像帧和点云/雷达目标的时间戳交汇是否稳定。
评测结果出来后,再和算法组事先给定的指标阈值做比对,给出 PASS/FAIL 的结论。这一步是整个回放验证的闭环环节——没有量化指标,回放就只能算"看了一遍数据",不能叫"验证"。
5. 踩坑记录:从时间漂移到花屏图像,问题定位思路分享
最后分享一下我们这段时间踩的比较有代表性的坑,有些甚至一度让我怀疑自己是不是选错了技术路线。把它们写下来,希望后来人少走弯路。
5.1 DAT 文件读不全?先查流描述文件再查指针位置
第一次跑批处理转换时,有个大文件转出来的 bag 里图像流中间断层,前后时间段完整,唯独中间 2 分钟的数据"凭空消失"。起初以为是读文件的部分出了问题,加了很多日志去打印 sample 的 index 和时间戳,发现断层的起点和终点都非常整齐,很像是有个流在某个时刻停止写入。后来一查采集端的日志才发现,当时路测车过了一段隧道,隧道里有一段 GPS 信号丢失,而 ADTF 的采集配置里某些流的触发是依赖 GPS 同步信号的——GPS 一丢,这个流整体停止采集。数据本身没有坏,是采集在物理层面停了。这种情况没法靠转换器解决,只能在采集端增强配置——不依赖 GPS 做触发,改用自由运行的硬件时钟加独立时间同步。
所以转换器在遇到"流中断"这个情况时,不能简单抛异常,而应该记录日志并继续处理后续流。可靠性比速度更重要,跑两三天批处理程序崩掉,那份焦虑我是感受过的。
5.2 时间戳相差整 8 个小时:时区问题引发的"假时间漂移"
有一次转换完做时间分析,发现所有图像流的时间都比如今北京时间晚了整整 8 个小时。当时第一反应是源数据采集端的时间基准用的是 UTC,而 ROS 的rostime期望的是 epoch 秒——差 8 小时刚好是 UTC 和 UTC+8 的时区差。ADTF 采集端可以在配置里设置时区,但之前同事图省事直接用的 UTC,转换器加了时区配置项后这个问题就彻底解决了。这件事让我记住了:数据格式转换不是纯技术活,源数据的物理语义也要一起拷过来,时区、单位、坐标系、编码全是必须记录和检查的元数据。
5.3 图像全绿或花屏:编码映射错位的典型表现
开始做摄像头流转换时,RViz 里看到的图像一会儿全绿,一会儿像打翻颜料盘。排查了一圈,最后定位到 ADTF 的 YUV422 数据在写入sensor_msgs/Image时,encoding字段写成了rgb8,相当于把 YUV 数据当成 RGB 去解析,颜色自然全错。修复非常简单,把 encoding 改成yuv422或者干脆提前用 OpenCV 转成bgr8再写 bag。这件事看起来很小,但在做多传感器适配时极易遇到,尤其不同摄像头型号的数据编码存在差异。建议在转换器里做一个编码映射校验表,不认识的编码宁可报错,也不能猜。
5.4 rosbag record 或转换输出体积爆炸?先考虑压缩和降流
ROS bag 不压缩时体积非常可怕,6.3 GB 的源 .dat 文件转成 bag 可能变成 9~10 GB。如果只是做可视化检查,完全没必要保留这么多数据。rosbag 本身支持压缩,record 时可以加--lz4,回放前也可以用rosbag compress做一次压缩。转换器同样可以加选项:图像流转成sensor_msgs/CompressedImage,点云流抽帧降采样,这样 bag 体积能降到原来的 1/5 左右。但要注意,压缩和降采样只适合做可视化验收和算法功能验证,如果要做高精度的评测,还是必须保留无损的原始数据。
工具链这条路,一旦打通,后续的收益是很可观的。我们现在的日常是:路测车队每天产出的 ADTF 数据,晚上自动触发转换任务,第二天早上算法团队就能在 ROS 环境里拿到结构清晰、时间对齐、坐标系完整的 bag 包,直接开始回放和评测。整个流程也从"人肉搬数据"变成了"数据工厂流水线"。
如果你也在做类似的事情,我的建议是不要贪多,先把一条流(比如单摄像头 + CAN 信号)完整跑通,验证好时间戳和坐标系,再逐步扩展点云和雷达,最后再考虑批处理流水线。另外,转换器在团队里要当作正式的软件工具维护,版本管理、单元测试、配置文档都不能省,否则三个月后你自己都可能不知道当初那套映射表是怎么写的。