SLAM入門之視覺里程計(1):特徵點的匹配

SLAM 主要分爲兩個部分:前端和後端,前端也就是視覺里程計(VO),它根據相鄰圖像的信息粗略的估計出相機的運動,給後端提供較好的初始值。VO的實現方法能夠根據是否須要提取特徵分爲兩類:基於特徵點的方法,不使用特徵點的直接方法。 基於特徵點的VO運行穩定,對光照、動態物體不敏感。html

圖像特徵點的提取和匹配是計算機視覺中的一個基本問題,在視覺SLAM中就須要首先找到相鄰圖像對應點的組合,根據這些匹配的點對計算出相機的位姿(相對初始位置,相機的旋轉和平移)。
本文對這段時間對特徵點的學習作一個總結,主要有如下幾方面的內容:前端

  • 特徵點概述
  • 經常使用的特徵點算法,如SIFT,SURF,FAST等
  • OpenCV3中特徵點的提取和匹配

特徵點概述

如何高效且準確的匹配出兩個不一樣視角的圖像中的同一個物體,是許多計算機視覺應用中的第一步。雖然圖像在計算機中是以灰度矩陣的形式存在的,可是利用圖像的灰度並不能準確的找出兩幅圖像中的同一個物體。這是因爲灰度受光照的影響,而且當圖像視角變化後,同一個物體的灰度值也會跟着變化。因此,就須要找出一種可以在相機進行移動和旋轉(視角發生變化),仍然可以保持不變的特徵,利用這些不變的特徵來找出不一樣視角的圖像中的同一個物體。git

爲了可以更好的進行圖像匹配,須要在圖像中選擇具備表明性的區域,例如:圖像中的角點、邊緣和一些區塊,但在圖像識別出角點是最容易,也就是說角點的辨識度是最高的。因此,在不少的計算機視覺處理中,都是提取交掉做爲特徵,對圖像進行匹配,例如SFM,視覺SLAM等。github

可是,單純的角點並不能很好的知足咱們的需求,例如:相機從遠處獲得的是角點,可是在近處就可能不是角點;或者,當相機旋轉後,角點就發生了變化。爲此,計算機視覺的研究者們設計了許多更爲穩定的的特徵點,這些特徵點不會隨着相機的移動,旋轉或者光照的變化而變化。例如:SIFT,SURF,ORB等算法

一個圖像的特徵點由兩部分構成:關鍵點(Keypoint)和描述子(Descriptor)。 關鍵點指的是該特徵點在圖像中的位置,有些還具備方向、尺度信息;描述子一般是一個向量,按照人爲的設計的方式,描述關鍵點周圍像素的信息。一般描述子是按照外觀類似的特徵應該有類似的描述子設計的。所以,在匹配的時候,只要兩個特徵點的描述子在向量空間的距離相近,就能夠認爲它們是同一個特徵點。後端

特徵點的匹配一般須要如下三個步驟:多線程

  • 提取圖像中的關鍵點,這部分是查找圖像中具備某些特徵(不一樣的算法有不一樣的)的像素
  • 根據獲得的關鍵點位置,計算特徵點的描述子
  • 根據特徵點的描述子,進行匹配

這裏先介紹下特徵點的描述子,一個好的描述子是準確匹配的基礎,關鍵點的提取和特徵點的匹配,在後面介紹。函數

特徵點描述子

從圖像中提取到特徵的關鍵點信息,一般只是其在圖像的位置信息(有可能包含尺度和方向信息),僅僅利用這些信息沒法很好的進行特徵點的匹配,因此就須要更詳細的信息,將特徵區分開來,這就是特徵描述子。另外,經過特徵描述子能夠消除視角的變化帶來圖像的尺度和方向的變化,可以更好的在圖像間匹配。學習

特徵的描述子一般是一個精心設計的向量,描述了關鍵點及其周圍像素的信息。爲了可以更好的匹配,一個好的描述子一般要具備如下特性:測試

  • 不變性 指特徵不會隨着圖像的放大縮小旋轉而改變。
  • 魯棒性 對噪聲、光照或者其餘一些小的形變不敏感
  • 可區分性 每個特徵描述子都是獨特的,具備排他性,儘量減小彼此間的類似性。

其中描述子的可區分性和其不變性是矛盾的,一個具備衆多不變性的特徵描述子,其區分局部圖像內容的能力就比較稍弱;而若是一個很容易區分不一樣局部圖像內容的特徵描述子,其魯棒性每每比較低。因此,在設計特徵描述子的時候,就須要綜合考慮這三個特性,找到三者之間的平衡。

特徵描述子的不變性主要體如今兩個方面:

  • 尺度不變性 Scale Invarient
    指的是同一個特徵,在圖像的不一樣的尺度空間保持不變。匹配在不一樣圖像中的同一個特徵點常常會有圖像的尺度問題,不一樣尺度的圖像中特徵點的距離變得不一樣,物體的尺寸變得不一樣,而僅僅改變特徵點的大小就有可能形成強度不匹配。若是描述子沒法保證尺度不變性,那麼同一個特徵點在放大或者縮小的圖像間,就不能很好的匹配。爲了保持尺度的不變性,在計算特徵點的描述子的時候,一般將圖像變換到統一的尺度空間,再加上尺度因子。
  • 旋轉不變性 Rotation Invarient
    指的是同一個特徵,在成像視角旋轉後,特徵仍然可以保持不變。和尺度不變性相似,爲了保持旋轉不變性,在計算特徵點描述子的時候,要加上關鍵點的方向信息。

爲了有個更直觀的理解,下面給出SIFT,SURF,BRIEF描述子計算方法對比

從上表能夠看出,SIFT,SURF和BRIEF描述子都是一個向量,只是維度不一樣。其中,SIFT和SURF在構建特徵描述子的時候,保存了特徵的方向和尺度特徵,這樣其特徵描述子就具備尺度和旋轉不變性;而BRIEF描述子並無尺度和方向特徵,不具有尺度和旋轉不變性。

經常使用的特徵點算法

上面提到圖像的特徵點包含兩個部分:

  • 特徵點的提取,在圖像檢測到特徵點的位置
  • 特徵點的描述,也就是描述子。

在圖像中提取到關鍵點的位置信息後,爲了可以更有效的匹配(主要是保證尺度和旋轉不變性),一般使用一個向量來描述關鍵點及其周圍的信息。特徵的描述子,在特徵點的匹配中是很是重要的,上一小節中對其應該具備的性質作了介紹。但具體到一個算法來講,可能其既有特徵點的提取算法也有特徵點描述子的算法,也有可能其僅僅是一個特徵點提取算法或者是特徵點的描述子算法。在本小節就經常使用的特徵點算法作一個簡要的說明。

SIFT

提到特徵點算法,首先就是大名鼎鼎的SIFT算法了。SIFT的全稱是Scale Invariant Feature Transform,尺度不變特徵變換,2004年由加拿大教授David G.Lowe提出的。SIFT特徵對旋轉、尺度縮放、亮度變化等保持不變性,是一種很是穩定的局部特徵。
SIFT算法主要有如下幾個步驟:

  • 高斯差分金字塔的構建
    使用組和層的結構構建了一個具備線性關係的金字塔(尺度空間),這樣能夠在連續的高斯核尺度上查找圖像的特徵點;另外,它使用一階的高斯差分來近似高斯的拉普拉斯核,大大的減小了運算量。
  • 尺度空間的極值檢測及特徵點的定位
    搜索上一步創建的高斯尺度空間,經過高斯差分來識別潛在的對尺度和旋轉不變的特徵點。可是,在離散空間中,局部極值點可能並非真正意義的極值點,真正的極值點有可能落在離散點的間隙中,SIFT經過尺度空間DoG函數進行曲線擬合尋找極值點。
  • 特徵方向賦值
    基於圖像局部的梯度方向,分配給每一個關鍵點位置一個或多個方向,後續的全部操做都是對於關鍵點的方向、尺度和位置進行變換,從而提供這些特徵的不變性。
  • 特徵描述子的生成
    經過上面的步驟已經找到的SIFT特徵點的位置、方向、尺度信息,最後使用一組向量來描述特徵點及其周圍鄰域像素的信息。

SIFT算法中及包含了特徵點的提取算法,也有如何生成描述子的算法,更進一步的SIFT算法介紹可參看SIFT特徵詳解

SURF

SURF全稱 Speeded Up Robust Features,是在SIFT算法的基礎上提出的,主要針對SIFT算法運算速度慢,計算量大的缺點進行了改進。
SURF的流程和SIFT比較相似,這些改進體如今如下幾個方面:

  • 特徵點檢測是基於Hessian矩陣,依據Hessian矩陣行列式的極值來定位特徵點的位置。而且將Hession特徵計算與高斯平滑結合在一塊兒,兩個操做經過近似處理獲得一個核模板。
  • 在構建尺度空間時,使用box filter與源圖像卷積,而不是使用DoG算子。
  • SURF使用一階Haar小波在x、y兩個方向的響應做爲構建特徵向量的分佈信息。

FAST特徵點提取算法

SIFT和SURF是很是好的,穩定的特徵點算法,但運算速度是其一大弊端,沒法作到實時的特徵提取和匹配,其應用就有了很大的侷限性。FAST特徵提取算法彌補了這一侷限,檢測局部像素灰度變化明顯的地方,以速度快而著稱,其全稱爲:Features From Accelerated Segment Test。在FAST算法的思想很簡單:若是一個像素與周圍鄰域的像素差異較大(過亮或者過暗),那麼能夠認爲該像素是一個角點。和其餘的特徵點提取算法相比,FAST算法只須要比較像素和其鄰域像素的灰度值大小,十分便捷。
FAST算法提取角點的步驟:

  • 在圖像中選擇像素p,假設其灰度值爲:\(I_p\)
  • 設置一個閾值T,例如:\(I_p\)的20%
  • 選擇p周圍半徑爲3的圓上的16個像素,做爲比較像素
  • 假設選取的圓上有連續的N個像素大於\(I_p + T\)或者\(I_p - T\),那麼能夠認爲像素p就是一個特徵點。(N一般取12,即爲FAST-12;經常使用的還有FAST-9,FAST-11)。

FAST算法只檢測像素的灰度值,其運算速度極快,同時不可避免的也有一些缺點

  • 檢測到的特徵點過多而且會出現「扎堆」的現象。這能夠在第一遍檢測完成後,使用非最大值抑制(Non-maximal suppression),在必定區域內僅保留響應極大值的角點,避免角點集中的狀況。
  • FAST提取到的角點沒有方向和尺度信息

上面的介紹的SIFT和SURF算法都包含有各自的特徵點描述子的計算方法,而FAST不包含特徵點描述子的計算,僅僅只有特徵點的提取方法,這就須要一個特徵點描述方法來描述FAST提取到的特徵點,以方便特徵點的匹配。下面介紹一個專門的特徵點描述子的計算算法。

BRIEF描述子

BRIEF是一種二進制的描述子,其描述向量是0和1表示的二進制串。0和1表示特徵點鄰域內兩個像素(p和q)灰度值的大小:若是p比q大則選擇1,反正就取0。在特徵點的周圍選擇128對這樣的p和q的像素對,就獲得了128維由0,1組成的向量。那麼p和q的像素對是怎麼選擇的呢?一般都是按照某種機率來隨機的挑選像素對的位置。
BRIEF使用隨機選點的比較,速度很快,並且使用二進制串表示最終生成的描述子向量,在存儲以及用於匹配的比較時都是很是方便的,其和FAST的搭配起來能夠組成很是快速的特徵點提取和描述算法。

ORB算法

ORB的全稱是Oriented FAST and Rotated BRIEF,是目前來講很是好的可以進行的實時的圖像特徵提取和描述的算法,它改進了FAST特徵提取算法,並使用速度極快的二進制描述子BRIEF。
針對FAST特徵提取的算法的一些肯定,ORB也作了相應的改進。

  • 使用非最大值抑制,在必定區域內僅僅保留響應極大值的角點,避免FAST提取到的角點過於集中。
  • FAST提取到的角點數量過多且不是很穩定,ORB中能夠指定須要提取到的角點的數量N,而後對FAST提取到的角點分別計算Harris響應值,選擇前N個具備最大響應值的角點做爲最終提取到的特徵點集合。
  • FAST提取到的角點不具備尺度信息,在ORB中使用圖像金字塔,而且在每一層金字塔上檢測角點,以此來保持尺度的不變性。
  • FAST提取到的角點不具備方向信息,在ORB中使用灰度質心法(Intensity Centroid)來保持特徵的旋轉不變性。

OpenCV3中特徵點的提取和匹配

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);
  1. 獲取檢測器的實例
    在OpenCV3中從新的封裝了特徵提取的接口,可統一的使用Ptr<FeatureDetector> detector = FeatureDetector::create()來獲得特徵提取器的一個實例,全部的參數都提供了默認值,也能夠根據具體的須要傳入相應的參數。
  2. 在獲得特徵檢測器的實例後,可調用的detect方法檢測圖像中的特徵點的具體位置,檢測的結果保存在vector<KeyPoint>向量中。
  3. 有了特徵點的位置後,調用compute方法來計算特徵點的描述子,描述子一般是一個向量,保存在Mat中。
  4. 獲得了描述子後,可調用匹配算法進行特徵點的匹配。上面代碼中,使用了opencv中封裝後的暴力匹配算法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)

  • KNN匹配
    K近鄰匹配,在匹配的時候選擇K個和特徵點最類似的點,若是這K個點之間的區別足夠大,則選擇最類似的那個點做爲匹配點,一般選擇K = 2,也就是最近鄰匹配。對每一個匹配返回兩個最近鄰的匹配,若是第一匹配和第二匹配距離比率足夠大(向量距離足夠遠),則認爲這是一個正確的匹配,比率的閾值一般在2左右。
    OpenCV中的匹配器中封裝了該方法,上面的代碼能夠調用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)匹配剔除。

  • RANSAC
    另外還可採用隨機採樣一致性(RANSAC)來過濾掉錯誤的匹配,該方法利用匹配點計算兩個圖像之間單應矩陣,而後利用重投影偏差來斷定某一個匹配是否是正確的匹配。OpenCV中封裝了求解單應矩陣的方法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++的內存泄漏和空指針致使的各類奇葩問題;知道了使用未初始化的變量的不穩定性;知道了項目設計中擴展性的重要的... 寫以前以爲本身虛度了半年,總結下來,這半年下來時間仍是成長了很多的,內心的愧疚感下降了很多。不過之後仍是要堅持寫博客記錄下學習的過程...

相關文章
相關標籤/搜索