1. 数据爆炸是刚需:一次实测算出点云加图像直录有多夸张
先说个我自己踩过的坑。有次在园区里做环境采集,车上装了某款16线激光雷达和一台工业彩色相机,想着先把原始数据都录下来再说。跑了大概四十分钟,回来一看磁盘,好家伙,直接吃掉了快30GB。当时我就意识到,在Ubuntu 18.04这种常用机器人开发环境下,如果不把激光雷达点云和图像topic做压缩处理,录制bag数据包这件事根本没法长期干。
这个数字是怎么来的?我给大家算一笔账,看完你就明白为什么必须压缩。
- 某款16线激光雷达,单帧点云大约3万点,每个点用PointXYZI格式存储,X、Y、Z坐标各4字节float32,强度I又占4字节,合起来16字节。那么一帧就是30000 x 16 = 480KB。10Hz发布频率下每秒就是4.8MB,一分钟288MB,半小时就是8.6GB。
- 工业相机如果输出1280x720 RGB三通道原始图像,一帧是1280 x 720 x 3 = 2.76MB,按30fps算每秒就是82MB,一分钟将近5GB,半小时直接破140GB。当然很多相机实际传输出来的可能是YUV或已经做了硬件压缩,那也要20-40MB/s。
点云加图像,原始数据直录的情况下,一小时一两百GB都很正常。而车载工控机的存储通常也就512GB到1TB,不压缩的话,跑半天就满了。更要命的是写入带宽:普通机械硬盘连续写入速度在80-120MB/s左右,稍微老一点的甚至更低,一旦点云和图像并发涌进来,写盘根本跟不上,rosbag会疯狂丢帧,录出来的是残缺数据。
所以这篇文章要解决的就是三件事:怎么把图像topic压缩成JPEG再放进bag,怎么把点云topic在保证不丢帧的前提下高效入库,以及怎么用rosbag自带的压缩参数把整个bag体积再压一轮。我会把每一步具体操作、参数选择和背后原因都写清楚,适合那些正在搭数据采集栈、或者在Ubuntu 18.04上做机器人项目的开发者参考。
2. 开工前的环境检查:驱动、编解码插件与磁盘写入速度
很多人一上来就急着敲rosbag record,结果录到一半要么报错要么丢帧。压缩录制这件事,环境底子不打好,后面全是坑。我建议在动手之前花二十分钟把下面几项全部检查一遍。
2.1 ROS Melodic核心组件确认
Ubuntu 18.04对应的ROS版本是Melodic。检查是否装全,最简单的方法是:
rosversion -d正常会输出melodic。如果输出为空或者报错,说明ROS环境变量没加载,检查source /opt/ros/melodic/setup.bash是否已经写进了~/.bashrc。
接下来确认三个关键包是不是都在,缺一个后面都会卡壳:
rosdep check --from-paths . --ignore-src 2>/dev/null; dpkg -l | grep -E "ros-melodic-(image-transport|rosbag|pcl-ros)"我习惯直接看包是否安装:
dpkg -l | grep ros-melodic-image-transport dpkg -l | grep ros-melodic-rosbag dpkg -l | grep ros-melodic-pcl-rosimage_transport负责图像topic的压缩传输,rosbag是录制工具本体,pcl_ros是点云处理的基础库。这三个是这条链路的命根子。
2.2 image_transport 插件到底装了几个
这里有个容易忽略的点:image_transport本身只是个框架,真正干活的是它底下的插件。默认情况下,Ubuntu 18.04上安装ros-melodic-image-transport时,只会带上compressed_image_transport和theora_image_transport这两个插件。其中JPEG压缩走的是compressed_image_transport,Theora视频压缩走的是theora_image_transport。
确认插件是否可用,可以运行:
rosrun image_transport list_transports正常输出至少包含:
- "compressed": JPEG压缩传输
- "theora": 视频流式传输
- "raw": 原始未压缩传输
如果只有raw而看不到compressed,说明compressed_image_transport没装,补一下:
sudo apt install ros-melodic-compressed-image-transport2.3 磁盘写入速度实测:低于这个数就别录了
压缩录制的本质,是让CPU替磁盘扛压力。但就算压得再狠,最终数据还是要落盘。所以磁盘写入速度是硬门槛。
我一般用这个命令做粗测:
dd if=/dev/zero of=./test.bin bs=1M count=1024 conv=fdatasync这会在当前目录写一个1GB的文件并强制落盘,输出会显示实际写入速度。测完删掉:
rm test.bin参考标准:
| 存储介质 | 连续写入速度 | 是否适合录多传感器bag |
|---|---|---|
| 机械硬盘 | 80-120MB/s | 勉强,配合高压缩率可用 |
| SATA SSD | 300-500MB/s | 推荐 |
| NVMe SSD | 1000-3000MB/s | 非常宽裕 |
实测下来,如果连续写速低于150MB/s,录制时建议开启LZ4压缩(后面会详细讲),并且压低图像帧率或降低分辨率,否则大概率丢帧。另外,千万不要把bag写在/tmp下,很多发行版的/tmp是内存文件系统,重启就没了。
2.4 激光雷达驱动与图像驱动的topic名梳理
动手压缩之前,先摸清楚自己的topic叫什么是基本功。我通常的做法是:
rosbag record -O probe.bag --duration 5 -a录五秒所有topic,然后:
rosbag info probe.bag看里面的topic列表和每条topic的消息类型。这里有两个重点:第一,点云topic的类型是sensor_msgs/PointCloud2还是sensor_msgs/LaserScan,两者压缩思路完全不同;第二,图像topic是sensor_msgs/Image还是sensor_msgs/CompressedImage,如果是后者,说明驱动已经做了JPEG输出,就不用自己在应用层再压一遍了。
做完这些基础检查,才算真正具备压缩录制的环境条件。接下来我按图像和点云两条线分别说。
3. 图像topic压缩:image_transport 与 CompressedImage 的落地姿势
图像数据是bag体积的大头。在不改驱动源码的前提下,最优雅的方案是用image_transport的republish节点,把原始Imagetopic转成CompressedImagetopic,然后录制压缩后的topic。这样对原系统零侵入,录完的bag可以直接被后续节点订阅。
3.1 republish 最小可用命令
假设相机发布的是/camera/image_raw,类型是sensor_msgs/Image,执行:
rosrun image_transport republish raw in:=/camera/image_raw compressed out:=/camera/image_raw这条命令会做两件事:订阅/camera/image_raw,然后发布两个新topic——/camera/image_raw/compressed(JPEG压缩后的图像)和/camera/image_raw/theora(Theora视频流)。我们录制时用/camera/image_raw/compressed。
这里面的参数名很容易搞混,raw in:=表示输入的是原始图像topic,compressed out:=表示输出CompressedImage topic,需要remap的主题名字段必须完全一致,不能写错。
3.2 JPEG压缩质量怎么调
很多人在这一步直接用默认设置,其实JPEG压缩质量是可以调的。默认质量是80,对于视觉SLAM或者人工标注来说够用,但如果做精细的纹理恢复,或者目标检测任务对图像细节敏感,我建议把质量拉到95。方法是在compressed_image_transport的配置里指定:
rosrun image_transport republish raw in:=/camera/image_raw compressed out:=/camera/image_raw _compressed_jpeg_quality:=95缩略参数名是_compressed_jpeg_quality,注意这是私有参数,要放在节点名后面,用小写开头。这个参数直接决定bag里图像画质和体积的平衡点。
我的经验值:
| 场景 | JPEG质量 | 压缩后单帧大小(约) | 说明 |
|---|---|---|---|
| SLAM定位 | 75-80 | 100-150KB | 特征点提取足够 |
| 视觉标注 | 90-95 | 250-400KB | 人眼细节还原好 |
| 无人车数据闭环 | 95 | 300-500KB | 宁可大一点不能糊 |
每个项目要根据相机分辨率和场景纹理复杂度实测,通常测试一分钟,观察单帧大小波动,再决定质量值。
3.3 直接改驱动发布端:更省事的长久方案
如果这个相机要长期用于数据采集,我更推荐直接改驱动节点,在发布原始Image的同时,也发布一个CompressedImage。这样连republish节点都省了,录制时直接选压缩topic。伪代码思路是:
// 在已有的图像回调里,加一个压缩发布器 image_transport::Publisher compressed_pub; compressed_pub = it.advertise("/camera/image_raw/compressed", 1); sensor_msgs::CompressedImage compressed_msg; compressed_msg.header = raw_msg.header; compressed_msg.format = "jpeg"; cv::imencode(".jpg", cv_bridge::toCvCopy(raw_msg, "bgr8")->image, compressed_msg.data, {cv::IMWRITE_JPEG_QUALITY, 95}); compressed_pub.publish(compressed_msg);这里有个关键点:cv::imencode的format字段必须写成"jpeg",小写。如果你写"jpg",部分订阅方解析时会出问题,因为ROS约定CompressedImage.format的取值范围就是jpeg、png、tiff这些标准字符串。
实际项目里,我用过另一种方案:在采集节点里用OpenCV的VideoWriter直接编码成H.264视频流,再手动分包塞进ROS消息。这样压缩率更高,但实现复杂度直线上升,而且回放时还需要解码器。对比下来,JPEG压缩是最平衡的:压缩率高、解码快、生态完善、下游工具全都认。
3.4 为什么一路用JPEG而不是PNG
对图像压缩,大家最常见的疑问是:PNG不是无损吗,为什么不用PNG?
简单回答:PNG的压缩率远低于JPEG。同样一张1280x720的图,PNG压缩后往往还有1-1.5MB,JPEG同画质下只要300KB以内。激光雷达点云本身就已经是稠密数据,再叠加PNG的存储开销,bag体积立刻失控。除非是做工业质检那种必须无损的场景,否则采集用的bag里一律JPEG。
4. 点云topic压缩:实时转PCL和二值编码都不如直接压bag香
图像这边搞定之后,轮到点云。点云数据比图像难搞,因为它不是规则网格,没法直接用JPEG这类变换编码。有朋友上来就尝试自己写Zlib压缩点云,我劝你先别折腾,点云最好的归宿就是原样塞进bag,然后用rosbag的底层压缩解决体积问题。
4.1 为什么点云不当场降采样
很多人的第一反应是:点云那么大,我先VoxelGrid降采样一遍再录,不就行了吗?
这个思路本身没错,但有个致命伤:降采样之后原始信息就永久丢了。比如你的三维重建算法后期想提细纹理,或者做高精度标定,原始密度根本不够。我自己的原则是:原始点云必须完整保留,如果录制时担心体积,应该交给bag压缩,而不是在源头上砍数据。
这句话多说一遍:数据采集阶段宁可多存,不要少存。降采样是后处理阶段的事,不是采集阶段的事。
4.2 pcl_ros能做的那点事:近似压缩是给在线传输用的
pcl_ros里有个话题叫compressed_pointcloud2,听起来很对口,但实际是PCLPointCloud2的编码压缩,主要用于在线传输场景,不是给bag存储设计的最佳选择。而且它依赖pcl_ros的插件机制,录出来的topic很多人不知道怎么解。
我测试过一轮,结论是:不管是sensor_msgs/PointCloud2直录,还是压缩后录,最终的bag体积差距并没有想象中大。因为点云的数值分布太分散,通用压缩算法能压掉的空间有限。
真正让点云bag体积小的,是把坐标从float64转成float32(如果你的雷达输出不是float32的话),以及雷达本身如果提供了range和intensity分离字段,可以只存必要字段。这些在驱动层优化,比后处理压缩更有效。
4.3 点云topic里字段裁剪的实操
PointCloud2在ROS里是高度灵活的:一帧点云可以带X/Y/Z,也可以带颜色、强度、环号、时间戳等额外字段。每个字段都在加大单帧体积。
查看当前点云字段:
rosrun pcl_ros pointcloud_to_pcd /lidar_points /tmp/test.pcd然后写个简单的过滤节点,保留x、y、z、intensity四个字段,其他如ring、time按需保留。如果你确定后续算法不用环号,完全可以砍掉。字段数量从6个降到4个,单帧体积直接减少三分之一,这个优化一点不费力。
这里给一个简化的字段过滤脚本片段:
// 伪代码:从PointCloud2中提取指定字段 sensor_msgs::PointCloud2 filtered; sensor_msgs::PointCloud2Modifier modifier(filtered); modifier.setPointCloud2FieldsByString(2, "xyz", "intensity"); copyPointCloud2Fields(original, filtered, {"x", "y", "z", "intensity"});实际操作中用pcl::fromROSMsg转成pcl::PointCloud<pcl::PointXYZI>再序列化回去,更直观:
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZI>()); pcl::fromROSMsg(input_msg, *cloud); // cloud里只有XYZI,其余字段自然被丢弃 sensor_msgs::PointCloud2 output_msg; pcl::toROSMsg(*cloud, output_msg); output_msg.header = input_msg.header;注意:pcl::PointXYZI会把强度值自动归一化到[0,1]区间,如果你后续要用原始强度值做反射率图,这里要小心,最好保留uint16类型的原始强度字段而不是用PointXYZI。
5. rosbag record 压缩参数实战:LZ4、chunksize 与分片策略
图像压缩成JPEG、点云字段精简完,接下来就要让rosbag record真正管好落盘体积了。这节的每一个参数我都实战验证过,直接照着抄就行。
5.1 选择LZ4还是BZ2:实时录制永远是LZ4
rosbag record支持两种压缩算法:bz2和lz4。用法是在命令后面加--compression参数:
# LZ4压缩(快速,CPU开销低) rosbag record -O lidar_cam.bag --lz4 /lidar_points /camera/image_raw/compressed # BZ2压缩(高压缩率,CPU开销高) rosbag record -O lidar_cam.bag --bz2 /lidar_points /camera/image_raw/compressed团力对比我对同一段数据做过实测:
| 压缩算法 | 压缩后体积比例 | CPU占用 | 录制是否丢帧 |
|---|---|---|---|
| 无压缩 | 100% | 极低 | 高带宽时丢帧 |
| BZ2 | 约35%-45% | 高,可能打满两个核 | 点云图像并发时容易丢帧 |
| LZ4 | 约55%-70% | 低,单核轻负载 | 稳定,几乎不丢帧 |
BZ2的压缩率比LZ4好看一些,但代价是CPU飙高。实时录制场景,尤其是激光雷达和相机并发时,丢帧的损失远大于省下来的那10%体积。所以只要你不是离线批量压缩,一律用--lz4。
5.2 chunksize 和 buffsize:两个容易被忽视的保帧参数
即使选了LZ4,如果参数没调对,大点云帧进来照样可能丢。这里两个参数非常关键:
--chunksize:bag内部每个数据块的大小阈值,默认768KB。对点云来说,单帧经常超过1MB,如果把chunksize调大,每个chunk能容纳更多完整帧,压缩效率更高,也减少I/O次数。--buffsize:rosbag的接收缓冲区大小,默认8192KB也就是8MB。点云加图像并发时,8MB很容易溢出导致丢message。
我实际使用的命令:
rosbag record -O lidar_cam.bag --lz4 \ --buffsize 256 \ --chunksize 2048 \ /lidar_points \ /camera/image_raw/compressed--buffsize单位是KB,256就是256MB,给足缓冲。--chunksize单位也是KB,2048就是2MB,正好装点云一帧或压缩图像若干帧。
5.3 topic过滤与分片录制:长时间采集的正确姿势
长时间采集一定要按时间或者按大小自动分片,否则bag文件越来越大,后续处理麻烦,而且单个文件损坏就全废了。分片参数:
# 按文件大小分片,每片2GB rosbag record -O lidar_cam.bag --lz4 --split --size 2048 /lidar_points /camera/image_raw/compressed # 按时间分片,每5分钟一片 rosbag record -O lidar_cam.bag --lz4 --split --duration 300 /lidar_points /camera/image_raw/compressed注意:--size单位是MB,--duration单位是秒。用了--split之后,-O给出的文件名是前缀,实际生成的文件会带序号后缀,比如lidar_cam_0.bag、lidar_cam_1.bag。
我在项目里最常用的组合是按时间分片,因为后续做数据回放和数据筛选时,按时间找数据比按大小找数据直觉得多。
5.4 多topic录制时别忘了__name重名冲突
一个特别隐蔽的坑:如果你同时录制多个image_transport输出的耗时topic,比如/camera/image_raw/compressed和/camera/image_raw/theora,rosbag record会成功,但回放时可能会因为节点名冲突出错。解决办法是给录制过程重映射节点名:
rosbag record -O lidar_cam.bag --lz4 \ __name:=my_bag_recorder \ /lidar_points \ /camera/image_raw/compressed这个__name:=是ROS节点级重映射,不指定的话节点名默认是/record,多个实例互相抢占。
6. 录完之后的验证与回放:确认压缩没有弄丢关键帧
bag录完了,不代表就万事大吉。压缩录制过程中最常见的问题不是压缩本身,而是丢帧。下面是我每次录完必做的三步验证。
6.1 rosbag info 检查topic完整度
先看基本信息:
rosbag info lidar_cam.bag关注几项:
duration:实际录制时长,和预计时长是否一致messages:各topic的消息数量compression:显示lz4表示压缩生效size:文件物理大小
各topic消息数量对比发布的频率:点云10Hz跑10分钟理论应该有6000帧,图像30fps跑10分钟理论18000帧,误差超过1%就要回头查原因。
6.2 逐帧回放,确认时间戳单调性
压缩和解压过程可能会引入时间戳抖动,回放时最好写个简单节点确认:
rosbag play lidar_cam.bag --rate 1.0另开终端:
rostopic echo -n 100 /lidar_points/header/stamp看时间戳是否单调递增、间隔是否大致均匀。如果出现明显跳变,说明录制时存在缓存区溢出,换用更大的--buffsize重新录。
6.3 图像topic的CompressedImage解码验证
压缩后的图像topic类型变了,回放给下游节点前要验证能不能正常解码。方法:
rosrun image_transport republish compressed in:=/camera/image_raw compressed out:=/camera/image_raw然后VSCode里用ROS插件订阅compressed源,或者用rqt_image_view直接看:
rosrun rqt_image_view rqt_image_view /camera/image_raw/compressed如果画面清晰、没有大量花屏,说明JPEG编码链路正常。花屏通常是压缩参数设置不当或者传输丢包导致的,需要检查_compressed_jpeg_quality和缓冲区配置。
6.4 如果非要解压成原始bag怎么操作
大多数情况,LZ4压缩的bag可以直接被rosbag play播放,不需要解压。但如果下游工具太老不认压缩格式,那就离线解压:
rosbag decompress lidar_cam.bag解压后会在同目录生成lidar_cam.bag的普通版本(原文件自动改名备份)。不过解压只是让bag内chunk回到未压缩状态,已经变成CompressedImage类型的图像消息并不会变回原始Image——这个话题类型转换要在驱动层面做,解压操作不会逆转。
6.5 数据回放时的CPU考量
LZ4压缩数据在回放时的CPU开销也要心里有数。我自己测过,32线程的桌面级处理器,同时回放一路点云加一路JPEG图像,CPU占用大约在15%-25%之间。如果是工控机,建议回放时用--rate 0.5降速,避免下游节点吃不到完整数据。
最后再分享一个自己的习惯:每次长时间录制,我都会先在现场录30秒测试bag,当场跑一遍rosbag info和回放验证,确认topic名、类型、压缩率都符合预期,再正式开录。这个习惯帮我省掉了好几次全量采集后发现参数配错的返工开销。压缩录制这套链路,看起来参数多,其实核心就一句话——图像走CompressedImage、点云精简字段、bag整体上LZ4。把这三层都做到位,一小时采集的数据量基本能控制在10GB以内,磁盘和CPU都稳稳的。