4. 扩展卡尔曼滤波(EKF):非线性系统线性化、雅可比矩阵计算、EKF在导航中的应用

卡尔曼滤波(KF)在导航领域很强大,但它有个硬伤——只能处理线性系统。现实世界哪有那么多线性?车辆转弯、卫星轨道、传感器姿态,全是非线性。这时候,扩展卡尔曼滤波(EKF)就派上用场了。

说白了,EKF 的核心思想就是:把非线性问题强行“掰直”,然后用标准卡尔曼滤波那一套去处理。怎么掰?用泰勒展开,只取一阶项。我刚开始学的时候觉得这方法有点粗暴,后来在项目中跑通了,才发现这招确实管用。

核心一句话:EKF 通过雅可比矩阵对非线性函数进行局部线性化,使得卡尔曼滤波框架可以应用于非线性系统。

4.1 为什么需要非线性处理?

先看一个典型的导航场景。你有个无人机,状态量是位置 (x, y) 和偏航角 θ。你通过里程计测量速度 v 和角速度 ω。状态方程是这样的:

x_k = x_{k-1} + v * cos(θ) * dt
y_k = y_{k-1} + v * sin(θ) * dt
θ_k = θ_{k-1} + ω * dt

看到 cos 和 sin 了吗?这就是非线性。标准卡尔曼滤波假设状态转移和观测都是线性关系,遇到这种三角函数就傻眼了。

我印象很深,有一次做车载组合导航,直接用标准 KF 处理 GPS 和 IMU 数据,结果滤波发散得一塌糊涂。后来换成 EKF,问题就解决了。原因很简单——车辆转弯时的航向变化,必须用非线性模型才能准确描述。

4.2 非线性系统线性化:泰勒展开

EKF 的线性化手段是一阶泰勒展开。假设我们有非线性状态转移函数 f(x) 和观测函数 h(x),在某个工作点(通常是上一时刻的状态估计值)附近展开:

f(x) ≈ f(x̂) + F * (x - x̂)
h(x) ≈ h(x̄) + H * (x - x̄)

这里的 F 和 H 就是雅可比矩阵。它们分别描述了状态转移和观测函数对状态变量的偏导数。

你想想看,这就像你在爬山时,用脚下的切线方向来近似山势。虽然不能精确描述整座山,但在你站的那一小块区域里,这个近似是足够用的。

我的经验:泰勒展开的精度取决于两个因素:一是展开点离真实值有多近,二是系统非线性的强度。如果非线性太强(比如大角度机动),一阶近似可能不够,这时候可以考虑二阶 EKF 或无迹卡尔曼滤波(UKF)。

4.3 雅可比矩阵计算

雅可比矩阵是 EKF 的“心脏”。计算错了,整个滤波就废了。我们来看一个具体的例子。

假设状态向量为 x = [x, y, θ]ᵀ,控制输入为 u = [v, ω]ᵀ。状态转移函数 f 为:

f₁ = x + v * cos(θ) * dt
f₂ = y + v * sin(θ) * dt
f₃ = θ + ω * dt

状态转移的雅可比矩阵 F 是 f 对 x 的偏导数:

F = ∂f/∂x = 
[ 1,  0,  -v * sin(θ) * dt ]
[ 0,  1,   v * cos(θ) * dt ]
[ 0,  0,  1                ]

注意第三列,那是 f 对 θ 的偏导。cos 的导数是 -sin,sin 的导数是 cos。这个细节很容易写错,我就在这上面栽过跟头。

我曾经踩过的坑:有一次我把 sin 的导数写成了 cos,结果滤波结果在转弯时出现剧烈震荡。排查了两天才发现是雅可比矩阵算错了。所以我的建议是:每次写完雅可比,先用数值微分验证一遍。

数值微分验证的方法很简单:

# 数值微分验证雅可比
epsilon = 1e-6
F_numerical = np.zeros((3, 3))
for i in range(3):
    x_plus = x.copy()
    x_plus[i] += epsilon
    x_minus = x.copy()
    x_minus[i] -= epsilon
    f_plus = f(x_plus, u, dt)
    f_minus = f(x_minus, u, dt)
    F_numerical[:, i] = (f_plus - f_minus) / (2 * epsilon)

把解析解和数值解对比一下,误差在 1e-6 量级就说明算对了。

4.4 EKF 在导航中的完整流程

EKF 的流程和标准 KF 很像,只是把线性矩阵换成了雅可比矩阵。我习惯把它分成五个步骤:

  1. 预测状态:用非线性状态转移函数计算先验估计 x̄_k = f(x̂_{k-1}, u_k)
  2. 预测协方差:P̄_k = F * P_{k-1} * Fᵀ + Q
  3. 计算卡尔曼增益:K_k = P̄_k * Hᵀ * (H * P̄_k * Hᵀ + R)⁻¹
  4. 更新状态:x̂_k = x̄_k + K_k * (z_k - h(x̄_k))
  5. 更新协方差:P_k = (I - K_k * H) * P̄_k

注意第三步和第四步里的 H 和 h。H 是观测函数的雅可比矩阵,h(x̄_k) 是预测观测值。残差 z_k - h(x̄_k) 是在非线性观测空间里算的,不是线性化之后的。

下面这张图展示了 EKF 在导航中的完整数据流:

EKF 导航数据流 控制输入 u 观测值 z 状态预测 x̄ = f(x̂, u) + 过程噪声 协方差预测 P̄ = F·P·Fᵀ + Q 雅可比矩阵计算 F = ∂f/∂x, H = ∂h/∂x 状态更新 x̂ = x̄ + K·(z - h(x̄)) 协方差更新 P = (I - K·H)·P̄ 卡尔曼增益 K = P̄·Hᵀ·(H·P̄·Hᵀ+R)⁻¹

4.5 导航中的典型应用场景

EKF 在导航领域用得最多的几个地方:

应用场景 状态量 非线性来源 观测方式
GPS/IMU 组合导航 位置、速度、姿态 姿态旋转、重力模型 GPS 伪距、IMU 加速度/角速度
车辆航位推算 位置、航向 cos/sin 航向变化 轮速编码器、陀螺仪
无人机视觉SLAM 位姿、地图点 相机投影模型 图像特征点匹配
机器人里程计融合 位姿、速度 运动学模型 激光雷达、IMU

拿 GPS/IMU 组合导航来说,IMU 的加速度计和陀螺仪数据是高频的(100Hz 以上),但存在漂移。GPS 是低频的(10Hz),但精度高。EKF 的作用就是把两者融合起来——用 IMU 做高频预测,用 GPS 做低频修正。

我的建议:在实际项目中,不要一上来就写完整的 EKF。先写一个简化版本,只处理位置和速度,验证雅可比矩阵和协方差传播是否正确。确认无误后,再逐步加入姿态、偏置等状态量。这样调试起来会轻松很多。

4.6 调参实战要点

EKF 的调参和标准 KF 类似,但有几个额外需要注意的地方:

  • 过程噪声 Q 矩阵:非线性系统的 Q 通常比线性系统大一些,因为线性化本身会引入误差。我一般会把 Q 设成标准 KF 的 1.5 到 2 倍。
  • 观测噪声 R 矩阵:这个和传感器特性有关。GPS 的 R 可以根据 HDOP 动态调整,IMU 的 R 通常固定。
  • 初始协方差 P₀:如果初始状态不确定,P₀ 设大一点(比如对角元素 100),让滤波器快速收敛。
  • 雅可比更新频率:如果计算资源紧张,可以每 N 步重新计算一次雅可比矩阵,而不是每步都算。N 通常取 5-10。

我曾经踩过的坑:有一次在嵌入式平台上跑 EKF,发现每次计算雅可比矩阵耗时太长,导致滤波频率跟不上 IMU 数据率。后来改成每 5 步计算一次雅可比,中间步骤用上一次的雅可比近似,问题就解决了。代价是精度略有下降,但在可接受范围内。

4.7 一个完整的 EKF 代码框架

下面是一个简化的 EKF 实现框架,适用于二维车辆导航:

class EKF:
    def __init__(self, dt):
        self.dt = dt
        # 状态: [x, y, θ]
        self.x = np.zeros(3)
        self.P = np.eye(3) * 100  # 初始协方差
        self.Q = np.diag([0.1, 0.1, 0.05])  # 过程噪声
        self.R = np.diag([1.0, 1.0])  # 观测噪声(GPS位置)

    def f(self, x, u):
        """非线性状态转移函数"""
        v, ω = u
        θ = x[2]
        return np.array([
            x[0] + v * np.cos(θ) * self.dt,
            x[1] + v * np.sin(θ) * self.dt,
            x[2] + ω * self.dt
        ])

    def F_jacobian(self, x, u):
        """状态转移雅可比矩阵"""
        v, ω = u
        θ = x[2]
        return np.array([
            [1, 0, -v * np.sin(θ) * self.dt],
            [0, 1,  v * np.cos(θ) * self.dt],
            [0, 0,  1]
        ])

    def h(self, x):
        """观测函数:直接观测位置"""
        return x[:2]

    def H_jacobian(self, x):
        """观测雅可比矩阵"""
        return np.array([
            [1, 0, 0],
            [0, 1, 0]
        ])

    def predict(self, u):
        """预测步骤"""
        F = self.F_jacobian(self.x, u)
        self.x = self.f(self.x, u)
        self.P = F @ self.P @ F.T + self.Q

    def update(self, z):
        """更新步骤"""
        H = self.H_jacobian(self.x)
        y = z - self.h(self.x)  # 残差
        S = H @ self.P @ H.T + self.R
        K = self.P @ H.T @ np.linalg.inv(S)
        self.x = self.x + K @ y
        self.P = (np.eye(3) - K @ H) @ self.P

这个框架虽然简单,但已经包含了 EKF 的核心逻辑。实际项目中,你只需要替换 f、h 以及对应的雅可比矩阵,就能适配不同的传感器和运动模型。

嗯,EKF 的内容就讲到这里。记住一点:雅可比矩阵是 EKF 的灵魂,算对了,滤波就成功了一半。剩下的就是调参和工程优化了。