详细解读ORBSLAM中的描述子提取过程
一直都在基于ORBSLAM做一些相关的开发,只知道进来的图片会直接提取出BRIEF描述子,但是都没有详细地看过它具体的提取过程,今天仔细研究了一下代码和相关理论,弄清楚之后感觉神清气爽,部分内容查找有些费劲,所以特此整理出来,希望对需要的人有所帮助。
1. 前言
ORBSLAM中使用的ORB特征是FAST特征和BRIEF描述子的集合,详细的FAST特征的提取过程这里大概说一下,方便后面对描述子的理解;
FAST特征的提取过程:
1. 构建高斯金字塔:ComputePyramid()
第一层为原图像,往上依次递减
先用高斯函数进行模糊,再缩放
将图像保存至mvImagePyramid[]中
2. 计算每层图像的兴趣点ComputeKeyPointsOctTree()
对金字塔图像进行遍历,将图像分割成cCols X nRows 个30*30的小块
对每个小块进行FAST兴趣点能提取,并将提取到的特征点保存在vToDistributeKeys vector中
对vToDistributeKeys中的特征点进行四叉树节点分配DistributeOctTree()
这一步将提取出来的所有特征点都保存在变量allKeypoints中,但是这里注意allKeypoints的形式是vector < vector<KeyPoint> >,也就是说并不是所有的特征点都保存在一起,外层的vector表示不同的金字塔层,也就是level;
2. 描述子提取的层层嵌套
2.1 提取描述子的第一步先建立用于保存所有描述子的变量 cv::Mat descriptors; 这里应用了形参_descriptors,为其开辟了一块内存,然后将地址给descriptors,后面对于descriptors的修改其实最终都会保存到形参_descriptors所对应的内存中;
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
if( nkeypoints == 0 )
_descriptors.release();
else
{
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
通过新建测Mat大小也可以看到,descriptors的ROW数量与关键点个数nkieypoints一样,其实就是一行对应一个特征点;
2.2 接下来就是具体的提取过程,不是一下子提取,而是按照图像金字塔来提取,从第0层开始提取,一直到最高层nlevels;
具体每一层金字塔图像要提取多少个描述子,就要看其对应到这一层有多少个特征点nkeypointsLevel;
a. 第一步先对这一层的金字塔图像做高斯模糊;
b. 将这一层所要占用的在descriptors中的内存地址放进来,进行描述子的计算;
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern);
c. 接下来进入到computeDescriptors()函数中,就开始对单个的特征点进行提取:
descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1); //现将这块的内容初始化为0
for (size_t i = 0; i < keypoints.size(); i++)
computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));
d. 而具体每个特征点所对应的描述子具体的提取过程其实在computeOrbDescriptor()中,这里一层层的嵌套,看起来有些啰嗦,其实是很整洁的,从一个完整的Mat,到基于Level的块儿,再到基于每个特征点的row,所以,computeOrbDescriptor()中传入的地址是descriptors.ptr((int)i), 也就是当前块描述子的第i行;
f. 接下来是详细的提取过程,这里传入的几个参数分别为
keypoints -- 关键点坐标 img -- 当前level的被高斯后的图像,pattern就是BRIEF的提取模板,保存的是一组一组的坐标值,最后提取出的描述子保存在desc中;
3. BRIEF描述子的模板:
要提取BRIEF描述子,这里需要先明白的一个变量就是pattern,它里面具体保存的内容,以及他的作用,个人觉得与BRIEF相关的其实就是这里(貌似也没有其他地方了[捂脸])
理解pattern之前需要先看一个变量,也就是bit_pattern_31_,也就是那个256*4的变量,看过无数遍,一直假装它并不重要,这里只摘抄两行出来:
static int bit_pattern_31_[256*4] =
{
8,-3, 9,5/*mean (0), correlation (0)*/,
4,2, 7,-12/*mean (1.12461e-05), correlation (0.0437584)*/,
...
}
这个变量在ORBSLAM的代码中总共是256行,代表了256个点对儿,也就是每一个都代表了一对点的坐标,如第一行表示点q1(8,-3) 和点 q2(9,5), 接下来就是要对比这两个坐标对应的像素值的大小;
好了,明白了bit_pattern_31_里面保存的点对就可以,接下来在ORBextractor的构造函数中,将这个数组转换成了std::vector<cv::Point> pattern; 也就是一个包含512个Point类型的变量;
const int npoints = 512;
const Point* pattern0 = (const Point*)bit_pattern_31_;
// copy [pattern0,pattern0+npoints] 到std::vector<cv::Point> pattern
std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));
至此,BRIEF描述子的模板已经保存成功,将要在下面的描述子成型中使用;
4. 描述子成型:
这里要先说一下BRIEF的提取步骤:
a. 在特征点周围选择一个patch,在ORBSLAM中patch的size为31*31
b. 在这个patch内通过某种方法挑选出nd个点对;
这里的某种方法,就是某个pattern,不同的pattern表示在这个patch中选择点的方式不同,ORBSLAM中nd为256, 也就是我 们上面说的选择256个点对儿,后面每一个点对儿会对应一个0或1的值;
c. 对于每一个点对,比如上面提到的q1(8,-3) 和点 q2(9,5), 比较这两点在patch中所对应的像素点的坐标;
d. 如果 I(p1) > I(p2) , 则该点对应的值为1, 反之为0;
最后得到了nd×1的描述子;
对应到ORBSLAM中的代码就是 computeOrbDescriptor() 中:
center为中心,因为点对的数量为256,也就是512个点,这里将其分成32组,每一组包含16个点,也就是8个点对;
for (int i = 0; i < 32; ++i, pattern += 16)
{
int t0, t1, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1); //GET_VALUE用于获取该id对应的坐标出的像素值
val = t0 < t1;
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1;
}
GET_VALUE(idx)的主要作用就是获取坐标点的像素值,这里做的转换就是以特征点的坐标位置为0,0, 其他依次为正负{-15,15},组成31*31的patch;
将上面的for循环完成,也就的到了该特征点对应的描述子,1*256的一个Mat,其中第一个点对q1(8,-3) 和点 q2(9,5) 所计算出的二值放在最前面,然后依次,第二个点对儿,第三个....
最后的结果再一层一层的“传出去”,最后保存到每一个Frame对应的类成员变量mDescriptors 中;
这个变量与保存地图点的keyPOint是对应的,这样就可以保证后面进行匹配是能根据mappoint直接找到对应的描述子,用于后面计算距离;
参考:
BRIEF描述子:https://www.cnblogs.com/ronny/p/4081362.html
ORB算法原理: https://www.cnblogs.com/ronny/p/4083537.html
back_inserter: https://www.jianshu.com/p/6862a79eba0a
上一篇: Pytorch中的LSTM详细代码解读
下一篇: css3小记----动画