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

资讯详情

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

ROS2中PointCloud2点云格式深度解析:从字段结构到GO2实机保存与转换

ROS2中PointCloud2点云格式深度解析:从字段结构到GO2实机保存与转换

做过机器人SLAM的朋友应该都有这种体会:跑通一套建图算法不难,真正麻烦的是搞清楚数据从哪儿来、长什么样、该怎么处理。我前阵子拿到一台宇树机器狗GO2,打算在上面做一套完整的SLAM建图方案,第一个要啃的骨头就是点云格式。这期先不讲算法怎么调参,也不讲里程计怎么配,单纯把点云这件事说透——因为不管你后面上fast-lio、LIO-SAM还是别的方案,点云数据本身搞不明白,后面全是坑。

GO2出厂自带一个Livox MID-360雷达,配合机载主控跑的是ROS2。虽然官方默认有一套建图Demo能直接跑,但你要真想拿它做自己的东西,比如接入自研算法、改话题类型、自己保存数据集,就必须从点云消息结构开始理解。这篇我会把PointCloud2消息字段、坐标系关系、实操中如何查看和保存点云,以及Livox这类非重复扫描雷达点云的独特性质一次讲清楚,顺便把我在GO2上踩过的几个典型坑也整理出来。

这期内容适合手上有GO2、准备深入研究SLAM的开发者,也适合那些在模拟器和普通ROS2机器人上跑过SLAM、但没碰过固态激光雷达的朋友。就算你只是对点云数据结构好奇,看完也能搞清楚点云到底是个什么东西。

1. 为什么做SLAM之前,必须先把点云格式吃透

先说个我自己的感受:很多新手拿到GO2,第一反应是直接跑官方Demo,看到RViz2里绿油油的点云地图刷出来,觉得挺酷,然后就不知道怎么往下走了。等到想换算法、想改配置、想把点云数据存下来离线调试时,才发现自己对数据本身一无所知——话题名字不清楚、消息类型对不上、坐标系关系一团浆糊,半天时间全耗在查报错上。

1.1 点云在SLAM链路里的位置

一台四足机器人做SLAM建图,数据的流转链路大致是这样的:雷达发射激光、接收回波,驱动节点把这些原始测量转换成点云消息,发布到ROS2话题上;SLAM算法订阅这个点云消息,结合IMU数据做帧间匹配和位姿估计,再把匹配好的点云投影到全局坐标系,不断拼接成地图。

整个过程里,点云是所有后续计算的“原材料”。你用什么算法、调什么参数,本质上都是在处理这些三维点。如果连原材料的格式都没搞清,比如不知道每个点除了xyz还有没有强度值、不知道点云坐标系是雷达系还是机体系,那后面调试算法时基本都是盲人摸象。

1.2 GO2这套硬件上的点云链路特殊在哪

GO2用的Livox MID-360不是传统机械式激光雷达,而是固态非重复扫描雷达。它的扫描模式决定了点云的分布规律跟Velodyne那类16线、32线雷达完全不同——这个后面我会单独展开讲。

另外GO2的软件层是基于ROS2 Humble的,点云消息走的是ROS2的sensor_msgs/msg/PointCloud2。虽然这套消息类型在ROS1里也有同名版本,但ROS2在DDS传输、QoS策略这些底层机制上跟ROS1差别很大。很多从ROS1转过来的人,会在这上面吃不少亏。

1.3 三种常见点云数据格式,别搞混

在实际工作中,点云数据至少有三个层面的存在形式:

格式说明使用场景
PointCloud2消息ROS和ROS2里的标准点云消息类型,用于节点间实时传输话题通信、算法处理
PCD文件Point Cloud Data,PCL库定义的磁盘存储格式保存点云数据、离线调试、数据集制作
LAS/LAZ文件行业标准点云格式,带分类、强度等丰富属性测绘、GIS、地形建模

这三者经常被混为一谈,但事实上它们的用途完全不同。你在GO2上通过ros2 topic echo看到的是一帧一帧的PointCloud2消息;你把点云用pointcloud_to_pcd节点保存下来,得到的是PCD文件;你要把地图导入到专业测绘软件里做后处理,可能需要转成LAS。搞清楚这三者的区别,能帮你避免很多“数据明明是好的,为什么用不了”的困惑。

2. PointCloud2消息解剖:字段、坐标、时间戳一个都不能少

PointCloud2是ROS2里点云数据的通用容器。看起来就是一堆字节流,但读明白它并不难,关键是要掌握几个核心概念:消息头、字段列表、数据布局。

2.1 先看字段定义

我直接用一个实际话题来说明。在GO2上,MID-360雷达的点云话题通常是/livox/lidar或者类似的名字,消息类型就是sensor_msgs/msg/PointCloud2。它的核心字段是这样的:

Header header uint32 seq # 序列号 time stamp # 时间戳 string frame_id # 坐标系标识 uint32 height # 点云高度(2D扫描时为1) uint32 width # 点云宽度(点数或每行点数) PointField[] fields # 每个点的字段定义 uint8 datatype # 数据类型(INT8/UINT8/INT16/UINT16/INT32/UINT32/FLOAT32/FLOAT64) string name # 字段名(x、y、z、intensity等) uint32 offset # 字段在点数据中的字节偏移 uint8 is_bigendian # 是否大端字节序 uint32 point_step # 单点占用的字节数 uint32 row_step # 一行数据的字节数 uint8[] data # 实际的点云数据 bool is_dense # 是否包含无效点(NaN)

这里最重要的就是fields数组。以MID-360为例,它发布的点云通常包含以下字段:

字段名含义数据类型常见偏移
xX轴坐标(米)FLOAT320
yY轴坐标(米)FLOAT324
zZ轴坐标(米)FLOAT328
intensity反射强度FLOAT3212
tag属性标签UINT816
offset_time相对时间偏移FLOAT3217

注意,不同驱动版本、不同雷达型号,字段组成可能不一样。有些雷达会加一个ring字段表示线束编号,有些会有timestamp字段。所以看一个PointCloud2消息,第一件事不是直接取数,而是看它的fields里到底有什么。

2.2 理解数据的线性布局

PointCloud2的消息体是一个扁平字节数组,看起来唬人,其实规矩很简单:一帧点云展开后,前point_step个字节是第一个点的数据,接下来的point_step个字节是第二个点,以此类推。一个点的数据内部,不同字段按offset指定的偏移量排列。

举个例子,如果point_step是20字节,x字段的偏移是0、y偏移是4、z偏移是8,那么第10个点的x坐标就位于data[9 * 20 + 0]到data[9 * 20 + 3]这4个字节里。用Python的struct模块解析时,直接按这个规律读就好。

我在实际处理时更推荐用现成库。ROS2 Python客户端里,可以这样取点:

from sensor_msgs.msg import PointCloud2 from sensor_msgs_py import point_cloud2 as pc2 # 假设拿到了msg points = pc2.read_points(msg, field_names=["x", "y", "z", "intensity"], skip_nans=True) for p in points: print(p) # (x, y, z, intensity)

C++侧则直接用PCL的fromROSMsg接口,一下就能转成pcl::PointCloud<pcl::PointXYZI>。这也是大家平时最常用的路子,毕竟谁也不想手撸字节解析。

2.3 坐标系:搞清楚frame_id是雷达系还是机体系

点云消息里的frame_id信息很关键,它告诉你当前这帧点云的参照坐标系是什么。在GO2上,如果frame_id是livox_frame,说明点云是相对于雷达本体坐标系的;如果经过TF变换后frame_id变成了base_link,那就是相对于机体中心坐标系的。

这个区别在SLAM里非常重要。fast-lio这类算法做状态估计时,通常把机体坐标系作为核心参照,雷达相对机体的安装位置和姿态,是通过外参标定得到的。如果你拿到一帧点云后,不去看frame_id就直接当作机体系去用,那姿态和位置全都会偏,建出来的图也会扭曲。

我之前调试时遇到过一次“地图像喝醉了酒一样歪斜”的情况,排查到晚上才发现是直接把livox_frame的点云输给了算法,安装外参被重复施加了一次,相当于把同一个变换做了两遍。后来我习惯性地在每一条链路前面都打印一次frame_id,这坑才被彻底绕开。

2.4 时间戳与你该关心的“时间同步”

PointCloud2的Header里带一个stamp,表示雷达采集这帧数据的时刻。在ROS2里,时间戳用于消息过滤、时间同步和TF查询,不是可有可无的东西。

GO2上跑多传感器融合时,时间戳尤其重要。激光雷达和IMU都有各自的时钟源,如果不同传感器的时间戳不同步,融合算法就会算出完全错误的位姿。一般有两种做法:一是用硬件时间同步,让雷达和IMU共用同一套时钟基准;二是在软件层把不同传感器的时间戳通过滤波器对齐。GO2的驱动层本身已经做了很多时间同步的工作,但你自己写算法时,仍然要检查时间戳的精度是否满足需求。

另外注意一个小细节:ROS2的时间戳用的是纳秒,ROS1用的是秒加纳秒。做数据转换时,例如把旧ROS1的数据包转过来用,很容易在时间上直接乘个1000之类导致精度丢失,这种错误非常隐蔽。

3. 实操:在GO2上看点云、存点云、转格式

理论聊得差不多了,接下来上真机操作。我下面这套操作流程都是我在GO2上实际跑过的,每一步都可以直接复现。

3.1 先看点云长什么样

启动GO2的雷达驱动,然后打开终端查看话题列表:

ros2 topic list

输出里能看到类似下面的结果:

/livox/lidar /livox/imu /tf /tf_static /odom

/livox/lidar就是我们要关注的点云话题。查看它的消息类型和频率:

ros2 topic info /livox/lidar ros2 topic hz /livox/lidar

topic info输出的Type应该就是sensor_msgs/msg/PointCloud2。topic hz会显示发布频率,GO2的MID-360通常以10Hz发布点云,不同固件版本可能有差异。

要偷看消息内容,用topic echo是最直接的:

ros2 topic echo /livox/lidar --once

终端会刷出一大段JSON格式的消息,我建议别盯着data字段看,先看fields和point_step,这两个字段能直观反映数据结构。如果只输出一部分就截断了,可以加--fields限定字段:

ros2 topic echo /livax/lidar --once --fields header.stamp,header.frame_id,width,height,point_step

这样能看到更精简的信息,比如时间戳、frame_id、点数和单点步长。

3.2 RViz2可视化点云

实时看点云,最方便的还是RViz2。启动方式:

rviz2

在RViz2里做三件事:

  1. 把Fixed Frame改成livox_frame或base_link,取决于TF树里你用哪个坐标系做参照;
  2. 添加一个PointCloud2显示组件,在Topic一栏填入/livox/lidar;
  3. 把Size(Pixels)调成2左右,ColorTransformer可以选Intensity,这样点云会按反射强度着色,看起来比单一颜色清楚很多。

如果RViz2里看不到点云,先别急着怀疑雷达坏了,八成是Fixed Frame和消息里的frame_id对不上,或者TF树没连上。确认一下TF树的状态:

ros2 run tf2_tools view_frames

这个命令会在当前目录生成一个frames.pdf,打开就能看到坐标系之间的关系。在GO2上,一般会有livox_frame→base_link→ ... 这样的链路,如果链路断了,点云就无法被正确显示在全局坐标系下。

3.3 保存点云为PCD文件

离线调试是SLAM开发里省不了的一步。把实时点云存成本地文件,就可以反复回放,不用每次都在真机上折腾。保存PCD最简单的方式是用pcl_ros的pointcloud_to_pcd节点:

ros2 run pcl_ros pointcloud_to_pcd --ros-args -r input:=/livox/lidar -p prefix:=./bag_pcd

执行之后,它会按时间戳连续保存PCD文件,文件名为prefix + time_stamp.pcd。保存时目录要存在,否则会静默失败或者报错,这点我踩过:第一次跑的时候prefix写了一个不存在的路径,命令看起来在运行,实际一个文件都没存下来。

如果你想自己控制保存逻辑,比如过滤掉无效点再存,或者只存每隔多少帧存一次,可以直接写一个简单的ROS2 Python节点,订阅PointCloud2,转换成numpy数组再保存。下面是个精简版示例:

import numpy as np import rclpy from rclpy.node import Node from sensor_msgs.msg import PointCloud2 from sensor_msgs_py import point_cloud2 as pc2 class PcdSaver(Node): def __init__(self): super().__init__('pcd_saver') self.sub = self.create_subscription( PointCloud2, '/livox/lidar', self.callback, 10) self.count = 0 def callback(self, msg): if self.count % 10 != 0: self.count += 1 return self.count += 1 gen = pc2.read_points(msg, field_names=["x", "y", "z", "intensity"], skip_nans=True) points = np.array(list(gen), dtype=np.float32) if len(points) == 0: return self.save_pcd(f"frame_{self.count:06d}.pcd", points) def save_pcd(self, 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(f"WIDTH {len(points)}\n") f.write("HEIGHT 1\n") f.write("VIEWPOINT 0 0 0 1 0 0 0\n") f.write(f"POINTS {len(points)}\n") f.write("DATA binary\n") f.write(points.tobytes())

这个节点每10帧存一帧,存下来的文件可以用CloudCompare打开查看。需要注意PCD的binary存储格式是严格按字段顺序和大小排列的,写文件时的SIZE、TYPE、COUNT必须和实际字节完全一致,耗时点在于空格和换行都不能错,否则PCL读不出来。

3.4 点云格式转换:PCD转PLY、LAS

从算法跑出来的地图,如果要给别人看或者做后续处理,经常需要转成通用格式。CloudCompare是最省事的工具,图形界面里直接File → Open打开PCD,再Save As选目标格式就行。命令行也可以批量处理:

CloudCompare -SILENT -O frame_000010.pcd -SAVE_CLOUDS FILE frame_000010.ply

如果你对规模有要求,比如地图点云几十GB,需要转成LAS格式给GIS软件用,可以考虑用PDAL库做批处理,速度快而且可以同时做抽稀、裁剪。PDAL安装好之后,一条命令:

pdal translate input.pcd output.laz

PDAL还内置了filters.voxeldownsize、filters.outlier这类过滤器,可以顺便做预处理。另外,从PointCloud2直接转LAS也不是不行,但一般路径还是“先转PCD,再转LAS”更顺,因为PCD已经被PCL生态验证得很成熟了。

4. Livox点云的特殊性:它跟传统雷达不是一回事

如果你之前用过机械式雷达,拿到MID-360的点云后会有种强烈的“不适感”。这很正常,因为Livox的固态扫描方案跟传统旋转式雷达有本质区别。这一节讲清楚这些区别,后面做建图时会少走很大弯路。

4.1 非重复扫描:为什么点云像“毛玻璃”

传统机械雷达靠电机旋转带动激光头,扫描线轨迹是固定的重复圆环,所以每一帧点云都有明确的“线”结构。而MID-360的扫描是花瓣形的非重复轨迹,每一帧的采样点位置都在变化,多帧叠加之后视场内的点会越积越密。

这个特性带来的直接好处是:时间越长,同一区域的点云覆盖率越高,对小物体的探测能力越强。但对算法来说,它意味着“单帧点云”里没有明确的线束编号概念,传统基于线束特征做特征提取的算法(比如很多基于Loam的方案)需要做适配。

这也是为什么fast-lio这类直接对原始点云做匹配的算法,能在Livox雷达上有天然优势——它们不做线束假设,而是直接处理点云集合。所以网上说到“mid360使用fast-lio建图”很流行,这背后是有硬件逻辑支撑的。

4.2 环状伪影和运动畸变

Livox点云另一个特点是容易在扫描边缘出现环状伪影(比如墙角处点云“拉环”),这是因为激光打在锐利边缘时,测距值在真值和虚假回波之间抖动,产生一串无规律的点。另外,如果雷达载体本身在运动,一帧点云内部的点并不是同一时刻采样的,会带运动畸变,也就是旋转和位移导致的点位置错位。

这两个问题在建图时都会直接影响精度。环状伪影一般通过距离滤波和角度变化率滤除;运动畸变则靠SLAM算法的去畸变处理——fast-lio就是利用IMU和运动模型做逐点去畸变的,所以它在GO2这种动态载体上表现不错。

日常调试时你可以做个简单实验:让GO2原地静止,在RViz2里观察一帧单帧点云,边缘的环状伪影比较少;然后让机器狗原地转圈,再观察单帧点云,会发现边缘伪影明显增多。这就是运动畸变和扫描边缘抖动叠加的效果,理解了这个过程,你就知道为什么各算法都在“去畸变”上花那么多精力。

4.3 体素降采样不是可选项,是必选项

MID-360每秒能产生20万左右个点,这个话题听起来“也就那样”,但如果连续跑几分钟建图,地图点云动辄几千万甚至上亿个点。你不可能拿这些原始点直接去做配准、回环检测和全局优化,必须降采样。

体素降采样(Voxel Grid Downsample)的原理很简单:把三维空间划分成固定尺寸的小立方体(体素),每个体素内部只保留一个代表点,这个点通常是体素内所有点的重心。体素尺寸越大,点数越少,细节损失也越大。

在GO2上做中近距离的室内建图,我常用的体素边长是0.1米到0.2米;做室外大场景,0.3米左右就够了。不要小看这个参数,它直接影响建图速度和地图精细度。设太小,帧率掉得厉害;设太大,地图会显得“糊”。你可以在RViz2里同时打开原始点云和降采样后的点云做对比,找到感觉。

4.4 LIVOX驱动和ROS2的QoS是个大坑

聊点实操,还没真机跑过的人基本不知道:Livox点云话题在ROS2里默认的QoS策略,跟常见雷达有差异。有些用户习惯用默认的QoS参数订阅话题,结果发现一直收不到数据。

这类问题典型报错是“waiting for messages”。解决方案是在自己的节点里显式指定与发布端兼容的QoS,比如rmw_qos_profile_sensor_data或对应的rclpy.qos.QoSProfile(depth=10, reliability=QoSReliabilityPolicy.BEST_EFFORT)。因为点云和IMU这类传感器数据通常用BEST_EFFORT、容忍丢帧,而不是RELIABLE传输。

这个话题在ROS2里讨论得很频繁,你只要记住一点:在写自己的订阅节点之前,先用ros2 topic info /livox/lidar --verbose查看发布端的QoS参数,然后照着设置,基本就能避开这个坑。

5. 常见问题与排查技巧实录

下面这些是我实际调试GO2点云时遇到的问题汇总。很多问题看起来五花八门,根因其实就那么几个:坐标系、字段、时间戳、QoS。附上一个速查表,方便你对照排查。

现象可能原因排查方法
RViz2里看不到点云Fixed Frame与frame_id不一致查看TF树,设置正确的Fixed Frame
点云颜色全是纯色ColorTransformer设成FlatColor改为Intensity或RGB
点云在RViz2里乱飞、漂移时间戳不同步,或坐标系跳变查看stamp是否连续,检查TF
自己写的节点订阅不到点云QoS策略不兼容查看发布端QoS,用BEST_EFFORT订阅
保存的PCD用PCL打不开PCD头信息写错,或二进制字节不对核对FIELDS、SIZE、TYPE、COUNT
点云边缘出现大量远距离杂点雷达边缘扫描抖动增加距离滤波、角度滤波
建图时地图发生扭曲外参标定不准确重新标定雷达-IMU外参
内存占用不断上涨没有做降采样或没有做点云释放引入体素降采样,及时清理历史帧

5.1 帧率正常但点云稀疏怎么办

这个现象一般出现在暗光环境或远距离场景,也就是雷达回波微弱导致有效点减少。先确认话题频率没有明显下降(ros2 topic hz),如果频率正常,那就是点密度确实低,可以做多帧叠加或调大雷达的功率档位。另外检查一下is_dense字段,如果为False,说明消息里有NaN点,在读取时用skip_nans=True过滤就好。

5.2 点云部分区域“撕裂”

“撕裂”很多时候是在启动阶段的雷达运动畸变导致的。车/机器狗启动加速时,单帧点云的起点和终点位置差距大,如果不做点云去畸变,拼接时就会在边缘出现撕裂。解决办法是确保SLAM算法配好了IMU,并且在启动阶段动作慢一点,等算法完成初始化就正常了。

5.3 怎么确认自己存下来的点云跟原始一致

离线数据调试时,数据一致性很重要。我会用一个最简单的自查方法:把保存的PCD文件拖进CloudCompare,看整体形状跟RViz2里是否一致;再对比点数和边界范围。如果点数对不上,大概率是保存时没过滤NaN,或者字段读错了。边界范围可以用CloudCompare的Bounding Box功能看,设置成跟RViz2里的坐标范围对比。

6. 给GO2上做点云处理的一些小建议

写到最后,把实际经验里觉得最值得跟新手分享的点列一下。

一是在动手写算法前,先把数据可视化这件事做到“顺手”。RViz2 + CloudCompare这两个工具用熟了,后面调试效率至少翻一倍。

二是养成看话题元数据的习惯。每拿到一个新的点云话题,顺手ros2 topic info+ros2 topic echo --once,很多问题就不会出现。

三是不要盲目追求点云“越多越好”。对SLAM来说,合适的密度才是关键,我用的是“地图点数不爆炸、特征不过滤干净”的标准来选降采样体素,这个标准你在实施时会有自己的体会。

四是保存数据集时把条件记清楚。哪台机器、什么雷达、什么高度、什么速度、光照条件如何,这些看似不经意的信息,之后回放数据时就是救命稻草。

我自己做下来最大的感受是:点云格式这一关,是后面所有SLAM工作的地基。地基不牢,后面每一步都可能出问题。下一篇我会接着讲GO2上的IMU数据与标定,以及它和点云之间如何配合,有兴趣的朋友可以持续关注。如果你在点云这一步遇到了别的奇怪问题,也欢迎在评论区把现象和报错贴出来,大家一起分析。

返回列表