1. 从一个真实的数据抖动场景说起
做过传感器数据处理的人大概都遇到过这样的画面:明明设备静止不动,串口打印出来的数值却像心电图一样上下跳动;或者小车匀速直线行驶,GPS轨迹却在路面上画起了波浪线。我第一次接触这类问题时,本能反应是"加个滑动平均不就行了",结果发现平滑是平滑了,但信号也跟着变迟钝,转弯时明显滞后,急停时又冲过头。后来才明白,滑动平均本质上是个低通滤波器,它把噪声和有用信号一起削弱了,而卡尔曼滤波的聪明之处在于——它知道"系统下一步应该往哪走",用这个预测去和实测值做加权,权重还是动态调整的。
这篇内容就是围绕"用MATLAB对输入数据做卡尔曼滤波"这件事展开的。不管你是做无人机飞控、机器人定位、电池SOC估算,还是单纯处理一组带噪声的实验数据,只要涉及"从含噪观测里估计真实状态",这套方法都能直接套用。我会从最朴素的直觉讲起,把状态空间、预测更新、协方差矩阵这些听起来唬人的概念拆开揉碎,再给出一份可以直接复制运行的MATLAB代码,最后聊聊我在实际调参中踩过的那些坑。适合有基本MATLAB基础、但对卡尔曼滤波还停留在"知道名字"阶段的读者,也适合已经用过但总调不好参数的朋友。
需要先说明一点:卡尔曼滤波不是万能平滑器,它有明确的适用前提——系统近似线性、噪声近似高斯、模型能大致描述真实动态。脱离这些前提硬套,结果可能比不用还糟。所以下面我会花不少篇幅讲"为什么这么建模",而不是上来就甩公式。
2. 卡尔曼滤波到底在算什么:用"预测+称重"的直觉理解
2.1 一个不用公式的类比
想象你在跟踪一辆在直道上行驶的小车。你有两个信息来源:一是根据上一时刻的位置和速度,推算它现在应该在哪儿(这叫预测);二是车上GPS给你的一个读数(这叫观测)。问题是,预测不一定准,因为小车可能加速或减速;GPS也不一定准,因为信号有噪声。那到底信谁多一点?
卡尔曼滤波给出的答案是:谁的不确定性小,就多信谁。它用协方差来量化"不确定性"——预测的协方差大,说明我对预测没底,那就多信观测;观测噪声大,说明GPS不靠谱,那就多信预测。而且这个权重不是拍脑袋定的,是通过一套递推公式自动算出来的。这就是它比固定权重的互补滤波高明的地方。
2.2 状态、观测、噪声:三个必须想清楚的东西
落到MATLAB实现上,你首先要定义三样东西:
- 状态向量 x:你真正想知道的量。比如一维运动就是
[位置; 速度],二维平面运动就是[x; y; vx; vy]。 - 观测向量 z:传感器实际给你的量。可能只测位置,也可能位置速度都测。
- 噪声:过程噪声 Q(模型没描述到的扰动)和观测噪声 R(传感器误差)。
很多人卡在第一步——不知道该把什么放进状态里。我的经验是:状态里放"你关心的、且能被模型递推的量"。比如你只关心位置,但位置的变化率(速度)会影响下一时刻位置,那速度就得进状态,哪怕传感器不直接测它。卡尔曼滤波的一个隐藏福利就是能估计"没直接测量"的量,这叫可观测性带来的估计能力。
2.3 五个核心方程,其实就两组动作
标准卡尔曼滤波的五个方程可以归成两组:
预测组(时间更新):
x_pred = A * x_prev P_pred = A * P_prev * A' + Q校正组(观测更新):
K = P_pred * H' / (H * P_pred * H' + R) x_new = x_pred + K * (z - H * x_pred) P_new = (I - K * H) * P_predA是状态转移矩阵,H是观测矩阵,K是卡尔曼增益。你不用死记,理解成"先盲猜,再用观测修正,修正力度由K决定"就够了。K越大越信观测,K越小越信预测。而K本身又由P、Q、R共同决定,形成闭环。
提示:初学阶段最容易犯的错是把Q设成0。Q=0意味着你百分百相信自己的模型,滤波器会越来越"固执",最后完全不理会观测,输出一条平滑但严重偏离真实的曲线。
3. 在MATLAB里把模型搭起来:从状态方程到矩阵赋值
3.1 先确定采样时间和运动模型
假设我们处理的是匀速直线运动(CV模型),采样周期为dt。状态取[位置; 速度],那么状态转移矩阵就是:
dt = 0.1; % 采样周期,单位秒 A = [1, dt; 0, 1]; % 匀速模型的状态转移矩阵为什么长这样?因为位置更新是p = p + v*dt,速度在匀速假设下不变,所以第二行是[0, 1]。如果你做的是匀加速运动,状态要加加速度,A变成3x3,第三行是[0, 0, 1],第一行还要补上0.5*dt^2的加速度项。这一步选错,后面怎么调参都救不回来,因为模型和真实动态不匹配属于"结构性误差"。
3.2 观测矩阵H:传感器测什么就写什么
如果传感器只测位置,那么:
H = [1, 0]; % 只观测位置如果位置和速度都能测(比如某些组合导航系统),H就是单位阵。H的作用是把状态空间映射到观测空间,让预测的观测值和真实观测值能相减。这个相减的结果叫新息(innovation),是卡尔曼滤波的"燃料"——没有新息,滤波器就没有修正的依据。
3.3 Q和R:最考验经验的两个矩阵
Q是过程噪声协方差,R是观测噪声协方差。它们决定了滤波器的"性格"。
R相对好办,因为传感器手册通常会给噪声指标,或者你可以让设备静止采集一段数据,算方差:
static_data = ...; % 静止状态下的观测序列 R = var(static_data); % 用实测方差作为观测噪声Q就麻烦多了,因为它代表模型没建模到的扰动,没有现成数据可测。工程上常用两种办法:一是根据经验给一个较小的值,靠试凑;二是用"过程噪声强度"参数化,比如对CV模型:
q = 0.01; % 过程噪声强度,可调 Q = q * [dt^3/3, dt^2/2; dt^2/2, dt];这个形式来自连续白噪声加速度模型的离散化,比随便填两个数更有物理意义。我一般先用这个结构,再调q这一个标量,比同时调四个元素省心得多。
3.4 初始状态和初始协方差
x0 = [0; 0]; % 初始状态估计 P0 = eye(2) * 10; % 初始不确定性,给大一点没关系P0给大一点是安全的,因为滤波器会通过前几次观测快速收敛。反倒是给太小会让滤波器一开始就"过度自信",收敛变慢。这一点和很多人的直觉相反——不确定性估计宁可高估,不要低估。
4. 一份可直接运行的完整MATLAB实现
4.1 生成带噪声的仿真数据
为了让你能立刻验证效果,我先造一段"真值+噪声"的数据:
clear; clc; close all; dt = 0.1; T = 10; t = 0:dt:T-dt; N = length(t); % 真实状态:匀速运动,位置从0开始,速度2 true_pos = 2 * t; true_vel = 2 * ones(1, N); % 观测:只测位置,叠加高斯噪声 rng(42); % 固定随机种子,方便复现 obs_noise_std = 1.5; z = true_pos + obs_noise_std * randn(1, N);这里把噪声标准差设成1.5,比真实信号的变化幅度还大,故意制造一个"难滤"的场景,方便你看清滤波器的价值。
4.2 主循环:预测与校正的交替
A = [1, dt; 0, 1]; H = [1, 0]; q = 0.05; Q = q * [dt^3/3, dt^2/2; dt^2/2, dt]; R = obs_noise_std^2; x_est = [0; 0]; P = eye(2) * 10; I = eye(2); x_hist = zeros(2, N); for k = 1:N % ---- 预测 ---- x_pred = A * x_est; P_pred = A * P * A' + Q; % ---- 校正 ---- y = z(k) - H * x_pred; % 新息 S = H * P_pred * H' + R; % 新息协方差 K = P_pred * H' / S; % 卡尔曼增益 x_est = x_pred + K * y; P = (I - K * H) * P_pred; x_hist(:, k) = x_est; end这段代码就是卡尔曼滤波的全部核心。你会发现它短得有点意外——真正难的不是写代码,而是把A、H、Q、R这四个矩阵和你的实际问题对应上。
4.3 结果可视化与效果对比
figure; plot(t, z, 'Color', [0.8 0.8 0.8], 'LineWidth', 1); hold on; plot(t, true_pos, 'b', 'LineWidth', 1.5); plot(t, x_hist(1,:), 'r', 'LineWidth', 1.5); legend('观测', '真值', '滤波估计'); xlabel('时间 (s)'); ylabel('位置'); title('卡尔曼滤波效果对比'); grid on;跑出来你会看到:灰色观测点抖得厉害,蓝色真值是一条直线,红色滤波曲线紧贴真值且平滑。更妙的是,x_hist(2,:)还能给出速度估计,尽管我们从没直接测过速度——这就是状态空间方法"顺带估计"的能力。
4.4 用表格看清各矩阵的物理含义
| 矩阵 | 维度 | 物理含义 | 调参方向 |
|---|---|---|---|
| A | n×n | 状态如何随时间演化 | 由运动模型决定,不轻易改 |
| H | m×n | 状态到观测的映射 | 由传感器配置决定 |
| Q | n×n | 模型未建模扰动的强度 | 增大→更信观测,响应快但抖 |
| R | m×m | 观测噪声强度 | 增大→更信预测,平滑但滞后 |
| P | n×n | 当前估计的不确定性 | 自动递推,初值给大即可 |
| K | n×m | 观测修正的权重 | 由P、Q、R自动算出 |
这张表建议打印出来贴在显示器边上,调参时对着看,比翻公式快得多。
5. 调参这件事:Q和R的博弈与我的踩坑记录
5.1 Q/R比值才是关键,绝对值意义不大
很多人纠结Q到底该填0.01还是0.001,其实单独看Q没有意义,真正起作用的是Q和R的相对大小。把Q放大10倍和把R缩小10倍,效果几乎一样。所以我的习惯是:先把R用实测噪声方差固定住,然后只调Q这一个旋钮。
调Q的直观感受是这样的:
- Q太小:滤波曲线过于平滑,跟不上真实信号的快速变化,表现为滞后。
- Q太大:滤波曲线开始跟着观测噪声抖,表现为毛刺。
- 合适:在平滑和跟随之间取得平衡。
5.2 我踩过的三个典型坑
第一个坑:把R设得比实际噪声小。有次我用一个标称精度很高的传感器,就按手册把R设得很小,结果滤波器过度信任观测,输出几乎等于原始噪声。后来让设备静止采了5分钟数据一算方差,发现实际噪声是手册值的3倍。教训是:R永远用实测值,别信手册。
第二个坑:状态里漏了关键量。做二维跟踪时我只放了位置没放速度,结果滤波器无法预测运动趋势,本质上退化成了一个加权平均。补上速度状态后,跟踪精度立刻上了一个台阶。判断标准很简单:如果某个量的变化会影响你关心的量的未来值,它就该进状态。
第三个坑:忘了检查新息。新息y = z - H*x_pred应该是一个零均值白噪声序列。如果新息有明显趋势或周期性,说明模型和实际不匹配,这时候再怎么调Q、R都是治标不治本。我现在的习惯是每次滤波后都画一下新息序列,这是最便宜的模型诊断工具。
5.3 数值稳定性:别用inv求逆
新手常写K = P_pred * H' * inv(H*P_pred*H' + R)。功能上没错,但数值上不推荐,因为显式求逆容易在矩阵接近奇异时出问题。MATLAB里更稳的写法是用左除:
K = (P_pred * H') / (H * P_pred * H' + R);对于标量观测(单传感器),分母就是个标量,直接除更快。这个细节在长时间运行或嵌入式移植时特别重要。
6. 从标量到多维:多传感器融合的扩展思路
6.1 观测矩阵堆叠实现多源融合
卡尔曼滤波最迷人的能力之一是天然支持多传感器融合。假设你同时有GPS(测位置)和轮速计(测速度),只需要把观测矩阵堆起来:
H = [1, 0; % GPS测位置 0, 1]; % 轮速计测速度 R = diag([r_gps, r_odo]);% 各自噪声方差放对角 z = [gps_reading; odo_reading];代码主体一行都不用改,因为公式本身就是矩阵形式。这就是为什么我强烈建议一开始就用矩阵写,而不是针对标量情况写死。
6.2 处理缺失观测:让滤波器"跳过"校正
实际系统里传感器偶尔会丢数据。这时不要中断滤波,而是只做预测不做校正:
if is_valid(k) % 正常校正 y = z(k) - H * x_pred; S = H * P_pred * H' + R; K = P_pred * H' / S; x_est = x_pred + K * y; P = (I - K * H) * P_pred; else % 观测缺失,只保留预测 x_est = x_pred; P = P_pred; end注意缺失期间P会持续增大(因为每步都加Q),这意味着滤波器"越来越不确定",一旦观测恢复,它会用更大的增益快速拉回。这个行为非常符合直觉,也是卡尔曼滤波优雅的地方。
6.3 非线性场景的过渡:EKF和UKF
如果你的系统是非线性的(比如用角度做状态、涉及三角函数),标准卡尔曼滤波就不够用了,需要扩展卡尔曼滤波(EKF)或无迹卡尔曼滤波(UKF)。EKF的思路是在当前估计点对非线性函数做一阶泰勒展开,用雅可比矩阵代替A和H。MATLAB里没有内置的通用EKF函数,但自己实现也不难,核心就是把A、H换成雅可比矩阵,其余公式不变。UKF则用一组sigma点传播均值和协方差,精度更高但计算量更大。选哪个取决于你的非线性程度和算力预算——轻度非线性用EKF足够,强非线性且算力充裕再上UKF。
7. 工程落地时的几个实用细节
7.1 用kalman函数做快速验证
如果你只是想快速验证效果,MATLAB控制系统工具箱提供了kalman函数,可以直接根据系统模型算出稳态卡尔曼增益:
sys = ss(A, [eye(2), zeros(2)], H, 0, dt); [~, K_ss] = kalman(sys, Q, R);稳态增益意味着你不用每步都更新P,计算量大幅下降,适合资源受限的嵌入式场景。代价是初始收敛阶段精度略低,但稳态后几乎无差别。
7.2 定点化与实时性考量
如果最终要移植到单片机上,浮点运算可能吃不消。这时可以考虑定点化,但要注意P矩阵的动态范围可能很大,定点位宽不够会导致协方差失去正定性,滤波器发散。我的建议是:先在MATLAB里用浮点跑通并记录P矩阵的取值范围,再据此决定定点格式。另外,如果A、H是常数,很多中间量可以预先算好,进一步省算力。
7.3 一段可复用的函数封装
把主循环封装成函数,方便在不同项目里复用:
function [x_est, P] = kalman_step(x_prev, P_prev, z, A, H, Q, R) x_pred = A * x_prev; P_pred = A * P_prev * A' + Q; y = z - H * x_pred; S = H * P_pred * H' + R; K = (P_pred * H') / S; x_est = x_pred + K * y; P = (eye(size(P_prev)) - K * H) * P_pred; end这样每次调用只需传入当前状态和观测,逻辑清晰,也便于单元测试。
7.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 输出几乎等于观测 | Q过大或R过小 | 增大R或减小Q |
| 输出严重滞后 | Q过小或R过大 | 减小R或增大Q |
| 输出缓慢漂移 | 模型缺少关键状态 | 检查A是否漏项 |
| 新息有趋势 | 模型与真实动态不匹配 | 重新建模或加状态 |
| 滤波器发散 | P失去正定性 | 检查数值稳定性,改用Joseph形式 |
Joseph形式是另一种P更新写法,数值上更稳:
P = (I - K*H) * P_pred * (I - K*H)' + K * R * K';代价是计算量略大,但在长时间运行或低精度浮点环境下值得。
8. 我个人在实际操作中的体会
卡尔曼滤波这东西,公式看一遍就能懂,但真正用好得靠项目喂。我最大的体会是:八成的滤波效果问题,根源都在建模,而不是调参。很多人一上来就猛调Q、R,其实如果A矩阵描述的运动和实际对不上,怎么调都是拆东墙补西墙。所以每次效果不理想,我的第一反应不是动参数,而是回去看状态方程——是不是漏了某个影响状态的量,是不是采样时间和实际不符,是不是把非线性系统硬当线性处理了。
另一个体会是关于新息诊断的。养成画新息序列的习惯后,我排查问题的效率至少翻了一倍。新息均值应该接近零,如果偏离,说明有系统性偏差(比如传感器零偏没标定);新息应该有界,如果越来越大,说明滤波器在发散。这两个简单检查能帮你快速定位大部分问题。
最后分享一个小技巧:调试阶段可以把Q和R都设成可调参数,用MATLAB的fminsearch或fmincon以新息似然为目标自动优化。虽然实际工程里很少这么做(因为要在线运行),但在离线分析阶段,它能帮你快速找到一组合理的参数作为起点,比纯手工试凑高效得多。找到起点后再根据实时性要求做简化,这条路我走过好几次,都挺顺。