特徵點檢測學習_2(surf演算法)
網上有些文章對surf做了介紹,比如:
surf演算法原理,有一些簡單介紹.
對surf的某些細節做了通俗易懂的解釋.
這篇文章名字叫做《surf原文翻譯》,寫得非常好,看完會對surf中採用的一些技術更加深入的理解,不過本文卻不是翻譯英文的,而是該作者自己的理解,對積分圖,Hessian矩陣等引入的原因都做了通俗的解釋,推薦一看。
一、Surf描述子形成步驟
1. 構造高斯金字塔尺度空間
其實surf構造的金字塔影象與sift有很大不同,就是因為這些不同才加快了其檢測的速度。Sift採用的是DOG影象,而surf採用的是Hessian矩陣行列式近似值影象。首先來看看影象中某個畫素點的Hessian矩陣,如下:
即每一個畫素點都可以求出一個Hessian矩陣。但是由於我們的特徵點需要具備尺度無關性,所以在進行Hessian矩陣構造前,需要對其進行高斯濾波。這樣,經過濾波後在進行Hessian的計算,其公式如下:
公式中的符號,估計有點數學基礎的朋友都能夠猜到,這裡就不多解釋了。
最終我們要的是原影象的一個變換影象,因為我們要在這個變換影象上尋找特徵點,然後將其位置反對映到原圖中,例如在sift中,我們是在原圖的DOG圖上尋找特徵點的。那麼在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檔案中實現,我這次試驗就是採用的這個方法。
附錄二:
作者:tornadomeet出處:http://www.cnblogs.com/tornadomeet歡迎轉載或分享,但請務必宣告文章出處。