卡尔曼滤波的简单实现(Matlab & OC)

本文将不解释卡尔曼滤波具体的数学原理,不作为小白晋升高端玩家的台阶,只说卡尔曼滤波在简单场景下(处理温度、加速度计、RSSI等数据)怎样只用三行代码实现,及相关参数的设置。代码提供Matlab和Objective-C版本。

最近做的项目用到iPhone收集的蓝牙信号强度,即RSSI值。发现果然如文献中所说,即使手机保持静止,检测到的来自同一蓝牙信标的信号强度也会不停地上下波动,且数据(滤除粗大误差后)呈高斯分布。许多文献中也提到,对付这样的噪声可以用卡尔曼滤波器。同样地,做自平衡车、四旋翼等用到的陀螺仪、加速度数据也常常用到卡尔曼滤波。

图一:iPhone以10Hz的频率扫描蓝牙,记录下的RSSI值,大致呈高斯分布。
图二:蓝线为RSSI原始数据,大致围绕-67上下浮动;红线为卡尔曼滤波后平缓变化的数据。

百度百科对卡尔曼滤波的简介:

卡尔曼滤波(Kalman filtering)一种利用线性系统状态方程,通过系统输入输出观测数据,对系统状态进行最优估计的算法。由于观测数据中包括系统中的噪声和干扰的影响,所以最优估计也可看作是滤波过程。

卡尔曼滤波的数学原理当然是它最美妙的部分,不过据说挺难看懂的,我也就知难而退了,毕竟在程序中实现好像不需要非常理解其数学原理。有两篇英文博客写得不错,比较生动形象地描述了这个滤波器的工作过程,建议阅读:

一、最后一点点数学

为了不让实现滤波器的代码显得突兀,这里还是留下几个最最不可或缺的方程。实在心急,可以直接跳过这一部分。
(要找OC代码的朋友请不要一并跳过第二部分,那里讲到了参数设置)

  • 被观测的系统

首先我们需要用方程来描述被观测的系统,因为后面滤波器的方程要用到这里的一些参数。卡尔曼滤波器适用于线性系统,设该系统的状态方程和观测方程为:

x(k) = A · x(k-1) + B · u(k) + w(k)
z(k) = H · x(k) + y(k)

x(k) —— k时刻系统的状态
u(k) —— 控制量
w(k) —— 符合高斯分布的过程噪声,其协方差在下文中为Q
z(k) —— k时刻系统的观测值
y(k) —— 符合高斯分布的测量噪声,其协方差在下文中为R
A、B、H —— 系统参数,多输入多输出时为矩阵,单输入单输出时就是几个常数

注意在后面滤波器的方程中我们将不会再直接面对两个噪声w(k)和y(k),而是用到他们的协方差Q和R。至此,A、B、H、Q、R这几个参数都由被观测的系统本身测量过程中的噪声确定了。

  • 滤波器

工程中用到的卡尔曼滤波是一个迭代的过程,每得到一个新的观测值迭代一次,来回来去地更新两个东西:“系统状态”(x)“误差协方差”(P)。由于每次迭代只用到上一次迭代的结果和新的测量值,这样滤波对计算资源的占用是很小的。

每一次迭代,两个步骤:预测修正。预测是根据前一时刻迭代的结果,即x(k-1|k-1)和P(k-1|k-1),来预测这一时刻的系统状态和误差协方差,得到x(k|k-1)和P(k|k-1):

x(k|k-1) = A · x(k-1|k-1) + B · u(k)
P(k|k-1) = A · P(k-1|k-1) · AT + Q

(这里用到的A、B、H、Q、R就是从前面的状态/观测方程中拿来的)
然后计算卡尔曼增益K(k),和这一次的实际测量结果z(k)一起,用于修正系统状态x(k|k-1)及误差协方差P(k|k-1),得到最新的x(k|k)和P(k|k):

K(k) = P(k|k-1) · HT · (H · P(k|k-1) · HT + R)-1
x(k|k) = x(k|k-1) + K(k) · (z(k) - H · x(k|k-1))
P(k|k) = (I - K(k) · H) · P(k|k-1)

好了好了不会有新的公式了。至此这次迭代就算结束了,x(k|k)就是我们要的滤波后的值,它和P(k|k)将会作为x(k-1|k-1)和P(k-1|k-1)用在下一时刻的迭代里。别看现在公式还是一大堆,在我们所谓的“简单场景”下,系统参数一定下来,就能简化很多很多。

先看状态方程和观测方程。假设我们是在测温度、加速度或者记录蓝牙RSSI,此时控制量是没有的,即:

B · u(k) ≡ 0

另外,参数A和H也简单地取1。现在滤波器的预测方程简化为:

① x(k|k-1) = x(k-1|k-1)
② P(k|k-1) = P(k-1|k-1) + Q

同时修正方程变成:

③ K(k) = P(k|k-1) / (P(k|k-1) + R)
④ x(k|k) = x(k|k-1) + K(k) · (z(k) - x(k|k-1))
⑤ P(k|k) = (1 - K(k)) · P(k|k-1)

①对②、③无影响,于是④可以写成:

x(k|k) = x(k-1|k-1) + K(k) · (z(k) - x(k-1|k-1))

在程序中,该式写起来更简单:

x = x + K * (新观测值 - x);

观察②,对这一时刻的预测值不就是上一时刻的修正值+Q嘛,不妨把它合并到上一次迭代中,即⑤改写成:

P(k+1|k) = (1 - K(k)) · P(k|k-1) + Q

这一时刻的P(k+1|k),会作为下一时刻的P(k|k-1),刚好是③需要的。于是整个滤波过程只用这三个式子来迭代即可:

K(k) = P(k|k-1) / (P(k|k-1) + R)
x(k|k) = x(k-1|k-1) + K(k) · (z(k) - x(k-1|k-1))
P(k+1|k) = (1 - K(k)) · P(k|k-1) + Q

二、Matlab实现 & 参数设置

经过前面的推导,我们发现在程序中只需要不断重复这一小段即可实现卡尔曼滤波:

while(新观测值)
{
    K = P / (P + R);
    x = x + K * (新观测值 - x);
    P = (1 - K) · P + Q;
}

没看前面推导的朋友们别担心。现在我们只从程序的角度来看,不管物理意义。只要定好参数、赋好初值,它就能好好迭代下去。我们发现这一堆字母中,x和P只需要赋初值,每次迭代会产生新值;K用不着赋初值;Q和R赋值以后在之后的迭代中也可以改

好消息是,x和P的初值是可以随便设的,强大的卡尔曼滤波器马上就能抹除不合理之处。但需注意,P的初值不能为0,否则滤波器会认为已经没有误差了,不玩儿了,看下图——

只要P(0) ≠ 0,一切都好。

剩下的也就是Q和R了。他俩的物理意义是噪声的协方差,他们的值是需要我们试出来的。举个例子,我们仍用上图的数据——

减小R值以后,滤波效果就没有这么明显了。但这并不意味着R值越大越好,因为我们的数据也可能是这样:

右图中增大了R值以后,虽然滤波结果更平滑,但滤波器会变得不敏感,会有滞后。因此应该根据具体的使用场景收集到的数据来决定Q和R的取值。他俩的取值当然也可以是时变的,可以识别跳变,可以自适应……这些就是高端玩法了,这里不做讨论。

至此,所有准备工作就做完了。Matlab代码也很简单:

% kalman_filter.m

function X = kalman_filter(data,Q,R,x0,P0)
N = length(data);

K = zeros(N,1);
X = zeros(N,1);
P = zeros(N,1);

X(1) = x0;
P(1) = P0;

for i = 2:N
    K(i) = P(i-1) / (P(i-1) + R);
    X(i) = X(i-1) + K(i) * (data(i) - X(i-1));
    P(i) = P(i-1) - K(i) * P(i-1) + Q;
end

调用及作图:

% test.m

clear;

data = [-59,-60,-55,-56,-56,-56,-57,-57,-59,-61,-57,-57,-57,-57,-57,-58,-57,-61,-57,-61,-59,-61,-60,-60,-59,-57,-56,-58,-58,-57,-57,-57,-57,-57,-60,-58,-61,-60,-60,-60,-57,-57,-57,-57,-58,-57,-58,-57,-58,-57,-58,-61,-58,-61,-62,-61,-61,-57,-57,-57,-57,-58,-57,-58,-57,-58,-61,-61,-60,-60,-58,-57,-57,-57,-57,-58,-58,-58,-58,-60,-61,-60,-57,-57,-57,-57,-57,-58,-58,-57,-57,-57,-60,-57,-60,-60,-59,-60,-57,-56]';
result = kalman_filter(data,1e-6,4e-4,-60,1);
i = 1:length(data);
plot(i,result,'r',i,data,'b');

做平面定位的时候可能需要滤x轴和y轴的数据,加速度计还可能有z轴数据。Matlab里面可以很方便地把A、B、H、Q、R变成矩阵,一次性把几个数据同时滤了;但实质上当这些数据互相之间没什么影响时,同时滤和分开滤是一毛一样的,所以上面的代码都可以不改,几个轴的数据分别调用滤波函数就是了。

三、Objective-C实现

虽然代码不多,但还是单开一个类好了。代码借鉴了MGKalman库。

//  KalmanFilter.h

#import <Foundation/Foundation.h>

@interface KalmanFilter : NSObject

/*-----------------------------------------------*\
|  Kalman Filter equations                        |
|                                                 |
|  state equation                                 |
|  x(k) = A·x(k-1) + B·u(k) + w(k-1)              |
|                                                 |
|  observations equation                          |
|  z(k) = H·x(k) + y(k)                           |
|                                                 |
|  prediction equations                           |
|  x(k|k-1) = A·x(k-1|k-1) + B·u(k)               |
|  P(k|k-1) = A·P(k-1|k-1)·A^T + Q                |
|                                                 |
|  correction equations                           |
|  K(k) = P(k|k-1)·H^T·(H·P(k|k-1)·H^T + R)^(-1)  |
|  x(k|k) = x(k|k-1) + K(k)·(z(k) - H·x(k|k-1))   |
|  P(k|k) = (I - K(k)·H)·P(k|k-1)                 |
\*-----------------------------------------------*/

@property (assign, nonatomic) UInt64 timestamp;
@property (assign, nonatomic) float Q;              // Process noise covariance
@property (assign, nonatomic) float R;              // Observation noise covariance
@property (assign, nonatomic) float K;              // Kalman gain
@property (assign, nonatomic) float X;              // State
@property (assign, nonatomic) float P;              // Covariance

- (KalmanFilter *)initWithQ:(float)q R:(float)r X0:(float)x0 P0:(float)p0;
- (float)filterWithObservation:(float)observation;

@end
//  KalmanFilter.m

#import "KalmanFilter.h"

@implementation KalmanFilter

- (KalmanFilter *)initWithQ:(float)q R:(float)r X0:(float)x0 P0:(float)p0 {
    if (self = [super init]) {
        _timestamp = [[NSDate date] timeIntervalSince1970] * 1000;
        _Q = q;
        _R = r;
        _K = 0;
        _X = x0;
        _P = p0;
    }
    
    return self;
}

- (float)filterWithObservation:(float)observation {
    /*-----------------------------------------------*\
    |  Simplified Kalman Filter equations             |
    |                                                 |
    |  state equation                                 |
    |  x(k) = x(k-1) + w(k)                           |
    |                                                 |
    |  observations equation                          |
    |  z(k) = x(k) + y(k)                             |
    |                                                 |
    |  prediction equations                           |
    |  x(k|k-1) = x(k-1|k-1)                          |
    |  P(k|k-1) = P(k-1|k-1) + Q                      |
    |                                                 |
    |  correction equations                           |
    |  K(k) = P(k|k-1)·(P(k|k-1) + R)^(-1)            |
    |  x(k|k) = x(k|k-1) + K(k)·(z(k) - x(k|k-1))     |
    |  P(k|k) = (I - K(k))·P(k|k-1)                   |
    |                                                 |
    |  Iteration equations                            |
    |  K(k) = P(k|k-1)/(P(k|k-1) + R)                 |
    |  x(k|k) = x(k-1|k-1) + K(k)·(z(k) - x(k-1|k-1)) |
    |  P(k+1|k) = (1 - K(k))·P(k|k-1) + Q             |
    \*-----------------------------------------------*/
    
    _timestamp = [[NSDate date] timeIntervalSince1970] * 1000;
    _K = _P / (_P + _R);
    _X = _X + _K * (observation - _X);
    _P = (1 - _K) * _P + _Q;
    
    return _X;
}

@end

初始化及调用:

……
#define val_Q       0.00002
#define val_R        0.0004
#define val_X0            0
#define val_P0            1
……
@property (strong, nonatomic) KalmanFilter *kalmanFilter;
……
// 初始化
_kalmanFilter = [[KalmanFilter alloc] initWithQ:val_Q
                                              R:val_R
                                             X0:val_X0
                                             P0:val_P0];
……
// 完成一次测量时调用
float newX = [_kalmanFilter filterWithObservation:(float)新观测值];
……

这样每一次得到的newX就是滤波后的数据。如果同时要滤几个轴的数据,也可以放在矩阵里运算。但同样地,如果几个轴之间的数据互不影响的话,一起滤和分开滤是一样的。

前面提到的MGKalman是一个OC实现的矩阵形式的卡尔曼滤波器,其用法在GitHub上有说明。本文的数学方程也是从那里摘抄以后修改而来的。

作者给的示例代码中同时滤了两个相互独立的量;经过我的测试,把他俩拆开,不用矩阵,分别用两个滤波器来处理,效果确实一毛一样。这也是我为什么我在项目中没有用他矩阵的那一套,而是看过一点原理以后只用那三行代码的原因。毕竟使用场景简单嘛。

卡尔曼滤波(KF)的简单使用就介绍到这里了。接下来还可以去实时调整Q和R,可以加控制量u(k),可以进一步实现扩展卡尔曼滤波(EKF)、无迹卡尔曼滤波(UKF)等各种算法来应对非线性、非高斯等各种情况。只是对于我来说,现在这样就暂时够用了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容