卡尔曼滤波是一种递归的估计,即只要获知上一时刻状态的估计值以及当前状态的观测值就可以计算出当前状态的估计值,因此不需要记录观测或者估计的历史信息。

卡尔曼滤波器分为两个阶段:预测更新。在预测阶段,滤波器使用上一状态的估计,做出对当前状态的估计。在更新阶段,滤波器利用对当前状态的观测值优化在预测阶段获得的预测值,以获得一个更精确的新估计值。

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 跟踪鼠标位置

#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdio.h>
using namespace cv;
using namespace std;
 
const int winHeight=600;
const int winWidth=800;
 
 //400,300
Point mousePosition= Point(winWidth>>1,winHeight>>1);
 
//mouse event callback
void mouseEvent(int event, int x, int y, int flags, void *param )
{
	if (event==CV_EVENT_MOUSEMOVE) {   ///CV_EVENT_MOUSEMOVE :鼠标移动
		mousePosition = Point(x,y);
	}
}
 
int main (void)
{
	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);	
 
	/*初始化状态转移矩阵A 和 测量矩阵H*/
	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(1e-5));                            //系统噪声方差矩阵Q
	setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));                        //测量噪声方差矩阵R

	/*0时刻的后验状态和协方差*/
	rng.fill(KF.statePost,RNG::UNIFORM,0,winHeight>winWidth?winWidth:winHeight);   //初始状态值x(0)
	setIdentity(KF.errorCovPost, Scalar::all(1));                                  //后验估计协方差矩阵P
	
	Mat measurement = Mat::zeros(measureNum, 1, CV_32F);           //测量向量z(k)
	
	namedWindow("kalman");
	setMouseCallback("kalman",mouseEvent);
		
	Mat image(winHeight,winWidth,CV_8UC3,Scalar(0));
 
	while (1)
	{
		//2.kalman prediction
		Mat prediction = KF.predict();
		Point predict_pt = Point(prediction.at<float>(0),prediction.at<float>(1) );   //预测值(x',y')
 
		//3.update measurement
		measurement.at<float>(0) = (float)mousePosition.x;
		measurement.at<float>(1) = (float)mousePosition.y;		
 
		//4.update
		KF.correct(measurement);
 
		//draw 
		image.setTo(Scalar(255,255,255,0));   //这里将图像的像素值设置为白色
		circle(image,predict_pt,5,Scalar(0,255,0),3);    //绿色点是预测位置
		circle(image,mousePosition,5,Scalar(255,0,0),3); //蓝色点是正确位置	
		
		/*
		首先定义了一个char类型的数组buf,大小为256。然后使用snprintf函数将字符串格式化后存储到buf中。
		第一个字符串是"predicted position:(%3d,%3d)",其中%3d表示输出一个3位的整数,predictpt.x和predictpt.y是两个整数变量。
		第二个字符串是"current position :(%3d,%3d)",其中%3d表示输出一个3位的整数,mousePosition.x和mousePosition.y是两个整数变量。
		最后,使用putText函数将buf中的字符串绘制到图像上,位置分别为(10,30)和(10,60)。绘制的字体为CVFONTHERSHEYSCRIPTCOMPLEX,
		大小为1,颜色为黑色,线宽为1,线型为8。
		*/
		char buf[256];
		snprintf(buf,256,"predicted position:(%3d,%3d)",predict_pt.x,predict_pt.y);
		/*putText函数,用于在图像上绘制文本*/
		putText(image,buf,Point(10,30),CV_FONT_HERSHEY_SCRIPT_COMPLEX,1,Scalar(0,0,0),1,8);   //它将字符串变量buf中的内容绘制在图像image上
		snprintf(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;   
		}		
	}
}

 


rng.fill(KF.statePost,RNG::UNIFORM,0,winHeight>winWidth?winWidth:winHeight);

使用了OpenCV库中的RNG类来生成随机数,并将其填充到KF.statePost数组中。其中,RNG::UNIFORM表示生成的随机数服从均匀分布,0表示随机数的最小值,最大值为窗口高度和宽度中的较小值。因此,这段代码的作用是生成一组随机数并将其填充到KF.statePost数组中。

snprintf(buf,256,"predicted position:(%3d,%3d)",predict_pt.x,predict_pt.y);

snprintf函数的作用是将格式化的数据写入字符串缓冲区。在这个例子中,它将预测的位置(predictpt.x,predictpt.y)写入了一个长度为256的字符数组buf中。最终,这个字符数组被用于在图像上绘制文本。

putText(image,buf,Point(10,30),CV_FONT_HERSHEY_SCRIPT_COMPLEX,1,Scalar(0,0,0),1,8);

putText用于在图像上绘制文本。具体来说,它将字符串变量buf中的内容绘制在图像image上,位置为(10,30),字体为CVFONTHERSHEYSCRIPTCOMPLEX,字体大小为1,颜色为黑色,线宽为1,线型为8。

例3 

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

using namespace cv;
using namespace std;
 
int main( )  
{  
	float A[10][3] = 
	{
		10,    50,     15.6,
		12,    49,     16,
		11,    52,     15.8,
		13,    52.2,   15.8,
		12.9,  50,     17,
		14,    48,     16.6,
		13.7,  49,     16.5,
		13.6,  47.8,   16.4,
		12.3,  46,     15.9,
		13.1,  45,     16.2
	};	
 
	const int stateNum=3;
	const int measureNum=3;
	KalmanFilter KF(stateNum, measureNum, 0); 
	Mat measurement = Mat::zeros(measureNum, 1, CV_32F); //观测值

	KF.transitionMatrix = (Mat_<float>(3, 3) <<1,0,0,0,1,0,0,0,1);  //转移矩阵A  
	setIdentity(KF.measurementMatrix);                                             //测量矩阵H  

	setIdentity(KF.processNoiseCov, Scalar::all(1e-5));                            //系统噪声方差矩阵Q  
	setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));                        //测量噪声方差矩阵R  

	//初始状态值和协方差
	KF.statePost = (Mat_<float>(3, 1) <<A[0][0],A[0][1],A[0][2]); 
	cout<<"state0="<<KF.statePost.t()<<endl;
	setIdentity(KF.errorCovPost, Scalar::all(1)); 

	for(int i=1;i<=9;i++)
	{
	    //预测
	    Mat prediction = KF.predict(); 			
                //计算测量值
	    measurement.at<float>(0) = (float)A[i][0];  
	    measurement.at<float>(1) = (float)A[i][1]; 
	    measurement.at<float>(2) = (float)A[i][2]; 
	    //更新
	    KF.correct(measurement);  
                //输出结果
	    cout<<"predict ="<<"\t"<<prediction.at<float>(0)<<"\t"<<prediction.at<float>(1)<<"\t"<<prediction.at<float>(2)<<endl;
	    cout<<"measurement="<<"\t"<<measurement.at<float>(0)<<"\t"<<measurement.at<float>(1)<<"\t"<<measurement.at<float>(2)<<endl;
	    cout<<"correct ="<<"\t"<<KF.statePost.at<float>(0)<<"\t"<<KF.statePost.at<float>(1)<<"\t"<<KF.statePost.at<float>(2)<<endl;
	    cout<<"*******************"<<endl;
	}
	
}

 转移矩阵A是单位矩阵,前一时刻的状态值和当前时刻的预测值相同。