ORBSLAM2--特征提取

ORBSLAM2--特征提取

    • 特征提取(ORBextractor.h and ORBextractor.cpp)
    • ORBextractor
    • operator()
    • ComputePyramid
    • ComputeKeyPointsOctTree
        • 尺度不变性的理解
    • computeOrientation
          • IC_Angle
    • 高斯模糊
    • computeDescriptors
    • 最后操作
    • 结语

说白了特征提取也就是对图像进行一定的操作,也就是对像素点进行一些操作,跟相邻的一些像素点进行比较,通过一些模板进行滤波卷积等操作,再通过阈值进行一些控制,找到了可以代表该图像的某些位置,这也就是特征提取。关于ORB的特征可参考相应论文和中文总结

那下一步进行特征提取,orb-slam中,作者对opencv中的orb源码进行了修改,将特征进行均匀化。 对每一层图像划分30*30的栅格,在每个栅格内提取FAST角点,保证了角点分布的均匀,根据这一层的nfeatures使用DistributeOctTree()进行剔除。而opencv自带的orb特征点检测,可能会造成特征点过于集中,这样对于tracking很不利,很容易就跟丢了。具体调用和opencv一致,也是重载了函数调用操作符operator()。这只是对ORBSLAM2源码里的特征提取步骤思路进行了一个梳理,但是里面的具体实现还有很多不太明白。

特征提取(ORBextractor.h and ORBextractor.cpp)

源码里面的特征提取的实现主要就是一个类,我们称它为一个特征提取器。整个流程:ORBSLAM2--特征提取_第1张图片

下面主要是把对应的源码贴上,部分文件会有具体注释,但是有些没有注释的目前还没有理解,有待之后深入学习,这里先建立一个思路。

  • 方向不变性的理解
  1. ORB特征提取会通过灰度质心给每个特征点分配一个方向
  2. 计算描述子,其中邻域内初始(特征点方向是零的时候)的选取比较对的规则是固定的,那对于每个特征点的旋转角度乘上这个当前的固定的(0度)选取点对可以得到新的选取点对,这样提取到的描述子就是RBREIF,带有方向的,就具有旋转不变性了

ORBextractor

这其实是一个构造函数,主要完成的是一些参数的确定,如在yaml文件读取到的尺度因子和金字塔层数、特征点数量小,对每一幅图像的每一层金字塔的尺度进行确定,和每一层应该提取的特征点数进行计算。

ORBextractor::ORBextractor(int _nfeatures, float _scaleFactor, int _nlevels,
         int _iniThFAST, int _minThFAST):
    nfeatures(_nfeatures), scaleFactor(_scaleFactor), nlevels(_nlevels),
    iniThFAST(_iniThFAST), minThFAST(_minThFAST)
{
      //mvScaleFactor是用来存储金字塔中每层图像对应的尺度因子的vector变量
    //所以他的大小就是金字塔的层数
    mvScaleFactor.resize(nlevels);
    mvLevelSigma2.resize(nlevels);
    mvScaleFactor[0]=1.0f;
    mvLevelSigma2[0]=1.0f;
    //计算金字塔中每一层图像对应的尺度因子和尺度因子的平方。
    //可以发现金字塔中第0层的尺度因子是1,然后每向上高一层,图像的尺度因子是在上一层图像的尺度因子 
    //上乘以scaleFactor,在本工程下该值为1.2
    //1   1*1.2   1*1.2*1.2    1*1.2*1.2*1.2   ...

    for(int i=1; i<nlevels; i++)
    {
        mvScaleFactor[i]=mvScaleFactor[i-1]*scaleFactor;
        mvLevelSigma2[i]=mvScaleFactor[i]*mvScaleFactor[i];//尺度因子的平方。
    }
    //如果说上面是正向的尺度,那么下面的就是逆向尺度了,是正向尺度的倒数
    mvInvScaleFactor.resize(nlevels);//有一个Inv
    mvInvLevelSigma2.resize(nlevels);
    for(int i=0; i<nlevels; i++)
    {
        mvInvScaleFactor[i]=1.0f/mvScaleFactor[i];
        mvInvLevelSigma2[i]=1.0f/mvLevelSigma2[i];
    }
    //mvImagePyramid是一个存储金字塔图像的vector,vector中的每一个元素是用Mat数据类型表示的图        
    //像, std::vector mvImagePyramid;
    mvImagePyramid.resize(nlevels);
    //mnFeaturesPerLevel是一个存储金字塔中每层图像应该提取的特征点的个数
    //std::vector mnFeaturesPerLevel;
    mnFeaturesPerLevel.resize(nlevels);
        //那在这里factor = 1/1.2;
    float factor = 1.0f / scaleFactor;
    //nDesiredFeaturesPerScale是根据总的要在整幅图像中提取的特征点数nFeatures(在这里是1000)
    //还有金字塔的层数来计算每层图像上应该提取的特征点的个数。
    //根据下面的程序可以得知nDesiredFeaturesPerScale是第0层图像上应该提取的特征点的个数
    //0.2/(1-(1/1.2)^8)*1000
    float nDesiredFeaturesPerScale = nfeatures*(1 - factor)/(1 - (float)pow((double)factor, (double)nlevels));

    int sumFeatures = 0;
    for( int level = 0; level < nlevels-1; level++ )//最后一层不在这个循环里面赋值
    {
              //那么mnFeaturesPerLevel[0]就是上面计算出来的这个值喽
        mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);
	        //由于四舍五入的原因将实际计算出的数值加到一起与预设的1000比较
        sumFeatures += mnFeaturesPerLevel[level];
	//层数越高则需要提取的特征的个数就越少,并且相邻层图像上提取的特征点的个数存在1.2倍
        //关系
        //这里是我计算的结果
        //217+180+150+125+104+87+72+60 = 995
        nDesiredFeaturesPerScale *= factor;
    }
        //看到这里就会知道上面为啥要把计算出来的特征点个数求和的原因了,就是来决定最后
    //一层图像应该提取的特征的个数了,根据计算最后一层要提取60个点,那现在就得提取65个
    //才能达到总共提取1000个点的要求。
    mnFeaturesPerLevel[nlevels-1] = std::max(nfeatures - sumFeatures, 0);

        //下面这一块是为了提取特征点做准备了。
    const int npoints = 512;
    const Point* pattern0 = (const Point*)bit_pattern_31_;
    std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));

    //This is for orientation
    // pre-compute the end of a row in a circular patch
    umax.resize(HALF_PATCH_SIZE + 1);

    int v, v0, vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1);
    int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);
    const double hp2 = HALF_PATCH_SIZE*HALF_PATCH_SIZE;
    for (v = 0; v <= vmax; ++v)
        umax[v] = cvRound(sqrt(hp2 - v * v));

    // Make sure we are symmetric
    for (v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v)
    {
        while (umax[v0] == umax[v0 + 1])
            ++v0;
        umax[v] = v0;
        ++v0;
    }
}

operator()

这里还是和opencv的orb特征提取的方法一样,同样是重载了运算符 () 这就是整个特征提取的主程序,在Frame中被调用来对每一帧的特征进行提取。


void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
                      OutputArray _descriptors)
{ 
    if(_image.empty())
        return;

    Mat image = _image.getMat();
    assert(image.type() == CV_8UC1 );//无符号8位单通道

    // Pre-compute the scale pyramid
    // 构建图像金字塔
    ComputePyramid(image);

    // 计算每层图像的特征点
    vector < vector<KeyPoint> > allKeypoints; // vector>
    //把每一层金字塔层分成一个个网格,在进行提取,后进行八叉树的优化剔除,以便让特征点分布均匀和特征点数量不超过
    //前面计算得出的每层的特征点数量,之后最运用灰度质心法求每个特征点的方向
    ComputeKeyPointsOctTree(allKeypoints);
    //ComputeKeyPointsOld(allKeypoints);

    Mat descriptors;

    int nkeypoints = 0;
    for (int level = 0; level < nlevels; ++level)
        nkeypoints += (int)allKeypoints[level].size();//得到所有关键点数量
    if( nkeypoints == 0 )
      //_descriptors是实参,Mat类型
        _descriptors.release();
    else
    {
      //创建nkeypoints行,32列的,CV_8U类型的矩阵,Mat_对应的是CV_8U,Mat_对应的是CV_8U
        _descriptors.create(nkeypoints, 32, CV_8U);
        descriptors = _descriptors.getMat();//把InputArray类型转化成Mat
    }

    _keypoints.clear();
    _keypoints.reserve(nkeypoints);

    int offset = 0;
    for (int level = 0; level < nlevels; ++level)
    {
      //得到每一层的keypoints指针,直接对allKeypoints[level]操作
        vector<KeyPoint>& keypoints = allKeypoints[level];
        int nkeypointsLevel = (int)keypoints.size();//对应层的特征点数量

        if(nkeypointsLevel==0)
            continue;

        // preprocess the resized image 对图像进行高斯模糊
        Mat workingMat = mvImagePyramid[level].clone();
	/*高斯滤波:GaussianBlur函数
   函数原型:  void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
                                double sigmaX, double sigmaY = 0,
                                int borderType = BORDER_DEFAULT );  
   参数详解:
   InputArray src-----源图像
   OutputArray dst-----目标图像
   Size ksize----高斯内核大小,其中ksize.width和ksize.height可以不同,但是必须为正数
                        和奇数,也可为零,均有sigma计算而来。
   double sigmaX----表示高斯函数在X方向的标准偏差
   double sigmaY---- 表示高斯函数在Y方向的标准偏差
                               若sigma为零,就将它设为sigmaX,如果两者均为零,就由ksize.width
                               和ksize.height计算出来。
   int borderType -----用于推断图像外部像素的某种边界模式。
                                 默认值 BORDER_DEFAULT       
                                 
                                 https://blog.csdn.net/godadream/article/details/81568844
                                 http://blog.sina.com.cn/s/blog_c936dba00102vzhu.html
                                 */
        GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);

        // Compute the descriptors 计算描述子
        Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
	//pattern在构造函数中已经赋值
        computeDescriptors(workingMat, keypoints, desc, pattern);

        offset += nkeypointsLevel;

        // Scale keypoint coordinates
        if (level != 0)
        {
            float scale = mvScaleFactor[level]; //getScale(level, firstLevel, scaleFactor);
            for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
                 keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
                keypoint->pt *= scale;//对每一个关键点附上尺度信息
        }
        // And add the keypoints to the output
        _keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
    }
}

ComputePyramid

对当前帧图像进行金字塔的分层,并计算每一层的尺度因子和每层应提取的特征点数量,金字塔介绍请看 1 和 2,金字塔的目的主要目的是解决尺度的不变性,在低尺度下可以看清楚很多细节,在高尺度下可以看到轮廓,这样可以让提取的特征点更具鲁棒性(同一个场景下,不管摄像机的前进与后退都会造成特征点块的放大和缩小,为了能在不同大小的图像中提取到相同的特征点,所以引入图像金字塔,如这里介绍尺度空间的目的)。

/**
 * 构建图像金字塔
 * 主要是进行了一层一层图像的大小缩放
 * @param image 输入图像
 */
void ORBextractor::ComputePyramid(cv::Mat image)
{
    for (int level = 0; level < nlevels; ++level)
    {
      //反向尺度因子在这里用到
        float scale = mvInvScaleFactor[level];
	//不同的level,scale的值不同所以就算出了每一层上的图像的大小
	//由此可见是在不断缩小的
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
	//为新生成的图像加上边界
        Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
	//根据上面的计算的尺度来创建一幅新的图像, Mat类型
        //type()是Mat类中的一个成员函数,返回数据类型
        Mat temp(wholeSize, image.type()), masktemp;
	//mvImagePyramid是用来存储每一层图像的vector变量,为他的每一个元素设置特定大小的图像
        //mvImagePyramid[0]中存储的是原图像
        //通过Rect定义temp图像的左上侧xy坐标,图像起始位置
	//图像中设置了ROI(Region Of Interest ,感兴趣区域)
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));

        // Compute the resized image
        if( level != 0 )
        {
	  //resize( )将会对源图像进行尺寸调整并填充到目标图像的ROI中
	  //第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
          //第二个参数,OutputArray类型的dst,输出图像,放在由Rect()函数创建的ROI内
	  //当其非零时,有着dsize(第三个参数)的尺寸,或者由src.size()计算出来。
          //第三个参数,Size类型的dsize,输出图像的大小;

            resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, cv::INTER_LINEAR);

	    //该函数就是将mvImagePyramid[level]图像复制到temp里面,并且4个边缘分别扩充EDGE_THRESHOLD个像素
	    //扩充方法是对称法BORDER_REFLECT_101:   gfedcb|abcdefgh|gfedcba
	    //temp后续没有任何操作?
            copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101+BORDER_ISOLATED);            
        }
        else
        {
	  //当处于第一层的时候,就是mvImagePyramid[0],原图像就是image
            copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101);            
        }
    }

}

} //namespace ORB_SLAM

其中copyMakeBorder
该函数的目的是将原图像复制到目标Mat图像中,并且对边缘进行扩充,以便后续处理,扩充方法有多种
扩充src的边缘,将图像变大,然后以各种外插方式自动填充图像边界,这个函数实际上调用了函数cv::borderInterpolate,
这个函数最重要的功能就是为了处理边界,比如均值滤波或者中值滤波中,使用copyMakeBorder将原图稍微放大,然后我们就可以处理边界的情况了。介绍

src,dst:原图与目标图像

top,bottom,left,right分别表示在原图四周扩充边缘的大小

borderType:扩充边缘的类型,就是外插的类型,OpenCV中给出以下几种方式

  • BORDER_REPLICATE
  • BORDER_REFLECT
  • BORDER_REFLECT_101
  • BORDER_WRAP
  • BORDER_CONSTANT

Various border types, image boundaries are denoted with ‘|’

  • BORDER_REPLICATE: aaaaaa|abcdefgh|hhhhhhh
  • BORDER_REFLECT: fedcba|abcdefgh|hgfedcb
  • BORDER_REFLECT_101: gfedcb|abcdefgh|gfedcba
  • BORDER_WRAP: cdefgh|abcdefgh|abcdefg
  • BORDER_CONSTANT: iiiiii|abcdefgh|iiiiiii with some specified ‘i’

ComputeKeyPointsOctTree

这一步就是进行各个金字塔图层进行特征提取,首先对每个图层进行边界的处理,之后会在每个图层上画出相应大小的格子,最后后在每个格子上进行特征提取。这时候提取出来的特征点数量和质量还需要进行处理,还会进行八叉树的优化和剔除,让每个图层上的特征点尽可能的满足数量和分布的尽量的均匀。之后还会使用灰度质心法计算每个特征点的方向,具体看介绍

void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
    allKeypoints.resize(nlevels);//8层,每层提取固定的点

    const float W = 30;

    // 对每一层图像做处理
    for (int level = 0; level < nlevels; ++level)
    {
        //GE_THRESHOLD=19
        //minBorderX, minBorderY, maxBorderX, maxBorderY四个变量一起设定了用于提取特征的区域
      //给出一定的预留,这里是3像素点
        const int minBorderX = EDGE_THRESHOLD-3;
        const int minBorderY = minBorderX;
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
        //定义变量vToDistributeKeys存储从每一层图像中提取出来的特征。
        //vector中存储的数据类型是在opencv中定义的KeyPoint类
        vector<cv::KeyPoint> vToDistributeKeys;
	//reserve:分配空间,更改capacity但是不改变size 预留空间
        //resize:分配空间,同时改变capacity和size
        vToDistributeKeys.reserve(nfeatures*10);
	
        //丈量了可以用来进行操作的“场地”大小,也就是操作的空间
	
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);
	
	//预将图像划分为30*30的网状
        //计算每个小格子的长和宽各占多少个像素
        //纵向和横向分别有多少个格子
        const int nCols = width/W;
        const int nRows = height/W;
	//ceil向上取整,就整数位+1,小数位清零
	//每个格子的长宽像素
        const int wCell = ceil(width/nCols);
        const int hCell = ceil(height/nRows);

	//各个格子之间大概有6个像素的重合
        for(int i=0; i<nRows; i++)
        {
	  //当前cell的左上角Y值
            const float iniY =minBorderY+i*hCell;
	    //当前cell左下角Y值,并在基础上加上了6个像素,作为预留
            float maxY = iniY+hCell+6;

	    //对于初始起点超出了前面制定的特征提取区域就跳出
            if(iniY>=maxBorderY-3)
                continue;
	    //最后一个框的时候,边界等于上面设定的特征提取边界
            if(maxY>maxBorderY)
                maxY = maxBorderY;

            for(int j=0; j<nCols; j++)
            {
	      //当前cell的左上角X值
                const float iniX =minBorderX+j*wCell;
		//当前cell右上角X值,并在基础上加上了6个像素,作为预留
                float maxX = iniX+wCell+6;
		//对于初始起点超出边界就跳出
                if(iniX>=maxBorderX-6)
                    continue;
		//最后一个框的时候,边界等于上面设定的特征提取边界
                if(maxX>maxBorderX)
                    maxX = maxBorderX;

                // FAST提取兴趣点, 自适应阈值
                vector<cv::KeyPoint> vKeysCell;
		//iniThFAST=20
		//这个函数在哪里进行定义了????
		//iniThFAST这个阈值应该是在检测点的时候,中心点和圆周上的点灰度值差的阈值
                FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                     vKeysCell,iniThFAST,true);

                if(vKeysCell.empty())
                {
		    //如果按照上面的方法在这个cell中提取不到特征点,那么就放低要求,
                    //用minThFAST阈值来提取FAST角点
                    FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                         vKeysCell,minThFAST,true);
                }

                if(!vKeysCell.empty())
                {
		                //如果已经提取到关键点,那么就遍历这些所有提取的关键点
                    for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
                    {
		      ///迭代器vit就相当于一个指向vector中存储的KeyPoint的指针,通过*vit就可以获取指针所指向的的
//特定的点
                        //pt表示KeyPoint的位置属性
                  //KeyPoint是opencv中的一个类,pt是该类中的一个属性,获取获取关键点的坐标
//因为单纯的(*vit).pt.x和(*vit).pt.y表示在当前cell下的坐标,还要转化为在可提取特征范围内的坐标
//j*wCell表示当前是横向第几个格子,i*hCell表示当前纵向是第几个格子
                        (*vit).pt.x+=j*wCell;
                        (*vit).pt.y+=i*hCell;
			               //把从每个cell中提取的特征点都存储到vector变量vToDistributeKeys中去
                        vToDistributeKeys.push_back(*vit);
                    }
                }

            }
        }
//截止到这里已经将该层图像中的所有cell遍历结束并且,将提取的所有的特征点都已经存储到vector
//变量vToDistributeKeys中去了


        //vector >& allKeypoints
        //allKeypoints是一个用来存储vector的vector
        //allKeypoints的大小是金字塔的层数nlevels
        //allKeypoints[level]是一个对应于每层图像上提取的特征点的vector
        //allKeypoints[level].size也就是在该层上要提取的特征点的个数

        vector<KeyPoint> & keypoints = allKeypoints[level];
        keypoints.reserve(nfeatures);

        // 根据mnFeaturesPerLevel,即该层的兴趣点数,对特征点进行剔除
	//所有提取的关键点,提取的范围,是从哪一层上提取的特征
        //下面这部分是将提取的关键点进行八叉树优化
	//剔除和优化特征点的分布,使之数量不多于mnFeaturesPerLevel[level]和分布均匀
	//keypoints变量是mnFeaturesPerLevel[level]别名,直接对其进行操作
        keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
                                      minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);
	
	
 ///PATCH_SIZE指代什么呢
        ////level=0表示原图像,随着层数的增加图像越来越小,那么在每幅图像上提取的特征个数
//也会相应的减少
        // PATCH_SIZE = 31.
        //vector变量mvScaleFactor中存储了一幅图像对应的一个金字塔中所有层图像的尺度因子
        //不同层图像的尺度因子不同,那么在该层中提取的特征点所对应的有效区域就不同

        const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];

        // Add border to coordinates and scale information
        const int nkps = keypoints.size();
        for(int i=0; i<nkps ; i++)
        {
	       ///遍历在该层图像上提取的所有的特征点,在这些特征点坐标上都加上整幅图像的边界信息就可以
               //得到关键点在整幅图像中的坐标,,这只是知道当前层的像素坐标吧??
            keypoints[i].pt.x+=minBorderX;
            keypoints[i].pt.y+=minBorderY;
            keypoints[i].octave=level;
            keypoints[i].size = scaledPatchSize;
        }
    }

    // compute orientations
    for (int level = 0; level < nlevels; ++level)
        computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}

这个程序流程是:对每个图层进行网格子的计算---->在每个网格子内进行FAST特征点的提取----->对当前图层进行特征点分布优化----->对每一个特征点的方向进行计算

尺度不变性的理解

  • 不同的尺度下,图像的大小是不一样,这相当于模拟了人眼远近观测物体得到的现象
  • 在金字塔第m层下的尺度提取一个特征点,这里的特征点周边会生成一个尺寸的框,包含了一些图像信息,但是如果换到其它层级的尺度图像下,那么这个框包含的信息会变化,往低图层上考虑,这个框框出来的图像信息会变少,往高图层考虑,框出来的图像信息会越多
  • 根据资料,每一图层上Log作用的曲线都会存在一个峰值,在这个峰值可以实现不同尺度下框出来的图像信息是一样的,可参考A 和 B
  • 对于一些点,不管你图片初始尺寸是多少,我都能调整LoG的半径使这个点在作用后达到级值,在达到极值的尺度下所框出的信息量是一样的。
  • 这里说的尺度不变,应该就是说不同尺度下,提取特征点的框应该都是包含相同图像信息的。

computeOrientation

使用灰度质心法计算每一个特征点的方向,可以提高图像的旋转不变性。

static void computeOrientation(const Mat& image, vector<KeyPoint>& keypoints, const vector<int>& umax)
{
  //对该金字塔层上的所有特征点逐一的进行遍历
    for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
         keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
    {
        keypoint->angle = IC_Angle(image, keypoint->pt, umax);
    }
}

在这里面会调用IC_Angle函数来进行主要的计算

IC_Angle

//灰度质心法计算质心,和方向
static float IC_Angle(const Mat& image, Point2f pt,  const vector<int> & u_max)
{
    int m_01 = 0, m_10 = 0;

    //这是把当前pt点作为原点,求质心
    //这是该像素点的指针
    //center应该是中心的灰度值,但是怎么是一个数组呢???
    const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));

    // Treat the center line differently, v=0
    // image不是灰度图吗,为什么会有这么多通道????
    for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
        m_10 += u * center[u];

    // Go line by line in the circuI853lar patch
    int step = (int)image.step1();//每一行的通道数量,应该是和每一行的像素点数一致(为什么不是图像的宽度?)
    for (int v = 1; v <= HALF_PATCH_SIZE; ++v)
    {
        // Proceed over the two lines
        int v_sum = 0;
        int d = u_max[v];
        for (int u = -d; u <= d; ++u)
        {
            int val_plus = center[u + v*step], val_minus = center[u - v*step];
            v_sum += (val_plus - val_minus);
            m_10 += u * (val_plus + val_minus);
        }
        m_01 += v * v_sum;
    }

    //θ=arctan(m01,m10)
    return fastAtan2((float)m_01, (float)m_10);
}

高斯模糊

在对当前图像进行了特征提取完成之后,会对图像进行一个平滑处理,也就是高斯模糊,主要是消除一些噪声的影响,具体还是不太理解。参考1 and 2

整个函数模型是:

void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0,
 int borderType=BORDER_DEFAULT);

参数详解:
InputArray src-----源图像
OutputArray dst-----目标图像
Size ksize----高斯内核大小,其中ksize.width和ksize.height可以不同,但是必须为正数
和奇数,也可为零,均有sigma计算而来。
double sigmaX----表示高斯函数在X方向的标准偏差
double sigmaY---- 表示高斯函数在Y方向的标准偏差
若sigma为零,就将它设为sigmaX,如果两者均为零,就由ksize.width
和ksize.height计算出来。
int borderType -----用于推断图像外部像素的某种边界模式。
默认值 BORDER_DEFAULT

computeDescriptors

剩下的就是对描述子计算,ORB选择了BRIEF作为特征描述方法,但是我们知道BRIEF是没有旋转不变性的,所以我们需要给BRIEF加上旋转不变性,把这种方法称为“Steer BREIF”。除了给描述子加上方向信息还要提高描述子的区分度,所以还需要进行一定的处理,这里也就不详细展开。
在描述子提取的时候为了增加描述子的旋转不变性(图像旋转后描述子的提取区域任然不变),描述子的提取邻域同样要
根据这个角度进行旋转

static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
                               const vector<Point>& pattern)
{
  //创建符合当前层特征点数量的描述子容器
    descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);

    for (size_t i = 0; i < keypoints.size(); i++)
      //对每一个特征点进行描述子的计算
        computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));
}

同样的在这里会调用computeOrbDescriptor主程序

const float factorPI = (float)(CV_PI/180.f);
static void computeOrbDescriptor(const KeyPoint& kpt,
                                 const Mat& img, const Point* pattern,
                                 uchar* desc)
{
    float angle = (float)kpt.angle*factorPI;
    float a = (float)cos(angle), b = (float)sin(angle);

    const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
    const int step = (int)img.step;

    #define GET_VALUE(idx) \
        center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + \
               cvRound(pattern[idx].x*a - pattern[idx].y*b)]


    for (int i = 0; i < 32; ++i, pattern += 16)
    {
        int t0, t1, val;
        t0 = GET_VALUE(0); t1 = GET_VALUE(1);
        val = t0 < t1;
        t0 = GET_VALUE(2); t1 = GET_VALUE(3);
        val |= (t0 < t1) << 1;
        t0 = GET_VALUE(4); t1 = GET_VALUE(5);
        val |= (t0 < t1) << 2;
        t0 = GET_VALUE(6); t1 = GET_VALUE(7);
        val |= (t0 < t1) << 3;
        t0 = GET_VALUE(8); t1 = GET_VALUE(9);
        val |= (t0 < t1) << 4;
        t0 = GET_VALUE(10); t1 = GET_VALUE(11);
        val |= (t0 < t1) << 5;
        t0 = GET_VALUE(12); t1 = GET_VALUE(13);
        val |= (t0 < t1) << 6;
        t0 = GET_VALUE(14); t1 = GET_VALUE(15);
        val |= (t0 < t1) << 7;

        desc[i] = (uchar)val;
    }

    #undef GET_VALUE
}

最后操作

进行了前面的步骤基本上完成了整个图像的ORB特征的提取,这里会对提取出来的特征点附上相关的金字塔层信息,以便于之后通过地图点与相机帧的距离推测出相关的金字塔层,然后进行同金字塔层提取到的特征进行匹配提供信息。

结语

  • 整个特征提取是VO的前奏,所以要实现一个VO,必须先保证能够提取到稳定可靠的特征点
  • ORB_SLAM2是对opencv中的orb特征提取API进行了一个修改(具体改哪里不清楚),这里为了后面更好参照该工程实现自己的VO做一个总结
  • 这次总结的目的是先进行一个归纳梳理,一些原理性的东西还不是很清楚,待后续的深入学习

参考
http://www.fengbing.net/2016/04/03/%E4%B8%80%E6%AD%A5%E6%AD%A5%E5%AE%9E%E7%8E%B0slam2-orb%E7%89%B9%E5%BE%81%E6%A3%80%E6%B5%8B/

https://blog.csdn.net/weixin_38636815/article/details/81943109

你可能感兴趣的:(ORBSLAM2学习)