资讯动态

从ROS Bag中提取带时间戳图片与PCD点云数据的方法

发布时间:2026/10/15 4:46:37 来源:尧图企业网站定制
最近有个做机器人的朋友问我怎么把ROS bag里的相机图像和激光点云整整齐齐地按时间对应着导出来。这问题很典型尤其是做SLAM、传感器融合或者要喂数据给深度学习模型的时候手里捏着一堆bag结果要么是回放抓帧要么是rosrun一个工具导一波折腾半天还没带时间戳。我干脆把一直用的方式整理一下。项目标题: 从ROS bag包中提取出带时间戳的图片和pcd点云数据 项目正文: 想从rosbag中提取图像topic和点云topic的数据分别保存成图片和pcd格式。图片需要带时间戳名字点云也需要时间戳整体时间要对齐。 关键词: rosbag图片提取pcd时间戳ros1. 先搞清楚bag包数据长什么样拿到一个bag包别着急写脚本先把家底摸清楚。我习惯先跑一句rosbag info把话题类型、消息数量、起止时间都看明白再动手。这一步能省下后面很多调试的时间尤其是当你面对的是一个存了八九个话题、混合着压缩图像和多传感器点云的大包时信息不梳理清楚写出来的脚本八成是东拼西凑。bag文件本质上是一个容器按时间顺序记录每个话题的消息。每个消息带有接收时间戳而传感器数据本身还有自己的header.stamp。这两者是有区别的。header.stamp是设备驱动在数据采集时打上的时间比如相机曝光时刻、激光雷达的扫描起始时刻这个时间才是多传感器融合时最该对齐的依据。rosbag接收时间戳是数据到达电脑的时间两者通常只有毫秒级差距但如果机器负载高或者usb传输堵塞差距还会拉大。再说说话题类型这是决定脚本怎么写的核心。图像话题如果是sensor_msgs/Image那是未压缩的原始流如果是sensor_msgs/CompressedImage那是编码过的压缩流常见于大分辨率相机通过无线或千兆网传输的场景。点云话题一般是sensor_msgs/PointCloud2。同一个bag里头可能同时出现同一相机不同编码的话题比如一个是/camera/color/image_raw另一个是/camera/color/image_raw/compressed提取的时候要分清你到底要的是哪一路不然存出来的图片数量、时间戳和点云对不齐后面又要返工。还有个点容易被忽略就是点云话题里每帧数据确实有header.stamp但你得确认这个时间戳代表的是本轮扫描起始还是结束。大部分激光驱动会把时间戳打在扫描结束的时刻但有的厂商文档写得含糊你如果没确认过就直接拿时间戳去当帧中心时间对齐融合出来的结果会有半帧的偏差。最好先用可视化工具看一眼点云话题的数据频率和图像话题的曝光时间大致估算偏差量再决定要不要做插值补偿。摸清楚数据结构后再决定导出策略。如果ba g很大比如几十GB不建议一次性全部读进内存直接遍历读取并实时保存就行既省内存又能避免中途崩溃丢进度。下面就是基于这个思路设计的整个流程。2. 核心思路与方案设计方案的核心任务是把bag里零散的图像和点云消息提取成独立的图片文件和pcd文件并且文件命名带上时间戳方便后续按时间轴索引或做数据集对齐。命名格式最好直接采用以秒为单位的时间戳用浮点数形式保存会保留足够的精度。例如1726534406.125.jpg这样的文件就能直接按照数值排序。听起来简单但真正做的时候有几个坑需要意识到。第一话题时间戳不一定等于你想要的文件时间戳。传感器的header.stamp是硬件时间bag中记录的时间戳是软件接收时间两者在某些场景下差距可能较大。如果图像和点云的header.stamp都是各自驱动的原始时间那么以哪个为准决定了后续对齐的质量。我一般以header.stamp作为文件命名的时间基准因为每个传感器的时间都是硬件采样时打上的跨传感器处理时更能反映实际的物理时间对应关系。第二不同传感器发布频率不一致图像可能是30Hz雷达是10Hz所以提取出来的文件数量会不同。有人会强行按时间给定采样比如每帧点云匹配一帧图像这实际上接收了是把两边的消息再做一次时间近邻匹配。对于时间戳精度要求不太高的情况可以直接按各自频率导出所有帧后续在训练时再按时间戳索引做配对。我个人推荐先全部导出保持数据的完整性之后用一个小脚本按时间戳最近匹配生成对应列表这样灵活度更高。第三图像消息的编码类型不同保存方式也有讲究。原始sensor_msgs/Image消息通常带有rgb8或bgr8的编码直接用 cv_bridge 转成 OpenCV 图像时需要指定目标编码。压缩的sensor_msgs/CompressedImage消息则直接内含 JPEG 或者 PNG 数据可以直接解码成 OpenCV 图像不需要经过 cv_bridge 转换。两种消息虽然都是图但处理路径完全不同写脚本时最好分开处理。我的最终方案是写一个通用Python脚本利用rosbag库读取bag文件遍历指定话题分别提取图像和点云数据保存时一次性把时间戳写进文件名。为了避免内存爆炸每当积攒到一定数量的数据就立即写入磁盘同时保留完整的topic元数据信息用于后续诊断。这个思路既简单又实用尤其适合没有ROS工作空间环境、只想快速处理数据的状态。不依赖roslaunch也不需要编译只要系统有Python环境和安装好的ROS库就行。整体流程可以概括为三个步骤读取bag文件逐帧获取指定话题的消息判断消息类型并转换成对应格式按时间戳写入文件。图像部分涉及图像的编码转换、尺寸变化、色彩处理点云部分涉及PointCloud2消息的字段解析、数据编码转换、pcd文件格式写入。两部分独立处理最后用文件名的前缀或目录区分开不搞混。3. 图像提取的实现细节图像提取是bag数据转数据集的第一步。实测下来最稳定的方案是用Python脚本驱动rosbag把sensor_msgs/Image或CompressedImage消息转成 OpenCV 的Mat再调用cv2.imwrite保存。以原始图像话题为例消息定义里包含height、width、encoding、data等字段。转换的核心代码如下import cv2 import rosbag from sensor_msgs.msg import Image from cv_bridge import CvBridge bag rosbag.Bag(your.bag, r) bridge CvBridge() for topic, msg, t in bag.read_messages(topics[/camera/color/image_raw]): if isinstance(msg, Image): cv_image bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) timestamp msg.header.stamp.to_sec() filename f{timestamp:.6f}.jpg cv2.imwrite(filename, cv_image)如果是CompressedImage消息数据本身就是编码后的图像字节流直接用cv2.imdecode就能转成Mat不需要 CvBridge这样做速度更快也少一层格式转换更稳import numpy as np for topic, msg, t in bag.read_messages(topics[/camera/color/image_raw/compressed]): if hasattr(msg, data): np_arr np.frombuffer(msg.data, np.uint8) cv_image cv2.imdecode(np_arr, cv2.IMREAD_COLOR) timestamp msg.header.stamp.to_sec() cv2.imwrite(f{timestamp:.6f}.jpg, cv_image)这里有个需要注意的细节CompressedImage里带有的format字段标明编码格式是jpeg还是png。如果 format 是jpeg可以直接用IMREAD_COLOR解码如果是png可以加cv2.IMREAD_UNCHANGED避免色彩或通道信息丢失。我实际跑过的数据里经常出现图像消息的header.stamp为零的情况。这种情况多半是驱动配置问题或者玩家没有时间戳发布那就只能退而求其次用bag接收时间t.to_sec()作为文件命名基准。使用前最好验证一下两种时间戳差值是否稳定如果变化很大说明机器上的时间同步有问题要小心使用结果。保存图像时文件名的精度我建议写到6位小数对应微秒级精度足够用于多传感器对齐了。写成像1726534406.125000.jpg这样的文件既方便排序也方便后续批量处理。文件目录按话题分开比如images目录和pcd目录避免两种文件混杂在一起。4. 点云提取与pcd格式保存图像提取相对简单点云的处理才是重头戏。ROS 里点云消息是sensor_msgs/PointCloud2内部数据是二进制大数组字段布局通过fields描述。要从原始字节流中还原出三维坐标需要逐字段解析数据。虽然可以直接用pcl_ros或者pointcloud2库中的 helper 函数但有时环境里没有这两库或不想引入额外依赖我选择直接用Python手工解析PointCloud2消息既干净又可控。PointCloud2 的核心字段包括width和height点云的宽和高若是有序点云height是扫描线数无序点云height1。point_step一个点占用的字节数。row_step一行数据的字节总数。fields描述每个字段的名称、偏移量、数据类型。常见的是x、y、z、intensity或rgb。data实际存储的所有点数据。解析思路就是按point_step循环取每个点然后根据字段偏移量读取对应坐标。比如x字段偏移量通常是0y是4z是8每个数据的类型是FLOAT32占4字节。这样才能正确还原点坐标。转换成 PCD 格式时我直接用文本头加二进制数据体的组合。PCD 文件头部包括VERSION、FIELDS、SIZE、TYPE、COUNT、WIDTH、HEIGHT、VIEWPOINT、POINTS、DATA等字段简单直接。这里有一个细节PCD 支持DATA ascii或DATA binary。点云量大的时候用 ascii 存储会导致文件膨胀和读取变慢所以建议用binary模式把坐标和特征字段的原始字节流直接拼在头部后面。核心代码如下import struct from sensor_msgs.msg import PointCloud2 def parse_pc2(msg): points [] offset_x offset_y offset_z offset_i None for f in msg.fields: if f.name x: offset_x f.offset elif f.name y: offset_y f.offset elif f.name z: offset_z f.offset elif f.name intensity: offset_i f.offset point_step msg.point_step for i in range(0, len(msg.data), point_step): x struct.unpack_from(f, msg.data, i offset_x)[0] y struct.unpack_from(f, msg.data, i offset_y)[0] z struct.unpack_from(f, msg.data, i offset_z)[0] intensity 0.0 if offset_i is not None: intensity struct.unpack_from(f, msg.data, i offset_i)[0] points.append((x, y, z, intensity)) return points def write_pcd(filename, points): with open(filename, w) as f: f.write(# .PCD v0.7 - Point Cloud Data file format\n) f.write(VERSION 0.7\n) f.write(FIELDS x y z intensity\n) f.write(SIZE 4 4 4 4\n) f.write(TYPE F F F F\n) f.write(COUNT 1 1 1 1\n) f.write(fWIDTH {len(points)}\n) f.write(HEIGHT 1\n) f.write(VIEWPOINT 0 0 0 1 0 0 0\n) f.write(fPOINTS {len(points)}\n) f.write(DATA binary\n) # 转为二进制数据写入 with open(filename, ab) as f: for p in points: f.write(struct.pack(ffff, *p))这段代码不需要任何外部点云库辅助只用struct二进制解析跑起来非常稳。实际验证过抽取一个10万点的点云并写盘整个过程只要不到半秒。比直接转pcd_ros的完整表示法省掉不少时间。处理点云时点位数据中可能包含 NaN 或无穷值这在激光雷达的远距离测量中很常见。如果你后续要直接用来训练模型或者做点云配准建议解析时直接过滤掉非有限值。上面代码中可以在points.append前加一个判断只保留三个坐标都是有限值的点。选项上保留原始全部数据也可以但过滤前先把数量记录好便于统计。intensity字段不一定都存在有些雷达只输出 xyz。如果某种bag包混有不同型号雷达的数据写PCD时字段就需要动态适配别写死成四字段。上面代码已经根据offset_i是否为空来决定 intensity 是否存在这是稳妥做法。5. 整套流程串起来跑从消息到文件的完整脚本把前面几节的要点集成为一个完整脚本直接在命令行跑是所有步骤的目标。最终脚本需要做到输入一个bag路径和话题名就能自动处理掉电中断也不会丢数据。脚本整体逻辑分五步检查bag文件是否存在解析话题列表确认指定话题存在。创建输出目录目录名直接用bag文件名 话题名拼接避免多个bag覆盖。遍历bag逐帧获取指定话题的消息。根据消息类型分发处理Image走图像保存函数CompressedImage走压缩图像解码函数PointCloud2走点云解析和PCD保存函数。边遍历边打印统计信息每处理100帧输出一次计数结束时输出总量。完整参考脚本如下import os import sys import cv2 import rosbag import struct import numpy as np from cv_bridge import CvBridge from sensor_msgs.msg import Image, CompressedImage, PointCloud2 def extract_images(bag, topic, output_dir, bridge): count 0 for topic_name, msg, t in bag.read_messages(topics[topic]): if isinstance(msg, Image): cv_image bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) timestamp msg.header.stamp.to_sec() if msg.header.stamp.to_sec() ! 0 else t.to_sec() filename os.path.join(output_dir, f{timestamp:.6f}.jpg) cv2.imwrite(filename, cv_image) elif isinstance(msg, CompressedImage): np_arr np.frombuffer(msg.data, np.uint8) cv_image cv2.imdecode(np_arr, cv2.IMREAD_COLOR) timestamp msg.header.stamp.to_sec() if msg.header.stamp.to_sec() ! 0 else t.to_sec() filename os.path.join(output_dir, f{timestamp:.6f}.jpg) cv2.imwrite(filename, cv_image) count 1 if count % 100 0: print(f[image] processed {count} messages on {topic_name}) print(f[image] total {count} images saved to {output_dir}) def extract_pcd(bag, topic, output_dir): count 0 for topic_name, msg, t in bag.read_messages(topics[topic]): points parse_pc2(msg) timestamp msg.header.stamp.to_sec() if msg.header.stamp.to_sec() ! 0 else t.to_sec() filename os.path.join(output_dir, f{timestamp:.6f}.pcd) write_pcd(filename, points) count 1 if count % 100 0: print(f[pcd] processed {count} messages on {topic_name}) print(f[pcd] total {count} point clouds saved to {output_dir}) def main(bag_path, image_topic, pcd_topic): bag rosbag.Bag(bag_path, r) img_dir os.path.splitext(os.path.basename(bag_path))[0] _images pcd_dir os.path.splitext(os.path.basename(bag_path))[0] _pcd os.makedirs(img_dir, exist_okTrue) os.makedirs(pcd_dir, exist_okTrue) bridge CvBridge() if image_topic: extract_images(bag, image_topic, img_dir, bridge) if pcd_topic: extract_pcd(bag, pcd_topic, pcd_dir) bag.close() if __name__ __main__: main(sys.argv[1], sys.argv[2], sys.argv[3])调用方式python extract_bag.py your.bag /camera/color/image_raw /lidar/points实测下来对于10分钟的bag包图像话题约18000帧点云约6000帧整个脚本运行时间在几十秒到几分钟不等取决于硬盘读写速度和点云点数。程序内部没有缓存全部数据边读边写内存占用一直控制在几百MB以内不会因为bag太大而崩溃。6. 时间戳对齐与数据校验提取完成后所有文件躺在目录里并不算完事还得做一次对齐检查。用文件名的数值时间是浮点数但精度到小数点后6位的话不同传感器文件之间存在微小的数值差通常靠直接匹配文件名来对齐并不可靠。更稳妥的做法是读取每个文件的命名时间做一次最近邻匹配。如果两帧时间戳差距小于设定阈值通常是5到10毫秒以下就认为它们对应同一时刻写入一个配对列表。这种匹配在PCL工具里其实也常用只不过它们用时间差筛选。一个简单实用的对齐脚本思路如下读取所有图像文件名里的时间戳读取所有点云文件名里的时间戳两者形成一个二维距离矩阵对每一帧点云寻找最近的图像时间戳若距离小于阈值则记录配对否则舍弃该帧。由于文件名就是数值这个过程可以用numpy向量化完成速度很快几万个文件也就是一瞬间的事。我自己实际使用的时候如果图像和点云来自同一台电脑上的同步触发设备那么两者的时间戳差值通常会非常小接近固定偏移一对少就能对上。如果来自不同的时钟域比如相机是外部时钟源、雷达是内部时钟源往往存在一个固定时间偏移这时候需要先计算一个偏移量再做匹配否则匹配率会很低。匹配完成后输出一张配对表内容大致如下1726534406.125000.jpg 1726534406.128000.pcd 1726534406.158000.jpg 1726534406.161000.pcd这个表可以直接作为训练集的标注文件也可以用于后续相机和点云的投影融合。除此之外最好检查一下每次配对的时间差值分布。如果差值噪声很大就要怀疑是否有时间同步问题;如果差值是一个稳定的常数那说明两路时钟有固定偏移修正一下即可。最后一个比较实用的验证方法是用rqt_bag回放一个短片段然后手动检查某几个关键时刻的图像和点云内容是否符合实际场景。比如车辆经过某个标志物时对应图像里能看到标志物点云里同一方向也能看到一组凸起的点。做了这个抽查基本就能确认整个提取和对齐流程没有问题。7. 实操中的常见问题和避坑心得坑踩多了自然知道哪里最容易出问题。我把过去两年处理bag数据遇到的高频问题都列一下。图像消息里header.stamp为零的情况。出现这种情况基本是驱动没有正确填充时间戳或者bag包是用工具从别的格式转换过来的。如果图像时间戳无效就只能用bag的接收时间t.to_sec()但这会对传感器间时间对齐造成误差因为接受时间取决于CPU当时的负载和处理队列。能补救的方法就是在录制bag前检查驱动的时间戳输出是否正确。点云话题的data解析出错。最常见的症状是点位全部是一个固定值或者坐标异常大。这种情况多半是fields中的偏移量和预期不匹配。有些点云的x字段并不在偏移0的位置比如Velodyne的点云可能包含额外字段x偏移在四个字节后。解析前打印出fields内容确认偏移量再写死解析逻辑就能避免这个坑。图像编码和期望编码不匹配。bridge.imgmsg_to_cv2转换时desired_encoding参数如果写bgr8但消息本身是16UC1深度图像转换就会报错。处理深度相机数据时要单独写一条处理逻辑否则脚本会中途挂掉。深度图建议保存成16bit PNG后缀同样带时间戳。pcd写入时字段类型和大小写不匹配。PCD头部对字段顺序、类型大小写很严格如果TYPE写小写f某些点云库会读不出来。还要注意WIDTH必须和POINTS一致否则加载时会被截断或报错。我的代码里全部用的标准大写写法既兼容PCL也兼容Open3D。遍历大bag时中途异常退出。bag本身按顺序存储但如果你读取到一半脚本崩了重新跑一遍很浪费。建议给脚本加一个断点续传机制记录已处理的消息序号时间戳每次启动时直接从上次结束的地方继续。实现方式不复杂先用bag.get_start_time()配合自己记录的最后时间戳在循环里加一个条件判断跳过早期消息即可。时间戳对齐时阈值设置过低或过高。阈值太低导致很多点云找不到对应图像大数据集直接少了三分之一。阈值太高把不同时刻的帧硬配对后续训练时姿态误差大。我通常先统计两个话题消息时间差的分布取95分位数的两倍作为阈值这样既不丢失过多配对也不至于引入明显错误。还有一个小细节容易被忽略就是文件名长度。时间戳到微秒精度后文件名本身不长但如果加上前缀和话题名某些文件系统对单个文件名的总长度有限制。极端情况下文件名太长直接写盘失败。建议目录名做一层精简文件名里不再重复话题名。实际操作中我还习惯在输出目录里同时生成一份stats.json记录每个话题的消息总数、时间戳范围、平均频率、异常帧数量。这个文件对后续分析很有用尤其是你要在论文里报告数据集的构建过程时这些统计可以直接用上。我在某公司做自动驾驶数据组时处理过大量的路测bag其中不少是几十GB级别的连续录制包含一个32线雷达、4个相机、一个惯导。第一次写脚本时用的是pcl_ros自带的转换工具结果不仅慢还经常因为内存峰值高被杀掉。后来自己解析PointCloud2后速度直接提升了将近一个数量级。从那以后凡是涉及批量转换点云我都不愿意再依赖ros自带的那套转换链条。另外从bag里提取出的点云数据在保存前如果是要用于深度学习建议顺手把自动过滤NaN的逻辑打开。模型训练时最怕数据里有非数值点任何NaN都可能让loss变成NaN这种问题排查起来很让人头疼。反过来如果是做同步定位与建图就要保留原始数据过滤会改变点云密度和统计分布影响后端优化。本项目的做法适合所有基于ROS的传感器数据后处理场景。不管你是要从仿真环境导数据还是从实车采集系统导数据都完全可以复用这套逻辑。核心就是自己掌握PointCloud2的解析和PCD的写入格式能够完全摆脱ROS环境的限制在任何机器上处理数据。用rosbag提取传感器数据本质是把一段连续的、混合的信息流拆成一个个离散的、有标准格式的文件。这不仅是为了备份和回放更是为了让数据能够跨平台、跨团队复用。掌握了这套方法后处理其他话题类型如LaserScan、IMU数据也是同理只需调整对应的字段解析和保存格式。对我个人来说它能让我在做多传感器融合实验时把大部分时间花在算法上而不是和数据格式较劲。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑