资讯详情

ORB-SLAM2 代码解读(二):跟踪线程



0. 总体介绍跟踪线程

?? Tracking 系统主线程中的线程运行,,可简单理解为 SLAM 前端里程计部分,但也有一些优化。

0.1 流程简述

  • :在 System SLAM() 初始化 SLAM 系统时
    mpTracker = new Tracking(this,                              mpVocabulary,   //字典                          mpFrameDrawer,    //帧绘制器                          mpMapDrawer,   ///地图绘制器                          mpMap,     //地图                          mpKeyFrameDatabase,   //关键帧地图                          strSettingsFile,    //配置文件路径                          mSensor);    //传感器类型 
    • Tracking.ccTracking() 构造函数中主要初始化系统的一些参数包括
      • 设置特征时注意提取数量,
        // 假如是双眼,tracking 也将在这个过程中使用 mpORBextractorRight 作为右目提取器的特征 if(sensor==System::STEREO)     mpORBextractorRight = new ORBextractor(nFeatures,fScaleFactor,nLevels,fIniThFAST,fMinThFAST); // 使用单目初始化时 mpIniORBextractor 作为特征提取器 if(sensor==System::MONOCULAR)     mpIniORBextractor = new ORBextractor(2*nFeatures,fScaleFactor,nLevels,fIniThFAST,fMinThFAST); 
    • 注意初始化 SLAM 该系统还初始化并运行了其他三个线程:局部建图线程、闭环检测线程和可视化线程,但
    • mono_tum.cc 例如,发送单张图片和时间戳SLAM.TrackMonocular(im,tframe)
      • TrackMonocular() 实时跟踪模式、定位模式、重置等。
      • 然后再传达图像和时间戳 Tracking::GrabImageMonocular() 函数。
    • Tracking::GrabImageMonocular() 函数
      • 将图像转换为灰度图;
      • Frame::Frame()(第一帧提取orb特征点数量多,是后帧的两倍);
      • 进入 Track() 开始跟踪。
    • 返回当前帧的相机位置。
    • li>
      enum eTrackingState{ 
                 
              SYSTEM_NOT_READY=-1,        // 系统没有准备好的状态,一般就是在启动后加载配置文件和词典文件时候的状态
              NO_IMAGES_YET=0,            // 当前无图像,图像复位过、或者第一次运行
              NOT_INITIALIZED=1,          // 有图像但是没有完成初始化
              OK=2,                       // 正常时候的工作状态
              LOST=3                      // 系统已经跟丢了的状态
              };
      //跟踪状态
      eTrackingState mState;
      
      • A. Tracking::MonocularInitialization()(需要两帧);
      • B. Tracking::StereoInitialization()(由于具有深度信息,直接生成MapPoints)。
      • mbOnlyTracking = false
        • 如果 mState==OK
          • 先检查并更新上一帧:CheckReplacedInLastFrame()
          • 如果当前运动模型为空或刚完成重定位,则TrackReferenceKeyFrame()
          • 如果有运动速度,则使用(跟踪上一帧):TrackWithMotionModel()
            • 如果恒速运动跟踪失败,则再考虑跟踪参考帧:TrackReferenceKeyFrame()
        • 如果,则只能重定位:Relocalization()
      • mbOnlyTracking = true
        • 如果mState==LOST,只能进行重定位:bOK = Relocalization();
        • 如果
          • 如果跟踪了较多的地图点 mbVO=false
            • 情形 A: 如果有运动速度,则使用bOK = TrackWithMotionModel();
            • 情形 B: 如果当前运动模型为空或刚完成重定位,则bOK = TrackReferenceKeyFrame();
          • 如果跟踪地图点较少(少于 10 )
            • 步骤 ①: 如果有运动速度,则使用bOK = TrackWithMotionModel();
            • 步骤 ②: 同时使用得到当前帧的位姿
            • 步骤 ③: 两者只要有一个成功了,则认为跟踪成功,并且
      • TrackLocalMap()
      • 局部地图 local map:包括当前帧、当前帧的MapPoints、当前关键帧与其它关键帧共视关系;
      • :更新关键帧和地图点,更新运动模型,清除当前帧中不好的点;
      • 步骤 1:判断bool Tracking::NeedNewKeyFrame()
        • : 很⻓时间没有插入关键帧,局部地图空闲,跟踪快要跟丢,跟踪地图的 MapPoints 地图点比例比较少;
      • 步骤 2:创建void Tracking::CreateNewKeyFrame()

0.2 两种工作模式

 在 Pangolin 可视化界面可以选择两种工作模式

  • mbOnlyTracking = false
    • 线程的同时有
  • mbOnlyTracking = true
    • 不插入新的关键帧,不添加新的地图点,局部地图线程不工作,而且回环检测线程也不会工作,

0.3 五种跟踪状态

    • 系统没有准备好的状态,一般就是在启动后加载配置文件和词典文件时候的状态;
    • 当前无图像,图像复位过、或者第一次运行;
    • 当等待到了新的一帧,将线程状态改变为 NOT_INITIALIZED。
    • 有图像但是跟踪线程没有完成初始化的状态;
    • 单目相机至少需要两帧来初始化,第一帧建立初始化器,设定该帧作为初始化参考帧。第二帧作为匹配帧,通过这两帧之间进行匹配,进而通过单应性矩阵和基础矩阵计算两帧之间的位姿以及匹配点的深度信息。初始化成功之后初始化地图。
    • 双目或 RGB-D 相机只需要一帧,设置初始帧位姿,并初始化地图。
    • 经过初始化的系统追踪线程就转为 OK 状态,在没有丢帧或者是复位的情况下系统将一直处于 OK 状态;
    • 处于OK状态的系统就可以进行位姿估计,地图点追踪。
    • 跟踪失败,需要进行重定位。

0.4 三种跟踪模式

跟踪线程用了三种模式进行跟踪, 分别是

  •   假设物体处于匀速运动,那么可以。上一帧的速度可以通过前面几帧的位 姿计算得到。这个模型,比如匀速运动的汽⻋、机器人、人 等。而对于运动比较随意的目标,当然就会失效了。此时就要用到下面两个模型。
  •   假如 motion model 已经失效,那么首先可以 。毕竟当前帧和上一个关键帧的距离还不是很远。作者利用了 bag of words ( BoW )来加速匹配。,单词的描述子肯定比较相近 ,用单词的描述子进行匹配可以
    • 首先,计算当前帧的 BoW,并设定初始位姿为上一帧的位姿;
    • 其次,根据位姿和 BoW 词典来寻找特征匹配(参⻅ ORB − SLAM (六)回环检测);
    • 最后,利用匹配的特征优化位姿(参⻅ ORB − SLAM (五)优化)。
  •   假如当前帧与最近邻关键帧的匹配也失败了,意味着此时,无法确定其真实位置。此时,只有去,看能否找到合适的位置。
    • 首先,计算
    • 其次,利用 BoW 词典选取若干关键帧作为备选(参⻅ ORB − SLAM (六)回环检测);,选取部分距离短的候选关键帧
    • 然后,当前帧和候选关键帧分别进行,寻找;
    • 最后,利用( RANSAC 框架下,因为相对位姿可能比较大,局外点会比较多)。
    • 如果有关键帧有足够多的内点,那么选取该关键帧优化出的位姿。
    • ,从 LastFrame (上一普通帧)直接预测出(乘以一个固定的位姿变换矩阵)当前帧的姿态;
    • 如果是(运用恒速模型后反投影发现 LastFrame 的地图点和 CurrentFrame 的特征点匹配很少),则采用,通过,获取较多匹配后,计算当前位姿;
    • 若这两者均失败,即代表 tracking 失败, mState!=OK ,则 ,在 RANSAC 框架下使用

0.5 局部地图跟踪

  前面三种,后面会通过。   一旦我们通过上面三种模型获取了初始的相机位姿和初始的特征匹配,就可以。但是投影完整的地图,在 large scale 的场景中是很耗计算而且也没有必要的,因此,这里

    • 与当前帧相连的关键帧 K1,以及与 K1 相连的关键帧 K2 ();
    • K1 、K2 对应的
    • Kf 。
    • 局部地图点
      • ① 抛弃
      • ② 抛弃观测视⻆和地图点
      • ③ 抛弃特征点的尺度和地图点的(通过高斯金字塔层数表示)
    • 计算
    • 将地图点的描述子和当前帧 ORB 特征的,需要根据地图点尺度在初始位姿获取的粗略投影位置附近搜索;
    • 根据

1. 构造帧

这部分其实是在进入 Track() 之前进行的,最主要的是进行 ORB 特征提取。

1.1 创建特征提取器

  • 在构造帧之间,初始化跟踪线程的时候,
    • tracking 过程都会用到 mpORBextractorLeft 作为特征点提取器,在单目初始化的时候,会用 mpIniORBextractor 来作为特征点提取器,两者的区别是
    // tracking过程都会用到 mpORBextractorLeft 作为特征点提取器
        mpORBextractorLeft = new ORBextractor(  nFeatures,      /* 每一帧提取的特征点数 1000 */
                                                fScaleFactor,   /* 图像建立金字塔时的变化尺度 1.2 */
                                                nLevels,        /* 尺度金字塔的层数 8 */
                                                fIniThFAST,     /* 提取fast特征点的默认阈值 20 */
                                                fMinThFAST);    /* 如果默认阈值提取不出足够fast特征点,则使用最小阈值 8 */
    // 如果是双目,tracking 过程中还会用用到 mpORBextractorRight 作为右目特征点提取器
    if(sensor==System::STEREO)
        mpORBextractorRight = new ORBextractor(nFeatures,fScaleFactor,nLevels,fIniThFAST,fMinThFAST);
    // 在单目初始化的时候,会用 mpIniORBextractor 来作为特征点提取器
    if(sensor==System::MONOCULAR)
        mpIniORBextractor = new ORBextractor(2*nFeatures,fScaleFactor,nLevels,fIniThFAST,fMinThFAST);
    
  •   构造函数位于 ORBextractor.cc 中,传入每一帧提取的特征点数量 nFeatures(1000),高斯金字塔每层之间的缩放尺度 fScaleFactor(1.2),高斯金字塔的层数 nLevels(8),Fast 角点提取时的阈值 fIniThFAST(20)和 fMinThFAST(8)。
    • 首先计算,存储在 mvScaleFactor 中,同时计算了其平方 mvLevelSigma2,其倒数 mvInvScaleFactor 及其平方的倒数 mvInvLevelSigma2
      for(int i = 1; i < nlevels; i++)
      { 
                  
          // 累乘计算得到缩放系数
          mvScaleFactor[i] = mvScaleFactor[i-1]*scaleFactor;
          // 每层图像相对于初始图像缩放因子的平方.
          mvLevelSigma2[i] = mvScaleFactor[i]*mvScaleFactor[i];
      }
      
    • 然后,保证每层的特征点数量是均匀的,用到,将每层的特征点数存放在 std::vector<int> mnFeaturesPerLevel 中;
      • 注意:第零层的特征点数是 nfeatures×(1-1/scaleFactor)/(1-(1/scaleFactor)^nlevels),然后下一层是上一层点数的 1/scaleFactor 倍,以此类推,最后一层兜底;
      // STEP 将每层的特征点数量进行均匀控制
      float nDesiredFeaturesPerScale = nfeatures*(1 - factor)/(1 - (float)pow((double)factor, (double)nlevels));
      // STEP 开始逐层计算要分配的特征点个数,顶层图像除外(看循环后面)
      for( int level = 0; level < nlevels-1; level++ )
      { 
                  
          // 分配 cvRound : 返回个参数最接近的整数值
          mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);
          // 累计
          sumFeatures += mnFeaturesPerLevel[level];
          // 乘缩放系数
          nDesiredFeaturesPerScale *= factor;
      }
      
    • std::vector<cv::Point> pattern,用于后面
      std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));
      
    • 最后通过求 x 坐标对应在半径为 HALF_PATCH_SIZE(15, 使用灰度质心法计算特征点的方向信息时,图像块的半径)的圆上的 y 坐标,
      • 代码中 umax 存储的是 u 坐标绝对值的最大值。

1.2 Frame() 构造函数构造图像帧

  Frame::Frame() 函数传入图像,时间戳,特征点提取器,字典,相机内参矩阵等参数来构造图像帧。首先把要构造金字塔的相关参数给 Frame 类中的跟金字塔相关的元素,然后提取 ORB 特征。(以下为构造过程)

  • 读取传入的特征提取器的相关参数,然后进入 Frame::ExtractORB(int flag, const cv::Mat &im) 进行 ,这一步实际上调用了重载函数操作符 ORBextractor::operator()
    (*mpORBextractorLeft)(im,	        // 待提取特征点的图像
                          cv::Mat(),	// TODO 
                          mvKeys,	// 输出变量,用于保存提取后的特征点
                          mDescriptors);// 输出变量,用于保存特征点的描述子
    
    • ORBextractor::ComputePyramid(cv::Mat image)
      • 该函数对传入的图像构造 nlevels 层的金字塔,mvImagePyramid[level] 存储金字塔第 level 层的图像,它是用 resize() 函数得到大小为 level-1 层图像的 scale倍的线性插值后的图像;
      • 为了方便做卷积计算,用 opencv 提供的 copyMakeBorder() 函数来做边界填充。
    • 计算金字塔每层的兴趣点,找到 ORBextractor::ComputeKeyPointsOctTree(allKeypoints)
      • 依次对金字塔每层图像进行操作,首先在图像四周长度为 EDGE_THRESHOLD-3 个单位的像素点的
      • 对去掉边界的,每个窗口的大小为 W=30 个像素的正方形;
      • 对每个
        • 前面网格化的目的是为了使得每个网格都有特征,从而使得特征点在图像上的分布相对均匀点;
        • 如果存在有的窗口中提取的特征点数为 0,则降低阈值 minThFAST 继续提取;
        FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),	// 待检测的图像,这里就是当前遍历到的【图像块】
            vKeysCell,	// 存储角点位置的容器
            iniThFAST,	// 检测阈值
            true);		// 使能非极大值抑制
        
        • 然后对提取出了的关键点 vKeysCell 换算出其位于 level 层的被裁掉边界的图像中的位置,并将每个窗口中的
      • 将每层的 vToDistributeKeys 送入到 ORBextractor::DistributeOctTree() 中进行
        • 确定四叉树有几个,每个初始节点的 x 方向有多少个像素
          const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));
          const float hX = static_cast<float>(maxX-minX)/nIni;
          
        • 到子提取器节点 vector<ExtractorNode*> vpIniNodes 中;
          for(size_t i=0;i<vToDistributeKeys.size();i++)
          { 
                          
              // 获取这个关键点对象
              const cv::KeyPoint &kp = vToDistributeKeys[i];
              // 按点的横轴位置,分配给属于那个图像区域的提取器节点(最初的提取器节点)
              vpIniNodes[kp.pt.x/hX]->vKeys.push_back(kp);
          }
          
        • ,当节点没有分配到关键点时就删除该节点;
        • ,当 bFinish 的值为 true 时就不再进行区域划分;
          • 首先对目前的区域进行划分,把每次划分得到的有关键点的子区域设为新的节点,将 nToExpand 参数加 1,并插入到节点列表的前边,删除掉其父节点;只要新节点中的关键点的个数超过一个,就继续划分,继续插入列表前面,继续删除父节点,,然后迭代器加以移动到下一个节点,继续划分区域;
          • 当划分的区域即或者分裂过程没有增加节点的个数时就将 bFinish 的值设为 true,不再进行划分;
          • 如果以上条件没有满足,但是满足 ((int)lNodes.size()+nToExpand * 3)>N,表示再分一次即将结束,所以开始按照特征点的数量对节点进行排序,特征点数多的节点优先划分,直到节点数量满足;
          • vSizeAndPointerToNode 是前面分裂出来的子节点(n1, n2, n3, n4)中可以分裂的节点。按照它们特征点的排序,先从特征点多的开始分裂,分裂的结果继续存储在 lNodes 中;每分裂一个节点都会进行一次判断,如果 lNodes 中的节点数量大于所需要的特征点数量,退出整个 while(!bFinish) 循环,如果进行了一次分裂,并没有增加节点数量,退出整个 while(!bFinish) 循环;
        • 总结:因为经过 FAST 提取出的关键点有很多,当划分的子区域一旦大于 mnFeaturesPerLevel[level] 根据nfeatures 算出的每一个 level 层最多的特征点数的时候就不再进行区域划分了,所以;这个函数的意义就是
        • 经过以上步骤,我们提出来 level 层在无边界图像中的特征点,并给特征点条件边界补偿及尺度信息。
      • 分层 computeOrientation(),具体方向计算在 IC_Angle() 函数中
        • 为了使得提取的特征点具有旋转不变性,需要计算每个特征点的方向;方法是计算,以
        • 可参考十四讲中 ORB 特征的介绍。
         /* @param[in] image 要进行操作的原图像(块) * @param[in] pt 要计算特征点方向的特征点的坐标 * @param[in] u_max 图像块的每一行的u轴坐标边界(1/4) * @return float 角度,弧度制 */
        static float IC_Angle(  const Mat& image,
                                Point2f pt,
                                const vector<int> & u_max)
        
    • computeDescriptors() 函数中调用 computeOrbDescriptor() 函数具体实现
      /** * @brief 计算ORB特征点的描述子 * @param[in] kpt 特征点对象 * @param[in] img 提取出特征点的图像 * @param[in] pattern 随机采样点集 * @param[out] desc 用作输出变量,保存计算好的描述子,长度为32*8bit */
      static void computeOrbDescriptor(const KeyPoint& kpt,	//特征点对象
                                       const Mat& img, 	//提取出特征点的图像
                                       const Point* pattern,	//随机采样点集
                                       uchar* desc)		//用作输出变量,保存计算好的描述子
      
      • ORB 使用 BRIEF 作为特征描述子,原始的 BRIEF 描述子不具有方向信息,这里就是,称之为 Steer BRIEF 描述子使其具有较好的
      • 在计算的时候需要将这里选取的随机点点集 pattern 的 x 轴方向旋转到特征点的方向,并获得随机点集中某个 idx 所对应的点的灰度;
      • brief 描述子由 32 * 8 位组成,其中每一位是来自于,所以每比较出 8bit 结果,需要 16 个随机点(这也就是为什么 pattern 需要+=16);
      • 通过对随机点像素灰度的比较,得出 BRIEF 描述子,一共是 32 * 8 = 256 位。
  • 检查是否成功提取了本帧的特征点,如果
  • 对提取的 Frame::UndistortKeyPoints()
    • 调用 opencv 提供的 cv::undistortPoints 进行畸变矫正
  • ,先默认所有的地图点均为内点
    // 初始化存储地图点句柄的vector
    mvpMapPoints = vector<MapPoint*>(N,static_cast<MapPoint*>(NULL));
    // 开始认为默认的地图点均为inlier
    mvbOutlier = vector<bool>(N,false);
    
  • 判断是否需要进行进行,这个过程一般是在第一帧或者是重定位之后进行;
  • Frame::AssignFeaturesToGrid()
    • 先创建一个 std::vector<std::size_t> mGrid[FRAME_GRID_COLS][FRAME_GRID_ROWS] 空间存储的是每个图像网格内特征点的 id;
    • 从类的成员变量中获取的每一个特征点
      const cv::KeyPoint &kp = mvKeysUn[i];
      
      • 并利用 Frame::PosInGrid() 找到该特征点所处的网格,输出为指定的图像特征点所在的图像网格的横纵 id(其实就是图像
    • 根据上一步返回的网格坐标,将该特征点分配到网格中
      if(PosInGrid(kp,nGridPosX,nGridPosY))
          mGrid[nGridPosX][nGridPosY].push_back(i);
      

2. 初始化

  在前面构造完 Frame 图像帧之后即进入到 ,分为的情况。

2.1 单目初始化

  单目初始化通过 ,恢复出最开始

同时计算两个模型:    
用于平面场景的单应性矩阵 H(4对 3d-2d点对,线性方程组,奇异值分解)    
用于非平面场景的基础矩阵 F(8对 3d-2d点对,线性方程组,奇异值分解)    

锐单商城 - 一站式电子元器件采购平台