你有没有遇到过这种情况:手机导航显示“您已到达目的地”,可你明明还在路口等第三个红灯。GPS信号在楼宇之间来回反射,位置估计像喝醉一样乱跳。卡尔曼滤波就是收拾这种乱局的经典工具——它把模型预测和传感器观测按各自的不确定度做加权融合,用五个公式、几十行代码,就能把一条抖成电钻的轨迹变成稳步前进的直线。
这篇文章我会从一个能直接跑通的一维直线运动实例出发,把卡尔曼滤波的公式怎么来的逐个拆开讲,再给完整Python代码和运行结果图,帮你弄懂原理的同时也能在自己的数据上快速上手。适合正在做定位导航、目标跟踪、机器人、时序平滑的读者,也适合纯粹想搞懂卡尔曼滤波公式的同学。放心,不要求你已经会矩阵运算,我会尽量把“为什么”也讲清楚。
1. 卡尔曼滤波到底在解决什么问题
1.1 信预测还是信观测,这是个权衡
先说一个场景。你开车进入隧道,GPS信号完全丢失,导航只能靠车辆运动模型继续推算位置。出了隧道,GPS恢复,它给出的位置和导航里推算的位置差了二十米。这时候你该信哪个?
这就是典型的状态估计问题:预测结果和观测结果不一致,谁也不能全信。我见过很多人第一反应是“取平均”。但取平均看起来很合理,实际效果却不好——预测误差是累积的,车开得越久,模型推算的位置越不可靠;观测误差相对稳定,但偶尔也有剧烈抖动。用一个固定系数把两者平均掉,等于无视误差特性随时间变化的事实。卡尔曼滤波做的事情,是在每个时刻动态计算一个加权系数,这个系数根据当前预测的不确定度和观测噪声的相对大小来调整:观测更可信就多信观测,模型更有把握就多信模型。有理有据,而不是拍脑袋定个0.7、0.3。
1.2 两个假设决定了它的适用范围
卡尔曼滤波能广泛使用,是因为它在两个假设下能得到简洁的解析解。第一,系统是线性的,下一时刻状态可以由当前状态和控制输入的线性组合表示;第二,噪声是高斯的,过程噪声和观测噪声都服从正态分布。这两个假设在现实里当然不完美,但这不代表不能用。很多系统在小范围内可以近似成线性,非高斯噪声也常常能用方差描述主要不确定度。如果你的系统是强非线性,比如无人机大幅度旋转,那就得考虑扩展卡尔曼滤波(EKF)或无迹卡尔曼滤波(UKF),它们是在卡尔曼框架上做的推广。知道边界在哪,比拿着公式硬套重要得多。
1.3 整个算法其实就是一个循环
卡尔曼滤波的完整流程简化成一句话:预测下一步,再用观测修正,然后循环。每一轮分成两个阶段。
预测阶段:用状态方程把上一时刻的最优估计前推一步,同时让协方差矩阵变大。为什么变大?因为模型不完美,每前推一步都会引入新的不确定度。更新阶段:把观测值和预测值的差,也就是新息,乘以一个动态计算出的卡尔曼增益,加回到预测结果上,得到新的最优估计。这时候观测提供了新信息,协方差矩阵相应缩小。就这样循环下去,协方差的趋势是每一轮先变大、再变小,整体逐渐收敛到一个稳定值。如果你把协方差理解成“不确定度的温度计”,整个滤波行为会非常直观。
2. 公式拆解:五个公式看懂卡尔曼滤波
2.1 记号约定,别被下标吓退
卡尔曼滤波的记号体系在中文资料里略有差别,我先把这篇文章统一使用的记号固定下来,后面代码也会一一对应。
x 是状态向量,比如位置和速度;u 是控制输入,比如你踩油门产生的加速度;z 是传感器观测值。A 是状态转移矩阵,描述系统状态如何随时间演化;B 是控制输入矩阵;H 是观测矩阵,把状态映射到观测空间。Q 是过程噪声协方差矩阵,衡量模型本身没考虑到的随机扰动;R 是观测噪声协方差矩阵,衡量传感器读数有多不靠谱;P 是状态估计的协方差矩阵,反映当前估计的不确定度。小写的 x、u、z 是向量,大写的 A、B、H、Q、R、P 是矩阵。一维情况下它们全部退化成普通数字,这也是我们先用一维模型入门的原因。
2.2 预测阶段:我相信模型能往前走多远
预测阶段包含两个公式。第一个是状态预测:
$$ \hat{x}{k|k-1} = A \hat{x}{k-1|k-1} + B u_k $$
意思是:k-1 时刻的最优估计,乘以状态转移矩阵 A,再加上控制输入的影响,就得到 k 时刻还没看到观测结果时的先验估计。下标 k|k-1 表示“站在 k-1 时刻去预测 k 时刻”,k-1|k-1 表示“k-1 时刻已经融合了观测的最优估计”。这套下标读法非常值得养成习惯,很多公式歧义都来自下标没看明白。
第二个是协方差预测:
$$ P_{k|k-1} = A P_{k-1|k-1} A^T + Q $$
它描述不确定性在模型前推过程中怎么变化。如果系统本身是确定性的,A 作用在 P 上会把不确定度变换过去。多维情形下变换规则是 A P Aᵀ,一维时退化成 a²σ²——这其实是随机变量线性变换公式的自然结果。然后再叠加过程噪声 Q,模型没考虑的真实随机扰动让不确定度进一步增加。可以把协方差想象成一个采样形成的“云团”,A P Aᵀ 就是对整个云团做旋转拉伸,拉伸完再叠加上新的模糊,新云团比旧云团更散是必然的。
2.3 更新阶段:观测到了,怎么修正
更新阶段有三个公式,是卡尔曼滤波最精华的部分。
第一个是卡尔曼增益:
$$ K = P_{k|k-1} H^T \left( H P_{k|k-1} H^T + R \right)^{-1} $$
直观理解,分母是预测在观测空间的协方差加上观测噪声协方差,分子是状态预测和观测预测的交叉协方差。当测量噪声 R 很小时,分母由 H P Hᵀ 主导,K 偏大;当 R 很大时,分母巨大,K 趋近于 0。一句话总结:观测越准,K 越大;预测越自信,K 越小。
第二个是状态更新:
$$ \hat{x}{k|k} = \hat{x}{k|k-1} + K \left( z_k - H \hat{x}_{k|k-1} \right) $$
括号里的 z - H x_pred 叫新息(innovation),意思是“观测值和预测值差了多少”。K 乘以新息,相当于告诉系统该向观测方向修正多少。K 大,估计被观测拽得很厉害;K 小,估计几乎保持预测不变。这个结构就是“按不确定性加权修正”的数学体现。
第三个是协方差更新:
$$ P_{k|k} = \left( I - K H \right) P_{k|k-1} $$
观测带来了信息,不确定度应当下降。把原来协方差中已经被观测解释掉的部分剥离开,剩下的就是更新后的不确定度。工程上为了数值稳定,常写成等价形式 P = P_pred - K H P_pred,我后面代码里也采用这种写法。
2.4 为什么 K 是最优的:一个小推导的思路
经常有人问,K 为什么偏偏长这样,是凑出来的吗?不是。卡尔曼滤波的最优性标准是:最小化后验估计误差的均方误差,也就是让 P(k|k) 的迹尽可能小。把状态更新公式代回协方差的定义,得到一个关于 K 的矩阵函数,对 K 求导令其为零,解出来的结果正是那个增益公式。换句话说,在线性高斯模型下,卡尔曼滤波就是最小均方误差意义下的最优线性估计器,任何固定结构都不可能比它做得更好。这个结论非常强,也是卡尔曼滤波诞生半个多世纪仍然被广泛使用的原因。
| 编号 | 公式 | 名称 | 一句话直觉 |
|---|---|---|---|
| 1 | x̂_pred = A x̂ + B u | 先验状态预测 | 按模型把状态往前推 |
| 2 | P_pred = A P Aᵀ + Q | 先验协方差预测 | 不确定度随模型演化并叠加噪声 |
| 3 | K = P_pred Hᵀ (H P_pred Hᵀ + R)⁻¹ | 卡尔曼增益 | 根据预测与观测的不确定度分配信任 |
| 4 | x̂ = x̂_pred + K(z - H x̂_pred) | 后验状态更新 | 用观测修正预测 |
| 5 | P = (I - KH)P_pred | 后验协方差更新 | 修正后不确定度下降 |
3. 完整实例:一维直线运动估计(附 Python 代码)
3.1 场景建模:一条直路上的位置和速度
为了验证滤波效果,模拟时我们先钦定一条“真实轨迹”,再在观测上加噪声,事后把滤波结果和真实轨迹放一起对比,谁优谁劣一目了然。
场景设定为:小车在一条直路上行驶,我们只能通过传感器观测它的位置,且位置观测噪声不小;小车的真实速度大约是 2.5 m/s,但会有随机加速度扰动。仿真 100 步,采样间隔 dt = 1 秒。在这个模型里,状态向量有两个分量:位置 x 和速度 v。状态转移矩阵是:
$$ A = \begin{bmatrix} 1 & dt \ 0 & 1 \end{bmatrix} $$
这表示新位置 = 旧位置 + 速度 x dt,新速度维持不变。观测矩阵 H = [1, 0],意味着传感器只能看到位置,看不到速度。过程噪声 Q 采用连续白噪声加速度模型的离散化形式,加速度扰动标准差取 sigma_a = 0.1,于是:
$$ Q = \begin{bmatrix} \frac{dt^4}{4}\sigma_a^2 & \frac{dt^3}{2}\sigma_a^2 \ \frac{dt^3}{2}\sigma_a^2 & dt^2\sigma_a^2 \end{bmatrix} $$
位置观测噪声方差 R 取 0.5,也就是观测标准差大约 0.7 米。这个配置很接近现实:模型相对可信但非理想,传感器观测存在明显抖动,可以完整看出卡尔曼滤波的平滑作用。
3.2 完整可运行代码,逐段对应公式
下面这段代码可以直接复制到 Jupyter 或任何 Python 环境运行,依赖只有 numpy 和 matplotlib。
import numpy as np import matplotlib.pyplot as plt # 仿真参数 dt = 1.0 # 采样间隔(秒) N = 100 # 仿真步数 sigma_a = 0.1 # 真实加速度随机扰动的标准差 x_true, v_true = 0.0, 2.5 # 真实初始位置和速度 R = 0.5 # 位置观测噪声方差 # 根据连续白噪声加速度模型,得到离散过程噪声协方差 Q Q = np.array([ [dt**4 / 4 * sigma_a**2, dt**3 / 2 * sigma_a**2], [dt**3 / 2 * sigma_a**2, dt**2 * sigma_a**2] ]) A = np.array([[1, dt], [0, 1]]) # 状态转移矩阵 H = np.array([[1, 0]]) # 观测矩阵:只能观测到位置 # 生成真实轨迹与带噪声观测 true_positions = [] measurements = [] for _ in range(N): a = np.random.normal(0, sigma_a) # 真实随机加速度 v_true += a * dt x_true += v_true * dt true_positions.append(x_true) measurements.append(x_true + np.random.normal(0, np.sqrt(R))) true_positions = np.array(true_positions) measurements = np.array(measurements) # 卡尔曼滤波初始化 x_est = np.array([0.0, 2.0]) # 初始估计:位置 0,速度 2 P_est = np.array([[10, 0], [0, 1]]) # 初始协方差:位置不确定度 10m²,速度不确定度 1m²/s² filtered_positions = [] filtered_velocities = [] for z in measurements: # 预测阶段(对应公式 1 和 2) x_pred = A @ x_est P_pred = A @ P_est @ A.T + Q # 卡尔曼增益(对应公式 3) S = H @ P_pred @ H.T + R # 这里的 S 是标量,因为只观测一维位置 K = P_pred @ H.T / S # 在标量观测下,K 退化为一个 2x1 列向量 # 更新阶段(对应公式 4 和 5) innovation = z - H @ x_pred x_est = x_pred + K @ innovation P_est = P_pred - K @ H @ P_pred # 等价于 (I - KH)P_pred,数值上更对称 filtered_positions.append(x_est[0]) filtered_velocities.append(x_est[1]) filtered_positions = np.array(filtered_positions) filtered_velocities = np.array(filtered_velocities)代码流程和公式一一对应:预测阶段算 x_pred、P_pred,更新阶段算 K、x_est、P_est。这样你不再需要背公式,因为代码结构就是公式结构。K 的计算里我对标量 S 直接做了除法,省去了矩阵求逆步骤,更直观。如果你把状态扩到多维,只需要把这里的除法换成 np.linalg.inv(S) 即可,其余逻辑完全一样。
3.3 运行结果图:看着曲线理解滤波行为
这部分对应标题里的“图”。用下面这段代码把结果画出来:
plt.figure(figsize=(10, 4)) plt.plot(range(N), true_positions, 'g-', linewidth=2, label='真实位置') plt.plot(range(N), measurements, 'r.', alpha=0.4, label='位置观测') plt.plot(range(N), filtered_positions, 'b-', linewidth=2, label='卡尔曼估计') plt.legend() plt.xlabel('时间/s') plt.ylabel('位置/m') plt.title('卡尔曼滤波效果:蓝色估计线明显比红色观测点平滑') plt.grid(True) plt.show() # 再看速度估计,这是从位置观测中“无中生有”估计出来的状态 plt.figure(figsize=(10, 4)) plt.plot(range(N), filtered_velocities, 'b-', linewidth=2) plt.axhline(2.5, color='g', linestyle='--', label='真实速度 2.5 m/s') plt.legend() plt.xlabel('时间/s') plt.ylabel('速度/m/s') plt.title('速度估计:前几步从初值收敛到真实值附近') plt.grid(True) plt.show()第一张图里你会看到,红色观测点围绕绿色真实线上下乱跳,而蓝色估计线紧贴绿线而且平滑得多。前两三步蓝色线会有从初始位置 0 往真实位置赶的过程,随后就稳定跟随。第二张图更有意思:真实速度其实没有直接被观测到,但滤波器的速度估计会在大约 5 步内从初值 2.0 收敛到真实速度 2.5 附近,之后只做小幅波动。这就是卡尔曼滤波的“透视能力”:通过位置观测和运动模型,反推出不可直接观测的速度。很多初学者第一次跑出这张图时都会有点惊喜,这正是理解卡尔曼滤波最好的瞬间。
3.4 参数手感:R、Q、P0 改了曲线会怎样变
我在实际调参时发现,光看公式很难建立参数与曲线形态的关联。下面这个经验表是跑了大量实验后沉淀下来的,直接照着看就行:
| 调整哪个参数 | 曲线会怎样变化 | 直观解释 |
|---|---|---|
| R 调大 | 滤波曲线更平滑,但滞后变大 | 认为观测更不可靠,更依赖模型预测 |
| Q 调大 | 滤波曲线更贴观测,抖动变大 | 认为模型扰动更大,需要观测及时纠偏 |
| P0 调大 | 前几步快速跳向观测,然后收敛 | 初始不确定度大,第一轮就会大幅信任观测 |
| P0 设成 0 | 几乎不更新,长期贴着初始估计 | 初始绝对自信,后续观测很难改变观点 |
如果你把代码里的 R 改成 10,会看到蓝色估计线变得异常平直,但真实轨迹里一旦有速度变化,估计会慢半拍;把 Q 改成 10,蓝色线会放弃模型的平滑作用,几乎完全跟随红色观测点,抖动重新变大。调 Q 和 R 本质上就是调“模型”和“传感器”的信任比,这比理论推导更能帮你直接建立工程直觉。
4. 从一维到多维:矩阵扩展与典型应用
4.1 二维平面上的目标跟踪状态模型
如果你已经跑通上面的一维代码,扩展到二维平面几乎是零成本。比如在二维平面上跟踪一个目标,状态向量可以设为 [x, y, vx, vy]ᵀ,四个分量分别代表横向位置、纵向位置和两个方向的速度。假设近匀速运动,dt 时间后:
$$ A = \begin{bmatrix} 1 & 0 & dt & 0 \ 0 & 1 & 0 & dt \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} $$
如果传感器能直接观测平面位置,则观测矩阵是:
$$ H = \begin{bmatrix} 1 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 \end{bmatrix} $$
Q、R、P 也相应扩展成 4x4 或 2x2 矩阵。滤波循环里每一行代码都和前面一维版本一模一样,只是矩阵维度变大。我见过不少初学者看到多维场景就重写代码,其实完全没必要——卡尔曼滤波的递推式天生就是矩阵形式,写代码时把维度换成 4 就行。
4.2 连续模型怎么离散到 Q 矩阵
不少教程直接把 Q 当作一个随意设置的矩阵,但工程里 Q 来自连续模型的离散化。热词里“卡尔曼滤波连续到离散”说的就是这一步。假设连续状态方程为 dx/dt = Fx + Gw,其中 w 是连续白噪声,谱密度矩阵为 Qc。离散化后:
$$ Q_d = \int_0^{dt} e^{F\tau} G Q_c G^T e^{F^T\tau} d\tau $$
工程上常用一级近似:A ≈ I + F dt,Q ≈ G Q_c Gᵀ dt。匀速运动模型里,F 的幂次项积分恰好产生包含 dt² 和 dt³ 的项,这就是前面代码中 Q 矩阵里 dt 幂次形式的来源。这一块虽然推导繁琐,但关乎滤波的实际表现:同样的加速度扰动,采样间隔 dt 不同,Q 的量级会截然不同。把连续模型正确离散化,是避免滤波发散的重要前提。
4.3 三个真实场景:图像跟踪、机器人定位、时序平滑
卡尔曼滤波不是只能用在导航上。做图像目标跟踪时,常把目标中心坐标和宽高作为状态,检测器漏检时用卡尔曼预测结果“续命”,这也是热词里“卡尔曼滤波算法 图像”和“yolo实例分割”里反复出现的组合套路。我见过不少视觉工程直接把检测框序列丢给卡尔曼滤波做平滑,目标框抖动明显减轻,效果立竿见影。
做机器人定位时,轮式里程计相当于模型预测,激光雷达或视觉定位相当于观测,不少 ROS2 定位模块就是卡尔曼或它的变体在底层工作。金融市场上也有人拿它平滑价格序列,但说实话金融时序的噪声远远偏离高斯假设,用它做简单的形态平滑可以,直接预测涨跌就力不从心了。关键还是那句老话:先把实际问题映射到“线性系统 + 高斯噪声”框架,映射得好不好,直接决定滤波效果上限。
5. 常见问题与排查技巧实录
5.1 滤波发散,估计越跑越偏怎么排查
滤波发散是初学者最容易遇到、也最让人崩溃的问题。现象通常很一致:估计曲线和观测曲线看起来都挺正常,但和真实值一比,偏差已经大到完全不可信。
| 症状 | 可能原因 | 排查思路 |
|---|---|---|
| 估计曲线缓慢漂离真实值 | Q 太小,模型过于自信,不能及时跟随真实变化 | 适当调大 Q,或调小 R,给观测更多话语权 |
| 估计曲线剧烈抖动 | R 太小,过于相信观测,噪声跟得太紧 | 调大 R,曲线会立刻安静下来 |
| 前几步剧烈跳变 | P0 初值取得过大,一开始太信任第一个观测 | 把 P0 调到和真实初始不确定度大致匹配 |
| 长期不收敛,一直徘徊在初始值附近 | P0 过小或设为 0,等于不相信所有后续观测 | 给 P0 一个正值,至少和 R 同一量级 |
我自己就吃过一次亏。当时做压力传感器估计,曲线稳定后仍然整体偏了 10%,最初怀疑传感器标定问题,折腾了快两天才发现 Q 被设成了 10⁻⁹ 这种极小值,滤波器把模型当成金科玉律,真实缓慢漂移根本更新不进去。把 Q 调到和漂移方差一致后,问题立刻消失。Q 不是越小越好,它必须匹配真实过程的随机波动水平。
5.2 观测野值会把滤波器带偏,怎么防护
真实传感器数据里偶尔会出现离谱的野值,可能是信号遮挡、电磁干扰,甚至是代码解析错误。这些野值经过卡尔曼更新后,会让估计瞬间跳出去,需要好几步才能拉回来。处理思路是看新息大小:如果新息 z - H x_pred 超过某个门限,说明这次观测非常可疑,应当降低它在更新中的权重。
一维情况下可以这样写:
innovation = z - H @ x_pred S = H @ P_pred @ H.T + R mahal = abs(innovation) / np.sqrt(S) # 一维马氏距离,也就是归一化新息 if mahal > 3.0: # 超过 3 倍标准差,判定为野值,跳过更新,保留预测结果 x_est = x_pred P_est = P_pred else: K = P_pred @ H.T / S x_est = x_pred + K @ innovation P_est = P_pred - K @ H @ P_pred这种做法俗称“门限滤波”或“抗差卡尔曼”的简化版,实际效果很强。它利用的是 S 这个量:预测协方差在观测空间的投影加上观测噪声方差,本身就是归一化尺度,不需要额外计算特征值。我建议在基础滤波调通后立即加上这个逻辑,它能帮你省掉大量排查数据异常的精力。
5.3 调参心得:一次只动一个旋钮
最后分享一个调试流程。我自己的通用做法是,拿到数据先跑一遍,输出两张图:一张是状态估计曲线,一张是新息序列曲线。状态曲线负责看滞后和抖动,新息曲线负责验证噪声假设是否合理。如果新息序列的方差明显大于你设定的 R,说明 R 被低估了,或者模型本身存在未建模误差;如果新息表现出明显的非零均值,说明初始状态或模型可能系统性偏差。
然后开始调参,铁律是一次只动一个参数。改完立刻看两张图的变化,不要同时动 Q、R、P0,否则出了问题都不知道是谁干的。Q 和 R 的绝对值不重要,重要的是它们的比值——比值决定了滤波在“信任模型”和“信任观测”之间的位置。这个过程非常像调 PID,不理解参数含义就随机搜索,效率极低;理解了之后,每个参数改动的方向基本可预期。我习惯把 Q、R、P0 直接暴露成函数参数,方便批量试验。
写这篇记录时,我又过了一遍自己做定位项目踩过的坑——从 P0 设成 0 导致长期不收敛,到 R 设小了曲线抖成心电图。卡尔曼滤波并不是一个冰冷的黑盒公式,它本质上是一种按不确定度动态变化的加权平均,你要做的,是把对模型和传感器的信任翻译成 Q 和 R,再用一张图去验证。建议你把上面的代码拷贝下来运行,把 Q 改成 0.5、R 改成 10 各跑一遍,眼睛看熟了参数和曲线的关系,比背十遍公式都有用。希望这篇实例、公式、代码和图俱全的记录,能让你少走几步弯路。