目標跟蹤學習筆記_5(opencv中kalman點跟蹤例子)


 

    一些網絡資料

  關於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者結合,這樣效果會好很多。

 

  附錄:

  實驗工程code下載

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM