Mid360点云转2D导航全攻略:ROS2 Humble到Nav2落地实践
2026/9/23 7:36:54 网站建设 项目流程

1. 这个转换为什么绕不开:3D感知与2D导航的天然代沟

1.1 你手里的点云,导航根本"接不住"

很多人第一次把Mid360接上ROS2 Humble,看到RViz里那个以20Hz频率刷新的彩色点云时,第一反应都是"卧槽,真清晰,啥都能看见"。但真到了要跑导航的时候,问题就来了:Nav2栈里最核心的costmap、AMCL、路径规划器,绝大多数插件吃的都是2D LaserScan或者直接存成2D OccupancyGrid的栅格地图。Mid360吐出来的PointCloud2,一没有"一条线"的概念,二不是二维平面数据,直接塞给move_base或者Nav2,根本没法用。

这个"天然代沟"不是ROS的设计缺陷,而是历史惯性和计算效率共同决定的。2D导航的核心任务是"在平面上回答能不能走",因此它只需要一张像剪纸一样的俯视地图——墙是黑的,空地是白的,未知是灰的。3D点云则是一个包含数百万个点的空间集合,每个点都有x/y/z坐标和反射强度,信息量远超导航所需,但反过来也让"判断某个位置能否通行"这件事变得极其别扭。

所以这个教程的核心问题就一句话:同样是Mid360给的数据,怎么把它从"三维空间感知"翻译成"二维平面认知",让建图、定位、导航这一整套2D链路用起来

1.2 Mid360在转换链路里的特殊定位

Livox Mid360因为是非重复扫描的固态激光雷达,点云形态和传统的16线/32线机械雷达差别很大。传统雷达的每一帧天然就是一圈圈的线束,角度分辨率均匀,投影成2D时只需要把每条光束的高度角过滤掉,再按水平角排列就行。Mid360没有固定线束的概念,点云是类花瓣状的不均匀分布,近处点密密麻麻,远处逐渐稀疏。这个特点决定了转换逻辑不能做得太"粗暴",否则要么近处的栏杆被当成一堵墙,要么远处的墙角被完全漏掉。

同时Mid360的横向FOV是360°,纵向只有-7°到+52°,在垂直方向上是"天多地少"的分布。这反而是做2D转换的一个利好——因为地面点的占比比传统雷达小不少,滤波时少了很多麻烦。

在我实际测试下来,Mid360 + ROS2 Humble的组合,在小型室内移动机器人、TurtleBot3改装的底盘、以及一些服务机器人原型上,是非常稳定的点云来源。它本体重量轻、功耗低,不需要外部IMU就能输出40Hz的点云,对工控机的CPU和带宽压力也小。但在转换这个事情上,它又比传统雷达更考验代码质量和参数调优。

2. 起步阶段:ROS2 Humble、Livox驱动与坐标系检查

2.1 Ubuntu 22.04上ROS2 Humble的安装与配置

说到ROS2 Humble,它是针对Ubuntu 22.04(Jammy)发布的长期支持版本,2022年5月发布,官方支持到2027年。很多做ROS1 Noetic迁移过来的朋友,第一感觉是没那么顺手,但从我的体验来说,Humble的稳定性在ROS2各版本里排得上前列,配套文档和社区问答也是最丰富的。

安装其实就是一个标准流程。系统源替换成国内镜像源之后,需要先配置软件源里的ROS2 GPG key,然后把ROS2的apt源加进去。接着:

sudo apt update sudo apt install ros-humble-desktop

这里我建议装ros-humble-desktop而不是ros-humble-ros-base,因为desktop版本把RViz2、demo节点、tf2相关工具都带上了,后面调试点云、查看TF树、写launch文件时会省掉一半的麻烦。如果磁盘空间紧张,至少也要装ros-humble-desktop-common和rviz2。

环境变量别忘了写进bashrc:

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc source ~/.bashrc

然后装一些绕不开的基础工具:

sudo apt install python3-colcon-common-extensions ros-humble-tf2-tools ros-humble-rviz2

colcon是ROS2的编译工具,tf2-tools里面有view_frames这个看TF树的利器,rviz2是点云可视化主力。这些都装上,下面的步骤才有基础设施。

2.2 编译运行livox_ros_driver2,拿到Mid360原始点云

Mid360在ROS2下的驱动,Livox官方维护的是livox_ros_driver2,注意不是ROS1时代的livox_ros_driver,两个分支的接口和话题名都不一样。Humble对应的是ros2分支,拷贝下来之后需要手动改一下CMakeLists里面ROS2版本相关的一个判断,Compatible版本选Humble那一档。

编译本身不复杂:

git clone https://github.com/Livox-SDK/livox_ros_driver2.git -b ros2 cd livox_ros_driver2 colcon build --symlink-install source install/setup.bash

然后用对应的launch文件启动:

ros2 launch livox_ros_driver2 rviz_MID360.launch.py

这个launch会同时拉起驱动和RViz2,正常的话几秒之后你就能看到Mid360输出的/pcl_pub_0话题,消息类型是sensor_msgs/msg/PointCloud2。要注意的是,Mid360的IP是出厂固定的192.168.1.1xx,需要把工控机网口IP配到同一网段,一般是192.168.1.50这样的地址,子网掩码255.255.255.0。

提示:如果你发现RViz2里什么点云都没有,先不要折腾驱动编译,先ping一下192.168.1.1,再在容器/主机里用ros2 topic list确认话题是否出现。八成问题出在网络配置上,而不是驱动代码上。

2.3 坐标系树是否正确决定了后面所有事

做转换之前,有一件事比写代码更优先:确认TF树。转换节点要做的事情,本质上是把Mid360坐标系下的点,变换到机器人基座坐标系(base_link或base_footprint)下,再投影成scan数据。如果TF树没配好,后面所有转换结果都是歪的。

规范的TF树长这样:

  • map → odom → base_footprint → base_link → lidar_link

对于室内轮式机器人,base_footprint是地面投影点,base_link通常和它重合或差一个高度,lidar_link挂在base_link下面的固定变换。你需要写一个静态TF发布节点,或者在URDF里定义这个关系。我建议直接在URDF里定义,这样后续跑robot_state_publisher时一起发布,省得多维护一个静态变换程序。

例如,如果你把Mid360装在底盘正中心上方0.35m处:

<joint name="lidar_joint" type="fixed"> <parent link="base_link"/> <child link="lidar_link"/> <origin xyz="0 0 0.35" rpy="0 0 0"/> </joint>

然后启动后:

ros2 run tf2_tools view_frames

用evince打开frames.pdf,检查map、odom、base_link、lidar_link是否都在,父子和旋转关系是否符合你的安装实际。这一步漏了,后面定位漂移、建图糊掉,你根本不会想到是TF的问题。

3. 点云预处理:控制计算量,也控制地图质量

直接拿原始点云去做投影,不是不行,但结果会很糙。Mid360一秒钟出大约40万点,如果不做预处理,转换节点就得每个点都做坐标变换和角度映射,CPU会持续跑高;更麻烦的是,原始点云里包含了很多会让2D地图"变脏"的点,比如地面上的小杂物、头顶上的线缆、车底视角扫到的桌腿底部,这些统统不该出现在导航地图里。

3.1 直通滤波/ROI裁剪与体素降采样

我习惯在转换节点里先做两步预处理,虽然PCL库里有现成的滤波器,但写进ROS2节点里用也很快:

第一步是直通滤波(PassThrough),把明显超出机器人感知意图范围的点丢掉。比如你做室内导航,机器人最关心的是前后左右0.1m到15m范围,那z轴范围设在-0.5到1.2m,x和y轴各设在-15m到15m。这样既切掉了天花板悬挂物,又切掉了地面以下因多路径效应产生的噪点。

第二步是体素降采样(VoxelGrid),把整个空间切成边长为0.03m的小立方体,每个立方体里只保留一个点的位置和强度平均。这一步能把面数降到原来的1/5到1/10,对后续的坐标变换和角度投影是决定性的。特别是Mid360这种近处点集中的传感器,不降采样的话,近处地面点密度大得离谱,投影成LaserScan时会让最近的一两米“栅栏化”。

当然,预处理参数不能拍脑袋。我的建议是先不加任何滤波,直接做一个投影测试,看生成的2D scan在RViz2里长什么样,再逐步加滤波参数,对比前后差异。这样你能直观感受到每种滤波到底滤掉了什么。

3.2 地面移除与障碍物保留的取舍

这个取舍,很多人做反了。2D导航地图要的是"地面上凸起的物体轮廓",而Mid360纵向FOV在-7°到+52°,大部分点都在地面以上,如果机械地把z=0以下全部砍掉,确实能去掉地面点,但同时也会把一部分低矮障碍物比如门坎、小台阶、掉在地上的箱子切掉。

我实测下来的经验是:不要在转换节点里做严格的"地面分割",而应该用高度区间来做"关注区域过滤"。也就是把z范围设成"底盘平面以下0.05m到底盘平面以上0.8m"这样的区间。底盘平面以下留5cm的余量,是为了包容机器人上下坡时的姿态变化;底盘平面以上0.8m,基本能覆盖居家环境中大多数障碍物的上半部分,又不会把天花板下的吊灯、线缆卷进来。

你可以用Mid360自带的IMU数据做姿态补偿,在转换节点里根据当前姿态把高度阈值动态调整。但这个做法对IMU标定有要求,初期可以先做固定阈值,跑通了再优化。

3.3 时间戳同步:TF与时间戳错位引发的"灵异现象"

点云转换最隐蔽的坑,就是时间同步。Mid360以40Hz出点云,TF树中map→odom之间的变换是AMCL以10Hz左右更新的,odom→base_link是轮式里程计以50Hz更新的。如果转换节点拿到点云后,不是在点云自身的那个时间戳下查TF,而是用当前时间来查,就会导致点云位置和机器人位姿对不上——表现就是机器人稍微转个弯,地图上就多出一圈重影或者拉出长条残影

具体写法上,要用tf2的lookupTransform同时传入目标坐标系、源坐标系、点云时间戳三个参数,并且用一个可以容忍小数秒延迟的buffer来等待。设置tf_buffer的缓存时间到2秒以上,lookupTransform时timeout给0.1-0.2秒,基本能规避绝大多数同步问题。

如果某次点云等到超时还是拿不到TF变换,宁可丢弃这一帧,也不要拿最新TF硬算。一帧丢了对整体影响很小,但一帧算错了,代价地图上就多一块脏数据。

4. 从PointCloud2到LaserScan:手写转换节点的保姆级步骤

4.1 原理:把圆柱体展开成一条线

很多人看到"点云转slam"这几个字就默认要用某个现成工具,其实核心原理非常朴素:把以lidar_link为轴心、半径固定范围内的三维点云,按水平角theta归类到一个个角度bins里,每个bin只保留距离最近的点的距离值,就得到了一帧LaserScan。

类比一下,点云是地面上密密麻麻的人群,你站在圆心举着相机转一圈,把每个方向"最先碰到你的那个人"记下来,得到的那个按角度排列的距离数组,就是LaserScan。2D导航要的从来不是"面前有几个人、长什么样",而是"哪个方向能走多远"。

4.2 转换节点关键代码拆解

我习惯用C++写这个节点,因为PointCloud2在C++里可以直接用PointCloud2Iterator遍历点字段,性能比Python快一个数量级。当然你只是想快速验证思路,用Python写一个ros2 py节点也行,下面我用C++把骨架给你。

先说输入输出:

  • 订阅话题:/livox/lidar,PointCloud2
  • 发布话题:/scan,LaserScan
  • TF变换:lidar_link → base_link(或直接保持点在lidar_link系,看你要不要补偿安装偏移)

核心代码大概长这样:

class PointCloudToScanNode : public rclcpp::Node { public: PointCloudToScanNode() : Node("pointcloud_to_scan") { pointcloud_sub_ = this->create_subscription<sensor_msgs::msg::PointCloud2>( "/livox/lidar", rclcpp::SensorDataQoS(), [this](const sensor_msgs::msg::PointCloud2::SharedPtr msg) { pointcloud_callback(msg); }); scan_pub_ = this->create_publisher<sensor_msgs::msg::LaserScan>("/scan", rclcpp::SensorDataQoS()); tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock()); tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_); } private: void pointcloud_callback(const sensor_msgs::msg::PointCloud2::SharedPtr msg) { // 1. 查TF:把点云时间戳下的lidar_link点变换到base_link geometry_msgs::msg::TransformStamped transform; try { transform = tf_buffer_->lookupTransform( "base_link", msg->header.frame_id, tf2::TimePointZero); // 实际用rclcpp::Time(msg->header.stamp) } catch (tf2::TransformException &ex) { RCLCPP_WARN(this->get_logger(), "TF lookup failed: %s", ex.what()); return; } tf2::Stamped<tf2::Transform> tf_transform; tf2::fromMsg(transform, tf_transform); // 2. 构建LaserScan消息头,角度范围是-π到π,角分辨率0.1rad // 3. 遍历点云每个点,过滤掉无效点和超出ROI的点 // 4. 把点从lidar_link系变换到base_link系 // 5. 计算水平角theta = atan2(y, x),映射到对应的bin索引 // 6. 如果该点的欧氏距离 < 当前bin里的最小距离,则更新bin值 } // ... };

有几个细节你一定要处理:

一是角分辨率不能太小也不能太大。0.1rad(约5.7°)在3m外对应的横向分辨率约0.3m,对小障碍物可能漏检;0.05rad会把数据量翻倍,占用的带宽和后续costmap处理开销都会增加。我个人测试下来,室内场景0.1rad够用,室外开阔地0.05rad起步。

二是每个bin的距离初值要设为range_max,否则会出现"全0距离"的假障碍。

三是速度快的机器人要考虑scan的时间累积问题。Mid360一帧是40Hz,也就是25ms一帧,大部分机器人在这个时间尺度内位移不超过5cm,一般不需要做运动畸变补偿。如果底盘速度很快,比如3m/s以上,建议用EKF里程计预测来校正每帧的累积畸变。

4.3 参数选择:高度区间、角分辨率、去噪策略

这里我给出一组我实测可用的初始参数,室内轮式机器人、底盘高度约30cm、Mid360装在底盘中心上方40cm的场景:

参数推荐值说明
min_z-0.05m略低于底盘平面,容纳起伏
max_z0.80m覆盖大多数障碍物,滤掉吊灯线缆
min_range0.10m小于这个距离的点视为噪声
max_range15.0mMid360有效探测范围
angle_min / angle_max-3.14159 / 3.14159全向扫描
angle_increment0.1rad室内够用,室外建议0.05
scan_time / time_increment0.025s / 0.0s对应40Hz频率

去噪策略上,除了直通和体素降采样,我还会在生成scan后做一次中值滤波,窗口3个bin。这能有效压制单点噪点产生的"毛刺",又不会把真实的细柱状障碍物(比如桌腿)抹掉。拖着不做滤波直接建图,你会在2D地图上看到一堆"雪花点",后期清洗工作量更大。

5. 用slam_toolbox构建栅格地图,并验证质量

scan这个话题已经出来了,接下来就是用2D SLAM去构建栅格地图。ROS2 Humble的导航栈里最常见的选择是slam_toolbox,它继承了ROS1时代KartoSLAM的思路,地图质量和实时性都比较均衡,不挑传感器模型,非常适合这种由点云投影出来的合成scan。

5.1 建图配置文件怎么写

以slam_toolbox为例,我常用的配置如下:

slam_toolbox: ros__parameters: pose_quantize: 0.02 scan_match_size: 12 minimum_time_interval: 0.25 resolution: 0.05 max_laser_range: 12.0 map_update_interval: 2.0 use_scan_matching: true use_scan_barycenter: true minimum_travel_distance: 0.0 minimum_travel_heading: 0.0 scan_buffer_size: 30 link_scan_maximum_distance: 4.0 loop_match_minimum_chain_size: 10 loop_match_maximum_variance_coarse: 3.0 loop_match_minimum_response_coarse: 0.35 loop_match_maximum_variance_fine: 0.06 loop_match_minimum_response_fine: 0.65

这里面有两个参数值得单独说:

  • resolution: 0.05,意思是每格5cm。Mid360的近处点密度完全可以支撑2.5cm分辨率的栅格图,但地图文件大小会变成4倍,Nav2的代价地图层加载、膨胀计算的开销也变大。对于大多数室内导航,5cm是"看起来够用又不拖后腿"的甜点值。
  • max_laser_range: 12.0,我故意不设到15m。Mid360的远距离点噪声相对大,建模时如果让SLAM去匹配远距离稀疏点,反而可能引入累积误差。12m对于室内环境已经绰绰有余,走廊宽度一般不超过3m。

5.2 遥控移动建图与地图保存

启动建图时,我通常先用这个launch命令:

ros2 launch slam_toolbox online_async_launch.py slam_params_file:=/你的路径/slam_toolbox_params.yaml

然后遥控底盘慢慢走。这里有一个关键节奏:别急着转圈,别急着走快。SLAM的核心是相邻两帧scan的配准,走快了,相邻帧之间重叠度低,匹配结果容易滑到局部最优,地图上就会出现错位和重影。我建图时习惯让机器人先沿墙走一整圈,再在房间中部做"S"型穿插,最后回到起点附近。这样既能覆盖大部分空间,又给回环检测创造了条件。

建图完成后保存地图:

ros2 run nav2_map_server map_saver_cli -f ~/maps/my_map

这条命令会在指定路径生成my_map.pgm和my_map.yaml两个文件。打开PGM文件用肉眼看一遍,这一步比任何指标都直观。正常的地图应该黑白分明,墙壁是连续闭合的黑线,没有"拖尾"或者"双线"。

5.3 常见地图缺陷与补救

实测中地图出现最多的问题有三类:

第一类是走廊尽头或大房间中间出现"缺失"——那是因为slam跑着跑着掉了帧或者走了从未匹配的远点,导致空地变成灰色未知区。补救办法是回到那段区域重新走一遍,不用改参数。

第二类是某一个方向延展出去的长廊歪了,甚至角度整体偏了十几度。这是典型的累计误差没被回环纠正。解决办法有两种:一是保证回环闭环,尽量走成一个圈;二是调低minimum_travel_distance和minimum_travel_heading,让SLAM在转弯幅度很小时也持续优化位姿图。

第三类是**"双墙"问题**,地图上同一面墙出现两条平行黑线。这通常出现在走廊中段,前后两段建图时的姿态估计误差较大,拼接成栅格图后就成了双墙。我遇到这种情况第一反应是检查TF树里的lidar_link安装角度有没有微小的偏转——哪怕偏1°,远距离都能拉出明显双墙。其次才是怀疑SLAM参数问题。

6. 导航阶段:Nav2如何吃下这张2D地图

地图建好之后,整条链路从"建图模式"切换到"导航模式",Nav2正式接手。

6.1 navigation2 launch配置

Nav2的配置核心是nav2_params.yaml。先看地图文件路径是否对得上,然后需要重点确认几个和我们的转换链路强相关的参数。

首先是amcl:

amcl: ros__parameters: use_sim_time: false scan_topic: /scan base_frame_id: base_link odom_frame_id: odom transform_tolerance: 0.5 initial_pose: x: 0.0 y: 0.0 yaw: 0.0

scan_topic已经变成了我们的合成scan,base_frame_id和odom_frame_id必须和TF树严格对应。如果这里写错了,AMCL会报"Frame base_link does not exist"之类的错误。

接着是costmap的全局和局部代价地图。global_costmap的robot_base_frame用base_footprint,而local_costmap建议直接用base_link,减少一次坐标变换的延迟。

启动Nav2:

ros2 launch nav2_bringup bringup_launch.py map:=/你的路径/my_map.yaml params_file:=/你的路径/nav2_params.yaml

启动后RViz2里加上RobotModel、Map、ParticleCloud、Path这几个显示组件,然后手动2D Pose Estimate给一个初始位置,再用2D Goal Estimate下发目标点,看机器人能不能平滑地绕开障碍并到达。

6.2 costmap的传感器配置与避障

Nav2的实际避障并不直接读scan,而是通过costmap的obstacle layer把传感器数据转成栅格代价,再做膨胀。这里有一个非常容易踩的坑:一旦我们喂给Nav2的是合成scan,它就只有2D一个平面。如果机器人底盘上方的悬空障碍物(比如桌板、门框下沿)突出到导航层的高度区间之外,Nav2是完全看不出来的。

所以我把costmap的obstacle layer参数单独拎出来调:

obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" enabled: True observation_sources: scan scan: topic: /scan sensor_frame: lidar_link observation_persistence: 0.0 expected_update_rate: 20.0 data_type: "LaserScan" clearing: True marking: True max_obstacle_height: 0.8 min_obstacle_height: -0.05

max_obstacle_height和min_obstacle_height必须和转换节点的高度过滤区间保持一致,framework不一致costmap可能直接把点当成"无需关注"或"整块都是障碍物"。

如果你觉得一个平面的scan不够,担心机器人撞到"桌面以下、身位以上的悬空物",一个较为省事的思路是:在转换节点里多生成几个不同高度区间的scan,分别作为独立的observation source喂给costmap。比如一个scan用-0.05到0.35m区间做地面层障碍识别,另一个scan用0.35到0.8m区间做上层障碍识别。Nav2会让你配置多个topic,两个source各扫各的,互不干扰。

6.3 AMCL定位抖动处理

AMCL的常见问题是机器人原地旋转时,粒子云会发散。Mid360合成scan的角分辨率不如原生机械雷达均匀,在空旷区域远点少,匹配源不够,粒子权值会乱跳。

我的处理方案有两个:一是把amcl的update_min_a设为0.2到0.5,降低角速度带来的频繁更新;二是把transform_tolerance从0.2放宽到0.5,给TF buffer更多等待时间。这两个改动对多数室内场景能让定位稳定很多。

另外要提醒的是,AMCL的初始位置一定要给准。建图完成后重新上电,扫描匹配和地图都加载好了,但如果没有给定初始位置,粒子云会散在地图各处,要等很久才能收敛。在RViz2里用2D Pose Estimate点击机器人的真实位置,这是导航链路里最容易出问题也最容易解决的一环。

7. 实际跑下来总结的避坑清单

最后分享几段我自己的调试经验,都是一行一行的代码里趟出来的。

第一,TF的时间戳一定要用点云时间戳,而且lookupTransform的timeout要给足。有一次我图省事直接用了rclcpp::Time(0),lookupTransform返回的是"最近一帧可用TF",结果就是机器人一转弯,地图就糊一次。改成带时间戳的查询后,问题当场消失。

第二,体素降采样和直通滤波的顺序不要颠倒。先降采样后直通,会让ROI边缘出现一些"方块状"的空洞;先直通后降采样,边缘点虽然还在,但数据量比前者少30%到50%。实际效果差别不小,建议先在转换节点里做ROI裁剪,再降采样。

第三,scan消息的intensity字段可以不用管,但frame_id绝对不能写错。有时候NodeHandle的frame_id没设置好,发出去的scan还挂着livox_frame,Nav2在收到消息后会尝试做livox_frame到base_link的坐标变换,明明只有5cm的安装偏移,硬是给你在代价地图上转出一个弧形的伪障碍物。这个问题排查方式很直接:rviz2里显示TF,看一下发布者frame_id和sensor_frame是否一致。

第四,也是最容易忽视的一点:slam_toolbox建图结束后,一定要在看门狗里保存地图之后再把里程计置零。有一次我建完图急着重启程序,里程计累积误差还没被闭环完全抹掉,保存下来的地图起点位置和真实起点差了30cm,后面导航全部偏着走。后来我养成一个习惯:地图保存后,立即把机器人搬回建图起点位置,用rviz2的2D Pose Estimate手动校一次初始位姿,整个导航流程才彻底顺了。

整个链路拆下来,其实每一环都没有特别深奥的地方,难的是把TF、点云预处理、scan生成、SLAM、AMCL、costmap这些环节串起来,并且知道每一步哪里会坑你。你按这个教程走一遍,应该半天左右能跑通最短闭环:Mid360通电 → RViz2看到点云 → /scan话题出现 → slam_toolbox建图 → Nav2导航。后面要提升地图质量或导航稳定性,大概率也就是在这些参数之间来回调一调,思路已经入门了。

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

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

立即咨询