滤波方法(四):卡尔曼滤波

上一篇介绍了维纳滤波。它是第一个统计最优的线性滤波器,但受限于三个致命假设:信号平稳、离线处理、单变量。1960 年,鲁道夫·卡尔曼发表了《A New Approach to Linear Filtering and Prediction Problems》,用状态空间方法替代频域方法,一举突破了这三个限制。阿波罗登月的导航计算机、GPS 接收机的位置解算、自动驾驶的传感器融合、金融时间序列的在线平滑,都依赖卡尔曼滤波或其变体。

与维纳滤波的关系

维纳滤波在频域中设计最优滤波器,需要知道信号和噪声的完整功率谱密度,且只能离线处理。卡尔曼滤波把问题转换到时域:用状态方程描述系统动力学,用观测方程描述测量过程,以递推方式在每个时刻仅用当前观测更新估计。两者在平稳线性高斯的特殊情况下给出等价的结果,但卡尔曼滤波的适用范围远大于维纳滤波——它允许非平稳信号、多维耦合状态、实时在线计算。

状态空间模型

卡尔曼滤波将问题描述为一个离散线性动态系统。系统在时刻 $k$ 的状态 $x_k \in \mathbb{R}^n$ 既不可直接观测,又随时间演化且受过程噪声扰动。我们所能获得的只有含观测噪声的测量值 $z_k \in \mathbb{R}^m$:

状态方程: $$x_k = F_k x_{k-1} + B_k u_k + w_k, \quad w_k \sim \mathcal{N}(0, Q_k)$$

观测方程: $$z_k = H_k x_k + v_k, \quad v_k \sim \mathcal{N}(0, R_k)$$

$F_k$($n \times n$)是状态转移矩阵,描述系统在没有噪声和输入时如何从上一状态演化到当前状态。$H_k$($m \times n$)是观测矩阵,描述状态如何映射到测量空间。过程噪声 $w_k$ 和观测噪声 $v_k$ 均为零均值高斯白噪声,互相独立,协方差矩阵分别为 $Q_k$ 和 $R_k$。

预测-更新递推

卡尔曼滤波器以两步递推方式工作。

预测步——基于 $k-1$ 时刻的后验估计 $\hat{x}{k-1|k-1}$ 和后验协方差 $P$,计算 $k$ 时刻的先验估计:

$$\hat{x}{k|k-1} = F_k \hat{x} + B_k u_k$$

$$P_{k|k-1} = F_k P_{k-1|k-1} F_k^T + Q_k$$

更新步——用时刻 $k$ 的观测 $z_k$ 修正先验估计:

$$K_k = P_{k|k-1} H_k^T \left( H_k P_{k|k-1} H_k^T + R_k \right)^{-1}$$

$$\hat{x}{k|k} = \hat{x} \right)$$} + K_k \left( z_k - H_k \hat{x}_{k|k-1

$$P_{k|k} = \left( I - K_k H_k \right) P_{k|k-1}$$

$K_k$ 称为卡尔曼增益矩阵($n \times m$)。$(z_k - H_k \hat{x}_{k|k-1})$ 称为新息或残差,代表观测与预期的偏离。

卡尔曼增益的直觉

以标量情况为例($n = m = 1$,$F = H = 1$),记 $p = P_{k|k-1}$,则

$$K_k = \frac{p}{p + R}$$

当观测噪声方差 $R$ 很大(测量不可靠)时,$K \approx 0$,滤波器几乎忽略观测,后验约等于先验预测。当 $R$ 很小(测量精确)时,$K \approx 1$,滤波器充分信任观测。若过程噪声 $Q$ 较大导致先验协方差 $p$ 增大,$K$ 也随之增大——滤波器意识到自己的预测不可靠,转而更多依赖观测。

贝叶斯视角

在高斯噪声假设下,预测步给出的 $\hat{x}{k|k-1}$ 和 $P}$ 恰好是先验分布 $p(x_k \mid z_{1:k-1}) = \mathcal{N}(\hat{x{k|k-1}, P})$ 的均值和协方差。更新步用贝叶斯公式将观测似然 $p(z_k \mid x_k) = \mathcal{N}(H_k x_k, R_k)$ 与先验结合,所得的后验分布 $p(x_k \mid z_{1:k})$ 的均值恰好是 $\hat{x{k|k}$,协方差恰好是 $P$。因此卡尔曼滤波本质上是线性高斯动态系统的精确贝叶斯递推。

可视化

下图的 GIF 展示了滤波追踪的动态过程:随着新观测不断到达,滤波器的估计(红线)逐步收敛到真实轨迹(黑线),红色椭圆代表估计的不确定性,随时间逐渐缩小。

滤波04-图1 卡尔曼滤波:二维轨迹追踪与不确定性椭圆

Python 实现

下面的代码实现二维平面中匀速运动目标的卡尔曼滤波跟踪。状态向量 $x_k = [p_x, v_x, p_y, v_y]^T$ 包含两个方向的位置和速度,观测只能获取位置分量。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Ellipse
import csv, os

np.random.seed(42)
os.makedirs('images', exist_ok=True)

dt = 1.0
F = np.array([[1, dt, 0,  0],
              [0,  1, 0,  0],
              [0,  0, 1, dt],
              [0,  0, 0,  1]])
H = np.array([[1, 0, 0, 0],
              [0, 0, 1, 0]])
q = 0.05
Q = q * np.array([[dt**3/3, dt**2/2, 0, 0],
                  [dt**2/2, dt, 0, 0],
                  [0, 0, dt**3/3, dt**2/2],
                  [0, 0, dt**2/2, dt]])
R = np.diag([15, 15])

N = 80
x_true = np.zeros((N, 4))
z_obs  = np.zeros((N, 2))
xk = np.array([0, 5, 0, 3])
for k in range(N):
    xk = F @ xk + np.random.multivariate_normal(np.zeros(4), Q)
    x_true[k] = xk
    z_obs[k] = H @ xk + np.random.multivariate_normal(np.zeros(2), R)

x_est  = np.zeros((N, 4))
P_est  = np.zeros((N, 4, 4))
K_hist = np.zeros((N, 4, 2))
x = np.zeros(4)
P = np.eye(4) * 50

for k in range(N):
    x_pred = F @ x
    P_pred = F @ P @ F.T + Q
    S = H @ P_pred @ H.T + R
    K = P_pred @ H.T @ np.linalg.inv(S)
    x = x_pred + K @ (z_obs[k] - H @ x_pred)
    P = (np.eye(4) - K @ H) @ P_pred
    x_est[k]  = x
    P_est[k]  = P
    K_hist[k] = K

fig, axes = plt.subplots(2, 2, figsize=(14, 12))
ax = axes[0, 0]
ax.plot(x_true[:, 0], x_true[:, 2], 'k-', lw=1.0, label='True Trajectory')
ax.scatter(z_obs[:, 0], z_obs[:, 1], s=8, c='gray', alpha=0.4, label='Obs')
ax.plot(x_est[:, 0], x_est[:, 2], 'r-', lw=1.8, label='KF Estimate')
for k in range(0, N, 5):
    cov = P_est[k][[0,2]][:,[0,2]]
    vals, vecs = np.linalg.eigh(cov)
    ang = np.degrees(np.arctan2(vecs[1, 0], vecs[0, 0]))
    ell = Ellipse((x_est[k,0], x_est[k,2]), 2*np.sqrt(vals[0]), 2*np.sqrt(vals[1]),
                  angle=ang, fc='red', alpha=0.06, ec='red', lw=0.3)
    ax.add_patch(ell)
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.set_title('Position Trajectory with Uncertainty Ellipses')
ax.legend(fontsize=8); ax.grid(alpha=0.3); ax.set_aspect('equal')

ax = axes[0, 1]
for d, label, ls in [(0, 'K pos x', '-'), (2, 'K pos y', '--')]:
    ax.plot(K_hist[:, d, 0], ls=ls, lw=1.2, label=label)
ax.set_xlabel('k'); ax.set_ylabel('Gain')
ax.set_title('Kalman Gain Convergence'); ax.legend(); ax.grid(alpha=0.3)

ax = axes[1, 0]
for d, label, c in [(0, 'x Error', 'tab:orange'), (2, 'y Error', 'tab:green')]:
    ax.plot(np.abs(x_true[:, d] - x_est[:, d]), color=c, lw=1.0, label=label)
ax.set_xlabel('k'); ax.set_ylabel('|Error|')
ax.set_title('Estimation Error'); ax.legend(); ax.grid(alpha=0.3)

ax = axes[1, 1]
rmse = np.sqrt(np.cumsum((x_true[:, 1] - x_est[:, 1])**2) / np.arange(1, N+1))
ax.plot(rmse, 'tab:purple', lw=1.5)
ax.set_xlabel('k'); ax.set_ylabel('RMSE')
ax.set_title('Velocity RMSE'); ax.grid(alpha=0.3)

plt.tight_layout()
plt.savefig('images/kalman_tracking.svg', format='svg', bbox_inches='tight')
plt.savefig('images/kalman_tracking.png', dpi=150, bbox_inches='tight')
plt.close()

with open('images/kalman_tracking_data.csv', 'w', newline='') as f:
    w = csv.writer(f)
    w.writerow(['k','true_x','true_y','obs_x','obs_y','est_x','est_y','err_x','err_y'])
    for k in range(N):
        w.writerow([k] + [f'{v:.4f}' for v in [
            x_true[k,0], x_true[k,2], z_obs[k,0], z_obs[k,1],
            x_est[k,0], x_est[k,2],
            abs(x_true[k,0]-x_est[k,0]), abs(x_true[k,2]-x_est[k,2])]])

fig_gif, ax_gif = plt.subplots(figsize=(8, 6))
ax_gif.set_xlim(x_true[:,0].min()-10, x_true[:,0].max()+20)
ax_gif.set_ylim(x_true[:,2].min()-10, x_true[:,2].max()+20)
ax_gif.set_xlabel('x'); ax_gif.set_ylabel('y'); ax_gif.set_aspect('equal')
true_line, = ax_gif.plot([], [], 'k-', lw=1.0, label='True Trajectory')
meas_scat  = ax_gif.scatter([], [], s=6, c='gray', alpha=0.4, label='Obs')
est_line,  = ax_gif.plot([], [], 'r-', lw=1.8, label='KF Estimate')
ell_patch  = Ellipse((0,0), 1, 1, angle=0, fc='red', alpha=0.1, ec='red', lw=0.5)
ax_gif.add_patch(ell_patch)
ax_gif.legend(loc='upper left')

def animate(k):
    true_line.set_data(x_true[:k+1, 0], x_true[:k+1, 2])
    meas_scat.set_offsets(z_obs[:k+1])
    est_line.set_data(x_est[:k+1, 0], x_est[:k+1, 2])
    cov = P_est[k][[0,2]][:,[0,2]]
    ev, evec = np.linalg.eigh(cov)
    if ev[0] > 0:
        ell_patch.set_width(2*np.sqrt(ev[0]))
        ell_patch.set_height(2*np.sqrt(ev[1]))
    ell_patch.set_angle(np.degrees(np.arctan2(evec[1,0], evec[0,0])))
    ell_patch.set_center((x_est[k,0], x_est[k,2]))
    ax_gif.set_title(f'Kalman Filter Tracking  k={k}')
    return true_line, meas_scat, est_line, ell_patch

ani = FuncAnimation(fig_gif, animate, frames=range(0, N, 2),
                    interval=120, blit=False)
ani.save('images/滤波04-图1.gif', writer='pillow', fps=8, dpi=72)
plt.close()
print('Kalman filter outputs saved')

演化与局限

卡尔曼滤波相比维纳滤波是革命性的进步:它支持多维状态、实时递推、非平稳信号,且在线性高斯假设下是最优的。但"线性"这个假设本身就是局限。雷达测量的是距离和方位角——观测方程是非线性的。飞行器的动力学涉及姿态旋转——状态方程是非线性的。一旦 $f$ 或 $h$ 不是线性函数,标准卡尔曼方程中的矩阵乘法就不再适用。

最直接的推广思路是:把非线性函数在当前估计点做一阶泰勒展开,用雅可比矩阵替代线性模型中的 $F_k$ 和 $H_k$。这就是扩展卡尔曼滤波(EKF),下一篇的主题。

参考文献

[1] arXiv:2203.06105 — A Summary on the UD Kalman Filter.

[2] arXiv:2305.00252 — Model-Based Monitoring and State Estimation for Digital Twins: The Kalman Filter.

Category
Tagcloud
Junck Kalman Tool Science Moving Average Exponential Smoothing UKF Nonlinear Filtering FckZhiHu Wiener Filter Code GIS Probability Code Generation Bayesian Story Architecture Non-Gaussian Nonlinear Hackintosh Hadoop Matplotlib OSX-KVM Optimal Estimation History Lens Cellular Automata Translate Unscented Transform Discuss Sequential Monte Carlo Bayesian Estimation Photo OpenWebUI Time Series Windows AI PVE Computability State Space Algorithm QGIS Kalman Filter Photography CUDA Signal Processing Pyenv RX590 PHD C Radio Communicate Prerequisites Ubuntu Sigma Point Simulation LLM Tools Geology Ollama Visualization GlumPy Programming University Scholar Particle Filter ChromeBook Hack RTL-SDR Kivy Guide Qwen3 Camera Math EKF Turing LlamaFactory AMD Jacobian Book Data Game Ventoy Hardware VirtualMachine Mac Frequency Domain Memory Computer GPT-OSS Learning Linux Filtering ML Python Life NumPy Mathematical Modeling Poem Microscope