---
# System prepended metadata

title: Optimizer.cpp

---

# 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;
        
```