ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分

文章目录

  • 什么是SLAM?
    • 前端:视觉里程计(Visual Odometry)
    • 后端:非线性优化(Optimization)
    • 回环检测(Loop Closing)
  • ORB_SLAM系列概述
    • ORB_SLAM
    • ORB_SLAM2
  • 结合ORB_SLAM2的代码进行分析
    • 主要文件介绍
    • 跑通ORB_SLAM2
    • 紧跟TrackMonocular
    • 什么是ORB特征提取
      • 特征提取oFast
        • Fast特征
        • oFAST特征
      • 描述方法BRIEF
        • 特征描述子
        • 二进制描述子
        • BRIEF
        • steered BRIEF
        • rBRIEF
    • Tracking之ExtractORB
      • 构建图像金字塔
      • 提取特征点并进行筛选(oFAST):ComputeKeyPointsOctTree
        • 八叉树筛选特征点DistributeOctTree
        • 计算特征点方向computeOrientation
      • 计算特征点描述子

什么是SLAM?

参考大牛讲堂|SLAM第一篇:基础知识。
SLAM全称为Simultaneous Localization and Mapping(即时定位与地图构建技术)

传感器分为激光和视觉两大类。基于视觉传感器的SLAM称为视觉SLAM。视觉SLAM又分为单目、双目(多目)和RGB-D。实现难度依次降低。

视觉SLAM分为四个模块:视觉里程计(VO)、后端、建图和回环检测。

前端:视觉里程计(Visual Odometry)

视觉里程计估计两个时刻机器人的相对运动。对于相机而言,它在欧式空间运动,通常需要估计一个三维空间的变换矩阵。求解这个矩阵是VO的核心问题。

求解的思路分为基于特征和不基于特征的方法。

基于特征的方法是目前VO的主流。简单来说,对于两幅图像,首先提取图像中的特征,然后根据两幅图像的特征匹配,计算相机的变换矩阵。最常用的是点特征,比如Harris角点、SIFT、ORB…

如果使用RGB-D相机,利用已知深度的特征点,可以直接估计相机的运动。

给定一组特征点以及他们自建的配对关系,来求解相机的姿态,该问题被称为PnP问题。

在单目成像中,我们只知道这两组点的像素坐标,而在双目和RGB-D的配置中,还知道特征点离相机的距离。所以该问题就有了很多形式:

  1. 2D-2D:通过两个图像的像素位置来估计相机运动;(对极几何)
  2. 3D-2D:知道一组点的3D坐标和另一组点的2D坐标,求相机运动;(PnP求解算法)
  3. 3D-3D:两组点的3D坐标已知,估计相机运动。(ICP方法,不需要匹配关系)

综上所述,VO之求解相对运动的过程是:

  1. 确定特征点;
  2. 特征匹配;
  3. 根据坐标,以及匹配关系进行求解

另外,VO还需要完成对于局部地图的估计。

后端:非线性优化(Optimization)

知道了相机的运动之后,机器人的轨迹也显而易见了。但是视觉里程计和普通里程计存在着累积误差的问题,因此,需要将帧间的相对运动放到后端中进行加工处理。

早期的SLAM后端采用滤波器的方式。21世纪之后,引入了Bundle Adjustment(捆集优化),考虑过去所有帧中的信息,通过优化将误差平均分配到每一次观测中。因为其常常以图的形式给出,也称之为Graph Optimization。

回环检测(Loop Closing)

指的是机器人识别曾到达场景的能力。如果检测成功,则可以显著地减小累积误差。

ORB_SLAM系列概述

ORB_SLAM是一个基于特征点的实时单目(Monocular)SLAM。其包括的模块有:

  1. Tracking;
  2. Mapping;
  3. Relocalization;
  4. Loop closing。

ORB_SLAM2在其基础上支持标定后的双目相机和RGB-D相机。

详细的介绍可以参考ORB-SLAM2详解(一)简介。

ORB_SLAM

ORB_SLAM的主要线程:
ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分_第1张图片

Tracking:1. 从图像中提取ORB特征;2. 根据上一帧进行姿态估计或者通过全局重定位初始化位姿; 3. 跟踪已经重建的局部地图;4. 确定新的关键帧(人大代表从人民群众中来)。

LOCAL MAPPING属于局部优化部分及建图、LOOP CLOSING属于回环检测部分,在本文不做介绍。

ORB_SLAM2

ORB_SLAM2的主要线程:
ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分_第2张图片

结合ORB_SLAM2的代码进行分析

主要文件介绍

  1. System.cpp
  2. Initializer.cpp
  3. Tracking.cpp
  4. LocalMapping.cpp
  5. LoopClosing.cpp
  6. Viwer.cpp

参考内容有代码文字介绍以及代码视频介绍。
接下来,我们随着数据流,对部分代码进行剖析。

此代码的核心是多线程思想,选取多线程的理由在于:

  1. 加快速度;
  2. 任务的不可预料。有关键帧才执行,否则Mapping与LoopClosing一直在sleep。

跑通ORB_SLAM2

./Examples/Monocular/mono_tum Vocabulary/ORBvoc.txt Examples/Monocular/TUM1.yaml ./Data/rgbd_dataset/freiburg1_xyz

可以知道,我们运行了mono_tum.cpp文件,其代码如下:

void LoadImages(const string &strFile, vector<string> &vstrImageFilenames,
                vector<double> &vTimestamps);

int main(int argc, char **argv)
{	
    if(argc != 4)	// 在命令行中,我们需要输入四个指令 1.运行文件;2.不知道;3.参数文件路径;4.数据集路径
    {
        cerr << endl << "Usage: ./mono_tum path_to_vocabulary path_to_settings path_to_sequence" << endl;
        return 1;
    }

    // Retrieve paths to images
    vector<string> vstrImageFilenames; // filename数组
    vector<double> vTimestamps; // timestamp数组
    string strFile = string(argv[3])+"/rgb.txt"; // 文件路径 该文件下记录着每一帧的时间戳和名字
    LoadImages(strFile, vstrImageFilenames, vTimestamps); // 组织数据

    int nImages = vstrImageFilenames.size();

    // Create SLAM system. It initializes all system threads and gets ready to process frames.
    // 将System类实例化,其中的三个线程也跟着启动
    // ORB词库的路径、配置文件的路径、传感器类型、是否使用Viwer
    ORB_SLAM2::System SLAM(argv[1],argv[2],ORB_SLAM2::System::MONOCULAR,true);

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

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

    // 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);

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

        double ttrack= std::chrono::duration_cast<std::chrono::duration<double> >(t2 - t1).count();

        vTimesTrack[ni]=ttrack;

        // Wait to load the next frame
        double T=0;
        if(ni<nImages-1)
            T = vTimestamps[ni+1]-tframe;
        else if(ni>0)
            T = tframe-vTimestamps[ni-1];

        if(ttrack<T)
            usleep((T-ttrack)*1e6);
    }

    // Stop all threads
    SLAM.Shutdown();

    // Tracking time statistics
    sort(vTimesTrack.begin(),vTimesTrack.end());
    float totaltime = 0;
    for(int ni=0; ni<nImages; ni++)
    {
        totaltime+=vTimesTrack[ni];
    }
    cout << "-------" << endl << endl;
    cout << "median tracking time: " << vTimesTrack[nImages/2] << endl;
    cout << "mean tracking time: " << totaltime/nImages << endl;

    // Save camera trajectory
    SLAM.SaveKeyFrameTrajectoryTUM("KeyFrameTrajectory.txt");

    return 0;
}

void LoadImages(const string &strFile, vector<string> &vstrImageFilenames, vector<double> &vTimestamps)
{
	// 文件路径;存储文件名称的数组;存储时间戳的数组
    ifstream f;
    f.open(strFile.c_str());

    // skip first three lines
    // 把三行注释给读掉
    string s0;
    getline(f,s0);
    getline(f,s0);
    getline(f,s0);
	// 把这些数据放到两个数组中去
    while(!f.eof())
    {
        string s;
        getline(f,s);
        if(!s.empty())
        {
            stringstream ss;
            ss << s;
            double t;
            string sRGB;
            ss >> t;
            vTimestamps.push_back(t);
            ss >> sRGB;
            vstrImageFilenames.push_back(sRGB);
        }
    }
}

紧跟TrackMonocular

// system文件中初始化Tracking类
mpTracker = new Tracking(this, mpVocabulary, mpFrameDrawer, mpMapDrawer,
                             mpMap, mpKeyFrameDatabase, strSettingsFile, mSensor);
// 读入图像和时间戳                             
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);
    }

    // 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;
    }
    }
	// 分析一下图像
    cv::Mat Tcw = mpTracker->GrabImageMonocular(im,timestamp);
    
    unique_lock<mutex> lock2(mMutexState);
    // 更新一下跟踪的状态
    mTrackingState = mpTracker->mState;
    // 更新一下地图点
    mTrackedMapPoints = mpTracker->mCurrentFrame.mvpMapPoints;
    // 更新一下关键点
    mTrackedKeyPointsUn = mpTracker->mCurrentFrame.mvKeysUn;

    return Tcw;
}

什么是ORB特征提取

参考ORB特征提取详解。

ORB(Oriented Fast and Rotated BRIEF)是一种快速的特征点提取/定位和描述的算法。

显而易见,其分为特征点提取(从每一幅图像中寻找那些能在其他图像中较好匹配的位置)和特征点描述(把检测到的关键点周围的每一个区域转化成一个更紧凑和稳定的描述子,通常是一个字串,二进制或十进制)两大部分。

在这里我们更宏观地介绍一下这些计算机视觉中的概念:为了获得特征点及其之间的对应关系,主要有两种方法。第一种是在第一幅图像中寻找那些可以使用局部搜索方法来精确跟踪的特征;第二种方法是在所有考察的图像中独立地检测特征点然后基于他们的局部表现进行匹配。

前一种方式在图像以相近的视角或被快速地连续拍摄时更为合适;后者适合于图像中存在大量的运动或者表观变化的时候。

后一种方式经历:特征检测、特征描述以及特征匹配。

特征提取oFast

特征点指的是有代表性的点:像素变化大、像素梯度大;

特征点有三个特性:尺度不变性、亮度不变性和旋转不变性。

Fast特征

ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分_第3张图片

oFAST特征

  1. 改进一:特征计算的简化
    我们一般取2、6、10、14这四个位置的点,如果有三个及以上和P不同,则认为它是一个特征点。
  2. 改进二:非最大值抑制
    取一个领域,只保留其中响应大的点,目的是为了简化计算。
  3. 改进三:金字塔
    FAST对亮度有鲁棒性,但没有尺度不变性,因为每次取的点的范围是一样的。
  4. 改进四:方向
    FAST没有旋转不变性,于是计算一个邻域内灰度值的质心,然后将中心点和质心的连线得到x轴,服务于后续的rBRIEF。

描述方法BRIEF

特征描述子

好的描述子的性质:

  1. 鲁棒性:对光照和几何变换;
  2. 独特性:能区分开;
  3. 紧致性:维度低;
  4. 计算搞笑:可以快速计算特征描述子并进行高效比较。

常见的特征描述子:HOG、SIFT、SURF、GLOH、BRIEF、ORB、BRISK、FREAK。

描述子就相当于一个学号,不同图的相同特征点的描述子应该是接近甚至一样的。

二进制描述子

  1. 为兴趣点选择一个邻域图像块;
  2. 为图像块内选择一系列像素点对;
  3. 对于每一对像素点,比较两者间的亮度,A比B亮,则结果为1;
  4. 把所有比较值拼接起来得到一个二进制串;
  5. 通过汉明距离来快速比较。

不同二进制描述子的主要区别在于选择像素对的策略,即像素对的选择方式和选择顺序。

BRIEF

ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分_第4张图片
序列的顺序是根据生成点对的顺序决定的。

steered BRIEF

增加了旋转不变性(BRIEF不具备)。

算出来点对之后,乘以了一个旋转矩阵。这个旋转矩阵是在oFAST部分计算得到的。

将得到的新的点对再按照BRIEF的操作。

rBRIEF的二进制串均值没有很多接近0.5,这说明了二进制串里面1和0是有偏向性分布的,1比较多或者0比较多,所以两者之间区分度不够。

sreered BRIEF的缺陷:描述子之间相似,相关性高,区分度低。

rBRIEF

假设有一张图像,其中有五个特征,每个特征的邻域里面取很多点对。于是我们有了一个矩阵,其shape为5*256,每一行都是一个BRIEF串。

我们对每一列计算一个均值,然后依据接近0.5的程度进行排序,越接近0.5越靠前。

把第一个列的数拿出来放到R矩阵,然后第二列和它做一个相关性计算,如果小于K,我们就认为这一列数据不怎么相关,比较好,把它放进R。然后下一列和R中所有向量计算相关性。不断遍历,直到R的大小到达256(可以设置)。

在应用中,对这些点还进行旋转,所以称为ORB描述子。

Tracking之ExtractORB

上一节已经介绍完了ORB特征提取的一个理论性算法。在这一章,我们介绍一下其是如何实现的。

参考ORB-SLAM2代码详解02: 特征点提取器ORBextractor。

在图像进来之后呢,会先进到Frame类中,此时,图像就开始与ORBExtractor打交道了。

在Frame.cpp文件中的ExtractORB操作着图像的提取器。

void Frame::ExtractORB(int flag, const cv::Mat &im)
{
    if(flag==0)
        (*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
    else
        (*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

这两个类型其实是对类指针取出类,执行了一个operator操作。这来到了ORBextractor.cpp。

void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
                      OutputArray _descriptors)
{ 
	// step1 检查图像的有效性
    if(_image.empty())
        return;

    Mat image = _image.getMat();
    assert(image.type() == CV_8UC1 );

    // step2 构建图像金字塔
    ComputePyramid(image);

	// step3 计算特征点进行八叉树筛选
    vector < vector<KeyPoint> > allKeypoints;
    ComputeKeyPointsOctTree(allKeypoints);

    Mat 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();
    }

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

	// step4 遍历每一层图像,计算描述子
    int offset = 0;
    for (int level = 0; level < nlevels; ++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(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);

        // Compute the descriptors
        Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
        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());
    }
}

构建图像金字塔

对于每层图像,执行两步操作:

  1. 将图像缩放到mvInvScaleFactor对应的尺寸;
  2. 在图像外补充一圈厚度为19的padding(提取FAST特征点需要半径为3的邻域,计算ORB描述子需要半径为16的邻域)。
void ORBextractor::ComputePyramid(cv::Mat image)
{
    for (int level = 0; level < nlevels; ++level)
    {
    	// 计算缩放和补padding之后的图像尺寸
        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);
        Mat temp(wholeSize, image.type()), masktemp;

		// mvImagePyramid用于存储初始图像
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
        // Compute the resized image
        // 通过插值的手段为这些将图像逐步缩放到初始图像
        if( level != 0 )
        {
            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);            
        }
    }

}

提取特征点并进行筛选(oFAST):ComputeKeyPointsOctTree

找特征点的方法到很简单,但其融入了一个很重要的思想:要求特征点均匀分布在图像中的所有部分。

在这里用了两个技巧:

  1. 分CELL搜索特征点,搜索不到的话就用低的阈值再找一遍;
  2. 对所有的特征点进行八叉树筛选,对过于密集的部分,只取其中响应值比较大的,也就是非极大值抑制。
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
    allKeypoints.resize(nlevels); // 存了每一层最后的特征点

    const float W = 30;
	
    for (int level = 0; level < nlevels; ++level)
    {
    	// 计算图像边界
        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;
		
		// 存储需要平均分配的特征点
        vector<cv::KeyPoint> vToDistributeKeys;
        vToDistributeKeys.reserve(nfeatures*10);
		
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);

        const int nCols = width/W;
        const int nRows = height/W;
        const int wCell = ceil(width/nCols);
        const int hCell = ceil(height/nRows);

		// step1 遍历每行和每列,依次分别用高低阈值搜索FAST特征点
        for(int i=0; i<nRows; i++)
        {
            const float iniY =minBorderY+i*hCell;
            float maxY = iniY+hCell+6;

            if(iniY>=maxBorderY-3)
                continue;
            if(maxY>maxBorderY)
                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;

                vector<cv::KeyPoint> vKeysCell;
                // 先用高阈值搜索FAST特征点
                // 该函数本身实现了高效计算和非极大值抑制,尺度不变性是ORB额外写的。
                FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                     vKeysCell,iniThFAST,true);
				// 如果没找到,再用低阈值搜索
                if(vKeysCell.empty())
                {
                    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).pt.x+=j*wCell;
                        (*vit).pt.y+=i*hCell;
                        vToDistributeKeys.push_back(*vit);
                    }
                }

            }
        }

        vector<KeyPoint> & keypoints = allKeypoints[level];
        keypoints.reserve(nfeatures);
		// step2 对提取到的特征点进行八叉树筛选
        keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
                                      minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);

        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);
}

八叉树筛选特征点DistributeOctTree

代码比较繁琐,在此处只介绍其思想。
ORB_SLAM2 代码分析及介绍(视觉VO及重定位,Tracking)第一部分_第5张图片

计算特征点方向computeOrientation

使用特征点与其周围半径为19的圆的重心的连线为该特征点的方向。

将特征点假设为原点,x轴正方向为0,采用FastAtan2函数,计算得到向量和重心的夹角,范围为 [ − π , π ] [-\pi,\pi] [π,π]

计算特征点描述子

计算BRIEF描述子的核心步骤是在特征点周围半径为16的圆域内选取256对点对,每个点对内比较得到1位,共得到256位的描述子,为保计算的一致性,工程上使用特定设计的点对pattern,在程序里被硬编码为成员变量了.

所以我们预先设定好的坐标集实际上是一个数组,共有25622个数。

在computeOrientation()中我们求出了每个特征点的主方向,在计算描述子时,应该将特征点周围像素旋转到主方向上来计算;为了编程方便,实践上对pattern进行旋转.

static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
                               const vector<Point>& pattern)
{
    descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);
	// 对每一个keypoint
    for (size_t i = 0; i < keypoints.size(); i++)
        computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));
}

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;
    
	// 旋转公式
	// x'= xcos(θ) - ysin(θ)
    // y'= xsin(θ) + ycos(θ)
    // cvRound 返回和参数最接近的整数值,即四舍五入
    #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; // desc为该特征点的串,是按照字节存储的,
    }

    #undef GET_VALUE
}

你可能感兴趣的:(智能机器人,SLAM)