1. 程式人生 > >特徵點檢測學習(surf演算法)

特徵點檢測學習(surf演算法)

  在上篇部落格特徵點檢測學習_1(sift演算法) 中簡單介紹了經典的sift演算法,sift演算法比較穩定,檢測到的特徵點也比較多,其最大的確定是計算複雜度較高。後面有不少學者對其進行了改進,其中比較出名的就是本文要介紹的surf演算法,surf的中文意思為快速魯棒特徵。本文不是專門介紹surf所有理論(最好的理論是作者的論文)的,只是對surf演算法進行了下整理,方便以後查閱。

  網上有些文章對surf做了介紹,比如:

  surf演算法原理,有一些簡單介紹.

  對surf的某些細節做了通俗易懂的解釋.

  這篇文章名字叫做《surf原文翻譯》,寫得非常好,看完會對surf中採用的一些技術更加深入的理解,不過本文卻不是翻譯英文的,而是該作者自己的理解,對積分圖,Hessian矩陣等引入的原因都做了通俗的解釋,推薦一看。

    一、Surf描述子形成步驟

         1. 構造高斯金字塔尺度空間

         其實surf構造的金字塔影象與sift有很大不同,就是因為這些不同才加快了其檢測的速度。Sift採用的是DOG影象,而surf採用的是Hessian矩陣行列式近似值影象。首先來看看影象中某個畫素點的Hessian矩陣,如下:

          

         即每一個畫素點都可以求出一個Hessian矩陣。但是由於我們的特徵點需要具備尺度無關性,所以在進行Hessian矩陣構造前,需要對其進行高斯濾波。這樣,經過濾波後在進行Hessian的計算,其公式如下:

   

         公式中的符號,估計有點數學基礎的朋友都能夠猜到,這裡就不多解釋了。

         最終我們要的是原影象的一個變換影象,因為我們要在這個變換影象上尋找特徵點,然後將其位置反對映到原圖中,例如在sift中,我們是在原圖的HOG圖上尋找特徵點的。那麼在surf中,這個變換圖是什麼呢?從surf的眾多資料來看,就是原圖每個畫素的Hessian矩陣行列式的近似值構成的。其行列式近似公式如下:

          

         其中0.9是作者給出的一個經驗值,其實它是有一套理論計算的,具體去看surf的英文論文。

         由於求Hessian時要先高斯平滑,然後求二階導數,這在離散的畫素點是用模板卷積形成的,這2中操作合在一起用一個模板代替就可以了,比如說y方向上的模板如下:

            

         該圖的左邊即用高斯平滑然後在y方向上求二階導數的模板,為了加快運算用了近似處理,其處理結果如右圖所示,這樣就簡化了很多。並且右圖可以採用積分圖來運算,大大的加快了速度,關於積分圖的介紹,可以去查閱相關的資料。

         同理,x和y方向的二階混合偏導模板如下所示:

    

  上面講的這麼多隻是得到了一張近似hessian行列式圖,這例比sift中的DOG圖,但是在金字塔影象中分為很多層,每一層叫做一個octave,每一個octave中又有幾張尺度不同的圖片。在sift演算法中,同一個octave層中的圖片尺寸(即大小)相同,但是尺度(即模糊程度)不同,而不同的octave層中的圖片尺寸大小也不相同,因為它是由上一層圖片降取樣得到的。在進行高斯模糊時,sift的高斯模板大小是始終不變的,只是在不同的octave之間改變圖片的大小。而在surf中,圖片的大小是一直不變的,不同的octave層得到的待檢測圖片是改變高斯模糊尺寸大小得到的,當然了,同一個octave中個的圖片用到的高斯模板尺度也不同。Surf採用這種方法節省了降取樣過程,其處理速度自然也就提上去了。其金字塔影象如下所示:

    

  2. 利用非極大值抑制初步確定特徵點

  此步驟和sift類似,將經過hessian矩陣處理過的每個畫素點與其3維領域的26個點進行大小比較,如果它是這26個點中的最大值或者最小值,則保留下來,當做初步的特徵點。

  3. 精確定位極值點

  這裡也和sift演算法中的類似,採用3維線性插值法得到亞畫素級的特徵點,同時也去掉那些值小於一定閾值的點。

  4. 選取特徵點的主方向。

  這一步與sift也大有不同。Sift選取特徵點主方向是採用在特徵點領域內統計其梯度直方圖,取直方圖bin值最大的以及超過最大bin值80%的那些方向做為特徵點的主方向。而在surf中,不統計其梯度直方圖,而是統計特徵點領域內的harr小波特徵。即在特徵點的領域(比如說,半徑為6s的圓內,s為該點所在的尺度)內,統計60度扇形內所有點的水平haar小波特徵和垂直haar小波特徵總和,haar小波的尺寸變長為4s,這樣一個扇形得到了一個值。然後60度扇形以一定間隔進行旋轉,最後將最大值那個扇形的方向作為該特徵點的主方向。該過程的示意圖如下:

    

  5. 構造surf特徵點描述運算元

  在sift中,是在特徵點周圍取16*16的鄰域,並把該領域化為4*4個的小區域,每個小區域統計8個方向梯度,最後得到4*4*8=128維的向量,該向量作為該點的sift描述子。

  在surf中,也是在特徵點周圍取一個正方形框,框的邊長為20s(s是所檢測到該特徵點所在的尺度)。該框帶方向,方向當然就是第4步檢測出來的主方向了。然後把該框分為16個子區域,每個子區域統計25個畫素的水平方向和垂直方向的haar小波特徵,這裡的水平和垂直方向都是相對主方向而言的。該haar小波特徵為水平方向值之和,水平方向絕對值之和,垂直方向之和,垂直方向絕對值之和。該過程的示意圖如下所示:

    

  這樣每個小區域就有4個值,所以每個特徵點就是16*4=64維的向量,相比sift而言,少了一半,這在特徵匹配過程中會大大加快匹配速度。

  二、特徵點的匹配過程

  surf特徵點的匹配過程和sift類似,這裡不做詳細介紹

  三、實驗部分

  該程式碼的作者給出的主函式實現了6中功能,包括靜態圖片特徵點的檢測,視訊中特徵點的檢測,圖片之間的匹配,視訊與圖片之間的匹配,特徵點聚類等6中功能。本次實驗就簡單的測試了圖片的檢測,匹配和特徵點聚類3個功能。並加入了簡單的介面。

  開發環境為:opencv2.4.2+Qt4.8.2+open surf+windosxp

  實驗分為下面3個部分來描述。

  Surf特徵點檢測和描述

  開啟軟體,單擊Open Image按鈕,選擇一張待檢測的圖片,效果如下:

  

  單擊Surf Detect按鈕,程式會對該圖片進行特徵點檢測,並顯示特徵結果,包括特徵點的主方向,尺度等資訊。效果如下:

  

  單擊Close 按鈕退出程式。

  Surf特徵點匹配

  開啟軟體,單擊Open Image 按鈕,依次開啟2幅待匹配的圖片,這2幅圖片要有相同的內容,只是尺度,旋轉,光照等不同。開啟圖片後如下:

  

  單擊Surf Detect按鈕,和上面類似,會先對圖片進行檢測,效果如下:

  

  單擊Surf Match 按鈕,程式會對檢測到的圖片進行特徵點匹配,效果如下:

  

  單擊Close 按鈕退出程式。

  Surf特徵點聚類

  開啟軟體,單擊Open Image 按鈕,選擇一張待特徵點分類的圖片,如下所示:

  

  單擊Surf Detect按鈕,首先對該圖片進行surf特徵點檢測,如下:

  

  單擊Kmeans Cluster按鈕,程式會對這些特徵點集合進行聚類,並顯示其結果,如下所示:

  

   單擊Close 按鈕退出程式。

  實驗主要函式部分及程式碼(附錄有工程code下載連結):

opensurf.h:

複製程式碼
#ifndef OPENSURF_H
#define OPENSURF_H

#include <QDialog>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include "ipoint.h"
#include "kmeans.h"

using namespace cv;

namespace Ui {
class OpenSurf;
}

class OpenSurf : public QDialog
{
    Q_OBJECT
    
public:
    explicit OpenSurf(QWidget *parent = 0);
    ~OpenSurf();
    
private slots:
    void on_openButton_clicked();

    void on_detectButton_clicked();

    void on_matchButton_clicked();

    void on_closeButton_clicked();

    void on_clusterButton_clicked();

private:
    Ui::OpenSurf *ui;
    IplImage *img1, *img2, *img_match1, *img_match2;
    IpVec ipts, ipts1, ipts2;
    IpPairVec matches;
    Kmeans km;
    int open_image_num;

};

#endif // OPENSURF_H
複製程式碼

opensurf.cpp:

複製程式碼
#include "opensurf.h"
#include "ui_opensurf.h"
#include <QtGui>
#include <QtCore>
#include "surflib.h"

using namespace std;

OpenSurf::OpenSurf(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::OpenSurf)
{
    open_image_num = 0;
    ui->setupUi(this);
}

OpenSurf::~OpenSurf()
{
    delete ui;
}

void OpenSurf::on_openButton_clicked()
{
    QString img_name = QFileDialog::getOpenFileName(this, "Open Image", "../open_surf",
                                                           tr("Image Files(*.png *.jpeg *.jpg *.bmp)"));
       if(0 == open_image_num)
           ui->textBrowser->clear();
       open_image_num ++;
       if( 1 == open_image_num )
           {
               img1 = cvLoadImage(img_name.toAscii().data());
               img_match1 = cvLoadImage(img_name.toAscii().data());
               cvSaveImage("../open_surf/load_img1.jpg", img1);
               ui->textBrowser->setFixedSize(img1->width, img1->height);
               ui->textBrowser->insertHtml("<img src=../open_surf/load_img1.jpg>");
           }
       else if(2 == open_image_num)
           {
               img2 = cvLoadImage(img_name.toAscii().data());
               img_match2 = cvLoadImage(img_name.toAscii().data());
               cvSaveImage("../open_surf/load_img2.jpg", img2);
               ui->textBrowser->setFixedSize(img1->width+img2->width, std::max(img1->height, img2->height));
               //取消自動換行模式,讓2幅圖片水平顯示
               ui->textBrowser->setWordWrapMode (QTextOption::NoWrap);
               ui->textBrowser->insertHtml("<img src=../open_surf/load_img2.jpg>");
           }
       else if(3 == open_image_num)
           {
               open_image_num = 0;
               ui->textBrowser->clear();
           }
}

void OpenSurf::on_detectButton_clicked()
{
    if( 1 == open_image_num )
        {
            //用surf對特徵點進行檢測
            surfDetDes(img1, ipts, false, 5, 4, 2, 0.0004f);
            //在影象中將特徵點畫出來
            drawIpoints(img1, ipts);
            cvSaveImage("../open_surf/detect_img1.jpg", img1);
            ui->textBrowser->clear();
            ui->textBrowser->setFixedSize(img1->width, img1->height);
            ui->textBrowser->insertHtml("<img src=../open_surf/detect_img1.jpg>");
        }
    else if (2 == open_image_num)
        {
            //用surf對特徵點進行檢測
            surfDetDes(img1, ipts1, false, 5, 4, 2, 0.0004f);
            //在影象中將特徵點畫出來
            drawIpoints(img1, ipts1);
            cvSaveImage("../open_surf/detect_img1.jpg", img1);
            //用surf對特徵點進行檢測
            surfDetDes(img2, ipts2, false, 5, 4, 2, 0.0004f);
            //在影象中將特徵點畫出來
            drawIpoints(img2, ipts2);
            cvSaveImage("../open_surf/detect_img2.jpg", img2);
            ui->textBrowser->clear();
            ui->textBrowser->insertHtml("<img src=../open_surf/detect_img1.jpg>");

            ui->textBrowser->setFixedSize(img1->width+img2->width, std::max(img1->height, img2->height));
            //取消自動換行模式,讓2幅圖片水平顯示
            ui->textBrowser->setWordWrapMode (QTextOption::NoWrap);
            ui->textBrowser->insertHtml("<img src=../open_surf/detect_img2.jpg>");
        }
}

void OpenSurf::on_matchButton_clicked()
{
    if(2 == open_image_num)
        {
            getMatches(ipts1,ipts2,matches);
            for (unsigned int i = 0; i < matches.size(); ++i)
              {
                drawPoint(img_match1,matches[i].first);
                drawPoint(img_match2,matches[i].second);

                const int & w = img1->width;
                const int & h1 = img1->height;
                const int & h2 = img2->height;
                //這裡因為我事先已經知道了圖片的相對開啟後顯示的位置,所以在畫匹配的直線時加了點常識
                //因此該方法不通用,只是適合本例中給的圖片,最好的方法就像Rob Hess的sift演算法那樣
                //把2張圖片合成一張,然後在一張圖片上畫匹配直線
                cvLine(img_match1,cvPoint(matches[i].first.x,matches[i].first.y),
                       cvPoint(matches[i].second.x+w,matches[i].second.y+std::abs(h1-h2)),
                       cvScalar(255,255,255),1);
                cvLine(img_match2,cvPoint(matches[i].first.x-w,matches[i].first.y-std::abs(h1-h2)),
                       cvPoint(matches[i].second.x,matches[i].second.y),
                       cvScalar(255,255,255),1);
              }
            cvSaveImage("../open_surf/match_img1.jpg", img_match1);
            cvSaveImage("../open_surf/match_img2.jpg", img_match2);
            ui->textBrowser->clear();
            ui->textBrowser->insertHtml("<img src=../open_surf/match_img1.jpg>");

            ui->textBrowser->setFixedSize(img1->width+img2->width, std::max(img1->height, img2->height));
            //取消自動換行模式,讓2幅圖片水平顯示
            ui->textBrowser->setWordWrapMode (QTextOption::NoWrap);
            ui->textBrowser->insertHtml("<img src=../open_surf/match_img2.jpg>");
        }
}


void OpenSurf::on_clusterButton_clicked()
{
    for (int repeat = 0; repeat < 10; ++repeat)
      {

        km.Run(&ipts, 5, true);
        drawPoints(img1, km.clusters);

        for (unsigned int i = 0; i < ipts.size(); ++i)
        {
          cvLine(img1, cvPoint(ipts[i].x,ipts[i].y), cvPoint(km.clusters[ipts[i].clusterIndex].x ,km.clusters[ipts[i].clusterIndex].y),cvScalar(255,255,255));
        }
        cvSaveImage("../open_surf/kmeans_img1.jpg", img1);
        ui->textBrowser->clear();
        ui->textBrowser->setFixedSize(img1->width, img1->height);
        ui->textBrowser->insertHtml("<img src=../open_surf/kmeans_img1.jpg>");
      }
}


void OpenSurf::on_closeButton_clicked()
{
    close();
}
複製程式碼

  總結:

     Surf在速度上比sift要快許多,這主要得益於它的積分圖技術,已經Hessian矩陣的利用減少了降取樣過程,另外它得到的特徵向量維數也比較少,有利於更快的進行特徵點匹配。

 附錄一:

  1、和RobHesson執行時一樣,這裡的open surf執行時出現如下錯誤:

  ipoint.obj:-1: error: LNK2019: 無法解析的外部符號 _cvFindHomography,該符號在函式 "int __cdecl translateCorners(class std::vector<struct std::pair<class Ipoint,class Ipoint>,class std::allocator<struct std::pair<class Ipoint,class Ipoint> > > &,struct CvPoint const * const,struct CvPoint * const)" ([email protected]@[email protected][email protected]@@[email protected]@[email protected]@[email protected][email protected]@@[email protected]@[email protected]@@[email protected]@[email protected]@[email protected]@[email protected]@Z) 中被引用

  不過這次的原因是沒有opencv_calib3d242d.lib庫,因為本open surf在進行特徵匹配時用到了opencv中的3維重建有關的函式cvFindHomography(該函式是求2個影象間的單應矩陣),所以很多人都會忘了新增這個庫檔案,就會導致這個錯誤。

  2、如果用了Qt或MFC等介面設計程式碼時,編譯該程式會報如下錯誤:

  moc_open_surf.obj:-1: error: LNK2005: "public: void __thiscall Kmeans::SetIpoints(class std::vector<class Ipoint,class std::allocator<class Ipoint> > *)" ([email protected]@@[email protected]@@[email protected]@@@[email protected]@@[email protected]@@Z) 已經在 main.obj 中定義

  其實是Open Surf的作者可能沒有考慮周全,它在kmeans.h檔案中把Kmeans這個類的成員函式方法在標頭檔案中實現了,其實這在標準c++中是不支援的。解決方法就是把kmeans.h改造成kemans.hpp(該方法我沒有去試過);另外一種方法就是新建一個kmeans.cpp檔案,把成員函式的實現過程放在cpp檔案中實現,我這次試驗就是採用的這個方法。

  附錄二:

原文出處:http://www.cnblogs.com/tornadomeet/archive/2012/08/17/2644903.html