状态估计与导航滤波这个话题,我在做飞控和组合导航那几年里反复啃过很多遍。每次带新人,我都会先问一个问题:你觉得卡尔曼滤波到底在干什么?十有八九的回答是"融合传感器数据"。这个答案不算错,但太浅了。真正做过工程的人会告诉你,卡尔曼滤波的本质是在"预测"和"观测"之间做一场带权重的博弈,而权重从哪来?从协方差矩阵来。协方差怎么传播?从系统模型来。所以整条链路的核心其实是三件事:状态怎么定义、噪声怎么建模、连续系统怎么离散化。这三件事任何一件搞错,滤波器要么发散,要么输出一条看起来平滑但完全错误的结果。
这篇内容我打算把状态估计与导航滤波这条线从头到尾捋一遍,重点放在工程实现里最容易翻车的地方。涉及的知识点包括卡尔曼滤波的基本框架、EKF的线性化处理、四元数姿态表示与更新、连续到离散的转换方法、Eigen库中四元数的实际用法,以及roll/pitch/yaw与四元数之间的互转。适合正在做飞控、机器人定位、组合导航或者任何需要姿态解算的读者。不管你是刚接触滤波的新手,还是已经写过几版滤波器但总觉得哪里不对的老手,应该都能从里面找到一些有用的东西。
1. 卡尔曼滤波到底在解决什么问题
1.1 从"两个都不准的测量"说起
先抛开公式,讲一个最直观的场景。你有一个陀螺仪,它测量角速度,积分之后能得到角度。但陀螺仪有零偏,积分会漂移,时间越长越离谱。你还有一个加速度计,静止时它能通过重力方向算出倾角,长期来看是准的,但振动一大就全是噪声。现在问题来了:这两个传感器单独用都不行,怎么把它们合起来得到一个又稳又准的角度?
这就是卡尔曼滤波要解决的核心问题。它做的事情可以概括成一句话:用系统模型做预测,用观测做修正,修正的力度由两者的不确定度决定。谁的噪声大,谁的话语权就小。陀螺仪短期准,那就让它在短时间尺度上主导;加速度计长期准,那就让它在长时间尺度上慢慢把漂移拉回来。
我第一次真正理解这一点,是在把一维卡尔曼滤波器的协方差更新手算了一遍之后。当时用的是一个最简单的角度估计模型,状态只有角度和零偏两个量。手算完那一遍,我才明白为什么Q矩阵(过程噪声)和R矩阵(观测噪声)的比值比它们的绝对值更重要。很多人调滤波器调不出来,就是因为一直在调绝对值,而没有关注两者的相对关系。
1.2 预测与更新的博弈逻辑
卡尔曼滤波的完整流程分两步:预测和更新。预测步用状态转移矩阵F把上一时刻的状态推到当前时刻,同时把协方差P也推过去,并且加上过程噪声Q。更新步用观测矩阵H把状态映射到观测空间,计算卡尔曼增益K,然后用观测残差去修正状态和协方差。
这里有一个非常关键的直觉:卡尔曼增益K本质上是一个权重,它决定了你多大程度上相信观测。当观测噪声R很大时,K会变小,滤波器更相信预测;当预测协方差P很大时,K会变大,滤波器更相信观测。这个博弈每一帧都在重新进行,所以滤波器是自适应的。
我在实际项目里遇到过一个典型问题:滤波器在静止时表现很好,一旦运动起来就跟不上。排查了很久才发现是Q矩阵设得太小,导致预测协方差增长太慢,滤波器过度相信预测,对快速变化的观测反应迟钝。把Q调大之后,跟踪性能立刻改善,但静止时的噪声也变大了。这就是一个典型的权衡,没有免费午餐。
1.3 为什么线性卡尔曼滤波不够用
标准卡尔曼滤波要求系统是线性的,也就是说状态转移和观测方程都必须是线性关系。但姿态估计这件事天然是非线性的。四元数的更新涉及四元数乘法,从四元数到欧拉角的转换涉及三角函数,从加速度计观测反推姿态也涉及非线性关系。所以必须用扩展卡尔曼滤波(EKF),也就是在工作点附近做一阶泰勒展开,用雅可比矩阵代替线性矩阵。
EKF的代价是引入了线性化误差。当状态偏离工作点较远时,线性化就不准了,滤波器可能发散。这也是为什么EKF在姿态估计里通常配合小角度假设使用,或者用误差状态的形式来做,让线性化始终在零点附近进行。误差状态卡尔曼滤波(ESKF)就是专门解决这个问题的,后面会详细讲。
2. 四元数:姿态表示的最优解还是必要之恶
2.1 欧拉角的万向锁问题
很多人一开始做姿态解算,第一反应是用欧拉角,因为roll、pitch、yaw三个角直观易懂。但欧拉角有一个致命问题:万向锁。当pitch角接近正负90度时,roll和yaw会耦合在一起,失去一个自由度。这在数学上表现为旋转矩阵出现奇异,在工程上表现为姿态解算突然跳变或者失控。
我在做一款云台控制的时候踩过这个坑。当时用欧拉角做姿态表示,云台俯仰到接近垂直时,航向角突然开始乱转,整个控制环路直接震荡。后来换成四元数,问题立刻消失。从那以后,我在任何涉及三维旋转的场合都优先用四元数,欧拉角只在最后显示给用户看的时候才转换出来。
2.2 四元数的数学本质
四元数可以理解为一个标量加一个三维向量,形式是q = w + xi + yj + zk,其中i、j、k是虚数单位,满足i² = j² = k² = ijk = -1。单位四元数可以表示三维空间中的任意旋转,而且没有奇异点。
四元数表示旋转的几何意义是:绕某个轴旋转某个角度。如果旋转轴是单位向量(a, b, c),旋转角是θ,那么对应的四元数是:
q.w = cos(θ/2) q.x = a * sin(θ/2) q.y = b * sin(θ/2) q.z = c * sin(θ/2)注意这里用的是半角,这是四元数的一个特点。半角的来源是四元数到旋转矩阵的转换关系,本质上是因为四元数表示的是旋转的双覆盖,q和-q表示同一个旋转。
2.3 四元数更新与微分方程
四元数的运动学方程是:
q_dot = 0.5 * q ⊗ ω其中ω是角速度四元数(0, ωx, ωy, ωz),⊗表示四元数乘法。这个方程是连续的,实际实现时需要离散化。最简单的离散化方法是零阶保持:
q(k+1) = q(k) + 0.5 * T * q(k) ⊗ ω(k)其中T是采样周期。但这样更新之后四元数会失去单位性,需要重新归一化。归一化的方法很简单,就是除以模长:
q = q / ||q||我在实际使用中发现,归一化的频率很关键。如果每步都归一化,计算量会大一些但精度好;如果隔几步归一化一次,计算量小但误差会累积。对于一般的飞控,每步归一化是标准做法,因为四元数乘法的计算量本身就不大。
2.4 Eigen库中的四元数实操
Eigen是C++里做线性代数和四元数运算最常用的库。它的四元数类叫Quaterniond(双精度)或Quaternionf(单精度)。下面是一些我常用的操作:
#include <Eigen/Dense> #include <Eigen/Geometry> // 构造四元数,注意Eigen的构造函数顺序是(w, x, y, z) Eigen::Quaterniond q(1.0, 0.0, 0.0, 0.0); // 单位四元数 // 从旋转向量构造(旋转向量是轴角表示,模长是角度) Eigen::Vector3d axis_angle(0.1, 0.2, 0.3); Eigen::Quaterniond q2(Eigen::AngleAxisd(axis_angle.norm(), axis_angle.normalized())); // 四元数乘法 Eigen::Quaterniond q3 = q * q2; // 归一化 q3.normalize(); // 转旋转矩阵 Eigen::Matrix3d R = q3.toRotationMatrix(); // 从旋转矩阵转四元数 Eigen::Quaterniond q4(R);这里有一个很容易踩的坑:Eigen的Quaterniond构造函数参数顺序是(w, x, y, z),但它的coeffs()方法返回的顺序是(x, y, z, w)。我在第一次用的时候就被这个坑过,把coeffs()的输出直接传给构造函数,结果姿态完全不对。后来养成了一个习惯,凡是涉及四元数存取的地方,都显式写出w、x、y、z,不用coeffs()。
3. 连续系统到离散系统的转换
3.1 为什么必须做离散化
卡尔曼滤波的推导是在连续时间下进行的,但实际实现一定是在离散时间下。传感器以固定频率采样,处理器以固定周期运行,所以必须把连续的系统模型转换成离散的。这个转换不是简单地把微分换成差分,而是要保持系统的动态特性一致。
对于线性系统x_dot = Ax + Bu,离散化后的形式是x(k+1) = F x(k) + G u(k),其中F = exp(AT),G = ∫exp(Aτ)dτ * B。这个积分在A可逆时有闭式解,但A不可逆时需要用级数展开或者数值积分。
3.2 一阶保持与零阶保持
最常用的两种离散化方法是零阶保持(ZOH)和一阶保持(FOH)。零阶保持假设输入在采样周期内保持不变,适用于大多数传感器输入。一阶保持假设输入在采样周期内线性变化,精度更高但计算更复杂。
对于姿态估计,角速度输入通常用零阶保持就够了,因为采样频率一般都在100Hz以上,一个周期内角速度变化很小。但如果采样频率很低,比如10Hz,那就需要考虑一阶保持了。
我在做一款低成本IMU的时候,采样频率只有50Hz,用零阶保持离散化,结果在快速转动时姿态误差明显。后来改用一阶保持,误差减小了大约30%。所以离散化方法的选择要根据实际采样频率来定,不能一概而论。
3.3 泰勒展开法的实际应用
对于非线性系统,精确的离散化很难做到,通常用泰勒展开近似。一阶泰勒展开就是:
F ≈ I + A*T二阶泰勒展开是:
F ≈ I + A*T + 0.5*(A*T)²对于姿态估计,一阶展开通常就够了,因为采样周期T很小,A*T的高阶项可以忽略。但如果T比较大,或者A的元素很大,就需要用二阶甚至更高阶。
我在实际项目里一般用二阶展开,因为计算量增加不多,但精度提升明显。特别是当角速度比较大的时候,一阶展开的误差会累积得很快。
3.4 离散化中的常见错误
最常见的错误是忘记更新协方差矩阵的离散形式。连续系统的过程噪声是Qc,离散化后应该是Qd = Qc * T(一阶近似)或者更精确的Qd = ∫F(τ)QcF(τ)^T dτ。很多人直接把Qc当Qd用,结果滤波器的噪声特性和预期完全不符。
另一个常见错误是状态转移矩阵和噪声矩阵的离散化方法不一致。比如F用二阶展开,Qd却用一阶近似,这会导致协方差传播的精度不匹配。我的建议是两者用同阶的近似,保持一致性。
4. EKF在姿态估计中的完整实现
4.1 状态定义与误差状态
做EKF的第一步是定义状态。对于姿态估计,最直接的状态是四元数,但四元数有四个分量且满足单位约束,直接作为状态会导致协方差矩阵奇异。所以通常用误差状态的形式,也就是ESKF。
ESKF的核心思想是:名义状态用四元数表示,误差状态用三维旋转向量表示。误差状态只有三个分量,没有约束,可以直接用标准卡尔曼滤波处理。每次更新完之后,把误差状态注入名义状态,然后误差状态清零。
这种做法的好处是线性化始终在零点附近进行,避免了四元数约束带来的问题。我在做组合导航的时候,ESKF是标配,几乎没有人直接用四元数做状态。
4.2 预测步的雅可比矩阵推导
预测步需要计算状态转移矩阵对误差状态的雅可比。对于误差状态δθ,其运动学方程是:
δθ_dot = -[ω]× δθ + nω其中[ω]×是角速度的反对称矩阵,nω是陀螺仪噪声。离散化后:
δθ(k+1) = (I - [ω]× T) δθ(k) + nω T所以状态转移矩阵F = I - [ω]× T,噪声矩阵G = I * T。
这个推导看起来简单,但实际写代码的时候很容易把符号搞错。反对称矩阵的定义是:
[ω]× = [ 0 -ωz ωy ] [ ωz 0 -ωx ] [-ωy ωx 0 ]我在第一次写的时候把符号弄反了,结果滤波器在转动时误差越来越大。后来对着推导一步步检查,才发现是反对称矩阵的符号问题。这种错误很隐蔽,因为静止时完全看不出来,只有运动时才暴露。
4.3 观测步:加速度计与磁力计
观测步需要定义观测模型。加速度计观测的是重力方向,在机体坐标系下表示为:
a_meas = R^T * g + na其中R是姿态旋转矩阵,g是世界坐标系下的重力向量(0, 0, -9.8),na是加速度计噪声。观测矩阵H是a_meas对误差状态δθ的雅可比。
磁力计观测的是地磁方向,类似地:
m_meas = R^T * m_world + nm观测矩阵的推导涉及旋转矩阵对误差状态的导数,这个推导比较繁琐,但结果是标准的。我建议直接查资料用现成的公式,不要自己从头推,容易出错。
4.4 实测中的调参经验
EKF的调参主要是调Q和R。Q是过程噪声,反映你对陀螺仪的信任程度;R是观测噪声,反映你对加速度计和磁力计的信任程度。
我的经验是:Q不要设得太小,否则滤波器对快速运动响应迟钝;R要根据实际传感器的噪声水平来设,不要凭感觉。最好的方法是先采集静态数据,计算传感器的噪声方差,然后以此为基准设置R。
还有一个技巧是给Q和R加自适应。比如当加速度计的模长偏离重力加速度很多时,说明有运动加速度干扰,这时候应该增大R,降低加速度计的权重。这个技巧在实际项目里非常有用,能显著提升运动状态下的姿态精度。
5. 四元数与欧拉角的互转及显示
5.1 从四元数反解欧拉角
虽然内部用四元数,但显示给用户的时候通常需要欧拉角。从四元数到欧拉角的转换公式是:
roll = atan2(2*(w*x + y*z), 1 - 2*(x² + y²)) pitch = asin(2*(w*y - z*x)) yaw = atan2(2*(w*z + x*y), 1 - 2*(y² + z²))注意pitch用的是asin,因为pitch的范围是[-90°, 90°],而roll和yaw用的是atan2,范围是[-180°, 180°]。
这个公式看起来简单,但有几个坑。第一,asin的输入必须限制在[-1, 1]之间,否则会出现NaN。由于浮点误差,2*(wy - zx)可能略微超出这个范围,所以需要做clamp。第二,当pitch接近正负90度时,roll和yaw会耦合,这是万向锁的表现,无法避免,只能在使用时注意。
5.2 欧拉角到四元数的转换
反过来,从欧拉角到四元数的转换是:
qw = cos(roll/2)*cos(pitch/2)*cos(yaw/2) + sin(roll/2)*sin(pitch/2)*sin(yaw/2) qx = sin(roll/2)*cos(pitch/2)*cos(yaw/2) - cos(roll/2)*sin(pitch/2)*sin(yaw/2) qy = cos(roll/2)*sin(pitch/2)*cos(yaw/2) + sin(roll/2)*cos(pitch/2)*sin(yaw/2) qz = cos(roll/2)*cos(pitch/2)*sin(yaw/2) - sin(roll/2)*sin(pitch/2)*cos(yaw/2)这个转换在初始化的时候很有用,比如你知道初始姿态是水平的,就可以直接用这个公式构造初始四元数。
5.3 旋转顺序的重要性
欧拉角的定义依赖于旋转顺序。常见的顺序有ZYX(先绕Z转yaw,再绕Y转pitch,最后绕X转roll)和XYZ。不同的顺序对应不同的转换公式。我在项目里统一用ZYX顺序,因为这是航空航天领域的标准。
如果你拿到的欧拉角是用XYZ顺序定义的,而你的转换公式是按ZYX写的,结果会完全不对。这个坑我在对接第三方传感器的时候踩过,对方给的欧拉角是XYZ顺序,我按ZYX转成四元数,姿态完全乱了。后来查了手册才发现顺序不一致。
6. 工程实现中的那些坑
6.1 数值稳定性问题
EKF的数值稳定性是一个大问题。协方差矩阵P在理论上应该始终是对称正定的,但由于浮点误差,实际计算中可能失去对称性甚至变成非正定。这会导致卡尔曼增益计算出现异常,滤波器发散。
解决方法有几个。第一,每次更新后强制对称化:P = (P + P^T) / 2。第二,用Joseph形式更新协方差,这个形式在数值上更稳定。第三,定期做Cholesky分解检查,如果失败就重置协方差。
我在一个长时间运行的项目里遇到过协方差矩阵慢慢失去正定性的问题,运行几个小时后滤波器就发散了。后来加了强制对称化和定期检查,问题解决。
6.2 传感器时间同步
多传感器融合的一个大问题是时间同步。陀螺仪、加速度计、磁力计的采样时刻可能不一致,如果直接按同一时刻处理,会引入误差。特别是在高动态场景下,几毫秒的时间差就可能导致明显的姿态误差。
解决方法有两种:硬件同步和软件插值。硬件同步是用同一个时钟触发所有传感器采样,这是最理想的。软件插值是在融合之前,把各传感器的数据插值到同一时刻。我在实际项目里通常用软件插值,因为硬件同步需要传感器支持,不是所有场合都能做到。
6.3 初始对准与零偏估计
滤波器启动时需要初始状态。姿态的初始值可以通过加速度计和磁力计计算,但零偏的初始值通常设为0,让滤波器自己估计。这就涉及初始对准的问题。
初始对准的质量直接影响后续的滤波性能。如果初始姿态误差太大,线性化误差会很大,滤波器可能需要很长时间才能收敛。我的做法是启动时先静止几秒钟,用这段时间的数据做平均,得到一个比较准的初始姿态和零偏估计,然后再启动滤波器。
6.4 计算资源与实时性
EKF的计算量主要来自矩阵运算。对于姿态估计,状态维度是3(误差状态),观测维度是3(加速度计)或6(加速度计加磁力计),矩阵都很小,计算量不大。但如果状态扩展到速度、位置,维度会增加到9甚至15,计算量就上去了。
在嵌入式平台上,我通常用单精度浮点,并且尽量避免动态内存分配。Eigen库支持固定大小的矩阵,用Matrix3f、Matrix<float, 3, 6>这样的类型可以避免堆分配,提升实时性。
7. 从EKF到更高级的滤波方法
7.1 误差状态卡尔曼滤波的完整框架
前面提到了ESKF,这里展开讲一下完整框架。ESKF的状态分为名义状态和误差状态。名义状态包括四元数、速度、位置、零偏等,误差状态包括姿态误差、速度误差、位置误差、零偏误差等。
每次迭代的流程是:先用陀螺仪和加速度计做名义状态的预测,同时用误差状态的线性化模型做协方差预测;然后用观测更新误差状态和协方差;最后把误差状态注入名义状态,误差状态清零。
这个框架的好处是线性化始终在零点附近,误差状态的量级很小,一阶近似就足够精确。我在做组合导航的时候,ESKF是标配,效果比直接EKF好很多。
7.2 无迹卡尔曼滤波的适用场景
UKF用sigma点来传播非线性,不需要计算雅可比矩阵,精度比EKF高一阶。对于强非线性系统,UKF的优势很明显。但UKF的计算量比EKF大,因为每个sigma点都要做一次非线性传播。
在姿态估计里,如果角速度很大或者采样频率很低,EKF的线性化误差会比较大,这时候可以考虑UKF。但我个人经验是,对于一般的飞控和机器人应用,ESKF已经足够好,没必要上UKF。
7.3 互补滤波与卡尔曼滤波的取舍
互补滤波是另一种常用的姿态估计方法,它用一个高通滤波器处理陀螺仪,一个低通滤波器处理加速度计,然后加权融合。互补滤波的计算量比卡尔曼滤波小很多,实现也简单,在资源受限的平台上很有优势。
但互补滤波的权重是固定的,不能自适应。卡尔曼滤波的权重是动态调整的,理论上更优。我的建议是:如果平台资源充足,用EKF或ESKF;如果资源非常受限,用互补滤波也能得到可用的结果。
8. 一些实战中的经验总结
8.1 调试滤波器的正确姿势
调试滤波器最忌讳的是只看最终输出。应该把中间变量都记录下来,包括预测状态、更新状态、卡尔曼增益、协方差矩阵的对角线元素等。这样出问题的时候才能定位到具体是哪一步。
我通常会把数据导出成CSV,用Python画图分析。特别是协方差的对角线元素,如果某个元素的数值异常大或者异常小,说明对应的状态估计有问题。
8.2 数据记录与回放
做滤波器开发,数据回放是必须的。因为滤波器是时序相关的,实时调试很难复现问题。我的做法是把原始传感器数据记录下来,然后离线回放,这样可以反复调试同一段数据,直到滤波器表现满意为止。
记录的数据要包括时间戳、陀螺仪、加速度计、磁力计,如果有GPS或者视觉数据也要记录。时间戳的精度很重要,最好用硬件时钟,不要用系统时间。
8.3 从仿真到实机的过渡
仿真里跑得好的滤波器,到实机上不一定好。因为仿真里的噪声模型是理想的,实机上的噪声可能有色噪声、有零偏、有温漂。所以从仿真到实机,一定要留出足够的调试时间。
我的做法是先在仿真里验证算法逻辑,然后在实机上用静态数据调噪声参数,最后在动态场景下验证性能。每一步都要有明确的验收标准,不能含糊。
8.4 长期运行中的漂移处理
滤波器长时间运行,零偏会慢慢漂移,姿态也会慢慢漂。处理漂移的方法有两种:一种是靠观测不断修正,比如磁力计修正yaw,加速度计修正roll和pitch;另一种是定期做零偏标定,比如静止时重新估计零偏。
我在一个长时间运行的项目里,用了磁力计做yaw的修正,但磁力计容易受干扰,所以加了干扰检测,干扰大的时候降低磁力计的权重。这个策略在实际运行中效果不错,yaw的漂移被控制在可接受范围内。
8.5 多传感器融合的扩展思路
姿态估计只是状态估计的一个子问题。如果把状态扩展到速度和位置,就变成了组合导航。再进一步,如果融合视觉、激光雷达、轮速计等,就是多传感器融合定位。这些扩展的核心框架是一样的,都是EKF或ESKF,只是状态维度和观测模型不同。
我在做组合导航的时候,状态维度到了15维,包括位置、速度、姿态、加速度计零偏、陀螺仪零偏。观测包括GPS位置、GPS速度、磁力计、气压高度等。这个规模的EKF,调参和调试的复杂度比纯姿态估计高一个量级,但基本方法论是一样的。
最后分享一个我在实际项目中总结的小技巧:每次修改滤波器参数或者模型之后,一定要用同一段数据回放对比,看指标是变好了还是变差了。不要凭感觉判断,要用数据说话。我见过太多人改了一堆参数,结果滤波器性能反而下降了,就是因为没有做对比测试。滤波器开发是一个迭代的过程,每一次迭代都要有明确的依据和验证。