4、卡尔曼滤波实战:一维位置滤波、二维位置滤波、速度与加速度建模

卡尔曼滤波,说白了就是一套「预测+修正」的数学框架。很多初学者一上来就被那五个公式吓住了,其实没那么玄乎。我当年第一次在嵌入式板子上跑卡尔曼滤波,也是对着公式发懵——但真正上手调过一两个项目后,你会发现它就是个聪明的加权平均器。

这一章,咱们直接动手。从最简单的一维位置滤波开始,逐步扩展到二维,最后加入速度和加速度模型。嗯,代码我会给,但更重要的是背后的物理直觉。

4.1 一维位置滤波:从单点定位说起

假设你只有一个GPS模块,输出的是经度或纬度的一维数值。信号有噪声,你希望得到一个平滑的位置估计。这就是最经典的「一维位置滤波」场景。

状态向量很简单:x = [位置]。状态转移矩阵 F = [1],因为位置在短时间内变化不大。观测矩阵 H = [1],因为我们直接观测位置。

我习惯把卡尔曼滤波拆成两步走:

  1. 预测步:根据上一时刻的状态,猜当前时刻的状态。
  2. 更新步:用当前观测值,修正这个猜测。

下面是一个C语言风格的一维位置滤波核心代码:

// 一维位置卡尔曼滤波
typedef struct {
    float x;    // 状态:位置
    float P;    // 状态协方差
    float Q;    // 过程噪声
    float R;    // 观测噪声
} Kalman1D;

void Kalman1D_Predict(Kalman1D *kf) {
    // 预测状态:位置不变(匀速假设)
    kf->x = kf->x;
    // 预测协方差:加上过程噪声
    kf->P = kf->P + kf->Q;
}

void Kalman1D_Update(Kalman1D *kf, float z) {
    // 卡尔曼增益
    float K = kf->P / (kf->P + kf->R);
    // 更新状态
    kf->x = kf->x + K * (z - kf->x);
    // 更新协方差
    kf->P = (1 - K) * kf->P;
}
调参小技巧:Q值越大,滤波器越「相信」观测值,响应快但噪声大;R值越大,滤波器越「平滑」,但延迟也大。我在项目中一般先设Q=0.01、R=1.0,然后根据实际波形微调。

4.2 二维位置滤波:东向+北向联合估计

实际定位中,我们通常需要同时处理东向(E)和北向(N)两个分量。二维滤波和一维的核心思想完全一样,只是状态向量变成了2x1,矩阵变成了2x2。

状态向量:x = [E, N]^T。状态转移矩阵 F = I(单位矩阵)。观测矩阵 H = I。

为什么可以直接用单位矩阵?因为我们在短时间内假设位置不变——说白了就是「静止假设」。如果目标在运动,那就需要引入速度模型了,这个后面会讲。

二维滤波的代码实现,其实就是把一维的标量运算换成矩阵运算。但在嵌入式平台上,我建议你手写2x2矩阵运算,别偷懒用通用矩阵库——省下的那点代码量,换来的可能是几倍的性能提升。

// 二维位置滤波(简化版,仅展示核心)
typedef struct {
    float x[2];      // 状态:[E, N]
    float P[2][2];   // 协方差矩阵
    float Q[2][2];   // 过程噪声矩阵
    float R[2][2];   // 观测噪声矩阵
} Kalman2D;

void Kalman2D_Update(Kalman2D *kf, float z[2]) {
    // 计算卡尔曼增益矩阵 K = P * (P + R)^(-1)
    float S00 = kf->P[0][0] + kf->R[0][0];
    float S01 = kf->P[0][1] + kf->R[0][1];
    float S10 = kf->P[1][0] + kf->R[1][0];
    float S11 = kf->P[1][1] + kf->R[1][1];
    float det = S00 * S11 - S01 * S10;
    float invS00 = S11 / det;
    float invS01 = -S01 / det;
    float invS10 = -S10 / det;
    float invS11 = S00 / det;

    float K00 = kf->P[0][0]*invS00 + kf->P[0][1]*invS10;
    float K01 = kf->P[0][0]*invS01 + kf->P[0][1]*invS11;
    float K10 = kf->P[1][0]*invS00 + kf->P[1][1]*invS10;
    float K11 = kf->P[1][0]*invS01 + kf->P[1][1]*invS11;

    // 更新状态
    float innov0 = z[0] - kf->x[0];
    float innov1 = z[1] - kf->x[1];
    kf->x[0] += K00 * innov0 + K01 * innov1;
    kf->x[1] += K10 * innov0 + K11 * innov1;

    // 更新协方差(简化版)
    // 实际项目中建议用 Joseph 形式保证对称性
    float temp[2][2];
    // ... 矩阵运算略
}
注意:二维滤波中,协方差矩阵的对称性非常重要。我曾经在项目中因为浮点误差导致P矩阵不对称,结果滤波器直接发散。建议每步更新后强制对称化:P[0][1] = P[1][0] = (P[0][1] + P[1][0]) / 2。

4.3 速度与加速度建模:让滤波器动起来

静止假设的滤波器,在目标移动时会有明显的滞后。怎么办?把速度和加速度也加入状态向量。

状态向量扩展为:x = [位置, 速度, 加速度]^T。状态转移矩阵变成:

F = [[1, dt, 0.5*dt^2],
     [0,  1,    dt    ],
     [0,  0,    1     ]]

这个矩阵的物理含义很直观:位置的变化由速度和加速度共同决定,速度的变化由加速度决定,加速度假设为常数(或者受过程噪声驱动)。

我个人的经验是:加速度模型不要轻易用。为什么?因为加速度的估计非常依赖观测数据的质量。如果GPS更新频率低(比如1Hz),加速度估计基本是噪声。我一般在更新频率≥10Hz时才启用加速度模型。

下面是一个带速度的一维卡尔曼滤波代码片段:

// 一维位置+速度卡尔曼滤波
typedef struct {
    float x[2];    // [位置, 速度]
    float P[2][2];
    float Q[2][2];
    float R;       // 观测噪声(标量,只观测位置)
    float dt;      // 时间步长
} Kalman1D_Vel;

void Kalman1D_Vel_Predict(Kalman1D_Vel *kf) {
    float dt = kf->dt;
    // 预测状态
    float new_x0 = kf->x[0] + dt * kf->x[1];
    float new_x1 = kf->x[1];
    kf->x[0] = new_x0;
    kf->x[1] = new_x1;

    // 预测协方差
    float F[2][2] = {{1, dt}, {0, 1}};
    // P = F * P * F^T + Q
    // ... 矩阵运算略
}
核心要点:速度模型能有效减少动态场景下的跟踪延迟,但代价是状态维度增加,计算量翻倍。对于低速运动(如行人),一维位置滤波+大Q值往往就够了;对于高速运动(如车辆),必须引入速度模型。

4.4 知识体系总览

下面这张图总结了本章的核心逻辑:从一维到二维,从静止到运动,卡尔曼滤波的复杂度逐步提升,但核心的「预测-更新」框架始终不变。

卡尔曼滤波实战:从一维到运动建模 一维位置滤波 状态:位置 矩阵:标量运算 适用:静止/慢速 二维位置滤波 状态:E, N 矩阵:2x2运算 适用:平面定位 速度建模 状态:位置+速度 矩阵:2x2/4x4 适用:动态跟踪 加速度建模 状态:位置+速度+加速度 矩阵:3x3/6x6 适用:高频动态 核心框架:预测(时间更新)→ 更新(观测更新)

4.5 实战避坑指南

最后,分享几个我在项目中踩过的坑:

  • 协方差发散:我曾经在调试车载定位时,滤波器跑了20分钟突然发散。查了半天,发现是浮点精度导致P矩阵失去正定性。解决方案:每步更新后强制检查P矩阵的对角线元素,如果出现负数,重置为一个小正数。
  • 时间步长不一致:卡尔曼滤波假设固定的dt,但实际GPS数据可能不均匀。我建议在预测步中动态计算dt,而不是用固定值。否则,滤波器在数据间隔突然变大时会严重偏离真实值。
  • Q和R的物理意义:别把Q和R当成玄学参数。Q反映你对模型的信任程度,R反映你对传感器的信任程度。一个简单的调参方法:让Q和R的比值等于「模型误差方差」与「观测误差方差」的比值。
我的习惯:在嵌入式平台上,我会把卡尔曼滤波的调试接口暴露出来——比如通过串口打印P矩阵的对角线元素。这样在实车测试时,可以实时观察滤波器是否收敛。嗯,这个习惯帮我省了不少排查时间。

好了,这一章的内容就到这里。从一维到二维,从静止到运动,卡尔曼滤波的每一步扩展都有明确的物理含义。你想想看,其实它就是在做一件事:用模型预测未来,用观测修正现在。掌握了这个核心思想,剩下的就是矩阵运算和参数调优了。

可可精品资料库