【学习SLAM】ORB SLAM2代码解析(1)

  • Post author:
  • Post category:其他


今天我们来一起读ORB SLAM2的代码,其实前几个月的时候,在博客写过一些自己的理解,但是不是很详细。然后最近再用ORB SLAM2跑一些室内的数据集,所以会更加详细一些。

我们按照github上面的说明搭建好环境,然后下载对应的代码,发现也是一个CMAKE工程,然后我们进去对应的CMakeLists.txt,发现是把对应几个.cc文件编译成可执行文件。分别是:把rgbd_tum.cc编译成rgbd_tum可执行文件;把stereo_kitti.cc编译成stereo_kitti可执行文件;把stereo_euroc.cc编译成stereo_euroc可执行文件;把mono_tum.cc编译成mono_tum可执行文件;把mono_kitti.cc编译成mono_kitti可执行文件;把mono_euroc.cc编译成mono_euroc可执行文件;


 
 
  1. add_executable(rgbd_tum
  2. Examples/ RGB-D/rgbd_tum.cc)
  3. add_executable(stereo_kitti
  4. Examples/Stereo/stereo_kitti.cc)
  5. add_executable(stereo_euroc
  6. Examples/Stereo/stereo_euroc.cc)
  7. add_executable(mono_tum
  8. Examples/Monocular/mono_tum.cc)
  9. add_executable(mono_kitti
  10. Examples/Monocular/mono_kitti.cc)
  11. add_executable(mono_euroc
  12. Examples/Monocular/mono_euroc.cc)

下面我们重点看一下单目的例子,即mono_tum.cc程序。

首先判断输入是否是4个参数,按照正确的输入应该是:./mono_tum path_to_vocabulary path_to_settings path_to_sequence。这里需要注意的是传入的参数分别对应argv[0],argv[1],argv[2],argv[3]。并且argv[0]对应的是程序本身。这里总有一些小伙伴搞不清楚。


 
 
  1. if(argc !=  4)
  2.     {
  3.          cerr <<  endl <<  "Usage: ./mono_tum path_to_vocabulary path_to_settings path_to_sequence" <<  endl;
  4.          return  1;
  5.     }

举个例子:

我调用程序的输入为:

./Examples/Monocular/mono_tum  Vocabulary/ORBvoc.txt /home/w/Documents/shLeft/shleft.yaml  /home/w/Documents/shLeft

 
 

cd ~/ORB_SLAM2/

./Examples/Monocular/mono_tum Vocabulary/ORBvoc.txt Examples/Monocular/TUM1.yaml Examples/rgbd_dataset_freiburg1_xyz

mono_tum 程序

ORBvoc.txt     词典

TUM1.yaml 内参

rgbd_dataset_freiburg1_xyz 图片序列路径与时间戳

接下来,定义一个类型为vector<string> 的变量vstrImageFilenames和类型为vector<double>的变量vTimestamps,一个字符串类型变量strFile,用来存储/home/w/Documents/shLeft/rgb.txt这个文件。


 
 
  1.      vector< string> vstrImageFilenames;
  2.      vector< double> vTimestamps;
  3.      string strFile =  string(argv[ 3])+ "/rgb.txt";

接下来进入一个子函数:

LoadImages(strFile, vstrImageFilenames, vTimestamps)

 
 

进入这个子函数,我们发现这个函数的功能就是将strFile文件中的时间戳信息存到vTimestamps中去,将png图片路径存在vstrImageFilenames中去。


 
 
  1. void LoadImages(const string &strFile, vector<string> &vstrImageFilenames, vector<double> &vTimestamps)
  2. {
  3.     ifstream f;
  4.     f.open(strFile.c_str());
  5.      cout <<  "already open the file:" << strFile <<  endl;
  6.      // skip first three lines
  7.      string s0;
  8.     getline(f,s0);
  9.     getline(f,s0);
  10.     getline(f,s0);
  11.      while(!f.eof())
  12.     {
  13.          string s;
  14.         getline(f,s);  //读取每一行的内容到s里面去
  15.          if(!s.empty())
  16.         {
  17.              stringstream ss; double t; string sRGB;
  18.             ss << s;
  19.             ss >> t;
  20.              //将strFile文件中的时间戳信息存到vTimestamps中去
  21.             vTimestamps.push_back(t);
  22.             ss >> sRGB;
  23.              //将png图片路径存在vstrImageFilenames中去。
  24.             vstrImageFilenames.push_back(sRGB);
  25.         }
  26.     }
  27. }

然后把控制权返回到主函数,继续往下执行。获取图片的个数,这里是在对应的文件中得到的。如果我们需要跑自己采集的数据集,那么就需要在数据集文件中预处理对应的信息。

int nImages = vstrImageFilenames.size();//图片的个数

 
 

下面进入函数的重要部分,初始化SLAM系统,(这里是进入了system.cc文件中)这里会初始化三个线程:Tracking,local Mapping和Loop Closing。


 
 
  1. //  Create SLAM system. It initializes all  system threads  and gets ready  to process frames.
  2.     ORB_SLAM2:: System SLAM(argv[ 1],argv[ 2],ORB_SLAM2:: System::MONOCULAR, true);

以我的输入为例子,这里的参数是:词典;相机参数文件;单目相机;true;

Vocabulary/ORBvoc.txt /home/w/Documents/shLeft/shleft.yaml

 
 

我们先暂停进去system.cc里面看看程序是如何进行初始化这三个线程的吧。先看一下传入的参数:


 
 
  1. System::System( const  string &strVocFile,  const  string &strSettingsFile,  const eSensor sensor, const  bool bUseViewer):
  2.                         mSensor(sensor), mpViewer( static_cast<Viewer*>( NULL)), mbReset( false),mbActivateLocalizationMode( false),mbDeactivateLocalizationMode( false)

首先是词典文件,以我的例子这里是Vocabulary/ORBvoc.txt 第二个参数是相机参数文件,以我的例子是/home/w/Documents/shLeft/shleft.yaml。第三个参数是用ORB_SLAM2::System::MONOCULAR初始化mSensor。其余的参数不用太在意,已经用默认的参数进行初始化了。

输出ORB SLAM2的欢迎信息,然后判断传感器的类型是单目、立体相机或者RGB-D相机。


 
 
  1.      cout <<  "Input sensor was set to: ";
  2.      if(mSensor==MONOCULAR)
  3.          cout <<  "Monocular" <<  endl;
  4.      else  if(mSensor==STEREO)
  5.          cout <<  "Stereo" <<  endl;
  6.      else  if(mSensor==RGBD)
  7.          cout <<  "RGB-D" <<  endl;

然后检查一下相机参数文件。

//Check settings file

cv::FileStorage fsSettings(strSettingsFile.c_str(), cv::FileStorage::READ);//strSettingsFile:相机参数文件

if(!fsSettings.isOpened())

{


cerr << “Failed to open settings file at: ” << strSettingsFile << endl;

exit(-1);

}

然后加载ORB词典文件。


 
 
  1.      //加载 ORB Vocabulary
  2.      cout <<  endl <<  "Loading ORB Vocabulary. This could take a while..." <<  endl;
  3.     mpVocabulary =  new ORBVocabulary();
  4.      bool bVocLoad = mpVocabulary->loadFromTextFile(strVocFile);
  5.      if(!bVocLoad)
  6.     {
  7.          cerr <<  "Wrong path to vocabulary. " <<  endl;
  8.          cerr <<  "Falied to open at: " << strVocFile <<  endl;
  9.          exit( -1);
  10.     }
  11.      cout <<  "Vocabulary loaded!" <<  endl <<  endl;

创建关键帧数据库


 
 
  1.     // Create KeyFrame  Database 创建关键帧数据库
  2.     mpKeyFrameDatabase =  new KeyFrameDatabase(*mpVocabulary);

创建地图

     mpMap = new Map();

 
 

创建两个显示窗口FrameDrawer,MapDrawer


 
 
  1.      //Create Drawers. These are used by the Viewer 创建两个显示窗口FrameDrawer,MapDrawer
  2.     mpFrameDrawer =  new FrameDrawer(mpMap);
  3.     mpMapDrawer =  new MapDrawer(mpMap, strSettingsFile);

初始化Tracking线程。


 
 
  1.     //Initialize the Tracking thread 初始化Tracking线程。
  2.      //(it will live in the main thread of execution, the one that called this constructor)
  3.     mpTracker =  new Tracking( this, mpVocabulary, mpFrameDrawer, mpMapDrawer,
  4.                              mpMap, mpKeyFrameDatabase, strSettingsFile, mSensor);

初始化Local Mapping线程并开启线程运行。


 
 
  1.     //Initialize the Local Mapping thread and launch. 初始化Local Mapping线程并开启线程运行。
  2.     mpLocalMapper =  new LocalMapping(mpMap, mSensor==MONOCULAR);
  3.     mptLocalMapping =  new thread(&ORB_SLAM2::LocalMapping::Run,mpLocalMapper);

初始化loop closing对象,并开启线程运行


 
 
  1.      //Initialize the Loop Closing thread and launch 初始化loop closing对象,并开启线程运行
  2.     mpLoopCloser =  new LoopClosing(mpMap, mpKeyFrameDatabase, mpVocabulary, mSensor!=MONOCULAR);
  3.     mptLoopClosing =  new thread(&ORB_SLAM2::LoopClosing::Run, mpLoopCloser);

初始化显示线程Viewer(),开启线程显示图像和地图点


 
 
  1.      //Initialize the Viewer thread and launch. 初始化显示线程Viewer(),开启线程显示图像和地图点
  2.      if(bUseViewer)
  3.     {
  4.         mpViewer =  new Viewer(this, mpFrameDrawer,mpMapDrawer,mpTracker,strSettingsFile);
  5.         mptViewer =  new thread(&Viewer::Run, mpViewer);
  6.         mpTracker->SetViewer(mpViewer);
  7.     }

设置线程之间的指针。


 
 
  1.      //Set pointers between threads 设置线程之间的指针
  2.     mpTracker->SetLocalMapper(mpLocalMapper);
  3.     mpTracker->SetLoopClosing(mpLoopCloser);
  4.     mpLocalMapper->SetTracker(mpTracker);
  5.     mpLocalMapper->SetLoopCloser(mpLoopCloser);
  6.     mpLoopCloser->SetTracker(mpTracker);
  7.     mpLoopCloser->SetLocalMapper(mpLocalMapper);

下面我们把注意力收回来主函数,接着下面定义一个vector<float>类型的容器vTimesTrack来存放追踪的时间,并把容器的大小设置为图片数量的大小。


 
 
  1.   // Vector for tracking time statistics
  2.      vector< float> vTimesTrack; //记录每张图片的跟踪时间的容器,下面循环中使用。
  3.     vTimesTrack.resize(nImages);

下面进入主循环。主循环主要完成下面几件事情,循环读取每一张图片,对图片中的特征点进行跟踪,记录跟踪的时间。代码的注释已经写在对应的代码附近啦。


 
 
  1. // Main loop
  2.     cv::Mat im;
  3.      for( int ni= 0; ni<nImages; ni++)
  4.     {
  5.          // Read image from file
  6.         im = cv::imread( string(argv[ 3])+ "/"+vstrImageFilenames[ni],CV_LOAD_IMAGE_UNCHANGED); // 路径:/home/w/Documents/shLeft/rgb/1305031102.175304.png
  7.          double tframe = vTimestamps[ni]; //时间戳信息---->tframe变量中。
  8.          if(im.empty())
  9.         {
  10.              cerr <<  endl <<  "Failed to load image at: "
  11.                  <<  string(argv[ 3]) <<  "/" << vstrImageFilenames[ni] <<  endl;
  12.              return  1;
  13.         }
  14.          cout<< " Pass the image to the SLAM system-----ni = "<<ni<< endl;
  15. #ifdef COMPILEDWITHC11
  16.          std::chrono::steady_clock::time_point t1 =  std::chrono::steady_clock::now();
  17. #else
  18.          std::chrono::monotonic_clock::time_point t1 =  std::chrono::monotonic_clock::now();
  19. #endif
  20.          // Pass the image to the SLAM system
  21.         SLAM.TrackMonocular(im,tframe); //图片和时间戳
  22. #ifdef COMPILEDWITHC11
  23.          std::chrono::steady_clock::time_point t2 =  std::chrono::steady_clock::now();
  24. #else
  25.          std::chrono::monotonic_clock::time_point t2 =  std::chrono::monotonic_clock::now();
  26. #endif
  27.          double ttrack=  std::chrono::duration_cast< std::chrono::duration< double> >(t2 - t1).count();
  28.         vTimesTrack[ni]=ttrack;
  29.          // Wait to load the next frame
  30.          double T= 0;                       //上一帧、当前帧、下一帧
  31.          if(ni<nImages -1)
  32.             T = vTimestamps[ni+ 1]-tframe; //(下一帧-当前帧)的时间戳的差
  33.          else  if(ni> 0)
  34.             T = tframe-vTimestamps[ni -1]; //(当前帧-上一帧)的时间戳的差
  35.          if(ttrack<T) // 跟踪的时间 < 帧之间的时间差,则等待。
  36.             usleep((T-ttrack)* 1e4);
  37.     }

循环处理完每一张图片后,就需要关闭这个SLAM系统了。关闭的时候会判断每个线程都结束了才会关闭,不然就处于等待状态。


 
 
  1.     //  Stop all threads
  2.     SLAM.Shutdown();

然后程序在结束后会对跟踪的时间进行排序,然后计算跟踪时间的中位数和跟踪时间的平均值。之后就把程序中的关键帧的信息保存在文件

KeyFrameTrajectory.txt中。保存的格式是:timestamp,tx,ty,tz,qx,q,qz,qw。


 
 
  1.      // Tracking time statistics
  2.     sort(vTimesTrack.begin(),vTimesTrack.end());
  3.      float totaltime =  0;
  4.      for( int ni= 0; ni<nImages; ni++)
  5.     {
  6.         totaltime+=vTimesTrack[ni];
  7.     }
  8.      cout <<  "-------" <<  endl <<  endl;
  9.      cout <<  "median tracking time: " << vTimesTrack[nImages/ 2] <<  endl;
  10.      cout <<  "mean tracking time: " << totaltime/nImages <<  endl;
  11.      // Save camera trajectory
  12.     SLAM.SaveKeyFrameTrajectoryTUM( "KeyFrameTrajectory.txt");