ORB_SLAM2学习笔记(2)——特征提取

ORB_SLAM2学习笔记(2)——特征提取

本文主要参考slam十四讲,顺序参考一步步带你看懂orbslam2源码

下面函数为单目Tracking线程的接口(文件momo_tum.cc中):

        // Pass the image to the SLAM system整个系统的核心所在,负责实时跟踪输入图片,
        SLAM.TrackMonocular(im,tframe);//输入的是图片和时间

下面为图形控制界面,控制是否激活定位模式和是否系统复位功能
在这里插入图片描述
代码如下(文件System.cc中):

cv::Mat System::TrackMonocular(const cv::Mat &im, const double &timestamp) //单目前系统 同文件夹下还有双目以及RGBD的代码实现
{
    if(mSensor!=MONOCULAR)//判断是否为单目
    {
        cerr << "ERROR: you called TrackMonocular but input sensor was not set to Monocular." << endl;
        exit(-1);//程序终止
    }

    // Check mode change
    {
        unique_lock<mutex> lock(mMutexMode);
        if(mbActivateLocalizationMode)//激活定位模式
        {
            mpLocalMapper->RequestStop();

            // Wait until Local Mapping has effectively stopped
            while(!mpLocalMapper->isStopped())
            {
                usleep(1000);//延时操作
            }
            
            mpTracker->InformOnlyTracking(true);
            mbActivateLocalizationMode = false;//确保只执行一次
        }
        if(mbDeactivateLocalizationMode)//非激活定位模式
        {
            mpTracker->InformOnlyTracking(false);//打开该线程
            mpLocalMapper->Release();
            mbDeactivateLocalizationMode = false;//确保只执行一次
        }
    }
     // Check reset
    {
    unique_lock<mutex> lock(mMutexReset);
   	   if(mbReset)//重置
       {
          mpTracker->Reset();
          mbReset = false;//确保只执行一次
       }
    }

下面函数为开始进行ORB特征提取(文件Frame.cc中):

// ORB extraction
ExtractORB(0,imGray);

转到该函数定义,这里用到了一个重载过的括号运算符:


void Frame::ExtractORB(int flag, const cv::Mat &im)
{
    // mpORBextractorLeft是ORBextractor对象,因为ORBextractor重载了()
    // 所以才会有下面这种用法
    if(flag==0)
        (*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
    else
        (*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

下面为重载运算符中,提取特征点和描述子的流程:

主要就是构建金字塔,然后提取特征点和描述子,然后根据金字塔的结构依次映射在每一层中,为后面特征点匹配作准备。
在这里插入图片描述

图像金字塔的构建
第0层为最下面一层,随着金字塔层数的增多,逐渐模糊(文件ORBextractor.cc中)

void ORBextractor::ComputePyramid(cv::Mat image)
{
    for (int level = 0; level < nlevels; ++level)//遍历金字塔每一层
    {
        float scale = mvInvScaleFactor[level];
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));//按照尺度进行缩放
        Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);//加个边界EDGE_THRESHOLD = 19 前面定义的,一个边界宽度
        Mat temp(wholeSize, image.type()), masktemp;
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
        //按照scale比例缩放后,定义的相应大小的Mat
        //Rect(矩形左上角横坐标,纵坐标以及矩形的宽度,高度),每经过一次循环,照片大小将按scale缩放,creat pre-created dist image
        // Compute the resized image
        if( level != 0 )
        {
            //mvImagePyramid[1]开始,都是由上一层的图像的尺寸得到
            // dsize = Size(round(fx*src.cols), round(fy*src.rows))
            //dsize是输出图像的大小,按照上面的计算公式计算得到了已经
            //resize(InputArry src, Output dst, Size dsize, double fx = 0, double fy = 0, 
            //int interpolation = INTER_LINEAR)
            //将设置出的图像分别拷贝到相应的层上去
            //根据上一层金字塔进行缩放
            resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
            //将设置出的图像分别拷贝到相应的层上去
            copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101+BORDER_ISOLATED);            
        }
        else
        {
            copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101);   //原图不需要缩放   temp和mvImagePyramid[0]共用的是同一段地址,temp和mvImagePyramid[0]中的任何一个发生了变化,都会导致共用的内存地址中的内容发生变化。      
        }
    }

}

下面是一个简化版金字塔结构

void ORBextractor::ComputePyramid_brief(cv::Mat image) {
   for (int level = 0; level < nlevels; ++level) {
        float scale = mvInvScaleFactor[level];
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));

        // Compute the resized image
        if (level != 0) {
            resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
       } else {
            mvImagePyramid[level] = image;
        }
    }
}

Fast 关键点的提取
下面函数进入计算关键点并进行四叉树存储的(文件ORBextractor.cc中):

   //关键点全部存在allKeypoints中
    vector < vector<KeyPoint> > allKeypoints;
    ComputeKeyPointsOctTree(allKeypoints);

转到ComputeKeyPointsOctTree 的函数(流程就是划分网格,提取fast关键点,转化为整个图像坐标,保存)

//函数的三要素是:函数名称,函数参数, 函数返回值
//ComputeKeyPointsOctTree是类ORBextractor的成员函数
//参数是vector类型的引用变量allKeypoints.
//返回值是void类型
//在参数文件TUM1.yaml下预定义了一些变量的值
//ORBextractor. nFeatures: 1000
//ORBextractor. scaleFactor: 1.2
//ORBextractor. nlevels: 8
//ORBextractor. iniThFAST: 20
//ORBextractor. minThFAST: 7
//在ORBextractor.h中用带参构造函数来初始化类ORBextractor中的相应的变量
//在类ORBextractor中还有一串变量
//std::vector<float> mvScaleFactor;
//std::vector<float> mvInvScaleFactor;
//std::vector<float> mvlevelSifma2;
//std::vector<float> mvInvLevelSigma2;
//如果说第一组变量是金字塔中某一层图像的属性,那么第二组是成员变量是一幅图像的属性。
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
    allKeypoints.resize(nlevels);//通过vector中的resize函数来重新将vector变量allKeypoints的大小设置为nlevels.

    const float W = 30;//定义了网格的大小,实际大小会根据图片大小而定,看下文


    for (int level = 0; level < nlevels; ++level)//一张图片,遍历整个金字塔
    {
        const int minBorderX = EDGE_THRESHOLD-3;//x,y轴边界阈值为16个像素
        const int minBorderY = minBorderX;
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;//整行——最小就是最大的X
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
        //vector中存储的数据类型是在opencv中定义的KeyPoint类
        vector<cv::KeyPoint> vToDistributeKeys;
        //reserve:分配空间,更改capacity但是不改变size 预留空间
        //resize:分配空间,同时改变capacity和size
        vToDistributeKeys.reserve(nfeatures*10);
        //计算出除去不考虑的边缘外,图片的width和height
        //预将图像划分为30*30的网状
        //计算每个小格子的长和宽各占多少个像素
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);
        //计算最终长和宽被分成了多少个小格子cell
        const int nCols = width/W;//网格列,实际上可以值为21.6,然后取21,最多分为21个
        const int nRows = height/W;//网格行
        const int wCell = ceil(width/nCols);//向上取整,大于该值的最小整数,计算出实际上每个网格的宽
        const int hCell = ceil(height/nRows);//计算出实际上每个网格的高


        for(int i=0; i<nRows; i++)//纵向遍历所有网格
        {
            const float iniY =minBorderY+i*hCell;
            float maxY = iniY+hCell+6;//给像素点加一个外框,保证每个像素点都被遍历

            if(iniY>=maxBorderY-3)//超出图片Y轴范围
                continue;
            if(maxY>maxBorderY)//确保maxY的上限
                maxY = maxBorderY;

            for(int j=0; j<nCols; j++)//横向遍历
            {
                const float iniX =minBorderX+j*wCell;
                float maxX = iniX+wCell+6;
                if(iniX>=maxBorderX-6)
                    continue;
                if(maxX>maxBorderX)
                    maxX = maxBorderX;
                //vkeyscell用来放置提取的关键点
                vector<cv::KeyPoint> vKeysCell;
                //变量i和j的组合控制,当遍历到(i, j)个cell时,就提取这个cell下的FAST角点
                //如下是opencv中FAST函数的原型
                //输入图像,输出提取的特征点, 选取特征点的阈值
                ///FAST( InputArray image, CV_OUT vector<KeyPoint>& keypoints,
                /// int threshold, bool nonmaxSuppression=true );
                FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                     vKeysCell,iniThFAST,true);//提取FAST关键点,采用非极大值抑制,采用初始阈值提取FAST
                /*ORBextractor.iniThFAST: 20
                  ORBextractor.minThFAST: 7
                  位于配置表ORB_SLAM2/Examples/Monocular/TUM2.yaml中*/


                if(vKeysCell.empty())//如果初始阈值提取不到FAST,则采用minTHFAST阈值提取
                {
                    FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                         vKeysCell,minThFAST,true);
                }

                if(!vKeysCell.empty())//将每个网格中的FAST关键点坐标转换为实际图像中(不包含边界)的坐标,并存入vToDistributeKeys
                {
                    for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
                    //KeyPoint是opencv中的一个类,pt是该类中的一个属性,获取获取关键点的坐标
                    //因为单纯的(*vit).pt.x和(*vit).pt.y表示在当前cell下的坐标,还要转化为在可提取特征范围内的坐标
                    { 
                        (*vit).pt.x+=j*wCell;//计算出对应的x坐标
                        (*vit).pt.y+=i*hCell;
                        vToDistributeKeys.push_back(*vit);//存好关键点
                    }
                }

            }
        }


上述代码已经提取完对应金字塔层数的FAST关键点,并进行四叉树存储,下面代码实现在四叉树筛选出高质量关键点:

        //vector<vector<KeyPoint> >& allKeypoints
        //allKeypoints是一个用来存储vector的vector
        //allKeypoints的大小是金字塔的层数nlevels
        //allKeypoints[level]是一个对应于每层图像上提取的特征点的vector
        //allKeypoints[level].size也就是在该层上要提取的特征点的个数
        vector<KeyPoint> & keypoints = allKeypoints[level];
        keypoints.reserve(nfeatures);
        //将图片进行四叉树存储,筛选高质量关键点,确保均匀,所有提取的关键点,提取的范围,是从哪一层上提取的特征
        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++)//记录关键点在图片实际坐标(包含边界),记录该金字塔level
        {
                    {
        //遍历在该层图像上提取的所有的特征点,在这些特征点坐标上都加上整幅图像的边界信息就可以
        //得到关键点在整幅图像中的坐标
            keypoints[i].pt.x+=minBorderX;
            keypoints[i].pt.y+=minBorderY;
            keypoints[i].octave=level;//octave存储金字塔层数
            keypoints[i].size = scaledPatchSize;//不同层所提取的范围不同
        }
    }

    // compute orientations 
    // 遍历每一层图像以及在该层上提取的特征点,计算每个特征点的方向。
    for (int level = 0; level < nlevels; ++level)
    //对应的塔层,FAST关键点,umax可以表示像素点到区域块边界水平距离
        computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}

计算FAST关键点的方向计算公式:
在这里插入图片描述

计算FAST关键点的方向:(这个只是调用,具体在这个下方)

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函数的程序如下:

static float IC_Angle(const Mat& image, Point2f pt,  const vector<int> & u_max)
{
    int m_01 = 0, m_10 = 0;

    const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));//计算灰度值的公式

    // Treat the center line differently, v=0
    for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
        m_10 += u * center[u];//计算y轴上的纵坐标 * 灰度值 之和

    // 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);//x轴只选了一侧 ,无正负,需要加负号
            m_10 += u * (val_plus + val_minus);//纵坐标 * 各点灰度值 求和 一个纵坐标对应两个灰度值
        }
        m_01 += v * v_sum;//横坐标 * 各点灰度值 求和
    }

    return fastAtan2((float)m_01, (float)m_10);//求角度
}

经过筛选,关键点将被存放在Keypoints向量中,四叉树存储思路如下:

在这里插入图片描述首先根据图片的wide/height比值(取整数),将图片竖着划分为若干个根节点

    // Compute how many initial nodes //要求图片width/height≥0.5,TUM数据集nIni=  
    //首先根据图片的 width/height 比值(四舍五入后的比值),将图片竖着划分为若干个根节点
    const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));

下面函数为四叉树选择最大响应点并储存的过程,流程(比较各个节点下的关键点的响应值代表该节点进行存储);另外四叉树可以避免提取到的关键点有些地方密集有些地方稀疏,使得计算描述子出现困难:

    // Retain the best point in each node  每个节点中保存最好的点作为大哥哥
    vector<cv::KeyPoint> vResultKeys;
    //reserve:分配空间,更改capacity但是不改变size 预留空间
    vResultKeys.reserve(nfeatures);
    //迭代器指针lit遍历四叉树中所有节点
    for(list<ExtractorNode>::iterator lit=lNodes.begin(); lit!=lNodes.end(); lit++)
    { 
        
        vector<cv::KeyPoint> &vNodeKeys = lit->vKeys;//vNodeKeys指向每个节点中的FAST关键点
        cv::KeyPoint* pKP = &vNodeKeys[0];
        float maxResponse = pKP->response;//先后使得第一个为最强的点,后面可能会替换

        for(size_t k=1;k<vNodeKeys.size();k++)//遍历节点中每一个点
        {
            if(vNodeKeys[k].response>maxResponse)//用其他强壮的点替换初始化的强壮点
            {
                pKP = &vNodeKeys[k];
                maxResponse = vNodeKeys[k].response;
            }
        }

        vResultKeys.push_back(*pKP);//将每个节点中响应值最大的节点存入vResultKeys,完成四叉树的保存
    }

    return vResultKeys;
}

BRIEF描述子的计算

//计算描述子
static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
                               const vector<Point>& pattern)
{
    //创建一个描述子矩阵
    descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);//cv_8UC1 8个字节 无符号整形 单通道灰度图像

    for (size_t i = 0; i < keypoints.size(); i++)
        computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));//输入关键点 图像 点对 描述子矩阵指针
}

转到computeOrbDescriptor函数看下,按照一定的规则进行组成点对,然后比较灰度值,进行赋值1或0 的操作用于后面匹配(用汉明距离进行匹配例如两幅图片四组点对分别为1010与1111 汉明距离就是2,这个数字小于多时候就认定匹配成功,这里用的是256对点对):

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)//一次处理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
}

然后就是对特征点进行一些处理:
去畸变(文件Frame.cc中):

UndistortKeyPoints();//去畸变
void Frame::UndistortKeyPoints()
{
    //不需要去畸变的点直接传到mvkeysun中
    if(mDistCoef.at<float>(0)==0.0)
    {
        mvKeysUn=mvKeys;
        return;
    }

    // Fill matrix with points
    cv::Mat mat(N,2,CV_32F);//N行 2列  存储类型为32位Float
    for(int i=0; i<N; i++)  //遍历所以点,进行存储到mat
    {
        mat.at<float>(i,0)=mvKeys[i].pt.x;
        mat.at<float>(i,1)=mvKeys[i].pt.y;
    }

    // Undistort points
    //C++: Mat Mat::reshape(int cn, int rows=0) const cn表示通道数 row表示行数
    mat=mat.reshape(2);//变为2通道
    cv::undistortPoints(mat,mat,mK,mDistCoef,cv::Mat(),mK);//调用opencv进行去畸变处理
    mat=mat.reshape(1);//变为1通道

    // Fill undistorted keypoint vector
    //把去畸变后的点存在mvkeysun中
    mvKeysUn.resize(N);
    for(int i=0; i<N; i++)
    {
        cv::KeyPoint kp = mvKeys[i];
        kp.pt.x=mat.at<float>(i,0);
        kp.pt.y=mat.at<float>(i,1);
        mvKeysUn[i]=kp;
    }
}

网格化:将特征点坐标转化为网格坐标,并判断该坐标是否在网格坐标范围内,是的话存入网格,不是抛弃(文件Frame.cc中):

AssignFeaturesToGrid();//网格化 将特征点坐标转化为网格坐标,并判断该坐标是否在网格坐标范围内,是的话存入网格,不是抛弃
void Frame::AssignFeaturesToGrid()
{
    int nReserve = 0.5f*N/(FRAME_GRID_COLS*FRAME_GRID_ROWS); //设置每个网格大小 FRAME_GRID_ROWS 48 FRAME_GRID_COLS 64
    for(unsigned int i=0; i<FRAME_GRID_COLS;i++)//进行网格划分
        for (unsigned int j=0; j<FRAME_GRID_ROWS;j++)
            mGrid[i][j].reserve(nReserve);//生成网格

    for(int i=0;i<N;i++)
    {
        const cv::KeyPoint &kp = mvKeysUn[i];//取出去畸变的关键点

        int nGridPosX, nGridPosY;//网格坐标系
        if(PosInGrid(kp,nGridPosX,nGridPosY)) //如果该关键点在网格里就保存
            mGrid[nGridPosX][nGridPosY].push_back(i);
    }
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值