
ADTF 这个名字搞 ADAS 实车采集的工程师肯定不陌生但做算法开发的同事听到它大概率会皱眉头。前阵子我们团队就卡在这儿路测车队用 ADTF 辛苦跑了一个月攒了几百 GB 的高速、城区、隧道各种场景的传感器数据结果算法组的同事拿到手一看只能挠头——这些 DAT 文件我们 ROS 节点根本读不进去手里的感知模型和融合算法想拿真实数据验证还得先解决格式和工具链问题。于是就有了这次从数据采集到回放验证的完整打通实践把 ADTF 和 ROS 之间的适配层整个捋了一遍。这篇文章就把我的做法、踩过的坑和最终落地的方案整理出来给同样被数据孤岛卡住的团队一些参考。整个链路说简单也简单无非是采集、转换、回放、验证四步。但每一步都有不少隐藏的细节尤其是 ADTF 的数据模型、时间戳处理、坐标系对齐这些地方稍不留神就会让后续的回放验证前功尽弃。所以这篇不只是讲配置和命令更多的是讲讲我踩过的坑以及每一步背后的原理和取舍逻辑。1. ADTF 记录的数据为什么非搬到 ROS 里不可先解释一下背景。ADTFAutomotive 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 里消息时间戳出现明显抖动后续做时间同步算法时误判率会上升。二是录制时的分片策略我们建议每 1020 分钟切一个 .dat 文件方便管理和并行转换切片太大会导致单文件损坏后整体报废。三是存储介质必须用高速 SSDADTF 在写入大位流数据时对磁盘带宽非常敏感机械盘或者网络存储几乎必然丢帧丢了帧这个数据包基本就废了。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 APIC 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 消息类型映射说明VideoYUV/Bayer/RGBsensor_msgs/Image需指定 encoding、step、宽高Compressed Videosensor_msgs/CompressedImage保留原始压缩格式CAN Framecan_msgs/CanFrame 或自定需结合 DBC 解析GPS/GNSS NMEAsensor_msgs/NavSatFix需处理经纬度坐标Radar Object Listcustom_msgs/RadarObject参考原厂协议提取目标列表Point CloudLAS/自定义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 pkgtf2_ros typestatic_transform_publisher namecam_rear_to_base args0.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 和 UTC8 的时区差。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 可能变成 910 GB。如果只是做可视化检查完全没必要保留这么多数据。rosbag 本身支持压缩record 时可以加--lz4回放前也可以用rosbag compress做一次压缩。转换器同样可以加选项图像流转成sensor_msgs/CompressedImage点云流抽帧降采样这样 bag 体积能降到原来的 1/5 左右。但要注意压缩和降采样只适合做可视化验收和算法功能验证如果要做高精度的评测还是必须保留无损的原始数据。工具链这条路一旦打通后续的收益是很可观的。我们现在的日常是路测车队每天产出的 ADTF 数据晚上自动触发转换任务第二天早上算法团队就能在 ROS 环境里拿到结构清晰、时间对齐、坐标系完整的 bag 包直接开始回放和评测。整个流程也从人肉搬数据变成了数据工厂流水线。如果你也在做类似的事情我的建议是不要贪多先把一条流比如单摄像头 CAN 信号完整跑通验证好时间戳和坐标系再逐步扩展点云和雷达最后再考虑批处理流水线。另外转换器在团队里要当作正式的软件工具维护版本管理、单元测试、配置文档都不能省否则三个月后你自己都可能不知道当初那套映射表是怎么写的。