做机器人SLAM的兄弟,应该没几个不知道FAST-LIO的。从港大MaRS实验室开源到现在,这套基于迭代误差状态卡尔曼滤波器的激光惯性里程计算法,几乎成了中高端激光雷达方案的标配。这两年我一直在折腾它的ROS2版本,从最开始的编译报错到后来拿着MID-360在园区里跑闭环,踩过的坑能写一本小册子。
今天这篇就把FAST-LIO的ROS2版本掰开揉碎讲清楚:先讲算法核心在解决什么问题,再讲从ROS1搬到ROS2要跨过哪些坎,最后给一套可以直接抄作业的部署流程,包括常见问题的排查思路。无论是刚接触ROS2的菜鸟,还是已经在用ROS1版本做项目的老手,这篇文章应该都能帮你省下不少折腾时间。
1. FAST-LIO在做什么——先用大白话讲清楚
1.1 激光雷达加IMU,为什么要紧耦合
先说结论:单靠激光雷达做里程计,在快速旋转、剧烈颠簸这种场景下,点云畸变能把地图糊成一团。单靠IMU做积分,漂移又大得没法看。FAST-LIO的思路就是把两者“捆”在一起,用IMU的高频数据来预测运动,用激光雷达的点云来修正预测误差,两边互相纠正。
打个比方:IMU像那个开车时总觉得自己没走偏的司机,而激光雷达像是车顶上的全景摄像头。司机凭感觉开一段,摄像头每隔一段时间纠正一下方向。FAST-LIO厉害的地方在于,它不是简单地“先IMU积分、再点云匹配”,而是把IMU的状态估计和激光雷达的配准过程放到同一个迭代框架里,每一帧点云都参与到状态修正中。这种紧耦合的方式,让它在高速运动、低纹理环境下比松耦合方案稳得多。
1.2 它和LOAM、LIO-SAM这些方案差在哪
你可能听过LOAM或者LIO-SAM,简单区分一下:早期方案大多是“特征提取 + 扫描匹配”的路线,先把点云里的角点、平面点挑出来,再做帧间或帧到地图的配准。这套思路在结构化环境里很能打,但到了草丛、树林这种特征稀疏的地方,特征提取不够用,匹配就容易跑飞。
FAST-LIO走的是直接配准的路线,尤其是FAST-LIO2之后,干脆不做特征提取,直接把降采样后的原始点云和局部地图配准。好处是算法对场景的适应能力上了一个台阶,田间地头、矿区隧道这类环境都能顶得住。而ROS2版本最大的改变不在算法本身,而是整个工程从ROS1的消息框架迁移到了ROS2的DDS通信框架,这带来的连锁反应我之前没意识到,后面会专门讲。
2. 算法核心:迭代误差状态卡尔曼滤波器是怎么运转的
2.1 误差状态是什么,为什么要绕这个弯
FAST-LIO的状态估计核心是IESKF,也就是迭代误差状态卡尔曼滤波器。先把“误差状态”这个概念讲明白。
整体状态一般包括位置、速度、姿态、陀螺仪零偏、加速度计零偏、重力向量。如果直接对这些量做卡尔曼滤波,姿态这东西用欧拉角会有万向锁问题,用四元数又有归一化约束,更新的时候处理起来很麻烦。误差状态滤波器的思路是:把状态拆成“名义状态”和“误差状态”两部分。名义状态用粗略的IMU积分来推进,误差状态则是实际状态和名义状态之间的小偏差,卡尔曼滤波只对这个偏差做估计和修正。
这样绕一圈的好处很明显:误差量是小量,线性化精度高,而且误差状态可以用简单的向量加减来更新,不用处理四元数那种复杂的运算规则。每次迭代修正完误差状态,再把它叠加回名义状态,就完成了新一轮的状态更新。这个“绕弯”的做法,是FAST-LIO数值稳定性好的重要原因。
2.2 IESKF的预测、迭代更新和点云配准是怎么串起来的
整个算法流程可以分成三个大步骤。第一步是IMU传播:拿到IMU的角速度和加速度后,对状态做高频预测,同时更新协方差矩阵。这一过程大约在几百赫兹的频率下运行,确保状态估计不会因为激光点云帧率低而滞后。
第二步是点云去畸变与配准。激光雷达扫描一帧需要时间,期间机器人是运动的,所以每个激光点的坐标都带着运动畸变。FAST-LIO利用当前的状态估计,把每个点投影到统一坐标系下,消除畸变影响。然后对去畸变的点云做体素降采样,再和局部地图进行最近邻匹配,计算残差。这里的残差本质上是一个点到面的距离,反映了“用当前状态去解释点云观测”的吻合程度。
第三步是迭代更新。这是和传统卡尔曼滤波最大的区别。常规EKF只做一次线性化和更新,而FAST-LIO会把“匹配-计算残差-更新状态”这个过程重复多次。每迭代一次,状态估计更准,点云去畸变就更准,匹配的残差就更小,如此循环,直到收敛或达到迭代次数上限。这种迭代反馈结构,让算法在大机动飞行、高速平移时也能保持稳定。
2.3 反向传播:FAST-LIO里一个容易被忽略的细节
再提一个细节:在IESKF里,点云去畸变不是孤立进行的,它和状态更新是联动的。因为激光点的时间戳各不相同,每个点对应的机器人位姿也不同。为了计算点云残差对状态的雅可比矩阵,需要把每个点在扫描周期内的位姿变化,通过后向传播的方式关联到当前状态估计上。
你可以把它理解为:不是先单独把点云配准好,再拿配准结果去更新状态,而是把“点云畸变补偿”和“状态修正”放进同一个优化问题里,每个激光点都带着自己的时间戳信息参与残差计算。这样做的数学推导会复杂不少,但换来的是更精确的时间对齐,避免了传统方案里“先补偿畸变、再匹配”这种串行流程带来的误差累积。这也是FAST-LIO在剧烈运动下不容易发散的原因之一。
3. 从ROS1搬到ROS2,算法之外还藏着哪些坑
3.1 消息类型与驱动适配:Livox CustomMsg的迁移
如果你用的是Livox的雷达,比如MID-360或者Avia,会发现驱动从livox_ros_driver换成了livox_ros_driver2,这里面最大的变化之一就是自定义消息类型的处理。FAST-LIO的核心订阅话题是Livox点云的自定义消息类型,而不是ROS标准的sensor_msgs/PointCloud2。在ROS1时代,自定义消息的编译和使用相对随意。到了ROS2,消息类型的包管理、接口导出和编译依赖关系都变得更加严格。
实际操作中要注意:livox_ros_driver2的消息定义通过rosidl_generate_interfaces生成,如果你的工作空间里既有驱动包又编译FAST-LIO,必须保证两个包的消息依赖关系正确。最常见的问题是找不到livox_ros_driver2/msg/CustomMsg,多半是编译顺序不对,或者没有把livox_ros_driver2的安装路径加到环境的AMENT_PREFIX_PATH里。
3.2 QoS与DDS:为什么有时候收不到数据
ROS2最大的变化是底层的DDS通信机制,而DDS里的QoS策略,是很多人从ROS1转过来后遇到的第一个拦路虎。ROS1的通信模型是“发送方广播、接收方监听”,只要话题名字对上就能收到。ROS2里,如果发送方和接收方的QoS策略不匹配,数据根本不会传输。
FAST-LIO的ROS2版本中,IMU话题一般用SensorDataQoS,点云话题根据驱动不同可能使用不同的QoS策略。如果你看到rviz2里没有点云,ros2 topic echo也收不到任何消息,大概率就是QoS不匹配。排查方式很简单,用ros2 topic info /livox/lidar --verbose看看发送方的QoS,再去源码里确认订阅方的QoS,两者对不上就改成一致的。这个坑我在第一次运行时就踩了,当时差点以为驱动没装好。
3.3 launch文件与TF管理的变化
ROS2的launch系统也全面重写了,基于Python的launch文件在灵活性和复用性上更强,但语法和ROS1的launch差距很大。FAST-LIO的ROS2 launch文件通常要完成这样几件事:加载yaml参数、启动FAST-LIO节点、执行静态变换发布(把IMU坐标系和雷达坐标系关联起来)。
特别提醒一下,使用bag包回放数据验证时,一定要在launch里设置use_sim_time为True,并让所有节点都使用仿真时钟,否则IMU和点云的时间戳会乱套,表现出的特征就是点云剧烈跳动或者算法直接罢工。这里涉及的时间同步问题在第五节还需要重点说。
4. 实战部署:从零开始把FAST-LIO的ROS2版本跑起来
4.1 环境准备与依赖安装:Ubuntu 22.04加ROS2 Humble
我选择的组合是Ubuntu 22.04加ROS2 Humble,这是目前ROS2各发行版里资料最多、兼容性最好的组合之一。开始之前,先把ROS2基础环境装好并完成source。需要注意,编译FAST-LIO前要确保以下依赖已安装:Eigen3、PCL、livox_ros_driver2。不同分支可能还会要求OpenCV、yaml-cpp等,最好按仓库README列出的依赖逐项安装,不要跳过。
如果你完全是从零开始的ROS2新手,建议先不要碰FAST-LIO,而是先跑通ROS2自带的小乌龟例子,搞清楚话题、节点、工作空间编译这些基本概念。不然如果在环境没准备好的情况下直接编译FAST-LIO,报错会特别多,排查起来容易劝退。
4.2 编译配置与常见报错处理
拿到FAST-LIO的ROS2分支代码后,把它放进src目录,然后colcon build --symlink-install。用symlink方式编译,调launch文件和Python脚本时不用重新编译,开发效率会高不少。
第一次编译经常会遇到两个报错。一个是找不到Eigen3或者PCL头文件,一般是CMake找不到依赖路径,可以通过安装libeigen3-dev和libpcl-dev解决。另一个是自定义消息相关的类型找不到,这通常需要先编译livox_ros_driver2,确保消息接口被正确生成并安装到install目录。
还有一个比较隐蔽的问题是Eigen版本与代码兼容性。有些老的FAST-LIO分支对Eigen3的版本有要求,如果你本机装的Eigen版本过新或过旧,编译时可能报一些奇怪的模板错误。这类问题没有统一解法,一般是根据报错找到对应的头文件,看是哪个API用错了,然后在代码里做兼容适配。
4.3 参数配置详解:以MID-360为例
FAST-LIO的所有参数都放在一个yaml文件里,编译完成后,在launch文件或config目录中能找到。以MID-360为例,核心参数分组如下。传感器参数部分要指定雷达型号livox_lidar_type,点云话题名和IMU话题名,以及二者的frame_id。外参部分最关键,包括雷达到IMU的平移和旋转矩阵,这个参数必须准确,否则点云融合会直接发散。
系统参数里,有几个需要重点理解:point_filter_num表示每隔几个点采一个点,数值越大参与配准的点越少,计算越快但精度可能下降。filter_size_surf是降采样体素大小,太大丢失细节,太小计算量大。maximum_iteration是IESKF的最大迭代次数,一般设为2到3次就够,迭代过多收益有限。此外还有初始噪声协方差设置,这些值严重影响算法的收敛性和稳健性,新手建议先保持默认值,等流程跑通了再慢慢调。
下面是一段供参考的配置片段,使用MID-360时的关键参数示例。实际使用中不同雷达、不同安装位置,外参和话题名都要相应修改,不要直接照抄。
common: lid_topic: "/livox/lidar" imu_topic: "/livox/imu" lidar_type: 1 blind: 0.5 point_filter_num: 6 step_filter_num: 2 filter_size_surf: 0.5 filter_size_map: 0.5 extrinsic_est_en: false extrinsic_T: [0.041, 0.015, 0.031] extrinsic_R: [1, 0, 0, 0, 1, 0, 0, 0, 1]4.4 运行launch并启动Rviz2验证效果
编译通过、参数改好之后,就可以运行了。启动launch时,它会自动拉起核心节点和rviz2可视化。启动后要注意控制台输出,正常状态下会有点云帧的匹配数量、残差值、耗时统计等信息周期性刷新。
如果算法正常运转,rviz2地图区域会出现一个从稀疏逐渐变密集的点云地图。可以从一个角落开始扫描,走到空旷区域再回来,观察地图是否闭合良好。手头没有实体雷达的话,可以用官方提供的bag包做回放验证。使用bag包时注意把launch里的use_sim_time打开,同时回放命令加上--clock标志,这样节点才能读取bag中的时间戳。
我在实验中最常用的验证方式是原地旋转一周再回到起点,看地图中同一个墙面有没有出现重影。如果重影明显,大概率是外参标定不准或者IMU噪声参数设置有问题。这个测试简单有效,强烈建议部署前先做一遍。
5. 常见问题与排查技巧实录
5.1 收不到IMU数据或点云数据
把话题列表打开,输入“ros2 topic list”确认两个话题是否存在。如果话题存在但节点里始终没有数据输出,就要分两种情况处理。点云话题的话,先检查QoS是否匹配,用ros2 topic info /livox/lidar --verbose看两端策略是否一致。IMU话题的话,在ROS2里如果消息类型或帧ID和代码里的预期不一致,程序会静默丢弃或直接报错,要重点检查imu_frame和代码里写死的frame_id是否一致。
这个类型的话题问题我在调试时遇到过多次,印象最深的一次是换了一台IMU,frame_id从imu_link变成imu_data,导致FAST-LIO完全“看不见”IMU数据,折腾了半个小时才发现是字符串不匹配。
5.2 点云漂移、地图发散的排查顺序
地图漂移是SLAM调试验证中最常见的问题。按照我个人的排查顺序,第一步肯定是查外参。雷达和IMU之间的外参一旦有偏差,地图上就会有典型的表现:扫描同一墙面时,前后两帧出现平行偏移或者旋转错位。如果外参看起来没问题,第二步查IMU噪声参数。IMU噪声设得太小,等于告诉滤波器“IMU数据非常可信”,系统中运动会过度相信积分结果,导致轻微漂移被放大。第三步则是检查运行速度。如果机器人跑得太快或旋转太剧烈,超出了算法标定的动力学范围,也会导致跟踪丢失。
5.3 时间戳不同步导致的地图错乱
ROS2的bag包回放和真实传感器运行,时间戳处理方式完全不同。真实传感器模式下,IMU和雷达到达节点的时间基本就是它们的采集时间,时间同步压力不大。回放模式下,如果节点和bag不共享同一个clock,那么bag里的IMU时间戳和当前系统时间没有对齐,算法就会认为“IMU和雷达的时间差越来越大”,初始化时就会出问题。
解决办法很简单:launch文件里设置use_sim_time为True,然后回放bag时加上--clock参数。注意这个参数必须加,不加等于没设置。另外一个容易被忽略的坑是,bag包录制的时间戳本身可能就有问题,比如录制时已经用了仿真时钟,回放时又用系统时钟,这种双重时间偏移问题排查起来更麻烦,建议回放测试尽量使用官方公开发布的数据集。
5.4 里程计输出到其他模块的适配
如果想把FAST-LIO的定位结果接入导航或建图模块,还有几个工作要做。FAST-LIO默认通过Odometry话题发布里程计数据,并广播雷达坐标系到地图坐标系的变换。但很多下游模块希望接收的是IMU坐标系到地图坐标系的变换,两者差了一个外参,这个转换在launch里可以通过静态变换发布来补充。
还遇到过的问题是,下游模块对里程计话题的QoS要求不同,导致订阅不到数据。ROS2里不同模块各自设定QoS策略很容易互相错过,建议在工程里统一维护一份QoS配置文件,所有节点从同一个配置读取,避免这种低级但很烦人的问题。
5.5 一些小问题的速查
把平时遇到的一些小问题整理成表格,方便快速对照。比如编译时找不到PCL或Eigen,一般是依赖没装全,重装libpcl-dev、libeigen3-dev即可。launch启动时报参数加载失败,多半是yaml格式问题或路径不对,检查config文件路径。rviz2中没有点云但节点在运行,可能topic名或frame不匹配,用ros2 topic和ros2 run tf2_tools看TF树。
这表格我在这篇文章的初稿里就写了,后续配合文章使用会比较方便。另外再补一句:如果是在Docker容器里部署,一定要做好端口和共享内存的映射,否则可能导致DDS通信异常,容器内节点无法和宿主机上的rviz2建立连接。关于Docker和micro-ROS的组合,我在另一个项目中尝试过,坑也不少,这个以后有机会单独写一篇。
回到FAST-LIO本身,我个人的体会是,多数“跑不起来”的问题其实都出在环境、时间同步和参数设置上,真正算法本身出问题的情况极少。如果你被某个报错卡住,不妨先退一步,理一遍“点云从哪来、IMU从哪来、时间戳是否对齐、QoS是否匹配、外参是否正确”这条链路,八成能快速定位问题。
最后再分享一个小技巧:调参的时候不要大改大动,最好一次只改一个参数,记录地图表现的变化。比如先只调整点云降采样大小,跑一遍数据,再改IMU的加速度噪声方差,看看地图谁变好了谁变差了。这种控制变量的方法,比一次性改一堆参数然后“凭感觉调”要有效得多。我当时把MID-360的配图从“有些飘”调到“基本不飘”,就是用这个办法,前后花了不到一个下午。