OKVIS代碼結構:
VIO的初始化是一個比較重要的問題,和純視覺SLAM初始化只需要三角化出3D地圖點的深度不同,還需要完成相機IMU外參、陀螺儀零偏、尺度以及重力的估計。但是,OKVIS的初始化流程似乎非常簡單,但是需要對傳感器各項參數有較好的先驗值,例如需要在配置文件中給出一個比較靠譜的IMU零偏prior:
1 sigma_bg: 0.01 # gyro bias prior [rad/s] 2 sigma_ba: 0.1 # accelerometer bias prior [m/s^2]
根據提供的okvis_app_synchronous.cpp,系統入口在類ThreadedKFVio(該類繼承自VioInterface接口)的構造函數中,在okvis_multisensor_processing目錄下找到該類對應的文件,構造函數中調用init(),接着調用startThreads(),開啟各線程:
1 void ThreadedKFVio::startThreads() { 2 3 // consumer threads 4 for (size_t i = 0; i < numCameras_; ++i) { 5 frameConsumerThreads_.emplace_back(&ThreadedKFVio::frameConsumerLoop, this, i); 6 } 7 for (size_t i = 0; i < numCameraPairs_; ++i) { 8 keypointConsumerThreads_.emplace_back(&ThreadedKFVio::matchingLoop, this); 9 } 10 imuConsumerThread_ = std::thread(&ThreadedKFVio::imuConsumerLoop, this); 11 positionConsumerThread_ = std::thread(&ThreadedKFVio::positionConsumerLoop, 12 this); 13 gpsConsumerThread_ = std::thread(&ThreadedKFVio::gpsConsumerLoop, this); 14 magnetometerConsumerThread_ = std::thread( 15 &ThreadedKFVio::magnetometerConsumerLoop, this); 16 differentialConsumerThread_ = std::thread( 17 &ThreadedKFVio::differentialConsumerLoop, this); 18 19 // algorithm threads 20 visualizationThread_ = std::thread(&ThreadedKFVio::visualizationLoop, this); 21 optimizationThread_ = std::thread(&ThreadedKFVio::optimizationLoop, this); 22 publisherThread_ = std::thread(&ThreadedKFVio::publisherLoop, this); 23 }
其中,positionConsumerLoop,gpsConsumerLoop,magnetmeterConsumerLoop,differentialConsumerLoop均未實現(暫不提供GPS,磁力計以及差分氣壓計支持),也就是開了6個線程,分別執行6個函數:
1 void ThreadedKFVio::frameConsumerLoop(size_t cameraIndex) 2 void ThreadedKFVio::matchingLoop() 3 void ThreadedKFVio::imuConsumerLoop() 4 // backend algorithms 5 void ThreadedKFVio::visualizationLoop() 6 void ThreadedKFVio::optimizationLoop() 7 void ThreadedKFVio::publisherLoop()
然后,在okvis_app_synchronous.cpp中,將IMU和camera的數據使用addImage()和addImuMeasurement()傳入,注意OKVIS中數據流采用了阻塞式(可以通過ThreadKFVio.setBlocking()設定)的線程安全隊列。
1. IMU消費者線程:
在imuConsumerLoop()中主要處理imu的propagation
每次imuMeasurementsReceived_隊列中出現IMU數據,就會propagate一次,如果剛完成BA優化(需要repropagationNeeded_),則將優化后的狀態值作為propagation的初值,否則在上一狀態基礎上完成狀態propagation。
主要對應ImuError::propagation()函數,該函數大概兩百行,主要實現OKVIS論文中的 4.2 IMU Kinematics and bias model。
2. Frame消費者線程
2.1 判斷該幀是否關鍵幀(第一幀是關鍵幀)
2.2 利用IMU預測pose,為特征點匹配提供方向參考
在frameConsumerLoop()中Image和IMU的同步策略是這樣的:
若沒有IMU數據,則不處理;IMU第一幀數據之前的那一幀Image也拋棄,下一幀Image(第一幀Frame)才進行特征檢測處理。同時第一幀之前的IMU數據會用來計算pose(該函數返回值永遠是true,因此initPose是否准確完全依賴IMU給出的讀數):
bool success = okvis::Estimator::initPoseFromImu(imuData, T_WS);
第一幀之后的IMU數據進行propagation(注意multiframe在單目情形下就是frame),注意到這里propagation的covariance和jacobian均為0,僅僅用於預測,對特征點檢測提供先驗的T_WC:
okvis::ceres::ImuError::propagation(imuData, parameters_.imu, T_WS, speedAndBiases, lastTimestamp, multiFrame->timestamp());
2.3 Harris角點檢測+BRISK描述子計算
接下來對frame特征檢測(Harris)和描述子(BRISK)計算(這里的T_WC由前一步的propagation提供,主要為了獲取重力方向,提高描述子匹配魯棒性):
frontend_.detectAndDescribe(frame->sensorId, multiFrame, T_WC, nullptr);
將檢測到的keyPoint都push到隊列中,提供給matchingLoop()線程使用:
keypointMeasurements_.PushBlockingIfFull(multiFrame, 1)
3. Matching線程
該線程需要Frame線程提供的keyPointMeasrements_(阻塞隊列)。
在matching之前,通過frame和imuData的信息,將當前狀態添加到后端估計中去;這里的imuData包含上一幀和當前幀時間戳±20ms范圍內的IMU,因此,在frame附近的IMU數據,是會重復使用一次的。OKVIS的算法可以解決該問題(TODO)。
estimator_.addStates(frame, imuData, asKeyframe)
至此,可以獲取通過上一幀和IMU數據計算出的系統狀態(T_WS和speedAndBias)。
該線程最主要的函數是:
frontend_.dataAssociationAndInitialization(estimator_, T_WS, parameters_, map_, frame, &asKeyframe);
完成
- 特征點匹配;
- 3D點初始化;
- 外點剔除
- RANSAC
- 關鍵幀選擇
在rotationOnly的運動時,使用2D-2D跟蹤,使用IMU給出軌跡;有平移運動可以三角化出3D點時,通過3D-2D匹配計算出pose;這里均使用了Opengv中算法
參考:
1. OKVIS代碼框架
