ARTICLE DETAIL

资讯详情

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

Livox Mid-360点云解析:CustomMsg转换PointCloud2实战指南

Livox Mid-360点云解析:CustomMsg转换PointCloud2实战指南 第一次把 Livox-Mid-360 的包录下来我习惯性地用 ros2 bag info 看了一眼话题列表心里就咯噔一下主话题/livox/lidar的类型不是常见的sensor_msgs/PointCloud2而是一个叫livox_interfaces/msg/CustomMsg的自定义消息。点开看x、y、z、reflectivity 这些字段明明都有可就是没有现成的 intensity 和统一时间戳时间信息被塞进了timebase和offset_time这两个字段里。第一次接触激光雷达点云处理的人十有八九会卡在这里。这篇文章我会从 Livox-Mid-360 的自定义点云格式讲起把消息结构、从 bag 里提取数据、按 tag 过滤、时间戳处理、IP 配置以及对接 Cartographer、CloudCompare 这些常用工具时容易踩的坑完整过一遍。内容针对的是正在做激光雷达 SLAM、目标检测前处理或者只是想把点云从包里挖出来做可视化验证的人。我尽量把每一步都写到能直接复制去用的程度。1. 为什么 Mid-360 的点云不能按普通 PointCloud2 直接读1.1 非重复扫描与私有消息类型的来龙去脉Livox-Mid-360 不是传统的机械旋转激光雷达它用的是非重复扫描技术。内部激光束通过棱镜组带动扫描轨迹是类似花瓣状的花纹而不是一圈一圈稳定的水平线。机械雷达一帧点云里每个点是哪条线束扫出来的角度关系非常固定Mid-360 这种扫描方式下点云空间分布天生就是不均匀的不同时刻同一个位置的覆盖密度差异很大。由于扫描模式完全不同Livox 不能直接把点云硬塞进标准的 PointCloud2 消息里。它自己定义了一套消息把硬件能拿到的所有信息都保留下来每个点相对帧起点的时间偏移、反射率、质量标签、内部发射器编号。这些信息对后续的去畸变、噪声过滤和 SLAM 前端都有用但如果直接用通用点云处理库去读你会发现自己死活找不到强度字段。1.2 和机械雷达点云数据组织方式的差异用 Velodyne 这类机械雷达时驱动发布的是标准的sensor_msgs/PointCloud2每一帧点云的 ring 字段直接标出线束编号intensity 字段就是反射强度。各种现成工具链、PCL 过滤器、Cartographer 的输入接口都可以原封不动地对接。Mid-360 的 CustomMsg 则是一套私有协议。我整理了一个对比表格方便你快速定位差异对比维度机械旋转雷达Velodyne 等Livox Mid-360 CustomMsg消息类型sensor_msgs/PointCloud2livox_interfaces/msg/CustomMsg点的时间信息帧内统一用 header.stamptimebase offset_time组合反射值字段intensityreflectivity点质量标记无标准字段tag0 正常1 无效2 超视距3 噪声线束/发射器ring/firing 编号line 字段是否可被通用工具直接消费是否需要转换或定制解析一旦了解到这个差异接下来的思路就清楚了先把 CustomMsg 的每个字段吃透再决定是转换还是直接消费。2. CustomMsg 格式逐字段拆解2.1 CustomMsg 与 CustomPoint 的消息结构拿到包以后我建议你先用ros2 interface show livox_interfaces/msg/CustomMsg看一份当前版本驱动里的字段定义。不同版本的驱动消息字段基本一致下面这个结构可以作为一个通用的理解框架# livox_interfaces/msg/CustomMsg std_msgs/Header header # frame_id 一般是 livox_frame uint64 timebase # 本帧内部时间基准纳秒 uint32 point_num # 本帧点数 uint8 lidar_id # 雷达设备 ID CustomPoint[] points # 变长点数组 # livox_interfaces/msg/CustomPoint uint32 offset_time # 相对 timebase 的时间偏移 float32 x # 前向单位米 float32 y # 左向单位米 float32 z # 上向单位米 uint8 reflectivity # 反射率0~255 uint8 tag # 点质量标签 uint8 line # 内部激光发射器编号注意这个header.stamp和timebase的区别。timebase是雷达数据帧内部的时间基准属于设备时间轴header.stamp是数据包被驱动接收并发布时的系统时间。每个点的精确采集时间逻辑上应该是timebase offset_time。实际用的时候如果不做传感器之间的时间同步很多人拿header.stamp offset_time来近似这在大多数低动态场景下问题不大但严格来说设备时间和系统时间之间是存在偏移的。2.2 offset_time 到底当纳秒还是微秒看这是最容易翻车的地方。在 ROS1 时代的旧版 livox_ros_driver 中offset_time的单位曾经是微秒而 livox_ros_driver2 里我实测下来是纳秒。也许你会看到网上老帖子写微秒拿过来直接除以 1e6 去跟真值时间对结果差了三个数量级怎么都对不上。我的建议是不要靠记忆动手打印验证。找一帧数据把相邻两个点的offset_time相减再对比相邻点实际距离变化。如果时间差单位是纳秒正常点云相邻点的时间差通常在几百到几千微秒量级也就是几十万到几百万纳秒。如果你算出的是几百那基本可以判断它实际是纳秒。遇到问题先打印永远比想象靠谱。2.3 reflectivity、tag、line 字段的真实含义reflectivity是反射率编码值范围 0 到 255。它不是光强除以最大值的简单比例而是硬件内部根据回波信号强度、距离等参数计算出来的一个相对反射率值。实际观察里白色墙面、反光路牌会偏高黑色轮胎、深色衣物会偏低。做目标检测前处理时可以利用这个值快速剔除低反射率的地面和噪声点也可以用来提取高反射特征物。tag是点质量标签。官方定义里0 是正常点1 是无效点2 是超视距点3 是噪声点。其中 1 和 3 是要重点关注的。无效点通常出现在反射率过低或距离超出有效测程的情况噪声点则可能是多路径反射、边缘衍射造成的离群点。我实测下来室内白墙环境下 tag0 的正常点占比通常在 90% 以上但在阳光直射的场景或者玻璃幕墙附近无效点比例会明显上升。line表示这个点来自内部哪个激光发射器/扫描模组。Mid-360 上这个值通常只有 0、1、2、3 这几个整数。不同的 line 值对应不同的扫描子模块点云在空间上呈现不同的分布花样。如果你做特征提取时发现某些特征点频繁出现在特定 line 值上可以利用这个信息做更精细的过滤比如剔除某个发射器异常产生的固定噪声。3. 从 bag 里把原始数据提取出来完整可跑的方案3.1 准备阶段驱动启动与录包你至少需要一个能跑的环境。推荐直接用 Docker 或者 Ubuntu 22.04 ROS2 Humble拉取livox_ros_driver2源码编译。编译通过后Mid-360 对应的启动命令一般是source /opt/ros/humble/setup.bash source ~/livox_ws/install/setup.bash ros2 launch livox_ros_driver2 msg_MID360.launch.py启动后可以确认一下话题ros2 topic info /livox/lidar -v正常情况下会显示类型为livox_interfaces/msg/CustomMsg发布频率和雷达设置一致默认 10Hz。录包直接用ros2 bag record /livox/lidar -o mid360_data我建议把/tf和 IMU 相关话题一并录进去后续做 SLAM 会需要。只录一个原始点云话题能做的分析会受限。3.2 ROS2 节点实时转换CustomMsg 转 PointCloud2如果你不需要离线处理直接在 ROS2 里写一个转换节点最方便。下面这个代码片段是我在实际项目里精简出来的核心是构造 PointCloud2并把 reflectivity 映射到 intensity 字段。#include rclcpp/rclcpp.hpp #include livox_interfaces/msg/custom_msg.hpp #include sensor_msgs/msg/point_cloud2.hpp #include sensor_msgs/point_cloud2_iterator.hpp class CustomMsgToPointCloud2 : public rclcpp::Node { public: CustomMsgToPointCloud2() : Node(custom_msg_to_pc2) { sub_ create_subscriptionlivox_interfaces::msg::CustomMsg( /livox/lidar, rclcpp::SensorDataQoS(), [this](const livox_interfaces::msg::CustomMsg::SharedPtr msg) { convert(msg); }); pub_ create_publishersensor_msgs::msg::PointCloud2(/mid360/pointcloud2, rclcpp::SensorDataQoS()); } private: void convert(const livox_interfaces::msg::CustomMsg::SharedPtr msg) { size_t valid_num 0; for (size_t i 0; i msg-points.size(); i) { if (msg-points[i].tag 0) { valid_num; } } sensor_msgs::msg::PointCloud2 cloud; cloud.header.stamp msg-header.stamp; cloud.header.frame_id livox_frame; cloud.height 1; cloud.width static_castuint32_t(valid_num); cloud.is_dense false; sensor_msgs::PointCloud2Modifier mod(cloud); mod.setPointCloud2FieldsByString(2, xyz, 1, intensity); mod.resize(valid_num); sensor_msgs::PointCloud2Iteratorfloat it_x(cloud, x); sensor_msgs::PointCloud2Iteratorfloat it_y(cloud, y); sensor_msgs::PointCloud2Iteratorfloat it_z(cloud, z); sensor_msgs::PointCloud2Iteratorfloat it_i(cloud, intensity); for (size_t i 0; i msg-points.size(); i) { const auto pt msg-points[i]; if (pt.tag ! 0) { continue; } *it_x pt.x; *it_y pt.y; *it_z pt.z; *it_i static_castfloat(pt.reflectivity); it_x; it_y; it_z; it_i; } pub_-publish(cloud); } rclcpp::Subscriptionlivox_interfaces::msg::CustomMsg::SharedPtr sub_; rclcpp::Publishersensor_msgs::msg::PointCloud2::SharedPtr pub_; };这段代码里我默认丢弃了 tag 非 0 的点。如果你要保留原始全部点做调试把过滤条件去掉即可。注意SensorDataQoS是为了让点云消息走 best effort 传输避免丢帧这一点对接 SLAM 时很关键。3.3 离线 Python 解析 bag 的快速方法没有 ROS2 环境或者机器带不动 rviz 的时候离线解析是最省资源的。我通常用rosbags库配合ros2 bag play的组合。前提是先把 livox_interfaces 的自定义消息定义加载进类型系统。方式是把 CustomMsg 和 CustomPoint 的消息定义字符串传给get_types_from_msg。大致流程如下from pathlib import Path from rosbags.highlevel import AnyReader from rosbags.typesys import Stores, get_typestore custom_msg_text std_msgs/Header header uint64 timebase uint32 point_num uint8 lidar_id livox_interfaces/msg/CustomPoint[] points custom_point_text uint32 offset_time float32 x float32 y float32 z uint8 reflectivity uint8 tag uint8 line typestore get_typestore(Stores.ROS2_HUMBLE) typestore.register(get_types_from_msg(custom_point_text, livox_interfaces/msg/CustomPoint)) typestore.register(get_types_from_msg(custom_msg_text, livox_interfaces/msg/CustomMsg)) with AnyReader(Path(mid360_data), default_typestoretypestore) as reader: for connection, timestamp, rawdata in reader.messages(): if connection.msgtype ! livox_interfaces/msg/CustomMsg: continue msg reader.deserialize(rawdata, connection.msgtype) # 这里 msg.points 就是一个包含所有点的列表 break拿到msg.points之后直接用 numpy 转为数组做后续处理就可以了。这个方法不依赖 ROS2 运行时很适合把数据批量倒出来做算法调研。唯一要注意的是消息定义文本必须和你 bag 里实际使用的版本一致否则字段对齐会出错。3.4 保存为 PCD 文件与可视化验证转成 PointCloud2 之后下一步通常是保存成 PCD 文件方便后续查看。我是用 PCL 直接做的比较省事pcl::PointCloudpcl::PointXYZI::Ptr cloud(new pcl::PointCloudpcl::PointXYZI); pcl::fromROSMsg(pc2_msg, *cloud); pcl::io::savePCDFileBinary(mid360_frame.pcd, *cloud);保存完用 CloudCompare 打开看一眼应该能看到典型的花朵状扫描轨迹。如果你看到的点云成了一条条整齐的线那说明数据反而可能不对——Mid-360 的原始帧点云空间分布就是不均匀的。这个视觉特征也可以用来快速判断你的解析逻辑是否正确。4. 数据提取实战按 tag 过滤、反射率筛选和 ROI 裁剪4.1 tag 过滤与去噪逻辑tag 过滤是点云预处理里最重要的一步。雷达本身输出的点里无效点和噪声点的比例会受环境光、物体材质、距离影响。在室外晴天对着玻璃幕墙扫一圈tag1 的点能占到接近 20%。这些点如果直接喂给 SLAM 或目标检测会给特征匹配造成大量假阳性。实际项目中我的策略是在转换阶段就把 tag ! 0 的点丢掉。这一步不做后续所有处理都会被污染。如果你用的是第 3 节的转换节点直接在填充 PointCloud2 前过滤即可。4.2 reflectivity 的筛选技巧反射率筛选是灰度图上做分割之外最便宜的语义手段。比如在园区场景里路牌、车道线、反光立柱的 reflectivity 通常远高于路面和树丛。我做目标检测前处理时会先对点云做一个反射率阈值过滤intensity point_cloud[:, 3] high_reflectivity_mask intensity 100这样能快速拉出高反射特征点。反过来如果你想保留地面点做地面分割用低反射率掩码也可以辅助。需要提醒的是不同距离下同一个物体的 reflectivity 会有差异阈值不能定死最好按距离区间分段设置。我在 10 米以内经常用 80~100 作为阈值30 米外就要往下调到 50 左右。4.3 用 offset_time 做帧内时间切片理解了 offset_time 单位以后你可以按时间维度切分一帧点云。比如研究多路径干扰时我只想保留每个点云帧中最早发出的那部分让点空间分布更接近干净状态# 假设 offset_time 单位是纳秒 mask np.mod(points[offset_time].astype(np.int64) // 1_000_000, 100) 30 sub_points points[mask]这个做法还可以用来分析运动畸变把一帧点云按时间切成两个子帧分别投影你就能直观看到车辆运动造成的点云扭曲程度。这个技巧在排查建图飘问题时非常有用。4.4 空间 ROI 裁剪与多帧积分叠加Mid-360 单帧点数大约是 2 万点左右在全向 360 度范围内展开稀疏度其实很高。做近距离障碍物检测时我经常先在空间上做 ROI 裁剪把半径 10 米外的点全部丢掉只保留雷达正前方的一个扇形区域。这样既降低了计算量又避免了大量背景点干扰特征提取。distance np.linalg.norm(points[:, :3], axis1) roi_mask (distance 10.0) (points[:, 0] 0) (np.abs(points[:, 1]) 5.0) filtered points[roi_mask]而当你需要更高空间密度时单帧就不够用了。非重复扫描的好处是时间越长覆盖越密。我通常会把同一位置连续 5 到 10 帧点云累加成一帧稠密点云再做平面拟合或者障碍物聚类。累加的前提是载体基本静止或者你能拿到帧间位姿做变换。实际代码就是在转换节点里维护一个环形缓冲收到新帧时先把上一帧的所有点按 TF 变换到当前坐标系再拼接。5. 对接 SLAM 与可视化工具时绕不开的坑5.1 时间戳同步与点云去畸变建图为什么飘很多人在 ROS2 里跑 Cartographer 建图投出来的地图一开始正常走几步就开始漂移回头看感觉好像激光雷达出问题了。但更常见的原因是点云包含运动畸变而没有做任何补偿。Mid-360 在一帧100ms扫描时间内载具已经移动了一段距离。如果雷达以 10Hz 频率发布消息但 SLAM 前端把一帧内的所有点都当作同一个时刻采集的那点云就被拉花了。在 Cartographer 里接入 Mid-360 时我建议先别急着非要上激光 SLAM而是拆两步第一步把 CustomMsg 转成 PointCloud2 时尽量保留offset_time到点云场的自定义字段或者用timebase offset_time给每个点赋予更精确的时间戳。第二步如果车辆运动较快应当先做去畸变。比较成熟的做法是配合 IMU 做运动补偿把一帧中点云按采集时刻变换到 scan 起始时刻。不做这步配置再完美的 SLAM 管线也会在转角处飘。pointcloud_to_laserscan这种投影工具也需要注意它默认输入是标准 PointCloud2并且每个点的 timestamp 可能被当作同一时刻这就丢了 offset_time 信息。因此在做投影之前最好先做时间补偿或者至少保证雷达在静止状态下录数据做标定和测试。5.2 IP 地址配置与设备发现Mid-360 的默认 IP 一般是 192.168.1.150但不同固件版本可能不同不建议靠猜。我推荐直接用 Livox Viewer 自动搜索设备。操作流程大致如下电脑有线网卡手动设置为静态 IP例如 192.168.1.100子网掩码 255.255.255.0网关可留空。打开 Livox Viewer软件会自动扫描局域网内的 Livox 设备会看到设备当前 IP。选中设备后进入设置修改 IP 地址保存后设备会重启。改完后电脑网卡要切到新 IP 对应的网段才能重新连上。如果你改错 IP导致设备从网内消失别慌。把网卡设成自动获取重新打开 Livox Viewer 扫描很多时候设备还是会以临时 IP 出现。实在找不到就用网线直连再手动重置设备网络配置。这个操作偶尔需要多试两次但不要先急着怀疑设备坏了。5.3 CloudCompare 里点云转 tif以及 PCL 字段变化的问题热词里有人问CloudCompare 怎么把点云保存成 tif 格式很多人以为直接导出成图片就行。实际上 CloudCompare 不能把一个散点集合直接存成单张 tif它需要一个两步操作把点云栅格化选中点云菜单 Tools - Projection - Rasterize设置输出格网大小比如 0.1m选择高程字段z 或强度。栅格化完成后生成一个 Raster 对象再选中它 File - Save文件类型里选择 GeoTIFF。这个 tif 本质是一张高程或者强度栅格图后续可以导入 GIS 软件和通用图像处理流程对接。如果你只是想保存点云本身CloudCompare 应该存 PCD、LAS、PLY 这类点云格式别混了。还有一点要注意用 PCL 读取通过转换节点生成的 PointCloud2 时reflectivity 会被映射到 intensity 字段tag 和 line 字段默认会丢失。如果你做后续处理仍然需要 tag 信息必须在转换时就自定义 PointCloud2 的 field或者使用自定义点类型把 tag 包进去。否则等数据进了 PCL 再说 tag 就晚了。6. 实测体会与排查建议6.1 拿到数据后先检查这三个打印点不要一上来就闷头跑 SLAM。我先教你三个快速判断数据是否正常的检查点打印point_num同型号雷达在同样配置下帧点数一般稳定在一个区间。如果突变成 0 或者突然翻倍说明驱动或通讯链路有问题。统计tag 0点的占比正常室内场景应该超过 90%。如果低于 70%先检查雷达是否靠近强反射物或者是否在强烈阳光下工作。连续打印timebase的差值设备重启后 timebase 会重新累计但正常情况下连续两帧的 timebase 差值应该与帧周期接近。如果差值不稳定说明驱动被动重启或网络丢包严重。这三个检查做完基本上能排除 80% 的数据看起来不对问题。6.2 从建图飘现象反推数据链路的哪个环节出问题网上关于激光雷达建图飘的求助特别多。通常我会按这个顺序反推先看是不是输入点云带大量 tag ! 0 的脏点。再把一帧点云在 RViz 里慢放观察点云是否明显畸变。如果畸变量和车辆运动方向一致那就是没去畸变。接着查时间戳和 TFCartographer 这类 SLAM 对输入点云时间戳的连续性很敏感时间戳倒流或者突然跳变会直接导致里程计崩掉。最后才是调 Cartographer 参数。我自己遇到过的最隐蔽的问题是 livox_ros_driver2 在特定网络环境下会周期性丢包导致 timebase 突然跳变一大截而 SLAM 前端毫不知情直接把跳变当成了一次大位移。这种问题排查起来非常费劲最后是用第三步的 timebase 差值检查发现的。所以那个检查点一定要做。6.3 一套可复用的点云提取工具链组合建议最后整理一下我目前比较顺手的一套工具链在线处理livox_ros_driver2 启动 自己写转换节点输出 PointCloud2 RViz 实时观察。离线处理ros2 bag record 录包 第 3 节 Python 解析脚本 numpy 做字段提取 PCL/CloudCompare 做可视化。SLAM 验证先把数据转成 PointCloud2配合点云去畸变后再接入 Cartographer如果想省事直接跑 FAST-LIO 这类对 Livox 点云原生支持更好的方案。标定与后处理CloudCompare 做强度/高程栅格化导出 tif或者直接保存 PCD 交给目标检测脚本。这套组合不挑机器从纯 CPU 笔记本到带 GPU 的工作站都能跑。关键是每一步的数据格式转换逻辑要清晰别一个环节丢字段后面全抓瞎。从我实际操作的经验看处理 Livox-Mid-360 这类自定义格式点云最忌讳的就是拿传统机械雷达的思路硬套。先把消息字段的物理含义、时间戳单位、tag 过滤规则搞清楚后面的转换、建图、检测都是水到渠成的事。你如果现在正在被 CustomMsg 卡住不妨先把ros2 interface show输出打出来对照着打印一帧数据把字段分布弄明白再考虑往下一步走。
返回列表