前言
本周博客我們給大家介紹一下SiftGPU。由於特征匹配是SLAM中非常耗時間的一步,許多人都想把它的時間降至最短,因此目前ORB成了非常受歡迎的特征。而老牌SIFT,則一直給人一種“很嚴謹很精確,但計算非常慢”的印象。在一個普通的PC上,計算一個640$\times$480的圖中的SIFT大約需要幾百毫秒左右。如果特征都要算300ms,加上別的ICP什么的,一個SLAM就只能做成兩幀左右的速度了,這是很令人失望的。而ORB,FAST之類的特征,由於計算速度較快,在SLAM這種實時性要求較高的場合更受歡迎。
那么,今天我們來說一個GPU版本的SIFT。它是由Wu Changchang同學寫的。它能夠明顯地提升你的程序提取SIFT的速度。同時,它的代碼大部分是基於OpenGL的,即使在沒有英偉達顯卡的機器上也能運行起來。但另一方面,出於某種(歷史或人為的)原因,SiftGPU的代碼配置起來並不很容易(特別是在Linux下,似乎SiftGPU作者是在win下開發的),代碼新人可能會覺得比較困難。現在我們帶着大家實踐一下SiftGPU,我會給出一個例程供大家測試。
首先,說說我的運行配置。我用的機器是Thinkpad T450, Intel+Nvidia GetForce 940m顯卡。但我個人只用Intel卡,所以我就不編譯Cuda了。各位有上好N卡的同學也可以搞個Cuda下來編,可能會提高一點速度(但我不保證)。我使用的操作系統是Ubuntu 14.04,OpenCV3.1版本。所以我假設你OpenCV已經裝好啦!(所以c++編譯器總有的吧!) 不過opencv是不是3.1版本是沒關系的,程序在2.x版本上也是能正常運行的。
小蘿卜:師兄你這真是宅男標配啊!你到底是在講配置環境還是在秀桌面啊!
下載SiftGPU與依賴庫
SiftGPU主頁:http://www.cs.unc.edu/~ccwu/siftgpu/
請找到“SiftGPU-V400"那個下載鏈接,保存到你的電腦上。然后解壓縮,進入壓縮后的文件夾。假定你也在用Ubuntu,那么你現在的目錄應該是 ~/Downloads/SiftGPU/ 。注意,為了和我保持一致,請你暫時不要下載github上面那個版本,那個與它稍有不同。如果你就是喜歡github,可以把這個編譯好,再考慮用github版本。
現在我們來安裝依賴項。首先,確保你機器上有OpenGL,請安裝以下幾項工具:
1 sudo apt-get install libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev
然后,要安裝glew1.5.1以上版本。據我個人經驗,最好是去下載glew網站的版本。
glew的網址:http://glew.sourceforge.net/
請下載那個1.13.0版本,zip文件或tgz均可。下載到本地並解壓,然后進入該文件夾。我的在~/Downloads/glew-1.13.0
glew是用makefile直接編譯的,不用cmake。所以我們直接敲:
1 make 2 sudo make install
即可。很快它就編譯好了。
注意看make install輸出的信息。它默認把編譯好的庫文件libglew.so.1.13放到了/usr/lib64下。由於之后我們要用cmake去編,但是它可能找不到這個文件夾,所以我們現在先告訴系統,該文件夾下有要找的鏈接庫:
1 sudo ldconfig /usr/lib64/
ok,現在我們處理完了glew,轉去編譯SiftGPU。SiftGPU也是用Makefile編譯的。現在轉到SiftGPU所在文件夾。調用
1 make
來完成編譯。如果順利的話,你會在bin/目錄里得到幾個二進制和一個libsiftgpu.so庫文件。我們主要使用這個庫文件。現在看一下它的鏈接是否正確:
1 ldd bin/libsiftgpu.so
這個命令會輸出與它鏈接的庫的信息。請保證沒有出現某個鏈接(特別是剛才的GLEW)沒有找到的情況(否則這里會通過,但后面會出現undefined reference)。像我這樣:
如果這步正確無誤,恭喜你,SiftGPU已經編譯完成了!真是可喜可賀呀!
小蘿卜:然后呢?師兄我還沒看到什么感覺很厲害的東西啊?
師兄:下面我們來實際找一個圖片,寫一段小程序調用SiftGPU,提一下特征試試。為測試速度,我們還要記錄一下代碼運行時間。
測試SiftGPU
現在我們來寫一個測試程序。由於它比較短,我就不專門搞個github了。請大家跟着我做即可。
首先,隨意新建一個目錄,比如test_siftgpu。我們要寫一個c++程序,然后用cmake編譯它。現在新建一個main.cpp,內容如下:
1 // SiftGPU模塊 2 #include <SiftGPU.h> 3 4 //標准C++ 5 #include <iostream> 6 #include <vector> 7 8 // OpenCV圖像 9 #include <opencv2/core/core.hpp> 10 #include <opencv2/highgui/highgui.hpp> 11 12 // boost庫中計時函數 13 #include <boost/timer.hpp> 14 15 // OpenGL 16 #include <GL/gl.h> 17 18 using namespace std; 19 20 int main( int argc, char** argv) 21 { 22 //聲明SiftGPU並初始化 23 SiftGPU sift; 24 char* myargv[4] ={ "-fo", "-1", "-v", "1"}; 25 sift.ParseParam(4, myargv); 26 27 //檢查硬件是否支持SiftGPU 28 int support = sift.CreateContextGL(); 29 if ( support != SiftGPU::SIFTGPU_FULL_SUPPORTED ) 30 { 31 cerr<<"SiftGPU is not supported!"<<endl; 32 return 2; 33 } 34 35 //測試直接讀取一張圖像 36 cout<<"running sift"<<endl; 37 boost::timer timer; 38 //在此填入你想測試的圖像的路徑!不要用我的路徑!不要用我的路徑!不要用我的路徑! 39 sift.RunSIFT( "/home/xiang/wallE-slam/data/rgb1.png" ); 40 cout<<"siftgpu::runsift() cost time="<<timer.elapsed()<<endl; 41 42 // 獲取關鍵點與描述子 43 int num = sift.GetFeatureNum(); 44 cout<<"Feature number="<<num<<endl; 45 vector<float> descriptors(128*num); 46 vector<SiftGPU::SiftKeypoint> keys(num); 47 timer.restart(); 48 sift.GetFeatureVector(&keys[0], &descriptors[0]); 49 cout<<"siftgpu::getFeatureVector() cost time="<<timer.elapsed()<<endl; 50 51 // 先用OpenCV讀取一個圖像,然后調用SiftGPU提取特征 52 cv::Mat img = cv::imread("/home/xiang/wallE-slam/data/rgb1.png", 0); 53 int width = img.cols; 54 int height = img.rows; 55 timer.restart(); 56 // 注意我們處理的是灰度圖,故照如下設置 57 sift.RunSIFT(width, height, img.data, GL_INTENSITY8, GL_UNSIGNED_BYTE); 58 cout<<"siftgpu::runSIFT() cost time="<<timer.elapsed()<<endl; 59 60 return 0; 61 }
Sift接口還是相當簡單的。在這程序里,我們一共做了三件事。一是直接對一個圖像路徑提Sift,二是獲取Sift的關鍵點和描述子。三是對OpenCV讀取的一個圖像提取Sift。我們分別測了三者的效果和時間。
接下來,寫一個CMakeLists.txt來編譯上面的文件。
cmake_minimum_required(VERSION 2.8.3) project(test_siftgpu) # OpenCV依賴 find_package( OpenCV REQUIRED ) # OpenGL find_package(OpenGL REQUIRED) # GLUT find_package(GLUT REQUIRED) # Glew find_package(Glew REQUIRED) # SiftGPU:手動設置其頭文件與庫文件所在位置 include_directories("/home/xiang/Downloads/SiftGPU/src/SiftGPU/" ${OpenGL_INCLUDE_DIR}) set(SIFTGPU_LIBS "/home/xiang/Downloads/SiftGPU/bin/libsiftgpu.so") add_executable( testSIFTGPU main.cpp ) target_link_libraries( testSIFTGPU ${OpenCV_LIBS} ${SIFTGPU_LIBS} ${GLEW_LIBRARIES} ${GLUT_LIBRARIES} ${OPENGL_LIBRARIES} )
對於SiftGPU,由於它本身沒有提供cmake的配置,我們手動去設置了它的頭文件與庫文件的鏈接方式。大家可以學習一下這種比較土的辦法……然后就是常見的cmake啦:
mkdir build cd build cmake .. make
等一下!是不是還忘了些什么呢?嗯,如果你直接去cmake的話,會報一個find_package找不到glew的錯!因為我們裝glew的時候是直接用make install裝的嘛,cmake怎么會知道我們干了這件事呢?所以此時find_package(Glew REQUIRED)就會出錯啦!
小蘿卜:為什么出錯了你還是很高興的樣子……
師兄:對!現在呢我們要自己寫一個FindGlew.cmake文件嘍。請打開你的編輯器,輸入:
1 # 2 # Try to find GLEW library and include path. 3 # Once done this will define 4 # 5 # GLEW_FOUND 6 # GLEW_INCLUDE_PATH 7 # GLEW_LIBRARY 8 # 9 10 IF (WIN32) 11 FIND_PATH( GLEW_INCLUDE_PATH GL/glew.h 12 $ENV{PROGRAMFILES}/GLEW/include 13 ${PROJECT_SOURCE_DIR}/src/nvgl/glew/include 14 DOC "The directory where GL/glew.h resides") 15 FIND_LIBRARY( GLEW_LIBRARY 16 NAMES glew GLEW glew32 glew32s 17 PATHS 18 $ENV{PROGRAMFILES}/GLEW/lib 19 ${PROJECT_SOURCE_DIR}/src/nvgl/glew/bin 20 ${PROJECT_SOURCE_DIR}/src/nvgl/glew/lib 21 DOC "The GLEW library") 22 ELSE (WIN32) 23 FIND_PATH( GLEW_INCLUDE_PATH GL/glew.h 24 /usr/include 25 /usr/local/include 26 /sw/include 27 /opt/local/include 28 DOC "The directory where GL/glew.h resides") 29 FIND_LIBRARY( GLEW_LIBRARY 30 NAMES GLEW glew 31 PATHS 32 /usr/lib64 33 /usr/lib 34 /usr/local/lib64 35 /usr/local/lib 36 /sw/lib 37 /opt/local/lib 38 DOC "The GLEW library") 39 ENDIF (WIN32) 40 41 IF (GLEW_INCLUDE_PATH) 42 SET( GLEW_FOUND 1 CACHE STRING "Set to 1 if GLEW is found, 0 otherwise") 43 ELSE (GLEW_INCLUDE_PATH) 44 SET( GLEW_FOUND 0 CACHE STRING "Set to 1 if GLEW is found, 0 otherwise") 45 ENDIF (GLEW_INCLUDE_PATH) 46 47 MARK_AS_ADVANCED( GLEW_FOUND )
然后呢,把這個文件放到cmake的modules文件夾中去!這樣cmake就會知道你在調用find_package(Glew)時怎么找啦!
sudo cp ./FindGlew.cmake /usr/share/cmake-2.8/Modules/
注意到這個文件所在的目錄通常是沒有寫權限的的哦!所以我們要用sudo提升到管理員權限才行呢。
這時,再調用cmake ..,就不會報上面的錯誤啦!而編譯也得以順利進行下去了。
但是!但是!編譯還是出錯了,錯誤如下:
/home/xiang/Downloads/SiftGPU/src/SiftGPU/SiftGPU.h:336:40: error: declaration of ‘operator new’ as non-function SIFTGPU_EXPORT void* operator new (size_t size);
這是什么原因呢?g++的編譯錯誤很難懂,一直為人詬病。師兄仔細查了查,發現SiftGPU作者重載了new運算符,但是它的參數"size_t size"中的"size_t"類型,在linux下編譯是需要指定一個頭文件的!所以我們打開~/Downloads/SiftGPU/src/SiftGPU/SiftGPU.h文件,在上頭加入一個
#include <stddef.h>
這樣編譯器就會找到size_t類型啦!編譯就能通過嘍!
SiftGPU運行結果
以下就是在師兄電腦上的運行結果啦,大家可以看一下:
對於OpenCV已經讀入的數據,在640x480的分辨率下,用SiftGPU只需40多毫秒即可完成計算了呢!GPU真的是很強大啊!即使在沒有Cuda的情況下都取得了近十倍的加速啊!效果拔群!
小蘿卜:我的ORB只要30毫秒就行了,哼.
小結
本篇介紹了SiftGPU,我們帶領讀者完成了它的編譯,並在自己的程序內實現了調用。可以看到它的加速效果還是不錯的!
另外,這也是我的一次嘗試,告訴讀者在編譯過程中遇到問題該如何處理。我本可以直接跳過這些buggy的部分,告訴大家運行的結果。但我覺得這樣子講可能對讀者更有幫助啦!
如果你覺得我的博客有幫助,可以進行幾塊錢的小額贊助,幫助我把博客寫得更好。