Kalman Filter原理简介及C++实现

本博文内容参考了北卡罗来纳大学教堂山分校的文章  An Introduction to the Kalman Filter

目录

一、Kalman Filter简介

二、估计与观测过程

三、KF的计算起源(Computational Origins of the Filter)

四、离散KF算法(Discrete Kalman Filter Algorithm)


一、Kalman Filter简介

1960年R.E. Kalman(匈牙利裔美国数学家,MIT本科,哥大博士。图为卡尔曼老爷子智慧的凝视。)发表了一篇著名的文章,描述离散线性滤波问题的递归解法。随着数字计算技术的不断发展,卡尔曼滤波器已经渗透到各学科领域,尤其是在控制科学和辅助导航领域。KF的本质是一组数学方程,用递归的方式来估计过程的状态(最小化根误差的均值)。KF的强大之处有以下几点:支持对past, present, 甚至是 future states的估计,即使是在系统精度未知的情况下。


二、估计与观测过程

KF的目的是对系统的状态向量x进行估计,一般是列向量n*1。KF通过一个含有随机量的差分方程来对系统的状态进行估计:

\large x_{k}=Ax_{k-1}+Bu_{k-1}+w_{k-1}

其中的x_k-1是当前时刻的状态,x_k是下一时刻的状态。A是n*n转移矩阵,B是n*l控制矩阵。w是状态转移过程的噪音。

因为我们对系统的观测并不是完美的,会存在一些测量噪音。故观测方程为:

\large z_{k}=Hz_{k}+v_{k}

其中的H是m*n观测矩阵,将n*1状态转化为m*1观测值。同时还要加上一个观测过程的偏差v。

同时,状态转移过程噪声w和测量噪声v都服从normal probability distributions:

\large \left\{\begin{matrix} p(w)\sim N(0,Q) \\ p(v)\sim N(0,R) \end{matrix}\right.

其中Q称为process noise covariance,R称为measurement noise covariance。这两个量会在后续内容中起到重要作用。


三、KF的计算起源(Computational Origins of the Filter)

首先,我们假定一个系统的真实值是无法获取的,但是我们有两个数据来源来去估计状态值:1.可以根据系统的参数(转移矩阵A、控制矩阵B)去推理下一时刻的系统状态;2.我们采用一种有误差的测量方法去测量状态值。

我们定义,根据系统参数推断出的状态叫priori state estimate,即先验状态估计\large \widehat{x}_{k}^{ -}(上标减号意味着还不完整,只是推理出来的值)。在priori的基础上,根据有误差的测量量进行修正过后的状态是posteriori state estimate,即后验估计\large \widehat{x}_{k}。一个例子可以描述一下这个过程:假设体温是以天为单位进行变化,我今天烧37度,然后我根据经验掐指一算明天肯定38度(先验估计),但是真到了明天我拿体温计一量居然是39度,这个破体温计肯定有误差,所以第二天我简单粗暴折衷了一下烧38.5度(后验估计)

好了,明确了这两个估计值,我们来引入两个误差:(疑问:这个真实值xk应该是永远也不明确的,为何可以直接用于计算?)

先验估计误差:\large e^{-}_{k}\equiv x_{k}-\widehat{x}^{-}_{k}

后验估计误差:\large e_{k}\equiv x_{k}-\widehat{x}_{k}

两个误差都是m*1维的列向量,现在将其转化为协方差矩阵:

先验估计误差协方差:\large P^{-}_{k}=E[e^{-}_{k}e^{-T}_{k}]

后验估计误差协方差:\large P_{k}=E[e_{k}e^{T}_{k}]

接下来我们看一个非常重要的公式,由先验估计和观测值来得到后验估计:

计算后验估计:\large \widehat{x}_{k}=\widehat{x}^{-}_{k}+K(z_{k}-H\widehat{x}^{-}_{k})

其中的\large {\color{Blue} {\color{Cyan} }}(z_{k}-H\widehat{x}^{-}_{k})称为残差,表示测量值与先验估计的偏差,注意此量的维度是m*1。K是卡尔曼滤波增益矩阵,负责将m*1的测量偏差量映射成为与状态相同维度的量,以便进行加和,K的维度是n*m。其实K的作用就是在调节预测和观测的关系。

接下来的任务就是调节卡尔曼滤波器增益矩阵K的值,使得\large \widehat{x}_{k}和真实的\large {x}_{k}之间误差最小,即后验估计误差协方差\large P_{k}最小。我们使用的方法就是先用\large \widehat{x}_{k}减去\large P_{k},然后取期望,再对K求导,使倒数为0。最终可以得到K的计算公式:

K计算:\large K_{k}=P^{-}_{k}H^{T}(HP^{-}_{k}H^{T}+R)^{-1}

也即:\large K_{k}=\frac{P^{-}_{k}H^{T}}{HP^{-}_{k}H^{T}+R}

其中的R是测量过程的噪声协方差。可以对K进行定性分析:

★  如果测量非常精准,测量误差趋近于0,R趋近于0,那么上边分式简化以后右侧只剩一个\large H^{-1},即:\large \lim_{R_{k}\rightarrow 0}K_{k}=H^{-1}

★  如果预测非常精准,预测误差趋近于0,\large P^{-}_{k}趋近于0,那么K就应该为0,不用考虑测量结果:\large \lim_{P^{-}_{k}\rightarrow 0}K_{k}=0


四、离散KF算法(Discrete Kalman Filter Algorithm)

Kalman Filter在某一时刻进行状态的预测,并且得到这一时刻的噪声测量反馈。因此,Kalman Filter组:可以分为两个组:time updatemeasurement update。前者负责向前推进状态和误差协方差的估计来获得先验估计。后者负责反馈,对测量结果和先验估计进行联合,得到后验估计。或者称为:predictor equationscorrector equations 
 

两个阶段的公式如下:

更新过程就是这样:


五、Kalman Filter 的 C++实现(基于OpenCV)

本程序共有两路轨迹:自动生成的直线轨迹 和 实时鼠标数据。

#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdio.h>
#include <iostream>

using namespace cv;
using namespace std;
const int winHeight = 700;
const int winWidth = 1200;

KalmanFilter CreateKF(double sys_noise);
Point Kalman_Predict(KalmanFilter KF);
Point mousePosition = Point(winWidth >> 1, winHeight >> 1);
bool Match_straegy_bin(Point pridict_1, Point pridict_2, Mat measurement_A, Mat measurement_B);

//mouse event callback
void mouseEvent(int event, int x, int y, int flags, void *param)
{
	if (event == CV_EVENT_MOUSEMOVE) {
		mousePosition = Point(x, y);
	}
}

int main(void)
{
	int i = 0;
	const int measureNum = 2;
	Point track[2][5000];
	Point predict_pt, predict_pt_1;	// 预测用的点

	KalmanFilter KF = CreateKF(1e-5);	// 创建一个Kalman Filter
	KalmanFilter KF_1 = CreateKF(1e-6);

	// 生成假数据
	Point data[2][800];
	for (int i = 0; i < 800; i++)
		data[0][i] = Point(i + 50, 200);
	for (int j = 0; j < 800; j++)
		data[1][j] = Point(j + 50, 200);
	
	//初始测量值x'(0),因为后面要更新这个值,所以必须先定义
	Mat measurement_A = Mat::zeros(measureNum, 1, CV_32F);                           
	Mat measurement_B = Mat::zeros(measureNum, 1, CV_32F);

	// 新建一个窗口界面
	namedWindow("kalman");
	setMouseCallback("kalman", mouseEvent);
	Mat image(winHeight, winWidth, CV_8UC3, Scalar(0));

	for(int k=100;k<500;k++)
	{
		//1.kalman prediction	
		predict_pt = Kalman_Predict(KF);
		predict_pt_1 = Kalman_Predict(KF_1);

		//2.update measurement
		//顺序版本的数据
		measurement_A.at<float>(0) = data[0][k].x;
		measurement_A.at<float>(1) = data[0][k].y;
		//鼠标+规整数据
		measurement_B.at<float>(0) = (float)mousePosition.x;
		measurement_B.at<float>(1) = (float)mousePosition.y;

		//使用匹配策略
		bool match_1_A = Match_straegy_bin(predict_pt, predict_pt_1, measurement_A, measurement_B);
		if (match_1_A)
		{
			KF.correct(measurement_A);
			KF_1.correct(measurement_B);
		}
		else
		{
			KF.correct(measurement_B);
			KF_1.correct(measurement_A);
		}

		//draw 
		image.setTo(Scalar(255, 255, 255, 0));
		circle(image, predict_pt, 5, Scalar(0, 255, 0), 3);    //predicted point with green
		circle(image, predict_pt_1, 5, Scalar(0, 0, 255), 3);    //predicted point with green
		circle(image, mousePosition, 5, Scalar(255, 0, 0), 3); //current position with red	

		track[0][i] = predict_pt;
		track[1][i] = predict_pt_1;
		

		// 绘制轨迹
		for (int j = 0; j < i; j++)
		{
			circle(image, track[0][j], 3, Scalar(0, 0, 0), 1);
			circle(image, track[1][j], 3, Scalar(225, 225, 0), 1);
		}

		i = i + 1;

		char buf[256];
		sprintf_s(buf, 256, "predicted position:(%3d,%3d)", predict_pt.x, predict_pt.y);
		putText(image, buf, Point(10, 30), CV_FONT_HERSHEY_SCRIPT_COMPLEX, 1, Scalar(0, 0, 0), 1, 8);

		sprintf_s(buf, 256, "current position :(%3d,%3d)", mousePosition.x, mousePosition.y);
		putText(image, buf, cvPoint(10, 60), CV_FONT_HERSHEY_SCRIPT_COMPLEX, 1, Scalar(0, 0, 0), 1, 8);

		imshow("kalman", image);
		int key = waitKey(3);
		if (key == 27) {//esc   
			break;
		}
	}
	return 0;
}

KalmanFilter CreateKF(double sys_noise)		// 创建一个Kalman滤波器
{
	RNG rng;
	//1.kalman filter setup
	const int stateNum = 4;                                      //状态值4×1向量(x,y,△x,△y)
	const int measureNum = 2;                                    //测量值2×1向量(x,y)	
	KalmanFilter KF(stateNum, measureNum, 0);	// 构建卡尔曼滤波器模型

	KF.transitionMatrix = (Mat_<float>(4, 4) << 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1);  //转移矩阵A
	setIdentity(KF.measurementMatrix);                                             //测量矩阵H
	setIdentity(KF.processNoiseCov, Scalar::all(sys_noise));                            //系统噪声方差矩阵Q 1e-5
	setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));                        //测量噪声方差矩阵R
	setIdentity(KF.errorCovPost, Scalar::all(1));                                  //后验错误估计协方差矩阵P
	rng.fill(KF.statePost, RNG::UNIFORM, 0, winHeight > winWidth ? winWidth : winHeight);   //初始状态值x(0)
	return KF;
}

Point Kalman_Predict(KalmanFilter KF)
{
	Mat prediction = KF.predict();
	Point predict_pt = Point(prediction.at<float>(0), prediction.at<float>(1));   //预测值(x',y')
	return predict_pt;
}

bool Match_straegy_bin(Point pridict_1, Point pridict_2, Mat measurement_A, Mat measurement_B)
{
	//检验预测1和A是否匹配
	double delt_1, delt_2;
	delt_1 = sqrt(pow((measurement_A.at<float>(0) - pridict_1.x), 2) + pow((measurement_A.at<float>(1) - pridict_1.y), 2));
	delt_2 = sqrt(pow((measurement_B.at<float>(0) - pridict_1.x), 2) + pow((measurement_B.at<float>(1) - pridict_1.y), 2));
	if (delt_1 < delt_2)
		return true;
	else
		return false;
}
发布了50 篇原创文章 · 获赞 59 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_36342854/article/details/88600266