卡尔曼滤波是一种递归的估计,即只要获知上一时刻状态的估计值以及当前状态的观测值就可以计算出当前状态的估计值,因此不需要记录观测或者估计的历史信息。
卡尔曼滤波器分为两个阶段:预测与更新。在预测阶段,滤波器使用上一状态的估计,做出对当前状态的估计。在更新阶段,滤波器利用对当前状态的观测值优化在预测阶段获得的预测值,以获得一个更精确的新估计值。
opencv中有KalmanFilter类,参考【1】
class CV_EXPORTS_W KalmanFilter
{
public:
CV_WRAP KalmanFilter();
//dynamParams状态的维度;measureParams 测量值的维度;controlParams控制向量的维数。
CV_WRAP KalmanFilter( int dynamParams, int measureParams, int controlParams = 0, int type = CV_32F );
//重新初始化卡尔曼滤波器
void init( int dynamParams, int measureParams, int controlParams = 0, int type = CV_32F );
//计算预测的状态,返回 statePre
CV_WRAP const Mat& predict( const Mat& control = Mat() );
//根据测量(观测)结果更新预测状态,返回statePost
//measurement不能通过观测方程进行计算得到,要自己定义
CV_WRAP const Mat& correct( const Mat& measurement );
/*
x(k) = A*x(k-1) + B*u(k)+w(k) 运动方程(w(k)协方差为Q)
z(k) = H*x(k) + v(k) 观测方程 (v(k)协方差为R)
*/
CV_PROP_RW Mat statePre; //预测值 (x'(k)): x(k)=A*x(k-1)+B*u(k)
CV_PROP_RW Mat statePost; //状态值 (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k))
CV_PROP_RW Mat transitionMatrix; //状态转移矩阵 (A)
CV_PROP_RW Mat controlMatrix; //控制矩阵 B
CV_PROP_RW Mat measurementMatrix; //测量矩阵 H
CV_PROP_RW Mat processNoiseCov; //系统误差 Q
CV_PROP_RW Mat measurementNoiseCov;//测量误差 R
CV_PROP_RW Mat errorCovPre; //先验协方差(P'(k)): P'(k)=A*P(k-1)*A^T + Q)
CV_PROP_RW Mat gain; //卡尔曼增益 K(k)=P'(k)*H^T*inv(H*P'(k)*H^T+R)
CV_PROP_RW Mat errorCovPost; //后验协方差 (P(k)): P(k)=(I-K(k)*H)*P'(k)
// temporary matrices
Mat temp1;
Mat temp2;
Mat temp3;
Mat temp4;
Mat temp5;
};
x(k) = A*x(k-1) + B*u(k)+w(k) 运动方程
z(k) = H*x(k) + v(k) 观测方程公式用到的变量:参考【2】
(1)定义三个列向量:
- 状态向量x(k):state
- 观测向量z(k):measurement
- 控制向量:u(k):control
(2)三个矩阵
- 状态转移矩阵A:transitionMatrix
- 控制矩阵B:controlMatrix
- 测量矩阵H:measurementMatrix
(3)估计的状态向量和协方差
k-1时刻:
- 后验估计的状态向量 : k-1时刻的最优的状态估计
- 后验估计的状态向量的协方差矩阵:k-1时刻的最优的状态估计的协方差
k时刻:
- 先验估计的状态向量:k时刻的预测的状态估计
- 先验估计的状态向量的协方差矩阵:k时刻的预测的状态估计的协方差
- 后验估计的状态向量:求出k时刻的最优的状态估计
- 后验估计的状态向量的协方差矩阵:求出k时刻的最优的状态估计的协方差
卡尔曼滤波做的是:用k-1时刻的后验数据求k时刻的先验;k时刻的先验数据求k时刻的后验数据(最优的状态估计)
(4)噪声及协方差矩阵
- 系统噪声w(k):协方差为Q (processNoiseCov)
- 测量噪声v(k):协方差R(measurementNoiseCov)
例1 opencv自带程序:建立一个绕某一圆心做匀速圆周运动的小球,但是实际中它会受到系统噪声影响从而其角度和角速度有所变化,通过带有噪声的观测值(真实值+观测噪声)和匀速运动模型的预测值为输入使用KF得到估计值。
在一个圆弧上运动,只有一个自由度:角度。是一个匀速运动,建立匀速运动模型,设定状态变量x = [ x1, x2 ] = [ 角度,△角度]
具体代码如下:
#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <stdio.h>
using namespace std;
using namespace cv;
//根据圆心和夹角计算点的二维坐标
static inline Point calcPoint(Point2f center, double R, double angle)
{
return center + Point2f((float)cos(angle), (float)-sin(angle))*(float)R; // 图像坐标系中y轴向下
}
//为什么cos(angle)没有乘R???
static void help()
{
printf( "\nExamle of c calls to OpenCV's Kalman filter.\n"
" Tracking of rotating point.\n"
" Rotation speed is constant.\n"
" Both state and measurements vectors are 1D (a point angle),\n"
" Measurement is the real point angle + gaussian noise.\n"
" The real and the estimated points are connected with yellow line segment,\n"
" the real and the measured points are connected with red line segment.\n"
" (if Kalman filter works correctly,\n"
" the yellow segment should be shorter than the red one).\n"
"\n"
" Pressing any key (except ESC) will reset the tracking with a different speed.\n"
" Pressing ESC will stop the program.\n"
);
}
int main(int, char**)
{
//help();
Mat img(500, 500, CV_8UC3);
/** @brief 创建卡尔曼滤波器对象KF.
@param dynamParams 2,状态向量的维度,二维列向量(角度,△角度)
@param measureParams 1,测量向量的维度,一维列向量(角度)
@param controlParams 0,控制向量的维度
*/
KalmanFilter KF(2, 1, 0);
Mat state(2, 1, CV_32F); //状态向量x(k)<=>state (角度,△角度)
Mat measurement = Mat::zeros(1, 1, CV_32F); // 测量向量z(k)
Mat processNoise(2, 1, CV_32F); //系统状态噪声w(k)
char code = (char)-1;
for(;;)
{
//返回高斯分布的随机矩阵
randn( state, Scalar::all(0), Scalar::all(0.1) ); //初始化系统状态真实值
/*初始化三个矩阵:状态转移矩阵Ak(transitionMatrix),控制矩阵Bk(controlMatrix)和测量矩阵Hk(measurementMatrix)*/
KF.transitionMatrix = (Mat_<float>(2, 2) << 1, 1, 0, 1); // 匀速运动模型中的状态转移矩阵A
setIdentity(KF.measurementMatrix); //测量矩阵H [1,0]
// 控制向量都没有,控制矩阵Bk(controlMatrix)更没有
/*两个噪声的协方差矩阵*/
setIdentity(KF.processNoiseCov, Scalar::all(1e-5)); //系统噪声方差矩阵Q[9.9999997e-06, 0; 0, 9.9999997e-06]
setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1)); //测量噪声方差矩阵R [0.1]
/*应该是指k-1时刻的后验状态和协方差*/
randn(KF.statePost, Scalar::all(0), Scalar::all(0.1)); //初始化后验估计的状态向量
setIdentity(KF.errorCovPost, Scalar::all(1)); //初始化后验协方差P(k) [1, 0;0, 1
for(;;)
{
Point2f center(img.cols*0.5f, img.rows*0.5f);
float R = img.cols/3.f;
/*真实点*/
double stateAngle = state.at<float>(0); //跟踪点角度
Point statePt = calcPoint(center, R, stateAngle); //真实位置
/*预测点*/
Mat prediction = KF.predict(); //计算预测值
double predictAngle = prediction.at<float>(0);
Point predictPt = calcPoint(center, R, predictAngle); //预测位置
/*测量点(观测点)*/
randn( measurement, Scalar::all(0), Scalar::all(KF.measurementNoiseCov.at<float>(0))); //给measurement赋N(0,R)的随机值
measurement += KF.measurementMatrix*state; //观测位置 = 真实位置 + 观测位置噪声 (z = H*x(k) + R)
double measAngle = measurement.at<float>(0);
Point measPt = calcPoint(center, R, measAngle); //观测位置
/*估计点*/
if(theRNG().uniform(0,4) != 0) //返回[0,4)范围内均匀分布的整数随机数,即四分之一机会返回0
KF.correct(measurement);
Mat gujizhi = KF.correct(measurement); // 使用观测值更新估计值,函数返回值是最优状态估计(statePost)
double gujiAngle = gujizhi.at<float>(0);
Point gujiPt = calcPoint(center, R, gujiAngle); //估计位置
#define drawCross( center, color, d ) \
line( img, Point( center.x - d, center.y - d ), \
Point( center.x + d, center.y + d ), color, 1, CV_AA, 0); \
line( img, Point( center.x + d, center.y - d ), \
Point( center.x - d, center.y + d ), color, 1, CV_AA, 0 )
img = Scalar::all(0);
circle(img, center, R, Scalar::all(70), 1);
/// 实时更新三个位置
drawCross( statePt, Scalar(255,255,255), 3 );
drawCross( measPt, Scalar(0,0,255), 3 );
drawCross( predictPt, Scalar(0,255,0), 3 );
drawCross( gujiPt, Scalar(255,0,0), 3 );
//line( img, statePt, measPt, Scalar(0,0,255), 3, CV_AA, 0 ); //真实和观测(红色)
//line( img, statePt, predictPt, Scalar(255,0,0), 3, CV_AA, 0 ); //真实和预测(蓝色)
//line( img, statePt, gujiPt, Scalar(255,0,0), 3, CV_AA, 0 ); //真实和估计(蓝色)
randn( processNoise, Scalar(0), Scalar::all(sqrt(KF.processNoiseCov.at<float>(0, 0))));
state = KF.transitionMatrix*state + processNoise; //加入噪声的state
imshow( "Kalman", img );
code = (char)waitKey(100);
if( code > 0 )
break;
}
if( code == 27 || code == 'q' || code == 'Q' ) //ASCII中27是ESC
break;
}
return 0;
}
原有的代码中比较的是真实值与预测值、真实值与观测值的偏差,没有用到估计值,因此我在上述代码中加入了真实值与估计值的比较。也可以把画线函数注释了,只看四个点的移动 。
真实值:
预测值:KF.predict()
观测值:真实值+观测噪声
估计值:KF.correct(measurement); // 使用观测值更新估计值
例2 跟踪鼠标位置