
1. 误差状态卡尔曼滤波到底是什么做机器人定位、无人机姿态融合、或者视觉惯性里程计VIO的时候绕不开一个名字误差状态卡尔曼滤波Error-State Kalman FilterESKF。我最早接触它也是被项目逼的——那时候用标准卡尔曼滤波直接估计四元数结果各种幺蛾子四元数归一化问题、状态协方差被零运动更新拖得越来越小最后滤波发散。后来换成误差状态结构很多坑莫名其妙就填平了。这篇文章我打算把ESKF这件事从直觉讲到数学再把工程落地里的细节全部倒出来。不适合零基础适合已经跑通过标准卡尔曼滤波、但想深入理解或者实际工程化的人也适合做导航、SLAM、自动驾驶传感器融合的同行算是一份可以对着抄作业的技术笔记。1.1 从标准卡尔曼滤波说起要理解误差状态得先回忆一下标准卡尔曼滤波的基础框架。标准的KF假设系统是线性的状态方程和观测方程长得规规矩矩x_k A x_{k-1} B u_k w_k z_k H x_k v_k但现实中的系统没这么老实。拿无人机上的IMU举例状态量包含位置、速度、姿态四元数、加速度计零偏、陀螺仪零偏。姿态演化是一个旋转群的乘积陀螺仪给的角速度还得做指数映射整个过程高度非线性。标准KF要求状态误差的高斯假设成立在强非线性、大机动情况下这个假设经常被打破。后来有了扩展卡尔曼滤波EKF思路是把非线性系统在当前状态附近做一阶泰勒展开线性化之后套KF的框架。EKF在工程上用得很广但有两个绕不开的痛点第一四元数状态必须强制归一化否则姿态协方差会越来越不靠谱第二状态量之间量纲差太多一个协方差矩阵里既要装位置的米又要装姿态的“弧度”数值上容易病态。1.2 误差状态的直觉不直接估计状态估计误差ESKF的思路换个角度看就清楚了主力系统用一个非线性模型在跑这个叫做“标称状态”它是当前最可信的状态估计。但标称状态不可能完全正确和真值之间的差异才是我们真正建模、用滤波器去估计的东西——这部分叫“误差状态”。这么说可能有点绕打个比方你在操场上闭着眼往前走每走一步都可能偏一点但你手上有一个很粗糙的指南针和计步器可以估算出自己大概的位置这是标称状态。同时你口袋里揣着一个精确的GPS每隔几秒告诉你一次真实位置和估算位置的偏差你要做的是修正“偏差”这个量而不是重新算整个路径。ESKF的精髓就在这儿用非线性模型给“大概在哪”滤波器只负责估计“偏差有多大”。误差很小的时候系统可以理解成一个几乎线性的动态系统高斯假设也合理得多。1.3 为什么误差状态更适合IMU和姿态系统直接估计四元数是个麻烦事因为四元数本质上是单位球面上的点不是普通欧氏空间里的向量。滤波器的更新是“加一个高斯噪声”的概念但四元数做“加法”是什么没人能说清楚。你得用四元数乘法来叠加一个微小旋转这个微小旋转恰恰就是误差状态。误差状态卡尔曼滤波等于把状态空间拆成了两块标称状态在流形上几何结构正确和误差状态在切空间里是一个普通向量可以随便加减乘除高斯噪声随便加。这样一来四元数的单位约束天然满足因为姿态误差是一个局部的微小旋转用三维向量表示注入回标称四元数时做一次归一化乘法就可以了。它另一个隐形好处是误差状态的量级通常很小线性化近似远比直接EKF准确。大姿态偏差下EKF容易飘但ESKF里的“误差”永远是小量一阶近似的误差就被压得很低。2. 为什么误差状态更好数学思想与选型对比2.1 流形与切空间ESKF的数学根源理解ESKF绕不开一个概念状态空间不是平直的向量空间而是一个流形。举个最简单的例子姿态的集合是SO(3)旋转群它是一个三维流形嵌在九维旋转矩阵空间里或者说是四维四元数空间里的一张三维曲面。你站在曲面上某个点这个点就是一个姿态你可以在这一点附近走一小步但不能随便往曲面外面跳。每点的“一小步”方向构成的线性空间叫切空间。误差状态就生活在这个切空间里。切空间是普通的三维向量空间可以做加法、减法、数乘也能套高斯噪声。ESKF的核心操作就是在流形上做标称状态的积分非线性在切空间里做误差状态的更新线性。等小球在碗底滑动一样碗底附近足够平滑任何微小的移动都可以在切线方向近似得很好。误差状态就是这个“微小移动”它越小线性近似越准。2.2 三种状态表示方式对比工程上常用的有三种做法。直接法是直接估计全量状态误差法就是我们讨论的ESKF还有一种混合法其实ESKF本质上就是混合法但具体实现时结构会有差别。方案优点缺点适用场景直接EKF实现简单资料多四元数约束难处理大误差时线性化差小角度、低机动场景ESKF精度高约束自然满足线性化误差小实现稍复杂数学门槛高无人机、VIO、自动驾驶无迹/粒子滤波非线性适应强计算量大粒子退化调参困难强非线性、小规模状态我实际做过对比测试同样的IMU数据直接EKF在大幅快速转动的时候姿态估计偶尔会跳变换成ESKF之后曲线明显顺滑很多而且协方差估计更可信。做传感器融合的想省心的话从ESKF入门其实比从直接EKF慢慢改成ESKF更划算。2.3 误差状态的三个实际优势第一个优势是单位约束天然满足。四元数标称状态每次用旋转积分更新后归一化一次误差状态始终是一个三维小量更新后注入标称状态再做一次归一化不会出现四元数越飞越偏、最后协方差矩阵奇异的问题。第二个优势是线性化误差小。ESKF估计的对象是“误差”。误差很小所以在误差附近做的一阶线性化几乎误差平方级别的增大都可以忽略。打个比方你要估计一座山的轮廓直接在三维空间里建模型误差很大但如果你已经知道它大概是个圆锥只需要估计表面粗糙度的那几毫米偏差线性模型就已经非常够用了。第三个优势是数值稳定性好。位置状态是米量级速度是米每秒量级姿态误差是毫弧度级。ESKF的协方差矩阵反映的是“误差”的方差不会出现一个矩阵里同时塞着“几十米的位置不确定度”和“几个毫弧度的姿态不确定度”这种量纲悬殊到影响矩阵运算的情况。3. 完整推导与算法流程3.1 符号定义与状态表示约定先说好符号约定后面所有推导都按这套来。标称状态记作x真实状态记作x_t误差状态记作δx它们之间的映射关系是真实位置 p_t p δp 真实速度 v_t v δv 真实姿态 q_t q ⊗ δq 陀螺零偏 b_g,t b_g δb_g 加速度零偏 b_a,t b_a δb_a注意姿态这里用的是左乘还是右乘约定不同文章可能不一样我用的是右乘真实姿态等于标称姿态乘以一个微小旋转δq。这个微小旋转用旋转向量参数化即δq ≈ [1, δθ/2]^T其中δθ是三维向量。⊕符号表示广义的“真状态 标称状态 ⊕ 误差状态”x_t x ⊕ δx对于向量部分就是加法对于姿态部分就是四元数乘法。这个约定在代码里非常关键搞混左乘右乘会导致整个滤波器发散。3.2 标称状态的传播预测过程标称状态的预测由IMU驱动。IMU的测量模型如下加速度计测量a_m R^T (a - g) b_a n_a 陀螺仪测量ω_m ω b_g n_g其中R是机体坐标系到世界坐标系的旋转矩阵g是世界坐标系下的重力向量。整理一下加速度的真值a等于a R (a_m - b_a) g标称状态在两次IMU测量之间的积分离散形式可以写成p ← p v Δt 0.5 (R (a_m - b_a) g) Δt^2 v ← v (R (a_m - b_a) g) Δt q ← q ⊗ Δq(ω_m - b_g, Δt)这里Δq(ω, Δt)表示角速度在Δt内积分得到的旋转增量四元数用旋转向量的指数映射计算。零偏的标称值在预测阶段保持不变一般用随机游走模型均值不漂移。3.3 误差状态的线性化动力学误差状态要建模成线性系统。把公式里真值全写成标称加误差然后做一阶泰勒展开去掉二阶以上小量整理后得到δp ← δp δv Δt δv ← δv (-R [a_m - b_a]_× δθ - R δb_a) Δt v_i δθ ← δθ - [ω_m - b_g]_× δθ - δb_g Δt θ_i δb_a ← δb_a a_i δb_g ← δb_g g_i其中[·]_×是三维向量的反对称矩阵对应叉乘操作。v_i, θ_i, a_i, g_i分别是速度和姿态误差传播时叠加的高斯噪声项它们的协方差来自IMU噪声和随机游走噪声。好看到反对称矩阵可能有点劝退但本质上这套方程说的就是小误差的传播是线性的因为所有量都是小量乘积可以忽略。IMU的噪声不断注入误差状态所以误差的协方差在预测阶段会逐渐变大。3.4 观测更新与状态注入观测更新用的是标准卡尔曼更新公式但它作用的对象是误差状态。假设我们有一个位置观测比如GPS或视觉定位给出的平移真值满足z p_t n p δp n所以观测残差是y z - p δp n观测模型在这个例子中就是H [ I3 0 0 0 ]这是最简的情况观测方程在误差状态下是线性的。ESKF中很多观测方程线性化后形式都非常干净因为误差量本身就是微小量。卡尔曼更新的常规流程S H P H^T R K P H^T S^{-1} δx K y P ← (I - K H) P然后做状态注入p ← p δp v ← v δv q ← q ⊗ δq b_a ← b_a δb_a b_g ← b_g δb_g最后误差状态归零协方差矩阵保持不变因为误差的期望被“重置”为零了。注意姿态注入之后一定要重新归一化四元数。这一步看似简单实际很多新手栽在这里。3.5 算法流程总结完整流程整理成一个伪代码方便实现时对照初始化 x x_0, P P_0, δx 0 每来一帧IMU预测 用 a_m, ω_m 更新标称状态 p, v, q 用误差状态线性方程更新 P 误差状态 δx 保持为 0 每来一帧观测更新 计算残差 y z - h(x) 计算雅可比矩阵 H 计算卡尔曼增益 K 计算误差状态 δx K y 注入x ← x ⊕ δx 更新 P ← (I - K H) P 零化δx ← 0这个流程看起来和普通EKF区别不大但注意它把“预测非线性”和“更新线性”分开处理数学上有清晰的结构。下面第4节讲工程落地时无数样例证明这套分离能让系统稳定性上一个大台阶。4. 工程落地中的关键细节与避坑4.1 IMU零偏的估计与状态扩增理论上ESKF的误差状态必须包含IMU零偏因为陀螺仪和加速度计零偏随时间缓慢漂移不估计它的话标称状态积分误差会越攒越大姿态和位置全会偏掉。工程上推荐的做法是一开始就把零偏放进状态向量δx [δp, δv, δθ, δb_a, δb_g]^T零偏的协方差初始值设成IMU出厂标定给的随机游走强度单位是m/s^3和rad/s^2这两个量是加速度计和陀螺仪零偏随机游走的参数。不知道怎么设的时候查IMU官方数据手册里的 “Random Walk” 参数通常给的是密度乘以时间再平方转方差。这一步别省直接决定滤波器长跑长时间之后的稳定性。4.2 观测更新里的残差方向问题不少人写ESKF更新时容易在观测残差的方向上犯错。以位置观测为例残差方向是“观测值减预测值”这个没问题。但姿态观测要小心比如视觉SLAM给出一个旋转矩阵你要先算标称姿态的逆乘以观测姿态再转成旋转向量最后决定是加还是减。很多实现里符号一错滤波器的姿态就开始振荡最后发散。一个经验法则所有观测残差都要写成“真值减去标称值”的形式也就是y z_true - h(x_nominal)如果观测本身是姿态残差定义为y_theta Log( R_obs^T R_nominal )这个三维向量表示观测姿态与标称姿态之间的微小旋转偏差方向约定要和误差状态定义对应。调试的时候在纸上把约定写清楚能省后面大概三个通宵。4.3 协方差矩阵的初始化与调参协方差矩阵初始化会影响滤波器的收敛速度。P0给得太小滤波器对自己初始状态过于自信前几帧观测更新不敏感给得太大开始阶段观测噪声占比过高容易出现抖动。位置和速度的初始协方差按传感器精度设一般位置给0.1到1米的平方速度给0.1到1米每秒的平方。Q矩阵和R矩阵的调参更头疼。我的经验是先从IMU数据手册的噪声密度换算起再在实际数据上微调。调参的时候想象中的Q影响是加速度噪声密度决定速度和位置状态的权重陀螺噪声密度决定姿态状态的权重零偏随机游走决定长期稳定性调试顺序建议先调陀螺方差确保静态时姿态不发散、不漂移再调加速度方差看速度位置是否跟得稳最后调零偏随机游走跑一个几十分钟的静止数据看位置曲线是否平稳。4.4 常见问题排查速查表现象可能原因排查方法姿态发散误差状态左右乘约定错残差符号反检查姿态观测残差方向打日志看残差均值是否在零附近位置缓慢漂移零偏随机游走设太大/太小IMU噪声模型不准静止采集1小时数据对比真实位置估计曲线协方差奇异长期没有观测更新Q矩阵太小增大Q或加入虚拟观测做约束更新后状态跳变状态注入和残差方向不一致单步调试打印更新前后状态变化数值发散时间步长采用方式不对检查是否用零阶保持积分Δt 是否过大遇到发散先不要怀疑算法本身先从符号约定、单位转换这两个地方查起80%的问题出在这两块。4.5 一个小坑IMU数据时间间隔IMU通常跑在100Hz到1000Hz观测视觉/GPS通常只有10Hz到30Hz。ESKF的预测和更新解耦天然适合这种异步频率。但要注意IMU数据的时间戳必须精确否则积分用的 Δt 不准确等效于给系统注入额外噪声。处理办法是在驱动层用硬件时间戳对齐并做好去重和补齐。还有一点观测数据进来之前标称状态必须已经积分到“观测时刻”。有些IMU和相机时间戳不齐直接用临近的IMU状态做更新会产生额外误差这个在视觉惯性系统中尤其明显。5. 实际应用从代码到场景5.1 一个最小Python实现的核心片段用ESKF做位置观测滤波核心骨架可以压缩在几十行代码里。下面这个例子我平时用来做实验验证只看姿态和位置的融合。import numpy as np class ESKF: def __init__(self, dim15, imu_noise0.1): # 状态: [p, v, theta, ba, bg] self.nominal np.zeros(10) # 标称状态, 姿态用4元素 self.nominal[6] 1.0 # 四元数初始化为单位四元数 self.P np.eye(dim) * 0.1 self.Q np.eye(dim) * imu_noise self.dim dim def predict(self, acc, gyro, dt): # 标称状态积分 p, v, q self.nominal[:3], self.nominal[3:6], self.nominal[6:] R self.quat_to_R(q) a R (acc - self.nominal[8:]) p v * dt 0.5 * a * dt * dt v a * dt q self.quat_mul(q, self.quat_from_axis(gyro * dt)) q / np.linalg.norm(q) self.nominal[:3] p; self.nominal[3:6] v; self.nominal[6:] q # 误差状态协方差传播, 这里省略F和G的完整推导 F self.compute_F(acc, gyro, dt) self.P F self.P F.T self.Q def update(self, z_pos, R_obs): H np.zeros((3, self.dim)) H[:, :3] np.eye(3) S H self.P H.T 0.01 K self.P H.T np.linalg.inv(S) y z_pos - self.nominal[:3] delta_x K y # 误差注入标称状态 self.nominal[:3] delta_x[0:3] self.nominal[3:6] delta_x[3:6] self.nominal[6:] self.quat_mul( self.nominal[6:], self.quat_from_axis(delta_x[6:9])) self.nominal[6:] / np.linalg.norm(self.nominal[6:]) self.P (np.eye(self.dim) - K H) self.P这只是一个演示骨架工程化时还要将compute_F补完整加入零偏状态把IMU噪声和零偏随机游走分开建模处理时间戳对齐逻辑。从骨架到可用的系统中间大概还有两三天开发量但核心逻辑就是这个。5.2 视觉惯性里程计里的ESKF扩展VIO是ESKF最典型的应用场景之一它把视觉观测作为更新源。视觉特征观测通常是重投影误差它的雅可比矩阵要从相机模型推起直接和误差状态挂钩。ESKF在这里的优势体现得特别清楚视觉重投影误差本质上是图像坐标的小量偏差误差状态模型下雅可比推导非常自然。工程上还有一个常用的扩展把相机位姿估计作为观测。比如先跑一个视觉SLAM得到相机位姿然后回灌给ESKF做位置姿态修正。这时观测方程是6维的平移3维、旋转3维H矩阵可以直接通过误差状态的定义写出保持不变性性质不错。5.3 与GPS融合时要注意什么GPS的频率低更新间隔大IMU在两次GPS之间要积分很久位置误差累积会偏大。这种情况下有两个技巧一是把GPS的位置延迟补偿到IMU积分时刻确保状态和观测对齐二是GPS高度通道噪声通常比水平大很多R矩阵不能简单设成各向同性要根据GPS的HDOP和VDOP来分配。另外GPS的跳变偶尔会出现观测更新前最好加一个马氏距离异常检测残差超过三倍标准差就丢掉这一帧。这个技巧能防止位置异常跳变带崩滤波器我在城市峡谷场景里实测很有用。5.4 实测下来最重要的心得说一个踩过的坑。最初我把“标称状态更新”和“误差状态协方差更新”的时间步长搞混了IMU在100Hz跑预测步正常但后面加入视觉更新时直接用了一个过时的标称状态结果位置老是慢半拍。后来改成观测帧到达时先检查时间戳如果观测时间晚于当前状态时间就把IMU预测补到观测时间再做更新一切恢复正常。做传感器融合数据的时间和坐标系管理往往占了一半的精力。ESKF本身数学是正确的但前提是喂给它的数据在时间上对齐、坐标系上统一。遇到问题先怀疑数据链路再怀疑滤波算法排查顺序真的很重要。另外一个心得是零偏估计不要一开始就放太强的信任。ESKF的零偏估计收敛需要几秒钟的激励运动如果传感器一直静止不动零偏状态是不可观的。所以系统启动阶段可以故意做一点小旋转激励让滤波器把IMU零偏收敛出来后面长跑姿态才稳。