该系统的真实温度为25度,图中用黑线表示。图中红线是卡尔曼滤波器输出的最优化结果(该结果在算法中设置了Q=1e-6,R=1e-1)。
一些网络资料
关于Kalman滤波器的理论,其数学公式太多,大家可以去查看一些这方面的文献.下面这篇文章对Kalman滤波做了个通俗易懂的介绍,通过文章举的例子可以宏观上理解一下该滤波器,很不错,推荐一看:http://www.cnblogs.com/feisky/archive/2009/11/09/1599247.html,
他的另一篇博客http://www.cnblogs.com/feisky/archive/2009/12/04/1617287.html
中介绍了opencv1.0版本的卡尔曼滤波的结构和函数定义等。
另外博文:http://blog.csdn.net/onezeros/article/details/6318944将opencv中自带的kalman改装成了鼠标跟踪程序,可以一看。
这篇博客http://blog.csdn.net/yang_xian521/article/details/7050398 对opencv中本身自带的Kalman例子讲解得很清楚。
kalman滤波简单介绍
Kalman滤波理论主要应用在现实世界中个,并不是理想环境。主要是来跟踪的某一个变量的值,跟踪的依据是首先根据系统的运动方程来对该值做预测,比如说我们知道一个物体的运动速度,那么下面时刻它的位置按照道理是可以预测出来的,不过该预测肯定有误差,只能作为跟踪的依据。另一个依据是可以用测量手段来测量那个变量的值,当然该测量也是有误差的,也只能作为依据,不过这2个依据的权重比例不同。最后kalman滤波就是利用这两个依据进行一些列迭代进行目标跟踪的。
在这个理论框架中,有2个公式一定要懂,即:
第一个方程为系统的运动方程,第二个方程为系统的观测方程,学过自控原理中的现代控制理论的同学应该对这2个公式很熟悉。具体的相关理论本文就不做介绍了。
Opencv目标版本中带有kalman这个类,可以使用它来完成一些跟踪目的。
下面来看看使用Kalman编程的主要步骤:
步骤一 :
Kalman这个类需要初始化下面变量:
转移矩阵,测量矩阵,控制向量(没有的话,就是0),过程噪声协方差矩阵,测量噪声协方差矩阵,后验错误协方差矩阵,前一状态校正后的值,当前观察值。
步骤二:
调用kalman这个类的predict方法得到状态的预测值矩阵,预测状态的计算公式如下:
predicted state (x'(k)): x'(k)=A*x(k-1)+B*u(k)
其中x(k-1)为前一状态的校正值,第一个循环中在初始化过程中已经给定了,后面的循环中Kalman这个类内部会计算。A,B,u(k),也都是给定了的值。这样进过计算就得到了系统状态的预测值x'(k)了。
步骤三:
调用kalman这个类的correct方法得到加入观察值校正后的状态变量值矩阵,其公式为:
corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k))
其中x'(k)为步骤二算出的结果,z(k)为当前测量值,是我们外部测量后输入的向量。H为Kalman类初始化给定的测量矩阵。K(k)为Kalman增益,其计算公式为:
Kalman gain matrix (K(k)): K(k)=P'(k)*Ht*inv(H*P'(k)*Ht+R)
计算该增益所依赖的变量要么初始化中给定,要么在kalman理论中通过其它公式可以计算。
经过步骤三后,我们又重新获得了这一时刻的校正值,后面就不断循环步骤二和步骤三即可完成Kalman滤波过程。
实验部分
本次实验来源于opencv自带sample中的例子,该例子是用kalman来完成一个一维的跟踪,即跟踪一个不断变化的角度。在界面中表现为一个点在圆周上匀速跑,然后跟踪该点。看起来跟踪点是个二维的,其实转换成角度就是一维的了。
该代码中,有这么几句
KalmanFilter KF(2, 1, 0);
...
KF.transitionMatrix = *(Mat_<float>(2, 2) << 1, 1, 0, 1);
...
一直在想Mat_<float>(2, 2) << 1是什么意思呢?
如果是用:Mat_<float> A(2, 2);则这表示的是定义一个矩阵A。
但是Mat_<float>(2, 2)感觉又不是定义,貌似也不是数,既然不是数怎能左移呢?后面发现自己想错了.
Mat_<float>(2, 2) << 1, 1, 0, 1是一个整体,即往Mat_<float>(2, 2)的矩阵中赋值1,1,0,1;说白了就是给Mat矩阵赋初值,因为初值没什么规律,所以我们不能用zeros,ones等手段来赋值。比如运行下面语句时:
Mat a;
a = (Mat_<float>(2, 2) << 1, 1, 0, 1);
cout<<a<<endl;
其结果就为
[1,1;
0,1]
实验结果如下:
跟踪中某一时刻图1:
跟踪中某一时刻图2:
其中红色的短线条为目标点真实位置和目标点的测量位置的连线,黄色的短线为目标点真实位置和预测位置的连线,所以2中颜色相交中间那个点坐标为目标的真实坐标。
实验主要部分代码和注释(附录有工程code下载链接地址):
Kalman.h:
#ifndef KALMAN_H #define KALMAN_H #include <QDialog> #include <QTimer> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/video/video.hpp> using namespace cv; namespace Ui { class kalman; } class kalman : public QDialog { Q_OBJECT public: explicit kalman(QWidget *parent = 0); ~kalman(); private slots: void on_startButton_clicked(); void kalman_process(); void on_closeButton_clicked(); private: Ui::kalman *ui; Mat img, state, processNoise, measurement; KalmanFilter KF; QTimer *timer; //给定圆心和周长,和圆周上的角度,求圆周上的坐标 static inline Point calcPoint(Point2f center, double R, double angle) { //sin前面有个负号那是因为图片的顶点坐标在左上角 return center + Point2f((float)cos(angle), (float)-sin(angle))*(float)R; } }; #endif // KALMAN_H
Kalman.cpp:
#include "kalman.h" #include "ui_kalman.h" #include <iostream> using namespace std; kalman::kalman(QWidget *parent) : QDialog(parent), ui(new Ui::kalman) { //在构造函数中定义变量不行? img.create(500, 500, CV_8UC3); cout<<img.rows<<endl; //状态维数2,测量维数1,没有控制量 KF.init(2, 1, 0); //状态值,(角度,角速度) state.create(2, 1, CV_32F); processNoise.create(2, 1, CV_32F); measurement = Mat::zeros(1, 1, CV_32F); timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(kalman_process())); ui->setupUi(this); //这句必须放在ui->setupUi(this)后面,因为只有这样才能访问ui->textBrowser ui->textBrowser->setFixedSize(500, 500); } kalman::~kalman() { delete ui; } void kalman::on_startButton_clicked() { /* 使用kalma步骤一 下面语句到for前都是kalman的初始化过程,一般在使用kalman这个类时需要初始化的值有: 转移矩阵,测量矩阵,过程噪声协方差,测量噪声协方差,后验错误协方差矩阵, 前一状态校正后的值,当前观察值 */ //产生均值为0,标准差0.1的二维高斯列向量 randn( state, Scalar::all(0), Scalar::all(0.1) ); //transitionMatrix为类KalmanFilter中的一个变量,Mat型,是Kalman模型中的状态转移矩阵 //转移矩阵为[1,1;0,1],2*2维的 KF.transitionMatrix = *(Mat_<float>(2, 2) << 1, 1, 0, 1); //函数setIdentity是给参数矩阵对角线赋相同值,默认对角线值值为1 setIdentity(KF.measurementMatrix); //系统过程噪声方差矩阵 setIdentity(KF.processNoiseCov, Scalar::all(1e-5)); //测量过程噪声方差矩阵 setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1)); //后验错误估计协方差矩阵 setIdentity(KF.errorCovPost, Scalar::all(1)); //statePost为校正状态,其本质就是前一时刻的状态 randn(KF.statePost, Scalar::all(0), Scalar::all(0.1)); //设置定时器时间,默认情况下其该定时器可无限定时,即其SingleShot为false //如果将其设置为true,则该定时器只能定时一次 //因此这里是每个33ms就去执行一次kalman处理函数 timer->start(33); // timer->setSingleShot( true ); } void kalman::kalman_process() { Point2f center(img.cols*0.5f, img.rows*0.5f); float R = img.cols/3.f; //state中存放起始角,state为初始状态 double stateAngle = state.at<float>(0); Point statePt = calcPoint(center, R, stateAngle); /* 使用kalma步骤二 调用kalman这个类的predict方法得到状态的预测值矩阵 */ //predicted state (x'(k)): x'(k)=A*x(k-1)+B*u(k) //其中x(k-1)为前面的校正状态,因此这里是用校正状态来预测的 //而校正状态corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k)) //又与本时刻的预测值和校正值有关系 Mat prediction = KF.predict(); //用kalman预测的是角度 double predictAngle = prediction.at<float>(0); Point predictPt = calcPoint(center, R, predictAngle); randn( measurement, Scalar::all(0), Scalar::all(KF.measurementNoiseCov.at<float>(0))); // generate measurement //带噪声的测量 measurement += KF.measurementMatrix*state; // measurement += KF.measurementMatrix*prediction; double measAngle = measurement.at<float>(0); Point measPt = calcPoint(center, R, measAngle); // plot points //这个define语句是画2条线段(线长很短),其实就是画一个“X”叉符号 #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); //状态坐标白色 drawCross( statePt, Scalar(255,255,255), 3 ); //测量坐标蓝色 drawCross( measPt, Scalar(0,0,255), 3 ); //预测坐标绿色 drawCross( predictPt, Scalar(0,255,0), 3 ); //真实值和测量值之间用红色线连接起来 line( img, statePt, measPt, Scalar(0,0,255), 3, CV_AA, 0 ); //真实值和估计值之间用黄色线连接起来 line( img, statePt, predictPt, Scalar(0,255,255), 3, CV_AA, 0 ); /* 使用kalma步骤三 调用kalman这个类的correct方法得到加入观察值校正后的状态变量值矩阵 */ //虽然该函数有返回值Mat,但是调用该函数校正后,其校正值已经保存在KF的statePost //中了,corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k)) KF.correct(measurement); randn( processNoise, Scalar(0), Scalar::all(sqrt(KF.processNoiseCov.at<float>(0, 0)))); //不加噪声的话就是匀速圆周运动,加了点噪声类似匀速圆周运动,因为噪声的原因,运动方向可能会改变 state = KF.transitionMatrix*state + processNoise; imwrite("../kalman/img.jpg", img); ui->textBrowser->clear(); //ui->textBrowser->setFixedSize(img.cols, img.rows); ui->textBrowser->append("<img src=../kalman/img.jpg>"); } void kalman::on_closeButton_clicked() { close(); }
在用Qt编程中碰到了下面的疑问
疑问1:
Qt界面编程中,由于构造函数中不能定义全局变量,我们需要把全局变量定义在内的内部(即在头文件中定义),那么在使用类似于opencv中这些自带初始值赋值的方法是不是就不能那样使用了呢?比如说Mat img(500, 500, CV_8UC3);因为在头文件中的定义不能同时初始化,在源文件中又不能定义。难道我们一定要将初始化和定义分开写?
查了下资料,基本上只能分开写了。其实类似opencv等开源库中的函数都考虑到了这一点。如果是用c编程,其全局变量可以直接这样赋值,Mat img(500, 500, CV_8UC3);如果是有关界面的c++编程,则Mat会提供另外一个函数来初始化的,Mat这里提供的是create函数,img.create(500, 500, CV_8UC3);其它函数类似。
疑问2:
Qt界面编程中,当我们按下一个按钮时,需要在这个按钮的槽函数里面实现一个死循环的功能,比如说让界面中一直显示一个运动的圆,由于该点在运动,所以需要代码不断的画圆,因此用了死循环。而当我们按下该界面中的另一个按钮,比如退出程序按钮时,因为前一个按钮中一直在响应,所以无法响应我们的退出按钮请求,遇到这种情况该怎么解决呢?
答案首先能肯定的是,一般情况下按钮响应槽函数中不宜使用死循环语句,否则外面函数无法响应。网上说一般解决这种问题用多线程编程技术比较好(这里我没有去试过,因为不了解多线程技术,当然了,这个以后有时间一定要去学下的)。我这里采用了另外一种方法即利用的定时器功能,触发定时器槽函数,每隔一段时间执行一下需要执行的函数。具体见代码中。
上面2个疑问是暂时的替代方案,大家有更好的可以提出来贡献下,谢谢。
总结:
kalman应用比较广,虽然学习它的理论都是数学公式,不过我们可以先学会在程序中怎么使用它,在慢慢去看理论,2者结合,这样效果会好很多。
Kalman 滤波器状态
typedef struct CvKalman { int MP; /* 测量向量维数 */ int DP; /* 状态向量维数 */ int CP; /* 控制向量维数 */ /* 向后兼容字段 */ #if 1 float* PosterState; /* =state_pre->data.fl */ float* PriorState; /* =state_post->data.fl */ float* DynamMatr; /* =transition_matrix->data.fl */ float* MeasurementMatr; /* =measurement_matrix->data.fl */ float* MNCovariance; /* =measurement_noise_cov->data.fl */ float* PNCovariance; /* =process_noise_cov->data.fl */ float* KalmGainMatr; /* =gain->data.fl */ float* PriorErrorCovariance;/* =error_cov_pre->data.fl */ float* PosterErrorCovariance;/* =error_cov_post->data.fl */ float* Temp1; /* temp1->data.fl */ float* Temp2; /* temp2->data.fl */ #endif CvMat* state_pre; /* 预测状态 (x'(k)): x(k)=A*x(k-1)+B*u(k) */ CvMat* state_post; /* 矫正状态 (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k)) */ CvMat* transition_matrix; /* 状态传递矩阵 state transition matrix (A) */ CvMat* control_matrix; /* 控制矩阵 control matrix (B) (如果没有控制,则不使用它)*/ CvMat* measurement_matrix; /* 测量矩阵 measurement matrix (H) */ CvMat* process_noise_cov; /* 过程噪声协方差矩阵 process noise covariance matrix (Q) */ CvMat* measurement_noise_cov; /* 测量噪声协方差矩阵 measurement noise covariance matrix (R) */ CvMat* error_cov_pre; /* 先验误差计协方差矩阵 priori error estimate covariance matrix (P'(k)): P'(k)=A*P(k-1)*At + Q)*/ CvMat* gain; /* Kalman 增益矩阵 gain matrix (K(k)): K(k)=P'(k)*Ht*inv(H*P'(k)*Ht+R)*/ CvMat* error_cov_post; /* 后验错误估计协方差矩阵 posteriori error estimate covariance matrix (P(k)): P(k)=(I-K(k)*H)*P'(k) */ CvMat* temp1; /* 临时矩阵 temporary matrices */ CvMat* temp2; CvMat* temp3; CvMat* temp4; CvMat* temp5; } CvKalman;
结构 CvKalman 用来保存 Kalman 滤波器状态。它由函数 cvCreateKalman 创建,由函数f cvKalmanPredict 和 cvKalmanCorrect 更新,由 cvReleaseKalman 释放. 通常该结构是为标准 Kalman 所使用的 (符号和公式都借自非常优秀的 Kalman 教程 [Welch95]):
其中:
对标准 Kalman 滤波器,所有矩阵: A, B, H, Q 和 R 都是通过 cvCreateKalman 在分配结构 CvKalman 时初始化一次。但是,同样的结构和函数,通过在当前系统状态邻域中线性化扩展 Kalman 滤波器方程,可以用来模拟扩展 Kalman 滤波器,在这种情况下, A, B, H (也许还有 Q 和 R) 在每一步中都被更新。
分配 Kalman 滤波器结构
CvKalman* cvCreateKalman( int dynam_params, int measure_params, int control_params=0 );
函数 cvCreateKalman 分配 CvKalman 以及它的所有矩阵和初始参数
释放 Kalman 滤波器结构
void cvReleaseKalman( CvKalman** kalman );
函数 cvReleaseKalman 释放结构 CvKalman 和里面所有矩阵
估计后来的模型状态
const CvMat* cvKalmanPredict( CvKalman* kalman, const CvMat* control=NULL ); #define cvKalmanUpdateByTime cvKalmanPredict
函数 cvKalmanPredict 根据当前状态估计后来的随机模型状态,并存储于 kalman->state_pre:
其中
函数返回估计得到的状态值
调节模型状态
const CvMat* cvKalmanCorrect( CvKalman* kalman, const CvMat* measurement ); #define cvKalmanUpdateByMeasurement cvKalmanCorrect
函数 cvKalmanCorrect 在给定的模型状态的测量基础上,调节随机模型状态:
其中
函数存储调节状态到 kalman->state_post 中并且输出时返回它。
下面实现了一个简单的跟踪小程序,直接给出程序源码:
void CSLAMApplicationView::OnEKFTracking() { // Initialize Kalman filter object, window, number generator, etc cvNamedWindow( "Kalman", 1 );//创建窗口,当为的时候,表示窗口大小自动设定 CvRandState rng; cvRandInit( &rng, 0, 1, -1, CV_RAND_UNI );/* CV_RAND_UNI 指定为均匀分布类型、随机数种子为-1 */ IplImage* img = cvCreateImage( cvSize(500,500), 8, 3 ); CvKalman* kalman = cvCreateKalman( 2, 1, 0 );/*状态向量为维,观测向量为维,无激励输入维*/ // State is phi, delta_phi - angle and angular velocity // Initialize with random guess CvMat* x_k = cvCreateMat( 2, 1, CV_32FC1 );/*创建行列、元素类型为CV_32FC1,元素为位单通道浮点类型矩阵。*/ cvRandSetRange( &rng, 0, 0.1, 0 );/*设置随机数范围,随机数服从正态分布,均值为,均方差为.1,通道个数为*/ rng.disttype = CV_RAND_NORMAL; cvRand( &rng, x_k ); /*随机填充数组*/ // Process noise CvMat* w_k = cvCreateMat( 2, 1, CV_32FC1 ); // Measurements, only one parameter for angle CvMat* z_k = cvCreateMat( 1, 1, CV_32FC1 );/*定义观测变量*/ cvZero( z_k ); /*矩阵置零*/ // Transition matrix F describes model parameters at and k and k+1 const float F[] = { 1, 1, 0, 1 }; /*状态转移矩阵*/ memcpy( kalman->transition_matrix->data.fl, F, sizeof(F)); /*初始化转移矩阵,行列,具体见CvKalman* kalman = cvCreateKalman( 2, 1, 0 );*/ // Initialize other Kalman parameters cvSetIdentity( kalman->measurement_matrix, cvRealScalar(1) );/*观测矩阵*/ cvSetIdentity( kalman->process_noise_cov, cvRealScalar(1e-5) );/*过程噪声*/ cvSetIdentity( kalman->measurement_noise_cov, cvRealScalar(1e-1) );/*观测噪声*/ cvSetIdentity( kalman->error_cov_post, cvRealScalar(1) );/*后验误差协方差*/ // Choose random initial state cvRand( &rng, kalman->state_post );/*初始化状态向量*/ // Make colors CvScalar yellow = CV_RGB(255,255,0);/*依次为红绿蓝三色*/ CvScalar white = CV_RGB(255,255,255); CvScalar red = CV_RGB(255,0,0); while( 1 ){ // Predict point position const CvMat* y_k = cvKalmanPredict( kalman, 0 );/*激励项输入为*/ // Generate Measurement (z_k) cvRandSetRange( &rng, 0, sqrt( kalman->measurement_noise_cov->data.fl[0] ), 0 );/*设置观测噪声*/ cvRand( &rng, z_k ); cvMatMulAdd( kalman->measurement_matrix, x_k, z_k, z_k ); // Update Kalman filter state cvKalmanCorrect( kalman, z_k ); // Apply the transition matrix F and apply "process noise" w_k cvRandSetRange( &rng, 0, sqrt( kalman->process_noise_cov->data.fl[0] ), 0 );/*设置正态分布过程噪声*/ cvRand( &rng, w_k ); cvMatMulAdd( kalman->transition_matrix, x_k, w_k, x_k ); // Plot Points cvZero( img );/*创建图像*/ // Yellow is observed state 黄色是观测值 //cvCircle(IntPtr, Point, Int32, MCvScalar, Int32, LINE_TYPE, Int32) //对应于下列其中,shift为数据精度 //cvCircle(img, center, radius, color, thickness, lineType, shift) //绘制或填充一个给定圆心和半径的圆 cvCircle( img, cvPoint( cvRound(img->width/2 + img->width/3*cos(z_k->data.fl[0])), cvRound( img->height/2 - img->width/3*sin(z_k->data.fl[0])) ), 4, yellow ); // White is the predicted state via the filter cvCircle( img, cvPoint( cvRound(img->width/2 + img->width/3*cos(y_k->data.fl[0])), cvRound( img->height/2 - img->width/3*sin(y_k->data.fl[0])) ), 4, white, 2 ); // Red is the real state cvCircle( img, cvPoint( cvRound(img->width/2 + img->width/3*cos(x_k->data.fl[0])), cvRound( img->height/2 - img->width/3*sin(x_k->data.fl[0])) ), 4, red ); CvFont font; cvInitFont(&font,CV_FONT_HERSHEY_SIMPLEX,0.5f,0.5f,0,1,8); cvPutText(img,"Yellow:observe",cvPoint(0,20),&font,cvScalar(0,0,255)); cvPutText(img,"While:predict",cvPoint(0,40),&font,cvScalar(0,0,255)); cvPutText(img,"Red:real",cvPoint(0,60),&font,cvScalar(0,0,255)); cvPutText(img,"Press Esc to Exit...",cvPoint(0,80),&font,cvScalar(255,255,255)); cvShowImage( "Kalman", img ); // Exit on esc key if(cvWaitKey(100) == 27) break; } cvReleaseImage(&img);/*释放图像*/ cvReleaseKalman(&kalman);/*释放kalman滤波对象*/ cvDestroyAllWindows();/*释放所有窗口*/ }
参考:opencv中文论坛
另外我的程序还实现了图片的打开和保存功能,具体也是参考了论坛的MFC中应用Opencv的帖子,不过我稍微改进了一下,不进行图片的缩放,显示源图像的大小:
首先是doc类定义CImage* m_Image;
CSLAMApplicationDoc::CSLAMApplicationDoc() { m_Image=NULL; } CSLAMApplicationDoc::~CSLAMApplicationDoc() { if(m_Image!=NULL) { m_Image->Destroy(); delete m_Image; } } // CSLAMApplicationDoc 命令 BOOL CSLAMApplicationDoc::OnOpenDocument(LPCTSTR lpszPathName) { if (!CDocument::OnOpenDocument(lpszPathName)) return FALSE; // TODO: Add your specialized creation code here m_Image=new CImage(); m_Image->Load(lpszPathName); return TRUE; } BOOL CSLAMApplicationDoc::OnSaveDocument(LPCTSTR lpszPathName) { // TODO: Add your specialized code here and/or call the base class m_Image->Save(lpszPathName); return CDocument::OnSaveDocument(lpszPathName); } // CSLAMApplicationView 绘制 void CSLAMApplicationView::OnDraw(CDC* pDC) { CSLAMApplicationDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // TODO: 在此处为本机数据添加绘制代码 CImage *img=pDoc->m_Image; if(img!=NULL) { CRect r; GetClientRect (&r); if(img->Width()<r.Width()) { r.right=img->Width(); } if(img->Height()<r.Height()) { r.bottom=img->Height(); } pDoc->m_Image->DrawToHDC(pDC->GetSafeHdc(),r); }