sift算法在cv領域的重要性不言而喻,該作者的文章引用率在cv界是number1.本篇博客只是本人把sift算法知識點整理了下,以免忘記。本文比較早的一篇博文opencv源碼解析之(3):特征點檢查前言1 中有使用opencv自帶的sift做了個簡單的實驗,而這次主要是利用Rob Hess的sift源碼來做實驗,其實現在的opencv版本中帶的sift算法也是Rob Hess的,只是稍微包裝了下。
首先網上有不少文章介紹了sift算法,寫得都不錯,比如:
http://www.cnblogs.com/cfantaisie/archive/2011/06/14/2080917.html
該博客對sift算法理論做了介紹,且有示意圖輔助理解,從該文中可以了解sift算法的大概流程.
http://www.cnblogs.com/linyunzju/archive/2011/06/14/2080950.html
這篇文章對sift算法做了通俗易懂的解釋.
http://blog.csdn.net/v_july_v/article/category/795430
這篇博客有教你怎樣用c語言一步一步寫sift算法。
http://underthehood.blog.51cto.com/2531780/658350
該文也對sift做了詳細的介紹,博客的作者還對sift匹配做了講解。
下面還是簡單看下sift算法的理論,具體的內容可以參考上面的幾篇文章。
一、Sift描述子形成的步驟
1、 構造高斯差分空間圖像。
Sift特征點的檢測時在DOG圖像上進行的,DOG圖像是將相鄰尺度空間圖像相減得到的。且金字塔的每一層都要構造一個DOG空間圖像。默認參數是金字塔4層,即4個octave,每一個octave中有5張不同尺度的圖片,不同octave的圖片尺寸大小不同,所以每一層中就會得到4幅DOG圖像。
高斯金字塔的第1層第1副原圖像是將原圖像放大2倍且sigma(sigma=1.6)模糊,第2幅圖像是k*sigma(k等於根號2)模糊,第3幅是k*k*sigma模糊,后面類推…
高斯金字塔第2層第1幅圖是選擇金字塔上一層(這里是第1層)中尺度空間參數為k*k*sigma的那幅圖(實際上是2倍的尺度空間)進行降采樣(尺寸大小為原來的1/4倍)得到,如果k不等於根號2,那么取原圖的2*sigma降采樣得到。第2層第2幅圖是在本層第一幅圖尺度模糊系數增加k倍模糊后的圖像,后面類似…
示意圖如下所示:
尺度不變當然是與圖片尺寸有關,即圖片的尺寸大小變化,但是其檢測結果不變。
2、尋找極大極小值點。
將每個像素點與其所在的那幅圖像鄰域的8個像素,它所在的向量尺度空間上下2幅圖對應位置鄰域各9個點,總共26個點進行像素值比較,如果該點是最大或者最小點,則改點就暫時列為特征點。
其鄰圖如下:
3、精確定位極值點
子像素級極值點:
由於上面找到的近似極值點落在像素點的位置上,實際上我們在像素點附近如果用空間曲面去擬合的話,很多情況下極值點都不是恰好在像素點上,而是在附近。所以sift算法提出的作者用泰勒展開找到了亞像素級的特征點。這種點更穩定,更具有代表性。
消除對比度低的特征點:
對求出亮度比較低的那些點直接過濾點,程序中的閾值為0.03.
消除邊界上的點:
處理方法類似harrs角點,把平坦區域和直線邊界上的點去掉,即對於是邊界上的點但又不是直角上的點,sift算法是不把這些點作為特征點的。
4、選取特征點主方向
在特征點附近選取一個區域,該區域大小與圖圖像的尺度有關,尺度越大,區域越大。並對該區域統計36個bin的方向直方圖,將直方圖中最大bin的那個方向作為該點的主方向,另外大於最大bin80%的方向也可以同時作為主方向。這樣的話,由於1個特征點有可能有多個主方向,所以一個特征點有可能有多個128維的描述子。如下圖所示:
5、 構造特征點描述算子。
以特征點為中心,取領域內16*16大小的區域,並把這個區域分成4*4個大小為4*4的小區域,每個小區域內計算加權梯度直方圖,該權值分為2部分,其一是該點的梯度大小,其二是改點離特征點的距離(二維高斯的關系),每個小區域直方圖分為8個bin,所以一個特征點的維數=4*4*8=128維。示意圖如下(該圖取的領域為8*8個點,因此描述子向量的維數為32維):
6、實驗部分
下面來做下試驗,試驗sift代碼采用Rob Hess的代碼,opencv目前版本中的sift源碼也是采用Rob Hess的。代碼可以在他的主頁上下載:http://blogs.oregonstate.edu/hess/code/sift/
這里我下載的是windows版本的,並采用Qt做了個簡單的界面。
環境:WindowsXP+Opencv2.4.2+Qt4.8.2+QtCreator2.5.1,QtCreator內部采用的是vc的編譯器。
運行軟件,單擊Open Image后選擇一張需要進行特征點檢測的圖片,我這里顯示的結果如下:
單擊Sift Detect按鈕后,檢測到的效果如下:
主要代碼部分如下(附錄有工程code下載鏈接):
SiftDetect.h:
#ifndef SIFTDETECT_H #define SIFTDETECT_H #include <QDialog> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> //#include <opencv/cxcore.h> //#include <opencv/highgui.h> //#include <opencv/imgproc.h> #include <stdio.h> #include <stdio.h> #include "sift.h" #include "imgfeatures.h" #include "utils.h" using namespace cv; namespace Ui { class SiftDetect; } class SiftDetect : public QDialog { Q_OBJECT public: explicit SiftDetect(QWidget *parent = 0); ~SiftDetect(); private slots: void on_openButton_clicked(); void on_detectButton_clicked(); void on_closeButton_clicked(); private: Ui::SiftDetect *ui; Mat src, dst; IplImage* img; struct feature* features; int n; int display; int intvls; double sigma; double contr_thr; int curv_thr; int img_dbl; int descr_width; int descr_hist_bins; }; #endif // SIFTDETECT_H
SiftDetect.cpp:
#include "siftdetect.h" #include "ui_siftdetect.h" #include <QtGui> #include <QtCore> #include "sift.h" #include "imgfeatures.h" #include "utils.h" //#include <sift.c> SiftDetect::SiftDetect(QWidget *parent) : QDialog(parent), ui(new Ui::SiftDetect) { ui->setupUi(this); n = 0; display = 1; intvls = SIFT_INTVLS; sigma = SIFT_SIGMA; contr_thr = SIFT_CONTR_THR; curv_thr = SIFT_CURV_THR; img_dbl = SIFT_IMG_DBL; descr_width = SIFT_DESCR_WIDTH; descr_hist_bins = SIFT_DESCR_HIST_BINS; } SiftDetect::~SiftDetect() { // cvReleaseImage( &img );//釋放內存退出程序后竟然報錯 delete ui; } void SiftDetect::on_openButton_clicked() { QString img_name = QFileDialog::getOpenFileName(this, "Open Image", "../sift_detect", tr("Image Files(*.png *.jpeg *.jpg *.bmp)")); // img = cvLoadImage( img_name.toAscii().data() ); src = imread( img_name.toAscii().data() ); imwrite( "../sift_detect/src.jpg", src ); ui->textBrowser->clear(); ui->textBrowser->setFixedSize( src.cols, src.rows ); ui->textBrowser->append( "<img src=../sift_detect/src.jpg>" ); } void SiftDetect::on_detectButton_clicked() { //將Mat型的src轉換成IplImage*型的img,因為這里是opencv新老版本混合編程的方法。 img = &src.operator IplImage(); n = _sift_features( img, &features, intvls, sigma, contr_thr, curv_thr, img_dbl, descr_width, descr_hist_bins ); if( display ) { draw_features( img, features, n ); ui->textBrowser->clear(); //將IplImage*型的img轉換成Mat型的dst,這也是opencv新老版本混合編程的一種方法。 dst = Mat( img ); imwrite( "../sift_detect/dst.jpg", dst ); //cvSaveImage( "../sift_detect/dst.jpg", img ); ui->textBrowser->append( "<img src=../sift_detect/dst.jpg>" ); } } void SiftDetect::on_closeButton_clicked() { close(); }
二、Sift特征點匹配過程
由步驟一我們已經獲得了圖片的特征點向量集合。現在來看看特征點匹配,特征點匹配的一個應用就是物體的識別,比如說我有2張圖片A和B,圖片的內容相同,只是圖片的大小尺寸不同。假設A圖片尺寸比較大,且我們已經采用sift算法對圖片A和B都進行了檢測,獲得了它們的特征點集合,現在我們的問題是需要把A和B中相應的特征點給對應連線起來。
既然是匹配,當然每個特征點向量要相似才能匹配到一起,這里采用的是歐式距離來衡量其相似度。
對B中的特征點x,去尋找A中最相似的點y,最簡單的方法就是拿x與A中所有點進行相似度比較,距離最小的那個為匹配點。但是如果圖片中特征點數目很多的話,這樣效率會很低。所以我們需要把A中特征點向量集合用一種數據結構來描述,這種描述要有利於x在A中的搜索,即減少時間復雜度。在sift匹配中,這種數據結構采用的是kd-tree。
關於kd-tree的講解,可以參考博文http://underthehood.blog.51cto.com/2531780/687160
里面講得比較詳細,且舉了例子,很容易理解,這里就沒有必要重復了。
同樣,采用Rob Hess的代碼做了個sift匹配的實驗,開發環境與上面的一樣。
打開軟件后,單擊Open Image按鈕,依次打開需要匹配的2張圖片,如下圖所示:
單擊Sift Detect按鈕,則程序會單獨對這2幅圖片進行sift特征點檢測,結果如下圖所示:
單擊Sift Match按鈕,則會對這2幅圖的特征點結果進行匹配,本次實驗的匹配圖如下所示:
實驗主要部分代碼(附錄有工程code鏈接下載):
SiftMatch.h:
#ifndef SIFTMATCH_H #define SIFTMATCH_H #include <QDialog> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace cv; namespace Ui { class SiftMatch; } class SiftMatch : public QDialog { Q_OBJECT public: explicit SiftMatch(QWidget *parent = 0); ~SiftMatch(); private slots: void on_openButton_clicked(); void on_detectButton_clicked(); void on_matchButton_clicked(); void on_closeButton_clicked(); private: Ui::SiftMatch *ui; Mat src1, src2, src1_c, src2_c, dst; IplImage *img1, *img2, *img3, *stacked; Point pt1, pt2; double d0, d1; struct feature *feat1, *feat2, *feat; struct feature **nbrs; struct kd_node *kd_root; int open_image_number; int n1, n2, k, i, m; }; #endif // SIFTMATCH_H
SiftMatch.cpp:
#include "siftmatch.h" #include "ui_siftmatch.h" #include <QtCore> #include <QtGui> #include "imgfeatures.h" #include "kdtree.h" #include "minpq.h" #include "sift.h" #include "utils.h" #include "xform.h" /* the maximum number of keypoint NN candidates to check during BBF search */ #define KDTREE_BBF_MAX_NN_CHKS 200 /* threshold on squared ratio of distances between NN and 2nd NN */ #define NN_SQ_DIST_RATIO_THR 0.49 SiftMatch::SiftMatch(QWidget *parent) : QDialog(parent), ui(new Ui::SiftMatch) { open_image_number = 0; m = 0; ui->setupUi(this); } SiftMatch::~SiftMatch() { delete ui; } void SiftMatch::on_openButton_clicked() { QString img_name = QFileDialog::getOpenFileName(this, "Open Image", "../sift_detect", tr("Image Files(*.png *.jpeg *.jpg *.bmp)")); open_image_number++; //打開第1張圖片 if( 1 == open_image_number ) { src1 = imread( img_name.toAscii().data() ); img1 = cvLoadImage( img_name.toAscii().data() ); //轉換成IplImage*類型,但是這樣轉換過的后面使用起來感覺還是不特別順利,說明並不是完全100%兼容了。 // img1 = &src1.operator IplImage(); imwrite( "../sift_match/src1.jpg", src1 ); ui->textBrowser->setFixedSize( src1.cols, src1.rows ); ui->textBrowser->append( "<img src=../sift_match/src1.jpg>" ); } //打開第2張圖片 else if( 2 == open_image_number ) { src2 = imread( img_name.toAscii().data() ); img2 = cvLoadImage( img_name.toAscii().data() ); // img2 = &src2.operator IplImage(); imwrite( "../sift_match/src2.jpg", src2 ); ui->textBrowser->setFixedSize( src2.cols+src1.cols, src2.rows+src1.rows ); ui->textBrowser->append( "<img src=../sift_match/src2.jpg>" ); } else open_image_number = 0; } void SiftMatch::on_detectButton_clicked() { //將2幅圖片合成1幅圖片 //img1 = cvLoadImage(); stacked = stack_imgs( img1, img2 ); ui->textBrowser->clear(); //顯示第1幅圖片上的特征點 n1 = sift_features( img1, &feat1 ); draw_features( img1, feat1, n1 ); src1_c = Mat(img1); imwrite("../sift_match/src1_c.jpg", src1_c); ui->textBrowser->append("<img src=../sift_match/src1_c.jpg>"); //顯示第2幅圖片上的特征點 n2 = sift_features( img2, &feat2 ); draw_features( img2, feat2, n2 ); src2_c = Mat(img2); imwrite("../sift_match/src2_c.jpg", src2_c); ui->textBrowser->append("<img src=../sift_match/src2_c.jpg>"); } void SiftMatch::on_matchButton_clicked() { kd_root = kdtree_build( feat2, n2 ); for( i = 0; i < n1; i++ ) { feat = feat1+i; k = kdtree_bbf_knn( kd_root, feat, 2, &nbrs, KDTREE_BBF_MAX_NN_CHKS ); if( k == 2 ) { d0 = descr_dist_sq( feat, nbrs[0] ); d1 = descr_dist_sq( feat, nbrs[1] ); if( d0 < d1 * NN_SQ_DIST_RATIO_THR ) { pt1 = Point( cvRound( feat->x ), cvRound( feat->y ) ); pt2 = Point( cvRound( nbrs[0]->x ), cvRound( nbrs[0]->y ) ); pt2.y += img1->height; cvLine( stacked, pt1, pt2, CV_RGB(255,0,255), 1, 8, 0 ); m++; feat1[i].fwd_match = nbrs[0]; } } free( nbrs ); } dst = Mat( stacked ); imwrite( "../sift_match/dst.jpg", dst ); ui->textBrowser->clear(); ui->textBrowser->setFixedSize( dst.cols, dst.rows ); ui->textBrowser->append("<img src=../sift_match/dst.jpg>"); } void SiftMatch::on_closeButton_clicked() { close(); }
總結:
通過整理下sift算法知識點,對sift算法有了更全面的認識,另外感謝Rob Hess開源了sift算法的代碼,感覺寫好這個算法確實不同意的。(另外,本文博客中引用了上面提到的博客中的圖片,在此聲明一下。)
附錄一:
Rob Hess sift的c代碼在c++中的使用。
由於Rob Hess的代碼是基於c的,如果在其它關於界面開發的c++程序中,比如Qt,MFC等。我這里是Qt,連接時會報如下錯誤:
siftdetect.obj:-1: error: LNK2019: 無法解析的外部符號 "int __cdecl _sift_features(struct _IplImage *,struct feature * *,int,double,double,int,int,int,int)" (?_sift_features@@YAHPAU_IplImage@@PAPAUfeature@@HNNHHHH@Z),該符號在函數 "private: void __thiscall SiftDetect::on_detectButton_clicked(void)" (?on_detectButton_clicked@SiftDetect@@AAEXXZ) 中被引用
網上也有不少網友碰到過類似的情況,比如http://www.opencv.org.cn/forum/viewtopic.php?p=53307和http://topic.csdn.net/u/20120111/21/0368bba0-54b4-42df-8ed5-8e50920ac197.html 但是他們都沒有給出解決辦法。
因此我們要找本質的原因,原因就是c語法和c++語法畢竟不同,所以難免有些兼容性問題。看錯誤提示我們知道是在函數_sift_features時報的錯,該函數在Rob Hess的頭文件中是被定義的extern類型。而在c的編譯器中,extern的函數文件名編譯后會自動在前面加一桿,”_”;而c++語法中會有函數重載的現象,因此它不是像c那樣直接在前面加”_”,否則會沖突,因此c++編譯器會在其函數名后面加入一些像亂碼的東西(反正鏈接時是機器去尋找,只要能區分即可)。
具體的內容可以參考博客:http://blog.csdn.net/wujian53/article/details/706975 這里面講得比較明白,謝謝這位博主。
本次實驗的解決方法是在Rob Hess的sift.h等幾個頭文件文件開始處加入語句:
#ifdef __cplusplus extern "C" { #endif
該文件的結處加入語句:
#ifdef __cplusplus } #endif
這樣的話編譯器在編譯該文件時會知道這是C語法,所以其中間文件命名規則會相應改變,問題也就相應的解決了。
另外,如果出現錯誤提示:
utils.obj:-1: error: LNK2019: 無法解析的外部符號 _va_end,該符號在函數 _fatal_error 中被引用。
則在utils.c代碼中找到va_start( ap, format );和va_end( ap );並將其注釋起來即可。
附錄二: