圖像的特徵點指的就是用圖像上的一些有代表性的點來表示這張圖像,這些特徵點在相機發生少許的位移、旋轉和尺度縮放時保持不變。圖像特徵點提取與匹配是單目視覺SLAM中關鍵的一步,同時也在很多其他的地方有應用,比如圖像拼接、三維重建等等。
常見的特徵點描述有:ORB、SIFT、SURF等。本文主要介紹ORB特徵點,並通過實例如何使用圖像處理庫OpenCV進行圖像的特徵點提取和匹配。
1、ORB特徵描述
ORB特徵由關鍵點和描述子兩部分組成。它的關鍵點稱為"Oriented FAST",是一種改進的FAST角點;描述子稱為BRIEF,用來描述關鍵點周圍的像素。
FAST角點
FAST角點主要用來檢測局部像素灰度變化明顯的地方,它的思想是:如果一個像素與它領域的像素差別較大(過亮或者過暗),那麼它就有可能是角點。
FAST角點的檢測過程:
- 在圖像中選取像素p,假設它的亮度為Ip
- 設定一個閾值T(比如Ip的20%)
- 以像素p為圓心,選取半徑為3的圓上的16個像素點
- 假如選取的圓上的16個像素點中,有 連續的N個像素點的亮度大於Ip+T或Ip−T,那麼就認為像素p是角點(N通常取12,對應的特徵點稱為FAST-12)
- 循環上面的步驟1-4,對每一個像素執行相同的操作
FAST角點檢測預測試: 對於每個像素,先直接檢測圓上的第1、5、9、13個像素的亮度,只有當這4個像素中有3個同時大於Ip+T或Ip−T,它才有可能是一個FAST角點;否則,將該像素直接排除。
非極大值抑制避免角點扎堆: 在一定區域內保留相應極大值的角點,避免角點集中。
FAST角點的缺陷:
- 特徵點數量很大且不確定(ORB對其做了改進)
- 對旋轉和尺度不具有描述性
(1-2) ORB使用的改進的FAST角點
添加對尺度的描述: 對圖像構建金字塔,並在金字塔的每一層上檢測角點
添加對方向的描述: 計算區塊的灰度質心,與區塊的幾何質心形成方向向量,得到特徵點的方向具體的過程如下:在一個小的圖像塊B中,定義圖像塊的矩為:於是圖像塊的灰度質心為:
連接圖像的幾何中心O和質心C, 得到方向向量OC,於是特徵點的方向為:
(1-3) ORB所使用的描述子BRIEF
Brief描述子是一種二進制描述子,它是由128個0/1所組成的描述向量。0/1編碼了關鍵點附近兩個像素(比如p、q)的大小關係,如果p>q,則取1;否則取0。一共取了128對這樣的p、q。
原始的Brief描述子是不具有旋轉不變性的,因為在提取FAST關鍵點的時候已經計算出來了關鍵點的方向,所以可以利用方向信息,計算旋轉之後的"Steer Brief"特徵,使用Brief具有較好的旋轉不變性。
綜上所述,ORB在平移、旋轉、縮放的變換下都具有良好的性能。
2、ORB特徵匹配
暴力匹配: 將圖像1的所有特徵點依次和圖像2的所有特徵點的描述子計算距離,然後排序,取最近的一個作為匹配點。
若描述子為浮點類型,則使用歐式距離;對於ORB使用的Brief描述子(二進制),使用漢明距離,指的是不同位數的個數。
暴力匹配的結果中含有很多的誤匹配結果,在實際使用時經常要採取一些方法去除誤匹配,最好用的是RANSAC算法。但是下面的例子中只是通過最小距離篩選了一次,不適用所有的情況。
3、ORB特徵提取與匹配實踐
<code>#include #include #include using namespace std; void drawKeyPoints(const cv::Mat& image, const vector& keypoints) { cv::Mat destImage = image.clone(); for (int i = 0; i < keypoints.size(); i++) { cv::Point2f p = keypoints[i].pt; cv::circle(destImage, p, 2, cv::Scalar(0, 0, 255), 1); } cv::namedWindow("Keypoints", cv::WINDOW_NORMAL); cv::imshow("Keypoints", destImage); cv::waitKey(0); cv::destroyWindow("Keypoints"); } void drawMatchKeyPoints( const cv::Mat& srcImage, const vector& kps1, const cv::Mat& dstImage, const vector& kps2, const vector& matches) { cv::Mat srcImageCopy = srcImage.clone(); cv::Mat dstImageCopy = dstImage.clone(); cv::Mat matchImage(srcImageCopy.rows, srcImageCopy.cols * 2,srcImageCopy.type()); cv::Mat left = cv::Mat(matchImage, cv::Rect(0, 0, srcImageCopy.cols, srcImageCopy.rows)); cv::Mat right = cv::Mat(matchImage, cv::Rect(srcImageCopy.cols, 0, dstImageCopy.cols, dstImage.rows)); srcImageCopy.copyTo(left); dstImageCopy.copyTo(right); for(int i=0; i& keypoints, cv::Mat& desc) { cv::Ptr extractor = cv::ORB::create(); auto detectStart = chrono::high_resolution_clock::now(); extractor->detect(image, keypoints); auto detectEnd = chrono::high_resolution_clock::now(); double detectTime = chrono::duration_cast(detectEnd - detectStart).count(); cout << "detectTime: " << detectTime << "ms..." << endl; cv::Ptr compute = cv::ORB::create(); auto computeStart = chrono::high_resolution_clock::now(); compute->compute(image, keypoints, desc); auto computeEnd = chrono::high_resolution_clock::now(); double computeTime = chrono::duration_cast(computeEnd - computeStart).count(); cout << "computeTime: " << computeTime << "ms..." << endl; } int matchKeyPoints( const cv::Mat& image1, const vector& keypoint1, const cv::Mat& desc1, const cv::Mat& image2, const vector& keypoint2, const cv::Mat& desc2, vector& matchesNoFilter, vector& matchesFilter) { // 創建使用"漢明距離"的暴力匹配器 cv::Ptr matcher = cv::DescriptorMatcher::create("BruteForce-Hamming"); matcher->match(desc1, desc2, matchesNoFilter); // lamda表達式 auto getMinimumDistance = [](const cv::Mat& desc, const vector& matches) -> double { double min_dis = 10000.; for(int i = 0; i < desc.rows; i++) { min_dis = min_dis < matches[i].distance ? min_dis : matches[i].distance; } return min_dis; }; double min_dist = getMinimumDistance(desc1, matchesNoFilter); // lamda表達式 auto getRightMatches = [](const double min_dis, const vector& matches, vector& goodMatches) -> void { for(int i = 0; i < matches.size(); i++) { //當描述子之間的距離大於兩倍的最小距離時,即認為匹配有誤.但有時候最小距離會非常小,設置一個經驗值30作為下限. if(matches[i].distance <= max(2*min_dis, 30.0)) goodMatches.push_back(matches[i]); } }; getRightMatches(min_dist, matchesNoFilter, matchesFilter); return 0; } int main() { cv::Mat image1 = cv::imread("1.png"); cv::Mat image2 = cv::imread("2.png"); if(image1.empty() || image2.empty()) { cout << "Load image failed..." << endl; return -1; } vector keypoint1, keypoint2; cv::Mat desc1, desc2; // 特徵點提取 extractKeypoints(image1, keypoint1, desc1); drawKeyPoints(image1, keypoint1); extractKeypoints(image2, keypoint2, desc2); drawKeyPoints(image2, keypoint2); // 特徵點匹配 vector matchesNoFilter, matchesFilter; matchKeyPoints(image1, keypoint1, desc1, image2, keypoint2, desc2, matchesNoFilter, matchesFilter); drawMatchKeyPoints(image1, keypoint1, image2, keypoint2, matchesNoFilter); drawMatchKeyPoints(image1, keypoint1, image2, keypoint2, matchesFilter); return 0; }/<code>
結果:
特徵點提取的結果:
特徵點匹配後沒有過濾誤匹配的結果:
過濾了誤匹配之後特徵點匹配結果:
得到了正確的匹配點之後,就可以根據這些匹配的點做很多其他的事情了,比如說估計位姿、三角測量等等,這些主要是在視覺SLAM中的應用。
今天的內容就到這兒了。如果對我的推、文有興趣,歡迎轉、載分、享。也可以推薦給朋友關、注哦。只推乾貨,寧缺毋濫。