zoukankan      html  css  js  c++  java
  • 目标跟踪学习笔记_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下载

  • 相关阅读:
    867. Transpose Matrix
    896. Monotonic Array
    Java并发包中线程池ThreadPoolExecutor原理探究
    Java中的线程协作之Condition
    Java中的读写锁
    Java中的锁——Lock和synchronized
    Java中的队列同步器AQS
    Java并发编程基础之volatile
    leetcode-数组中只出现一次的数字
    leetcode-比特位计数
  • 原文地址:https://www.cnblogs.com/tornadomeet/p/2646412.html
Copyright © 2011-2022 走看看