资讯动态

IKFoM前向推导:流形滤波预测的雅可比、协方差与噪声建模

发布时间:2026/10/26 21:59:27 来源:尧图企业网站定制
调试状态估计器最让人抓狂的时刻往往不是滤波器发散而是它发散得没有道理——残差看着正常观测也没问题协方差却在一个安静的时间段里悄悄鼓起来。这种时候把日志倒回去翻十有八九问题出在预测那一步雅可比少了一项、零偏的 Δt 落错了位置或者扰动的定义侧左乘还是右乘和更新部分对不上。IKFoM 这个模板库干的活就是把这堆容易出错的推导抽象成一套可以复用的流形工具让滤波器主体只需要调用 boxplus 和 boxminus 就能工作。但它抽象得越干净前向推导这部分就越值得单独拎出来捋一遍——因为一旦抽象层下面的约定和你脑子里的约定不一致你会在后面几个数量级的调试时间里反复怀疑人生。这篇笔记专门啃 IKFoM 的前向推导部分也就是预测环节的数学骨架状态怎么从流形上转移、误差状态怎么递推、协方差怎么传播、噪声怎么进模型。适合已经能跑通一个状态估计器、但对里面雅可比矩阵的每一项都说不清来路的人也适合刚接触流形滤波、想从零把公式推一遍的新手。我不打算只贴结论矩阵而是把每一步为什么这么写摊开讲包括我自己推错过的几个地方。1. 前向推导到底在推什么先把问题边界划清楚1.1 一次预测步骤只产出三样东西很多人学滤波器的时候脑子里是预测—更新两大块但真到写代码的时候会发现预测这一块其实被切得很细。IKFoM 的前向推导部分落到实处只做三件事把名义状态从上一时刻推到当前时刻把误差状态的转移关系线性化成一个矩阵把这个转移关系作用到协方差上。三件事对应的输出物分别是状态量 x̂、状态转移矩阵 F、过程噪声协方差 Q。这里有个非常关键的认知F 和 Q 描述的不是状态怎么变而是误差怎么变。滤波器内部真正在传播的是误差状态 δx 及其协方差名义状态只是被携带的一个参考点。所以你推 F 的时候泰勒展开的对象是误差量 δx不是状态量 x 本身。我见过不少初学者直接拿动力学方程去对状态求偏导写出来的东西维度都不对——因为对状态求导得到的是连续时间导数而 F 是离散时间、跨一个采样周期的转移。这两个东西差着一个时间积分的层次。还有一个容易混的点F 到底是什么维度的矩阵。如果状态是一个由多个流形分量拼起来的复合流形那么 F 的维度就是所有分量切空间维度之和。以常见的惯性导航状态为例姿态是三维旋转群切空间三维、位置速度各三维、两个零偏各三维加起来十五维F 就是一个 15×15 的方阵。这个维度是切空间的维度不是流形本身看起来的维度——比如四元数存了四个数但它对应的切空间还是三维。1.2 为什么不能用四元数直接做加减新手最常见的疑问是四元数不是可以直接加减吗为什么非要绕一套指数映射答案是四元数表示的旋转是单位四元数它躺在一个三维球面四维空间里的单位球上不在一个线性空间里。两个单位四元数相减结果大概率不再是单位四元数你得重新归一化。归一化这一步在滤波器里是致命的它把误差的分布硬生生投影到球面上破坏了高斯假设协方差的正定性也会跟着出问题。更麻烦的是小角度近似这一环。如果你直接在四元数上做线性加减误差量的第一个分量实部和后面三个分量虚部之间的耦合关系就丢掉了而协方差传播偏偏要靠这个耦合关系才能正确地对齐到切空间上。所以在流形上做滤波的核心操作不是加减而是在一个点处定义一个切空间把误差放到切空间里做完线性运算再映射回流形。这个思路推广开来就得到两个基本算子从一个流形点出发沿切空间里某个向量走一小步回到流形上以及反过来两个流形点之间差多少用切空间里的一个向量表示。这两个算子就是后面要反复出现的 boxplus 和 boxminus。只要你的状态能定义出这两个算子整个滤波器的主体代码就完全不需要知道底层是旋转、是球面、还是欧氏空间。1.3 推导的验证标准数值差分能对上才算推对了我在推这类公式时有个硬性习惯只要写出一个雅可比块立刻用数值差分验一遍。方法是给某个误差分量一个很小的扰动比如 1e-6前向走一步看误差状态的变化量和解析式算出来的 F 乘以扰动向量做比较相对误差应该落在扰动尺度量级内。这个习惯救过我很多次。有一次姿态块的符号写反了解析式看起来很像标准答案但数值差分立刻显示差了一个负号。还有一次是零偏项漏了 Δt 因子量级上差了两个数量级肉眼审公式根本看不出来数值差分一秒暴露。这类错误如果流到实际的系统里表现通常是滤波器在长时间静止后突然漂移或者转弯时协方差异常增大排查成本高得多。提醒做数值差分时扰动不要太小。双精度下 1e-9 量级会被舍入误差吃掉1e-6 到 1e-5 是比较安全的区间。同时要注意归一化操作的影响——如果你在差分时顺手做了四元数归一化等于引入了非线性差分结果会被污染。2. 流形上的加减法boxplus 与 boxminus 的工程语义2.1 指数映射把切空间的直线映射成流形上的弧先建立几何直觉。以旋转群为例它的切空间在单位元处就是三维实数空间里面的一个向量可以理解成旋转轴乘以旋转角度。指数映射做的事是把这条从原点出发的直线卷成流形上的一条测地线弧。走多远取决于这个向量的模长模长就是旋转角。这条弧的性质决定了小角度近似的适用范围。当模长很小比如 1e-3 弧度以下弧和直线几乎重合指数映射可以近似成单位阵加上反对称矩阵这种极其廉价的形式。但当模长到了 0.1 弧度以上二阶项开始显著这个近似就不可用了。在滤波器的预测步骤里每步的旋转增量通常是角速度乘以采样周期量级在 1e-3 到 1e-2 之间一阶近似绰绰有余但在一些低频传感器或者高机动场景下单步角度可能超过 0.1 弧度这时候如果还用一阶近似误差会以平方的速度累积。这里要区分两件事名义状态的递推和误差状态的递推。名义状态用的是完整的指数映射通常用查表或者三角函数实现精度足够误差状态做的是线性化用一阶近似是合理的因为线性化本身就是一阶截断。麻烦在于有些实现为了省算力把名义状态的递推也换成一阶近似这就把误差直接注入了参考轨迹长此以往会出现系统性偏差。我在早期自己写的版本里就干过这事表现是静止状态下姿态慢慢爬一小时爬出零点几度。2.2 boxplus 不是向量加法它有三条硬约束把沿切空间走一步这件事实现成函数就是 boxplus(x, δ)。要让它在滤波器里可用至少得满足三条约束。第一是封闭性boxplus 的结果必须还是同一个流形上的点不能跑出去。对旋转来说意味着结果仍然是正交阵对单位球面来说意味着结果仍然是单位向量。如果实现里用了近似封闭性可能被破坏所以在实现里通常会显式做一次归一化或者正交化来拉回来。第二是可逆性boxminus(boxplus(x, δ), x) 应该近似等于 δ。注意这里是近似因为流形是弯的走一个来回会有高阶残差。这个残差的量级决定了滤波器在高频预测下的精度上限。如果可逆性很差误差状态在预测和更新之间来回转换时会引入伪噪声协方差会虚高。第三是光滑性在 δ 0 附近必须有足够好的连续性和可导性。这一条在实现中容易被忽略。有些流形在奇异点附近比如球面的极点、旋转角接近 π雅可比会退化滤波器在那里会表现得非常不稳定。IKFoM 里对球面流形做了特殊处理来避开极点问题这部分逻辑在推导时也要一并考虑。2.3 拿旋转手推一遍看清楚近似到底丢掉了什么设名义旋转为 R̂误差旋转向量为 δθ。右扰动约定下真值旋转写成 R R̂ · Exp(δθ)。展开 Exp(δθ) 到一阶Exp(δθ) ≈ I [δθ]×其中 [·]× 表示把三维向量变成反对称矩阵。于是R ≈ R̂ R̂[δθ]×把它作用到一个向量 a 上R a ≈ R̂ a R̂ (δθ × a) R̂ a - R̂ [a]× δθ这个式子是后面所有推导的地基。它说明旋转的误差对向量的影响是通过向量自己的反对称矩阵来耦合的。注意这个负号很多符号错误都出在这里。如果你写成 R̂ a R̂[a]× δθ那就是把叉乘的顺序搞反了δθ × a -a × δθ。同样的二阶项的量级是 |δθ|²。在 δθ 为 1e-2 时二阶项约 1e-4相对于一阶项 1e-2 是百分之一的相对误差。这个误差会以偏差的形式进入协方差不会立刻爆炸但会在长时间的静默期里慢慢累积成不可忽视的量。所以如果有人在长时间运行后发现姿态协方差单调增长除了检查噪声参数也要看看离散化阶次是不是太低了。3. 误差状态递推的通式把推导变成可复用的套路3.1 名义轨迹与真值轨迹的分离流形滤波的标准做法是把状态拆成两条线一条是滤波器维护的名义状态它没有噪声按照确定性的模型递推另一条是理论上存在的真值状态它包含了所有噪声和模型误差。两者之间的差就是误差状态用 boxminus 表示。这个拆分不是形式主义它有一个非常实际的收益误差状态始终在零附近所以围绕它做的线性化永远是在原点附近的线性化线性化点不会跑远。如果直接对状态做线性化随着状态值变大比如位置跑到了几十万米线性化的相对精度会明显下降。这也是为什么流形滤波比直接在状态空间做扩展卡尔曼要稳定得多。具体到实现上预测分成两遍算。第一遍用名义模型把状态推一步得到新的名义状态第二遍把误差状态代入做泰勒展开得到转移矩阵 F。这两遍在代码里通常对应两个不同的函数IKFoM 里体现为状态预测回调和雅可比回调的分离。新手常犯的错误是试图在一个函数里同时干这两件事结果两个逻辑搅在一起调试时根本分不清是状态推错了还是雅可比错了。3.2 三步固定动作代入、配平、展开我推误差状态递推时固定走三步这套动作几乎对所有流形都适用。第一步是代入。把真值状态用名义状态和误差量表示代入真实的递推方程把名义状态代入名义递推方程。注意真实方程里带噪声项名义方程里噪声置零。第二步是配平。目标是凑成真值递推结果 名义递推结果 ⊞ 新的误差这种形式。这一步是最需要技巧的因为流形上的乘法不满足交换律误差量可能出现在表达式的左边也可能在右边。右扰动约定要求误差始终在右边如果推导过程中误差跑到了左边就得用一次左扰转右扰的恒等变换把它挪过去。旋转群上的变换关系是 R̂ · Exp(δ) · R̂ᵀ Exp(R̂ δ)反过来 Exp(δ) · R̂ R̂ · Exp(R̂ᵀ δ)。这一步用错了符号就会全线崩盘。第三步是展开。用贝克-坎贝尔-豪斯多夫公式的一阶截断把两个指数映射的乘积合并成一个。对旋转来说一阶形式是 Exp(a)Exp(b) ≈ Exp(a b 0.5 · a × b)。把噪声项和误差项都当作小量做一阶截断消去两边的公因子就得到了新的误差表达式。这套动作的价值在于可复用。换一个流形、换一个动力学方程三步还是这三步只是中间的代数细节不同。我建议每个做流形滤波的人都把这套流程默写下来比背任何具体的结果矩阵都有用。3.3 误差定义在哪一侧决定了整张 F 的长相左右扰动的选择不是审美问题它直接决定 F 矩阵的形态也决定和更新部分的兼容性。右扰动下误差固定在状态右侧姿态误差的转移矩阵里会出现一个旋转后的角速度的反对称矩阵项位置和速度对姿态的偏导里出现的是旋转矩阵本身而不是它的转置。左扰动下这些项会多出旋转矩阵的转置看起来更绕但它在某些推导里更顺手因为世界坐标系下的量可以直接作用在左边。真正的危险在于混用。假设预测部分的 F 是用右扰动推出来的而更新部分的观测雅可比是用左扰动推出来的两边各自都对合在一起就是错的。表现是滤波器在静态时看上去还稳定一旦有旋转运动状态就会被观测拽向错误方向或者残差呈现明显的系统性偏差。这种 bug 极难定位因为单看每一部分的公式都符合教科书。我的建议是一旦选定一种约定就在整个工程里用注释把它钉死所有涉及扰动的函数入口都注明右扰动或左扰动并且在最早的单元测试里就验证两者的组合行为。IKFoM 在这一点上做了统一它要求你通过 boxplus 和 boxminus 明确定义扰动的作用方式滤波器主体不再关心方向问题这本身就是一种防错设计。4. 以惯性递推为例把 F 矩阵逐块摊开4.1 状态定义与离散递推方程为了让推导有具体抓手选一个最经典的场景惯性导航状态的预测。状态取 15 维姿态 R三维旋转群、位置 p三维欧氏、速度 v三维欧氏、陀螺零偏 b_g三维欧氏、加计零偏 b_a三维欧氏。下面统一按右扰动约定记真值与名义量的关系为 R R̂ · Exp(δθ)其余分量写成真值 名义值 误差。连续时间动力学写成Ṙ R · [ω - b_g - n_g]× v̇ R · (a - b_a - n_a) g ṗ v ḃ_g n_bg ḃ_a n_ba离散化用一阶欧拉采样周期记作 Δt记 ω̂ ω - b̂_g、â a - b̂_aR̂ R̂ · Exp(ω̂ Δt) v̂ v̂ (R̂ â g) Δt p̂ p̂ v̂ Δt 0.5 (R̂ â g) Δt² b̂_g b̂_g b̂_a b̂_a零偏在预测步骤里保持不变这是随机游走模型的直接体现——它的变化完全由噪声驱动确定性部分为零。这一点在后面写 F 的时候会体现为两个单位阵。4.2 姿态误差块一阶近似下的那半个雅可比先把姿态的真值递推写出来噪声项保留R R · Exp((ω - b_g - n_g) Δt)代入 R R̂ Exp(δθ)、b_g b̂_g δb_gR R̂ Exp(δθ) · Exp((ω̂ - δb_g - n_g) Δt)用一阶的贝克-坎贝尔-豪斯多夫公式合并两个指数Exp(δθ) Exp(u Δt) ≈ Exp(δθ u Δt 0.5 · (δθ × u Δt))其中 u ω̂ - δb_g - n_g。把这个结果和名义递推 R̂ R̂ Exp(ω̂ Δt) 对齐两边同时左乘 R̂ᵀExp(δθ u Δt 0.5 (δθ × u) Δt) Exp(ω̂ Δt) Exp(δθ)这里右边再展开一次把 Exp(ω̂ Δt) Exp(δθ) 合起来。但更简洁的做法是把左边的一阶项化简后直接对比指数上的向量因为都在小量范围内指数映射在小量邻域内是单射。左边指数上的向量展开把 u 代回去注意 δθ × (ω̂ Δt) 这一项δθ ω̂ Δt - δb_g Δt - n_g Δt 0.5 (δθ × ω̂) Δt利用叉乘的反对称性质δθ × ω̂ -ω̂ × δθ -[ω̂]× δθ所以这一项可以写成 -0.5 [ω̂ Δt]× δθ。整理后得到δθ (I - 0.5 [ω̂ Δt]×) δθ - Δt δb_g - Δt n_g这就是姿态误差块的完整结论。三块分别是姿态自身的转移那个带 0.5 的反对称矩阵、零偏对姿态的耦合负 Δt 单位阵、噪声项。那个 0.5 的因子是很多人写错的地方——有人的实现里没有它短期看不出问题但在高角速度下会引入明显的相位误差。4.3 速度、位置与零偏块的推导速度的真值递推v v (R (a - b_a - n_a) g) Δt把 R 用一阶近似展开成 R̂ (I [δθ]×)再作用到 (â - δb_a - n_a) 上。关键的一步是那个负号R̂ (I [δθ]×)(â - δb_a - n_a) ≈ R̂ â - R̂ [â]× δθ - R̂ δb_a - R̂ n_a这里用到了 [δθ]× â δθ × â -â × δθ -[â]× δθ。整理后δv δv - R̂ [â]× Δt · δθ - R̂ Δt · δb_a - R̂ Δt · n_a位置块的推导路径完全一样只是多了一个 0.5 的系数和一项速度误差δp δp Δt · δv - 0.5 R̂ [â]× Δt² · δθ - 0.5 R̂ Δt² · δb_a - 0.5 R̂ Δt² · n_a零偏块最简单δb_g δb_g Δt · n_bg δb_a δb_a Δt · n_ba把这些拼起来按 [δθ, δp, δv, δb_g, δb_a] 的顺序排列就得到完整的 15×15 状态转移矩阵。下面这张表把每一块的来源和量级列清楚方便对着代码核对块位置表达式来源常见错误δθ 对 δθI - 0.5[ω̂Δt]×两次指数映射合并漏掉 0.5 因子δθ 对 δb_g-Δt · I角速度中零偏的直接代入漏掉 Δtδp 对 δθ-0.5 R̂[â]×Δt²二次积分引入漏掉 0.5 或 Δt²δp 对 δvΔt · I位置对速度积分误写成单位阵δp 对 δb_a-0.5 R̂ Δt²加速度中零偏的二次积分符号写反δv 对 δθ-R̂[â]×Δt旋转误差耦合到加速度漏掉负号δv 对 δb_a-R̂ Δt加速度中零偏的直接代入漏掉 R̂δb_g 对 δb_gI随机游走无确定性项多写了衰减因子4.4 一个反直觉的细节位置块的 Δt² 从哪里来不少人第一次看到位置对零偏的偏导是 0.5 R̂ Δt² 会觉得奇怪——为什么是平方而不是一次方直观解释是零偏误差先影响加速度加速度积分一次得到速度误差速度再积分一次得到位置误差两次积分各贡献一个 Δt。而 0.5 这个系数来自二次积分的梯形公式加速度恒定假设下位置增量是 0.5 a Δt²。这个细节的实际意义在于量级。当 Δt 是 0.005 秒200Hz时Δt² 是 2.5e-5比 Δt 小了 200 倍。所以位置对零偏的耦合在短周期预测里几乎可以忽略但在低频比如 20HzΔt 0.05时Δt² 2.5e-3相对贡献上升到千分之一就不能随便丢了。我见过有人为了简化计算把所有 Δt² 项直接删掉在 200Hz 下跑得挺好换成 50Hz 的传感器立刻出现明显的尺度漂移——原因就在这里。顺便说一句离散化的阶次。上面用的是显式欧拉误差是 O(Δt²) 量级。如果想要更精确可以用中点法或者龙格库塔代价是每一步要多次计算动力学。对绝大多数惯性传感器来说一阶欧拉在 100Hz 以上已经够用因为传感器噪声本身比离散化误差大得多。但如果你的应用需要和低频传感器对齐或者要做精细的离线回放提升积分阶次是有意义的。5. 噪声怎么进误差状态G 矩阵与 Q 的拼装5.1 连续噪声谱密度到离散协方差的尺度变化噪声参数是最容易被随手填的东西也是滤波器表现最敏感的旋钮。常见做法是从传感器手册上抄一个数填进去然后发现滤波器要么过度自信、要么完全迟钝。问题往往出在量纲和尺度上。传感器手册给的通常是连续时间的噪声密度单位是每根号赫兹比如陀螺的角度随机游走单位是度每根号小时。而滤波器里需要的是离散时间的噪声方差单位是平方每步。两者之间的换算关系是一条铁律离散方差 连续谱密度² × Δt。注意是谱密度的平方乘一次 Δt不是平方再乘 Δt 的平方。这个换算关系经常被写错。写成 Δt² 的话在 200Hz 下会让噪声被低估 200 倍滤波器变得极度自信观测一有偏差就会被猛烈拉扯表现为状态跳动。写成不乘 Δt 的话噪声被高估滤波器会过度平滑响应迟钝动态场景下出现明显的滞后。我调试过一个系统就是因为这个换算写成了平方静止时看不出问题一上动态就跟不上排查了整整两天。5.2 G 矩阵逐块写噪声进入误差状态的方式是把每个噪声源对应到它影响的误差分量上。把噪声向量按 [n_g, n_a, n_bg, n_ba] 排列从第 4 节的推导里直接抄出系数误差分量对 n_g对 n_a对 n_bg对 n_baδθ-Δt · I000δp0-0.5 R̂ Δt²00δv0-R̂ Δt00δb_g00Δt · I0δb_a000Δt · I这张表就是 G 矩阵15×12。有了 G过程噪声协方差就是 Q G · diag(σ_g², σ_a², σ_bg², σ_ba²) · Gᵀ。这里有个可以省算力的地方R̂ Δt 这种项因为 R̂ 是正交阵所以 (R̂ Δt)(R̂ Δt)ᵀ Δt² · I。也就是说 Q 里面旋转矩阵会被消掉最终 Q 的形式非常干净只和标量噪声参数以及 Δt 的幂次有关。这是个很实用的性质意味着你可以在推导阶段就把 Q 化简成对角块形式运行时不用做任何矩阵乘法。IKFoM 这类模板库在编译期就把这些结构确定下来了运行时的开销主要花在 F 的乘法上。5.3 Q 的对称性和正定性不能靠运气Q 必须对称正定否则协方差传播后可能失去正定性滤波器会直接崩。理论上是满足的因为它是 G 乘对角阵乘 Gᵀ 的形式但数值上不一定。问题出在参数极端的时候。如果某个噪声参数填成了 0那一行对应的协方差就是 0整个 Q 变成半正定某些方向上就失去了约束卡尔曼增益计算里的求逆可能出问题。实践中我建议所有噪声参数都给一个非零的下限哪怕传感器精度极高也给个 1e-8 之类的数兜底。另一个隐患是累积误差。随着迭代次数增加协方差的对称性会因为浮点误差逐渐被破坏。标准做法是每次传播后强制做一次对称化P 0.5 (P Pᵀ)。这个操作成本极低但能避免很多莫名其妙的数值崩溃。我在一个长时运行的场景里遇到过跑了八小时后协方差出现极小的负特征值导致求逆失败加上对称化之后就没再复现。注意对称化只是补丁不是解决方案。如果协方差经常需要靠对称化来维持正定说明 F 矩阵本身有问题或者噪声参数严重不合理。先查根因再谈加固。6. IKFoM 的模板封装把推导藏在了哪6.1 状态是流形的笛卡尔积不是一堆孤立变量IKFoM 最核心的设计思路是把整个状态看成一个复合流形也就是若干个子流形的笛卡尔积。姿态是旋转群、位置速度是三维欧氏空间、零偏也是三维欧氏空间把它们拼在一起整体仍然是一个流形。既然是流形就只需要提供 boxplus 和 boxminus 两个操作滤波器主体就能工作。这个抽象带来的直接好处是滤波器的预测和更新代码不需要知道状态里到底是哪些量。它只负责调用 boxplus 把状态往前走一步、调用 boxminus 算残差、按模板参数分配矩阵维度。真正的领域知识被推到了两个地方状态的定义哪些分量、什么类型以及用户自己写的动力学函数和雅可比函数。顺着这个思路库提供了一套构建机制用宏或者模板把若干流形类型声明成一个状态类型自动生成该状态的切空间维度、boxplus、boxminus。对每个子流形只需要实现自己的两个算子复合时逐分量应用即可。6.2 雅可比到底是手写还是自动求这是初学者最容易困惑的地方。boxplus 和 boxminus 自动生成没问题但 F 矩阵涉及对动力学函数求雅可比这一步库能自动做吗答案是取决于你怎么用。库通常提供两条路。第一条是让你手写雅可比回调把第 4 节里推出来的那些块按顺序填进去。第二条是提供一个数值差分的通用实现通过多次调用动力学函数来近似雅可比。前者的精度和速度都更好后者开发快但有代价。数值差分的代价不只是慢还有精度。因为差分步长的选择和状态量级有关位置在几万米时和在地面附近时合适的步长完全不同。固定步长的差分在状态量级变化大的场景下会出现精度退化。而且差分对动力学函数的光滑性要求很高如果你的动力学里有硬阈值或者查表插值差分结果会带噪声进而污染协方差。我自己的做法是开发阶段用数值差分快速跑通验证整个滤波器框架没问题然后逐块替换成解析雅可比每替换一块就用差分做一次对照验证。这样既保证了开发速度又保证了最终精度还能在替换过程中发现很多隐藏的符号问题。6.3 编译期展开的收益与代价模板库的一个显著特点是所有维度都确定在编译期运行时的矩阵维度是常量编译器可以把循环完全展开甚至把稀疏结构优化掉。对嵌入式平台来说这个收益很大因为动态内存分配和循环开销都被消除了。代价是编译时间和二进制体积。状态维度越大、复合流形层次越多模板实例化的组合就越多编译时间会明显增加。我在一个包含多种传感器配置的项目里遇到过编译时间从几十秒涨到几分钟的情况最后是通过拆分成独立的编译单元缓解的。另一个代价是错误信息的可读性。模板错误信息通常会展开好几屏真正的错误藏在最后几行。我的经验是遇到模板报错先从最后一行往前看找第一个提到具体类型名的地方那通常就是问题所在。另外状态定义里的括号配对错误是最常见的宏展开之后报错位置会非常误导。7. 我在前向推导里踩过的坑7.1 左右扰动混用最隐蔽的符号灾难前面已经提过这个问题但值得单独展开因为它的隐蔽程度远超其他错误。具体表现是这样的预测部分用右扰动推出 F更新部分用左扰动写观测雅可比。单独测试预测协方差传播正常单独测试更新状态修正量也符合预期。但合在一起运行会看到残差呈现出与运动方向相关的系统性偏差——直行时还好一转弯就偏。排查的难点在于两部分的代码单独看都没毛病单元测试也都能过。我当时的做法是把预测步骤的误差定义打印出来手动构造一个已知的小扰动走一步预测然后看残差和理论值对不对。这一步立刻发现更新部分算出来的残差方向是反的因为它假设误差在左边。从那以后我养成了一个习惯在状态的头文件里用一行注释明确写死扰动约定任何新加入的模块都必须对齐这个约定代码评审时把这一条当作硬性检查项。7.2 离散化阶次和协方差膨胀的因果关系有一次调试一个长时间运行的场景发现协方差在静止状态下单调增长位置标准差从厘米级慢慢涨到米级但实际状态并没有漂移。这说明协方差传播过保守了。追下去发现两个原因叠加。第一积分用的显式欧拉但 Δt 的取值是传感器周期而实际预测是以更高频率被调用的因为还有别的触发源导致每个小周期都注入了一份完整的离散噪声噪声被重复计数。第二Q 的计算里没有考虑预测步和观测步频率不一致的情况观测频率远低于预测频率时两次观测之间的噪声累积被低估了。修复方法是把噪声注入改成按实际经过的时间成比例缩放而不是按调用次数。这个改动看起来简单但它要求你在预测函数里传入真实的时间差而不是默认一个常数。很多实现为了简化都在这里写死了 Δt短期没问题一旦有丢帧或者变频率就会出问题。7.3 零偏项漏 Δt量级错误的典型样本这是个听起来很蠢但实际发生率极高的错误。零偏对姿态的耦合项应该是 -Δt 乘以单位阵写过的人有相当比例会漏掉 Δt。原因在于推导的时候很多人是分两步想的先在连续时间下写出关系再做离散化。连续时间下姿态误差对零偏的偏导就是单位阵因为角速度误差等于零偏误差离散化之后才乘上 Δt。如果推导过程中断在中间某一步很容易把连续时间的结果当成最终答案。这个错误的症状很有辨识度滤波器在静止时收敛正常一旦有持续旋转姿态会明显滞后于真值。因为零偏修正量被放大了约 1/Δt 倍在 200Hz 下是 200 倍滤波器对零偏的估计会过度激进表现就是姿态跟踪不上快速的角速度变化。我第一次遇到时以为是噪声参数问题调了半天才发现是量级错了。7.4 验证方法把差分测试写成脚本前面说过要用数值差分验证雅可比这里给一个具体可操作的做法。写一个测试脚本做三件事随机生成一个名义状态和一个小扰动用数值差分算出 F 的某一列对某个误差分量施加扰动看输出误差的变化和解析式算出的对应列做比较输出相对误差。关键是覆盖率。不要只测一个随机的状态要把姿态设成接近零、接近 π、任意角度三种情况把速度设成零和高速两种情况把零偏设成零和非零两种情况做笛卡尔积。我就是在姿态接近 π这一档里发现过指数映射的奇异处理有问题实际运行中几乎不可能随机撞到那个点但理论上有风险。对于噪声矩阵 G验证思路是把 Q 和直接用噪声样本做蒙特卡洛仿真的结果对比。生成几千组噪声样本按 G 作用到误差状态上算经验协方差和解析 Q 比较。这个验证成本高一些但能一次性确认所有噪声项的系数和量纲都对了非常值得。写到这里前向推导这部分的主干基本铺完了。真正把这套推导吃透的标志不是能默写出那张 15×15 的矩阵而是拿到任何一个新的流形、任何一个新的动力学模型都能按代入、配平、展开这三步把 F 和 G 重新搭出来。我现在遇到新传感器的融合需求第一件事就是把它的动力学写成流形形式然后走一遍这三步比去翻任何现成的公式都快。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑