# Optimizer.cpp ### Optimizer::LocalBundleAdjustment() 用於LocalMapping線程的局部BA優化 在[LocalMapping::Run()](LocalMapping::Run()#)中引用 #### 定義 ``` void Optimizer::LocalBundleAdjustment(KeyFrame *pKF, // 關鍵幀 bool* pbStopFlag, // 是否停止優化 Map* pMap) // 優化後,更新狀態時需要用到Map的互斥量mMutexMapUpdate ``` #### 函式流程 1. 將當前關鍵幀及其共視關鍵幀加入局部關鍵幀 2. 遍歷局部關鍵幀中(一級相連)關鍵幀,將它們觀測的地圖點加入到局部地圖點 3. 得到能被局部地圖點觀測到,但不屬於局部關鍵幀的關鍵幀(二級相連),這些二級相連關鍵幀在局部BA優化時不優化 4. 構造g2o優化器 5. 添加待優化的姿態頂點:局部關鍵幀的姿態 6. 添加不優化的位姿頂點:固定關鍵幀的位姿 7. 添加待優化的局部地圖點頂點 8. 在添加完了一個地圖點之後, 對每一對關聯的地圖點和關鍵幀構建邊 9. 第一階段優化 10. 檢測outlier,並設置下次不優化 11. 排除誤差較大的outlier後再次優化 -- 第二階段優化 12. 在優化後重新計算誤差,剔除連接誤差比較大的關鍵幀和地圖點 13. 優化後更新關鍵幀位姿以及地圖點的位置、平均觀測方向等屬性 #### 函式詳解 1. 將當前關鍵幀及其共視關鍵幀加入局部關鍵幀 ``` // 局部關鍵幀 list<KeyFrame*> lLocalKeyFrames; lLocalKeyFrames.push_back(pKF); pKF->mnBALocalForKF = pKF->mnId; // 找到關鍵幀連接的共視關鍵幀(一級相連),加入局部關鍵幀中 const vector<KeyFrame*> vNeighKFs = pKF->GetVectorCovisibleKeyFrames(); for(int i=0, iend=vNeighKFs.size(); i<iend; i++) { KeyFrame* pKFi = vNeighKFs[i]; // 把參與局部BA的每一個關鍵幀的 mnBALocalForKF設置為當前關鍵幀的mnId,防止重複添加 pKFi->mnBALocalForKF = pKF->mnId; // 保證該關鍵幀有效才能加入 if(!pKFi->isBad()) lLocalKeyFrames.push_back(pKFi); } ``` 2. 遍歷局部關鍵幀中(一級相連)關鍵幀,將它們觀測的地圖點加入到局部地圖點 ``` list<MapPoint*> lLocalMapPoints; // 遍歷局部關鍵幀中的每一個關鍵幀 for(list<KeyFrame*>::iterator lit=lLocalKeyFrames.begin() , lend=lLocalKeyFrames.end(); lit!=lend; lit++) { // 取出該關鍵幀對應的地圖點 vector<MapPoint*> vpMPs = (*lit)->GetMapPointMatches(); // 遍歷這個關鍵幀觀測到的每一個地圖點,加入到局部地圖點 for(vector<MapPoint*>::iterator vit=vpMPs.begin(), vend=vpMPs.end(); vit!=vend; vit++) { MapPoint* pMP = *vit; if(pMP) { if(!pMP->isBad()) //保證地圖點有效 // 把參與局部BA的每一個地圖點的 mnBALocalForKF設置為當前關鍵幀的mnId // mnBALocalForKF 是為了防止重複添加 if(pMP->mnBALocalForKF!=pKF->mnId) { lLocalMapPoints.push_back(pMP); pMP->mnBALocalForKF=pKF->mnId; } } // 判斷這個地圖點是否靠譜 } // 遍歷這個關鍵幀觀測到的每一個地圖點 } // 遍歷 lLocalKeyFrames 中的每一個關鍵幀 ``` 3. 得到能被局部地圖點觀測到,但不屬於局部關鍵幀的關鍵幀(二級相連),這些二級相連關鍵幀在局部BA優化時不優化 ``` list<KeyFrame*> lFixedCameras; // 遍歷局部地圖中的每個地圖點 for(list<MapPoint*>::iterator lit=lLocalMapPoints.begin(), lend=lLocalMapPoints.end(); lit!=lend; lit++) { // 觀測到該地圖點的KF和該地圖點在KF中的索引 map<KeyFrame*,size_t> observations = (*lit)->GetObservations(); // 遍歷所有觀測到該地圖點的關鍵幀 for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++) { KeyFrame* pKFi = mit->first; // pKFi->mnBALocalForKF!=pKF->mnId 表示不屬於局部關鍵幀, // pKFi->mnBAFixedForKF!=pKF->mnId 表示還未標記為fixed(固定的)關鍵幀 if(pKFi->mnBALocalForKF!=pKF->mnId && pKFi->mnBAFixedForKF!=pKF->mnId) { // 將局部地圖點能觀測到的、但是不屬於局部BA範圍的關鍵幀的mnBAFixedForKF標記為pKF(觸發局部BA的當前關鍵幀)的mnId pKFi->mnBAFixedForKF=pKF->mnId; if(!pKFi->isBad()) lFixedCameras.push_back(pKFi); } } } ``` 4. 構造g2o優化器 ``` // 圖優化模型 g2o::SparseOptimizer optimizer; // 求H*Δx=−b的線性求解器 g2o::BlockSolver_6_3::LinearSolverType * linearSolver; linearSolver = new g2o::LinearSolverEigen<g2o::BlockSolver_6_3::PoseMatrixType>(); // 矩陣塊求解器 求解雅克比矩陣和海塞矩陣 g2o::BlockSolver_6_3 * solver_ptr = new g2o::BlockSolver_6_3(linearSolver); // LM優化器 g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(solver_ptr); optimizer.setAlgorithm(solver); // 外界設置的停止優化標誌 // 可能在 Tracking::NeedNewKeyFrame() 裡置位 if(pbStopFlag) optimizer.setForceStopFlag(pbStopFlag); // 記錄參與局部BA的最大關鍵幀mnId unsigned long maxKFid = 0; ``` 5. 添加待優化的姿態頂點:局部關鍵幀的姿態 ``` for(list<KeyFrame*>::iterator lit=lLocalKeyFrames.begin(), lend=lLocalKeyFrames.end(); lit!=lend; lit++) { KeyFrame* pKFi = *lit; //6自由度的頂點,可以用來當作姿態 g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap(); // 設置初始優化位姿 vSE3->setEstimate(Converter::toSE3Quat(pKFi->GetPose())); vSE3->setId(pKFi->mnId); // 如果是初始關鍵幀,要鎖住位姿不優化 vSE3->setFixed(pKFi->mnId==0); optimizer.addVertex(vSE3); if(pKFi->mnId>maxKFid) maxKFid=pKFi->mnId; } ``` 6. 添加不優化的位姿頂點:固定關鍵幀的位姿 ``` // 注意這裡調用了vSE3->setFixed(true) // 不優化為啥也要添加?回答:為了增加約束信息 for(list<KeyFrame*>::iterator lit=lFixedCameras.begin(), lend=lFixedCameras.end(); lit!=lend; lit++) { KeyFrame* pKFi = *lit; g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap(); vSE3->setEstimate(Converter::toSE3Quat(pKFi->GetPose())); vSE3->setId(pKFi->mnId); // 所有的這些頂點的位姿都不優化,只是為了增加約束項 vSE3->setFixed(true); optimizer.addVertex(vSE3); if(pKFi->mnId>maxKFid) maxKFid=pKFi->mnId; } ``` 7. 添加待優化的局部地圖點頂點 ``` // 邊的最大數目 = 位姿數目 * 地圖點數目 const int nExpectedSize = (lLocalKeyFrames.size()+lFixedCameras.size())*lLocalMapPoints.size(); //3維點 vector<g2o::EdgeSE3ProjectXYZ*> vpEdgesMono; vpEdgesMono.reserve(nExpectedSize); vector<KeyFrame*> vpEdgeKFMono; vpEdgeKFMono.reserve(nExpectedSize); vector<MapPoint*> vpMapPointEdgeMono; vpMapPointEdgeMono.reserve(nExpectedSize); vector<g2o::EdgeStereoSE3ProjectXYZ*> vpEdgesStereo; vpEdgesStereo.reserve(nExpectedSize); vector<KeyFrame*> vpEdgeKFStereo; vpEdgeKFStereo.reserve(nExpectedSize); vector<MapPoint*> vpMapPointEdgeStereo; vpMapPointEdgeStereo.reserve(nExpectedSize); // 自由度為2的卡方分佈,顯著性水平為0.05,對應的臨界閾值5.991 const float thHuberMono = sqrt(5.991); // 自由度為3的卡方分佈,顯著性水平為0.05,對應的臨界閾值7.815 const float thHuberStereo = sqrt(7.815); // 遍歷所有的局部地圖點 for(list<MapPoint*>::iterator lit=lLocalMapPoints.begin(), lend=lLocalMapPoints.end(); lit!=lend; lit++) { // 添加頂點:MapPoint MapPoint* pMP = *lit; g2o::VertexSBAPointXYZ* vPoint = new g2o::VertexSBAPointXYZ(); vPoint->setEstimate(Converter::toVector3d(pMP->GetWorldPos())); // 前面記錄maxKFid的作用在這裡體現 int id = pMP->mnId+maxKFid+1; vPoint->setId(id); // 因為使用了LinearSolverType,所以需要將所有的三維點邊緣化掉 vPoint->setMarginalized(true); optimizer.addVertex(vPoint); // 觀測到該地圖點的KF和該地圖點在KF中的索引 const map<KeyFrame*,size_t> observations = pMP->GetObservations(); ``` 8. 在添加完了一個地圖點之後, 對每一對關聯的地圖點和關鍵幀構建邊 ``` // 遍歷所有觀測到當前地圖點的關鍵幀 for(map<KeyFrame*,size_t>::const_iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++) { KeyFrame* pKFi = mit->first; if(!pKFi->isBad()) { //當前三維點對應的特徵點 const cv::KeyPoint &kpUn = pKFi->mvKeysUn[mit->second]; // 根據單目/雙目兩種不同的輸入構造不同的誤差邊 // 單目模式下 if(pKFi->mvuRight[mit->second]<0) { Eigen::Matrix<double,2,1> obs; //三圍點的觀測,也就是二維特徵點 obs << kpUn.pt.x, kpUn.pt.y; //定義邊 g2o::EdgeSE3ProjectXYZ* e = new g2o::EdgeSE3ProjectXYZ(); // 為邊添加關聯的頂點 // 邊的第一個頂點是地圖點 e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id))); // 邊的第一個頂點是觀測到該地圖點的關鍵幀 e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKFi->mnId))); //添加測量值 e->setMeasurement(obs); // 權重為特徵點所在圖像金字塔的層數的倒數 const float &invSigma2 = pKFi->mvInvLevelSigma2[kpUn.octave]; //信息矩陣 e->setInformation(Eigen::Matrix2d::Identity()*invSigma2); // 使用魯棒核函數抑制外點 g2o::RobustKernelHuber* rk = new g2o::RobustKernelHuber; e->setRobustKernel(rk); rk->setDelta(thHuberMono); e->fx = pKFi->fx; e->fy = pKFi->fy; e->cx = pKFi->cx; e->cy = pKFi->cy; // 將邊添加到優化器,記錄邊、邊連接的關鍵幀、邊連接的地圖點信息 optimizer.addEdge(e); vpEdgesMono.push_back(e); vpEdgeKFMono.push_back(pKFi); vpMapPointEdgeMono.push_back(pMP); } else // Stereo observation { // 雙目或RGB-D模式和單目模式類似 Eigen::Matrix<double,3,1> obs; const float kp_ur = pKFi->mvuRight[mit->second]; obs << kpUn.pt.x, kpUn.pt.y, kp_ur; g2o::EdgeStereoSE3ProjectXYZ* e = new g2o::EdgeStereoSE3ProjectXYZ(); e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id))); e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKFi->mnId))); e->setMeasurement(obs); const float &invSigma2 = pKFi->mvInvLevelSigma2[kpUn.octave]; Eigen::Matrix3d Info = Eigen::Matrix3d::Identity()*invSigma2; e->setInformation(Info); g2o::RobustKernelHuber* rk = new g2o::RobustKernelHuber; e->setRobustKernel(rk); rk->setDelta(thHuberStereo); e->fx = pKFi->fx; e->fy = pKFi->fy; e->cx = pKFi->cx; e->cy = pKFi->cy; e->bf = pKFi->mbf; optimizer.addEdge(e); vpEdgesStereo.push_back(e); vpEdgeKFStereo.push_back(pKFi); vpMapPointEdgeStereo.push_back(pMP); } } } // 遍歷所有觀測到當前地圖點的關鍵幀 } // 遍歷所有的局部地圖中的地圖點 // 開始BA前再次確認是否有外部請求停止優化,因為這個變量是引用傳遞,會隨外部變化 // 可能在 Tracking::NeedNewKeyFrame(), mpLocalMapper->InsertKeyFrame 裡置位 if(pbStopFlag) if(*pbStopFlag) return; ``` 9. 第一階段優化 ``` optimizer.initializeOptimization(); // 迭代5次 optimizer.optimize(5); bool bDoMore= true; // 檢查是否外部請求停止 if(pbStopFlag) if(*pbStopFlag) bDoMore = false; // 如果有外部請求停止,那麼就不在進行第二階段的優化 if(bDoMore) { ``` 10. 檢測outlier,並設置下次不優化 ``` // 遍歷所有的單目誤差邊 for(size_t i=0, iend=vpEdgesMono.size(); i<iend;i++) { g2o::EdgeSE3ProjectXYZ* e = vpEdgesMono[i]; MapPoint* pMP = vpMapPointEdgeMono[i]; if(pMP->isBad()) continue; // 基於卡方檢驗計算出的閾值(假設測量有一個像素的偏差) // 自由度為2的卡方分佈,顯著性水平為0.05,對應的臨界閾值5.991 // 如果 當前邊誤差超出閾值,或者邊鏈接的地圖點深度值為負,說明這個邊有問題,不優化了。 if(e->chi2()>5.991 || !e->isDepthPositive()) { // 不優化 e->setLevel(1); } // 第二階段優化的時候就屬於精求解了,所以就不使用核函數 e->setRobustKernel(0); } // 對於所有的雙目的誤差邊也都進行類似的操作 for(size_t i=0, iend=vpEdgesStereo.size(); i<iend;i++) { g2o::EdgeStereoSE3ProjectXYZ* e = vpEdgesStereo[i]; MapPoint* pMP = vpMapPointEdgeStereo[i]; if(pMP->isBad()) continue; // 自由度為3的卡方分佈,顯著性水平為0.05,對應的臨界閾值7.815 if(e->chi2()>7.815 || !e->isDepthPositive()) { e->setLevel(1); } e->setRobustKernel(0); } ``` 11. 排除誤差較大的outlier後再次優化 -- 第二階段優化 ``` optimizer.initializeOptimization(0); optimizer.optimize(10); } vector<pair<KeyFrame*,MapPoint*> > vToErase; vToErase.reserve(vpEdgesMono.size()+vpEdgesStereo.size()); ``` 12. 在優化後重新計算誤差,剔除連接誤差比較大的關鍵幀和地圖點 ``` // 對於單目誤差邊 for(size_t i=0, iend=vpEdgesMono.size(); i<iend;i++) { g2o::EdgeSE3ProjectXYZ* e = vpEdgesMono[i]; MapPoint* pMP = vpMapPointEdgeMono[i]; if(pMP->isBad()) continue; // 基於卡方檢驗計算出的閾值(假設測量有一個像素的偏差) // 自由度為2的卡方分佈,顯著性水平為0.05,對應的臨界閾值5.991 // 如果 當前邊誤差超出閾值,或者邊鏈接的地圖點深度值為負,說明這個邊有問題,要刪掉了 if(e->chi2()>5.991 || !e->isDepthPositive()) { // outlier KeyFrame* pKFi = vpEdgeKFMono[i]; vToErase.push_back(make_pair(pKFi,pMP)); } } // 雙目誤差邊 for(size_t i=0, iend=vpEdgesStereo.size(); i<iend;i++) { g2o::EdgeStereoSE3ProjectXYZ* e = vpEdgesStereo[i]; MapPoint* pMP = vpMapPointEdgeStereo[i]; if(pMP->isBad()) continue; if(e->chi2()>7.815 || !e->isDepthPositive()) { KeyFrame* pKFi = vpEdgeKFStereo[i]; vToErase.push_back(make_pair(pKFi,pMP)); } } // Get Map Mutex unique_lock<mutex> lock(pMap->mMutexMapUpdate); // 刪除點 // 連接偏差比較大,在關鍵幀中剔除對該地圖點的觀測 // 連接偏差比較大,在地圖點中剔除對該關鍵幀的觀測 if(!vToErase.empty()) { for(size_t i=0;i<vToErase.size();i++) { KeyFrame* pKFi = vToErase[i].first; MapPoint* pMPi = vToErase[i].second; pKFi->EraseMapPointMatch(pMPi); pMPi->EraseObservation(pKFi); } } ``` 13. 優化後更新關鍵幀位姿以及地圖點的位置、平均觀測方向等屬性 ``` //Keyframes for(list<KeyFrame*>::iterator lit=lLocalKeyFrames.begin(), lend=lLocalKeyFrames.end(); lit!=lend; lit++) { KeyFrame* pKF = *lit; g2o::VertexSE3Expmap* vSE3 = static_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(pKF->mnId)); g2o::SE3Quat SE3quat = vSE3->estimate(); pKF->SetPose(Converter::toCvMat(SE3quat)); } //Points for(list<MapPoint*>::iterator lit=lLocalMapPoints.begin(), lend=lLocalMapPoints.end(); lit!=lend; lit++) { MapPoint* pMP = *lit; g2o::VertexSBAPointXYZ* vPoint = static_cast<g2o::VertexSBAPointXYZ*>(optimizer.vertex(pMP->mnId+maxKFid+1)); pMP->SetWorldPos(Converter::toCvMat(vPoint->estimate())); pMP->UpdateNormalAndDepth(); } ``` ### Optimizer::OptimizeSim3() 形成閉環時固定(不優化) 地圖點進行Sim3位姿優化 在[LoopClosing::ComputeSim3()](#LoopClosing::ComputeSim3())中引用 #### 定義 ``` int Optimizer::OptimizeSim3(KeyFrame *pKF1, // 當前幀 KeyFrame *pKF2, // 閉環候選幀 vector<MapPoint *> &vpMatches1, // 兩個關鍵幀之間的匹配關係 g2o::Sim3 &g2oS12, // 兩個關鍵幀間的Sim3變換,方向是從2到1 const float th2, // 卡方檢驗是否為誤差邊用到的閾值 const bool bFixScale) // 是否優化尺度,單目進行尺度優化,雙目/RGB-D不進行尺度優化 ``` #### 函式流程 1. 初始化g2o優化器 2. 設置待優化的Sim3位姿作為頂點(用來優化兩幀之間的變換) 3. 設置匹配的地圖點作為頂點 4. 添加邊(地圖點投影到特徵點) 5. g2o開始優化,先迭代5次 6. 用卡方檢驗剔除誤差大的邊 7. 再次g2o優化剔除後剩下的邊 8. 用優化後的結果來更新Sim3位姿 #### 函式詳解 1. 初始化g2o優化器 ``` // 先構造求解器 g2o::SparseOptimizer optimizer; // 構造線性方程求解器,Hx = -b的求解器 g2o::BlockSolverX::LinearSolverType * linearSolver; // 使用dense的求解器,(常見非dense求解器有cholmod線性求解器和shur補線性求解器) linearSolver = new g2o::LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>(); g2o::BlockSolverX * solver_ptr = new g2o::BlockSolverX(linearSolver); // 使用L-M迭代 g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(solver_ptr); optimizer.setAlgorithm(solver); // Calibration // 內參矩陣 const cv::Mat &K1 = pKF1->mK; const cv::Mat &K2 = pKF2->mK; // Camera poses const cv::Mat R1w = pKF1->GetRotation(); const cv::Mat t1w = pKF1->GetTranslation(); const cv::Mat R2w = pKF2->GetRotation(); const cv::Mat t2w = pKF2->GetTranslation(); ``` 2. 設置待優化的Sim3位姿作為頂點(用來優化兩幀之間的變換) ``` g2o::VertexSim3Expmap * vSim3 = new g2o::VertexSim3Expmap(); // 根據傳感器類型決定是否固定尺度 vSim3->_fix_scale=bFixScale; vSim3->setEstimate(g2oS12); vSim3->setId(0); // Sim3 需要優化 vSim3->setFixed(false); // 因為要優化Sim3頂點,所以設置為false vSim3->_principle_point1[0] = K1.at<float>(0,2); // 光心橫坐標cx vSim3->_principle_point1[1] = K1.at<float>(1,2); // 光心縱坐標cy vSim3->_focal_length1[0] = K1.at<float>(0,0); // 焦距 fx vSim3->_focal_length1[1] = K1.at<float>(1,1); // 焦距 fy vSim3->_principle_point2[0] = K2.at<float>(0,2); vSim3->_principle_point2[1] = K2.at<float>(1,2); vSim3->_focal_length2[0] = K2.at<float>(0,0); vSim3->_focal_length2[1] = K2.at<float>(1,1); optimizer.addVertex(vSim3); ``` 3. 設置匹配的地圖點作為頂點 ``` const int N = vpMatches1.size(); // 獲取pKF1的地圖點 const vector<MapPoint*> vpMapPoints1 = pKF1->GetMapPointMatches(); vector<g2o::EdgeSim3ProjectXYZ*> vpEdges12; //pKF2對應的地圖點到pKF1的投影邊 vector<g2o::EdgeInverseSim3ProjectXYZ*> vpEdges21; //pKF1對應的地圖點到pKF2的投影邊 vector<size_t> vnIndexEdge; //邊的索引 vnIndexEdge.reserve(2*N); vpEdges12.reserve(2*N); vpEdges21.reserve(2*N); // 核函數的閾值 const float deltaHuber = sqrt(th2); int nCorrespondences = 0; // 遍歷每對匹配點 for(int i=0; i<N; i++) { if(!vpMatches1[i]) continue; // pMP1和pMP2是匹配的地圖點 MapPoint* pMP1 = vpMapPoints1[i]; MapPoint* pMP2 = vpMatches1[i]; // 保證頂點的id能夠錯開 const int id1 = 2*i+1; const int id2 = 2*(i+1); // i2 是 pMP2 在pKF2中對應的索引 const int i2 = pMP2->GetIndexInKeyFrame(pKF2); if(pMP1 && pMP2) { if(!pMP1->isBad() && !pMP2->isBad() && i2>=0) { // 如果這對匹配點都靠譜,並且對應的2D特徵點也都存在的話,添加PointXYZ頂點 g2o::VertexSBAPointXYZ* vPoint1 = new g2o::VertexSBAPointXYZ(); // 地圖點轉換到各自相機坐標系下的三維點 cv::Mat P3D1w = pMP1->GetWorldPos(); cv::Mat P3D1c = R1w*P3D1w + t1w; vPoint1->setEstimate(Converter::toVector3d(P3D1c)); vPoint1->setId(id1); // 地圖點不優化 vPoint1->setFixed(true); optimizer.addVertex(vPoint1); g2o::VertexSBAPointXYZ* vPoint2 = new g2o::VertexSBAPointXYZ(); cv::Mat P3D2w = pMP2->GetWorldPos(); cv::Mat P3D2c = R2w*P3D2w + t2w; vPoint2->setEstimate(Converter::toVector3d(P3D2c)); vPoint2->setId(id2); vPoint2->setFixed(true); optimizer.addVertex(vPoint2); } else continue; } else continue; // 對匹配關係進行計數 nCorrespondences++; ``` 4. 添加邊(地圖點投影到特徵點) ``` // Set edge x1 = S12*X2 // 地圖點pMP1對應的觀測特徵點 Eigen::Matrix<double,2,1> obs1; const cv::KeyPoint &kpUn1 = pKF1->mvKeysUn[i]; obs1 << kpUn1.pt.x, kpUn1.pt.y; // 4.1. 閉環候選幀地圖點投影到當前關鍵幀的邊 -- 正向投影 g2o::EdgeSim3ProjectXYZ* e12 = new g2o::EdgeSim3ProjectXYZ(); // vertex(id2)對應的是pKF2 VertexSBAPointXYZ 類型的三維點 e12->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id2))); // ? 為什麼這裡添加的節點的id為0? // 回答:因為vertex(0)對應的是 VertexSim3Expmap 類型的待優化Sim3,其id 為 0 e12->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(0))); e12->setMeasurement(obs1); // 信息矩陣和這個特徵點的可靠程度(在圖像金字塔中的圖層)有關 const float &invSigmaSquare1 = pKF1->mvInvLevelSigma2[kpUn1.octave]; e12->setInformation(Eigen::Matrix2d::Identity()*invSigmaSquare1); // 使用魯棒核函數 g2o::RobustKernelHuber* rk1 = new g2o::RobustKernelHuber; e12->setRobustKernel(rk1); rk1->setDelta(deltaHuber); optimizer.addEdge(e12); // Set edge x2 = S21*X1 // 4.2. 當前關鍵幀地圖點投影到閉環候選幀的邊 -- 反向投影 // 地圖點pMP2對應的觀測特徵點 Eigen::Matrix<double,2,1> obs2; const cv::KeyPoint &kpUn2 = pKF2->mvKeysUn[i2]; obs2 << kpUn2.pt.x, kpUn2.pt.y; g2o::EdgeInverseSim3ProjectXYZ* e21 = new g2o::EdgeInverseSim3ProjectXYZ(); // vertex(id1)對應的是pKF1 VertexSBAPointXYZ 類型的三維點,內部誤差公式也不同 e21->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id1))); e21->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(0))); e21->setMeasurement(obs2); float invSigmaSquare2 = pKF2->mvInvLevelSigma2[kpUn2.octave]; e21->setInformation(Eigen::Matrix2d::Identity()*invSigmaSquare2); g2o::RobustKernelHuber* rk2 = new g2o::RobustKernelHuber; e21->setRobustKernel(rk2); rk2->setDelta(deltaHuber); optimizer.addEdge(e21); vpEdges12.push_back(e12); vpEdges21.push_back(e21); vnIndexEdge.push_back(i); } ``` 5. g2o開始優化,先迭代5次 ``` optimizer.initializeOptimization(); optimizer.optimize(5); ``` 6. 用卡方檢驗剔除誤差大的邊 ``` int nBad=0; for(size_t i=0; i<vpEdges12.size();i++) { g2o::EdgeSim3ProjectXYZ* e12 = vpEdges12[i]; g2o::EdgeInverseSim3ProjectXYZ* e21 = vpEdges21[i]; if(!e12 || !e21) continue; if(e12->chi2()>th2 || e21->chi2()>th2) { // 正向或反向投影任意一個超過誤差閾值就刪掉該邊 size_t idx = vnIndexEdge[i]; vpMatches1[idx]=static_cast<MapPoint*>(NULL); optimizer.removeEdge(e12); optimizer.removeEdge(e21); vpEdges12[i]=static_cast<g2o::EdgeSim3ProjectXYZ*>(NULL); vpEdges21[i]=static_cast<g2o::EdgeInverseSim3ProjectXYZ*>(NULL); // 累計刪掉的邊 數目 nBad++; } } // 如果有誤差較大的邊被剔除那麼說明回環質量並不是非常好,還要多迭代幾次;反之就少迭代幾次 int nMoreIterations; if(nBad>0) nMoreIterations=10; else nMoreIterations=5; // 如果經過上面的剔除後剩下的匹配關係已經非常少了,那麼就放棄優化。內點數直接設置為0 if(nCorrespondences-nBad<10) return 0; ``` 7. 再次g2o優化剔除後剩下的邊 ``` optimizer.initializeOptimization(); optimizer.optimize(nMoreIterations); // 統計第二次優化之後,這些匹配點中是內點的個數 int nIn = 0; for(size_t i=0; i<vpEdges12.size();i++) { g2o::EdgeSim3ProjectXYZ* e12 = vpEdges12[i]; g2o::EdgeInverseSim3ProjectXYZ* e21 = vpEdges21[i]; if(!e12 || !e21) continue; if(e12->chi2()>th2 || e21->chi2()>th2) { size_t idx = vnIndexEdge[i]; vpMatches1[idx]=static_cast<MapPoint*>(NULL); } else nIn++; } ``` 8. 用優化後的結果來更新Sim3位姿 ``` g2o::VertexSim3Expmap* vSim3_recov = static_cast<g2o::VertexSim3Expmap*>(optimizer.vertex(0)); g2oS12= vSim3_recov->estimate(); return nIn; ```