ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

无人机导航为何选择ESKF?从原理到实战的18维误差状态卡尔曼滤波

无人机导航为何选择ESKF?从原理到实战的18维误差状态卡尔曼滤波 1. 无人机导航里为什么偏偏选中了ESKF搞无人机飞控的人绕不开状态估计这个坎。你手上有一块IMU几百赫兹地往外吐角速度和加速度数据倒是挺勤快但你要是直接拿它积分算姿态和位置几分钟下来漂得连亲妈都不认识。这时候就得请出卡尔曼滤波器家族来救场。但卡尔曼滤波器的变种那么多为什么偏偏是误差状态卡尔曼滤波器Error State Kalman Filter后面统一叫ESKF在无人机导航里杀出了一条血路我刚开始接触这块的时候也犯过迷糊心想直接用扩展卡尔曼滤波器EKF不就完了吗把姿态、速度、位置一股脑塞进状态向量里该线性化就线性化该更新就更新。但实际跑过几次长航时的飞行数据之后我彻底改变了看法。EKF在处理三维旋转的时候有个天生的缺陷姿态是用四元数或者旋转矩阵表示的而四元数本身存在模长约束旋转矩阵存在正交约束。你在滤波过程中对这些量做加减运算很容易破坏它们的约束条件导致数值不稳定。飞个十几分钟可能还看不出来一旦飞到半小时以上姿态就开始出现莫名其妙的抖动严重的时候滤波器直接发散。ESKF的聪明之处在于它换了一个思路。它不去直接估计姿态本身而是去估计姿态的误差量。这个误差量是一个小量通常只有几度甚至零点几度用三维旋转向量来表示。旋转向量在原点附近做线性化精度非常高而且它没有模长约束你随便加减都不会出问题。这就好比你要测量一座大楼的高度直接量容易出错但你先量一个大概值然后再精确测量跟这个大概值的偏差最后把两个数加起来精度反而更高。ESKF玩的就是这个把戏。具体来说ESKF把真实状态拆成两部分一个是大信号的名义状态一个是小信号的误差状态。名义状态在每次IMU数据来的时候通过运动学方程递推误差状态则通过卡尔曼滤波的预测和更新步骤来估计。当误差状态估计出来之后把它注入到名义状态里进行修正然后把误差状态清零重新开始下一轮。这个流程听起来简单但里面藏着不少门道后面我会一步步拆开讲。无人机导航对状态估计的要求其实挺苛刻的。一方面无人机的计算资源有限你不可能跑一个计算量爆炸的滤波器另一方面无人机经常要做剧烈机动俯仰横滚可能瞬间变化几十度线性化误差必须控制住。ESKF在这两点上都表现得相当不错。它的状态维度通常只有18维位置3、速度3、姿态误差3、加速度计零偏3、陀螺仪零偏3、重力向量3计算量可控。而且因为误差状态一直保持在小量范围内线性化近似始终成立不会因为机动剧烈而失效。还有一个很实际的原因ESKF的代码实现相对友好。EKF你要推导复杂的雅可比矩阵尤其是姿态部分的雅可比推起来让人头大。ESKF的雅可比矩阵结构清晰很多项都是常数或者稀疏的写代码的时候不容易出错。我在实际项目中对比过两种实现ESKF的调试时间大概只有EKF的一半而且跑出来的轨迹明显更平滑。注意ESKF并不是万能的。如果你的IMU零偏特别大或者初始对准误差超过十几度ESKF也会出问题。这时候需要先做静态初始化把零偏和初始姿态估准了再切到ESKF。2. ESKF的核心原理拆解与状态定义2.1 名义状态与误差状态的分离哲学ESKF最核心的设计思想就是状态分离。你得把脑子里那套“滤波器直接估计所有状态”的惯性思维先放一放。在ESKF的世界里真实状态被定义为名义状态和误差状态的组合。名义状态是大信号它按照IMU的运动学方程自由演化不受卡尔曼滤波更新步骤的直接影响。误差状态是小信号它才是卡尔曼滤波器真正要估计的对象。为什么要这么设计因为卡尔曼滤波器的线性化假设要求状态量在均值附近变化很小。如果你直接把姿态四元数塞进状态向量飞行器一个横滚动作四元数分量可能从0.7变到0.1这个变化量一点都不小线性化误差就大了。但如果你估计的是姿态误差这个误差始终在零点几度到几度的范围内晃悠线性化精度自然就上去了。名义状态的递推公式是这样的位置更新用速度积分速度更新用旋转后的加速度减去重力再积分姿态更新用角速度积分。这些公式都是非线性的但因为名义状态不参与卡尔曼更新所以非线性不会破坏滤波器的稳定性。误差状态的演化方程则是线性的它描述的是名义状态误差如何随时间传播。这个线性化过程是在误差状态为零附近做的所以雅可比矩阵都是常数或者简单函数。我习惯把名义状态想象成一辆在高速公路上行驶的车误差状态就是方向盘的小幅度修正。车按照既定路线往前开你只需要偶尔微调一下方向就行。如果你试图直接控制车的绝对位置那每次调整都是大动作反而容易失控。2.2 18维状态向量的物理含义ESKF的状态向量通常是18维的我把它拆成六块来理解。前三块是位置误差、速度误差、姿态误差这三者构成了导航的核心状态。后三块是加速度计零偏、陀螺仪零偏、重力向量误差这三者是辅助状态用来补偿传感器的不完美和重力模型的偏差。位置误差就是名义位置和真实位置的差值单位是米。速度误差同理单位是米每秒。姿态误差稍微特殊一点它是一个三维旋转向量方向表示旋转轴模长表示旋转角度。这个旋转向量定义在名义姿态的局部切空间上所以它跟全局坐标系没有直接关系。加速度计零偏和陀螺仪零偏都是三维向量单位分别是米每二次方秒和弧度每秒。重力向量误差也是三维的用来微调重力方向因为有时候IMU的安装角度跟理论值有偏差。这18维状态不是随便凑出来的每一项都有明确的物理意义和可观测性分析。位置和速度误差通过GPS或者视觉里程计来观测姿态误差通过磁力计或者视觉来观测零偏通过长时间静止或者运动激励来观测重力向量通过加速度计在静止时的读数来观测。如果你少估计了某一项比如不估计加速度计零偏那速度就会一直漂GPS都拉不回来。提示实际项目中如果你的IMU精度很高比如工业级可以把零偏从状态向量里去掉降到12维计算量更小。但消费级IMU必须估计零偏否则滤波器根本收敛不了。2.3 连续时间误差状态方程的推导逻辑误差状态方程的推导是ESKF里最考验数学功底的部分但只要你抓住几个关键点其实也没那么可怕。推导的起点是真实状态的连续时间运动学方程然后把真实状态替换成名义状态加误差状态再做一阶泰勒展开最后把名义状态的导数项减掉剩下的就是误差状态的导数。姿态误差的推导稍微绕一点。名义姿态用四元数表示真实姿态等于名义姿态乘以误差四元数。对时间求导之后你会得到一个关于误差旋转向量的微分方程。这个方程里会出现一个反对称矩阵这个矩阵就是角速度的叉乘矩阵。最终得到的姿态误差导数等于负的角速度叉乘姿态误差再减去陀螺仪零偏误差再加上噪声项。速度误差的导数等于负的旋转矩阵乘以加速度计零偏误差再减去重力向量误差的旋转影响再加上噪声。位置误差的导数就是速度误差。零偏误差的导数等于零加上随机游走噪声因为零偏本身是缓变量它的变化率就是噪声。把这些方程整理成矩阵形式就得到了误差状态的连续时间状态转移矩阵。这个矩阵是稀疏的很多块都是零写代码的时候可以利用这个稀疏性来加速计算。我见过有人把这个矩阵当成稠密矩阵来算结果计算量翻了好几倍完全没必要。3. 从零搭建ESKF的实操流程3.1 环境准备与依赖安装动手写代码之前先把环境搭好。我推荐用Ubuntu 20.04或者22.04ROS版本选Noetic或者Humble。如果你刚开始学ROS可以用鱼香ROS一键安装脚本省去很多配置的麻烦。安装命令很简单一行就能搞定wget http://fishros.com/install -O fishros . fishros这个脚本会自动帮你配置软件源、安装ROS核心包、初始化rosdep对新手非常友好。装完之后记得source一下环境变量不然找不到ROS的命令。除了ROS本身你还需要Eigen库来做矩阵运算需要Sophus库来处理李群和李代数。Eigen一般ROS自带Sophus需要手动装sudo apt-get install libeigen3-dev git clone https://github.com/strasdat/Sophus.git cd Sophus mkdir build cd build cmake .. make -j4 sudo make installIMU驱动方面如果你用的是常见的消费级IMU比如MPU6050或者ICM20602可以通过micro-ROS接到ESP32上再通过串口或者WiFi把数据传给ROS。如果是工业级IMU一般都有现成的ROS驱动包直接apt安装就行。注意Ubuntu 24.04的用户要注意ROS版本兼容性。目前Humble是长期支持版建议优先选它。如果非要上最新版做好踩坑的心理准备。3.2 IMU数据预处理与标定IMU数据进滤波器之前必须做标定。不标定的IMU零偏可能高达0.1弧度每秒直接喂给ESKF滤波器根本收敛不了。标定分两步静态标定和动态标定。静态标定就是让IMU静止放置一段时间采集几千个样本求均值和方差。均值就是零偏的初始估计方差就是噪声的初始估计。这个过程通常跑个30秒到1分钟就够了。我习惯用ROS的imu_calib包来做这件事它会自动计算零偏和噪声密度生成一个YAML配置文件。动态标定稍微复杂一点需要把IMU拿在手里做各种旋转动作用椭球拟合的方法来估计尺度因子和非正交误差。对于消费级IMU动态标定能显著提升精度。如果你手头有转台那就更好了直接按照厂商的标定流程走一遍。标定完成之后把零偏和噪声参数写进ESKF的配置文件。加速度计噪声密度一般在0.01到0.1米每二次方秒每根号赫兹之间陀螺仪噪声密度在0.001到0.01弧度每秒每根号赫兹之间。这些值不是拍脑袋定的要根据你的IMU数据手册和实际标定结果来填。3.3 名义状态递推的代码实现名义状态递推是ESKF里跑得最频繁的部分每来一帧IMU数据就要执行一次。假设IMU频率是200赫兹那这个函数每秒要被调用200次所以代码必须写得高效。先定义名义状态的结构体struct NominalState { Eigen::Vector3d p; // 位置 Eigen::Vector3d v; // 速度 Eigen::Quaterniond q; // 姿态 Eigen::Vector3d ba; // 加速度计零偏 Eigen::Vector3d bg; // 陀螺仪零偏 Eigen::Vector3d g; // 重力向量 };递推函数接收IMU的角速度和加速度以及时间间隔dtvoid predictNominal(NominalState nom, const Eigen::Vector3d omega, const Eigen::Vector3d acc, double dt) { // 去零偏 Eigen::Vector3d omega_unbias omega - nom.bg; Eigen::Vector3d acc_unbias acc - nom.ba; // 姿态更新四元数积分 Eigen::Quaterniond dq; dq.w() 1.0; dq.vec() 0.5 * omega_unbias * dt; nom.q nom.q * dq; nom.q.normalize(); // 速度更新旋转加速度减去重力 Eigen::Vector3d acc_world nom.q * acc_unbias; nom.v (acc_world - nom.g) * dt; // 位置更新 nom.p nom.v * dt; }这段代码看起来简单但有几个细节要注意。四元数积分用的是欧拉法精度一般如果IMU频率低于100赫兹建议用中值积分或者龙格库塔法。四元数更新之后必须归一化不然模长会慢慢漂移。重力向量在递推过程中保持不变它的修正在卡尔曼更新步骤里做。3.4 误差状态预测与协方差传播误差状态的预测分两步先计算状态转移矩阵F再传播协方差矩阵P。状态转移矩阵F是18乘18的但大部分元素是零只有几个块是非零的。F矩阵的非零块包括位置误差对速度误差的偏导是单位矩阵乘以dt速度误差对姿态误差的偏导是负的旋转矩阵乘以加速度的叉乘矩阵再乘以dt速度误差对加速度计零偏的偏导是负的旋转矩阵乘以dt速度误差对重力向量误差的偏导是负的单位矩阵乘以dt姿态误差对姿态误差的偏导是负的角速度叉乘矩阵乘以dt姿态误差对陀螺仪零偏的偏导是负的单位矩阵乘以dt。协方差传播公式是P F * P * F转置 Q其中Q是过程噪声协方差矩阵。Q矩阵是对角阵对角线上的元素由IMU的噪声密度和dt决定。加速度计噪声方差等于噪声密度的平方除以dt陀螺仪噪声方差同理。零偏的随机游走噪声方差等于零偏随机游走密度的平方乘以dt。void predictCovariance(Eigen::Matrixdouble, 18, 18 P, const NominalState nom, const Eigen::Vector3d omega, const Eigen::Vector3d acc, double dt, const Eigen::Matrixdouble, 18, 18 Q) { Eigen::Matrixdouble, 18, 18 F Eigen::Matrixdouble, 18, 18::Identity(); // 位置误差对速度误差 F.block3,3(0, 3) Eigen::Matrix3d::Identity() * dt; // 速度误差对姿态误差 Eigen::Matrix3d acc_world nom.q * (acc - nom.ba); F.block3,3(3, 6) -skew(acc_world) * dt; // 速度误差对加速度计零偏 F.block3,3(3, 9) -nom.q.toRotationMatrix() * dt; // 速度误差对重力向量误差 F.block3,3(3, 15) -Eigen::Matrix3d::Identity() * dt; // 姿态误差对姿态误差 F.block3,3(6, 6) -skew(omega - nom.bg) * dt; // 姿态误差对陀螺仪零偏 F.block3,3(6, 12) -Eigen::Matrix3d::Identity() * dt; P F * P * F.transpose() Q; }这里的skew函数是构造反对称矩阵的辅助函数实现很简单就是把向量的三个分量填到矩阵的特定位置。协方差传播是ESKF里计算量最大的部分18乘18的矩阵乘法在嵌入式平台上可能要跑几毫秒。如果算力紧张可以考虑用稀疏矩阵库来加速。4. 观测更新与多传感器融合策略4.1 GPS位置观测更新GPS给的是全局位置直接跟名义状态里的位置做差就是观测残差。观测矩阵H是3乘18的只有位置对应的三列是单位矩阵其他都是零。观测噪声协方差R由GPS的定位精度决定一般水平精度在1到3米垂直精度在3到5米。更新步骤分四步计算卡尔曼增益K计算误差状态修正量dx更新协方差矩阵P把误差状态注入名义状态并清零。void updateGPS(NominalState nom, Eigen::Matrixdouble, 18, 18 P, const Eigen::Vector3d gps_pos, const Eigen::Matrix3d R) { Eigen::Matrixdouble, 3, 18 H Eigen::Matrixdouble, 3, 18::Zero(); H.block3,3(0, 0) Eigen::Matrix3d::Identity(); Eigen::Vector3d residual gps_pos - nom.p; Eigen::Matrix3d S H * P * H.transpose() R; Eigen::Matrixdouble, 18, 3 K P * H.transpose() * S.inverse(); Eigen::Matrixdouble, 18, 1 dx K * residual; P (Eigen::Matrixdouble, 18, 18::Identity() - K * H) * P; injectError(nom, dx); }injectError函数负责把误差状态加到名义状态上。位置和速度直接加姿态误差通过四元数乘法注入零偏和重力向量也是直接加。注入完成之后误差状态清零协方差矩阵保持不变。注意GPS更新频率通常只有1到10赫兹而IMU是100到200赫兹。两次GPS更新之间滤波器完全靠IMU递推所以IMU的零偏估计必须准不然位置漂移会很大。4.2 视觉里程计与IMU融合视觉里程计能给位置和姿态观测但它的输出频率不稳定有时候会丢帧。融合的时候要特别小心时间同步问题。我一般用ROS的message_filters来做时间对齐把IMU和视觉的时间戳对齐到最近的时刻。视觉观测的H矩阵比GPS复杂一点因为姿态观测是非线性的。对于小角度误差可以近似成线性关系。姿态残差用旋转向量表示观测矩阵在姿态误差对应的三列是单位矩阵。位置观测跟GPS一样。视觉里程计的噪声协方差R要根据实际场景调。纹理丰富的环境R可以设小一点比如0.01米纹理稀疏或者光照变化大的环境R要设大一点比如0.1米。如果视觉完全丢失直接把R设成无穷大相当于跳过这次更新。4.3 磁力计航向观测更新磁力计主要用来修正航向角的漂移。但磁力计很容易受干扰室内环境下几乎不可用。我的做法是只在室外飞行时启用磁力计更新室内直接关掉。磁力计观测的是地磁向量在机体坐标系下的方向。观测残差是测量到的地磁方向跟名义姿态旋转后的地磁方向之间的夹角。观测矩阵H在姿态误差对应的三列是地磁向量的叉乘矩阵。R矩阵根据磁力计的噪声水平来设一般航向精度在2到5度。如果磁力计受到干扰残差会突然变大。这时候可以用马氏距离来做异常检测如果残差的马氏距离超过阈值就跳过这次更新。这个技巧在实际飞行中非常有用能避免磁干扰导致的滤波器发散。5. 常见问题排查与实战避坑经验5.1 滤波器发散的典型原因ESKF发散的原因就那么几个我按出现频率从高到低排个序。第一个是IMU零偏估计错误尤其是陀螺仪零偏。如果零偏估计偏了0.01弧度每秒姿态在100秒内就会漂1度1000秒漂10度GPS都拉不回来。解决办法是确保静态初始化时间足够长至少30秒并且初始化期间不要碰IMU。第二个是协方差矩阵P的初始值设得太小。P设小了卡尔曼增益就小滤波器对新观测的信任度低收敛速度慢。我一般把位置和速度的初始方差设成1.0姿态设成0.1零偏设成0.01。这些值不是绝对的要根据你的传感器精度来调。第三个是观测噪声R设得太小。R设小了滤波器过度信任观测一旦观测出现异常值滤波器就被带偏了。GPS的R至少要设成1.0视觉的R至少要设成0.01。如果你不确定宁可把R设大一点滤波器收敛慢总比发散好。第四个是时间戳不同步。IMU和观测数据的时间戳差了几十毫秒滤波器就会把观测残差算错长期下来导致发散。解决办法是用ROS的message_filters做时间对齐或者用硬件触发来同步。5.2 姿态漂移的排查思路姿态漂移是ESKF最常见的症状。排查的时候先看漂移的方向和速率。如果漂移是匀速的那大概率是陀螺仪零偏没估准。如果漂移是随机的那可能是噪声参数设错了。如果漂移只在特定机动下出现那可能是线性化误差太大。我一般用rqt_plot把姿态角和零偏估计画出来。如果零偏估计曲线一直在缓慢变化说明滤波器还在收敛再等一会儿。如果零偏估计曲线稳定了但姿态还在漂那就要检查观测更新是否正常。把GPS和视觉的残差也画出来看看有没有异常值。还有一个隐蔽的问题四元数归一化。如果你在递推过程中忘了归一化四元数模长会慢慢偏离1姿态计算就会出错。这个bug很隐蔽因为短时间内看不出来跑个几分钟才显现。解决办法是在每次姿态更新后都调用normalize()。5.3 计算资源优化的实用技巧ESKF在嵌入式平台上跑计算资源是绕不开的坎。我总结了几条优化经验。第一利用F矩阵的稀疏性不要用稠密矩阵乘法。Eigen库的稀疏矩阵模块可以帮你自动优化但手动写稀疏乘法更快。第二协方差矩阵P是对称的只需要算上三角或者下三角能省一半计算量。第三如果算力实在不够可以把零偏从状态向量里去掉降到12维精度损失不大但计算量减少三分之一。还有一个技巧是降低协方差传播的频率。IMU是200赫兹但协方差传播不需要每帧都做可以每两帧做一次中间用状态转移矩阵的平方来近似。这个技巧在算力紧张的平台上很实用精度损失很小。提示如果你用ROS 2 Humble可以考虑用micro-ROS把IMU数据直接传到主控上省去中间的单片机。这样时间戳更准同步问题少很多。5.4 常见问题速查表问题现象可能原因排查方法解决办法姿态缓慢漂移陀螺仪零偏未收敛画零偏估计曲线延长静态初始化时间位置突然跳变GPS异常值检查GPS残差马氏距离加异常值检测跳过更新滤波器发散协方差矩阵P爆炸打印P的对角线减小过程噪声Q增大观测噪声R姿态抖动线性化误差大检查机动时的残差提高IMU频率用中值积分航向角漂移磁力计受干扰对比磁力计和视觉航向室内关闭磁力计更新计算超时矩阵运算太慢用ros2 topic hz看频率利用稀疏性降低传播频率6. 代码架构与工程化实践6.1 模块划分与接口设计一个可维护的ESKF代码库应该分成几个独立的模块。我习惯分成四个状态定义模块、预测模块、更新模块、工具模块。状态定义模块只管数据结构不包含任何计算逻辑。预测模块负责名义状态递推和协方差传播。更新模块负责各种观测的更新。工具模块放一些辅助函数比如反对称矩阵构造、四元数转旋转向量、异常值检测。接口设计上预测模块的输入是IMU数据和时间戳输出是更新后的名义状态和协方差矩阵。更新模块的输入是观测数据和观测类型输出是修正后的状态。模块之间通过结构体传递数据避免全局变量。class ESKF { public: void predict(const ImuData imu); void updateGPS(const Eigen::Vector3d pos, const Eigen::Matrix3d R); void updateVision(const Eigen::Vector3d pos, const Eigen::Quaterniond q, const Eigen::Matrixdouble, 6, 6 R); void updateMag(const Eigen::Vector3d mag, const Eigen::Matrix3d R); NominalState getState() const { return nom_; } Eigen::Matrixdouble, 18, 18 getCovariance() const { return P_; } private: NominalState nom_; Eigen::Matrixdouble, 18, 18 P_; Eigen::Matrixdouble, 18, 18 Q_; void injectError(const Eigen::Matrixdouble, 18, 1 dx); };这种设计的好处是每个模块可以单独测试。你可以先写一个假的IMU数据生成器只测预测模块看协方差矩阵是否正常增长。然后再写一个假的GPS数据测更新模块看状态是否收敛。最后再把它们串起来跑真实数据。6.2 参数配置与调参流程ESKF的参数不多但每一个都很关键。我一般把参数分成三组噪声参数、初始参数、观测参数。噪声参数包括加速度计噪声密度、陀螺仪噪声密度、零偏随机游走密度。初始参数包括初始协方差矩阵的对角线值。观测参数包括GPS噪声、视觉噪声、磁力计噪声。调参的顺序很重要。先调噪声参数再调初始参数最后调观测参数。噪声参数决定了滤波器对IMU的信任程度如果设得太小滤波器过度信任IMU观测拉不回来如果设得太大滤波器过度信任观测IMU的平滑作用就没了。我一般从数据手册的值开始然后根据实际轨迹的平滑程度微调。初始参数影响收敛速度。P设得大收敛快但初期抖动大P设得小收敛慢但初期平滑。我一般把位置的初始方差设成1.0速度设成1.0姿态设成0.1零偏设成0.01。这些值在大多数场景下都能工作。观测参数影响滤波器的鲁棒性。GPS的R设成1.0到10.0之间视觉的R设成0.01到0.1之间磁力计的R设成0.01到0.1之间。如果观测数据质量差R要设大一点。如果观测数据质量好R可以设小一点。6.3 仿真验证与实飞测试代码写完之后先在仿真里跑一遍。Gazebo里可以加载一个无人机模型给它加IMU噪声和GPS噪声然后看ESKF的输出跟真值的偏差。仿真里可以随便折腾把噪声调到很大看滤波器会不会发散。如果仿真里都跑不通实飞肯定没戏。仿真通过之后先做地面测试。把IMU和GPS放在车上推着车走一圈看轨迹是否闭合。地面测试能发现很多问题比如时间同步、坐标系定义、单位换算。我见过有人把加速度单位搞错了把g当成米每二次方秒结果位置漂到天上去了。地面测试没问题了再上实飞。实飞先从悬停开始看姿态是否稳定。然后做小范围航线看位置是否准确。最后做大机动看滤波器是否跟得上。每次飞行都录bag回来慢慢分析。我习惯把ESKF的输出跟GPS和视觉的原始数据画在一起一眼就能看出谁有问题。注意实飞测试一定要在开阔场地远离人群和建筑物。磁力计在建筑物附近会受干扰GPS在树林里会丢星。这些环境因素会让调试变得非常困难。7. 多传感器融合的进阶玩法7.1 LiDAR与IMU的紧耦合LiDAR和IMU的融合是最近几年的热点。松耦合的做法是LiDAR跑自己的里程计然后把结果喂给ESKF。紧耦合的做法是把LiDAR的特征点残差直接写进ESKF的观测方程。紧耦合精度更高但实现难度也大很多。我试过用VINS-Fusion的思路来做LiDAR-IMU融合把LiDAR的点云特征跟视觉特征一起优化。效果确实好但计算量也上去了。如果你的平台算力足够紧耦合是值得的。如果算力有限松耦合也能用只是精度差一点。LiDAR-IMU标定是另一个坑。LiDAR和IMU之间的外参旋转和平移必须标定准确不然融合出来的轨迹会有系统性偏差。我一般用lidar_imu_calib包来做这件事它通过手眼标定的方法来估计外参。标定的时候要让设备做充分的旋转和平移激励所有自由度。7.2 零速修正与运动约束零速修正ZUPT是ESKF里一个非常实用的技巧。当无人机在地面静止时速度应该为零。如果你能检测到静止状态就可以把速度观测设为零喂给ESKF。这个观测能极大地抑制零偏漂移因为零偏的主要影响就是让速度漂移。静止检测的方法很简单看IMU的加速度模长是否接近重力加速度看角速度模长是否接近零。如果连续几十个样本都满足条件就判定为静止。这时候把速度观测的R设得很小让滤波器相信速度确实为零。运动约束是另一个技巧。比如无人机在飞行时侧向速度通常很小除非在做侧滑。你可以把侧向速度设为零观测抑制侧向漂移。这个约束在固定翼无人机上特别有用因为固定翼的侧滑通常很小。7.3 在线零偏估计与温度补偿消费级IMU的零偏随温度变化很明显。冷启动的时候零偏可能跟热平衡后差好几度每秒。如果你的飞行时间超过几分钟温度漂移就会成为主要误差源。解决办法是在线估计零偏的温度系数。你可以把零偏建模成温度的线性函数把温度系数也放进状态向量里。这样滤波器在估计零偏的同时也在估计零偏随温度的变化率。实现起来稍微复杂一点但效果很明显。如果没有温度传感器也可以用时间来做粗略补偿。冷启动后的前几分钟零偏变化最快你可以给零偏的过程噪声设大一点让滤波器快速跟踪零偏变化。等温度稳定了再把过程噪声调小。8. 我踩过的那些坑与实战心得8.1 坐标系定义混乱导致的诡异bug坐标系是ESKF里最容易出错的地方。世界坐标系、机体坐标系、IMU坐标系、GPS坐标系、视觉坐标系每一个都有自己的定义。如果你搞混了滤波器输出就会乱七八糟。我踩过最惨的一次坑是把IMU的加速度方向搞反了。IMU的加速度计测量的是比力静止时读数是正g指向天空但我在代码里把它当成了负g。结果无人机一起飞速度就往下掉位置直接钻地。排查了一整天最后用rqt_plot把加速度和速度画出来才发现符号反了。还有一次是GPS的坐标系问题。GPS给的是经纬高我直接当成局部坐标用了结果位置差了十万八千里。后来用geographic_msgs转成局部ENU坐标才搞定。这个坑很常见新手一定要小心。提示每次定义一个新的坐标系都在纸上画出来标清楚三个轴的方向。代码里加注释写明每个变量的坐标系。这个习惯能帮你省下大量调试时间。8.2 时间同步问题的隐蔽性时间同步问题非常隐蔽因为短时间内看不出来。IMU和GPS的时间戳差了几十毫秒滤波器会把GPS的位置跟几十毫秒前的IMU状态做差残差算错了但滤波器还是能收敛只是精度差一点。跑个几分钟误差累积起来轨迹就歪了。我一般用ROS的message_filters来做时间对齐把IMU和GPS的时间戳对齐到最近的时刻。如果时间差超过阈值就丢弃这次观测。这个阈值一般设成10毫秒。如果你的传感器时间戳不准可以考虑用硬件触发来同步或者用PPS信号来校准。还有一个坑是ROS的时间戳单位。ROS 1用秒和纳秒两个字段ROS 2用纳秒一个字段。如果你在ROS 1和ROS 2之间移植代码时间戳处理要特别小心。8.3 参数调优的经验法则调参这件事说难也难说简单也简单。我的经验法则是先让滤波器跑起来再让它跑准。刚开始的时候把过程噪声Q设大一点观测噪声R也设大一点让滤波器对IMU和观测都不太信任这样不容易发散。等滤波器稳定了再慢慢调小Q和R提高精度。姿态的初始方差要设得比位置和速度小因为姿态通常有磁力计或者视觉来观测收敛快。零偏的初始方差要设得比姿态还小因为零偏是缓变量不需要快速收敛。如果滤波器输出抖动厉害先把R调大。如果滤波器输出滞后厉害先把Q调大。如果滤波器输出漂移厉害先把零偏的过程噪声调大。这三条法则能解决80%的调参问题。8.4 从理论到代码的最后一公里理论推导再漂亮代码写不出来也是白搭。我见过很多人ESKF的公式推得滚瓜烂熟但一到写代码就卡壳。问题出在理论推导用的是连续时间代码实现用的是离散时间。你得把连续时间的微分方程离散化才能写进代码。离散化的方法有很多种最简单的是欧拉法精度一般但实现简单。中值法精度高一点但需要保存上一时刻的状态。龙格库塔法精度最高但计算量大。我一般用中值法在精度和计算量之间取个平衡。还有一个坑是四元数的更新顺序。是先更新姿态再更新零偏还是先更新零偏再更新姿态顺序不同结果会有细微差别。我一般先更新零偏再用去零偏后的角速度更新姿态。这个顺序跟理论推导一致不容易出错。最后再分享一个小技巧把ESKF的输出跟原始IMU积分的结果画在一起。如果ESKF的输出跟原始积分差很多说明滤波器在起作用。如果两者几乎一样说明滤波器没起作用可能是观测更新没生效。这个对比能帮你快速判断滤波器是否正常工作。
返回列表