最近又被问了好几次:FAST_LIO_ROS2 能不能用 Gazebo 仿真?我的答案是能仿,但要先把预期摆正——在 Gazebo 里把这条数据链路跑通、把参数调明白,是很现实的需求;可你要是指望着仿真里跑出来的建图精度能直接等价于真机表现,那多半要失望。这篇文章把我从零搭环境的完整过程、卡过的地方、以及最后对“仿真到底有什么用”的理解都写出来,给正准备在 FAST_LIO_ROS2 上玩仿真的人一个直接能抄的作业。
先说结论:Gazebo 完全可以用作 FAST_LIO_ROS2 的仿真平台,前提是你接受两件事——第一,你得花时间把点云、IMU、TF、时钟这堆外围东西捋顺,算法本身反而不是最难的那一环;第二,仿真里的精度曲线只能用来验证逻辑,不能用来标榜真实性能。
1. 先说结论:能跑,但别把它当成“装上就能出图”的事
1.1 为什么很多人装完就放弃了
不少人在拿到 FAST_LIO_ROS2 以后,第一反应是把它塞进 Gazebo 里,却发现事情远没有想象中顺利。不是编译报错,就是雷达没点云,再不然就是算法跑起来几秒钟直接发散。问题通常不在算法本身,而在仿真环境对传感器数据链路的还原方式。
我见过最多的失败案例是这样的:用 Gazebo 默认的 ray 传感器给机器人装了个“雷达”,结果发现它发布的是sensor_msgs/LaserScan,而 FAST_LIO 要的是sensor_msgs/PointCloud2;数据接不上,自然一张图都建不出来。还有人把 IMU 插件的更新频率留在默认的 100Hz,FAST_LIO 前端跑两步就飘,跑三步就 NaN。
这一系列问题拼在一起,给人的第一印象就是“FAST_LIO 不适合仿真”。但实际不是不适合,是仿真环境里缺少一套能正确模拟激光雷达和 IMU 的组合方案。
1.2 什么场景下适合用仿真,什么场景下不适合
我自己的判断很简单,仿真适合干这三件事:
- 验证代码改动有没有破坏基本流程,比如改了外参标定逻辑、换了点云过滤策略;
- 研究退化环境定位,比如把机器人丢进一条只有纵深的走廊,观察 FAST_LIO 的退化防御是否生效;
- 批量跑参数实验,比如在不同点云频率、不同 IMU 噪声水平下观察算法的鲁棒性。
不适合干的事也很明确:在仿真里测绝对定位精度、测运动畸变补偿效果、测纹理退化场景。这些问题在仿真里要么不存在,要么被过度理想化,测出来的数字没有参考价值。
2. 仿真难点不在算法,在点云和 IMU 这条输入链路上
2.1 FAST_LIO 真正需要什么输入
FAST_LIO 本质上是紧耦合的激光惯性里程计,它需要的核心输入就两个:高频 IMU 数据和三维激光点云。但这两个东西背后还有一堆隐性要求。
IMU 必须有足够的频率和稳定时间戳,否则前端状态传播会失真,后端优化无法收敛。激光点云必须是带强度的三维点,而且点的坐标要在一个明确的雷达坐标系里。最后,IMU 和雷达之间的外参——也就是extrinsic_T和extrinsic_R——必须和机器人描述文件里定义的关节位置完全一致。
这套要求在真机上是靠硬件驱动和标定来满足的,在仿真里则要靠 Gazebo 插件和 URDF 模型的正确组合来满足。很多人仿真失败,恰恰是因为不了解这些隐性要求。
2.2 Gazebo 原生雷达输出的那个坑
Gazebo classic 自带的 ray 传感器,最常用的输出是sensor_msgs/LaserScan。LaserScan 是一维距离数组,本质上只能描述一个扫描平面,哪怕你配置了多条垂直射线,gazebo_ros_ray_sensor插件最后输出的 LaserScan 也无法完整表达多线束的三维信息。
FAST_LIO 需要的是三维点云,这意味着你需要一个能直接输出PointCloud2的雷达仿真插件。最省事的办法是使用 Velodyne 模拟器,即velodyne_simulator包。这个包里有 Velodyne 的模型描述和 Gazebo 插件,能够按照真实 Velodyne 雷达的扫描方式生成三维点云,并直接发布成/velodyne_points话题,类型就是sensor_msgs/PointCloud2。
这也是我在整套方案里选择 Velodyne 模拟器而不是手写 ray 传感器的原因:少写转换代码,数据格式天然匹配,对后续调参也友好。
2.3 点云时间戳是仿真和真机最大的分水岭
FAST_LIO 在做运动补偿时,会假设点云里的每个点带有相对扫描起点的偏移时间。真机雷达的驱动通常会在点云里填充精确的点级时间戳,这样算法才能根据当前速度把每个点投影到同一时刻。
但 Gazebo 里的大多数雷达插件,包括 Velodyne 模拟器,生成的PointCloud2往往只有一个扫描级的时间戳,所有点的时间偏差是同步的,或者根本没有逐点时间。这在机器人静止或低速运动时问题不大,一旦快速旋转或急加减速,运动补偿就会失真,建图出现重影、拖尾。
所以在仿真里看地图质量的时候,要先问自己一个问题:机器人运动速度快不快?如果快,地图重影大概率来自时间戳失真;如果慢,地图还重影,再去查外参和 IMU 噪声。
3. 实操:在 Ubuntu 22.04 + Humble 下把环境搭起来
3.1 版本选择:不要一上来就追新
先说版本,我在这个项目里用的是 Ubuntu 22.04 + ROS2 Humble + Gazebo classic 11。这个组合是目前 FAST_LIO_ROS2 社区里验证最多、资料最多的环境。
如果你用的是 Ubuntu 24.04 + Jazzy,系统默认的是 Gazebo Harmonic,gazebo_ros_pkgs 的插件机制和 classic 差别很大,FAST_LIO_ROS2 在 Jazzy 下的编译也容易碰上新版本 API 变更的问题。不是说完全跑不了,而是你会花大量时间在解决环境问题上,而不是在调试算法。等你在 Humble 上把整条链路跑熟了,再考虑迁移也不迟。
3.2 编译 FAST_LIO_ROS2 的完整依赖与坑
创建一个工作空间,把 FAST_LIO_ROS2 放进去:
mkdir -p ~/fastlio_ws/src cd ~/fastlio_ws/src git clone <FAST_LIO_ROS2仓库地址> --recursive cd ~/fastlio_ws编译前需要装好这些依赖:
sudo apt install libeigen3-dev libpcl-dev sudo apt install ros-humble-pcl-ros ros-humble-eigen3-cmake-moduleFAST_LIO_ROS2 一般会依赖livox_ros_driver2的消息定义,仓库里的 submodule 如果拉不下来,编译时会报找不到 livox 相关的头文件。这时候单独去把livox_ros_driver2克隆到 src 目录下和 FAST_LIO_ROS2 一起编译是一个比较稳妥的办法。
编译过程中常见的坑还有两个。一个是 Eigen 版本冲突,如果系统里存在多版本 Eigen,CMake 可能抓到旧版,导致alloca相关报错,建议把系统 Eigen3 升级到较新版本。另一个是 PCL 头文件路径问题,Ubuntu 自带的 PCL 版本通常没问题,但如果你之前装过别的点云库,要注意去 CMakeLists 里确认实际链接的是哪一个。
编译命令本身很简单:
colcon build --symlink-install source install/setup.bash如果livox_ros_driver2子模块没弄好,colcon build大概率会报错。这时候不要急着怀疑代码,先看完整日志,重点搜索livox和Could NOT find关键字。
3.3 用 Velodyne 模拟器拿到 PointCloud2
把velodyne_simulator加进工作空间:
cd ~/fastlio_ws/src git clone <velodyne_simulator仓库地址> cd ~/fastlio_ws colcon build --symlink-install这个包里包含了 Velodyne 的描述文件和 Gazebo 插件。接下来在你的机器人 URDF 里挂一个 Velodyne 模型,比如 VLP-32C,插件就会自动发布/velodyne_points。你可以在启动后先用命令测一下话题:
ros2 topic hz /velodyne_points ros2 topic echo /velodyne_points --once能看到点云持续以 10Hz 左右发布,说明雷达链路已经通了。这一步跑通后,后面 FAST_LIO 能不能出图,大概率就只剩参数和 TF 的问题了。
4. 建一个不那么“假”的仿真世界:雷达模型与特征环境
4.1 机器人模型怎么搭最省事
我建议把模型拆成三个 link:base_link、imu_link、velodyne_link。IMU 和雷达都通过固定关节挂到base_link上,不要省掉中间的 link,否则 Gazebo 插件挂载传感器时容易出现参考系混乱。
下面是一段精简的 URDF 结构:
<link name="base_link"/> <link name="imu_link"/> <joint name="base_to_imu" type="fixed"> <parent link="base_link"/> <child link="imu_link"/> <origin xyz="0 0 0.05" rpy="0 0 0"/> </joint> <link name="velodyne_link"/> <joint name="base_to_velodyne" type="fixed"> <parent link="base_link"/> <child link="velodyne_link"/> <origin xyz="0 0 0.10" rpy="0 0 0"/> </joint>然后在这个 URDF 里引入 Velodyne 描述,frameName一定要设置成velodyne_link,跟上面的关节子坐标系名称保持一致。否则插件发布点云时带了一个错误的 frame id,FAST_LIO 在 TF 树里根本找不到雷达坐标系。
4.2 IMU 要调到 400Hz,否则发散只是时间问题
FAST_LIO 的紧耦合前端依赖 IMU 高频读数来做状态传播,Gazebo 的 IMU 插件默认更新频率经常在 100Hz 左右,这对纯视觉或低频导航够用,但对激光惯性里程计来说就是灾难。
我在 URDF 里给 IMU 插件加了这么一组配置:
<gazebo reference="imu_link"> <sensor name="imu_sensor" type="imu"> <always_on>true</always_on> <update_rate>400</update_rate> <ros> <namespace>/</namespace> <remapping>imu:=imu/data</remapping> </ros> <plugin filename="libgazebo_ros_imu.so" name="imu_plugin"/> </sensor> </gazebo>update_rate调到 400Hz 后,CPU 占用会有所上升,但这个代价是值得的。如果你发现 Gazebo 运行变卡,优先降低世界里的模型数量,而不是调低 IMU 频率。
另外,仿真 IMU 的噪声不能完全设成零。太干净的 IMU 数据会让 FAST_LIO 的高斯滤波过于信任 IMU,真机上遇到振动噪声会立刻发散。建议给 IMU 加一点高斯白噪声,量级不需要完全模拟真机,只要别把噪声设成零就行。
4.3 世界文件里要有足够的几何特征
FAST_LIO 本质上是靠特征收敛的,如果 Gazebo 世界里只有一个空荡荡的房间,算法分分钟退化。我第一次测试时就犯了这个错误,结果机器人在房间里转圈,地图不仅没有闭合,还出现了明显的漂移。
后来我在世界里加了几根柱子和几个箱子,收敛立刻稳定。你不需要做一个特别复杂的场景,几个有棱角的几何体就够了:
- 0.4m x 0.4m 高 1.2m 的柱子,摆四根在房间四个角落;
- 1.0m x 1.0m x 1.0m 的箱体,随机放在房间中部;
- 一面带开口的墙,让机器人可以穿行,制造非平面结构。
在 world 文件里加一个柱子模型大概长这样:
<model name="column_01"> <pose>3 2 0.6 0 0 0</pose> <link name="link"> <collision> <geometry> <box><size>0.4 0.4 1.2</size></box> </geometry> </collision> <visual> <geometry> <box><size>0.4 0.4 1.2</size></box> </geometry> </visual> </link> </model>如果你不想自己造世界,也可以直接加载 Gazebo 自带的带物体场景,再手动塞几个 box 进去,效果差不多。
5. 跑通后的第一轮调参:时间同步、配置项与发散排查
5.1 先解决时钟:use_sim_time 不是可选项
FAST_LIO 对消息时间戳的一致性非常敏感。Gazebo 发布的所有传感器消息,时间戳用的都是仿真时钟,而 FAST_LIO 默认用的是系统时钟,两者不同步会导致 TF 疯狂报错,算法也会原地转圈。
启动时记得加这一句:
ros2 launch fast_lio mapping.launch.py use_sim_time:=true或者在 launch 文件里对所有节点设置use_sim_time: true。你可以用命令验证一下时钟是否生效:
ros2 topic echo /imu/data --once看消息头里的时间戳是否持续增长并且接近当前仿真时间。如果时间戳乱跳,那后面的一切都不用谈。
5.2 对表配置文件:lidar_type、话题名、外参
FAST_LIO_ROS2 的配置文件默认叫类似avia.yaml的名字,拉到 Gazebo 仿真里主要是改这几个项。
common: lid_topic: "/velodyne_points" imu_topic: "/imu/data" lidar_type: 2 scan_line: 32 blind: 0.5 det_range: 50.0 point_filter_num: 4 time_sync_en: falselidar_type: 2表示输入的是标准PointCloud2,而不是 Livox 的CustomMsg。这个不改的话,算法拿到的数据类型对不上,大概率直接崩。scan_line要跟你的雷达模型线数一致,我用的是 32 线,所以填 32。
blind是盲区距离,仿真里雷达近处很容易产生密集且无意义的点,适当调大盲区能让优化更稳定。point_filter_num是抽稀参数,点数太多时把它设大一点,可以明显降低 CPU 占用。
外参extrinsic_T和extrinsic_R也要和 URDF 里的关节位姿对应。如果你在 URDF 里把 IMU 放在 base_link 上方 5cm 处,雷达放在 10cm 处,那外参矩阵就应该是这两个坐标系的转换关系。外参错误的表现很有意思,地图通常是整体扭曲的,而不是完全发散,这种地图一眼就能看出来不对劲。
5.3 跑起来后怎么看效果
启动 RViz2,把 Fixed Frame 设置成map或者 FAST_LIO 输出里程计的坐标系,然后订阅这些话题:
/cloud_registered:配准后的点云,应该和地图逐渐贴合;/Odometry:机器人当前位置,通常用一个箭头或机器人模型表示;/path:历史轨迹,方便观察漂移。
如果地图里的墙体清晰锐利、轨迹平滑,说明整条链路已经通了。如果墙体边缘模糊、轨迹抖动,先不要急着调 FAST_LIO 内部的滤波参数,回头检查 IMU 频率和点云时间戳,大概率是外围数据链路的问题。
6. 我踩过的坑:从界面闪烁到仿真发散,一条条说
6.1 Gazebo 界面一直闪,跟算法没关系
网上很多人遇到 Gazebo 界面闪烁的问题,我当时也遇到了。表现形式是图形窗口里的场景不断闪烁、卡顿,有时候连鼠标交互都成问题。这个问题的根源几乎都是渲染后端与硬件驱动不匹配,尤其是在虚拟机或者双显卡笔记本上。
如果你用的是虚拟机,最直接的办法是强制软件渲染:
export LIBGL_ALWAYS_SOFTWARE=1 gazebo --verbose这样虽然帧率低一些,但界面不会再闪。如果是在双显卡笔记本上,可以手动指定 Gazebo 使用独立显卡运行。还有一条路是不用 GUI,直接用gzserver跑世界,然后用 RViz 看传感器数据,能绕开 Gazebo 界面的大部分渲染问题。
6.2 点云没输出:先查模型、再查插件、最后查算法
雷达话题没有任何数据时,很多人第一反应是去怀疑 FAST_LIO 的配置,但问题很可能出在模型本身。
我的排查顺序是:
- 用
ros2 topic list看/velodyne_points是否存在; - 如果存在但 hz 为 0,检查 Velodyne 模型是否真的挂在了机器人上;
- 如果话题根本不存在,去检查 URDF 里传感器的 plugin 是否加载成功;
- 打开 Gazebo 左侧的插入模型列表,确认 Velodyne 模型是否在仿真场景中生成。
插件加载失败时,Gazebo 终端通常会打印一行找不到共享库或者找不到模型的警告,回到终端看输出,比在代码里猜要快得多。
6.3 跑着跑着发散成 NaN:按这个顺序排查
仿真中最让人崩溃的是算法跑了几秒钟后,位姿变成 NaN,地图瞬间消失。我总结了一个排查顺序,按这个顺序查基本能覆盖大部分问题:
| 场景 | 可能原因 | 反查方法 |
|---|---|---|
| 启动即发散 | IMU 频率过低 | /imu/datahz 是否达到 300Hz 以上 |
| 启动后几秒发散 | use_sim_time 没开 | 检查消息时间戳是否和仿真相匹配 |
| 地图重影严重 | 点云时间戳失真 | 降低机器人运动速度,观察重影是否消失 |
| 地图整体扭曲 | 外参错误 | 对照 URDF 和 yaml 中的位姿 |
| 偶尔跳跃但能恢复 | 世界特征不足 | 增加几何体、减少长直走廊段 |
NaN 的根源往往不是算法本身,而是输入数据里混入了 Inf 或 NaN 点。Velodyne 模拟器在某些极端角度下可能生成无效点,如果不放心,可以在点云发布后加一个过滤节点,把非有限值点直接去掉,再喂给 FAST_LIO。
6.4 TF 时间戳原地转圈:基本是仿真时钟问题
RViz 里报No transform from map to base_link,或者Could not determine the reference frame,通常是因为仿真里的 TF 时间戳和算法期望的时钟基准不一致。
这个问题的排查很简单:先确认/use_sim_time是否为 true,再确认gzserver和 FAST_LIO 节点是否运行在同一个 ROS 环境。只要时钟基准统一,90% 的 TF 问题都能消失。
7. 仿真结果到底能不能代表真机表现
7.1 仿真能验证什么,验证不了什么
我把这套环境跑通以后,很快意识到一个事实:仿真里地图建得再干净,也不能说明算法在真机上就稳。
仿真中激光雷达没有多路径干涉、没有低反光材质、没有日光过曝,IMU 噪声模型也远比真机简单。FAST_LIO 在仿真里表现良好,只能说明你的数据链路、外参、参数配置没有原则性问题,不能说明它在复杂真机场景里能打赢同样的仗。
7.2 想认真验证精度,用官方数据包回放
如果你真的要评估 FAST_LIO 的精度和鲁棒性,最好的办法是在真机数据上做回放验证。FAST_LIO 官方仓库提供了多组真实传感器采集的数据包,里面包含车载激光雷达、IMU 原始数据以及真值轨迹。
用法很简单,先把数据包挂到工作空间,用ros2 bag play或者对应格式的播放工具,把它当成实时话题发布,再启动 FAST_LIO 的 mapping 节点。你就能在真实 IMU 噪声、真实雷达畸变下观察算法表现,这个结果才有说服力。
7.3 一套推荐的验证流程
我自己现在用的是三段式流程,分享出来供参考:
- Gazebo 仿真阶段,验证代码逻辑、数据链路、参数边界;
- 官方数据包回放阶段,验证精度、鲁棒性和调参方向;
- 真机小范围实验阶段,在简单环境下跑通全流程。
这个流程的好处是每一层过滤掉的问题不同。仿真解决的是“能不能跑通”,数据包解决的是“算法本身行不行”,真机解决的是“整套系统现场稳不稳”。每一层都省了下一层大量时间,尤其是当你频繁改代码、掺参数的时候,先跑仿真能少走很多弯路。
FAST_LIO_ROS2 和 Gazebo 的组合是一个非常有价值的调试平台,很多人一开始望而却步,是因为信息太零散,各个教程之间对不上。希望这篇文章能帮你在仿真里少踩几个坑,把时间花在真正有意义的算法调试上。