1. 误差状态卡尔曼滤波到底是什么
做机器人定位、无人机姿态融合、或者视觉惯性里程计(VIO)的时候,绕不开一个名字:误差状态卡尔曼滤波(Error-State Kalman Filter,ESKF)。我最早接触它也是被项目逼的——那时候用标准卡尔曼滤波直接估计四元数,结果各种幺蛾子:四元数归一化问题、状态协方差被零运动更新拖得越来越小,最后滤波发散。后来换成误差状态结构,很多坑莫名其妙就填平了。
这篇文章我打算把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, dim=15, imu_noise=0.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零偏收敛出来,后面长跑姿态才稳。