一、什么是特征?
图像的特征(Feature),是图像上最具代表性的一些点。所谓最具代表性,就是说这些点包含了图像表述的大部分信息。即使旋转、缩放,甚至调整图像的亮度,这些点仍然稳定地存在,不会丢失。找出这些点,就相当于确定了这张图像,它们可以用来做匹配、识别等等有意义的工作。
通常来说,角点容易成为特征点。角点是指图案中处于角落的点,这些点附近像素值变化剧烈,因而容易被检测出。
特征点由关键点(Key-point)和描述子(Descriptor)两部分组成。比如,提取ORB特征其实包括了提取关键点和计算描述子两件事情。
历史上,SIFT特征和SURF特征都是非常常用的特征,SIFT特征以精确著称,但计算量极大,无法在CPU上实时计算。SURF特征降低了SIFT的精确度,但提高了性能。而ORB特征是目前计算最快的特征,非常适合于实时SLAM,因此受到广大研究者的喜爱。
二、ORB特征
ORB全名为Oriented FAST and Rotated BRIEF,它采用改进的FAST关键点检测方法,使其具有方向性,并采用具有旋转不变性的BRIEF特征描述子。FAST和BRIEF都是非常快速的特征计算方法,因此ORB具有非同一般的性能优势。
1.FAST关键点
要想判断一个像素点p是不是FAST关键点,只需要判断其周围的16个像素点中是否有连续N个点的灰度值与p的差超出阈值。16个点的位置如下图所示。N一般取12,称为FAST-12,常用的还有FAST-9,FAST-11。阈值一般为p点灰度值的20%。
找出关键点后,还要计算该特征的方向,使用灰度质心法实现。灰度质心是指一小块图像中以每个像素的灰度值作为权重计算加权后的中心点。在上图以p点为中心的小块区域中,根据各个点的灰度值可以计算出一个灰度质心,通常不与p点重合,这样从p点到灰度质心的连线就是这个特征点的方向。
2.BRIEF描述子
BRIEF描述子是一种二进制描述子,通常为128位的二进制串。它的计算方法是从关键点p周围随机挑选128个点对,对于每个点对中的两个点,如果前一个点的灰度值大于后一个点,则取1,反之取0。
为了降低噪声的干扰,计算BRIEF描述子前需要对图像进行一次平滑处理,BRIEF的作者建议使用9×9的高斯核进行平滑。随机挑选的128个点对也建议服从高斯分布。
ORB的作者在BRIEF的基础上利用特征的方向改进了BRIEF描述子,使其具有旋转不变性。
更详细的ORB特征介绍见参考资料。
三、OpenCV提取ORB特征并匹配
OpenCV真是个强大的计算机视觉库,所有能想到的特征点提取算法里面都提供了。包括提取特征后的匹配算法也都提供了。所以下面的程序非常简单。
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main ( int argc, char** argv )
{
if ( argc != 3 )
{
cout<<"usage: feature_extraction img1 img2"<<endl;
return 1;
}
//-- 读取图像
Mat img_1 = imread ( argv[1], CV_LOAD_IMAGE_COLOR );
Mat img_2 = imread ( argv[2], CV_LOAD_IMAGE_COLOR );
//-- 初始化
std::vector<KeyPoint> keypoints_1, keypoints_2;
Mat descriptors_1, descriptors_2;
Ptr<FeatureDetector> detector = ORB::create();
Ptr<DescriptorExtractor> descriptor = ORB::create();
// Ptr<FeatureDetector> detector = FeatureDetector::create(detector_name);
// Ptr<DescriptorExtractor> descriptor = DescriptorExtractor::create(descriptor_name);
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create ( "BruteForce-Hamming" );
//-- 第一步:检测 Oriented FAST 角点位置
detector->detect ( img_1,keypoints_1 );
detector->detect ( img_2,keypoints_2 );
//-- 第二步:根据角点位置计算 BRIEF 描述子
descriptor->compute ( img_1, keypoints_1, descriptors_1 );
descriptor->compute ( img_2, keypoints_2, descriptors_2 );
Mat outimg1;
drawKeypoints( img_1, keypoints_1, outimg1, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
imshow("ORB特征点",outimg1);
//-- 第三步:对两幅图像中的BRIEF描述子进行匹配,使用 Hamming 距离
vector<DMatch> matches;
matcher->match ( descriptors_1, descriptors_2, matches );
//-- 第四步:匹配点对筛选
double min_dist=10000, max_dist=0;
//找出所有匹配之间的最小距离和最大距离, 即是最相似的和最不相似的两组点之间的距离
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;
}
// 仅供娱乐的写法
min_dist = min_element( matches.begin(), matches.end(), [](const DMatch& m1, const DMatch& m2) {return m1.distance<m2.distance;} )->distance;
max_dist = max_element( matches.begin(), matches.end(), [](const DMatch& m1, const DMatch& m2) {return m1.distance<m2.distance;} )->distance;
printf ( "-- Max dist : %f \n", max_dist );
printf ( "-- Min dist : %f \n", min_dist );
//当描述子之间的距离大于两倍的最小距离时,即认为匹配有误.但有时候最小距离会非常小,设置一个经验值30作为下限.
std::vector< DMatch > good_matches;
for ( int i = 0; i < descriptors_1.rows; i++ )
{
if ( matches[i].distance <= max ( 2*min_dist, 30.0 ) )
{
good_matches.push_back ( matches[i] );
}
}
//-- 第五步:绘制匹配结果
Mat img_match;
Mat img_goodmatch;
drawMatches ( img_1, keypoints_1, img_2, keypoints_2, matches, img_match );
drawMatches ( img_1, keypoints_1, img_2, keypoints_2, good_matches, img_goodmatch );
imshow ( "所有匹配点对", img_match );
imshow ( "优化后匹配点对", img_goodmatch );
waitKey(0);
return 0;
}
需要注意一下这里的匹配策略。从所有描述子中找两两距离最近的配对,配对后还需要进行一次筛选。这是因为所有特征点都参与了配对,但实际上有些特征点可能并没有在两张图中同时出现,因此误配对的情况还是很多的。筛选策略是,当描述子之间的距离大于所有配对的描述子的最小距离的两倍时,认为匹配有误。同时,还需要设置一个经验值30,以防最小距离过小。
以下三幅图分别是提取的ORB特征点、所有匹配点对和筛选后匹配点对。
可以看到,筛选前误匹配非常多,筛选后基本得到了正确的匹配结果。
代码下载地址:https://github.com/jingedawang/FeatureMethod
参考资料
《视觉SLAM十四讲》第7讲 视觉里程计1 高翔
ORB算法原理解读 yang843061497