欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

详细解读ORBSLAM中的描述子提取过程

程序员文章站 2022-03-01 20:54:15
...

一直都在基于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