☰
双Livox Mid-360激光雷达融合实战:标定、C++/PCL实现与性能优化
2026/10/6 6:15:51 网站建设 项目流程

双雷达融合这个活儿,说难不难,但坑是真多。我断断续续折腾了一个多月,从最开始两台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 LTS,ROS2 Humble,Livox 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_ALL=en_US.UTF-8 LANG=en_US.UTF-8 export LANG=en_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="-std=c++17"

驱动编译完成后,记得source环境:

source /opt/ros/humble/setup.bash source ~/livox_ws/install/setup.bash

2.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中的激光雷达-激光雷达标定部分,或者也可以使用开源社区里基于平面拟合的方案。核心步骤是:

  1. 录制两台雷达同时观测靶标的rosbag数据。
  2. 从点云中提取靶标平面,得到平面法向量和距离。
  3. 利用多组平面观测,求解两台雷达之间的旋转矩阵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_publisher<sensor_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::PointCloud<pcl::PointXYZI>::Ptr pcl_cloud1(new pcl::PointCloud<pcl::PointXYZI>()); pcl::PointCloud<pcl::PointXYZI>::Ptr pcl_cloud2(new pcl::PointCloud<pcl::PointXYZI>()); pcl::fromROSMsg(*cloud1, *pcl_cloud1); pcl::fromROSMsg(*cloud2, *pcl_cloud2); // 坐标变换 pcl::PointCloud<pcl::PointXYZI>::Ptr cloud1_transformed(new pcl::PointCloud<pcl::PointXYZI>()); pcl::transformPointCloud(*pcl_cloud1, *cloud1_transformed, extrinsic_); // 拼接 pcl::PointCloud<pcl::PointXYZI>::Ptr fused_cloud(new pcl::PointCloud<pcl::PointXYZI>()); *fused_cloud = *cloud1_transformed + *pcl_cloud2; // 体素降采样,减少重叠区域冗余点 pcl::VoxelGrid<pcl::PointXYZI> voxel; voxel.setInputCloud(fused_cloud); voxel.setLeafSize(0.02f, 0.02f, 0.02f); pcl::PointCloud<pcl::PointXYZI>::Ptr filtered_cloud(new pcl::PointCloud<pcl::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::Subscriber<sensor_msgs::msg::PointCloud2> sub_lidar1_; message_filters::Subscriber<sensor_msgs::msg::PointCloud2> sub_lidar2_; typedef message_filters::sync_policies::ApproximateTime< sensor_msgs::msg::PointCloud2, sensor_msgs::msg::PointCloud2> MySyncPolicy; std::shared_ptr<message_filters::Synchronizer<MySyncPolicy>> sync_; rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pub_cloud_; Eigen::Matrix4f extrinsic_; };

这段代码基本涵盖了核心流程。需要注意的几个细节:

  1. ApproximateTime的队列大小不能太小,建议10-20,太小容易丢帧,太大延迟会增加。
  2. pcl::transformPointCloud内部使用Eigen矩阵,标定结果需要注意平移向量是米还是毫米。
  3. 体素滤波的leaf size通常取0.02m~0.05m,具体看应用场景。叶子太小降采样效果不明显,太大则会丢失细节。

5.2 PCL点云类型选择与强度信息保留

Mid-360默认输出的点云类型是PointXYZI,包含x、y、z坐标和intensity强度值。在多线雷达里,intensity值对反射率较高的物体(如车道线、反光牌)有很好的区分度,所以融合过程中务必保留。

直接用pcl::fromROSMsg从sensor_msgs::msg::PointCloud2转换为pcl::PointCloud<pcl::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已经做了时间同步,但它只能保证两帧消息的时间戳接近,无法完全消除扫描周期内的运动畸变。

缓解方案有两个方向:

  1. 提高雷达的publish_freq配置到20Hz,时间偏差绝对时间变小,拖影减轻,但CPU开销翻倍。
  2. 在融合前对点云做运动畸变补偿,需要IMU或轮式里程计提供每帧内的位姿插值。

对于大多数低速移动的室内机器人,第一种方案就够用了。

6.3 性能表现:CPU与内存占用

融合节点在释放模式下运行,实测单帧点云约2万点的处理耗时约6-8ms,CPU占用稳定在一个核的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( package='livox_ros_driver2', executable='livox_ros_driver2_node', name='livox_lidar1', output='screen', 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( package='livox_ros_driver2', executable='livox_ros_driver2_node', name='livox_lidar2', output='screen', 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( package='lidar_fusion', executable='lidar_fusion_node', name='lidar_fusion_node', output='screen' ) ])

注意:Livox驱动配置文件里有两个独立的配置文件,分别是LivoxMid360_config_1.json和LivoxMid360_config_2.json,里面的ip必须分别指向两台雷达,broadcast_code也必须各自匹配。如果你只改IP不改broadcast_code,驱动会找不到设备。

9. 遇到问题时的排查清单:长时间运行稳定性问题

融合节点长期运行后,偶尔会出现点云断流或内存增长的问题。这个问题困扰了我几天,最后排查出的根源是ROS2的DDS发现机制在多网络接口设备上不够稳定。由于工控机有有线网口和无线网卡,FastDDS默认会在所有网络接口上做发现广播,有时会因为无线网络的不稳定导致话题丢帧。

解决方案很直接:通过ROS_LOCALHOST_ONLY=1环境变量强制只在本机协议栈通信,不过这个方案只适用于单机多雷达场景。如果你后续要把点云通过网络发到其他主机处理,就需要在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,希望这篇实战记录能给你省下几天的踩坑时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询