3. ROS Bag文件解析

做自动驾驶感知的人,每天打交道最多的数据格式,除了PCD、BIN,就是ROS Bag了。我个人觉得,Bag文件就像是一个时间胶囊——它把传感器数据、车辆状态、算法输出,全都按时间顺序打包存好。你随时可以打开它,回到那个采集时刻。

这一讲,我们就来彻底搞懂ROS Bag。从文件结构到底层原理,从命令行工具到Python读取,最后把点云话题数据提取出来。嗯,都是干货。

3.1 Bag文件结构——它到底长什么样?

ROS Bag本质上是一个二进制文件。但它不是乱糟糟的二进制流,而是有严格结构的。我当年第一次用十六进制编辑器打开Bag文件时,发现它其实分成了三个区域:

  • Header(文件头):记录版本号、索引位置、chunk数量等元信息
  • Chunks(数据块):真正存储消息的地方,每条消息都带时间戳
  • Index(索引区):记录每个话题在chunk中的位置,方便快速查找

说白了,Bag文件就是「索引 + 数据块」的组合。你想想看,如果没有索引,要找一个话题的数据就得从头扫到尾,那效率得多低?

核心概念:Bag文件中的每条消息都包含三个要素——话题名称、消息类型、时间戳。这三者缺一不可。

下面这张图,是我自己画的Bag文件结构示意,帮你快速建立直观印象:

ROS Bag 文件结构 Header(文件头) 版本号: 2.0 | Chunk数量: N | 索引偏移: 0x... Chunks(数据块区) Chunk 1: /points_raw (sensor_msgs/PointCloud2) 时间戳: 1645000000.123 ~ 1645000010.456 Chunk 2: /imu_data (sensor_msgs/Imu) 时间戳: 1645000000.789 ~ 1645000011.234 Chunk 3: /image_raw (sensor_msgs/Image) 时间戳: 1645000001.001 ~ 1645000012.567 Index(索引区) 话题索引表: /points_raw → Chunk 1, /imu_data → Chunk 2, /image_raw → Chunk 3

3.2 rosbag命令行工具——你每天都在用的瑞士军刀

ROS自带的rosbag命令行工具,说实话,功能非常强大。我平时80%的数据处理工作,靠它就能搞定。

3.2.1 常用命令一览

命令 作用 我常用的场景
rosbag info 查看Bag文件信息 拿到一个新bag,先info一下看看有哪些话题
rosbag play 回放Bag数据 测试算法时,模拟传感器数据流
rosbag record 录制话题数据 路测时采集数据,只录需要的几个话题
rosbag filter 按条件过滤 提取某个时间段的数据,或者只保留特定话题
rosbag compress 压缩Bag文件 大文件存档前压缩,能省60%空间

3.2.2 实战:用info命令查看Bag

# 查看bag文件基本信息
rosbag info 2024-03-15-14-30-00.bag

# 输出示例
path:        2024-03-15-14-30-00.bag
version:     2.0
duration:    180.2s
start:       Mar 15 2024 14:30:00.12
end:         Mar 15 2024 14:33:00.32
size:        2.4 GB
messages:    125000
compression: none [1/1 chunks]
types:       sensor_msgs/PointCloud2 [1158d4...]
             sensor_msgs/Imu           [6a62c6...]
topics:      /points_raw   10000 msgs : sensor_msgs/PointCloud2
             /imu_data     50000 msgs : sensor_msgs/Imu
             /vehicle_status 65000 msgs : std_msgs/String

我的小技巧:看到compression: none时,我一般会顺手压缩一下。用rosbag compress --lz4,压缩速度快,解压也快,适合日常使用。

3.2.3 实战:用filter提取点云话题

# 只保留点云话题,其他数据全部扔掉
rosbag filter input.bag output.bag "topic == '/points_raw'"

# 也可以按时间过滤:只取前60秒的数据
rosbag filter input.bag output.bag "t.to_sec() < 1645000060.0"

这里要注意,filter命令的表达式是Python语法。你可以在条件里写topic、t(时间戳)这些变量。我曾经踩过一个坑——时间戳比较时忘了转成秒,结果过滤出来的数据全是空的。

3.3 Python读取Bag文件——用代码掌控数据

命令行工具虽然方便,但要做精细化处理,还得上Python。ROS提供了rosbag Python包,接口设计得很简洁。

3.3.1 安装与导入

# 安装(如果你已经装了ROS,这个包默认就有)
pip install rosbag

# 导入
import rosbag
from sensor_msgs.msg import PointCloud2
import sensor_msgs.point_cloud2 as pc2

3.3.2 读取Bag文件的基本流程

bag = rosbag.Bag('2024-03-15-14-30-00.bag', 'r')

# 遍历所有消息
for topic, msg, t in bag.read_messages():
    print(f"话题: {topic}, 时间戳: {t.to_sec():.3f}")

bag.close()

这段代码很简单,但有个细节要注意——bag.read_messages()返回的是一个生成器,不会一次性把所有消息加载到内存。你想想看,一个2.4GB的bag,如果全读到内存里,机器不炸才怪。

3.3.3 只读取特定话题

# 只读取点云话题,效率高很多
bag = rosbag.Bag('2024-03-15-14-30-00.bag', 'r')

for topic, msg, t in bag.read_messages(topics=['/points_raw']):
    # 这里只处理点云数据
    print(f"收到一帧点云,时间: {t.to_sec():.3f}")

bag.close()

性能提示:指定topics参数后,rosbag内部会跳过不相关的话题数据,读取速度能提升5-10倍。我处理大文件时一定会加这个参数。

3.4 提取点云话题数据——从Bag到NumPy数组

好了,到了最核心的部分。我们要把Bag里的点云数据提取出来,转成NumPy数组,这样才能做后续处理。

3.4.1 PointCloud2消息结构

ROS里的点云消息类型是sensor_msgs/PointCloud2。它包含以下关键字段:

字段 类型 说明
header std_msgs/Header 包含时间戳、坐标系ID(如velodyne)
height uint32 点云高度(非结构化点云通常为1)
width uint32 点云宽度(即点的数量)
fields PointField[] 每个点的字段定义(x, y, z, intensity, ring等)
data uint8[] 原始二进制数据

3.4.2 完整提取代码

import rosbag
import sensor_msgs.point_cloud2 as pc2
import numpy as np

def extract_pointcloud_from_bag(bag_path, topic_name):
    """
    从bag文件中提取点云数据
    返回: 点云列表,每个元素是 (timestamp, points_np)
    """
    bag = rosbag.Bag(bag_path, 'r')
    cloud_data = []
    
    for topic, msg, t in bag.read_messages(topics=[topic_name]):
        # 将PointCloud2消息转为点列表
        points_list = list(pc2.read_points(
            msg, 
            field_names=('x', 'y', 'z', 'intensity'),
            skip_nans=True
        ))
        
        # 转为NumPy数组,形状为 (N, 4)
        points_np = np.array(points_list, dtype=np.float32)
        
        # 记录时间戳和点云
        cloud_data.append((t.to_sec(), points_np))
        
        # 打印一些信息
        if len(cloud_data) <= 3:
            print(f"第{len(cloud_data)}帧: {points_np.shape[0]}个点, "
                  f"时间戳: {t.to_sec():.3f}")
    
    bag.close()
    print(f"共提取 {len(cloud_data)} 帧点云")
    return cloud_data

# 使用示例
clouds = extract_pointcloud_from_bag(
    '2024-03-15-14-30-00.bag', 
    '/points_raw'
)

3.4.3 避坑指南

我曾经踩过的坑:

  • 内存爆炸:有一次我直接list(bag.read_messages()),结果一个8GB的bag把服务器内存撑爆了。记住,read_messages()是生成器,别转成list。
  • 字段名不匹配:不同激光雷达的字段名不一样。Velodyne用intensity,Ouster用signal,RoboSense用reflectivity。读取前先用msg.fields确认一下。
  • 时间戳精度:ROS的时间戳是秒+纳秒,转成浮点数时注意精度。我一般用t.to_sec(),够用了。

3.4.4 进阶:批量提取并保存为PCD文件

import open3d as o3d

def save_clouds_to_pcd(cloud_data, output_dir):
    """将提取的点云保存为PCD文件"""
    import os
    os.makedirs(output_dir, exist_ok=True)
    
    for i, (timestamp, points_np) in enumerate(cloud_data):
        # 只保留前100帧做测试
        if i >= 100:
            break
            
        # 创建Open3D点云对象
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(points_np[:, :3])
        
        # 如果有强度信息,可以赋颜色
        if points_np.shape[1] >= 4:
            intensity = points_np[:, 3]
            # 将强度归一化到0-1,映射为灰度颜色
            colors = np.zeros((len(intensity), 3))
            colors[:, 0] = intensity / 255.0
            colors[:, 1] = intensity / 255.0
            colors[:, 2] = intensity / 255.0
            pcd.colors = o3d.utility.Vector3dVector(colors)
        
        # 保存文件
        filename = f"{output_dir}/cloud_{timestamp:.3f}.pcd"
        o3d.io.write_point_cloud(filename, pcd)
        
        if i % 20 == 0:
            print(f"已保存 {i+1} 帧...")

# 执行保存
save_clouds_to_pcd(clouds, './extracted_clouds')

我的建议:实际项目中,我一般不会把所有帧都保存为PCD文件——太占磁盘了。通常的做法是:先提取关键帧(比如每10帧取1帧),或者只保存感兴趣时间段的点云。这样既节省空间,又不影响算法验证。

3.5 本章小结

这一讲我们完整走通了ROS Bag的解析流程。从文件结构到命令行工具,再到Python代码实现,最后把点云数据提取出来并保存为PCD文件。说白了,Bag文件就是一个带时间戳的数据容器,掌握了它的读写方法,你就能自由地处理自动驾驶数据了。

嗯,内容不少,但都是实战中一定会用到的。下次拿到一个新的Bag文件,你应该知道怎么下手了吧?