FAST-LIO:从误差状态卡尔曼滤波到高性能激光雷达里程计

张开发
2026/4/13 10:25:13 15 分钟阅读

分享文章

FAST-LIO:从误差状态卡尔曼滤波到高性能激光雷达里程计
1. FAST-LIO与误差状态卡尔曼滤波的完美结合第一次接触FAST-LIO时我就被它惊人的实时性能震撼到了。当时我正在调试一台搭载Velodyne VLP-16激光雷达的移动机器人传统LIO算法在快速转弯时总会出现轨迹漂移。直到尝试了FAST-LIO这个问题才得到彻底解决。这让我不禁好奇为什么这个算法能在保持轻量化的同时实现如此鲁棒的定位效果答案就藏在它独特的误差状态卡尔曼滤波ESKF设计中。误差状态卡尔曼滤波与传统卡尔曼滤波最大的不同在于它处理的不是完整状态量而是状态量的误差项。这就好比我们在纸上写字时不是直接修改已经写错的内容而是在旁边用红笔标注修正意见。这种设计带来了三个显著优势数值稳定性更好误差量通常远小于状态量本身避免了浮点数计算的精度损失线性化更准确在小误差假设下非线性系统的线性近似更接近真实情况计算效率更高只需要更新误差状态减少了不必要的计算开销在激光雷达里程计的具体场景中IMU数据的高频特性与激光雷达点云的精确性形成了完美互补。但IMU的零偏和噪声就像调皮的小鬼总是在你不注意时捣乱。FAST-LIO的聪明之处在于它用ESKF专门对付这些误差项让IMU做好短期精确的本职工作而激光雷达则负责长期可靠的全局校正。2. 误差状态方程的数学奥秘2.1 状态量的分解艺术让我们用一个日常比喻来理解状态分解假设你在玩VR游戏头盔的真实位姿真值状态xₜ可以分解为系统预测的位姿名义状态x和预测误差误差状态δx。在FAST-LIO中这个数学表达非常优雅// 伪代码示例状态量定义 struct State { Vec3d p; // 位置 Vec3d v; // 速度 Quatd q; // 旋转(四元数) Vec3d bg; // 陀螺零偏 Vec3d ba; // 加速度计零偏 Vec3d g; // 重力向量 };误差状态的特别之处体现在旋转表示上。不同于直接对旋转矩阵做加减ESKF使用李代数δθ来表示旋转误差Rₜ R * Exp(δθ)这个Exp映射正是SO(3)群上的指数映射它将三维向量转换为旋转矩阵。在实际编程中我常用Eigen库来实现这个运算Eigen::Matrix3d Exp(const Eigen::Vector3d w) { double theta w.norm(); if (theta 1e-7) return Eigen::Matrix3d::Identity(); Eigen::Matrix3d W skewSymmetric(w/theta); return Eigen::Matrix3d::Identity() sin(theta)*W (1-cos(theta))*W*W; }2.2 运动方程的推导技巧推导误差状态方程时有几个关键步骤让我印象深刻。首先是速度误差的推导需要考虑科氏加速度的影响。具体来说真实加速度可以表示为v̇ₜ Rₜ(ã - bₐₜ - ηₐ) gₜ展开后会出现一个有趣的交叉项 -Rã∧δθ这实际上反映了机体旋转对加速度测量的影响。我在调试时就曾忽略这个项导致无人机在快速滚转时出现高度估计漂移。旋转误差的推导更为精妙。需要利用SO(3)的伴随性质R⋅ω∧ (Rω)∧⋅R这个性质允许我们将旋转矩阵穿过反对称矩阵就像变魔术一样。最终得到的旋转误差方程δθ̇ -(ω̃ - b_g)∧δθ - δb_g - η_g揭示了陀螺仪误差是如何传播的。其中-(ω̃ - b_g)∧这个项特别重要它说明高速旋转时会放大角度误差这也解释了为什么无人机做快速机动时更容易丢失定位。3. 离散时间下的预测过程3.1 名义状态的预测在实际编程实现时我发现离散化处理有诸多细节需要注意。以位置预测为例p_{k1} p_k v_kΔt 0.5(R_k(ã - b_a))Δt² 0.5gΔt²这个看似简单的公式在实现时却有几个优化点Δt的选择通常取IMU的采样间隔(5-10ms)过大会导致积分误差累积加速度处理需要先扣除零偏b_a再旋转到世界坐标系重力补偿记得重力方向通常是z轴负方向下面是我在项目中使用的预测代码片段void predictNominalState(State state, const IMUData imu, double dt) { // 旋转积分 Eigen::Vector3d omega imu.gyro - state.bg; state.q * deltaQ(omega * dt); // 速度积分 Eigen::Vector3d acc imu.acc - state.ba; state.v (state.q * acc state.g) * dt; // 位置积分 state.p state.v * dt 0.5 * (state.q * acc state.g) * dt * dt; }3.2 误差状态的协方差传播误差状态的预测需要考虑噪声传播。FAST-LIO使用了一个巧妙的技巧将连续时间噪声转换为离散时间噪声。对于陀螺零偏噪声σ(η_g) √Δt ⋅ σ_{bg}这个√Δt的关系源于布朗运动的统计特性。在实现时我构建了一个噪声协方差矩阵Eigen::Matrixdouble,18,18 Q Eigen::Matrixdouble,18,18::Zero(); Q.block3,3(3,3) (dt*dt)*acc_noise*Eigen::Matrix3d::Identity(); // 速度噪声 Q.block3,3(6,6) (dt*dt)*gyro_noise*Eigen::Matrix3d::Identity(); // 旋转噪声 Q.block3,3(9,9) dt*gyro_bias_noise*Eigen::Matrix3d::Identity(); // 陀螺零偏噪声 Q.block3,3(12,12) dt*acc_bias_noise*Eigen::Matrix3d::Identity(); // 加速度零偏噪声雅可比矩阵F的构造是预测的核心它反映了各个误差项之间的耦合关系。特别是速度误差与旋转误差的耦合项 -R(ã - b_a)∧Δt这个项在实际测试中对系统稳定性影响很大。4. 激光雷达观测的更新过程4.1 点云到地图的匹配FAST-LIO的更新阶段是其性能关键。与传统ICP不同它采用紧耦合的迭代卡尔曼滤波框架。每次接收到激光雷达数据时我的处理流程通常是点云去畸变利用预测的IMU轨迹补偿激光雷达运动畸变特征提取从原始点云提取平面和边缘特征数据关联将特征点与体素地图进行最近邻匹配残差计算计算点到平面或点到边的距离残差观测模型可以表示为z h(x) v其中h(x)是非线性观测函数。对于平面特征点它的残差就是点到匹配平面的距离。这个计算看似简单但在实现时有几个优化点double computePlaneResidual(const Point point, const VoxelMap map) { // 查找最近的5个点 auto neighbors map.findNearestNeighbors(point, 5); // 计算平面参数 Eigen::Vector3d normal; double d; fitPlane(neighbors, normal, d); // 返回点到平面距离 return normal.dot(point) d; }4.2 误差状态的观测更新ESKF更新的精髓在于观测矩阵H的计算。由于我们更新的是误差状态需要用到链式法则H ∂h/∂x ⋅ ∂x/∂δx其中第二项∂x/∂δx对于旋转部分特别有趣∂R/∂δθ J_r⁻¹(R)这个J_r⁻¹是右雅可比矩阵的逆它反映了旋转矩阵对微小扰动的敏感度。在我的实现中采用以下近似计算Eigen::Matrix3d computeRightJacobianInverse(const Eigen::Vector3d w) { double theta w.norm(); if (theta 1e-7) return Eigen::Matrix3d::Identity(); Eigen::Matrix3d W skewSymmetric(w); return Eigen::Matrix3d::Identity() 0.5*W (1/theta/theta - (1cos(theta))/(2*theta*sin(theta)))*W*W; }卡尔曼增益的计算虽然标准但在资源受限的平台上需要特别注意矩阵求逆的效率。我通常使用QR分解来求解Eigen::MatrixXd K P * H.transpose(); Eigen::MatrixXd S H * P * H.transpose() V; K S.ldlt().solve(K.transpose()).transpose();5. 误差状态重置与实践技巧5.1 状态重置的数学细节误差状态更新后需要合并到名义状态这个过程看似简单实则暗藏玄机。特别是旋转部分的处理R_{k1} R_k * Exp(δθ_k)这个操作在数学上很优雅但在实现时要注意四元数的乘法顺序。我踩过的坑是不同数学库的四元数乘法约定可能不同导致结果完全错误。协方差矩阵的重置更微妙。理论上应该考虑旋转误差带来的切空间变化P_reset J_k * P * J_k^T其中J_k包含了一个小雅可比矩阵J_θ I - 0.5 δθ_k∧在实际应用中我发现当δθ_k很小时可以省略这个修正但对高速运动的机器人还是建议保留。5.2 工程实践中的调参经验经过多个项目的实战我总结了一些FAST-LIO的调参经验IMU噪声参数不要直接使用数据手册的值建议通过静态采集标定陀螺仪噪声通常1e-4 ~ 1e-3 rad/s/√Hz加速度计噪声通常1e-3 ~ 1e-2 m/s²/√Hz激光雷达匹配权重平面特征比边缘特征可以给更大权重平面特征0.5 ~ 1.0边缘特征0.1 ~ 0.3体素地图分辨率根据场景动态调整室内0.2 ~ 0.5米室外0.5 ~ 2.0米关键帧策略平移或旋转超过阈值时更新地图平移阈值0.1 ~ 0.3米旋转阈值5 ~ 10度下面是我的参数配置文件示例# FAST-LIO配置示例 imu: gyro_noise: 0.0001 acc_noise: 0.001 gyro_bias_noise: 0.00001 acc_bias_noise: 0.0001 lidar: plane_resolution: 0.3 edge_resolution: 0.5 plane_weight: 1.0 edge_weight: 0.2 mapping: voxel_size: 0.5 max_range: 50.0 min_range: 1.06. 性能优化与创新改进6.1 计算效率的提升技巧让FAST-LIO在嵌入式平台实时运行需要一些优化技巧。我常用的方法包括体素地图的懒更新只有当机器人移动一定距离后才更新局部地图特征点的选择性处理只处理信息量大的区域如墙角、边缘并行化处理将点云处理和状态估计分配到不同线程内存预分配避免运行时动态内存申请特别是在资源受限的平台上我重写了点云处理流水线class LidarProcessor { public: void process(const PointCloud::Ptr cloud) { // 预分配内存 if (tmp_cloud_.size() cloud-size()) { tmp_cloud_.resize(cloud-size()); } // 并行化特征提取 #pragma omp parallel for for (size_t i 0; i cloud-size(); i) { extractFeature((*cloud)[i], tmp_cloud_[i]); } // 后续处理... } private: std::vectorFeaturePoint tmp_cloud_; };6.2 算法改进的方向基于ESKF的框架我尝试过几个有效的改进方案运动补偿增强在高速运动场景采用二阶运动模型进行点云去畸变自适应噪声调整根据运动剧烈程度动态调整IMU噪声参数多传感器融合加入轮速计或视觉信息提升平面运动估计闭环检测集成结合Scan Context等方法实现后端优化其中自适应噪声调整的效果特别显著。当检测到机器人剧烈运动时适当增大过程噪声可以避免滤波器过自信if (angular_velocity.norm() 2.0) { // 高动态场景 Q.block3,3(6,6) * 4.0; // 增大旋转噪声 Q.block3,3(3,3) * 2.0; // 增大速度噪声 }7. 典型问题与调试方法7.1 常见问题排查在部署FAST-LIO时我遇到过各种奇怪的问题。以下是几个典型案例高度持续漂移通常是加速度计零偏估计不准建议延长静态初始化时间快速旋转时发散检查陀螺仪噪声参数可能需要增大过程噪声点云匹配失败降低体素分辨率或增加特征点数量计算延迟大优化点云降采样率和体素地图更新策略我常用的调试工具包括RViz实时可视化轨迹和点云匹配情况rqt_plot绘制状态估计和传感器原始数据对比gdb当程序崩溃时进行堆栈跟踪7.2 实战调试案例曾经在一个仓储机器人项目中出现过这样的问题当机器人靠近金属货架时定位会突然跳变。经过分析发现根本原因金属货架导致IMU受到磁干扰陀螺仪输出异常解决方案在IMU周围添加磁屏蔽材料实现异常检测算法当陀螺仪数据突变时暂时增大噪声参数增加激光雷达的匹配权重调试代码示例bool checkIMUAbnormal(const IMUData imu) { static Eigen::Vector3d last_gyro Eigen::Vector3d::Zero(); double delta (imu.gyro - last_gyro).norm(); last_gyro imu.gyro; return delta 0.5; // 超过0.5 rad/s的变化视为异常 }这个案例让我深刻认识到实际部署中传感器数据的可靠性比算法本身更重要。

更多文章