OpenCV2學習筆記(十三):基於SURF特徵的影象匹配
SURF演算法是著名的尺度不變特徵檢測器SIFT(Scale-Invariant Features Transform)的高效變種,它為每個檢測到的特徵定義了位置和尺度,其中尺度的值可用於定義圍繞特徵點的視窗大小,使得每個特徵點都與眾不同。這裡便是使用SURF演算法提取兩幅影象中的特徵點描述子,並呼叫OpenCV中的函式進行匹配,最後輸出一個視覺化的結果,開發平臺為Qt5.3.2+OpenCV2.4.9。以下給出影象匹配的實現步驟:
一、輸入兩幅影象,使用OpenCV中的cv::FeatureDetector介面實現SURF特徵檢測,在實際除錯中改變閾值可獲得不一樣的檢測結果:
// 設定兩個用於存放特徵點的向量
std::vector<cv::KeyPoint> keypoint1;
std::vector<cv::KeyPoint> keypoint2;
// 構造SURF特徵檢測器
cv::SurfFeatureDetector surf(3000); // 閾值
// 對兩幅圖分別檢測SURF特徵
surf.detect(image1,keypoint1);
surf.detect(image2,keypoint2);
二、OpenCV 2.0版本中引入一個通用類,用於提取不同的特徵點描述子。在這裡構造一個SURF描述子提取器,輸出的結果是一個矩陣,它的行數與特徵點向量中的元素個數相同。每行都是一個N維描述子的向量。在SURF演算法中,預設的描述子維度為64,該向量描繪了特徵點周圍的強度樣式。
cv::SurfDescriptorExtractor surfDesc;
// 對兩幅影象提取SURF描述子
cv::Mat descriptor1, descriptor2;
surfDesc.compute(image1,keypoint1,descriptor1);
surfDesc.compute(image2,keypoint2,descriptor2);
提取出兩幅影象各自的特徵點描述子後,需要進行比較(匹配)。可以呼叫OpenCV中的類cv::BruteForceMatcher構造一個匹配器。cv::BruteForceMatcher是類cv::DescriptorMatcher的一個子類,定義了不同的匹配策略的共同介面,結果返回一個cv::DMatch向量,它將被用於表示一對匹配的描述子。(關於cv::BruteForceMatcher 請參考:
三、在一批特徵點匹配結果中篩選出評分(或者稱距離)最理想的25個匹配結果,這通過std::nth_element實現。
void nth_element(_RandomAccessIterator _first, _RandomAccessIterator _nth, _RandomAccessIterator _last)
該函式的作用為將迭代器指向的從_first 到 _last 之間的元素進行二分排序,以_nth 為分界,前面都比 _Nth 小(大),後面都比之大(小),因此適用於找出前n個最大(最小)的元素。
四、最後一步,將匹配的結果視覺化。OpenCV提供一個繪製函式以產生由兩幅輸入影象拼接而成的影象,而匹配的點由直線相連:
// 以下操作將匹配結果視覺化
cv::Mat imageMatches;
cv::drawMatches(image1,keypoint1, // 第一張圖片和檢測到的特徵點
image2,keypoint2, // 第二張圖片和檢測到的特徵點
matches, // 輸出的匹配結果
imageMatches, // 生成的影象
cv::Scalar(128,128,128)); // 畫直線的顏色
要注意SIFT、SURF的函式在OpenCV的nonfree模組中而不是features2d,cv::BruteForceMatcher類存放在legacy模組中,因此函式中需要包含標頭檔案:
#include <opencv2/legacy/legacy.hpp>
#include <opencv2/nonfree/nonfree.hpp>
完整程式碼如下:
#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/legacy/legacy.hpp>
#include <opencv2/nonfree/nonfree.hpp>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 以下兩圖比之
// 輸入兩張要匹配的圖
cv::Mat image1= cv::imread("c:/Fig12.18(a1).jpg",0);
cv::Mat image2= cv::imread("c:/Fig12.18(a2).jpg",0);
if (!image1.data || !image2.data)
qDebug() << "Error!";
cv::namedWindow("Right Image");
cv::imshow("Right Image", image1);
cv::namedWindow("Left Image");
cv::imshow("Left Image", image2);
// 存放特徵點的向量
std::vector<cv::KeyPoint> keypoint1;
std::vector<cv::KeyPoint> keypoint2;
// 構造SURF特徵檢測器
cv::SurfFeatureDetector surf(3000); // 閾值
// 對兩幅圖分別檢測SURF特徵
surf.detect(image1,keypoint1);
surf.detect(image2,keypoint2);
// 輸出帶有詳細特徵點資訊的兩幅影象
cv::Mat imageSURF;
cv::drawKeypoints(image1,keypoint1,
imageSURF,
cv::Scalar(255,255,255),
cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
cv::namedWindow("Right SURF Features");
cv::imshow("Right SURF Features", imageSURF);
cv::drawKeypoints(image2,keypoint2,
imageSURF,
cv::Scalar(255,255,255),
cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
cv::namedWindow("Left SURF Features");
cv::imshow("Left SURF Features", imageSURF);
// 構造SURF描述子提取器
cv::SurfDescriptorExtractor surfDesc;
// 對兩幅影象提取SURF描述子
cv::Mat descriptor1, descriptor2;
surfDesc.compute(image1,keypoint1,descriptor1);
surfDesc.compute(image2,keypoint2,descriptor2);
// 構造匹配器
cv::BruteForceMatcher< cv::L2<float> > matcher;
// 將兩張圖片的描述子進行匹配,只選擇25個最佳匹配
std::vector<cv::DMatch> matches;
matcher.match(descriptor1, descriptor2, matches);
std::nth_element(matches.begin(), // 初始位置
matches.begin()+24, // 排序元素的位置
matches.end()); // 終止位置
// 移除25位後的所有元素
matches.erase(matches.begin()+25, matches.end());
// 以下操作將匹配結果視覺化
cv::Mat imageMatches;
cv::drawMatches(image1,keypoint1, // 第一張圖片和檢測到的特徵點
image2,keypoint2, // 第二張圖片和檢測到的特徵點
matches, // 輸出的匹配結果
imageMatches, // 生成的影象
cv::Scalar(128,128,128)); // 畫直線的顏色
cv::namedWindow("Matches"); //, CV_WINDOW_NORMAL);
cv::imshow("Matches",imageMatches);
return a.exec();
}
效果一,由於原圖中飛機的邊緣有鋸齒狀,因此只需觀察拐角處,匹配效果良好:
效果二,不涉及影象的旋轉和變形,只是將一幅影象進行縮放後進行匹配,得出的效果自然是很好:
效果三,用兩個不同的角度拍攝的影象進行匹配,其中部分特徵點匹配有偏差,總體效果良好,在除錯過程中還可以通過引數調整獲取更好的匹配效果。
附註:另一種匹配方法是使用 cv::FlannBasedMatcher 介面以及函式 FLANN 實現快速高效匹配(快速最近鄰逼近搜尋函式庫(Fast Approximate Nearest Neighbor Search Library))。網上有原始碼例程如下:
#include <stdio.h>
#include <iostream>
#include "opencv2/core/core.hpp"
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/legacy/legacy.hpp>
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
void readme();
/** @function main */
int main( int argc, char** argv )
{
if( argc != 3 )
{ readme(); return -1; }
Mat img_1 = imread( argv[1], CV_LOAD_IMAGE_GRAYSCALE );
Mat img_2 = imread( argv[2], 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;
}
/** @function readme */
void readme()
{ std::cout << " Usage: ./SURF_FlannMatcher <img1> <img2>" << std::endl; }
以上只是記錄這種方法的實現例程,並沒有驗證程式碼的正確性。
參考資料: