SLAM 主要分爲兩個部分:前端和後端,前端也就是視覺里程計(VO),它根據相鄰圖像的信息粗略的估計出相機的運動,給後端提供較好的初始值。VO的實現方法能夠根據是否須要提取特徵分爲兩類:基於特徵點的方法,不使用特徵點的直接方法。 基於特徵點的VO運行穩定,對光照、動態物體不敏感。html
圖像特徵點的提取和匹配是計算機視覺中的一個基本問題,在視覺SLAM中就須要首先找到相鄰圖像對應點的組合,根據這些匹配的點對計算出相機的位姿(相對初始位置,相機的旋轉和平移)。
本文對這段時間對特徵點的學習作一個總結,主要有如下幾方面的內容:前端
如何高效且準確的匹配出兩個不一樣視角的圖像中的同一個物體,是許多計算機視覺應用中的第一步。雖然圖像在計算機中是以灰度矩陣的形式存在的,可是利用圖像的灰度並不能準確的找出兩幅圖像中的同一個物體。這是因爲灰度受光照的影響,而且當圖像視角變化後,同一個物體的灰度值也會跟着變化。因此,就須要找出一種可以在相機進行移動和旋轉(視角發生變化),仍然可以保持不變的特徵,利用這些不變的特徵來找出不一樣視角的圖像中的同一個物體。git
爲了可以更好的進行圖像匹配,須要在圖像中選擇具備表明性的區域,例如:圖像中的角點、邊緣和一些區塊,但在圖像識別出角點是最容易,也就是說角點的辨識度是最高的。因此,在不少的計算機視覺處理中,都是提取交掉做爲特徵,對圖像進行匹配,例如SFM,視覺SLAM等。github
可是,單純的角點並不能很好的知足咱們的需求,例如:相機從遠處獲得的是角點,可是在近處就可能不是角點;或者,當相機旋轉後,角點就發生了變化。爲此,計算機視覺的研究者們設計了許多更爲穩定的的特徵點,這些特徵點不會隨着相機的移動,旋轉或者光照的變化而變化。例如:SIFT,SURF,ORB等算法
一個圖像的特徵點由兩部分構成:關鍵點(Keypoint)和描述子(Descriptor)。 關鍵點指的是該特徵點在圖像中的位置,有些還具備方向、尺度信息;描述子一般是一個向量,按照人爲的設計的方式,描述關鍵點周圍像素的信息。一般描述子是按照外觀類似的特徵應該有類似的描述子設計的。所以,在匹配的時候,只要兩個特徵點的描述子在向量空間的距離相近,就能夠認爲它們是同一個特徵點。後端
特徵點的匹配一般須要如下三個步驟:多線程
這裏先介紹下特徵點的描述子,一個好的描述子是準確匹配的基礎,關鍵點的提取和特徵點的匹配,在後面介紹。函數
從圖像中提取到特徵的關鍵點信息,一般只是其在圖像的位置信息(有可能包含尺度和方向信息),僅僅利用這些信息沒法很好的進行特徵點的匹配,因此就須要更詳細的信息,將特徵區分開來,這就是特徵描述子。另外,經過特徵描述子能夠消除視角的變化帶來圖像的尺度和方向的變化,可以更好的在圖像間匹配。學習
特徵的描述子一般是一個精心設計的向量,描述了關鍵點及其周圍像素的信息。爲了可以更好的匹配,一個好的描述子一般要具備如下特性:測試
其中描述子的可區分性和其不變性是矛盾的,一個具備衆多不變性的特徵描述子,其區分局部圖像內容的能力就比較稍弱;而若是一個很容易區分不一樣局部圖像內容的特徵描述子,其魯棒性每每比較低。因此,在設計特徵描述子的時候,就須要綜合考慮這三個特性,找到三者之間的平衡。
特徵描述子的不變性主要體如今兩個方面:
爲了有個更直觀的理解,下面給出SIFT,SURF,BRIEF描述子計算方法對比
從上表能夠看出,SIFT,SURF和BRIEF描述子都是一個向量,只是維度不一樣。其中,SIFT和SURF在構建特徵描述子的時候,保存了特徵的方向和尺度特徵,這樣其特徵描述子就具備尺度和旋轉不變性;而BRIEF描述子並無尺度和方向特徵,不具有尺度和旋轉不變性。
上面提到圖像的特徵點包含兩個部分:
在圖像中提取到關鍵點的位置信息後,爲了可以更有效的匹配(主要是保證尺度和旋轉不變性),一般使用一個向量來描述關鍵點及其周圍的信息。特徵的描述子,在特徵點的匹配中是很是重要的,上一小節中對其應該具備的性質作了介紹。但具體到一個算法來講,可能其既有特徵點的提取算法也有特徵點描述子的算法,也有可能其僅僅是一個特徵點提取算法或者是特徵點的描述子算法。在本小節就經常使用的特徵點算法作一個簡要的說明。
提到特徵點算法,首先就是大名鼎鼎的SIFT算法了。SIFT的全稱是Scale Invariant Feature Transform,尺度不變特徵變換,2004年由加拿大教授David G.Lowe提出的。SIFT特徵對旋轉、尺度縮放、亮度變化等保持不變性,是一種很是穩定的局部特徵。
SIFT算法主要有如下幾個步驟:
SIFT算法中及包含了特徵點的提取算法,也有如何生成描述子的算法,更進一步的SIFT算法介紹可參看SIFT特徵詳解
SURF全稱 Speeded Up Robust Features,是在SIFT算法的基礎上提出的,主要針對SIFT算法運算速度慢,計算量大的缺點進行了改進。
SURF的流程和SIFT比較相似,這些改進體如今如下幾個方面:
SIFT和SURF是很是好的,穩定的特徵點算法,但運算速度是其一大弊端,沒法作到實時的特徵提取和匹配,其應用就有了很大的侷限性。FAST特徵提取算法彌補了這一侷限,檢測局部像素灰度變化明顯的地方,以速度快而著稱,其全稱爲:Features From Accelerated Segment Test。在FAST算法的思想很簡單:若是一個像素與周圍鄰域的像素差異較大(過亮或者過暗),那麼能夠認爲該像素是一個角點。和其餘的特徵點提取算法相比,FAST算法只須要比較像素和其鄰域像素的灰度值大小,十分便捷。
FAST算法提取角點的步驟:
FAST算法只檢測像素的灰度值,其運算速度極快,同時不可避免的也有一些缺點
上面的介紹的SIFT和SURF算法都包含有各自的特徵點描述子的計算方法,而FAST不包含特徵點描述子的計算,僅僅只有特徵點的提取方法,這就須要一個特徵點描述方法來描述FAST提取到的特徵點,以方便特徵點的匹配。下面介紹一個專門的特徵點描述子的計算算法。
BRIEF是一種二進制的描述子,其描述向量是0和1表示的二進制串。0和1表示特徵點鄰域內兩個像素(p和q)灰度值的大小:若是p比q大則選擇1,反正就取0。在特徵點的周圍選擇128對這樣的p和q的像素對,就獲得了128維由0,1組成的向量。那麼p和q的像素對是怎麼選擇的呢?一般都是按照某種機率來隨機的挑選像素對的位置。
BRIEF使用隨機選點的比較,速度很快,並且使用二進制串表示最終生成的描述子向量,在存儲以及用於匹配的比較時都是很是方便的,其和FAST的搭配起來能夠組成很是快速的特徵點提取和描述算法。
ORB的全稱是Oriented FAST and Rotated BRIEF,是目前來講很是好的可以進行的實時的圖像特徵提取和描述的算法,它改進了FAST特徵提取算法,並使用速度極快的二進制描述子BRIEF。
針對FAST特徵提取的算法的一些肯定,ORB也作了相應的改進。
OpenCV中封裝了經常使用的特徵點算法(如SIFT,SURF,ORB等),提供了統一的接口,便於調用。 下面代碼是OpenCV中使用其feature 2D 模塊的示例代碼
Mat img1 = imread("F:\\image\\1.png"); Mat img2 = imread("F:\\image\\2.png"); // 1. 初始化 vector<KeyPoint> keypoints1, keypoints2; Mat descriptors1, descriptors2; Ptr<ORB> orb = ORB::create(); // 2. 提取特徵點 orb->detect(img1, keypoints1); orb->detect(img2, keypoints2); // 3. 計算特徵描述符 orb->compute(img1, keypoints1, descriptors1); orb->compute(img2, keypoints2, descriptors2); // 4. 對兩幅圖像的BRIEF描述符進行匹配,使用BFMatch,Hamming距離做爲參考 vector<DMatch> matches; BFMatcher bfMatcher(NORM_HAMMING); bfMatcher.match(descriptors1, descriptors2, matches);
Ptr<FeatureDetector> detector = FeatureDetector::create()
來獲得特徵提取器的一個實例,全部的參數都提供了默認值,也能夠根據具體的須要傳入相應的參數。detect
方法檢測圖像中的特徵點的具體位置,檢測的結果保存在vector<KeyPoint>
向量中。compute
方法來計算特徵點的描述子,描述子一般是一個向量,保存在Mat
中。BFMatcher
,該算法在向量空間中,將特徵點的描述子一一比較,選擇距離(上面代碼中使用的是Hamming距離)較小的一對做爲匹配點。上面代碼匹配後的結果以下:
特徵的匹配是針對特徵描述子的進行的,上面提到特徵描述子一般是一個向量,兩個特徵描述子的之間的距離能夠反應出其類似的程度,也就是這兩個特徵點是否是同一個。根據描述子的不一樣,能夠選擇不一樣的距離度量。若是是浮點類型的描述子,可使用其歐式距離;對於二進制的描述子(BRIEF)可使用其漢明距離(兩個不一樣二進制之間的漢明距離指的是兩個二進制串不一樣位的個數)。
有了計算描述子類似度的方法,那麼在特徵點的集合中如何尋找和其最類似的特徵點,這就是特徵點的匹配了。最簡單直觀的方法就是上面使用的:暴力匹配方法(Brute-Froce Matcher),計算某一個特徵點描述子與其餘全部特徵點描述子之間的距離,而後將獲得的距離進行排序,取距離最近的一個做爲匹配點。這種方法簡單粗暴,其結果也是顯而易見的,經過上面的匹配結果,也能夠看出有大量的錯誤匹配,這就須要使用一些機制來過濾掉錯誤的匹配。
// 匹配對篩選 double min_dist = 1000, max_dist = 0; // 找出全部匹配之間的最大值和最小值 for (int i = 0; i < descriptors1.rows; i++) { double dist = matches[i].distance; if (dist < min_dist) min_dist = dist; if (dist > max_dist) max_dist = dist; } // 當描述子之間的匹配大於2倍的最小距離時,即認爲該匹配是一個錯誤的匹配。 // 但有時描述子之間的最小距離很是小,能夠設置一個經驗值做爲下限 vector<DMatch> good_matches; for (int i = 0; i < descriptors1.rows; i++) { if (matches[i].distance <= max(2 * min_dist, 30.0)) good_matches.push_back(matches[i]); }
結果以下:
對比只是用暴力匹配的方法,進行過濾後的匹配效果好了不少。
交叉匹配
針對暴力匹配,可使用交叉匹配的方法來過濾錯誤的匹配。交叉過濾的是想很簡單,再進行一次匹配,反過來使用被匹配到的點進行匹配,若是匹配到的仍然是第一次匹配的點的話,就認爲這是一個正確的匹配。舉例來講就是,假如第一次特徵點A使用暴力匹配的方法,匹配到的特徵點是特徵點B;反過來,使用特徵點B進行匹配,若是匹配到的仍然是特徵點A,則就認爲這是一個正確的匹配,不然就是一個錯誤的匹配。OpenCV中BFMatcher
已經封裝了該方法,建立BFMatcher
的實例時,第二個參數傳入true
便可,BFMatcher bfMatcher(NORM_HAMMING,true)
。
bfMatcher->knnMatch(descriptors1, descriptors2, knnMatches, 2);
具體實現的代碼以下:const float minRatio = 1.f / 1.5f; const int k = 2; vector<vector<DMatch>> knnMatches; matcher->knnMatch(leftPattern->descriptors, rightPattern->descriptors, knnMatches, k); for (size_t i = 0; i < knnMatches.size(); i++) { const DMatch& bestMatch = knnMatches[i][0]; const DMatch& betterMatch = knnMatches[i][1]; float distanceRatio = bestMatch.distance / betterMatch.distance; if (distanceRatio < minRatio) matches.push_back(bestMatch); }const float minRatio = 1.f / 1.5f; const int k = 2; vector<vector<DMatch>> knnMatches; matcher->knnMatch(leftPattern->descriptors, rightPattern->descriptors, knnMatches, 2); for (size_t i = 0; i < knnMatches.size(); i++) { const DMatch& bestMatch = knnMatches[i][0]; const DMatch& betterMatch = knnMatches[i][1]; float distanceRatio = bestMatch.distance / betterMatch.distance; if (distanceRatio < minRatio) matches.push_back(bestMatch); }
將不知足的最近鄰的匹配之間距離比率大於設定的閾值(1/1.5)匹配剔除。
findHomography
,能夠爲該方法設定一個重投影偏差的閾值,能夠獲得一個向量mask來指定那些是符合該重投影偏差的匹配點對,以此來剔除錯誤的匹配,代碼以下:const int minNumbermatchesAllowed = 8; if (matches.size() < minNumbermatchesAllowed) return; //Prepare data for findHomography vector<Point2f> srcPoints(matches.size()); vector<Point2f> dstPoints(matches.size()); for (size_t i = 0; i < matches.size(); i++) { srcPoints[i] = rightPattern->keypoints[matches[i].trainIdx].pt; dstPoints[i] = leftPattern->keypoints[matches[i].queryIdx].pt; } //find homography matrix and get inliers mask vector<uchar> inliersMask(srcPoints.size()); homography = findHomography(srcPoints, dstPoints, CV_FM_RANSAC, reprojectionThreshold, inliersMask); vector<DMatch> inliers; for (size_t i = 0; i < inliersMask.size(); i++){ if (inliersMask[i]) inliers.push_back(matches[i]); } matches.swap(inliers);const int minNumbermatchesAllowed = 8; if (matches.size() < minNumbermatchesAllowed) return; //Prepare data for findHomography vector<Point2f> srcPoints(matches.size()); vector<Point2f> dstPoints(matches.size()); for (size_t i = 0; i < matches.size(); i++) { srcPoints[i] = rightPattern->keypoints[matches[i].trainIdx].pt; dstPoints[i] = leftPattern->keypoints[matches[i].queryIdx].pt; } //find homography matrix and get inliers mask vector<uchar> inliersMask(srcPoints.size()); homography = findHomography(srcPoints, dstPoints, CV_FM_RANSAC, reprojectionThreshold, inliersMask); vector<DMatch> inliers; for (size_t i = 0; i < inliersMask.size(); i++){ if (inliersMask[i]) inliers.push_back(matches[i]); } matches.swap(inliers);
以前寫過一篇OpenCV的特徵點匹配及一些剔除錯誤匹配的文章,OpenCV2:特徵匹配及其優化,使用的是OpenCV2,在OpenCV3中更新了特徵點檢測和匹配的接口,不過大致仍是差很少的。上一篇的文末附有練習代碼的下載連接,不要直接打開sln或者project文件,有可能vs版本不同打不開,本文的測試代碼尚未整理,等有時間好好打理下github,練習的代碼隨手都丟了,到想用的時候又找不到了。
翻了下,上一篇博客仍是6月30號發佈的,而今已經是12月底,半年6個月時間就這樣過去了。而我,好像沒有什麼成長啊,工資仍是那麼多,調試bug的技術卻是積累了不少,知道多線程程序調試;多進程通訊;學會了用Windebug:分析dump文件,在無代碼環境中attach到執行文件中分析問題或者拿着pdb文件和源代碼在現場環境中進行調試...;實實在在的感覺到了C++的內存泄漏和空指針致使的各類奇葩問題;知道了使用未初始化的變量的不穩定性;知道了項目設計中擴展性的重要的... 寫以前以爲本身虛度了半年,總結下來,這半年下來時間仍是成長了很多的,內心的愧疚感下降了很多。不過之後仍是要堅持寫博客記錄下學習的過程...