( What is the Kalman Filter? )
在学习卡尔曼滤波器之前,首先看看为什么叫 “ 卡尔曼 ” 。跟其他著名的理论(例如傅立叶变换,泰勒级数等等)一样,卡尔曼也是一个人的名字,而跟他们不同的是,他是个现代人!
卡尔曼全名 Rudolf Emil Kalman ,匈牙利数学家, 1930 年出生于匈牙利首都布达佩斯。 1953 , 1954 年于麻省理工学院分别获得电机工程学士及硕士学位。 1957 年于哥伦比亚大学获得博士学位。我们现在要学习的卡尔曼滤波器,正是源于他的博士论文和 1960 年发表的论文《 A New Approach to Linear Filtering and Prediction Problems 》(线性滤波与预测问题的新方法)。如果对这编论文有兴趣,可以到这里的地址下载: http://www.cs.unc.edu/~welch/media/pdf/Kalman1960.pdf。
简单来说,卡尔曼滤波器是一个 “optimal recursive data processing algorithm (最优化自回归数据处理算法) ” 。对于解决很大部分的问题,他是最优,效率最高甚至是最有用的。他的广泛应用已经超过 30 年,包括机器人导航,控制,传感器数据融合甚至在军事方面的雷达系统以及导弹追踪等等。近年来更被应用于计算机图像处理,例如头脸识别,图像分割,图像边缘检测等等。
2 .卡尔曼滤波器的介绍
( Introduction to the Kalman Filter )
为了可以更加容易的理解卡尔曼滤波器,这里会应用形象的描述方法来讲解,而不是像大多数参考书那样罗列一大堆的数学公式和数学符号。但是,他的 5 条公式是其核心内容。结合现代的计算机,其实卡尔曼的程序相当的简单,只要你理解了他的那 5 条公式。
在介绍他的 5 条公式之前,先让我们来根据下面的例子一步一步的探索。
假设我们要研究的对象是一个房间的温度。根据你的经验判断,这个房间的温度是恒定的,也就是下一分钟的温度等于现在这一分钟的温度(假设我们用一分钟来做时间单位)。假设你对你的经验不是 100% 的相信,可能会有上下偏差几度。我们把这些偏差看成是高斯白噪声( White Gaussian Noise ),也就是这些偏差跟前后时间是没有关系的而且符合高斯分配( Gaussian Distribution )。另外,我们在房间里放一个温度计,但是这个温度计也不准确的,测量值会比实际值偏差。我们也把这些偏差看成是高斯白噪声。
好了,现在对于某一分钟我们有两个有关于该房间的温度值:你根据经验的预测值(系统的预测值)和温度计的值(测量值)。下面我们要用这两个值结合他们各自的噪声来估算出房间的实际温度值。
假如我们要估算 k 时刻的是实际温度值。首先你要根据 k-1 时刻的温度值,来预测 k 时刻的温度。因为你相信温度是恒定的,所以你会得到 k 时刻的温度预测值是跟 k-1 时刻一样的,假设是 23 度,同时该值的高斯噪声的偏差是 5 度( 5 是这样得到的:如果 k-1 时刻估算出的最优温度值的偏差是 3 ,你对自己预测的不确定度是 4 度,他们平方相加再开方,就是 5 )。然后,你从温度计那里得到了 k 时刻的温度值,假设是 25 度,同时该值的偏差是 4 度。
由于我们用于估算 k 时刻的实际温度有两个温度值,分别是 23 度和 25 度。究竟实际温度是多少呢?相信自己还是相信温度计呢?究竟相信谁多一点,我们可以用他们的 covariance 来判断。因为 Kg^2=5^2/(5^2+4^2) ,所以 Kg=0.78 ,我们可以估算出 k 时刻的实际温度值是: 23+0.78*(25-23)=24.56 度。可以看出,因为温度计的 covariance 比较小(比较相信温度计),所以估算出的最优温度值偏向温度计的值。
现在我们已经得到 k 时刻的最优温度值了,下一步就是要进入 k+1 时刻,进行新的最优估算。到现在为止,好像还没看到什么自回归的东西出现。对了,在进入 k+1 时刻之前,我们还要算出 k 时刻那个最优值( 24.56 度)的偏差。算法如下: ((1-Kg)*5^2)^0.5=2.35 。这里的 5 就是上面的 k 时刻你预测的那个 23 度温度值的偏差,得出的 2.35 就是进入 k+1 时刻以后 k 时刻估算出的最优温度值的偏差(对应于上面的 3 )。
就是这样,卡尔曼滤波器就不断的把 covariance 递归,从而估算出最优的温度值。他运行的很快,而且它只保留了上一时刻的 covariance 。上面的 Kg ,就是卡尔曼增益( Kalman Gain )。他可以随不同的时刻而改变他自己的值,是不是很神奇!
下面就要言归正传,讨论真正工程系统上的卡尔曼。
3 . 卡尔曼滤波器算法
( The Kalman Filter Algorithm )
在这一部分,我们就来描述源于 Dr Kalman 的卡尔曼滤波器。下面的描述,会涉及一些基本的概念知识,包括概率( Probability ),随即变量( Random Variable ),高斯或正态分配( Gaussian Distribution )还有 State-space Model 等等。但对于卡尔曼滤波器的详细证明,这里不能一一描述。
首先,我们先要引入一个离散控制过程的系统。该系统可用一个线性随机微分方程( Linear Stochastic Difference equation )来描述:
X(k)=A X(k-1)+B U(k)+W(k)
再加上系统的测量值:
Z(k)=H X(k)+V(k)
上两式子中, X(k) 是 k 时刻的系统状态, U(k) 是 k 时刻对系统的控制量。 A 和 B 是系统参数,对于多模型系统,他们为矩阵。 Z(k) 是 k 时刻的测量值, H 是测量系统的参数,对于多测量系统, H 为矩阵。 W(k) 和 V(k) 分别表示过程和测量的噪声。他们被假设成高斯白噪声 (White Gaussian Noise) ,他们的 covariance 分别是 Q , R (这里我们假设他们不随系统状态变化而变化)。
对于满足上面的条件 ( 线性随机微分系统,过程和测量都是高斯白噪声 ) ,卡尔曼滤波器是最优的信息处理器。下面我们来用他们结合他们的 covariances 来估算系统的最优化输出(类似上一节那个温度的例子)。
首先我们要利用系统的过程模型,来预测下一状态的系统。假设现在的系统状态是 k ,根据系统的模型,可以基于系统的上一状态而预测出现在状态:
X(k|k-1)=A X(k-1|k-1)+B U(k) ……….. (1)
式 (1) 中, X(k|k-1) 是利用上一状态预测的结果, X(k-1|k-1) 是上一状态最优的结果, U(k) 为现在状态的控制量,如果没有控制量,它可以为 0 。
到现在为止,我们的系统结果已经更新了,可是,对应于 X(k|k-1) 的 covariance 还没更新。我们用 P 表示 covariance :
P(k|k-1)=A P(k-1|k-1) A’+Q ……… (2)
式 (2) 中, P(k|k-1) 是 X(k|k-1) 对应的 covariance , P(k-1|k-1) 是 X(k-1|k-1) 对应的 covariance , A’ 表示 A 的转置矩阵, Q 是系统过程的 covariance 。式子 1 , 2 就是卡尔曼滤波器 5 个公式当中的前两个,也就是对系统的预测。
现在我们有了现在状态的预测结果,然后我们再收集现在状态的测量值。结合预测值和测量值,我们可以得到现在状态 (k) 的最优化估算值 X(k|k) :
X(k|k)= X(k|k-1)+Kg(k) (Z(k)-H X(k|k-1)) ……… (3)
其中 Kg 为卡尔曼增益 (Kalman Gain) :
Kg(k)= P(k|k-1) H’ / (H P(k|k-1) H’ + R) ……… (4)
到现在为止,我们已经得到了 k 状态下最优的估算值 X(k|k) 。但是为了要另卡尔曼滤波器不断的运行下去直到系统过程结束,我们还要更新 k 状态下 X(k|k) 的 covariance :
P(k|k)= ( I-Kg(k) H ) P(k|k-1) ……… (5)
其中 I 为 1 的矩阵,对于单模型单测量, I=1 。当系统进入 k+1 状态时, P(k|k) 就是式子 (2) 的 P(k-1|k-1) 。这样,算法就可以自回归的运算下去。
卡尔曼滤波器的原理基本描述了,式子 1 , 2 , 3 , 4 和 5 就是他的 5 个基本公式。根据这 5 个公式,可以很容易的实现计算机的程序。
下面,我会用程序举一个实际运行的例子。。。
4 . 简单例子
( A Simple Example )
这里我们结合第二第三节,举一个非常简单的例子来说明卡尔曼滤波器的工作过程。所举的例子是进一步描述第二节的例子,而且还会配以程序模拟结果。
根据第二节的描述,把房间看成一个系统,然后对这个系统建模。当然,我们见的模型不需要非常地精确。我们所知道的这个房间的温度是跟前一时刻的温度相同的,所以 A=1 。没有控制量,所以 U(k)=0 。因此得出:
X(k|k-1)=X(k-1|k-1) ……….. (6)
式子( 2 )可以改成:
P(k|k-1)=P(k-1|k-1) +Q ……… (7)
因为测量的值是温度计的,跟温度直接对应,所以 H=1 。式子 3 , 4 , 5 可以改成以下:
X(k|k)= X(k|k-1)+Kg(k) (Z(k)-X(k|k-1)) ……… (8)
Kg(k)= P(k|k-1) / (P(k|k-1) + R) ……… (9)
P(k|k)= ( 1-Kg(k) ) P(k|k-1) ……… (10)
CvKalman
Kalman 滤波器状态
typedef struct CvKalman { int MP; int DP; int CP; #if 1 float* PosterState; float* PriorState; float* DynamMatr; float* MeasurementMatr; float* MNCovariance; float* PNCovariance; float* KalmGainMatr; float* PriorErrorCovariance; float* PosterErrorCovariance; float* Temp1; float* Temp2; #endif CvMat* state_pre; CvMat* state_post; CvMat* transition_matrix; CvMat* control_matrix; CvMat* measurement_matrix; CvMat* process_noise_cov; CvMat* measurement_noise_cov; CvMat* error_cov_pre; CvMat* gain; CvMat* error_cov_post; CvMat* temp1; CvMat* temp2; CvMat* temp3; CvMat* temp4; CvMat* temp5; } CvKalman;
结 构 CvKalman 用来保存 Kalman 滤波器状态。它由函数 cvCreateKalman 创建,由函数f cvKalmanPredict 和 cvKalmanCorrect 更新,由 cvReleaseKalman 释放. 通常该结构是为标准 Kalman 所使用的 (符号和公式都借自非常优秀的 Kalman 教程 [Welch95]):
-
系统运动方程:
-
系统观测方程:
其中:
- xk(xk ? 1) - 系统在时刻 k (k-1) 的状态向量 (state of the system at the moment k (k-1))
- zk - 在时刻 k 的系统状态测量向量 (measurement of the system state at the moment k)
- uk - 应用于时刻 k 的外部控制 (external control applied at the moment k)
-
wk 和
vk 分别为正态分布的运动和测量噪声
- p(w) ~ N(0,Q)
- p(v) ~ N(0,R),
- 即,
- Q - 运动噪声的相关矩阵,常量或变量
- R - 测量噪声的相关矩阵,常量或变量
对 标准 Kalman 滤波器,所有矩阵: A, B, H, Q 和 R 都是通过 cvCreateKalman 在分配结构 CvKalman 时初始化一次。但是,同样的结构和函数,通过在当前系统状态邻域中线性化扩展 Kalman 滤波器方程,可以用来模拟扩展 Kalman 滤波器,在这种情况下, A, B, H (也许还有 Q 和 R) 在每一步中都被更新。
CreateKalman
分配 Kalman 滤波器结构
CvKalman* cvCreateKalman( int dynam_params, int measure_params, int control_params=0 );
-
dynam_params
- 状态向量维数 measure_params
- 测量向量维数 control_params
- 控制向量维数
函数 cvCreateKalman 分配 CvKalman 以及它的所有矩阵和初始参数
ReleaseKalman
释放 Kalman 滤波器结构
void cvReleaseKalman( CvKalman** kalman );
-
kalman
- 指向 Kalman 滤波器结构的双指针
函数 cvReleaseKalman 释放结构 CvKalman 和里面所有矩阵
KalmanPredict
估计后来的模型状态
const CvMat* cvKalmanPredict( CvKalman* kalman, const CvMat* control=NULL ); #define cvKalmanUpdateByTime cvKalmanPredict
-
kalman
- Kalman 滤波器状态 control
- 控制向量 (uk), 如果没有外部控制 (control_params=0) 应该为 NULL
函数 cvKalmanPredict 根据当前状态估计后来的随机模型状态,并存储于 kalman->state_pre:
-
-
,
其中
- x'k 是预测状态 (kalman->state_pre),
- xk ? 1 是前一步的矫正状态 (kalman->state_post),应该在开始的某个地方初始化,即缺省为零向量,
- uk 是外部控制 (control 参数),
- P'k 是先验误差相关矩阵 (kalman->error_cov_pre)
- Pk ? 1 是前一步的后验误差相关矩阵 (kalman->error_cov_post),应该在开始的某个地方初始化,即缺省为单位矩阵.
函数返回估计得到的状态值
KalmanCorrect
调节模型状态
const CvMat* cvKalmanCorrect( CvKalman* kalman, const CvMat* measurement ); #define cvKalmanUpdateByMeasurement cvKalmanCorrect
-
kalman
- 被更新的 Kalman 结构的指针 measurement
- 指向测量向量的指针,向量形式为 CvMat
函数 cvKalmanCorrect 在给定的模型状态的测量基础上,调节随机模型状态:
其中
- zk - 给定测量(mesurement parameter)
- Kk - Kalman "增益" 矩阵
函数存储调节状态到 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 ); 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 ); cvRandSetRange( &rng, 0, 0.1, 0 ); 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)); // 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); 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); } 转自:http://lql990832.blog.163.com/blog/static/75304655200911255236464/?fromdm&fromSearch&isFromSearchEngine=yes http://ghyan1208.blog.163.com/blog/static/162866676201081425645732/