一些網絡資料
關於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者結合,這樣效果會好很多。
附錄: