ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FAST-LIVO2核心拆解:IMU状态递推与点云去畸变实战

FAST-LIVO2核心拆解:IMU状态递推与点云去畸变实战 前几个月把FAST-LIVO2完整跑通的时候我其实并没有特别在意IMU模块——当时满脑子都在调前端里程计的残差阈值后来真到40ms延迟的数据集上做回放才发现整个系统的不动点根本不在雷达而在这颗看起来不起眼的IMU上。FAST-LIVO2的完整链路是“RGB-DLiDARIMU”的紧耦合里程计代码仓库里IMU相关的代码占比不大但它承担了三个极其关键的任务状态递推、点云去畸变、以及为迭代误差状态卡尔曼滤波器iEKF提供运动先验。任何一个环节处理不好后面全局优化做得再漂亮都是白搭。这篇文章我想把这套IMU模块彻底拆开聊一遍从状态递推的离散模型讲起再到点云去畸变的实现思路中间穿插我在实际移植和调参时踩过的坑。如果你正在看FAST-LIVO2的源码或者想在自研的方案里参考它的IMU处理方式这篇文章应该能帮你省下不少找代码和debug的时间。1. IMU在FAST-LIVO2里到底扮演什么角色1.1 一个IMU配一颗固态激光雷达这套系统的基本盘FAST-LIVO2的前身FAST-LIVO面向的是Livox系列固态激光雷达这类雷达采用的是非重复扫描模式一帧点云并不是瞬时采样的而是在一段几十毫秒的时间窗口内不断累积。这意味着雷达坐标系下的点实际是在不同时刻的雷达位姿下被捕获的。如果直接把这一帧点云当作刚体来做配准运动造成的畸变会直接反映在配准残差上严重时边角特征会变成弧线精度直接崩掉。IMU在这套方案里的作用就是提供高频的位姿递推。它通常工作在200Hz到400Hz比雷达10Hz左右的帧率高出十几倍可以近似认为是连续的运动采样。有了IMU系统就能做到三件事把雷达每帧扫描时间段内的运动轨迹估算出来用这条轨迹把畸变的点云补偿回扫描起点的坐标系在两帧点云之间持续给出先验位姿让后续的点云配准不用从零开始迭代在视觉特征或雷达特征失效的短暂瞬间利用IMU递推继续维持系统的状态估计。从系统层面看IMU相当于这套里程计的“高频心跳”而雷达和相机则是“低频校准源”。没有心跳校准源再精确也无法构成连续的状态估计没有校准源心跳只能短时间维持漂移会迅速累积。1.2 IMU模块的三个核心任务拆开FAST-LIVO2的代码IMU相关的逻辑其实集中在几个地方状态传播、误差传播、点云补偿、以及后端优化中的残差约束。任务可以归纳成三条主线状态递推通过IMU的角速度和加速度对系统状态——姿态、位置、速度、bias——进行高频预测并同步推进协方差矩阵。点云去畸变利用预测出的运动轨迹把一帧雷达点云里不同时间戳的点补偿到统一的参考坐标系下。残差约束在iEKF的更新阶段以IMU递推值为参考把雷达配准的相对位姿或视觉重投影残差作为量测修正状态估计。很多教程喜欢把这三个任务分开讲但实际在FAST-LIVO2的架构里它们是耦合在一起的状态递推的精度直接影响去畸变效果去畸变后的点云质量又反过来决定配准更新的可靠性而更新的结果会作为下一次递推的初始值。所以这个模块不是一个单向的流水线而是一个闭环的反馈系统。这也是我后来才真正想明白的一点——当时单独看代码里某一段IMU传播逻辑觉得很简单拼起来才意识到整个系统的性能上限是由IMU模块的精度决定的。2. 状态递推从连续时间模型到离散实现2.1 误差状态卡尔曼滤波器的基本思路FAST-LIVO2的状态估计核心是iEKF和MSCKF、VINS-Fusion这类半紧耦合方案的最大区别在于它维护的不是单一位姿估计而是一个包含姿态、位置、速度、IMU bias、以及可能的外参变量的完整状态向量。IMU递推做的事情就是把这个状态向量从上一帧推进到当前时刻。iEKF的思路在SLAM圈子里已经不新鲜了把状态拆成“名义状态”和“误差状态”。名义状态用IMU的测量值直接积分误差状态则由线性化的误差传播方程驱动。这样做的最大好处是误差状态量级很小线性化误差可控数值稳定性比直接用大角度姿态做EKF要好得多。FAST-LIVO2沿用了这套经典框架但在离散化细节上做了不少具体实现上的取舍。这里有一个关键点值得注意名义状态的积分用的是旋转矩阵或四元数误差状态则使用李代数so(3)上的小量扰动。代码里你会经常看到R * Exp(phi)这种写法意思是在名义旋转基础上叠加以一个小的旋转向量phi。这个处理方式在姿态误差为小量时特别干净协方差的物理意义也更直观。2.2 数值积分细节中值积分与误差传递矩阵IMU的离散积分在FAST-LIVO2里使用的是中值积分也就说在两个IMU采样时刻之间认为角速度和加速度的测量值取平均值作为这段时间内的常值输入。公式表达如下姿态递推R_{k1} R_k * Exp((omega_k omega_{k1}) / 2 * dt)速度递推v_{k1} v_k (R_k * a_k g) * dt具体加速度项会取中值位置递推p_{k1} p_k v_k * dt 0.5 * (R_k * a_k g) * dt^2这里omega是IMU的角速度测量值减零偏a是加速度测量值减零偏g是重力加速度向量在导航系下的表示。中值积分相比欧拉积分的优势是在几乎不增加计算量的情况下显著减小加速度和角速度快速变化时的积分误差。对于FAST-LIVO2这种需要支撑点云去畸变的场景来说这个精度差异会直接反映到补偿后的点云是否有拖影。误差状态的传播矩阵则是把连续时间线性化模型离散化的结果。对于姿态误差phi速度误差dv位置误差dp以及bias误差离散传播公式的形式如下phi_{k1} phi_k - R_k^T * dt * dphi_bg noise dv_{k1} dv_k - (R_k * [a_k]_x * dt) * phi_k - R_k * dt * dba noise dp_{k1} dp_k dt * dv_k noise其中[a_k]_x是加速度测量值的反对称矩阵。实际代码里还会把bias误差的随机游走加进去并据此推进协方差。这段逻辑不太长但对数学模型的每个符号都要能对上否则后面配准更新时协方差会越传越大导致滤波增益异常。2.3 协方差更新与系统初始化协方差传播在iEKF里的重要性常常被低估。我见过不少人在代码里直接把协方差P的前几个对角块设成固定值觉得反正后面会收敛——这种想法在后面退化场景下会吃大亏。FAST-LIVO2的做法是严格执行误差状态的线性传播方程将噪声协方差、imu的高斯白噪声和bias随机游走都纳入考量。协方差矩阵物理意义的理解决定了一旦出现定位漂移你是去查IMU噪声参数还是去查外参标定。从实践角度来看系统稳定后协方差对角线数值应该保持在某一量级如果出现指数级增长说明噪声参数里的gyr_noise或acc_noise设置过小或者时间同步出了严重问题。系统初始化同样依赖IMU。FAST-LIVO2会利用静止时采集的一组IMU数据估计初始姿态中的重力方向并顺便算出陀螺仪bias的粗略初值。这里有个容易忽略的细节初始化时需要用IMU的静止判定来判断系统是否处于低动态状态。静止判定阈值设大了初始化结果里会混入运动分量设小了则可能永远无法通过初始化。代码里常用加速度方差和角速度方差双重判定经验值在0.02 m/s^2和0.05 rad/s附近但不同IMU型号会有明显差异。3. 点云去畸变为什么雷达成像会“流动”3.1 畸变是怎么产生的固态激光雷达的非重复扫描模式让雷达在一个积分周期内覆盖一片区域而不是像传统机械雷达那样在某一瞬间同时获取一圈点云。典型的一个扫描周期是100ms在这100ms里雷达已经随着载体在运动。举个例子一个垂直于扫描平面的边缘如果雷达静止时扫描到的是一条清晰的垂直线运动时这条边缘会被拉成一条斜线或弧线这就是点云畸变。传统机械雷达也有类似问题一帧数据对应一次360度旋转旋转期间平台一直在移动点云底部和顶部之间存在时间差。去畸变的思想是统一的给点云中的每个点恢复它被采样时刻的雷达位姿然后把所有点都变换到同一个参考时间戳对应的坐标系中。3.2 去畸变的两种常见思路我把实践中能用的去畸变方案分成两类基于位姿插值的和基于状态递推的。基于位姿插值如果雷达帧率够高或者载体运动较慢可以假设在相邻两帧雷达位姿之间做线性插值或球面线性插值对旋转用SLERP就能得到每个点对应时刻的位姿。这种方法实现简单计算量小但在快速运动时精度不够因为实际运动轨迹远比线性插值复杂。基于状态递推FAST-LIVO2的做法是在雷达扫描周期内用IMU递推出一串高频率的位姿序列每个点根据时间戳找到前后两个IMU递推位姿再做插值。这样即使是激烈的转弯或急加速去畸变后的点云也能基本保持原有的几何结构。这也是为什么FAST-LIVO2能把去畸变的精度推到厘米级。这里我要额外提一句RGB-D相机数据也存在类似的“运动畸变”和“时间对齐”问题FAST-LIVO2在处理RGB-D与雷达的融合时对视觉帧也做了基于IMU的时间戳补偿只是视觉特征通常在一帧图像曝光时间内形变不显著实际影响比雷达小。3.3 实操代码一次扫描内的点云补偿老规矩先看一段伪代码。假设点云里每个点都带有相对于扫描起始时刻的时间戳t_offsetIMU递推得到的关键帧位姿序列是pose_vec每秒200Hz// 伪代码示意去畸变流程 void undistortScan(const std::vectorPointType raw_cloud, const std::vectorPose6D imu_poses, // 时间戳对应的位姿序列 double scan_start_time, std::vectorPointType out_cloud) { out_cloud.reserve(raw_cloud.size()); Eigen::Matrix3d R_start imu_poses.front().R; // 扫描起点位姿 Eigen::Vector3d t_start imu_poses.front().t; for (const auto pt : raw_cloud) { double t scan_start_time pt.t_offset; Pose6D pose_k getPoseAt(imu_poses, t); // 按时间戳查/插值位姿 Eigen::Matrix3d R_t pose_k.R; Eigen::Vector3d t_t pose_k.t; // 把点从雷达坐标系变换到世界坐标系 Eigen::Vector3d pw R_t * pt.getVector3fMap() t_t; // 再变换回扫描起点坐标系 Eigen::Vector3d p_compensated R_start.transpose() * (pw - t_start); PointType new_pt pt; new_pt.getVector3fMap() p_compensated.castfloat(); out_cloud.emplace_back(new_pt); } }关键点在于getPoseAt这个函数——它通常在IMU递推位姿序列里做二分查找然后用线性插值平移部分和球面线性插值旋转部分得到更精确的位姿。点云数量大时这个查找和插值会占用不少CPU时间优化思路是按照时间戳单调性做游标推进避免每个点都做二分查找。我实测的一个经验当IMU频率从200Hz降到100Hz时去畸变效果在正常走路速度下差别不大但在车辆急转弯时补偿后的点云边缘会出现大约2厘米的撕裂感。这说明IMU递推频率是一个不能随意压低的关键参数。4. 标定与时间同步去畸变的“隐形前提”4.1 外参标定错一点去畸变残差就多一分去畸变的公式里使用了一个隐含假设雷达坐标系与IMU坐标系之间的外参是精确已知的。如果外参旋转矩阵误差只有1度在10米远的特征点上去畸变补偿就会引入大约17厘米的位置偏差。这个偏差不会因为滤波器的更新而消失它会以系统误差的形式持续进入配准过程最终让地图出现重影或者轨迹漂移。FAST-LIVO2不太关心你如何获得外参——你可以用lidar-imu标定工具比如li_calib、livox_calibration也可以自己写靶标配准脚本。但它要求你把这组外参写进配置里并且默认它是时不变的。实际运行中如果雷达和IMU的安装结构是刚性固定的这个假设没问题如果是用海绵胶临时粘贴的热胀冷缩或振动位移会让外参缓慢漂移这种场景下任何基于固定外参的系统都无法长期保持精度。一个更好的做法是把外参放在状态向量里在线估计。FAST-LIVO2对雷达-IMU外参的处理相对保守默认不优化外参这可以大大降低系统的非线性程度但对硬件安装就提出了更高的要求。我自己的习惯是每次更换安装结构后先用静止数据跑一遍外参标定再进FAST-LIVO2。4.2 时间同步没有同步去畸变是在盲人摸象比外参更容易被忽略的是时间同步。点云的每个点都有一个时间戳但这个时间戳通常来自雷达内部时钟IMU的测量值则带有IMU驱动的时间戳两个时钟如果不严格对齐去畸变时查找的位姿就会是“过去”或“未来”的位姿。FAST-LIVO2的代码里通常假设所有传感器的时间戳已经对齐到同一时间基准。常见做法是用ROS的message_filters::sync_policies::ApproximateTime做时间近似同步或者在硬件层面用同一个PPS信号给IMU和雷达授时。如果没有硬件同步至少要测量出固定延迟并补偿掉。我曾在一家客户现场遇到一个诡异现象车辆每次转弯时点云边缘都呈波浪状排查了很久最后发现是雷达驱动里时间戳用的是系统收到数据包的接收时间而不是雷达内部实际采样时间延迟高达30ms导致去畸变始终在使用滞后的位姿。4.3 标定流程的个人经验关于标定我提供一个成本最低、效果也过得去的参考流程准备一块带黑色棋盘格的标定板或者一面有强角点特征的墙。雷达和IMU刚性固定后标定现场静止采集20秒数据确保IMU初始化收敛。手持或装在移动平台上做包含旋转和平移的缓慢运动尽量让轨迹覆盖各个方向持续30-60秒。用li_calib这类工具联合优化雷达点云配准残差与IMU预积分残差得到外参和时延。这里要注意的是运动速度不能太快否则点云畸变本身会干扰标定结果也不能太慢否则IMU的bias和重力方向会耦合进外参估计。实际操作中我偏好“快速转向慢速平移”的组合这样对旋转外参的激励特别充分。平移外参的标定则需要有平移激励纯旋转运动对平移外参不可观测这个坑我也踩过。5. 常见问题与排查技巧实录5.1 去畸变后的边缘出现“拖尾”残留症状点云配准后的地图里墙面、柱子边缘出现明显的重影或弧形拖尾尤其在快速转弯过程中。排查顺序先看IMU递推的位姿序列是否连续平稳。如果位姿序列本身有跳变说明IMU数据存在丢帧或时间戳乱序需要先修时间同步。检查外参旋转矩阵是否使用了正确的坐标系约定。雷达坐标系是前-左-上还是右-下-前IMU坐标轴方向是否和驱动定义一致这些约定出错会导致去畸变出现系统性错位。检查点云时间戳的单位是秒还是毫秒我在移植代码时就是在这里栽过一次雷达的时间戳是毫秒但按秒来处理所有补偿量直接差了1000倍效果惨不忍睹。如果以上都没问题再考虑是不是运动太剧烈导致的插值精度不足。可以把IMU递推频率提高或者改用更高阶的插值方法。5.2 初始化阶段IMU bias不收敛FAST-LIVO2启动时如果IMU的角速度bias初值偏差过大初始化阶段的姿态估计会明显偏转严重时甚至直接发散。排查时重点看两点静止判断阈值是否适应当前的IMU噪声水平。工业级IMU和手机级IMU的噪声差距有十倍以上用一个固定阈值套在所有IMU上必然出问题。初始化阶段雷达是否发生了位移。代码里的静止判定是基于IMU信号的但如果雷达装配在柔性支架上IMU认为静止而雷达却在低速摆动配准更新就会和IMU递推产生冲突。经验做法是启动前保证设备静止10秒并且在日志里打印IMU的均值和方差快速确认初始化是否合理。5.3 高动态运动下的IMU饱和这是最后我要专门提的一个坑。IMU的加速度计有一个量程比如±16g或±6g角速度也有最大量程比如±2000deg/s。当运动超过量程时IMU测量值会“削顶”这等价于给状态递推注入了错误输入。系统的表现是点云突然错位配准残差飙升协方差也会因为观测与预测矛盾而出现异常波动。排查方法是监控IMU原始测量值是否频繁达到量程边界。如果发现需要换高量程IMU或者在运动控制上降低最大加速度和角速度。另一个妥协方案是在状态递推中引入“测量异常检测”当IMU测量值被判定为饱和时暂时只靠上一时刻状态外推避免错误信息进入系统。5.4 问题排查速查表下面这个表是我在实际调试中总结的覆盖了我遇到的大部分IMU相关异常现象可能原因快速验证方法解决思路点云出现弧形畸变时间戳单位错误打印点云时间戳最大值修正时间戳转换地图边缘重影外参旋转失效用静止数据做自标定重新标定外参初始化后姿态漂移陀螺仪零偏估计不准静止时期望角速度均值为0延长静止采集时间转弯后轨迹偏折IMU量程饱和查看IMU原始数据是否贴近限制值更换高量程IMU或限制运动速度协方差爆炸imu噪声参数过小打印协方差对角元素调大噪声参数这些排查流程没有多高深但真的能救场。我记得有一次在一台测试车上排查了一整天最后发现只是IMU的驱动里多了一句坐标轴变换的代码把Z轴翻了个方向导致所有绕Z轴的旋转补偿全部反向。从那以后我拿到新的传感器第一件事就是先做静止数据和已知旋转的判别测试确认IMU数据在坐标系变换前后符合右手定则再往FAST-LIVO2这种高耦合系统里集成。这也是我想提醒你的IMU模块的很多问题不在算法本身而在数据进算法的最后一公里。如果你正准备上手FAST-LIVO2建议先花时间把IMU的原始数据流理解透彻把时间戳、坐标系、量程这几件事锤实后面所有调试都会顺利得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进