咱們要實現一個基本的文件IO,用於讀取TUM數據集中的圖像。順帶的,還要作一個參數文件的讀取。python
首先,咱們來作一個參數讀取的類。該類讀取一個記錄各類參數文本文件,例如數據集所在目錄等。程序其餘部分要用到參數時,能夠今後類得到。這樣,之後調參數時只需調整參數文件,而不用從新編譯整個程序,能夠節省調試時間。linux
這種事情有點像在造輪子。可是既然我們本身作slam自己就是在造輪子,那就索性造個痛快吧!ios
參數文件通常是用yaml或xml來寫的。不過爲了保持簡潔,咱們就本身來設計這個文件的簡單語法吧。一個參數文件大概長這樣:c++
# 這是一個參數文件 # 這雖然只是個參數文件,可是是很厲害的呢! # 去你妹的yaml! 我不再用yaml了!簡簡單單多好! # 數據相關 # 起始索引 start_index=1 # 數據所在目錄 data_source=/home/xiang/Documents/data/rgbd_dataset_freiburg1_room/ # 相機內參 camera.cx=318.6 camera.cy=255.3 camera.fx=517.3 camera.fy=516.5 camera.scale=5000.0 camera.d0=0.2624 camera.d1=-0.9531 camera.d2=-0.0054 camera.d3=0.0026 camera.d4=1.1633
語法很簡單,以行爲單位,以#開頭至末尾的都是註釋。參數的名稱與值用等號相連,即 名稱=值 ,很容易吧!下面咱們作一個ParameterReader類,來讀取這個文件。多線程
在此以前,先新建一個 include/common.h 文件,把一些經常使用的頭文件和結構體放到此文件中,免得之後寫代碼前面100行都是#include:ide
include/common.h:函數
1 #ifndef COMMON_H 2 #define COMMON_H 3 4 /** 5 * common.h 6 * 定義一些經常使用的結構體 7 * 以及各類可能用到的頭文件,放在一塊兒方便include 8 */ 9 10 // C++標準庫 11 #include <iostream> 12 #include <fstream> 13 #include <vector> 14 #include <map> 15 #include <string> 16 using namespace std; 17 18 19 // Eigen 20 #include <Eigen/Core> 21 #include <Eigen/Geometry> 22 23 // OpenCV 24 #include <opencv2/core/core.hpp> 25 #include <opencv2/highgui/highgui.hpp> 26 #include <opencv2/calib3d/calib3d.hpp> 27 28 // boost 29 #include <boost/format.hpp> 30 #include <boost/timer.hpp> 31 #include <boost/lexical_cast.hpp> 32 33 namespace rgbd_tutor 34 { 35 36 // 相機內參模型 37 // 增長了畸變參數,雖然可能不會用到 38 struct CAMERA_INTRINSIC_PARAMETERS 39 { 40 // 標準內參 41 double cx=0, cy=0, fx=0, fy=0, scale=0; 42 // 畸變因子 43 double d0=0, d1=0, d2=0, d3=0, d4=0; 44 }; 45 46 47 48 // linux終端的顏色輸出 49 #define RESET "\033[0m" 50 #define BLACK "\033[30m" /* Black */ 51 #define RED "\033[31m" /* Red */ 52 #define GREEN "\033[32m" /* Green */ 53 #define YELLOW "\033[33m" /* Yellow */ 54 #define BLUE "\033[34m" /* Blue */ 55 #define MAGENTA "\033[35m" /* Magenta */ 56 #define CYAN "\033[36m" /* Cyan */ 57 #define WHITE "\033[37m" /* White */ 58 #define BOLDBLACK "\033[1m\033[30m" /* Bold Black */ 59 #define BOLDRED "\033[1m\033[31m" /* Bold Red */ 60 #define BOLDGREEN "\033[1m\033[32m" /* Bold Green */ 61 #define BOLDYELLOW "\033[1m\033[33m" /* Bold Yellow */ 62 #define BOLDBLUE "\033[1m\033[34m" /* Bold Blue */ 63 #define BOLDMAGENTA "\033[1m\033[35m" /* Bold Magenta */ 64 #define BOLDCYAN "\033[1m\033[36m" /* Bold Cyan */ 65 #define BOLDWHITE "\033[1m\033[37m" /* Bold White */ 66 67 68 } 69 70 #endif // COMMON_H
嗯,請注意咱們使用rgbd_tutor做爲命名空間,之後全部類都位於這個空間裏。而後,文件裏還定義了相機內參的結構,這個結構咱們以後會用到,先放在這兒。接下來是include/parameter_reader.h:性能
1 #ifndef PARAMETER_READER_H 2 #define PARAMETER_READER_H 3 4 #include "common.h" 5 6 namespace rgbd_tutor 7 { 8 9 class ParameterReader 10 { 11 public: 12 // 構造函數:傳入參數文件的路徑 13 ParameterReader( const string& filename = "./parameters.txt" ) 14 { 15 ifstream fin( filename.c_str() ); 16 if (!fin) 17 { 18 // 看看上級目錄是否有這個文件 ../parameter.txt 19 fin.open("."+filename); 20 if (!fin) 21 { 22 cerr<<"沒有找到對應的參數文件:"<<filename<<endl; 23 return; 24 } 25 } 26 27 // 從參數文件中讀取信息 28 while(!fin.eof()) 29 { 30 string str; 31 getline( fin, str ); 32 if (str[0] == '#') 33 { 34 // 以‘#’開頭的是註釋 35 continue; 36 } 37 int pos = str.find('#'); 38 if (pos != -1) 39 { 40 //從井號到末尾的都是註釋 41 str = str.substr(0, pos); 42 } 43 44 // 查找等號 45 pos = str.find("="); 46 if (pos == -1) 47 continue; 48 // 等號左邊是key,右邊是value 49 string key = str.substr( 0, pos ); 50 string value = str.substr( pos+1, str.length() ); 51 data[key] = value; 52 53 if ( !fin.good() ) 54 break; 55 } 56 } 57 58 // 獲取數據 59 // 因爲數據類型不肯定,寫成模板 60 template< class T > 61 T getData( const string& key ) const 62 { 63 auto iter = data.find(key); 64 if (iter == data.end()) 65 { 66 cerr<<"Parameter name "<<key<<" not found!"<<endl; 67 return boost::lexical_cast<T>( "" ); 68 } 69 // boost 的 lexical_cast 能把字符串轉成各類 c++ 內置類型 70 return boost::lexical_cast<T>( iter->second ); 71 } 72 73 // 直接返回讀取到的相機內參 74 rgbd_tutor::CAMERA_INTRINSIC_PARAMETERS getCamera() const 75 { 76 static rgbd_tutor::CAMERA_INTRINSIC_PARAMETERS camera; 77 camera.fx = this->getData<double>("camera.fx"); 78 camera.fy = this->getData<double>("camera.fy"); 79 camera.cx = this->getData<double>("camera.cx"); 80 camera.cy = this->getData<double>("camera.cy"); 81 camera.d0 = this->getData<double>("camera.d0"); 82 camera.d1 = this->getData<double>("camera.d1"); 83 camera.d2 = this->getData<double>("camera.d2"); 84 camera.d3 = this->getData<double>("camera.d3"); 85 camera.d4 = this->getData<double>("camera.d4"); 86 camera.scale = this->getData<double>("camera.scale"); 87 return camera; 88 } 89 90 protected: 91 map<string, string> data; 92 }; 93 94 }; 95 96 #endif // PARAMETER_READER_H
爲保持簡單,我把實現也放到了類中。該類的構造函數裏,傳入參數文件所在的路徑。在咱們的代碼裏,parameters.txt位於代碼根目錄下。不過,若是找不到文件,咱們也會在上一級目錄中尋找一下,這是因爲qtcreator在運行程序時默認使用程序所在的目錄(./bin)而形成的。測試
ParameterReader 實際存儲的數據都是std::string類型(字符串),在須要轉換爲其餘類型時,咱們用 boost::lexical_cast 進行轉換。優化
ParameterReader::getData 函數返回一個參數的值。它有一個模板參數,你能夠這樣使用它:
double d = parameterReader.getData<double>("d");
若是找不到參數,則返回一個空值。
最後,咱們還用了一個函數返回相機的內參,這純粹是爲了外部類調用更方便。
程序運行的基本單位是Frame,而咱們從數據集中讀取的數據也是以Frame爲單位的。如今咱們來設計一個RGBDFrame類,以及向數據集讀取Frame的FrameReader類。
咱們把這兩個類都放在 include/rgbdframe.h 中,以下所示(爲了顯示方便就都貼上來了):
1 #ifndef RGBDFRAME_H 2 #define RGBDFRAME_H 3 4 #include "common.h" 5 #include "parameter_reader.h" 6 7 #include"Thirdparty/DBoW2/DBoW2/FORB.h" 8 #include"Thirdparty/DBoW2/DBoW2/TemplatedVocabulary.h" 9 10 namespace rgbd_tutor{ 11 12 //幀 13 class RGBDFrame 14 { 15 public: 16 typedef shared_ptr<RGBDFrame> Ptr; 17 18 public: 19 RGBDFrame() {} 20 // 方法 21 // 給定像素點,求3D點座標 22 cv::Point3f project2dTo3dLocal( const int& u, const int& v ) const 23 { 24 if (depth.data == nullptr) 25 return cv::Point3f(); 26 ushort d = depth.ptr<ushort>(v)[u]; 27 if (d == 0) 28 return cv::Point3f(); 29 cv::Point3f p; 30 p.z = double( d ) / camera.scale; 31 p.x = ( u - camera.cx) * p.z / camera.fx; 32 p.y = ( v - camera.cy) * p.z / camera.fy; 33 return p; 34 } 35 36 public: 37 // 數據成員 38 int id =-1; //-1表示該幀不存在 39 40 // 彩色圖和深度圖 41 cv::Mat rgb, depth; 42 // 該幀位姿 43 // 定義方式爲:x_local = T * x_world 注意也能夠反着定義; 44 Eigen::Isometry3d T=Eigen::Isometry3d::Identity(); 45 46 // 特徵 47 vector<cv::KeyPoint> keypoints; 48 cv::Mat descriptor; 49 vector<cv::Point3f> kps_3d; 50 51 // 相機 52 // 默認全部的幀都用一個相機模型(難道你還要用多個嗎?) 53 CAMERA_INTRINSIC_PARAMETERS camera; 54 55 // BoW迴環特徵 56 // 講BoW時會用到,這裏先請忽略之 57 DBoW2::BowVector bowVec; 58 59 }; 60 61 // FrameReader 62 // 從TUM數據集中讀取數據的類 63 class FrameReader 64 { 65 public: 66 FrameReader( const rgbd_tutor::ParameterReader& para ) 67 : parameterReader( para ) 68 { 69 init_tum( ); 70 } 71 72 // 得到下一幀 73 RGBDFrame::Ptr next(); 74 75 // 重置index 76 void reset() 77 { 78 cout<<"重置 frame reader"<<endl; 79 currentIndex = start_index; 80 } 81 82 // 根據index得到幀 83 RGBDFrame::Ptr get( const int& index ) 84 { 85 if (index < 0 || index >= rgbFiles.size() ) 86 return nullptr; 87 currentIndex = index; 88 return next(); 89 } 90 91 protected: 92 // 初始化tum數據集 93 void init_tum( ); 94 protected: 95 96 // 當前索引 97 int currentIndex =0; 98 // 起始索引 99 int start_index =0; 100 101 const ParameterReader& parameterReader; 102 103 // 文件名序列 104 vector<string> rgbFiles, depthFiles; 105 106 // 數據源 107 string dataset_dir; 108 109 // 相機內參 110 CAMERA_INTRINSIC_PARAMETERS camera; 111 }; 112 113 }; 114 #endif // RGBDFRAME_H
關於RGBDFrame類的幾點註釋:
接下來,來看FrameReader。它的構造函數中須要有一個parameterReader的引用,由於咱們須要去參數文件裏查詢數據所在的目錄。若是查詢成功,它會作一些初始化的工做,而後外部類就能夠經過next()函數獲得下一幀的圖像了。咱們在src/rgbdframe.cpp中實現init_tum()和next()這兩個函數:
1 #include "rgbdframe.h" 2 #include "common.h" 3 #include "parameter_reader.h" 4 5 using namespace rgbd_tutor; 6 7 RGBDFrame::Ptr FrameReader::next() 8 { 9 if (currentIndex < start_index || currentIndex >= rgbFiles.size()) 10 return nullptr; 11 12 RGBDFrame::Ptr frame (new RGBDFrame); 13 frame->id = currentIndex; 14 frame->rgb = cv::imread( dataset_dir + rgbFiles[currentIndex]); 15 frame->depth = cv::imread( dataset_dir + depthFiles[currentIndex], -1); 16 17 if (frame->rgb.data == nullptr || frame->depth.data==nullptr) 18 { 19 // 數據不存在 20 return nullptr; 21 } 22 23 frame->camera = this->camera; 24 currentIndex ++; 25 return frame; 26 } 27 28 void FrameReader::init_tum( ) 29 { 30 dataset_dir = parameterReader.getData<string>("data_source"); 31 string associate_file = dataset_dir+"/associate.txt"; 32 ifstream fin(associate_file.c_str()); 33 if (!fin) 34 { 35 cerr<<"找不着assciate.txt啊!在tum數據集中這尼瑪是必須的啊!"<<endl; 36 cerr<<"請用python assicate.py rgb.txt depth.txt > associate.txt生成一個associate文件,再來跑這個程序!"<<endl; 37 return; 38 } 39 40 while( !fin.eof() ) 41 { 42 string rgbTime, rgbFile, depthTime, depthFile; 43 fin>>rgbTime>>rgbFile>>depthTime>>depthFile; 44 if ( !fin.good() ) 45 { 46 break; 47 } 48 rgbFiles.push_back( rgbFile ); 49 depthFiles.push_back( depthFile ); 50 } 51 52 cout<<"一共找着了"<<rgbFiles.size()<<"個數據記錄哦!"<<endl; 53 camera = parameterReader.getCamera(); 54 start_index = parameterReader.getData<int>("start_index"); 55 currentIndex = start_index; 56 }
能夠看到,在init_tum中,咱們從前一講生成的associate.txt裏得到圖像信息,把文件名存儲在一個vector中。而後,next()函數根據currentIndex返回對應的數據。
如今咱們來測試一下以前寫的FrameReader。在experiment中添加一個reading_frame.cpp文件,測試文件是否正確讀取。
experiment/reading_frame.cpp
1 #include "rgbdframe.h" 2 3 using namespace rgbd_tutor; 4 int main() 5 { 6 ParameterReader para; 7 FrameReader fr(para); 8 while( RGBDFrame::Ptr frame = fr.next() ) 9 { 10 cv::imshow( "image", frame->rgb ); 11 cv::waitKey(1); 12 } 13 14 return 0; 15 }
因爲以前定義好了接口,這部分就很簡單,幾乎不須要解釋了。咱們只是把數據從文件中讀取出來,加以顯示而已。
下面咱們來寫編譯此程序所用的CMakeLists。
代碼根目錄下的CMakeLists.txt:
1 cmake_minimum_required( VERSION 2.8 ) 2 project( rgbd-slam-tutor2 ) 3 4 # 設置用debug仍是release模式。debug容許斷點,而release更快 5 #set( CMAKE_BUILD_TYPE Debug ) 6 set( CMAKE_BUILD_TYPE Release ) 7 8 # 設置編譯選項 9 # 容許c++11標準、O3優化、多線程。match選項可避免一些cpu上的問題 10 set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -march=native -O3 -pthread" ) 11 12 # 常見依賴庫:cv, eigen, pcl 13 find_package( OpenCV REQUIRED ) 14 find_package( Eigen3 REQUIRED ) 15 find_package( PCL 1.7 REQUIRED ) 16 17 include_directories( 18 ${PCL_INCLUDE_DIRS} 19 ${PROJECT_SOURCE_DIR}/ 20 ) 21 22 set( thirdparty_libs 23 ${OpenCV_LIBS} 24 ${PCL_LIBRARY_DIRS} 25 ${PROJECT_SOURCE_DIR}/Thirdparty/DBoW2/lib/libDBoW2.so 26 ) 27 28 add_definitions(${PCL_DEFINITIONS}) 29 30 # 二進制文件輸出到bin 31 set( EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin ) 32 # 庫輸出到lib 33 set( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib ) 34 35 # 頭文件目錄 36 include_directories( 37 ${PROJECT_SOURCE_DIR}/include 38 ) 39 40 # 源文件目錄 41 add_subdirectory( ${PROJECT_SOURCE_DIR}/src/ ) 42 add_subdirectory( ${PROJECT_SOURCE_DIR}/experiment/ )
src/目錄下的CMakeLists.txt:
1 add_library( rgbd_tutor 2 rgbdframe.cpp 3 )
experiment下的CMakeLists.txt
1 add_executable( helloslam helloslam.cpp ) 2 3 add_executable( reading_frame reading_frame.cpp ) 4 target_link_libraries( reading_frame rgbd_tutor ${thirdparty_libs} )
注意到,咱們把rgbdframe.cpp編譯成了庫,而後把reading_frame連接到了這個庫上。因爲在RGBDFrame類中用到了DBoW庫的代碼,因此咱們先去編譯一下DBoW這個庫。
1 cd Thirdparty/DBoW2 2 mkdir build lib 3 cd build 4 cmake .. 5 make -j4
這樣就把DBoW編譯好了。這個庫之後咱們要在迴環檢測中用到。接下來就是編譯我們本身的程序了。若是你用qtCreator,能夠直接打開根目錄下的CMakeLists.txt,點擊編譯便可:
若是你不用這個IDE,遵循傳統的cmake編譯方式便可。編譯後在bin/下面生成reading_frame程序,能夠直接運行。
運行後,你能夠看到鏡頭在快速的運動。由於咱們沒作任何處理,這應該是你在電腦上能看到的最快的處理速度了(固然取決於你的配置)。隨後咱們要把特徵提取、匹配和跟蹤都加進去,可是但願它仍能保持在正常的視頻速度。
下節預告
下節咱們將介紹orb特徵的提取與匹配,並測試它的匹配速度與性能。
問題
若是你有任何問題,請寫在評論區中。有表明性的問題我會統一回復。