资讯动态

双Livox Mid-360激光雷达融合实战:标定、C++/PCL实现与性能优化

发布时间:2026/10/6 6:15:53 来源:尧图企业网站定制
双雷达融合这个活儿说难不难但坑是真多。我断断续续折腾了一个多月从最开始两台Mid-360的点云在rviz里各玩各的到最后完整跑通C/PCL融合链路中间踩过的坑足够写满一页A4纸。这篇文章就把整个流程掰开揉碎讲清楚包括环境配置、外参标定、代码实现和性能优化重点是那些文档里不写、但实操必然遇到的细节。1. 为什么需要双Mid-360融合单雷达的视野死角问题先聊聊项目背景。我在做一个室内移动机器人平台需要在车身前后各装一台Livox Mid-360原因很简单一台Mid-360的水平视场角虽然标称360°但由于它采用非重复扫描方式实际点云在车体附近存在明显的盲区尤其是正上方和贴近车体的区域。更重要的是移动机器人一旦转向单雷达的感知范围会出现非常致命的死角这对导航避障来说是灾难性的。两台Mid-360一前一后安装后理论上可以实现近似360°无死角覆盖。但这里有个关键问题两台雷达各自发布独立的点云话题坐标原点分别在各自的物理安装位置直接把两帧点云拼在一起是没法用的。它们之间有一个固定的旋转和平移关系也就是外参只有算准这个外参才能把两台雷达的点云变换到同一个坐标系下。所以整个项目的核心链路是环境配置 - 驱动调试 - 外参标定 - C节点融合 - 性能优化。这篇文章就按这个顺序来写。2. 环境准备Ubuntu 22.04 ROS2 Humble的完整踩坑记录先交代一下我的硬件和系统环境。主机是一台x86工控机16GB内存处理器是i7-12700H系统盘是NVMe SSD。软件环境是Ubuntu 22.04.3 LTSROS2 HumbleLivox ROS Driver 2。这套组合是目前Mid-360最主流的使用方案教程资料也最全建议新入门的朋友不要轻易尝试Ubuntu 20.04 ROS2 Foxy或者更老的环境会平白增加很多适配成本。2.1 ROS2 Humble安装中容易被忽略的细节ROS2 Humble的安装网上教程一大把但有几个细节值得专门提一下。首先一定要确保locale环境正确否则编译和运行时会出现莫名其妙的字符编码问题。建议在安装前就执行locale # 检查是否输出 UTF-8如果输出不是UTF-8先执行sudo apt update sudo apt install locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALLen_US.UTF-8 LANGen_US.UTF-8 export LANGen_US.UTF-8其次ROS2 Humble的apt源添加必须确保系统架构正确。在Ubuntu 22.04上直接使用官方源即可但国内用户建议换成镜像源否则下载速度会让人崩溃。我使用清华镜像源具体配置方法这里不展开核心是把packages.ros.org替换为mirrors.tuna.tsinghua.edu.cn/ros2。2.2 Livox驱动编译的三大高频报错Livox官方的ROS2驱动仓库是https://github.com/Livox-SDK/livox_ros_driver2编译本身不复杂但有几个高频报错值得提前说明。报错一找不到livox_interfaces包这个问题的根源是子模块没有拉取完整。正确做法是git clone https://github.com/Livox-SDK/livox_ros_driver2.git cd livox_ros_driver2 git submodule update --init --recursive报错二编译时提示找不到fastdds相关依赖Livox驱动在ROS2 Humble下默认使用FastDDS作为DDS中间件需要提前安装依赖sudo apt install ros-humble-rmw-fastrtps-cpp报错三livox_ros_driver2编译时报C标准错误这个报错比较隐蔽通常是因为系统默认编译器版本和驱动要求的C标准不匹配。可以检查CMakeLists.txt中对C标准的设置并在编译前显式指定export CXXFLAGS-stdc17驱动编译完成后记得source环境source /opt/ros/humble/setup.bash source ~/livox_ws/install/setup.bash2.3 双雷达的IP配置与驱动参数验证Mid-360出厂时默认IP是192.168.1.100默认子网掩码是255.255.255.0。如果只接一台雷达直接把电脑网卡IP配成192.168.1.50之类的地址就能通信。但双雷达场景下必须修改两台雷达的IP避免冲突。修改IP的方式有两种一是用Livox Viewer 2工具在图形化界面中修改并写入雷达二是用Livox提供的命令行工具livox_lidar_config。我个人推荐用命令行因为可以在脚本里批量操作方便后续重新刷固件时快速恢复。配置完成后务必在启动驱动前用ping验证连通性ping 192.168.1.101 # 假设雷达1的IP ping 192.168.1.102 # 假设雷达2的IP驱动配置文件config/LivoxMid360Config.json中的关键参数如下{ lidar_config: [ { ip: 192.168.1.101, pcl_data_type: 1, pattern_mode: 0, frame_id: livox_frame_1 }, { ip: 192.168.1.102, pcl_data_type: 1, pattern_mode: 0, frame_id: livox_frame_2 } ], publish_freq: 10.0 }注意frame_id一定要区分开否则后续在TF树里没法区分两个坐标系。3. 双雷达点云的时间同步策略为什么不能直接拼接外参标定之前先把时间同步这件事讲清楚。很多人以为双雷达融合就是把两帧点云做个坐标变换然后拼在一起实际操作中会被一个隐藏问题卡住两台雷达的扫描时刻并不完全一致。Mid-360内部有一个非重复扫描机制它通过旋转棱镜的方式扫描点云输出频率通常配置为10Hz。但两台雷达各自独立运行它们的扫描周期之间存在随机相位差也就是通常说的“帧不同步”。如果直接用同一时刻接收到的两帧点云做拼接在静止场景下问题不大一旦平台运动起来就会出现点云错位或拖影。解决这个问题的标准方案是使用ROS2的message_filters时间同步机制。我推荐使用ApproximateTime同步策略它允许两个话题的消息时间戳存在一定偏差通常设定为容差50ms在保证同步质量的同时不会因为微小的时钟偏差而频繁丢弃数据。注意这里踩过一个坑。如果使用ExactTime同步策略两台雷达的时间戳必须严格对齐但实际测试中即使我在两台雷达的驱动里都开启了PTP时间同步帧率依然会有微小抖动导致ExactTime频繁丢帧。所以实际项目中使用ApproximateTime是更稳妥的选择。时间同步问题的本质是我们以哪一台雷达的时间为基准。我的做法是融合节点内部以雷达2的时间戳为基准把雷达1的点云通过外参变换到雷达2坐标系后拼接。这样融合后的点云时间戳与雷达2保持一致方便下游模块直接订阅。4. 台架标定实操从靶标布设到旋转外参求解外参标定是整个融合流程中技术含量最高、最容易出问题的一环。Mid-360虽然是固态激光雷达但Livox官方提供了一套完整的标定工具链核心思路是利用两块雷达同时观测同一平面靶标通过平面约束求解相对位姿。4.1 靶标布设与数据采集规范标定靶标建议使用大尺寸平面板至少1m×1m表面需要足够平整和不反光。官方推荐的标定板是黑白棋盘格样式但实际使用时普通的白色泡沫板或金属平板也能达到不错的效果。关键是靶标必须同时被两块雷达完整看到。数据采集时需要缓慢变换靶标相对于雷达的姿态采集多组位姿下的点云数据。这里有一个实战经验靶标距离雷达太近小于0.5m时Mid-360的近距盲区会导致点云厚度异常太远大于5m时靶标边缘的点云噪声会显著增加。建议在1.5m到3m的范围内变换靶标姿态每组位姿保持5秒以上至少采集20组以上的数据。4.2 基于官方标定工具的外参求解外参标定工具我使用的是Livox提供的livox_camera_lidar_calibration中的激光雷达-激光雷达标定部分或者也可以使用开源社区里基于平面拟合的方案。核心步骤是录制两台雷达同时观测靶标的rosbag数据。从点云中提取靶标平面得到平面法向量和距离。利用多组平面观测求解两台雷达之间的旋转矩阵R和平移向量t。具体计算原理可以简单理解为同一物理平面在雷达1坐标系下的平面方程和在雷达2坐标系下的平面方程经过外参变换后应当完全一致。这样通过最小化平面残差即可求解外参。4.3 标定质量验证与常见失败原因标定完成后必须验证外参是否准确。我的习惯是把雷达1的点云根据标定结果变换到雷达2坐标系然后在rviz里叠加显示观察同一物体的点云是否完全重合。如果重叠区域出现错位说明标定精度不够需要重新采集数据。常见的标定失败原因有三种靶标姿态变换范围太小导致平面约束不足外参在某些方向上的可观测性差。靶标表面有反光材质点云中的平面提取受到反射点干扰。两台雷达的视野重叠区域太小导致靶标无法同时被完整观测。建议采集数据时靶标尽量在雷达的公共视野内做大范围姿态变换既要有正对雷达的姿态也要有明显的俯仰和偏航角变化这样才能把旋转外参的各个维度都约束住。5. C/PCL融合节点实现从零到一的核心代码环境、同步、标定都准备妥当之后最核心的融合节点就可以开始写了。这里我给出一个简洁但完整的C实现涉及的关键技术点包括ROS2节点编写、message_filters时间同步、PCL点云变换、拼接与体素降采样。5.1 节点整体架构融合节点的功能很简单订阅两个点云话题同步后把雷达1点云变换到雷达2坐标系拼接、降采样后发布融合点云。节点类设计如下#include rclcpp/rclcpp.hpp #include sensor_msgs/msg/point_cloud2.hpp #include message_filters/subscriber.h #include message_filters/synchronizer.h #include message_filters/sync_policies/approximate_time.h #include pcl/point_cloud.h #include pcl/point_types.h #include pcl/common/transforms.h #include pcl/filters/voxel_grid.h #include pcl_conversions/pcl_conversions.h class LidarFusionNode : public rclcpp::Node { public: LidarFusionNode() : Node(lidar_fusion_node) { // 外参矩阵将lidar1的点云变换到lidar2坐标系 // 实际数值由标定结果填入 extrinsic_.setIdentity(); extrinsic_(0, 3) 0.35; // x平移 0.35m extrinsic_(1, 3) 0.0; // y平移 0.0m extrinsic_(2, 3) 0.10; // z平移 0.10m sub_lidar1_.subscribe(this, /livox/lidar1/points); sub_lidar2_.subscribe(this, /livox/lidar2/points); sync_.reset(new SyncPolicy(MySyncPolicy(10), sub_lidar1_, sub_lidar2_)); sync_-registerCallback(LidarFusionNode::callback_fusion, this); pub_cloud_ this-create_publishersensor_msgs::msg::PointCloud2(/livox/fused/points, 10); } private: void callback_fusion(const sensor_msgs::msg::PointCloud2::ConstSharedPtr cloud1, const sensor_msgs::msg::PointCloud2::ConstSharedPtr cloud2) { pcl::PointCloudpcl::PointXYZI::Ptr pcl_cloud1(new pcl::PointCloudpcl::PointXYZI()); pcl::PointCloudpcl::PointXYZI::Ptr pcl_cloud2(new pcl::PointCloudpcl::PointXYZI()); pcl::fromROSMsg(*cloud1, *pcl_cloud1); pcl::fromROSMsg(*cloud2, *pcl_cloud2); // 坐标变换 pcl::PointCloudpcl::PointXYZI::Ptr cloud1_transformed(new pcl::PointCloudpcl::PointXYZI()); pcl::transformPointCloud(*pcl_cloud1, *cloud1_transformed, extrinsic_); // 拼接 pcl::PointCloudpcl::PointXYZI::Ptr fused_cloud(new pcl::PointCloudpcl::PointXYZI()); *fused_cloud *cloud1_transformed *pcl_cloud2; // 体素降采样减少重叠区域冗余点 pcl::VoxelGridpcl::PointXYZI voxel; voxel.setInputCloud(fused_cloud); voxel.setLeafSize(0.02f, 0.02f, 0.02f); pcl::PointCloudpcl::PointXYZI::Ptr filtered_cloud(new pcl::PointCloudpcl::PointXYZI()); voxel.filter(*filtered_cloud); // 发布 sensor_msgs::msg::PointCloud2 out_cloud; pcl::toROSMsg(*filtered_cloud, out_cloud); out_cloud.header cloud2-header; out_cloud.header.frame_id livox_frame_2; pub_cloud_-publish(out_cloud); } message_filters::Subscribersensor_msgs::msg::PointCloud2 sub_lidar1_; message_filters::Subscribersensor_msgs::msg::PointCloud2 sub_lidar2_; typedef message_filters::sync_policies::ApproximateTime sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2 MySyncPolicy; std::shared_ptrmessage_filters::SynchronizerMySyncPolicy sync_; rclcpp::Publishersensor_msgs::msg::PointCloud2::SharedPtr pub_cloud_; Eigen::Matrix4f extrinsic_; };这段代码基本涵盖了核心流程。需要注意的几个细节ApproximateTime的队列大小不能太小建议10-20太小容易丢帧太大延迟会增加。pcl::transformPointCloud内部使用Eigen矩阵标定结果需要注意平移向量是米还是毫米。体素滤波的leaf size通常取0.02m~0.05m具体看应用场景。叶子太小降采样效果不明显太大则会丢失细节。5.2 PCL点云类型选择与强度信息保留Mid-360默认输出的点云类型是PointXYZI包含x、y、z坐标和intensity强度值。在多线雷达里intensity值对反射率较高的物体如车道线、反光牌有很好的区分度所以融合过程中务必保留。直接用pcl::fromROSMsg从sensor_msgs::msg::PointCloud2转换为pcl::PointCloudpcl::PointXYZI时需要注意ROS2消息里的fields字段必须包含x, y, z, intensity。如果驱动配置中把pcl_data_type设置成别的类型比如PointXYZ转换时会报警告或丢失强度值。5.3 体素降采样参数选择不要无脑用2cm体素降采样这一步经常被忽略但它对融合后点云质量的提升是决定性的。两台Mid-360的重叠区域会有大量冗余点如果不做降采样重叠区域的点密度会是其他区域的两倍以上下游的栅格地图构建和点云分割会因此出现偏置。leaf size的选择依据是你要保证融合后点云的最小可分辨物体尺寸。对于室内机器人避障2cm足够对于室外无人车的远距离感知5cm更合适。叶子太大对细小障碍物如电线检测不友好这一点实测下来非常明显。我也尝试过不做体素降采样、直接拼接发布结果是点云总量约2倍下游SLAM跑起来明显卡顿且里程计精度下降。所以降采样不是可选项是必选项。5.4 调试技巧rviz快速验证外参是否正确代码写完第一次跑先在rviz里直接订阅两个原始点云话题手动添加一个TF变换或者用PointCloud2的Fixed Frame设为livox_frame_2然后看看雷达1的点云是否大致落在雷达2点云的合理位置上。如果两张点云在空间上完全错乱不用怀疑代码逻辑九成是外参矩阵加载错误。还有一个常见的调试技巧把外参矩阵中旋转和平移分开验证。先只设置平移、不设置旋转看点云是否相对平移到了大致正确的位置再设置旋转微调角度。不要上来就直接用完整外参出了问题不好定位。6. 实测效果与问题复盘静态精度和动态场景下的表现融合节点写完后我在室内外分别做了测试。真实结果和预期有不少出入这里把观察到的现象和问题处理记录写清楚方便大家对照。6.1 室内静态场景重叠区域点云厚度变化室内测试环境是一个约20平米的房间摆放了桌椅、纸箱和三角支架。雷达1和雷达2相距0.8m左右公共视野区域的点云拼接效果如下墙面点云厚度在未融合前单雷达扫描约为±2cm融合后经体素降采样墙面点云厚度保持在±2cm范围内。纸箱边缘轮廓清晰没有出现双影现象。重叠区域点云密度均匀没有明显的疏密突变。这个结果说明外参标定精度和体素降采样参数都选得比较合适。6.2 动态场景人走动时的拖影问题动态场景下我手持靶标在雷达前快速摆动融合点云中出现了轻微的拖影现象。这个问题的根源在于两台雷达的扫描时刻不同步。虽然ApproximateTime已经做了时间同步但它只能保证两帧消息的时间戳接近无法完全消除扫描周期内的运动畸变。缓解方案有两个方向提高雷达的publish_freq配置到20Hz时间偏差绝对时间变小拖影减轻但CPU开销翻倍。在融合前对点云做运动畸变补偿需要IMU或轮式里程计提供每帧内的位姿插值。对于大多数低速移动的室内机器人第一种方案就够用了。6.3 性能表现CPU与内存占用融合节点在释放模式下运行实测单帧点云约2万点的处理耗时约6-8msCPU占用稳定在一个核的40%左右。体素滤波是计算的绝对大头约占50%耗时。在优化时可以考虑只对重叠区域做体素滤波非重叠区域保留原始点云节省计算。使用pcl::ApproximateVoxelGrid替代VoxelGrid速度快但不保证点云均匀性。如果你把融合节点和下游SLAM放在同一台工控机上建议给融合节点启动--cpu-affinity绑定安静核避免被调度器频繁迁移导致延迟抖动。7. 双雷达标定中一个容易被忽略的细节点云强度对齐前面重点讲了外参旋转和平移标定但实际测试中发现在后期融合应用中还有一个比较少被提到、但影响明显的点两台雷达的强度值响应对同一物体的反射强度可能存在差异。这会造成下游分割或聚类算法在不同方向上点云特征不一致。如果你只需要做几何融合这个差异可以忽略但如果你要做基于反射强度的特征匹配或车道线检测建议使用Livox Viewer 2自带的多雷达强度自动对齐功能或者离线统计两台雷达对同一标定板反射强度直方图做分位数归一化。我的实测结论是Mid-360的两台设备之间强度差异约为3%~8%与出厂批次和温度相关。好在Livox驱动支持逐设备配置增益补偿系数我们通过标定将平均差异降到了2%以内后续做地面分割时就不会因为强度不连续而把同一平面误判成两块。8. 一套可以直接复用的完整CMakeLists与launch文件最后把项目完整的CMakeLists.txt和launch文件贴出来方便直接复用。这里用ament_cmake构建依赖rclcpp、sensor_msgs、message_filters、pcl_ros、pcl_conversions、libpcl-all-dev和Eigen3。cmake_minimum_required(VERSION 3.8) project(lidar_fusion) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES Clang) add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(sensor_msgs REQUIRED) find_package(message_filters REQUIRED) find_package(pcl_ros REQUIRED) find_package(pcl_conversions REQUIRED) find_package(Eigen3 REQUIRED) add_executable(lidar_fusion_node src/lidar_fusion_node.cpp) target_include_directories(lidar_fusion_node PRIVATE ${EIGEN3_INCLUDE_DIR} ${PCL_INCLUDE_DIRS}) target_link_libraries(lidar_fusion_node ${rclcpp_LIBRARIES} ${sensor_msgs_LIBRARIES} ${message_filters_LIBRARIES} ${pcl_ros_LIBRARIES} ${pcl_conversions_LIBRARIES} ${PCL_LIBRARIES}) ament_target_dependencies(lidar_fusion_node rclcpp sensor_msgs message_filters pcl_ros pcl_conversions) install(TARGETS lidar_fusion_node DESTINATION lib/${PROJECT_NAME}) ament_package()launch文件也一并给出用于同时启动两台雷达驱动和融合节点from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagelivox_ros_driver2, executablelivox_ros_driver2_node, namelivox_lidar1, outputscreen, parameters[{ xfer_format: 1, multi_topic: 1, data_src: lidar, publish_freq: 10.0, output_data_type: 0, frame_id: livox_frame_1, lvx_file_path: , user_config_path: src/robot/config/LivoxMid360_config_1.json }] ), Node( packagelivox_ros_driver2, executablelivox_ros_driver2_node, namelivox_lidar2, outputscreen, parameters[{ xfer_format: 1, multi_topic: 1, data_src: lidar, publish_freq: 10.0, output_data_type: 0, frame_id: livox_frame_2, lvx_file_path: , user_config_path: src/robot/config/LivoxMid360_config_2.json }] ), Node( packagelidar_fusion, executablelidar_fusion_node, namelidar_fusion_node, outputscreen ) ])注意Livox驱动配置文件里有两个独立的配置文件分别是LivoxMid360_config_1.json和LivoxMid360_config_2.json里面的ip必须分别指向两台雷达broadcast_code也必须各自匹配。如果你只改IP不改broadcast_code驱动会找不到设备。9. 遇到问题时的排查清单长时间运行稳定性问题融合节点长期运行后偶尔会出现点云断流或内存增长的问题。这个问题困扰了我几天最后排查出的根源是ROS2的DDS发现机制在多网络接口设备上不够稳定。由于工控机有有线网口和无线网卡FastDDS默认会在所有网络接口上做发现广播有时会因为无线网络的不稳定导致话题丢帧。解决方案很直接通过ROS_LOCALHOST_ONLY1环境变量强制只在本机协议栈通信不过这个方案只适用于单机多雷达场景。如果你后续要把点云通过网络发到其他主机处理就需要在DDS配置里指定具体的网卡和网段。另一个可靠性问题是长时间运行后的内存碎片化点云消息在高频发布时如果内存分配器不及时归还内存RSS会缓慢增长。建议发布端的history_depth不要设置太大适度使用reliable而不是best_effort的QoS同时给融合节点加一个心跳监控一旦点云间隔超过0.5秒就重启进程。至于C的开发体验我还是要多说一句写ROS2 PCL之前务必把指针、智能指针特别是shared_ptr的使用彻底搞熟。用错了共享指针导致节点崩溃的概率远高于算法本身出bug的概率。还有第一次跑PCL的时候会遇到PCL::PCLReader读取PCD文件报height given (0) but no width的错误这通常是因为PCD文件缺失或格式异常和融合节点本身没有关系但排查起来很容易误伤特此说明。整个双雷达融合的项目做到这里基本能稳定输出质量不错的融合点云后续在我的项目中直接对接了SLAM和避障算法整体效果比单雷达稳健很多。如果你也正计划给机器人加装双Livox Mid-360希望这篇实战记录能给你省下几天的踩坑时间。

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

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

免费获取报价 →
↑