天天看点

ORB2单目读代码笔记3--ORB特征提取构造函数仿函数、图像帧构造函数、计算图像金字塔并进行扩充

3.ORB特征提取构造函数仿函数、图像帧构造函数、计算图像金字塔并进行扩充

    • Tracking结束,返回System
    • System未结束,暂时返回main
    • main 跳转 TrackMonocular
    • TrackMonocular 跳转 GrabImageMonocular
    • GrabImageMonocular 跳转 Frame
    • Frame 跳转 ExtractORB
    • ExtractORB 跳转 operator()
    • operator() 跳转 ComputePyramid
    • ComputePyramid 结束,返回 operator()

输出提取器的信息:

cout << endl  << "ORB Extractor Parameters: " << endl;
    cout << "- Number of Features: " << nFeatures << endl;
    cout << "- Scale Levels: " << nLevels << endl;
    cout << "- Scale Factor: " << fScaleFactor << endl;
    cout << "- Initial Fast Threshold: " << fIniThFAST << endl;
    cout << "- Minimum Fast Threshold: " << fMinThFAST << endl;

    if(sensor==System::STEREO || sensor==System::RGBD)
    {
        // 判断一个3D点远/近的阈值 mbf * 35 / fx
        //ThDepth其实就是表示基线长度的多少倍
        mThDepth = mbf*(float)fSettings["ThDepth"]/fx;
        cout << endl << "Depth Threshold (Close/Far Points): " << mThDepth << endl;
    }

    if(sensor==System::RGBD)
    {
        // 深度相机disparity转化为depth时的因子
        mDepthMapFactor = fSettings["DepthMapFactor"];
        if(fabs(mDepthMapFactor)<1e-5)
            mDepthMapFactor=1;
        else
            mDepthMapFactor = 1.0f/mDepthMapFactor;
           

Tracking结束,返回System

后面的建图线程,回环检测线程暂时跳过,先返回main

System未结束,暂时返回main

nImages

int

类型,存储的是rgb图片名字长度

vTimesTrack

vector

类型,存储跟踪的时间统计数据

// Vector for tracking time statistics
    vector<float> vTimesTrack;
    vTimesTrack.resize(nImages);

    cout << endl << "-------" << endl;
    cout << "Start processing sequence ..." << endl;
    cout << "Images in the sequence: " << nImages << endl << endl;
           

im

Mat

类型,读取对应数据路径下rgb图片

tframe

double

类型,存储rgb图片对应时间戳

// Main loop
    cv::Mat im;
    for(int ni=0; ni<nImages; ni++)
    {
        // Read image from file
        im = cv::imread(string(argv[3])+"/"+vstrImageFilenames[ni],CV_LOAD_IMAGE_UNCHANGED);
        double tframe = vTimestamps[ni];
           

判断是否读到了图片

if(im.empty())
        {
            cerr << endl << "Failed to load image at: "
                 << string(argv[3]) << "/" << vstrImageFilenames[ni] << endl;
            return 1;
        }
           

宏定义统计时间

#ifdef COMPILEDWITHC11
        std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
#else
        std::chrono::monotonic_clock::time_point t1 = std::chrono::monotonic_clock::now();
#endif
           

单目跟踪线程启动

// Pass the image to the SLAM system
        SLAM.TrackMonocular(im,tframe);
           

main 跳转 TrackMonocular

cv::Mat System::TrackMonocular(const cv::Mat &im, const double &timestamp)
{
           

判断传感器类型是否符合

if(mSensor!=MONOCULAR)
    {
        cerr << "ERROR: you called TrackMonocular but input sensor was not set to Monocular." << endl;
        exit(-1);
    }
           

根据

mbActivateLocalizationMode

判断是否关闭局部地图线程

如果关闭,则只计算位姿,不更新局部地图

// Check mode change
    {
        // 独占锁,主要是为了mbActivateLocalizationMode和mbDeactivateLocalizationMode不会发生混乱
        unique_lock<mutex> lock(mMutexMode);
        // mbActivateLocalizationMode为true会关闭局部地图线程
        if(mbActivateLocalizationMode)
        {
            mpLocalMapper->RequestStop();

            // Wait until Local Mapping has effectively stopped
            while(!mpLocalMapper->isStopped())
            {
                usleep(1000);
            }

            // 局部地图关闭以后,只进行追踪的线程,只计算相机的位姿,没有对局部地图进行更新
            // 设置mbOnlyTracking为真
            mpTracker->InformOnlyTracking(true);
            // 关闭线程可以使得别的线程得到更多的资源
            mbActivateLocalizationMode = false;
        
           

根据

mbDeactivateLocalizationMode

判断是否释放局部地图线程

// 如果mbDeactivateLocalizationMode是true,局部地图线程就被释放, 关键帧从局部地图中删除.
        if(mbDeactivateLocalizationMode)
        {
            mpTracker->InformOnlyTracking(false);
            mpLocalMapper->Release();
            mbDeactivateLocalizationMode = false;
        }
    }
           

检查复位

// Check reset
    {
    unique_lock<mutex> lock(mMutexReset);
    if(mbReset)
    {
        mpTracker->Reset();
        mbReset = false;
    }
    }
           

使用

mpTracker->GrabImageMonocular

计算单目相机位姿

并用

Tcw

存储位姿

//获取相机位姿的估计结果
    cv::Mat Tcw = mpTracker->GrabImageMonocular(im,timestamp);
           

TrackMonocular 跳转 GrabImageMonocular

cv::Mat Tracking::GrabImageMonocular(const cv::Mat &im,const double &timestamp)
{
    mImGray = im;
           

将输入的rgb图像转换为灰度图

// Step 1 :将彩色图像转为灰度图像
    //若图片是3、4通道的,还需要转化成灰度图
    if(mImGray.channels()==3)
    {
        if(mbRGB)
            cvtColor(mImGray,mImGray,CV_RGB2GRAY);
        else
            cvtColor(mImGray,mImGray,CV_BGR2GRAY);
    }
    else if(mImGray.channels()==4)
    {
        if(mbRGB)
            cvtColor(mImGray,mImGray,CV_RGBA2GRAY);
        else
            cvtColor(mImGray,mImGray,CV_BGRA2GRAY);
    }
           

构造帧Frame,如果之前没有初始化过帧,则执行下面代码,有没有初始化的区别是,ORB特征提取器使用的不同,未初始化的帧使用

mpIniORBextractor

,会提取2倍特征点:

// Step 2 :构造Frame
    //判断该帧是不是初始化
    if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET) //没有成功初始化的前一个状态就是NO_IMAGES_YET
        mCurrentFrame = Frame(
            mImGray,
            timestamp,
            mpIniORBextractor,      //初始化ORB特征点提取器会提取2倍的指定特征点数目
            mpORBVocabulary,
            mK,
            mDistCoef,
            mbf,
            mThDepth);
           

初始化的帧使用的是

mpORBextractorLeft

,正常提取指定数目特征点:

else
        mCurrentFrame = Frame(
            mImGray,
            timestamp,
            mpORBextractorLeft,     //正常运行的时的ORB特征点提取器,提取指定数目特征点
            mpORBVocabulary,
            mK,
            mDistCoef,
            mbf,
            mThDepth);
           

GrabImageMonocular 跳转 Frame

Frame构造函数:

Frame::Frame(const cv::Mat &imGray, const cv::Mat &imDepth, const double &timeStamp, ORBextractor* extractor,ORBVocabulary* voc, cv::Mat &K, cv::Mat &distCoef, const float &bf, const float &thDepth)
    :mpORBvocabulary(voc),mpORBextractorLeft(extractor),mpORBextractorRight(static_cast<ORBextractor*>(NULL)),
     mTimeStamp(timeStamp), mK(K.clone()),mDistCoef(distCoef.clone()), mbf(bf), mThDepth(thDepth)
{
           

帧ID自增1,其中

nNextId

是static类的静态成员变量,在全局区初始化

// Step 1 帧的ID 自增
    mnId=nNextId++;
           

读取图像金字塔参数

// Step 2 计算图像金字塔的参数 
    //获取图像金字塔的层数
    mnScaleLevels = mpORBextractorLeft->GetLevels();
    //获取每层的缩放因子
    mfScaleFactor = mpORBextractorLeft->GetScaleFactor();    
    //计算每层缩放因子的自然对数
    mfLogScaleFactor = log(mfScaleFactor);
    //获取各层图像的缩放因子
    mvScaleFactors = mpORBextractorLeft->GetScaleFactors();
    //获取各层图像的缩放因子的倒数
    mvInvScaleFactors = mpORBextractorLeft->GetInverseScaleFactors();
    //TODO 也是获取这个不知道有什么实际含义的sigma^2
    mvLevelSigma2 = mpORBextractorLeft->GetScaleSigmaSquares();
    //计算上面获取的sigma^2的倒数
    mvInvLevelSigma2 = mpORBextractorLeft->GetInverseScaleSigmaSquares();
           

提取ORB特征点,第一个参数0表示左图,1表示右图

/** 3\. 提取彩色图像(其实现在已经灰度化成为灰度图像了)的特征点 \n Frame::ExtractORB() */

    // ORB extraction
    // Step 3 对图像进行提取特征点, 第一个参数0-左图, 1-右图
    ExtractORB(0,imGray);
           

Frame 跳转 ExtractORB

根据左图右图选择不同的提取特征点函数

这里

(*mpORBextractorLeft)

(*mpORBextractorRight)

是用了ExtractORB类中重载的括号

( )

是仿函数

3.4为什么要重載小括号运算符 operator0

可以用于仿函数(一个可以实现函数功能的对象)

仿函数( functor)又称为函数对象( function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator()运算符

1.仿函数可有拥有自己的数据成员和成员变量,这意味着这意味着仿函数拥有状态。这在一般函数中是不可能的。

2.仿函数通常比一般函数有更好的速度

类似于函数指针??

void Frame::ExtractORB(int flag, const cv::Mat &im)
{
    // 判断是左图还是右图
    if(flag==0)
        // 左图的话就套使用左图指定的特征点提取器,并将提取结果保存到对应的变量中 
        // 这里使用了仿函数来完成,重载了括号运算符 ORBextractor::operator() 
        (*mpORBextractorLeft)(im,               //待提取特征点的图像
                              cv::Mat(),        //掩摸图像, 实际没有用到
                              mvKeys,           //输出变量,用于保存提取后的特征点
                              mDescriptors);    //输出变量,用于保存特征点的描述子
    else
        // 右图的话就需要使用右图指定的特征点提取器,并将提取结果保存到对应的变量中 
        (*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}
           

ExtractORB 跳转 operator()

void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
                      OutputArray _descriptors)
{
           

判断图像是否为空,判断格式

// Step 1 检查图像有效性。如果图像为空,那么就直接返回
    if(_image.empty())
        return;

    //获取图像的大小
    Mat image = _image.getMat();
    //判断图像的格式是否正确,要求是单通道灰度值
    assert(image.type() == CV_8UC1 );
           

构建图像金字塔

ComputePyramid

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

operator() 跳转 ComputePyramid

void ORBextractor::ComputePyramid(cv::Mat image)
{
           

计算每个图层的信息

//开始遍历所有的图层
    for (int level = 0; level < nlevels; ++level)
    {
           

每个图层缩放系数和图像大小信息

其中

sz

指的是源图像大小,而

wholeSize

指的是扩充后图像大小:

ORB2单目读代码笔记3--ORB特征提取构造函数仿函数、图像帧构造函数、计算图像金字塔并进行扩充
//获取本层图像的缩放系数
        float scale = mvInvScaleFactor[level];
        //计算本层图像的像素尺寸大小
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
        //全尺寸图像。包括无效图像区域的大小。将图像进行“补边”,EDGE_THRESHOLD区域外的图像不进行FAST角点检测
        Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
           

定义新变量

temp

存储扩展图像,用Rect把原图像抠出来赋值给

temp

,于是

temp

就变成了大小为扩展图像,内部有一个大小为原图像的区域的Mat对象.

然后将

temp

传给当前金字塔层.

其中,

Rect

是cv矩阵类,四个参数分别是x坐标,y坐标,宽,高:

ORB2单目读代码笔记3--ORB特征提取构造函数仿函数、图像帧构造函数、计算图像金字塔并进行扩充
// 定义了两个变量:temp是扩展了边界的图像,masktemp作为掩膜
        Mat temp(wholeSize, image.type()), masktemp;
        // mvImagePyramid 刚开始时是个空的vector<Mat>
        // 把图像金字塔该图层的图像指针mvImagePyramid指向temp的中间部分(这里为浅拷贝,内存相同)
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
           

如果当前层数不为0,将原图像按一定缩放系数处理后拷贝到当前金字塔层中间的矩形区域

然后把当前层的图像拷贝给

temp

copyMakeBorder

作用是将第一个参数拷贝到第二个参数,边界留白长度为第三四五六个参数

// Compute the resized image
        //计算第0层以上resize后的图像
        if( level != 0 )
        {
            //!  原代码mvImagePyramid 并未扩充,上面resize应该改为如下
            resize(image,                   //输入图像
                   mvImagePyramid[level],   //输出图像
                   sz,                      //输出图像的尺寸
                   0,                       //水平方向上的缩放系数,留0表示自动计算
                   0,                       //垂直方向上的缩放系数,留0表示自动计算
                   cv::INTER_LINEAR);       //图像缩放的差值算法类型,这里的是线性插值算法

            //把源图像拷贝到目的图像的中央,四面填充指定的像素。图片如果已经拷贝到中间,只填充边界
            //这样做是为了能够正确提取边界的FAST角点
            //EDGE_THRESHOLD指的这个边界的宽度,由于这个边界之外的像素不是原图像素而是算法生成出来的,所以不能够在EDGE_THRESHOLD之外提取特征点            
            copyMakeBorder(mvImagePyramid[level],                   //源图像
                           temp,                                    //目标图像(此时其实就已经有大了一圈的尺寸了)
                           EDGE_THRESHOLD, EDGE_THRESHOLD,          //top & bottom 需要扩展的border大小
                           EDGE_THRESHOLD, EDGE_THRESHOLD,          //left & right 需要扩展的border大小
                           BORDER_REFLECT_101+BORDER_ISOLATED);     //扩充方式,opencv给出的解释:

            /*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'
            */          
            //BORDER_ISOLATED   表示对整个图像进行操作
            // https://docs.opencv.org/3.4.4/d2/de8/group__core__array.html#ga2ac1049c2c3dd25c2b41bffe17658a36

       
           

如果当前层数为0,将原图像拷贝到

temp

中间矩形部分

copyMakeBorder

作用是将第一个参数拷贝到第二个参数,边界留白长度为第三四五六个参数

else
        {
            //对于第0层未缩放图像,直接将图像深拷贝到temp的中间,并且对其周围进行边界扩展。此时temp就是对原图扩展后的图像
            copyMakeBorder(image,           //这里是原图像
                           temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101);            
        }
           

最后将

temp

拷贝给当前金字塔层,当前层金字塔构建完毕,继续进入下一层

//! 原代码mvImagePyramid 并未扩充,应该添加下面一行代码
        mvImagePyramid[level] = temp;
    }

}
           

ComputePyramid 结束,返回 operator()