目录
一、OpenCV的 ORB特征
二、手写ORB特征
1.Keypoint类
opencv中Keypoint类的默认构造函数为:
CV_WRAP KeyPoint() : pt(0,0), size(0), angle(-1), response(0), octave(0), class_id(-1) {}
pt(x,y):关键点的点坐标,是像素坐标;
size():该关键点邻域直径大小;angle:角度,表示关键点的方向,值为[0,360),负值表示不使用。 response:响应强度,选择响应最强的关键点; octacv:从哪一层金字塔得到的此关键点。 class_id:当要对图片进行分类时,用class_id对每个关键点进行区分,默认为-1。
2.Ptr智能指针模板类
Ptr detector = ORB::create();
//等价于
FeatureDetector * detector = ORB::create();
//创建一个Ptr类型的detector,用于接收ORB类中create()函数的返回值
Ptr是OpenCV中使用的智能指针模板类,可以轻松管理各种类型的指针。
3. ORB::create()函数
opencv中的ORB类是一个纯虚类,继承于Feature2D类,无法进行实例化创建对象。提供静态成员函数ORB::create()供调用。
static Ptr< ORB > create (int nfeatures=500, float scaleFactor=1.2f,
int nlevels=8, int edgeThreshold=31, int firstLevel=0, int WTA_K=2,
int scoreType=ORB::HARRIS_SCORE, int patchSize=31, int fastThreshold=20)
nfeatures - 最多提取的特征点的数量;
scaleFactor - 金字塔图像之间的尺度参数;
nlevels – 高斯金字塔的层数;
edgeThreshold – 边缘阈值,这个值主要是根据后面的patchSize来定的,靠近边缘edgeThreshold以内的像素是不检测特征点的。
firstLevel - 第一层的索引值,这里默认为0。
WET_K - 用于产生BIREF描述子的 点对的个数,一般为2个,也可以设置为3个或4个,那么这时候描述子之间的距离计算就不能用汉明距离了,而是应该用一个变种。OpenCV中,如果设置WET_K = 2,则选用点对就只有2个点,匹配的时候距离参数选择NORM_HAMMING,如果WET_K设置为3或4,则BIREF描述子会选择3个或4个点,那么后面匹配的时候应该选择的距离参数为NORM_HAMMING2。
scoreType - 用于对特征点进行排序的算法,你可以选择HARRIS_SCORE,也可以选择FAST_SCORE,但是它也只是比前者快一点点而已。
patchSize – 用于计算BIREF描述子的特征点邻域大小。
4. 特征检测器FeatureDetector 和 描述子提取器DescriptorExtractor类 和 描述子匹配器DescriptorMatcher
特征检测器FeatureDetector是虚类,通过定义FeatureDetector的对象可以使用多种特征检测及匹配方法,通过create()函数调用。
class CV_EXPORTS FeatureDetector
{
public:
virtual ~FeatureDetector();
void detect( const Mat& image, vector& keypoints,
const Mat& mask=Mat() ) const;
void detect( const vector& images,
vector >& keypoints,
const vector& masks=vector() ) const;
virtual void read(const FileNode&);
virtual void write(FileStorage&) const;
static Ptr create( const string& detectorType );
protected:
...
};
描述子提取器DescriptorExtractor是提取关键点的描述子抽象基类。
class CV_EXPORTS DescriptorExtractor
{
public:
virtual ~DescriptorExtractor();
void compute( const Mat& image, vector& keypoints,
Mat& descriptors ) const;
void compute( const vector& images, vector >& keypoints,
vector& descriptors ) const;
virtual void read( const FileNode& );
virtual void write( FileStorage& ) const;
virtual int descriptorSize() const = 0;
virtual int descriptorType() const = 0;
static Ptr create( const string& descriptorExtractorType );
protected:
...
};
描述子匹配器DescriptorMatcher用于特征关键点描述子匹配的抽象基类.
有两类匹配任务:匹配两个图像之间的特征描述子,或者匹配一个图像与另外一个图像集的特征描述子,
"BruteForce-Hamming"表示使用汉明距离进行匹配。
class cv::DescriptorMatcher {
public:
virtual void add(InputArrayOfArrays descriptors); // Add train descriptors
virtual void clear(); // Clear train descriptors
virtual bool empty() const; // true if no descriptors
void train(); // Train matcher
virtual bool isMaskSupported() const = 0; // true if supports masks
const vector& getTrainDescriptors() const; // Get train descriptors
// methods to match descriptors from one list vs. "trained" set (recognition)
void match(
InputArray queryDescriptors,
vector& matches,
InputArrayOfArrays masks = noArray()
);
void knnMatch(
InputArray queryDescriptors,
vector< vector >& matches,
int k,
InputArrayOfArrays masks = noArray(),
bool compactResult = false
);
void radiusMatch(
InputArray queryDescriptors,
vector< vector >& matches,
float maxDistance,
InputArrayOfArrays masks = noArray(),
bool compactResult = false
);
// methods to match descriptors from two lists (tracking)
//
// Find one best match for each query descriptor
void match(
InputArray queryDescriptors,
InputArray trainDescriptors,
vector& matches,
InputArray mask = noArray()
) const;
// Find k best matches for each query descriptor (in increasing order of distances)
void knnMatch(
InputArray queryDescriptors,
InputArray trainDescriptors,
vector< vector >& matches,
int k,
InputArray mask = noArray(),
bool compactResult = false
) const;
// Find best matches for each query descriptor with distance less than maxDistance
void radiusMatch(
InputArray queryDescriptors,
InputArray trainDescriptors,
vector< vector >& matches,
float maxDistance,
InputArray mask = noArray(),
bool compactResult = false
) const;
virtual void read(const FileNode&); // Reads matcher from a file node
virtual void write(FileStorage&) const; // Writes matcher to a file storage
virtual cv::Ptr clone(
bool emptyTrainData = false
) const = 0;
static cv::Ptr create(
const string& descriptorMatcherType
);
...
};
每个函数及其参数的意思直接看文档吧。重点看下create()和match()即可
4.ORB特征检测与匹配步骤如下:
第一步: 检测Oriented FAST角点位置,使用detect()函数
CV_WRAP virtual void detect( InputArray image,
CV_OUT std::vector& keypoints,
InputArray mask=noArray() );
detect()方法的原型参数:需要检测的图像,关键点数组,第三个参数为默认值
第二步:根据角点位置计算BRIEF描述子 ,使用compute()函数
CV_WRAP virtual void compute( InputArray image,
CV_OUT CV_IN_OUT std::vector& keypoints,
OutputArray descriptors );
compute()原型参数:图像,图像的关键点数组,Mat类型的描述子
关于使用该函数得到的描述子为什么是Mat矩阵?
用矩阵用来存储n行(假设有n个特征点)32列(ORB特征)的描述子,即每行代表一个特征点的描述子。
第三步:定义输出标记关键点的图像,使用drawKeypoints()函数
CV_EXPORTS_W void drawKeypoints( InputArray image,
const std::vector& keypoints,
InputOutputArray outImage,
const Scalar& color=Scalar::all(-1),
int flags=DrawMatchesFlags::DEFAULT );
drawKeypoints()函数原型参数:原图,原图关键点,带有关键点的输出图像,后面两个为默认值
后两个参数
第四步:对两幅图像中的BRIEF描述子进行匹配,使用match()函数
void DescriptorMatcher::match(
const Mat& queryDescriptors,
const Mat& trainDescriptors,
vector& matches,
const Mat& mask=Mat() ) const
queryDescriptors – 特征描述子查询集.
trainDescriptors – 待训练的特征描述子集.
matches –匹配点数组,元素类型为DMatch
mask – 特定的在输入查询和训练特征描述子集之间的Mask permissible匹配.
DMatch类 (在OpenCV2.x中Dmatch为结构体数据类型,在OpenCV3.x中为Class类型)
class CV_EXPORTS_W_SIMPLE DMatch
{
public:
CV_WRAP DMatch();
CV_WRAP DMatch(int _queryIdx, int _trainIdx, float _distance);
CV_WRAP DMatch(int _queryIdx, int _trainIdx, int _imgIdx, float _distance);
CV_PROP_RW int queryIdx; // query descriptor index
CV_PROP_RW int trainIdx; // train descriptor index
CV_PROP_RW int imgIdx; // train image index
CV_PROP_RW float distance;
// less is better
bool operator<( const DMatch &m ) const
{
return distance < m.distance;
}
};
前三行是构造函数,
int queryIdx –>query描述子下标,即match函数中位于前面的描述子
int trainIdx –> train描述子下标,match函数中位于后面的描述子
int imgIdx –>训练图像的索引(若有多个才有用)
float distance –>两个特征向量之间的欧氏距离,越小表明匹配度越高。
最后,是一个小于操作符的重载,用于比较和排序。 比较的是上述的distance,
queryIdx是图1中匹配的关键点的对应编号
trainIdx是图2中匹配的关键点的对应编号
画出所有的匹配结果,使用drawMatches()函数
CV_EXPORTS_W void drawMatches( InputArray img1,
const std::vector& keypoints1,
InputArray img2,
const std::vector& keypoints2,
const std::vector& matches1to2,
InputOutputArray outImg,
const Scalar& matchColor=Scalar::all(-1),
const Scalar& singlePointColor=Scalar::all(-1),
const std::vector& matchesMask=std::vector(),
int flags=DrawMatchesFlags::DEFAULT );
drawMatches()原型参数,简单用法就是:图1,图1关键点,图2,图2关键点,两张图片的关键点匹配数组(DMatch类型),承接图像,后面参数有默认值。
总结一下:到这里位置所有的匹配结果就已经完成从了,但是有大量的误匹配,因此需要对匹配结果进行筛选。
第五步:对匹配点进行筛选
当描述子之间的距离大于两倍的最小距离认为匹配有误;如果最小距离非常小也不可以,30是下限。
然后就是画出筛选后的匹配图片,同样使用drawMatches()函数
minmax_element() 是返回指向范围内最小和最大元素的一对迭代器。
default (1) template
pair
minmax_element (ForwardIterator first, ForwardIterator last);
custom (2) template
pair
minmax_element (ForwardIterator first, ForwardIterator last, Compare comp);
第二个是经常用到的,
first:一个输入迭代器,指示要比较的范围的第一个位置。
last:一个输入迭代器,指示要比较的范围中过去的最后一个元素。
comp: 一个用户定义的二元谓词函数,它接受两个参数,如果两个参数按顺序返回真,否则返回假。它遵循严格的弱排序来对元素进行排序。
它将较小的值作为第一个元素返回(first表示),较大的值作为的第二个元素返回(second表示)。
Lambda表达式(匿名函数) c++11特性
[外部变量访问方式说明符] (参数表) -> 返回值类型
{
语句块
}
“外部变量访问方式说明符” 可以是 =
或 &
,表示{}
中用到的、定义在{}
外面的变量在{}
中是否允许被改变。=
表示不允许,&
表示允许。当然,在{}
中也可以不使用定义在外面的变量。 “-> 返回值类型”可以省略。
int a[4] = {2, 11, 444, 3};
sort(a, a+4, [=](int x, int y) -> bool { return x%10 < y%10; } );
for_each(a, a+4, [=](int x) { cout << x << " ";} );
输出结果是 11 2 3 444
程序第 2 行使得数组 a 按个位数从小到大排序;
for_each 的第 3 个参数是一个 Lambda 表达式。for_each 执行过程中会依次以每个元素作为参数调用它,因此每个元素都被输出。
匿名函数详解
总结一下:minmax_element()实际上是对描述子之间的距离进行了排序。
写这段代码前建议先看下164页的一些总结,先了解下使用OpenCV和手写的区别。
OpenCV中描述子使用Mat来存储的,这里使用256位的二进制描述,即对应到8个32位的unsigned int 数据,用typedef 将它表示成DescType。
ORB特征点的计算使用FAST()函数,描述子的计算是自定义函数ComputeORB(),描述子的匹配也是自定义函数BfMatch(),在这个函数中使用了SSE指令集中的_mm_popcnt_u32函数计算一个unsigned int 变量中的一个数,从而达到汉明距离的效果。
1.检测FAST关键点使用 cv::FAST()函数
CV_EXPORTS void FAST( InputArray image, CV_OUT std::vector& keypoints,
int threshold, bool nonmaxSuppression=true );
image: 检测的灰度图像
keypoints: 在图像上检测到的关键点
threshold: 中心像素和围绕该像素的圆的像素之间的强度差阈值
nonmaxSuppression: 参数非最大值抑制,默认为真,对检测到的角点应用非最大值抑制。
2.函数ComputeORB() 输入图像和Fast角点,输出描述子
① 关于badpoints的筛选:
函数中利用if语句来进行badpoints的计数,在计算BRIEF描述子时,如果以该keypoints为中心的图像块,超出了图像范围,那么,在进行随机选取像素对时,其计算出的描述子将会是错误的,即该keypoints为badpoints。而以该keypoints为中心的图像块,其长和宽应该满足什么条件时,才能够超出图像呢?主要有四种况:
1、当kp.pt.x < half_boundary时,边长为boundary的图像块将在-x轴上超出图像。
2、当kp.pt.y < half_boundary时,边长为boundary的图像块将在-y轴上超出图像。
3、当kp.pt.x >= img.cols - half_boundary(kp.pt.x>= 640-16)时,边长为boundary(32)的图像块将在+x轴上超出图像。
4、当kp.pt.y >= img.rows - half_boundary(kp.pt.y>= 480-16)时,边长为boundary(32)的图像块将在+y轴上超出图像。
参考:ORB中的badpoints检测
②灰度质心法
为了保证旋转不变性,所以要在圆内计算。这是因为圆具备旋转不变的性质。比如说,一张图像中确定一个像素的质心之后,图像发生了旋转。这个时候,如果我们选择以该像素为中心,计算正方形区域内的像素的矩 m00、m01、m10、m11 就会发生变化,这样质心就发生改变。但是如果计算圆内的像素。只要半径保持不变,那么他的像素就不会发生变化。
half_patch_size表示半径
参考:
灰度质心法
灰度质心法的代码实现
③坐标旋转
在平面坐标上,任意点P(x1,y1),绕一个坐标点Q(x2,y2)旋转θ角度后,新的坐标设为(x, y)的计算公式如下:
x= (x1 - x2)* cos(θ) - (y1 - y2)* sin(θ) + x2 = x1* cos(θ) - y1* sin(θ)
y= (x1 - x2)* sin(θ) + (y1 - y2)* cos(θ) + y2 = x1* sin(θ) + y1* cos(θ)
④位运算
a |= b 等价于 a = a | b | 是位运算,两个都为0时,结果才为0
3.函数BfMatch 输入描述子desc1和描述子desc2,输出匹配对matches
SSE指令集
_mm_popcnt_u32 数32位 unsigned int中1的个数,而一个描述子由 8 * 32组成,因此距离为计算8个 (desc1[i1][k] ^ desc2[i2][k] , 32位 ) 中1的个数。
参考:
手写ORB特征代码解析