扩展卡尔曼滤波(EKF)详解

部署运行你感兴趣的模型镜像

一、卡尔曼滤波的局限性

卡尔曼滤波(KF)是一个强大的状态估计算法,但它有一个关键限制:只能处理线性系统。然而现实世界中的物理系统几乎都是非线性的:

  • 机器人运动(转弯时速度和方向的关系)

  • 飞行器姿态(涉及三角函数)

  • 传感器模型(如摄像头透视投影)

当系统存在非线性时,标准KF的线性假设会失效,导致估计结果不准确甚至发散。扩展卡尔曼滤波(EKF) 就是为了解决这个问题而诞生的。

二、EKF核心思想:泰勒展开(局部线性化的数学工具)

EKF的基本思路很直观:在系统当前状态附近,用线性函数近似非线性函数。就像在曲线上的某一点画一条切线来近似曲线。

数学工具:泰勒展开

EKF使用一阶泰勒展开进行线性化:

f(x)\approx f(a)+f^{​{}'}(a)(x-a)

其中:

  • f(x) 是原始非线性函数

  • a 是线性化点(当前状态估计)

  • f'(a) 是非线性函数在点 a 的导数(如果输入是多维的,就是雅可比矩阵(Jacobian Matrix) )

三、EKF系统模型

EKF 处理的是一个带有噪声的非线性动态系统:

1. 状态方程(预测模型

x^{k}=f(x^{k-1},u^{k-1},w^{k-1})

  • xᵏ:想要估计的系统在 k 时刻的状态向量 (例如: [位置X, 位置Y, 速度, 方向θ]ᵀ)。

  • f:非线性状态转移函数。它描述了系统状态如何从上一时刻 k-1 的状态 xᵏ⁻¹,在控制输入 uᵏ⁻¹ 和过程噪声 wᵏ⁻¹ 的影响下,演化到当前时刻 k 的状态 xᵏ

  • uᵏ⁻¹k-1 时刻的控制输入向量 (例如: 油门、方向盘角度)。

  • wᵏ⁻¹k-1 时刻的过程噪声向量。它代表了模型的不确定性、未知的外部扰动等。我们假设它是均值为 0、协方差矩阵为 Q 的高斯白噪声 (wᵏ⁻¹ ~ N(0, Qᵏ⁻¹))。

2. 观测方程(测量模型)

z^{k}=h(x^{k},v^{k})

  • zᵏk 时刻的观测向量 (测量值) (例如: GPS 坐标、摄像头看到的特征点像素坐标、激光测距值)。

  • h非线性的观测函数。它描述了系统状态 xᵏ 如何被转换成我们能测量到的观测值 zᵏ

  • vᵏk 时刻的观测噪声向量。它代表了传感器的误差、测量干扰等。我们假设它是均值为 0、协方差矩阵为 R 的高斯白噪声 (vᵏ ~ N(0, Rᵏ))。

四、EKF公式推导(分步详解)

EKF 的核心操作就是在当前的最佳估计点,对非线性函数 f 和 h 进行一阶泰勒展开(线性化),然后用线性化后的近似模型来替代原始的非线性模型,套用标准 KF 的公式框架。KF 有预测步(Predict)和更新步(Update),EKF 也有。

步骤 0:回顾 KF(重要!)

EKF 公式和 KF 公式形式上非常像!KF 的核心公式是:

  • 预测 (Predict):

    • x̂ᵏ⁻ = Fᵏ⁻¹ * x̂ᵏ⁻¹ + Bᵏ⁻¹ * uᵏ⁻¹ (预测状态)

    • Pᵏ⁻ = Fᵏ⁻¹ * Pᵏ⁻¹ * (Fᵏ⁻¹)ᵀ + Qᵏ⁻¹ (预测协方差)

  • 更新 (Update):

    • ỹᵏ = zᵏ - Hᵏ * x̂ᵏ⁻ (新息/残差)

    • Sᵏ = Hᵏ * Pᵏ⁻ * (Hᵏ)ᵀ + Rᵏ (新息协方差)

    • Kᵏ = Pᵏ⁻ * (Hᵏ)ᵀ * (Sᵏ)⁻¹ (卡尔曼增益)

    • x̂ᵏ = x̂ᵏ⁻ + Kᵏ * ỹᵏ (更新状态)

    • Pᵏ = (I - Kᵏ * Hᵏ) * Pᵏ⁻ (更新协方差)

步骤 1:线性化状态方程 f (用于预测步)

  • 线性化点: 我们选择在上一时刻的最优估计值 x̂ᵏ⁻¹ 处进行线性化。为什么是这里?因为这是我们目前对 k-1 时刻状态最好的猜测,我们希望基于这个“最佳猜测”来预测 k 时刻的状态。

  • 泰勒展开:
      xᵏ = f(xᵏ⁻¹, uᵏ⁻¹, wᵏ⁻¹) ≈ f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹

  • 深度解读:

    • f(x̂ᵏ⁻¹, uᵏ⁻¹, 0): 这是非线性函数 f 在点 (x̂ᵏ⁻¹, uᵏ⁻¹, 0) 的值。注意我们把过程噪声 wᵏ⁻¹ 暂时设为了它的均值 0。这是一个常数项,代表了基于线性化点的“基础预测”。

    • Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹):

      • (xᵏ⁻¹ - x̂ᵏ⁻¹) 表示真实状态 xᵏ⁻¹ 偏离我们估计值 x̂ᵏ⁻¹ 的误差。

      • Fᵏ⁻¹ 是函数 f 关于状态 x 的雅可比矩阵 (Jacobian Matrix),在点 (x̂ᵏ⁻¹, uᵏ⁻¹, 0) 处计算。Fᵏ⁻¹ 就是 f 在当前估计点附近的状态变化率(斜率矩阵)! 它告诉我们,状态 x 的一个微小变化,会导致预测状态 xᵏ 发生多大的变化。

      • 这个项线性地修正了由于状态估计误差带来的预测偏差。

    • Wᵏ⁻¹ * wᵏ⁻¹:

      • wᵏ⁻¹ 是过程噪声。

      • Wᵏ⁻¹ 是函数 f 关于过程噪声 w 的雅可比矩阵,也在点 (x̂ᵏ⁻¹, uᵏ⁻¹, 0) 处计算。它描述了过程噪声 w 是如何线性地影响预测状态 xᵏ 的。很多时候,如果噪声是加性的 (f(x, u, w) = f_nl(x, u) + w),那么 Wᵏ⁻¹ 就是单位矩阵 I

  • 得到线性化状态方程:
    经过线性化,我们把非线性的状态方程近似成了一个线性方程:
            xᵏ ≈ f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹

步骤 2:线性化观测方程 h (用于更新步)

  • 线性化点: 我们选择在当前时刻的预测状态值 x̂ᵏ⁻ 处进行线性化。为什么是这里?因为这是我们利用状态方程预测出来的 k 时刻状态的最佳估计,我们需要基于这个预测值来“预测”我们会观测到什么,然后与实际观测值比较。

  • 泰勒展开:
             zᵏ = h(xᵏ, vᵏ) ≈ h(x̂ᵏ⁻, 0) + Hᵏ * (xᵏ - x̂ᵏ⁻) + Vᵏ * vᵏ

  • 深度解读:

    • h(x̂ᵏ⁻, 0): 这是非线性函数 h 在点 (x̂ᵏ⁻, 0) 的值。注意我们把观测噪声 vᵏ 暂时设为了它的均值 0。这是一个常数项,代表了基于预测状态 x̂ᵏ⁻ 的“预测观测值”。

    • Hᵏ * (xᵏ - x̂ᵏ⁻):

      • (xᵏ - x̂ᵏ⁻) 表示真实状态 xᵏ 偏离我们预测值 x̂ᵏ⁻ 的误差。

      • Hᵏ 是函数 h 关于状态 x 的雅可比矩阵 (Jacobian Matrix),在点 (x̂ᵏ⁻, 0) 处计算。Hᵏ 就是 h 在当前预测状态点附近的“观测灵敏度”矩阵! 它告诉我们,状态 x 的一个微小变化,会导致预测观测值 zᵏ 发生多大的变化。

      • 这个项线性地修正了由于状态预测误差带来的观测预测偏差。

    • Vᵏ * vᵏ:

      • vᵏ 是观测噪声。

      • Vᵏ 是函数 h 关于观测噪声 v 的雅可比矩阵,也在点 (x̂ᵏ⁻, 0) 处计算。它描述了观测噪声 vᵏ 是如何线性地影响实际观测值 zᵏ 的。同样,如果噪声是加性的 (h(x, v) = h_nl(x) + v),那么 Vᵏ 就是单位矩阵 I

  • 得到线性化观测方程:
    经过线性化,我们把非线性的观测方程近似成了一个线性方程:
                zᵏ ≈ h(x̂ᵏ⁻, 0) + Hᵏ * (xᵏ - x̂ᵏ⁻) + Vᵏ * vᵏ

步骤 3:预测步 (Predict) - 基于线性化模型进行预测

现在我们有了线性化的状态方程,可以像 KF 的预测步一样操作了。关键点:在预测状态时,我们不知道 xᵏ⁻¹ 的真实值,也不知道噪声 wᵏ⁻¹ 的具体值,我们只有它们的统计特性(均值和协方差)。

  1. 预测状态 x̂ᵏ⁻

    • 回顾线性化方程:xᵏ ≈ f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹

    • 我们取这个方程的期望值 (Expected Value) 作为预测状态:
      x̂ᵏ⁻ = E[xᵏ] ≈ E[f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹]

    • 拆解:

      • f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) 是常数,期望是自己:f(x̂ᵏ⁻¹, uᵏ⁻¹, 0)

      • E[xᵏ⁻¹ - x̂ᵏ⁻¹]: x̂ᵏ⁻¹ 是 k-1 时刻的最优估计(后验估计),我们假设它的期望误差为 0 (E[xᵏ⁻¹ - x̂ᵏ⁻¹] = 0)。

      • E[wᵏ⁻¹] = 0 (噪声均值为 0)。

    • 因此:
                     x̂ᵏ⁻ = f(x̂ᵏ⁻¹, uᵏ⁻¹, 0)

    • 解读: 预测状态就是直接把非线性状态转移函数 f 作用在上一时刻的最优估计 x̂ᵏ⁻¹ 和当前控制输入 uᵏ⁻¹ 上,并且把过程噪声设为其均值 0。这是最直接的预测方式。

  2. 预测协方差 Pᵏ⁻

    • 协方差衡量状态估计的不确定性。我们需要计算预测状态 x̂ᵏ⁻ 的协方差 Pᵏ⁻ = Cov(x̂ᵏ⁻) ≈ Cov(xᵏ)

    • 回顾线性化方程:xᵏ ≈ f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹

    • 定义预测误差:eᵏ⁻ = xᵏ - x̂ᵏ⁻

    • 利用线性化方程和 x̂ᵏ⁻ 的表达式:
      eᵏ⁻ ≈ [f(x̂ᵏ⁻¹, uᵏ⁻¹, 0) + Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹] - f(x̂ᵏ⁻¹, uᵏ⁻¹, 0)
      eᵏ⁻ ≈ Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹

    • 现在计算协方差 Pᵏ⁻ = Cov(eᵏ⁻) ≈ Cov(Fᵏ⁻¹ * (xᵏ⁻¹ - x̂ᵏ⁻¹) + Wᵏ⁻¹ * wᵏ⁻¹)

    • 假设 xᵏ⁻¹ - x̂ᵏ⁻¹ (上一时刻的后验估计误差) 和 wᵏ⁻¹ (过程噪声) 是相互独立的。

    • 协方差传播公式:Cov(Ax + By) = A Cov(x) Aᵀ + B Cov(y) Bᵀ (当 x 和 y 独立时)。(基于线性化模型和协方差传播公式:Cov(Ax) = A Cov(x) Aᵀ)

    • 因此:
      Pᵏ⁻ ≈ Fᵏ⁻¹ * Cov(xᵏ⁻¹ - x̂ᵏ⁻¹) * (Fᵏ⁻¹)ᵀ + Wᵏ⁻¹ * Cov(wᵏ⁻¹) * (Wᵏ⁻¹)ᵀ
            Pᵏ⁻ ≈ Fᵏ⁻¹ * Pᵏ⁻¹ * (Fᵏ⁻¹)ᵀ + Wᵏ⁻¹ * Qᵏ⁻¹ * (Wᵏ⁻¹)ᵀ

    • 解读:

      • Pᵏ⁻¹:上一时刻状态估计 (x̂ᵏ⁻¹) 的协方差(不确定性)。

      • Fᵏ⁻¹ * Pᵏ⁻¹ * (Fᵏ⁻¹)ᵀ:这项表示状态估计的不确定性 Pᵏ⁻¹ 通过状态转移模型 Fᵏ⁻¹ 传播到了预测状态。如果模型放大误差 (F 的模很大),预测不确定性就变大;如果模型稳定 (F 的模小),不确定性传播就小。

      • Wᵏ⁻¹ * Qᵏ⁻¹ * (Wᵏ⁻¹)ᵀ:这项表示过程噪声的不确定性 Qᵏ⁻¹ 通过噪声模型 Wᵏ⁻¹ 传播到了预测状态。它代表了模型本身的不精确性带来的额外不确定性。

      • 预测协方差 Pᵏ⁻ 是这两项的和,意味着预测状态的不确定性总是比上一时刻更大(或至少不变),因为运动过程增加了未知因素(噪声)。

步骤 4:更新步 (Update) - 利用观测修正预测

现在我们有了预测状态 x̂ᵏ⁻ 和预测协方差 Pᵏ⁻,并且得到了线性化的观测方程。当实际观测值 zᵏ 到来时,我们就可以像 KF 的更新步一样操作了。关键点:我们不知道真实状态 xᵏ,也不知道观测噪声 vᵏ 的具体值,只有观测值 zᵏ 和噪声的统计特性。

  1. 计算新息 (Innovation) / 残差 ỹᵏ

    • 新息表示实际观测值 zᵏ 与基于预测状态的预测观测值 h(x̂ᵏ⁻, 0) 之间的差异。

    • ỹᵏ = zᵏ - h(x̂ᵏ⁻, 0)

    • 解读: 如果预测状态 x̂ᵏ⁻ 是完美的,并且没有观测噪声,那么 ỹᵏ 应该为 0。ỹᵏ 不为 0 就包含了两个信息:(1) 预测状态可能不准;(2) 观测有噪声。EKF 的工作就是尽可能合理地利用 ỹᵏ 来修正 x̂ᵏ⁻

  2. 计算新息协方差 Sᵏ

    • 新息 ỹᵏ 本身也有不确定性(因为它来源于状态预测误差和观测噪声)。新息协方差 Sᵏ 衡量了这个差异的不确定性。

    • 回顾线性化观测方程:zᵏ ≈ h(x̂ᵏ⁻, 0) + Hᵏ * (xᵏ - x̂ᵏ⁻) + Vᵏ * vᵏ

    • 代入新息定义:
      ỹᵏ = zᵏ - h(x̂ᵏ⁻, 0) ≈ [h(x̂ᵏ⁻, 0) + Hᵏ * (xᵏ - x̂ᵏ⁻) + Vᵏ * vᵏ] - h(x̂ᵏ⁻, 0)
      ỹᵏ ≈ Hᵏ * (xᵏ - x̂ᵏ⁻) + Vᵏ * vᵏ

    • 定义状态预测误差:eᵏ⁻ = xᵏ - x̂ᵏ⁻ (其协方差正是 Pᵏ⁻)。

    • 计算新息协方差 Sᵏ = Cov(ỹᵏ) ≈ Cov(Hᵏ * eᵏ⁻ + Vᵏ * vᵏ)

    • 假设 eᵏ⁻ (状态预测误差) 和 vᵏ (观测噪声) 是相互独立的。

    • 协方差传播公式:Cov(Ax + By) = A Cov(x) Aᵀ + B Cov(y) Bᵀ (当 x 和 y 独立时)。

    • 因此:
      Sᵏ ≈ Hᵏ * Cov(eᵏ⁻) * (Hᵏ)ᵀ + Vᵏ * Cov(vᵏ) * (Vᵏ)ᵀ
               Sᵏ ≈ Hᵏ * Pᵏ⁻ * (Hᵏ)ᵀ + Vᵏ * Rᵏ * (Vᵏ)ᵀ

    • 解读:

      • Hᵏ * Pᵏ⁻ * (Hᵏ)ᵀ:这项表示状态预测的不确定性 Pᵏ⁻ 通过观测模型 Hᵏ 映射到了观测空间。它代表了预测状态不准导致我们“预测会看到什么”也不准的程度。

      • Vᵏ * Rᵏ * (Vᵏ)ᵀ:这项表示观测噪声的不确定性 Rᵏ 通过噪声模型 Vᵏ 传播到了新息中。它代表了传感器本身的不精确性带来的差异。

      • Sᵏ 是这两项的和,是 ỹᵏ 的总协方差。Sᵏ 越大,说明观测值 zᵏ 的可信度越低。

  3. 计算卡尔曼增益 Kᵏ

    • 卡尔曼增益 Kᵏ 是一个权重矩阵,它决定了我们应该多大程度地相信观测值 zᵏ 来修正预测值 x̂ᵏ⁻

    • 公式:
      Kᵏ = Pᵏ⁻ * (Hᵏ)ᵀ * (Sᵏ)⁻¹

    • 深度解读:

      • (Hᵏ)ᵀ:将观测空间的信息转置回状态空间。

      • (Sᵏ)⁻¹:新息协方差的逆。Sᵏ 越小(观测越可靠),(Sᵏ)⁻¹ 越大,意味着增益 Kᵏ 会越大,我们会更信任这次观测。

      • Pᵏ⁻ * ...:状态预测协方差 Pᵏ⁻ 越大(预测越不可靠),增益 Kᵏ 也会倾向于更大,我们会更依赖观测来修正不靠谱的预测。

      • 本质上,Kᵏ 是状态预测误差协方差 Pᵏ⁻ 在观测空间投影 (Hᵏ * Pᵏ⁻ * (Hᵏ)ᵀ) 与总新息协方差 Sᵏ 的比值。它是最小化后验估计协方差 Pᵏ 的最优权重。

  4. 更新状态估计 x̂ᵏ

    • 这是 EKF 的核心修正步骤。我们利用卡尔曼增益 Kᵏ 将新息 ỹᵏ 的信息“融合”到预测状态 x̂ᵏ⁻ 中:
      x̂ᵏ = x̂ᵏ⁻ + Kᵏ * ỹᵏ

    • 解读:

      • 如果新息 ỹᵏ 是 0(观测值完全符合预测),那么更新后的状态 x̂ᵏ 就等于预测状态 x̂ᵏ⁻

      • 如果新息 ỹᵏ 不为 0,Kᵏ 决定了应该沿着状态空间的哪个方向(由 Hᵏ 决定,因为 ỹᵏ 在观测空间)调整 x̂ᵏ⁻,以及调整多少。

      • 这个公式直接使用了线性化观测方程得到的近似关系 ỹᵏ ≈ Hᵏ * (xᵏ - x̂ᵏ⁻)Kᵏ * ỹᵏ ≈ Kᵏ * Hᵏ * (xᵏ - x̂ᵏ⁻) 就是对状态预测误差 (xᵏ - x̂ᵏ⁻) 的一个线性估计修正项。

  5. 更新协方差估计 Pᵏ

    • 在融合了观测信息后,我们对状态的估计应该更准确了,不确定性应该减小了。更新后的协方差 Pᵏ 反映了这个降低的不确定性。

    • 公式:
      Pᵏ = (I - Kᵏ * Hᵏ) * Pᵏ⁻

    • 深度解读 (非常重要!):

      • 矩阵 (I - Kᵏ * Hᵏ) 被称为更新因子

      • 这个公式来源于更新后状态误差的协方差推导(基于线性化模型和最优估计理论)。它表示:融合观测信息后,状态估计的不确定性等于预测不确定性 Pᵏ⁻ 减去一个修正项 Kᵏ * Hᵏ * Pᵏ⁻

      • Kᵏ * Hᵏ 本质上代表了观测带来的信息量。 观测越可靠 (Sᵏ 小 -> Kᵏ 大),或者观测模型 Hᵏ 越敏感,这个修正项就越大,意味着融合后不确定性 Pᵏ 减少得越多。

      • 一个等价的、数值计算有时更稳定的形式是:Pᵏ = (I - Kᵏ * Hᵏ) * Pᵏ⁻ * (I - Kᵏ * Hᵏ)ᵀ + Kᵏ * Rᵏ * (Kᵏ)ᵀ。这个形式明确包含了观测噪声 Rᵏ 的影响,并保证了 Pᵏ 的对称正定性。

五、EKF算法流程

  1. 初始化:

    • 设定初始状态估计 x̂⁰ (你的最佳猜测)。

    • 设定初始状态估计协方差矩阵 P⁰ (表示你对初始猜测的不确定性有多大,通常设为一个较大的对角矩阵)。

  2. 循环(对于每个时间步k):
    a. 预测:

    • 计算状态转移雅可比矩阵 Fᵏ⁻¹ 和 Wᵏ⁻¹

    • 预测状态:x̂ᵏ⁻ = f(x̂ᵏ⁻¹, uᵏ⁻¹, 0)

    • 预测协方差:Pᵏ⁻ = Fᵏ⁻¹Pᵏ⁻¹(Fᵏ⁻¹)ᵀ + Wᵏ⁻¹Q(Wᵏ⁻¹)ᵀ

    b. 更新:

    • 计算雅可比矩阵 Hᵏ 和 Vᵏ

    • 计算新息:ỹᵏ = zᵏ - h(x̂ᵏ⁻, 0)

    • 计算新息协方差:Sᵏ = HᵏPᵏ⁻(Hᵏ)ᵀ + VᵏR(Vᵏ)ᵀ

    • 计算卡尔曼增益:Kᵏ = Pᵏ⁻(Hᵏ)ᵀ(Sᵏ)⁻¹

    • 更新状态:x̂ᵏ = x̂ᵏ⁻ + Kᵏỹᵏ

    • 更新协方差:Pᵏ = (I - KᵏHᵏ)Pᵏ⁻

六、EKF关键概念详解

1. 雅可比矩阵(Jacobian Matrix)

雅可比矩阵是多变量函数的导数矩阵。对于一个函数 f: Rⁿ → Rᵐ

         [ ∂f₁/∂x₁  ∂f₁/∂x₂  ...  ∂f₁/∂xₙ ]
J_f(x) = [ ∂f₂/∂x₁  ∂f₂/∂x₂  ...  ∂f₂/∂xₙ ]
         [   ...       ...   ...    ...   ]
         [ ∂fₘ/∂x₁  ∂fₘ/∂x₂  ...  ∂fₘ/∂xₙ ]

在EKF中:

  • F 是状态转移函数 f 关于状态 x 的雅可比

  • H 是观测函数 h 关于状态 x 的雅可比

2. 线性化误差

EKF的主要误差来源是线性化近似。当:

  • 系统高度非线性

  • 时间步长过大

  • 初始估计误差大
    时,线性化误差会导致性能下降甚至发散。

3. 与KF的对比

特性卡尔曼滤波 (KF)扩展卡尔曼滤波 (EKF)
系统类型线性非线性
核心方法精确解析解一阶泰勒近似
计算复杂度中(需计算雅可比)
精度最优(线性)次优(有近似误差)
所需矩阵F, H雅可比F, H
适用性线性系统弱非线性系统

七、代码示例

扩展卡尔曼滤波 (EKF) 在机器人位置估计中的 Python 实现

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import matplotlib.transforms as transforms


class ExtendedKalmanFilter:
    def __init__(self, initial_state, initial_covariance, process_noise, measurement_noise):
        """
        扩展卡尔曼滤波器初始化

        参数:
        initial_state: 初始状态估计 [x, y, theta, v]ᵀ
        initial_covariance: 初始协方差矩阵
        process_noise: 过程噪声协方差矩阵 Q
        measurement_noise: 测量噪声协方差矩阵 R
        """
        self.state = initial_state.astype(float)  # 状态向量 [x, y, theta, v]ᵀ
        self.covariance = initial_covariance.astype(float)  # 状态协方差矩阵

        # 噪声协方差矩阵
        self.Q = process_noise  # 过程噪声
        self.R = measurement_noise  # 测量噪声

        # 时间步长
        self.dt = 0.1  # 时间步长 (秒)

    def predict(self, u):
        """
        预测步 - 根据运动模型预测下一时刻的状态

        参数:
        u: 控制输入 [加速度, 角速度]ᵀ
        """
        # 从状态向量中提取变量
        x, y, theta, v = self.state
        a, omega = u  # 加速度和角速度

        # 1. 使用非线性运动模型预测状态
        # 状态转移函数 f(x, u)
        new_x = x + v * np.cos(theta) * self.dt + 0.5 * a * np.cos(theta) * self.dt ** 2
        new_y = y + v * np.sin(theta) * self.dt + 0.5 * a * np.sin(theta) * self.dt ** 2
        new_theta = theta + omega * self.dt
        new_v = v + a * self.dt

        # 更新预测状态
        self.state = np.array([new_x, new_y, new_theta, new_v])

        # 2. 计算状态转移函数的雅可比矩阵 F
        F = np.array([
            [1, 0, -v * np.sin(theta) * self.dt - 0.5 * a * np.sin(theta) * self.dt ** 2, np.cos(theta) * self.dt],
            [0, 1, v * np.cos(theta) * self.dt + 0.5 * a * np.cos(theta) * self.dt ** 2, np.sin(theta) * self.dt],
            [0, 0, 1, 0],
            [0, 0, 0, 1]
        ])

        # 3. 更新协方差矩阵: P = F * P * Fᵀ + Q
        self.covariance = F @ self.covariance @ F.T + self.Q

    def update(self, z):
        """
        更新步 - 根据传感器测量更新状态估计

        参数:
        z: 测量值 [x_gps, y_gps]ᵀ
        """
        # 1. 计算观测函数的雅可比矩阵 H
        # 观测模型 h(x) = [x, y]ᵀ,所以雅可比矩阵很简单
        H = np.array([
            [1, 0, 0, 0],
            [0, 1, 0, 0]
        ])

        # 2. 计算预测的测量值
        # 观测函数 h(x)
        z_pred = np.array([self.state[0], self.state[1]])

        # 3. 计算新息 (测量残差)
        innovation = z - z_pred

        # 4. 计算新息协方差 S = H * P * Hᵀ + R
        S = H @ self.covariance @ H.T + self.R

        # 5. 计算卡尔曼增益 K = P * Hᵀ * S⁻¹
        K = self.covariance @ H.T @ np.linalg.inv(S)

        # 6. 更新状态估计
        self.state = self.state + K @ innovation

        # 7. 更新协方差矩阵
        I = np.eye(self.covariance.shape[0])
        self.covariance = (I - K @ H) @ self.covariance

    def get_position_estimate(self):
        """返回当前位置估计 (x, y)"""
        return self.state[0], self.state[1]

    def get_position_covariance(self):
        """返回位置估计的协方差 (2x2 矩阵)"""
        return self.covariance[:2, :2]


def simulate_robot_motion(true_state, u, process_noise_std):
    """
    模拟机器人的真实运动(带过程噪声)

    参数:
    true_state: 当前真实状态 [x, y, theta, v]ᵀ
    u: 控制输入 [加速度, 角速度]ᵀ
    process_noise_std: 过程噪声标准差 [x, y, theta, v]

    返回:
    new_true_state: 新的真实状态
    """
    x, y, theta, v = true_state
    a, omega = u

    # 添加过程噪声
    noise = np.random.normal(0, process_noise_std, 4)

    # 更新状态(欧拉积分)
    new_x = x + v * np.cos(theta) * dt + 0.5 * a * np.cos(theta) * dt ** 2 + noise[0]
    new_y = y + v * np.sin(theta) * dt + 0.5 * a * np.sin(theta) * dt ** 2 + noise[1]
    new_theta = theta + omega * dt + noise[2]
    new_v = v + a * dt + noise[3]

    return np.array([new_x, new_y, new_theta, new_v])


def get_gps_measurement(true_position, measurement_noise_std):
    """
    模拟GPS测量(带测量噪声)

    参数:
    true_position: 真实位置 (x, y)
    measurement_noise_std: 测量噪声标准差 [x, y]

    返回:
    带有噪声的测量值 [x_gps, y_gps]ᵀ
    """
    noise = np.random.normal(0, measurement_noise_std, 2)
    return np.array([true_position[0] + noise[0], true_position[1] + noise[1]])


def plot_robot(ax, x, y, theta, color, label, uncertainty=None):
    """绘制机器人位置和方向"""
    # 绘制位置点
    ax.plot(x, y, 'o', color=color, label=label)

    # 绘制方向箭头
    arrow_length = 0.5
    dx = arrow_length * np.cos(theta)
    dy = arrow_length * np.sin(theta)
    ax.arrow(x, y, dx, dy, head_width=0.2, head_length=0.2, fc=color, ec=color)

    # 绘制不确定性椭圆(如果提供了协方差)
    if uncertainty is not None:
        # 计算椭圆的角度和半轴长度
        eigvals, eigvecs = np.linalg.eig(uncertainty)
        angle = np.degrees(np.arctan2(eigvecs[1, 0], eigvecs[0, 0]))
        width, height = 2 * np.sqrt(eigvals)

        # 绘制椭圆
        ellipse = Ellipse((x, y), width=width, height=height, angle=angle,
                          edgecolor=color, fc='none', lw=1, linestyle='--')
        ax.add_patch(ellipse)


def plot_uncertainty(ax, ekf_states, true_states, measurements, dt):
    errors = []
    std_devs = []

    # 1. 计算误差和标准差
    for i in range(len(ekf_states)):
        state, cov = ekf_states[i]
        true_state = true_states[i]

        # 计算位置误差(x、y方向的距离)
        error = np.linalg.norm(state[:2] - true_state[:2])
        errors.append(error)

        # 计算位置不确定性(协方差对角线之和的开方)
        std_dev = np.sqrt(cov[0, 0] + cov[1, 1])
        std_devs.append(std_dev)

    # 2. 绘制误差和标准差曲线
    time_steps = np.arange(len(errors)) * dt  # 时间轴:每个时间步对应一个时刻
    ax.plot(time_steps, errors, 'b-', label='估计误差(米)')
    ax.plot(time_steps, std_devs, 'r--', label='估计标准差(米)')

    # 3. 修正测量点时间坐标
    # 测量是“每3步执行一次”,因此第i次测量的实际时间是:i * 3 * dt
    for i, z in enumerate(measurements):
        measurement_time = i * 3 * dt  # 修正:每次测量间隔3个时间步
        # 在x轴对应时间、y=0处标记测量点(用绿色x)
        ax.plot(measurement_time, 0, 'gx', markersize=8, label='GPS测量点' if i == 0 else "")

    # 4. 设置图形属性(确保坐标轴范围合理,避免数据被截断)
    ax.set_xlabel('时间(秒)')
    ax.set_ylabel('位置误差/不确定性(米)')
    ax.set_title('EKF位置估计误差与不确定性演变')
    ax.legend()
    ax.grid(True)
    # 自动调整y轴范围,确保所有数据可见
    ax.set_ylim(bottom=0, top=max(max(errors) * 1.2, max(std_devs) * 1.2))


# 主程序
if __name__ == "__main__":
    # 设置随机种子以确保结果可重现
    np.random.seed(42)

    # 仿真参数
    dt = 0.1  # 时间步长 (秒)
    simulation_time = 20.0  # 总仿真时间 (秒)
    steps = int(simulation_time / dt)  # 总步数

    # 初始状态 [x, y, theta, v]ᵀ
    initial_state = np.array([0, 0, np.pi / 4, 1.0])  # 初始位置(0,0),方向45度,速度1m/s

    # 过程噪声标准差 [x, y, theta, v]
    process_noise_std = np.array([0.01, 0.01, 0.005, 0.02])

    # 测量噪声标准差 [x, y]
    measurement_noise_std = np.array([0.3, 0.3])

    # EKF初始化
    # 初始状态估计 - 我们故意设置一个错误的初始估计
    initial_estimate = np.array([-1.0, 1.0, np.pi / 2, 0.8])

    # 初始协方差矩阵 - 表示初始估计的不确定性
    initial_covariance = np.diag([1.0, 1.0, 0.5, 0.2]) ** 2

    # 过程噪声协方差矩阵
    Q = np.diag(process_noise_std) ** 2

    # 测量噪声协方差矩阵
    R = np.diag(measurement_noise_std) ** 2

    # 创建EKF实例
    ekf = ExtendedKalmanFilter(initial_estimate, initial_covariance, Q, R)

    # 存储状态历史用于绘图
    true_states = [initial_state]
    ekf_states = [(ekf.state.copy(), ekf.get_position_covariance().copy())]
    measurements = []

    # 控制输入序列 - 模拟机器人的运动
    # 前1/3时间:直线加速
    # 中间1/3时间:匀速转弯
    # 最后1/3时间:减速直线
    control_sequence = []
    for i in range(steps):
        if i < steps // 3:
            control_sequence.append([0.5, 0.0])  # 加速,直线
        elif i < 2 * steps // 3:
            control_sequence.append([0.0, 0.3])  # 匀速,转弯
        else:
            control_sequence.append([-0.4, 0.0])  # 减速,直线

    # 主仿真循环
    true_state = initial_state.copy()
    for i in range(steps):
        # 获取当前控制输入
        u = control_sequence[i]

        # 1. 模拟机器人真实运动(带过程噪声)
        true_state = simulate_robot_motion(true_state, u, process_noise_std)
        true_states.append(true_state)

        # 2. EKF预测步
        ekf.predict(u)

        # 3. 模拟GPS测量(每3步测量一次)
        if i % 3 == 0:
            z = get_gps_measurement(true_state[:2], measurement_noise_std)
            measurements.append(z)

            # EKF更新步
            ekf.update(z)

        # 存储EKF当前状态和位置协方差
        ekf_states.append((ekf.state.copy(), ekf.get_position_covariance().copy()))

    # 转换为NumPy数组以便索引
    true_states = np.array(true_states)
    measurements = np.array(measurements)

    # 设置Matplotlib支持中文显示
    plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]  # 支持中文的字体
    plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示异常问题

    # 绘制轨迹
    plt.figure(figsize=(12, 10))

    # 提取估计的位置用于绘图
    est_x = [state[0] for state, _ in ekf_states]  # 从状态元组中提取x
    est_y = [state[1] for state, _ in ekf_states]  # 从状态元组中提取y

    # 绘制真实轨迹
    plt.plot(true_states[:, 0], true_states[:, 1], 'g-', linewidth=2, label='真实轨迹')

    # 绘制EKF估计轨迹
    plt.plot(est_x, est_y, 'b-', linewidth=1.5, label='EKF估计轨迹')  # 使用提取的est_x和est_y

    # 绘制测量点
    plt.plot(measurements[:, 0], measurements[:, 1], 'rx', markersize=6, label='GPS测量')

    # 绘制起点和终点
    plt.plot(true_states[0, 0], true_states[0, 1], 'go', markersize=10, label='起点 (真实)')
    plt.plot(est_x[0], est_y[0], 'bo', markersize=8, label='起点 (估计)')
    plt.plot(true_states[-1, 0], true_states[-1, 1], 'g*', markersize=12, label='终点 (真实)')
    plt.plot(est_x[-1], est_y[-1], 'b*', markersize=10, label='终点 (估计)')

    # 在关键点绘制机器人和方向
    for i in [0, len(true_states) // 3, 2 * len(true_states) // 3, len(true_states) - 1]:
        # 获取该时刻的状态和协方差
        state, cov = ekf_states[i]
        # 绘制EKF估计机器人
        plot_robot(plt.gca(), state[0], state[1], state[2], 'b', '', cov)

    # 设置图形属性
    plt.title('扩展卡尔曼滤波 (EKF) - 机器人位置估计')
    plt.xlabel('X 位置 (米)')
    plt.ylabel('Y 位置 (米)')
    plt.legend(loc='upper left')
    plt.grid(True)
    plt.axis('equal')

    # 绘制不确定性演变
    fig, ax = plt.subplots(figsize=(12, 6))
    plot_uncertainty(ax, ekf_states, true_states, measurements, dt)

    # 显示图形
    plt.tight_layout()
    plt.show()

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JMFS1119

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值