第一次接触卡尔曼滤波,是在一个室内定位项目里:手里只有一坨抖得不成样子的蓝牙RSSI测距值,却想画出一条平滑移动轨迹。用移动平均,延迟大到不可用;完全相信传感器,坐标就在原地漂移。后来把卡尔曼滤波跑起来,才真正体会到“预测+修正”这套思路的厉害。如果你也是第一次面对这套公式,或者在网上看过一堆推导却不知道代码里每个矩阵怎么填,这篇文章就是给你的。我会按“为什么需要它 → 五个核心公式 → 一维手算实例 → 二维跟踪代码 → 图像怎么画 → 参数怎么调”的顺序,把卡尔曼滤波拆开讲透。整篇内容不依赖特定平台,所有代码都是Python + NumPy,看完你就能在自己的数据上跑起来。
1. 卡尔曼滤波到底在解决什么问题
1.1 一个传感器不够,一半靠猜一半靠测
卡尔曼滤波这个名字听着吓人,但解决的核心问题很简单:你有一个不完美的测量,还有一个不完美的数学模型,怎么把两者结合起来,得到比任何单一来源都更准的估计。
举个例子。你在开车导航,GPS每秒报一个位置,但城市里高楼遮挡、信号反射,位置会有几米甚至十几米的抖动。你还有另一条信息:车在上一个位置的基础上,按方向盘角度和车速,大概应该往前走了多远。GPS是“测量”,车辆运动模型是“预测”。单看任何一边都不靠谱——预测累积误差会越偏越大,测量又充满噪声。卡尔曼滤波做的事情,就是每秒钟把这两条信息做一次最优合并,而且合并的权重不是拍脑袋固定的,而是根据当前不确定性动态调整的。
1.2 关键前提:线性系统+高斯噪声
卡尔曼滤波能成立,靠两个前提。
第一,系统是线性的。也就是说,下一个状态可以写成“当前状态乘一个系数再加输入”的形式。匀速直线运动、匀加速运动、简单电路响应,都属于这类。第二,所有噪声服从高斯分布(正态分布),而且均值为0。高斯分布有一条很好的性质:两个高斯分布相乘,结果还是高斯分布。这意味着“预测的不确定性”和“测量的不确定性”合并后,不确定性依然能用均值和协方差完整描述。
如果系统非线性,标准卡尔曼滤波直接用会发散,那就得换成扩展卡尔曼滤波EKF或无迹卡尔曼滤波UKF。但那些都是后话,先把线性情况吃透,后面学EKF是水到渠成的事。
1.3 一个生活化类比:老猎人的射击修正
我习惯用这个类比理解卡尔曼滤波的工作方式。一个猎人瞄准远处的猎物,他先凭经验(模型)估算弹道落点,再通过瞄准镜观察猎物当前位置(测量)。他真正的策略是:两边的信息都不全信,而是根据“猎物动作的可预测性”和“瞄准镜的清晰度”来决定各信多少。猎物跑得越随机,他就越依赖瞄准镜;瞄准镜越模糊,他就越依赖经验轨迹。卡尔曼滤波里的Q矩阵和R矩阵,对应的正是“猎物跑得有多随机”和“瞄准镜有多模糊”这两个量的数学描述。
这套框架能适用的场景远不止导航:温度传感器数据平滑、股票价格噪声过滤、目标跟踪、图像中物体位置估计、无人机姿态解算、电池SOC估算,底层都是同一个结构。
2. 五个公式,逐行拆到能背下来
2.1 状态空间模型先说清楚
在公式之前,先约定记号。卡尔曼滤波把一个物理系统写成两个方程:
状态方程:x(k) = F x(k-1) + G u(k) + w(k) 观测方程:z(k) = H x(k) + v(k)
其中x(k)是k时刻的状态向量,比如目标的位置和速度;F是状态转移矩阵,描述“系统自己怎么随时间演变”;u(k)是外部控制量,例如机器人电机指令;G是控制输入矩阵;z(k)是传感器读数;H是观测矩阵,描述“状态怎么映射到测量值”;w和v分别是过程噪声和测量噪声,服从w~N(0, Q)、v~N(0, R)。
很多教材一上来就抛这堆字母,初学者直接懵。我建议你先不要纠结每个矩阵怎么推,只记住一个核心关系:状态是一个你关心的隐藏量,测量是一个能看到但带噪声的量,F负责让状态随时间走,H负责把状态变成测量。
工程里常见情况是连续模型先给定,再按采样周期离散化。比如一个匀速运动模型,连续状态方程是x' = A x,离散后变成x(k) = F x(k-1),其中F = I + A·dt,dt是采样间隔。这就是为什么你会看到很多代码里F矩阵写成[[1, dt], [0, 1]],对应“位置 + 速度”的状态搭配。
2.2 五个公式分三组记
卡尔曼滤波的五个核心公式可以分成三组:
预测组(两个公式):
(1)状态预测:x_pred = F x_prev (2)协方差预测:P_pred = F P_prev F^T + Q
修正组(三个公式):
(3)卡尔曼增益:K = P_pred H^T (H P_pred H^T + R)^(-1) (4)状态更新:x_new = x_pred + K (z - H x_pred) (5)协方差更新:P_new = (I - K H) P_pred
这组公式的记忆要点是:预测组只依赖系统模型F和Q,跟测量值一点关系都没有;修正组才把测量值z引进来。换句话说,在没有新测量的时候,你只能让状态按模型往前推,同时老实承认不确定性在增长(P越来越大);一旦来了测量,就用增益K决定“这个测量值到底信多少”,然后把估计拉向测量。
2.3 每个字母和每个矩阵应该怎么理解
先讲P矩阵。P是状态估计的协方差矩阵,它的对角线元素表示每个状态变量自身的不确定性,非对角线元素表示变量之间的相关性。P越小,代表你对当前估计越有把握。P的传播公式P_pred = F P F^T + Q,本质上是“协方差在线性变换下的传播法则”:一个随机向量经过线性变换F,协方差会变成F P F^T,而Q是过程中新增的不确定性。你可以把它理解成“已知一位朋友的大致位置,他说他在匀速直走,你越猜他走多远,位置的不确定性区间就越大”,Q就是每一步新增的“位置模糊度”。
再讲卡尔曼增益K。K是卡尔曼滤波的魂。先看它的形态:K = P_pred H^T (H P_pred H^T + R)^(-1)。如果简化成标量一维情况,公式变成K = P/(P + R)。这就是一个比例因子:预测的不确定性P越大,K越接近1,说明测量越可信;测量噪声R越大,K越接近0,说明测量越不可信。K的本质是“预测不确定性与测量不确定性之间的比值决定信任权重”。
最后看更新公式x_new = x_pred + K (z - H x_pred)。括号里的部分叫“创新项”,意思是“测量值和预测值之间的差距”。如果创新项为0,说明测量与预测完全一致,什么都不用改;如果不为0,就按增益K修正一部分。这里恰恰体现了卡尔曼滤波的精髓:**它永远不是全盘接受或全盘否定测量,而是有根据地部分接受。**关于协方差更新P_new = (I - K H) P_pred,直观理解是:引入一次有效测量后,你对状态的把握应该变大,P应该变小。I - K H这个矩阵恰好让P收缩。这个收缩量取决于H和K,数学上对应的是“测量提供了多少关于状态的信息”。
2.4 连续到离散的补充说明
标题相关热词里有“卡尔曼滤波连续到离散”,这里多说两句。实际项目里,你的状态方程往往是从物理规律出发用连续微分方程写的,比如动量方程、运动学方程。写成x'(t) = A x(t) + w之后,必须离散成x(k+1) = F x(k) + w_d才能在计算机里跑。对零阶保持输入,一阶近似是F = I + A·dt,Q_d ≈ Q_c·dt。更精确的方式是用矩阵指数:F = e^(A·dt)。对大多数运动学模型,一阶近似已经够用;但如果dt比较大(比如1秒采样间隔),建议用矩阵指数做离散化,代码里直接用scipy.linalg.expm就能算。
3. 一维实例:手把手算一遍温度估计
3.1 场景与参数设定
很多教程直接上二维跟踪代码,新手看完了还是不知道数字从哪来。我先用一个最简单的标量例子把计算流程走一遍。
场景是我在一间恒温实验室里测室温,温度计测量有噪声,总体在25°C附近波动。状态量x是室温,用一个常数模型来建模,即假设房间里温度基本不变,F=1,没有控制输入。采样序列的测量值假设为:z = [25.5, 24.8, 25.2, 24.9]。
参数我都给成实际值:
- 过程噪声方差Q = 0.01,代表“温度真的会发生缓慢漂移”,但幅度很小
- 测量噪声方差R = 0.25,代表“温度计单次读数方差约为0.25”,即标准差0.5°C
- 初始状态x0 = 0,初始协方差P0 = 1,代表“一开始完全不知道室温是多少,所以给一个较大的不确定性”
这里的P0=1不是随便给的,它表示把初始估计方差设为1,也就是标准差1°C。初值给大一点完全没问题,卡尔曼滤波会在若干步内快速收敛。真正不合适的是把P0设成0,那等于告诉滤波器“初始状态绝对准确”,后面测量反而很难修正,这个问题第5节还会展开。
3.2 逐步手算,每一步都看得见
第一步,k=1,收到第一个测量z1 = 25.5。
预测:x_pred = F x0 = 0 P_pred = F P0 F + Q = 1 + 0.01 = 1.01
卡尔曼增益:K = P_pred / (P_pred + R) = 1.01 / (1.01 + 0.25) ≈ 0.8016
状态更新:x1 = 0 + 0.8016 × (25.5 - 0) ≈ 20.44
协方差更新:P1 = (1 - 0.8016) × 1.01 ≈ 0.2004
注意,虽然第一步就把估计拉到20.44,离真值25还有距离,但P已经一下从1缩到0.2。这是因为第一次测量权重很高(K约0.8),滤波器主要信了测量,所以不确定性快速缩小。
第二步,k=2,收到z2 = 24.8。
预测:x_pred = 20.44,P_pred = 0.2004 + 0.01 = 0.2104 K = 0.2104 / (0.2104 + 0.25) ≈ 0.457 x2 = 20.44 + 0.457 × (24.8 - 20.44) ≈ 22.43 P2 = (1 - 0.457) × 0.2104 ≈ 0.1143
第三步,k=3,收到z3 = 25.2。
预测:x_pred = 22.43,P_pred = 0.1143 + 0.01 = 0.1243 K = 0.1243 / (0.1243 + 0.25) ≈ 0.332 x3 = 22.43 + 0.332 × (25.2 - 22.43) ≈ 23.35 P3 = (1 - 0.332) × 0.1243 ≈ 0.0830
第四步,k=4,收到z4 = 24.9。
预测:x_pred = 23.35,P_pred = 0.0830 + 0.01 = 0.0930 K = 0.0930 / (0.0930 + 0.25) ≈ 0.271 x4 = 23.35 + 0.271 × (24.9 - 23.35) ≈ 23.77 P4 = (1 - 0.271) × 0.0930 ≈ 0.0678
表格式的结果如下:
| 时刻 | 测量值 z | 预测值 x_pred | 增益 K | 更新值 x | 协方差 P |
|---|---|---|---|---|---|
| 1 | 25.5 | 0.00 | 0.802 | 20.44 | 0.2004 |
| 2 | 24.8 | 20.44 | 0.457 | 22.43 | 0.1143 |
| 3 | 25.2 | 22.43 | 0.332 | 23.35 | 0.0830 |
| 4 | 24.9 | 23.35 | 0.271 | 23.77 | 0.0678 |
你发现了什么?K在逐步变小,P也在逐步变小。这不是巧合。随着测量不断进来,滤波器对室温的估计越来越有把握,每一次新测量能引起的修正也就越来越小。增益从0.8一路降到0.27,说明滤波器越来越相信自己的预测、越来越不轻易被单个噪声测量带跑。这正好呼应前面说的“信任度动态调节”——这是卡尔曼滤波和普通加权平均的本质区别。普通加权平均的权重是固定的,卡尔曼滤波的权重却是根据不确定性实时算出来的。
3.3 从手算结果中读出的三个结论
第一,协方差P不是单调“往小了缩”,而是在每次更新后缩小、每次预测时又被Q撑大一点,最后在稳态时达到平衡。你能看到P1到P4都是更新后的值,预测阶段引入的Q会让P略微回升。这个“收缩-放大”的循环,是整套算法稳定收敛的微观机制。
第二,如果过程噪声Q设得很小,P会一直缩到非常小,K也趋向0,滤波器几乎不再修正,看起来轨迹很“平滑”,但实际上会跟不上真实变化,这就是工程里常见的“滤波滞后”或“过拟合预测模型”。相反,如果R设得很小,滤波器会非常激进地跟随测量,噪声抑制效果就差。
第三,这个一维例子的代码可以直接写成:
import numpy as np F = 1.0 H = 1.0 Q = 0.01 R = 0.25 x = 0.0 P = 1.0 measurements = [25.5, 24.8, 25.2, 24.9] for z in measurements: x_pred = F * x P_pred = F * P * F + Q K = P_pred * H / (H * P_pred * H + R) x = x_pred + K * (z - H * x_pred) P = (1 - K * H) * P_pred print(f"x={x:.4f}, P={P:.4f}, K={K:.4f}")这段代码如果跑出来,跟手算结果四舍五入是一致的。一维情况下F、H都是标量,代码看起来略像普通的指数平滑,但一旦进入多维,你就需要矩阵版本了。
4. 二维实例:目标跟踪的完整代码
4.1 状态设计:为什么是位置+速度
进入多维示例。最常见的例子是二维平面上的目标跟踪:目标做近似匀速直线运动,传感器只能观测到位置坐标,不能直接测速度。这就有意思了——状态里需要一个测量根本看不到的量(速度),卡尔曼滤波却能把它间接估计出来。
我选择的状态向量是:
x = [px, py, vx, vy]
也就是x方向位置、y方向位置、x方向速度、y方向速度。为什么这样设计?因为已知目标近似匀速运动,位置和速度之间存在明确的线性关系。如果你只把位置放进状态,那系统模型里就没有“位置在连续变化”这一项,滤波效果会很差。把速度加进去,就给了预测一个合理的物理基础。
采样间隔dt取0.1秒,那么状态转移矩阵:
F = [[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]]
这个矩阵的含义非常直接:新的位置 = 旧位置 + 速度 × dt,速度本身保持不变。观测矩阵只需要位置,所以:
H = [[1, 0, 0, 0], [0, 1, 0, 0]]
测量噪声R取[[1.0, 0], [0, 1.0]],这个含义是位置测量的x、y方向噪声方差各为1,且互不相关——也就是说位置测量的标准差是1个单位。过程噪声Q则以“加速度扰动”的形式给出,这里取一个很小的对角阵,比如[[0.01, 0, 0, 0], [0, 0.01, 0, 0], [0, 0, 0.01, 0], [0, 0, 0, 0.01]],代表“目标并不是完全匀速,会有小幅加速”。
4.2 从零写一个最小可用的卡尔曼滤波类
很多项目里的KF代码是从filterpy库直接调的,filterpy确实是卡曼滤波生态里用得最广的第三方库。但为了不黑盒,我先用纯NumPy写一个极简类,方便你逐行看懂。
import numpy as np class KalmanFilter: def __init__(self, F, H, Q, R, x0, P0): self.F = F self.H = H self.Q = Q self.R = R self.x = x0.copy() self.P = P0.copy() def predict(self): self.x = self.F @ self.x self.P = self.F @ self.P @ self.F.T + self.Q def update(self, z): S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.inv(S) innovation = z - self.H @ self.x self.x = self.x + K @ innovation self.P = (np.eye(self.x.shape[0]) - K @ self.H) @ self.P这一段代码要逐行说清楚。
predict方法里,x = F @ x完成状态预测,P = F @ P @ F.T + Q完成协方差预测。这里的@是NumPy矩阵乘法。filterpy内部就是这么干的。
update方法里:
- S = H P H.T + R,称为“新息协方差”,表示预测与测量之间的总不确定性
- K = P H.T @ inv(S),卡尔曼增益矩阵
- innovation = z - H x,测量残差
- x = x + K @ innovation,更新状态
- P = (I - K H) P,更新协方差
为什么用np.linalg.inv(S)而不是直接np.linalg.inv(S)? 因为S是2×2的矩阵,直接求逆没问题。但如果你状态维度高、S接近奇异,更稳的写法是用np.linalg.solve,后面会讲。
4.3 生成模拟数据和完整跑通流程
代码需要一份带噪声的观测数据。我按真实轨迹生成“真实状态”,再往位置坐标上叠高斯噪声,模拟传感器观测。
import numpy as np import matplotlib.pyplot as plt np.random.seed(42) dt = 0.1 T = 100 total_time = T * dt # 真实状态:匀速直线运动 vx, vy = 1.5, 0.8 px, py = 0.0, 0.0 true_states = [] measurements = [] for _ in range(T): px += vx * dt py += vy * dt true_states.append([px, py, vx, vy]) z = [px + np.random.randn() * 1.0, py + np.random.randn() * 1.0] measurements.append(z) true_states = np.array(true_states) measurements = np.array(measurements)再定义滤波器参数并初始化。这里P0我会给一个相对合理的初始值:位置方差给10(标准差约3.2),速度方差给0.1(标准差约0.32)。并不是特别精确的调参,但它反映了初始时“位置不确定、速度大致知道”的状态。
F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]]) H = np.array([[1, 0, 0, 0], [0, 1, 0, 0]]) Q = np.eye(4) * 0.01 R = np.eye(2) * 1.0 x0 = np.array([0, 0, 1.5, 0.8]) P0 = np.array([[10, 0, 0, 0], [0, 10, 0, 0], [0, 0, 0.1, 0], [0, 0, 0, 0.1]]) kf = KalmanFilter(F, H, Q, R, x0, P0) estimates = [] for z in measurements: kf.predict() kf.update(z) estimates.append(kf.x.copy()) estimates = np.array(estimates)整个循环只有三行:先predict,再update,把状态存下。这也是使用卡尔曼滤波的标准姿势,每一步都必须先预测后更新。正常输出会像这样:
第 10 步估计: [ 14.92 7.84 1.44 0.79] 第 50 步估计: [ 73.15 38.92 1.49 0.81] 第 100 步估计: [149.78 79.42 1.51 0.82]如果你自己跑一遍,会发现位置估计与真实位置比较贴近,速度估计从初始的1.5/0.8经过几步后收敛到接近真实值1.5/0.8附近。速度估计的收敛其实是卡尔曼滤波很厉害的一个体现:你从没直接测过速度,只给了位置测量,它却能通过位置变化趋势把速度猜出来。
4.4 协方差传递和增益矩阵的大小怎么看
跑的时候有一个值得观察的点:协方差矩阵P会很快收敛到一个比较小的稳态范围。打印前几步的P就能看到,对角线从10.0掉到1左右,然后不再大幅下降。原因是Q持续注入新的不确定性,阻止P无限趋近于0。这跟一维例子是一致的。
增益矩阵K也会稳定下来。你可以打印K的值,比如它会收敛到类似:
[[0.68, 0.0], [0.0, 0.68], [0.34, 0.0], [0.0, 0.34]]这表示滤波器稳定后,位置那些维度给测量约68%的信任度,速度维度则通过位置测量间接修正,约34%的调整比例。如果你看到K的所有元素都极端接近0,那通常说明Q设得太小;如果K老是接近1,说明R被设得太大或者Q过大,滤波器几乎等于直接信测量,噪声抑制效果会很差。
5. 图怎么画,以及图里应该看到什么
5.1 标准三张图:轨迹、误差、速度
网上讲卡尔曼滤波的帖子里,代码贴了一堆,却没有把“图怎么展示”讲清楚。我建议你固定画三张图,分别对应三个问题:滤波轨迹是否贴合真值、误差是否在理论范围内、速度估计是否合理收敛。
fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # 图一:轨迹对比 axes[0].plot(true_states[:, 0], true_states[:, 1], 'k-', label='True') axes[0].plot(measurements[:, 0], measurements[:, 1], 'b.', alpha=0.3, label='Measured') axes[0].plot(estimates[:, 0], estimates[:, 1], 'r-', label='KF Estimate') axes[0].set_xlabel('x') axes[0].set_ylabel('y') axes[0].legend() axes[0].set_title('Trajectory') # 图二:位置误差与2σ包络 pos_err = np.linalg.norm(estimates[:, :2] - true_states[:, :2], axis=1) # 从P里取位置部分的协方差并算RMS P_pos = estimates_p.copy() # 需要提前保存每一步P sigma = np.sqrt(np.array([P[0, 0] + P[1, 1] for P in P_pos])) axes[1].plot(pos_err, 'r-', label='Position error') axes[1].plot(2 * sigma, 'k--', label='2 sigma') axes[1].set_xlabel('time step') axes[1].set_ylabel('error') axes[1].legend() axes[1].set_title('Position Error') # 图三:速度估计 axes[2].plot(true_states[:, 2], 'k-', label='True vx') axes[2].plot(estimates[:, 2], 'r-', label='Estimated vx') axes[2].plot(true_states[:, 3], 'k--', label='True vy') axes[2].plot(estimates[:, 3], 'b--', label='Estimated vy') axes[2].set_xlabel('time step') axes[2].set_ylabel('velocity') axes[2].legend() axes[2].set_title('Velocity Estimate') plt.tight_layout() plt.show()注意图二需要你在循环里保存每一步的P矩阵,否则画不了包络。修改循环:
estimates = [] P_list = [] for z in measurements: kf.predict() kf.update(z) estimates.append(kf.x.copy()) P_list.append(kf.P.copy())5.2 合格滤波图的三条判据
图拿到手后,你要学会“看”。我看过太多人贴出一张轨迹图就说“效果很好”,但判断滤波好不好,至少要看三条:
第一条,轨迹图上红色估计线是否紧贴黑色真实线,同时蓝色测量点明显比红色线抖动得更厉害。如果估计线和测量线贴得太紧,说明滤波器信任度过高、噪声抑制不足;如果估计线比测量线还平滑但没有贴住真值,就要小心滞后。
第二条,误差图上位置误差是否大部分落在2σ虚线以内。如果误差经常跑出2σ包络,说明你的Q或R设置与实际不符,模型误差被低估。卡尔曼滤波的协方差输出本身就是一个可信区间工具,不去用它太可惜了。如果误差长期贴着某个方向跑,比如始终偏正,这说明系统模型本身有偏置,不是纯随机噪声的问题,可能需要加入偏置项或使用扩展模型。
第三条,速度估计是否快速收敛且后续不震荡。如果速度估计在真实值附近来回大摆,说明Q/R的比例失衡,滤波器对测量过于敏感。如果速度收敛很慢,说明位置噪声太大而对位置信任太低,可以适当调低R。
5.3 一张图看出“滤波发散”的典型形态
我特别建议把你的模拟数据里的Q设成0.0001试试,再把R设成10,你会看到一种非常典型的“发散”形态:估计轨迹在初始段被拉到某个地方后,后面的曲线几乎是直线,跟测量完全不接触,误差图上的误差会单调上升,冲出2σ包络。原因很简单:滤波器认为模型几乎完美,于是几乎放弃测量,预测误差不断累积,协方差P又因为Q太小而被低估,增益K趋近于0,形成恶性循环。这是卡尔曼滤波调试里最经典的一课:协方差输出不只是给人看的中间量,它的数值大小直接控制增益,而增益决定测量有多大的修正权力。
6. 参数调参经验和常见问题速查
6.1 Q、R、P0三个参数的调参逻辑
卡尔曼滤波的调参,本质上就是在Q和R之间找平衡。工程界有一句顺口溜:Q设大,跟测量;R设大,信模型。更准确地说,它们是比值关系:真正起作用的是Q/R的比例,而不是绝对大小。你把Q和R同时乘10,滤波轨迹几乎不变,因为K只依赖于它们的比值(严格说是P和R的相对关系)。但P的数值会变,所以如果你后续要用P来算置信区间,还是得尽量让Q/R的绝对大小贴合实际单位。
具体怎么给初始值,我的做法是:
- R尽量通过实验标定:采集一段静止数据,直接算测量序列的方差,R就有了。这是最可靠的方式,没必要拍脑袋。
- Q更难标定,通常从一个较小的值开始,比如状态量方差的数量级再乘一个0.01~0.1的系数,然后观察误差图。误差经常超2σ,就调大Q;误差轨迹太毛糙、不够平滑,就调小Q。这个过程跟PID调参有点像,没有一次性正确的值,只有“在当前场景下合适”的值。
- P0只会影响滤波前几十步的过渡行为。给一个偏大的初始P0,让前几步的K大一点,滤波器会快速收敛;给一个接近0的P0,会让滤波器“自以为是”,好半天不肯接受测量。我习惯把P0对角线设为比R对角线大几倍,例如标量例子里的P0=1,对应R=0.25。
6.2 代码层面的数值稳定性坑
卡尔曼滤波写起来简单,跑起来却有几个隐蔽坑,这里逐个排掉。
第一个坑是P矩阵失去对称性。理论上P一直是对称正定阵,但浮点运算误差会慢慢破坏对称性,导致后续计算异常甚至P变成非正定、增益出现负值。解决方法是每步更新后做一次对称化:
self.P = (self.P + self.P.T) / 2.0第二个坑是用np.linalg.inv求逆在数值上不够稳。更推荐用np.linalg.solve:
S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.solve(S, np.eye(S.shape[0]))原理是solve通过LU分解求S X = I,比显式求逆再乘更稳定,也更快。对于S可能奇异的情况,还可以用np.linalg.pinv做伪逆,但正常调好的系统里S不会奇异。
第三个坑是滤波发散后P矩阵出现负对角线。一旦发现P里有负数,几乎可以断言是Q或R设得离谱,或者模型与真实系统严重不匹配。排查顺序是:先打印每一步P和K,看K是否趋向0或1,再用误差图判断是否持续超界。
6.3 实际项目里最常见的四个问题对照表
| 现象 | 可能原因 | 排查/解决方向 |
|---|---|---|
| 估计轨迹非常平滑,但滞后真实轨迹明显 | Q太小,模型过度自信 | 调大Q;确认运动模型是否符合实际 |
| 估计轨迹跟随测量抖动,噪声抑制差 | R太小,或Q太大 | 调大R;重新标定传感器噪声方差 |
| 前期估计快速跳变,后期几乎不再修正 | P0设置过小 | 调大P0;检查是否每步都做了预测后再更新 |
| 误差长期跑出2σ包络 | 模型偏差或噪声非高斯 | 检查系统模型是否带偏置;考虑扩展卡尔曼滤波 |
还有一个我在多个项目里踩过的坑:忘记在每步循环里先调用predict再update。有人把调用顺序写成update后再predict,结果就是测量信息总要晚一步才被利用,轨迹出现明显滞后。卡尔曼滤波的设计里,predict是用上一刻的后验状态生成当前时刻的先验预测,update是用当前测量修正这个先验,所以顺序必须是predict→update。在实时系统里,如果测量到达频率和预测频率不一致,还要在这个基础上处理“时间校正”的逻辑,那就是话题之外的内容了。
7. 关于卡尔曼滤波的扩展应用和我的个人体会
卡尔曼滤波不只是教科书里的一堆矩阵,它在我的项目里真正解决过问题。最早调试那个室内定位项目时,我一开始把R估小了十倍,滤波轨迹抖得像心电图;调大R之后又发现穿越走廊拐角时轨迹跟不上。后来我才明白,那种情况下单一R值根本不够,合理的做法是在代码里根据运动状态动态切换R,或者让Q跟随位置速度变化。这就是“自适应卡尔曼滤波”的动机,业界确实有很多变体在解决类似问题。
如果你后续要做视频目标跟踪,你还会发现常规卡尔曼滤波的一个明显短板:测量值偶尔会突然丢失。处理办法不算复杂,丢失测量时只执行predict、不执行update,让状态继续按模型走;一旦测量恢复,再正常执行update。这个策略在很多视觉检测跟踪项目里是标配,比如YOLO检测到的目标框中心作为测量,卡尔曼滤波负责在检测框短暂消失时维持预测轨迹。
还有一点个人经验想留给看到最后的读者:用卡尔曼滤波前,不要急着写代码。先把你的系统模型写成状态方程和观测方程,把每个矩阵的物理单位标清楚,再动手。我见过太多人卡在“F矩阵怎么填”这一步,其实只要回到物理过程想清楚“状态随时间怎么变”,F自然就出来了。模型错了,调参再狠也救不回来;模型对了,卡尔曼滤波真的能给到超出预期的平滑结果。
最后分享一个我调试时的小习惯:不管怎么调参,都把每一步的增益K打印出来。K是整套算法对“预测和测量信任比例”的直接反映,它能告诉你滤波器的状态健康不健康。慢慢地,你会形成手感:K收敛到一个中等大小的稳定值,说明设置合理;K要么0要么1,基本可以断定Q和R失衡了。调试卡尔曼滤波,有时候跟调一部老车很像,参数不是终点,而是你和系统之间反复对话的过程。希望这篇文章已经帮你找到了对话的入口。