十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ROS2 bag高效导出利器:ros2_unbag原理与实战

ROS2 bag高效导出利器:ros2_unbag原理与实战 做了这么多年机器人数据采集和后处理我最烦躁的事情之一就是处理 ROS 2 的 bag 文件。明明是自家传感器录的数据想把它变成能丢进 pandas 或者拿来训练的干净数据集却总要绕一大圈先ros2 bag play再写一堆订阅节点或者硬啃 sqlite3 里的二进制流。直到我把ros2_unbag这套工具链用顺了才算把从 bag 到数据这条路彻底走通。这篇博文就从我自己的使用体验出发把 ros2_unbag 的原理、部署、实操命令和踩过的坑完整拆给你看无论你是跑 VINS-Fusion 想做数据集还是想按时间范围切 bag 做训练集都能在里面找到一套可以直接抄作业的流程。1. 我为什么绕开 ros2 bag 自带的导出链路1.1 官方命令的三个明显短板先别急着学工具得先把痛点说清楚。面对一个 bag大多数人的第一反应是ros2 bag info看一眼主题然后ros2 bag play回放。可一旦你要的不是看而是导出这套官方组合拳就开始变扭了。第一个短板是速度。ros2 bag play是按消息原始发布频率回放的一个 10 分钟的数据包你就得等 10 分钟才能跑完。你以为可以按倍速加速但消息发布频率一旦被倍速放大对订阅端节点的处理能力就是考验更别说图像数据流回放时经常出现丢帧或排队最后导出的时间戳都变形了。第二个短板是格式。官方ros2 bag convert只能做存储插件之间的迁移比如把 sqlite3 格式转成 MCAP它不会帮你把sensor_msgs/msg/Image变成一张 PNG也不会把sensor_msgs/msg/Imu变成 CSV。数据要从序列化二进制变成能分析的结构化文件官方根本没提供这条路径。第三个短板是筛选能力。按时间范围切 bag、按主题挑数据、把 100Hz 的 IMU 降到 10Hz 再导出这些高频需求在官方 CLI 里基本没有直接参数。每次想做这类操作都得写 Python 脚本然后一步步处理非常痛苦。1.2 ros2_unbag 是什么它和 rosbags 这类库有何区别ros2_unbag 就是冲着上面这些痛点去的。它本质上是一个直接解析 ros2 bag 文件的高性能导出工具不需要启动节点、不需要回放、不依赖 ROS 2 的运行时拓扑。你给它一个 bag 路径它会把里面的消息按主题逐条读出来反序列化后写到磁盘上输出成图片、CSV、PCD 等格式。可能有人要问那 Python 生态里有rosbags这个库功能看起来也类似到底该用哪个我的看法是两者定位不同。rosbags是纯 Python 实现跨发行版、改脚本方便适合做原型验证和深度定制而 ros2_unbag 这类 C 实现更注重吞吐量在处理几百 GB 的大 bag、或者需要快速批量导出图像时优势明显。我个人的习惯是跑一次性分析用 Python rosbags跑长流水线或批量导出预训练数据用 ros2_unbag两者互补不冲突。2. 打开 bag 文件的底层逻辑存储结构、序列化与时间戳2.1 存储插件从 sqlite3 到 MCAP想把 ros2_unbag 用明白得先知道 ros2 bag 到底长什么样。ROS 2 的 rosbag2 框架是插件化设计的目前最常见的两种存储后端是 sqlite3 和 MCAP。如果是 sqlite3 格式你会看到一个目录里面有metadata.yaml和若干.db3文件。metadata.yaml记录了这个 bag 的起止时间、主题列表、消息数量、存储插件类型。.db3是 SQLite 数据库文件里面主要是一张topics表记录主题元数据一张messages表以行形式存放每条消息的序列化二进制数据。打开 sqlite3 文件看一下你会发现大多数列都是二进制字段直接读是读不出含义的——因为消息内容是 CDR 序列化后的字节流。如果是 MCAP 格式则是一个单一.mcap文件里面既有 schema 定义又有 chunk 数据自包含性和压缩效率更好。ros2_unbag 需要兼容这两种后端读metadata.yaml或 MCAP 的 header 来获取主题与类型信息然后按需读取对应通道的消息块。这里有个实用经验拿到一个 bag第一步永远是看存储后端因为这会直接影响你后续排查问题的思路。如果打开一个 bag 结果显示 0 条消息先别急着怀疑工具先看metadata.yaml里的storage_identifier是什么再确认文件后缀和插件是否匹配。2.2 消息序列化CDR 二进制如何变成可读数据CDR 是 OMG 定义的通用数据表示格式ROS 2 默认的消息序列化就基于它。ROS 2 中每个消息类型都对应一份类型支持代码负责把结构体如sensor_msgs::msg::Imu编码成字节流或把字节流解码回结构体。ros2_unbag 能读懂bag 里的消息正是因为它复用了 ROS 2 的类型支持机制。它会根据metadata.yaml里记录的主题类型名找到对应的类型支持库然后对messages表里的二进制数据做反序列化还原成结构体对象再按你指定的规则写到文件里。这里面最关键的认知是反序列化不一定需要启动节点也不需要完整的 ROS 2 图通信。ros2_unbag 走的是一条脱机解析路线它只需要类型支持代码不需要 master、不需要 discovery、不需要ros2 bag play。这也是它能高速导出的根本原因——没有回放调度和网络传输的开销。你可以把它类比成解压文件bag 是一个压缩包CDR 是里面文件的编码格式ros2_unbag 就是那个知道每种文件格式该怎么解码的解压器。前提是它得认识这个编码格式也就是得有对应的消息类型支持。2.3 时间戳两种时间来源导出前必须想清楚时间戳是 bag 处理里最容易出问题的地方我这里单独拉出来讲。一个 bag 里面其实存在两套时间概念第一套是录制端时间也就是 ros2 bag record 进程写入消息时记录的时间它表示这条消息是什么时候被写入 bag 的第二套是消息内部时间比如std_msgs/msg/Header.stamp它表示这条消息在传感器驱动里产生的时间。很多场景下这两套时间几乎一样但如果录制时有网络延迟、处理排队、或者数据是被离线灌进去的两者就会产生滑动偏差。ros2_unbag 在导出时通常默认保留录制端时间作为输出索引同时也会解析消息内部的header.stamp写入导出文件。我的建议是导出的每一行数据都必须同时保留这两种时间戳宁可冗余也不要丢失来源信息。否则等你想做时间对齐时才发现其中一种时间没有导出又得重新跑一遍白白浪费时间。如果你做的是传感器标定或 VIO 精度评估请务必以传感器消息内部的header.stamp为准而不是录制端时间。原因很简单回放延迟只影响录制端时间不影响传感器真实触发时刻算法做时间对齐时需要的是采集时刻而不是落盘时刻。3. 部署与实操从安装到第一条数据导出3.1 从源码编译安装ros2_unbag 目前最稳的安装方式是源码编译。它所依赖的东西主要包括对应版本的 ROS 2我用的是 Humble、Eigen3、Boost以及类型支持相关的运行时库。在 Humble 环境下我通常这样操作source /opt/ros/humble/setup.bash cd ~/workspace git clone https://github.com/社区路径/ros2_unbag.git cd ros2_unbag mkdir -p build cd build cmake .. make -j$(nproc) sudo make install编译过程里最容易翻车的点有两个一是没有先 source 对应发行版的 setup.bash 导致rosidl相关 CMake 包找不到二是系统里同时存在多个 OpenCV 或 Boost 版本导致链接混乱。所以编译前建议先把环境整理干净用printenv | grep ROS确认当前用的是 Humble 而不是其他发行版。装完后在终端敲ros2_unbag --help能看到完整的子命令和参数说明就说明基础环境对了。具体参数名不同版本可能略有差异以实际打印的 help 为准。3.2 一条命令导出所有主题安装验证没问题后最直接的用法是整包导出ros2_unbag export -i /path/to/rosbag2_2024_05_01-15_30_00 -o ./extracted_data跑完后在extracted_data目录下你会看到按主题名组织的子目录每个子目录里的文件就是该主题对应的导出结果。图像主题一般是序列帧 PNG 或 JPG数值型主题一般是带时间戳的 CSV。这个目录结构非常直觉后续处理脚本直接按主题路径去读就行。我第一次跑的时候还担心它会像ros2 bag play一样按原速回放结果几 GB 的包几十秒就出完了当时的反应是这也太爽了。不过要注意导出速度主要取决于磁盘 IO 和反序列化 CPU 开销图像多且未压缩时瓶颈通常在 CPU 解码和写文件上。3.3 按主题、按时间范围、按频率控制导出内容大多数情况下我们不需要导出所有主题ros2_unbag 提供了灵活的参数来控制范围。我经常用的组合大致是这样ros2_unbag export \ -i /path/to/bag \ -o ./vins_extract \ --topics /cam0/image_raw /imu0 \ --start-time 2024-05-01 15:30:05 \ --end-time 2024-05-01 15:31:00 \ --rate 0.5参数含义可以整理成下面这张表参数作用使用建议--topics只导出列出的主题可多个用空格分隔少用--all-topics避免输出垃圾数据--start-time/--end-time按绝对时间范围切分 bag格式一般支持YYYY-MM-DD HH:MM:SS或 epoch 秒--rate导出频率倍率0.5 代表抽取一半2.0 代表按两倍密度采样常用于降采样--include-topics/--exclude-topics更精细的主题过滤规则支持通配符适合主题名成体系的 bag这里特别说一下--start-time和--end-time。很多人习惯先记下 bag 的起止时间然后用时间字符串切分。我在实测中发现如果 bag 的时间跨度特别长字符解析偶尔会有歧义这时直接用 epoch 秒最稳。先用ros2 bag info看到起止时间戳再换算成秒填进去样本结果准确率最高。3.4 落地为 CSV、PNG、PCD 等格式的组织方式ros2_unbag 对不同消息类型有默认的文件输出策略这也是它能一键导出的核心设计sensor_msgs/msg/Image默认输出 PNG 或 JPG 序列文件名就是时间戳或帧号方便后期用 OpenCV 读帧sensor_msgs/msg/Imu输出 CSV列通常包括时间戳、角速度三轴、线加速度三轴nav_msgs/msg/Odometry输出 CSV列包含位姿、姿态四元数、速度等字段sensor_msgs/msg/PointCloud2输出 PCD 文件可以直接塞给 PCL 或 Open3Dtf2_msgs/msg/TFMessage输出 CSV记录每个坐标系间变换的时间戳和平移旋转。这个设计对后续数据处理极其友好。比如我要做 VINS-Fusion 数据集预处理/imu0和/cam0/image_raw分别落成 CSV 和图片序列几乎不用写转换代码直接喂给算法就能跑。需要强调的是导出的图片命名时间戳最好统一用浮点数的秒值而不是%06d之类的帧号否则后续和 IMU 数据做时间对齐时还得来回查映射关系平白增加工作量。4. 实战三个我反复跑的 bag 导出场景4.1 VINS-Fusion 数据集批量预处理VINS-Fusion 是很多视觉惯性 SLAM 玩家绕不开的框架而跑它需要的数据组织形式通常是图片目录 IMU 的 CSV。以前我用手工脚本从 bag 里抽帧要写 cv_bridge 转换、要维护时间戳对应表效率极低。现在流程变成这样ros2_unbag export \ -i /path/to/vins_bag \ -o ./vins_dataset \ --topics /cam0/image_raw /imu0导出后./vins_dataset/cam0/image_raw下就是按时间戳命名的 PNG./vins_dataset/imu0下是标准 IMU CSV。我检查过几十个 bag 的导出结果图片序号的连续性、IMU 数据的完整性和 bag 原记录保持一致没有因为导出过程丢消息。这里有几个必须实测验证的细节第一确认图像编码是不是rgb8或bgr8有的 bag 里编码是mono8灰度图导出的 PNG 通道数不同后续读图时别搞混第二IMU 数据的字段顺序各传感器驱动可能不同尤其是四元数和角速度的次序导出后先打印前几行核对一下再灌给算法。4.2 多传感器对齐导出用于标定与验证多传感器标定时经常需要把 IMU、轮式里程计和 TF 三者拉在同一时间轴上对比。以前的做法是写三个订阅节点等数据搜集完再统一对齐。现在用 ros2_unbag 一步到位ros2_unbag export \ -i /path/to/calib_bag \ -o ./calib_data \ --topics /imu/data /odom /tf三个主题各自落成 CSV 文件时间戳都是浮点秒。我拿到这些 CSV 后在 Python 里用pandas.merge_asof做最近邻时间对齐几行代码就能得到同一时刻的 IMU、里程计、位姿联合数据表。这个方案比实时订阅节点稳定太多因为离线处理不受实时调度影响时间戳完全是 bag 里记录的真实时刻。对齐时要注意不同传感器频率差异很大比如 IMU 是 200Hz里程计是 50HzTF 可能只有 10Hz直接用merge_asof时方向参数别填反。另外如果是为标定服务强烈建议在导出时同时保留header.stamp和录制端时间标定算法通常需要前者而排查数据缺失时后者更管用。4.3 按时间范围切分 bag 用于训练集/测试集划分怎么按时间范围切 bag是社区里被问烂的问题。官方ros2 bag convert至今没有直接的时间范围参数很多人只能写脚本慢慢抠。ros2_unbag 的绝对时间参数就是用来干这件事的ros2_unbag export \ -i ./full_rosbag \ -o ./train_segment \ --start-time 2024-06-01 10:00:00 \ --end-time 2024-06-01 10:10:00这样切出来的数据相当于把原始 bag 在时间轴上截断了只保留目标区间内的消息。我经常用它把一段包含多个采集场景的长时间 bag切成若干段短数据一段用来训练一段用来做验证集再留一段做测试集。切完之后我会再跑一遍ros2_unbag export --topics ... --all-topics对切出的数据做完整性检查确认每段数据里各主题消息数符合预期。有一个经验值得分享如果 bag 是边采集边录的文件尾部可能存在一条录制中未完整写入的边界消息切分时最好避开起止时刻的前后几百毫秒稍微留一点余量避免把半截消息切进数据集。即使你没主动切分在把整包数据喂给算法前也建议检查首尾消息的时间戳和内容是否残缺。5. 我在迁移与使用中踩过的坑以及排查思路5.1 打开 bag 却显示 0 条消息这个坑我栽过不止一次。第一次遇到时我以为是 bag 文件损坏差点重录。后来排查才发现根因是 bag 的存储插件和工具默认插件不匹配。当前你的系统可能同时装了rosbag2_storage_sqlite3和rosbag2_storage_mcap插件但 ros2_unbag 默认优先尝试某一种一旦打开失败就静默降级成什么都没有。排查链路是这样的先用ros2 bag info看这个包能否被官方工具识别如果能就看输出里Storage id是sqlite3还是mcap然后在 ros2_unbag 的参数里显式指定存储插件类型或者检查自己安装的版本是否带对应插件支持。还有一个隐蔽情况如果metadata.yaml里的 duration 为 0但ros2 bag info显示每条消息都正常可能是这个 bag 在录制时被强制中断过metadata.yaml写入不完整有些工具会直接拒绝读取。对于疑似损坏的 sqlite3 bag我常用的排查姿势是sqlite3 bag_dir/bag.db3 PRAGMA integrity_check;这条命令会返回ok或具体的损坏位置。如果只有最后几条消息损坏通常不影响前面大部分数据的导出如果损坏发生在前面需要检查录制的存储介质是不是有坏块。5.2 自定义消息类型反序列化失败用 ros2_unbag 处理自研节点录的 bag 时最常见的错误是无法找到类型支持或导出直接报错。原因是 bag 里记录的消息类型是your_package/msg/YourMessage但当前环境里没有加载对应包的类型支持代码。解决办法说起来很简单在运行 ros2_unbag 之前先 source 你所依赖工作空间的 setupsource /opt/ros/humble/setup.bash source ~/your_ws/install/setup.bash如果你的自定义消息包是编译安装的导出时工具就能通过运行时的类型发现机制找到反序列化代码。这个问题在纯 Python 环境下用 rosbags 时更难察觉因为 rosbags 遇到未知类型只会报一个 warning 然后跳过该主题而 ros2_unbag 这种 C 工具经常会直接终止。处理原则是录 bag 时尽量把消息类型的源码和版本留档否则过了几个月回头想导出旧数据找不到匹配的类型支持才是真麻烦。我遇到过最折腾的场景是bag 录制于某个分支代码消息定义后来改了字段名再导出时字段对不上。这种情况只能在旧分支上重新编一个类型支持环境来导出或者从消息二进制流里手动拼字段。我的教训是数据采集工程里消息类型定义一定要和 bag 一起归档版本号写进文件名里别偷懒。5.3 大 bag 导出内存与 IO 瓶颈ros2_unbag 对单个 bag 是流式处理的这比一次性加载整个 bag 到内存的方案稳健得多。但即便这样导出一个 500GB 的全传感器数据包时仍然需要注意三个瓶颈。第一个是磁盘写入速度。图像和点云导出的成品体积通常比 bag 里的压缩体积大很多曾经我把一个 100GB 的 bag 导成图片后占掉了 400GB 磁盘。建议导出前用du -sh预估一下输出体积留足空间顺便确认目标磁盘不是那块正在录制数据的老机械盘否则 IO 会成为最大瓶颈。第二个是 CPU 解码开销。导出图像时如果 bag 里是压缩图像解码需要 CPU 密集计算多核机器可以开多线程参数让每个主题并行导出。第三个是 inode 数量。导出大量小图片时文件系统 inode 可能先被耗尽这时考虑先导出成视频或者压缩包减少小文件数量。内存方面ros2_unbag 的流式设计让内存占用相对平稳但如果某个主题是高频点云反序列化和坐标转换过程需要不少临时内存监控工具htop可以边跑边看。我遇到过一次因为输出目录和 bag 在同一块磁盘导致 IO 排队严重把输出路径放到另一块固态盘后速度立刻上来了。5.4 图像大量导出时的时间和空间成本图像数据是 bag 导出中的大头。同样一段视频流如果导成 PNG体积会非常大如果导成 JPG速度快、体积小但有损。我平时处理 VINS 数据集时如果算法要求无损图像就用 PNG如果只是做可视化或者快速验证JPG 质量 95 足够。还有一个常被忽略的点帧率控制。原始 bag 里如果图像是 30Hz但你的算法只需要 10Hz导出时直接抽帧能省大量空间和时间。用--rate 0.333或者更精确的时间间隔采样参数来降采样比导出后再抽帧高效得多。降采样时切记时间戳不能重新编号要保留原始header.stamp否则后续做插值或对齐时没有真实时间参考会出大问题。如果 bag 里本来就是压缩图像比如h264编码的sensor_msgs/msg/CompressedImage需要确认 ros2_unbag 的构建选项中是否启用了视频解码库支持不然导出也会失败。这里我的建议是需要高质量图像数据时优先录制未压缩的sensor_msgs/msg/Image避免二次编解码带来的损失。最后再分享一个小技巧。我平时构建数据流水线时不会把所有处理都压在一个工具上。ros2_unbag 负责把 bag 变成原始素材后续的筛选、对齐、清洗交给 Python 脚本处理。这样每一层只做一件事出了问题排查起来最快。尤其是时间戳这种贯穿全流程的关键信息从一开始就保持完整、不重命名、不丢失后面的数据集制作会省掉非常多麻烦。
返回列表