自己手里装好的FAST_LIO2第一次跑起来的时候,点云不是地图,而是一团被拧成麻花的线。我把手柄往左一甩,桌角直接拖出半米长的尾巴,地图里的墙面像喝了酒一样扭来扭去。折腾了一整天之后我才意识到,问题根本不在后端滤波,而在两个最容易被忽略的前置环节:IMU初始化和点云畸变矫正。这两个环节没搞定,FAST_LIO2本身的ESKF再能算,也救不回来。
这篇内容就是我当时踩坑之后的完整复盘,从环境搭建、IMU标定、初始化原理,到畸变矫正到底在代码里怎么发生、最常见的几个翻车点,一条线全部拆开讲。适合那种已经把FAST_LIO2编译过、但实跑时地图发飘或点云模糊的人,也适合准备给自己的雷达和IMU做标定、但还不知道从哪入手的初学者。
1. 为什么FAST_LIO2离了IMU就玩不转:畸变问题的根源
1.1 一帧点云不是"同一时刻"拍出来的
很多刚接触激光SLAM的人会默认一帧点云就是摄像机快门那一瞬间的快照,所有点都对应同一个雷达坐标系。但激光雷达不是这样工作的。机械式雷达靠旋转电机带动激光头扫描,每发射一个点都需要时间,一帧内第一个点和最后一个点的时间差,就是雷达的扫描周期。
拿常见的10Hz机械雷达来说,一帧的扫描周期是100ms。如果雷达装在一台以1m/s速度前行的机器人上,帧头到帧尾这100ms内,机器人已经往前走了10cm。假如把这一帧的所有点都按照帧头时刻的雷达位置去表达,帧尾那些点的实际位置就会整体偏后10cm。速度越快,偏差越大;如果机器人还在旋转,那问题就更明显——角速度一大,整帧点云就像被拧过的毛巾,墙面拉出弧线。
Livox那种非重复扫描的固态雷达也一样,只是畸变形态不同。它的点不是按固定线束顺序扫出来的,而是散布在整个帧周期内,每个点的时间戳都可能不一样。这种点云如果直接交给帧间匹配,畸变会被当成真实的几何特征,匹配结果自然越来越歪。
1.2 没有IMU的时候,传统方案怎么处理畸变
早期纯激光里程计的做法是把每一帧点云当成刚体,认为帧内所有点都在同一个坐标系下。低速、小角速度的场景下凑合能用,一旦运动快了,点云畸变直接转化为配准误差。之后有人提出用匀速运动模型去估计帧内运动:假设雷达在帧周期内速度不变,按点在帧内的相对时间做线性插值。但这个假设在机器人急停、急转弯时完全不成立。
所以畸变矫正本质上是一个"需要高频运动先验"的问题。激光雷达本身帧率低,两个点云帧之间没有中间信息;而IMU的输出频率通常是100Hz甚至更高,可以以毫秒级的分辨率描述雷达在这个帧周期内经历了怎样的旋转和平移。这也是FAST_LIO2这类激光惯性里程计在架构上的立身之本——用IMU填补激光帧内的运动空白。
1.3 FAST_LIO2的"边匹配边去畸变"到底是怎么回事
FAST_LIO2和早期那种"先做点云去畸变再做配准"的两段式处理不太一样。它的做法可以理解为:把畸变矫正和点云配准放在同一个迭代循环里,用状态估计的结果去修正畸变,再用修正后的点云去更新状态估计。
简单解释就是:IMU持续高频输出,滤波器利用这些IMU数据把状态从上一帧推进到当前时刻,得到每个激光点采集时刻的粗略位姿。然后把这些激光点投影到全局地图的ikd-Tree里做最近邻匹配,形成残差,再通过迭代更新修正状态。每迭代一次,点的投影位姿就更准一点,畸变矫正的效果也就更好一点。也就是说,去畸变不是一个孤立的预处理步骤,而是和位姿估计相互耦合、一起收敛的。
理解这一点很重要。如果你只是把FAST_LIO2当成一个黑盒跑通,遇到问题大概率会不知道从哪里排查;一旦明白畸变矫正在这个框架里的真实位置,后面很多参数调整和bug定位就都有方向了。
2. 从零搭起完整环境:依赖、驱动与编译踩坑记录
2.1 版本矩阵是第一个隐形坑
FAST_LIO2对ROS、PCL、Eigen的版本组合比较敏感。我自己的环境是Ubuntu 20.04 + ROS Noetic,系统自带的PCL 1.10和Eigen 3.3.7基本够用。如果你还在用Ubuntu 18.04 + ROS Melodic,PCL 1.8也能跑,但编译时偶尔会出一些类型不匹配的问题,尤其当你的工作空间里还混着其他功能包的时候。
我的建议是:先单独建一个干净的catkin工作空间,只放livox驱动和FAST_LIO2,不要和别的一堆算法包混在一起。依赖装好之后,再验证一下Eigen的安装位置。有些情况下你apt安装的Eigen和某个库自己带的Eigen头文件会冲突,报错信息长得很吓人,实际上就是因为/usr/include/eigen3和/usr/local/include/eigen3里各有一份Eigen,CMake找错了版本。
2.2 livox_ros_driver2和livox_ros_driver怎么选
这里特别容易晕。老仓库livox_ros_driver对应旧的Livox SDK,新仓库livox_ros_driver2对应新版SDK2,两者在ROS下发布的消息类型不一样。FAST_LIO2的配置文件里有lidar_type这个参数:如果你用的是Livox自定义消息类型,就设成1;如果用的是标准sensor_msgs/PointCloud2,就设成0。
我在实践里推荐直接用livox_ros_driver2,因为新固件的Livox雷达支持更好。但有个坑:这个驱动仓库同时支持ROS1和ROS2,默认分支可能是ROS2版本。如果你在ROS1的catkin工作空间里编译后找不到包,先检查是不是编译到了ROS2的分支。具体来说,livox_ros_driver2里有一个ROS1环境变量的开关,需要按官方README设置好再编译。
最省心的操作是把驱动克隆到catkin工作空间的src目录下,和FAST_LIO2放在同一个工作空间里一起编译。这样FAST_LIO2的CMake就能直接找到livox_ros_driver2提供的信息。如果你把它单独编译在别的目录、又没有source,编译FAST_LIO2时就会报找不到包配置文件。
2.3 编译阶段最常见的两个致命错误
第一个是Could not find a package configuration file provided by "livox_ros_driver"。出现这个错误基本就是依赖包的路径没被CMake找到。处理方法:先单独编译驱动,然后source devel/setup.bash,再回去编译FAST_LIO2。如果驱动和FAST_LIO2在同一个工作空间,直接在工作空间根目录执行catkin_make一次性编完就行。
第二个是PCL或Eigen相关的头文件缺失。比如fatal error: pcl/point_types.h,说明系统里没有安装PCL开发包,执行sudo apt install libpcl-dev就能解决;Eigen缺失则安装libeigen3-dev。如果报错信息指向某个具体的Eigen源文件不存在,大概率是版本不兼容,可以考虑源码安装Eigen 3.3.x。
我把编译阶段常遇到的问题整理成了一个速查表,方便你对照排查。
| 报错现象 | 可能原因 | 处理办法 |
|---|---|---|
Could not find a package configuration file provided by "livox_ros_driver" | 驱动未编译或未source | 同一工作空间内先编译驱动,或source驱动环境 |
fatal error: pcl/point_types.h | 缺少PCL开发包 | sudo apt install libpcl-dev |
No such file or directory: Eigen/src/Core/... | Eigen版本混乱 | 安装或升级libeigen3-dev,清理多余Eigen头文件 |
| ROS1下编译driver2找不到包 | driver2默认编译到ROS2分支 | 按README设置ROS1相关环境变量 |
3. IMU标定:别拿厂家的"大概值"糊弄系统
3.1 标定到底在标什么:零偏、比例因子、轴偏差
IMU不是一个理想的传感器。即使完全静止,陀螺仪的输出也不会是0,而是一个缓慢漂移的值,这就是零偏(bias)。加速度计也有零偏,同时每个轴还可能有比例因子误差和轴间非正交误差。对FAST_LIO2这类滤波算法来说,陀螺零偏是最关键的一项,因为它直接影响姿态积分;加速度计零偏则会影响重力方向的估计。
很多人觉得IMU出厂时厂家已经标定过了,拿到手直接用就行。实际上,厂家给出的指标是芯片级参数,你拿到的模块还受到焊接、温度、安装应力的影响,真实零偏和手册值往往对不上。对精度要求不高的场景,差一点系统也能跑,但初始化会更容易发散,长距离轨迹漂移也会更明显。
3.2 用imu_utils做艾伦方差分析的完整操作
IMU标定最常用的方案是imu_utils。它的思路是采集一段完整的静止状态下的IMU数据,利用艾伦方差(Allan Variance)分析,把噪声拆成角度随机游走、速度随机游走、零偏稳定性等几个分量。
实际操作分三步。第一步,把IMU固定好,确保完全不动,然后用rosbag录制数据。需要注意录制长度至少要1小时,录得越久,艾伦方差曲线在低频段的表现越可信。Topic就用你的IMU实际发布的topic,比如/livox/imu。
rosbag record -O imu_static.bag /livox/imu第二步,编译imu_utils和它的依赖code_utils。这里有一个编译顺序问题:先编译code_utils,再编译imu_utils。很多人在新版Ubuntu上编译会报错,因为code_utils依赖的backward库和较新的GCC版本存在兼容性问题,需要手动修改一些头文件的包含路径。
第三步,配置launch文件,把topic名改成你自己的,然后启动。imu_utils会自动把rosbag里的数据读出来,在data目录下生成一系列txt和yaml文件。yaml文件里能看到类似这样的结果:
gyro: noise_density: 0.0016 random_walk: 0.00001 bias_stability: 0.0002 acc: noise_density: 0.006 random_walk: 0.0001 bias_stability: 0.0013.3 标定结果怎么填进FAST_LIO2的配置
FAST_LIO2的YAML配置文件里,在IMU相关的位置有几个噪声参数,分别对应陀螺仪噪声、加速度计噪声、陀螺仪零偏噪声、加速度计零偏噪声。imu_utils输出的数值和FAST_LIO2需要的数值可能不是同一单位,需要做换算。最典型的是角度随机游走的单位,有的工具输出的是deg/h/sqrt(Hz),而配置文件里常用rad/s/sqrt(Hz),要除以57.3再除以60。
这里给一个示例格式,但数值不要直接抄,不同IMU差异很大。
imu: # 由标定结果得到的噪声密度 gyro_meas_noise: 0.0016 acc_meas_noise: 0.006 gyro_bias_noise: 0.00001 acc_bias_noise: 0.00001如果实在不知道怎么换算,保守的做法是先用imu_utils输出的noise_density直接填,然后跑一段数据看效果,再慢慢调大或调小。不要试图一次把所有参数都调到"完美",因为滤波算法本身对部分参数是有鲁棒性的。
3.4 没有转台怎么办:静态数据也能榨出有效信息
如果手头没有专业的标定转台,艾伦方差分析依然可以做,因为它只需要静态数据就能得到随机噪声和零偏稳定性之类的指标。至于比例因子和轴间非正交误差,静态数据无法估计,只能忽略或使用厂家数值。
另一种更省事的方式是,直接录一段静态数据,算一下陀螺仪三个轴输出的平均值,把它当作陀螺零偏的粗略值。这个值虽然粗糙,但比完全用0或厂家值要靠谱,尤其当你的IMU零偏比较大时,给初始化阶段减少不少压力。之后ESKF会在运行中继续在线估计并修正剩余零偏。
4. 初始化阶段发生了什么:从"不知道姿态"到"锁定重力方向"
4.1 FAST_LIO2的初始化本质是状态估计的"冷启动"
FAST_LIO2启动的时候,它对世界一无所知:不知道自己的姿态朝向哪里,不知道重力方向在世界坐标系里怎么表示,不知道当前速度是0,更不知道IMU零偏是多少。滤波器需要从第一批IMU数据和第一帧激光点云中把这些初始状态"猜"出来。
代码层面的表现是:雷达第一帧点云到来之前,系统一直在等;第一帧点云到来之后,用它初始化ikd-Tree地图,同时根据这段时间内IMU输出的平均值,粗略估计重力方向在IMU坐标系下的投影,并给出一个初始旋转矩阵。这个过程可以理解为把世界坐标系的Z轴和重力方向对齐。陀螺零偏也在这个阶段被粗略估计出来,因为如果陀螺仪静止时输出不为零,这部分就会被当作初始角速度误差处理。
4.2 初始化为什么必须动起来
一个非常常见的误区是:初始化过程要尽量保持静止。实际上,如果你完全静止不动,IMU的加速度计读数等于"重力+加速度零偏"的合成量,陀螺仪读数等于"陀螺零偏"。这两个量在静止状态下是严重耦合的,滤波器无法区分哪些是真实的运动加速度、哪些是零偏。
只有当你让设备运动起来,角速度和线加速度的测量值里出现由真实运动引起的成分,滤波器才有机会把零偏从观测值中剥离出来。而且运动要包含充分的旋转,最好绕多个轴转动,让陀螺仪三个轴都被激活。平移运动能帮助估计加速度计零偏和速度。
所以我的实操习惯是:上电后先静止3到5秒,让系统读取原始的零偏情况,然后手持或遥控设备做几个大幅度的慢速旋转,接着再做一些平移运动,动作幅度大一点但没有必要特别剧烈,重点是要让IMU感受到明显的角速度变化。
4.3 初始化失败的现象与应对
初始化失败通常有几种典型表现。第一种是启动之后的前几帧点云就完全散了,地图看起来像爆炸现场,这通常是时间戳错位或外参方向错误。第二种是初始化过程一直没有完成,或者完成了但轨迹迅速漂移,这大概率是IMU数据噪声太大,或者启动瞬间设备在运动,导致初始姿态估错。
我踩过最典型的一个坑是:把设备放在桌面上上电,然后又顺手碰了一下桌子,这一个小小的抖动直接让第一帧的IMU状态废掉,点云瞬间发散。后来我养成了习惯:每次启动前把设备放置稳固,等日志里出现初始化完成相关提示之后再开始操作。如果做了这些还是不稳,就录一段bag离线反复跑,一次只改一个参数,对比地图质量。
5. 点云畸变矫正的全流程拆解:从原始点到去畸变点
5.1 畸变矫正的数学原理:IMU积分如何补偿每个点
先明确一个概念:一帧点云里,每个激光点实际上是从雷达在不同位姿下发出的,只是雷达把这些点打包成同一帧消息,并且默认用帧起始时刻的坐标系来表达。假设帧头时刻雷达的位姿是T0,某一个点是在帧内偏移时间t_j之后发出的,那一瞬间雷达的位姿是T_j。如果想把这个点转换到帧头坐标系,需要知道T_j相对T0的相对位姿。
这个相对位姿来自IMU。IMU以高频在两帧激光之间积分,每来一帧激光,系统会把每个点的时间戳换算成帧内偏移时间,然后在IMU状态序列里找到对应时刻的姿态,做插值或取最近邻。拿到这个相对姿态之后,把点从它自己采集时刻的坐标系变换到帧头坐标系。所有点都完成这个变换,整帧点云就"被拉直"了。
5.2 代码层面的关键路径
FAST_LIO2的代码里,预处理模块会先对点云做时间戳排序、盲区滤除、下采样之类的操作。原始的Livox自定义消息里每个点带有一个offset_time字段,表示这个点相对于帧起始时刻的时间偏移,这就是畸变矫正的时间依据。
接下来的关键路径不是单独调用一个"去畸变函数"就结束的。点云会被变换到全局地图坐标系,在ikd-Tree里做最近邻搜索,形成残差,然后进入ESKF的迭代更新。每迭代一次,滤波器会得到新的后验状态,重新投影所有点,再搜索最近邻。因为迭代过程中状态越来越准,点的投影位置也越来越准,畸变矫正的效果随之提升。
这部分理解透了,你就明白为什么FAST_LIO2在剧烈运动下依然能保持较好的建图效果:它没有把去畸变当成一个一次性的预处理步骤,而是把畸变消除的整个过程融进了状态估计的主循环里。
5.3 矫正效果怎么验证:看什么指标
验证畸变矫正做得好不好,最直观的方式是看点云轮廓。找一个有明显直线边缘的物体,比如包装箱,在它前面快速晃动雷达,然后观察箱子的边缘线。如果畸变矫正不到位,箱子边缘会变弯、拖尾;矫正到位,边缘是一条干净利落的直线。
另一个验证方式是看地图重影。在同一个场景里反复走两遍,如果畸变矫正不足,两次扫描的地面、墙面会明显分层。还有一个更定量的指标是配准残差:在Rviz里看当前点云和全局地图的贴合程度,或者从日志里观察最近邻匹配的平均距离,这个值越小越好。
我自己的经验是:如果前几帧地图花了,先不要急着调滤波器参数,先用静止数据检查IMU和雷达的时间戳、外参,以及初始化时的运动方式。把畸变矫正这条线理顺了,滤波器的调节才有意义。
6. 实测过程中最容易翻车的三个环节
6.1 时间同步:雷达和IMU的时间戳没对齐,矫正就是白做
这是我最想强调的一个坑。IMU数据用来给激光点提供运动先验,前提是IMU消息的时间戳和激光点的时间戳必须在同一个时间基准上。如果雷达消息用的是电脑主机时间,而IMU消息来自独立MCU且时间基准没有同步,那状态估计里就会存在一个隐蔽的固定时间偏移。激光点在畸变矫正时取到的IMU状态实际上是"未来"或"过去"的状态,整个去畸变自然就是错的。
检查方法很简单:用rostopic echo同时打印雷达和IMU的消息,观察两者的时间戳是否大致对齐、时间戳差值是否稳定。或者把两个topic录制进rosbag,回放时用rqt_bag查看时间轴。
如果存在固定延迟,厂商提供的驱动里通常有补偿字段,或者你需要在代码里手动减去这个偏移量。时间轴同步对FAST_LIO2来说不是可有可无的优化,而是正确运行的前提。
6.2 外参不准:比零偏误差更容易被忽略的坑
IMU和雷达之间的外参extrinsic_T和extrinsic_R在配置文件里是两个非常不起眼的数组,但它们对点云畸变矫正的影响直接而猛烈。外参的作用是把激光点从雷达坐标系变换到IMU坐标系,然后再用IMU的姿态去补偿运动。如果这个值差得离谱,畸变矫正会给每个点叠加一个随姿态变化的错误偏移,地图会出现奇怪的扭动。
我见过很多人直接拷贝别人的YAML配置文件来用,连注释里写的是Lidar到IMU还是IMU到Lidar的方向都没看,结果启动后地图莫名其妙地分层。正确做法是:先确认自己的设备型号对应的官方外参,如果有条件再用动态标定工具做一次联标,至少也要让旋转部分的方向和注释保持一致。
简单验证外参是否正确的方法是:慢速原地旋转设备,观察Rviz里当前帧点云和之前帧点云的重合度。如果外参旋转方向错了,点云会往同一个固定方向偏;如果只是平移错了,小幅度运动时区别不明显,大幅运动时偏差会被放大。
6.3 运动激励和退化场景:初始化失败跟激励直接相关
前面提到初始化时要做充分运动,但"充分"具体是多充分,很多人拿不准。我自己的经验是:把一个"8字"轨迹走完,中间包含明显的左右旋转和前后平移,大概就是比较充分的激励。要注意的是,不要只是原地打转,平移也很重要,因为加速度计零偏的估计需要线加速度的变化来激发。
另一个容易翻车的点是跑道退化场景,比如又长又直的走廊。在这个场景里,激光配准在走廊前进方向上的约束非常弱,即使IMU工作正常,位置估计也会沿走廊方向慢慢漂移。这不是FAST_LIO2的bug,而是任何激光SLAM系统都要面对的几何退化问题。遇到这种情况,要么引入其他传感器约束,要么在程序上识别退化并调整信任权重。
最后再分享一个我个人的实操习惯:每次跑FAST_LIO2之前,我会先录一段短bag,包含静止、初始化、快速运动三部分,离线跑一遍确认整体流程正常,再切换到实时模式。这样一旦实时运行出了问题,我可以对比bag回放结果,快速判断是传感器问题还是参数问题。这套流程帮我省掉了很多"现场瞎调"的时间,也希望你能在实操中少走几个弯路。