1. 程式人生 > >單目視覺(5):SFM之特徵點匹配(四)

單目視覺(5):SFM之特徵點匹配(四)

SFM之特徵點匹配(四)

引入

在經過對每幅影象進行特徵提取之後,可以發現在一幅影象中存在非常多的特徵點(特殊情況下可能特徵點很少)。那麼如何去找出不同影象中的哪些特徵點反應在現實世界中是同一個物理座標呢?這需要做的工作就是對兩幅影象中的特徵點進行匹配。

相似性

如何判別分別在兩幅影象中的特徵點是同一個呢? 也就是說需要著一種方法或者標準來衡量兩個特徵點之間的相似程度。我們稱這種標準或方法為相似性度量。這個是可以通過自己定義的方法,並沒有一個統一的標準。因此,不同的度量標準對結果的精確性也有一定的影響。常用的相似性度量有各種距離,角度等。

匹配

匹配過程解決的是:在參考影象中的一個特徵點,如何從目標影象中的所有特徵點中找出與之相對應的特徵點。假設以常見的歐氏距離作為相似性度量標準。我們使用提取的特徵點(用特徵向量來描述),那麼計算歐式距離就是兩個點的距離。其”暴力“計算過程中,對於任意的兩個點,每一維都進行運算。那麼當維度很低,特徵點較少時,暴力計算也是可行的。但是,一旦維度很高,加之由於複雜場景導致的更多的特徵點數量,“暴力”計算是一個非常不明智的方法。因此,需要選在一種能有效減少計算複雜度和計算耗時的演算法。快速近鄰匹配(Approximate Nearest Neighbors Match)

的有一系列的辦法可以實現。其中很有效的方法是隨機K-d樹和優先搜尋K均值樹。在OpenCV中的FLANN(fast library for approximate nearest neighbors)集成了包括這兩種演算法在內的多種演算法。

K-d樹(K-dimensional Tree)

a. 經典KD樹
經典的kd樹是將資料集排列成一個類似二叉樹,每個節點表示超平面的一個區域範圍。在理想的情況下,二叉查詢樹的父節點與節點是靠近比較近的,而與其他節點的距離離得比較遠。那麼,在大量的資料中查詢某個特殊的值,只需要依靠二叉查詢樹就能很好的解決搜尋問題。因此,如何將所有的資料構造成一個較理想的類似於二叉查詢樹的結構?

一維的情況下,可以直接構造一個二叉查詢樹。在多維的情況下,我們不能直接的進行比較了。但是我們可以選擇依據其中的某一個維度先進行劃分,然後一次按照各個維度劃分下去,那麼就可以構造一個類似於二叉樹的結構。只不過此時構造的結構是在一個超平面(二維的話就是一個普通平面)中。

那麼,如何選擇先哪個維度再哪個維度呢?也就是維度的先後順序問題。各個維度的資料值我們可以看成一個數據子集,如果資料分佈的比較“散”,那麼各個資料的區分度就很明顯,那麼也更容易將這個子集進行劃分開來。回顧在統計理論中的知識,描述資料分佈集中程度的概念是方差。方差越大,資料分佈越“發散”;方差越小,資料分佈越“集中”。因此,可以參考這個方案,使用各個維度的方差來作為維度順序的選擇依據。因此,這個方法也稱為:最大方差法(max variance)。

接下來,選定好了維度的順序,該如何確定某個確定的值來作為“根(父)結點”呢?如果都選擇最大值,那麼所有資料都會在“根節點”的同一側。那麼也就這樣也會使得演算法的效能下降。回想在平衡二叉樹,其左子樹和右子樹的節點數目差不多,其效能較前一種二叉樹要好。因此,是否可以通過某種方法來確定一個值,是的最後劃分的結果中,兩側的資料量是差不多的,儘量避免出現“一邊偏”的情況。回顧統計理論中的知識,中值可以作為一個大概的分界線。因此,可以採用各個維度的中值來作為劃分的另一個依據。

以上,就可以歸納出經典K-d樹的基本演算法:
(1) 在資料集合中選擇具有最大方差的維度,然後在該維度上選擇中值對該資料集合進行劃分,得到兩個子集合;同時建立一個”父結點“,用於儲存相關值;

(2)對兩個子集合重複(1)步驟的過程,直至所有子集合都不能再劃分為止;如果某個子集合不能再劃分時,則將該子集合中的資料儲存到”葉結點“。

b. 隨機K-d森林
建立多棵隨機k-d樹,從具有最高方差的N_d維中隨機選取若干維度,用來做劃分。在對隨機k-d森林進行搜尋時候,所有的隨機k-d樹將共享一個優先佇列。 增加樹的數量能加快搜索速度,但由於記憶體負載的問題,樹的數量只能控制在一定範圍內,比如20,如果超過一定範圍,那麼搜尋速度不會增加甚至會減慢。

c.基於K-d樹最鄰近查詢演算法
(1)將查詢資料從根結點開始,與各個結點的比較結果向下訪問Kd-Tree,直至達到葉子結點。

其中該資料與結點的比較指的是將對應於結點中的k維度上的值與中值進行比較,若該資料小於中值,則訪問左子樹,否則訪問右子樹。達到葉子結點時,計算該資料與葉子結點上儲存的資料之間的距離,記錄下最小距離對應的資料點,記為當前“最近鄰點”和最小距離。

(2)進行回溯操作,該操作是為了找到離該資料更近的“最近鄰點”。即判斷未被訪問過的分支裡是否還有離該資料更近的點,它們之間的距離小於。

如果該資料與其父結點下的未被訪問過的分支之間的距離小於最小距離,則認為該分支中存在更近的資料,進入該結點,進行(1)步驟一樣的查詢過程,如果找到更近的資料點,則更新為當前的“最近鄰點”,並更新最小距離。

如果該資料與其父結點下的未被訪問過的分支之間的距離大於最小距離,則說明該分支內不存在與該資料更近的點。

誤匹配

錯誤匹配的出現會印象演算法的效能。因此在SFM中,常使用多種方式或約束來剔除誤匹配。常見的方法有RANSAC(隨機抽樣一致)。詳見:隨機抽樣一致性(RANSAC: Random Sample Consensus)

FLANN

FLANN(Fast Library for Approximate Nearest Neighbors)是一個執行快速近似最近鄰搜尋的庫。FLANN使用C++寫成。他能夠很容易地通過C,MTALAB和Python等繫結提供的庫,用在很多環境中。

利用FLANN進行特徵點匹配

官方示例程式,使用 FlannBasedMatcher 介面以及函式 FLANN 實現快速高效匹配。
這段程式碼的主要流程分為以下幾部分:

1)使用SURF特徵提取關鍵點
2)計算SURF特徵描述子
3)使用FLANN匹配器進行描述子向量匹配

OpenCV提供了 兩種KeyPoint Matching的方式 :

 Brute-force matcher (cv::BFMatcher) //Brute-force matcher就是用暴力方法找到點集一中每個descriptor在點集二中距離最近的 descriptor;
 Flann-based matcher (cv::FlannBasedMatcher) //Flann-based matcher 使用快速近似最近鄰搜尋演算法尋找。

為了提高檢測速度,你可以呼叫matching函式前,先訓練一個matcher。訓練過程可以首先使用cv:: FlannBasedMatcher來優化,為 descriptor建立索引樹,這種操作將在匹配大量資料時發揮巨大作用(比如在上百幅影象的資料集中查詢匹配影象)。而 Brute-force matcher在這個過程並不進行操作,它只是將train descriptors儲存在記憶體中。

#include <stdio.h>
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/nonfree/features2d.hpp"

using namespace cv;

/** @function main */
int main( int argc, char** argv )
{
    Mat img_1 = imread("box.png", CV_LOAD_IMAGE_GRAYSCALE );
    Mat img_2 = imread("box_in_scene.png", CV_LOAD_IMAGE_GRAYSCALE );

    if( !img_1.data || !img_2.data )
    { std::cout<< " --(!) Error reading images " << std::endl; return -1; }

    //-- Step 1: Detect the keypoints using SURF Detector
    int minHessian = 400;

    SurfFeatureDetector detector( minHessian );

    std::vector<KeyPoint> keypoints_1, keypoints_2;

    detector.detect( img_1, keypoints_1 );
    detector.detect( img_2, keypoints_2 );

    //-- Step 2: Calculate descriptors (feature vectors)
    SurfDescriptorExtractor extractor;

    Mat descriptors_1, descriptors_2;

    extractor.compute( img_1, keypoints_1, descriptors_1 );
    extractor.compute( img_2, keypoints_2, descriptors_2 );

    //-- Step 3: Matching descriptor vectors using FLANN matcher
    FlannBasedMatcher matcher;
    std::vector< DMatch > matches;
    matcher.match( descriptors_1, descriptors_2, matches );

    double max_dist = 0; double min_dist = 100;

    //-- Quick calculation of max and min distances between keypoints
    for( int i = 0; i < descriptors_1.rows; i++ )
    { double dist = matches[i].distance;
    if( dist < min_dist ) min_dist = dist;
    if( dist > max_dist ) max_dist = dist;
    }

    printf("-- Max dist : %f \n", max_dist );
    printf("-- Min dist : %f \n", min_dist );

    //-- Draw only "good" matches (i.e. whose distance is less than 2*min_dist )
    //-- PS.- radiusMatch can also be used here.
    std::vector< DMatch > good_matches;

    for( int i = 0; i < descriptors_1.rows; i++ )
    { if( matches[i].distance < 2*min_dist )
    { good_matches.push_back( matches[i]); }
    }

    //-- Draw only "good" matches
    Mat img_matches;
    drawMatches( img_1, keypoints_1, img_2, keypoints_2,
        good_matches, img_matches, Scalar::all(-1), Scalar::all(-1),
        vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );

    //-- Show detected matches
    imshow( "Good Matches", img_matches );

    for( int i = 0; i < good_matches.size(); i++ )
    { printf( "-- Good Match [%d] Keypoint 1: %d  -- Keypoint 2: %d  \n", i, good_matches[i].queryIdx, good_matches[i].trainIdx ); }

    waitKey(0);

    return 0;
}

Reference