作者丨lovely_yoshino
0. 前言
随着路徑的不斷延伸,機器人的建圖過程會存在不斷地累計誤差。而傳統的以gmapping為代表的使用粒子濾波進行定位的slam建圖方式。以及ORB-SLAM為代表包含的局部優化和全局優化來調整外。但是這些處理方式隻能減緩誤差累計的程度,無法消除,而現在最為常用消除累計誤差的方法就是利用回環檢測來優化位姿。
當新的關鍵幀加入到優化模型時,在關鍵幀附近進行一次局部優化。
在全局優化中,所有的關鍵幀(除了第一幀)和三維點都參與優化。
1. 回環檢測
回環檢測作為近年來slam行業必備的部分,是指機器人識别曾到達某場景,使得地圖閉環的能力。
回環檢測之是以能成為一個難點,是因為:如果回環檢測成功,可以顯著地減小累積誤差,幫助機器人更精準、快速的進行避障導航工作。而錯誤的檢測結果可能使地圖變得很糟糕。是以,回環檢測在大面積、大場景地圖建構上是非常有必要的 。
詞袋模型(bag of words,BoW)早期是一種文本表征方法,後引入到計算機視覺領域,逐漸成為一種很有效的圖像特征模組化方法。它通過提取圖像特征,再将特征進行分類建構視覺字典,然後采用視覺字典中的單詞集合可以表征任一幅圖像。
換句話說,通過BOW可以把一張圖檔表示成一個向量。這對判斷圖像間的關聯很有幫助,是以目前比較流行的回環解決方案都是采用的BoW及其基礎上衍生的算法IAB-MAP、FAB-MAP是在濾波架構下計算回環機率,RTAB-MAP采用關鍵幀比較相似性,DLoopDetector(在DBoW2基礎上開發的回環檢測庫)采用連續幀的相似性檢測判斷是否存在回環。回環檢測主要由BoW子產品、算法子產品、驗證子產品三部分組成。
詞袋模型(BoW子產品)
每一幀都可以用單詞來描述,也就是這一幀中有哪些單詞,這裡隻關心了有沒有,而不必關心具體在哪裡。隻有兩幀中單詞種類相近才可能構成回環。
(a)圖像預處理,假設訓練集有M幅圖像,将圖像标準化為patch,統一格式和規格;
(b)特征提取,假設M幅圖像,對每一幅圖像提取特征,共提取出N個SIFT特征;
(c)特征聚類,采用K-means算法把N個對象分為K個簇 (視覺單詞表),使簇内具有較高的相似度,而簇間相似度較低;
(d)統計得到圖像的碼本,每幅圖像以單詞表為規範對該幅圖像的每一個SIFT特征點計算它與單詞表中每個單詞的距 離,最近的加1,便得到該幅圖像的碼本;還需要碼本矢量歸一化,因為每一幅圖像的SIFT特征個數不定,是以需要歸一化。
相似度計算(算法子產品)
現在有了字典,但還有一點需要注意。我們利用單詞表示圖像,目的是發現回環,也就是兩幀圖像具有較高的相似性。那其實不同的單詞對這一目的的貢獻性是不同的。例如,我這篇文章中有很多“我們”這個詞,但這個詞并不能告訴我們太多資訊。
而有些單詞例如“回環”、“K-means”就比較具有代表性,能大概告訴我們這句話講了什麼。是以不同的單詞應該具有不同的權重。
(a)貝葉斯估計方法。采用BoW描述機器人每一位置的場景圖像,估計已擷取圖像與對應位置的先驗機率,對目前時刻計算該新場景圖像與已通路位置比對的後驗機率,機率大于門檻值則标記為閉環。
(b)相似性方法。有了字典以後,給定任意特征點f_i,隻要在字典樹中逐層查找,最後都能找到與之對應的單詞w_i。通常字典足夠大,可以說它們來自同一類物體。但是這種方法對所有單詞都是同樣對待,正常的做法是采用TF-IDF(term frequency-inverse document frequency)
IDF(Inverse Document Frequency):描述單詞在字典中出現的頻率(建構字典時),越低越具有代表性
n為所有描述子數, n_i為該單詞出現次數。ln的作用大概是降低量級,畢竟n很大。
TF(Term Frequency):單詞在單幀圖像中出現的頻率,越高越具有代表性
n為一幀圖像中所有單詞數, n_i為一幀圖像中該單詞出現次數。
是以将一幀圖像轉化為單詞表示時,我們要計算其單詞的權重:
是以一幀圖像A由單詞 w_i,次數n_i以及權重η_i組成。
驗證子產品(回環處理)
回環的判斷也并沒有這麼簡單,含有很多的篩選環節,畢竟錯誤的回環将帶來巨大災難,甯可不要。例如某一位姿附近連續多次(ORB-SLAM中為3次)與曆史中某一位姿附近出現回環才判斷為回環;回環候選幀仍然要比對,比對點足夠才為回環。方式主要有兩種:
(a)時間一緻性。正确的回環往往存在時間上的連續性,是以如果之後一段時間内能用同樣的方法找到回環,則認為目前回環是正确的,也叫做順序一緻性限制。
(b)結構一緻性校驗。對回環檢測到的兩幀進行特征比對并估計相機運動,因為各個特征點在空間中的位置是唯一不變的,與之前的估計誤差比較大小。
然後下面主要就會涉及如何對找到的回環進行優化了。
2. SLAM優化
閉環糾正
在判斷出現回環後,兩幀計算Sim3變換(因為有尺度漂移),也就是從曆史幀直接推算目前位姿。下面我們以ORB-SLAM2為代表減少形成閉環時的Sim3優化。
單目SLAM一般都會發生尺度(scale)漂移,是以Sim3上的優化是必要的。相對于SE3,Sim3的自由度要多一個,而且優化的目标是矯正尺度因子,是以優化并沒有加入更多的變量。
//BRIEF 形成閉環時進行Sim3優化
//Sim3用于回環檢測時計算關鍵幀與回環幀之間的關系 R 、t、 s
//相似變換群Sim3
int Optimizer::OptimizeSim3(KeyFrame *pKF1, //關鍵幀1
KeyFrame *pKF2, //關鍵幀2
vector<MapPoint *> &vpMatches1,//兩幀比對關系
g2o::Sim3 &g2oS12, //兩個關鍵幀間的Sim3變換
const float th2, //核函數門檻值
const bool bFixScale//是否進行尺度優化
)
{
//建立優化器
g2o::SparseOptimizer optimizer;
g2o::BlockSolverX::LinearSolverType * linearSolver;
linearSolver = new g2o::LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>();
g2o::BlockSolverX * solver_ptr = new g2o::BlockSolverX(linearSolver);
//使用LM算法
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();
// Set Sim3 vertex
g2o::VertexSim3Expmap * vSim3 = new g2o::VertexSim3Expmap();
vSim3->_fix_scale=bFixScale;
//兩幀之間的位姿變化為待估計量
vSim3->setEstimate(g2oS12);
vSim3->setId(0);
//不進行固定參數
vSim3->setFixed(false);
//提取相機内參參數
vSim3->_principle_point1[0] = K1.at<float>(0,2);
vSim3->_principle_point1[1] = K1.at<float>(1,2);
vSim3->_focal_length1[0] = K1.at<float>(0,0);
vSim3->_focal_length1[1] = K1.at<float>(1,1);
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);
// Set MapPoint vertices
//建立地圖點 頂點
const int N = vpMatches1.size();
const vector<MapPoint*> vpMapPoints1 = pKF1->GetMapPointMatches();
//定義兩幀的公共地圖點
vector<g2o::EdgeSim3ProjectXYZ*> vpEdges12;
vector<g2o::EdgeInverseSim3ProjectXYZ*> vpEdges21;
//定義邊的尺寸
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;
MapPoint* pMP1 = vpMapPoints1[i];
MapPoint* pMP2 = vpMatches1[i];
const int id1 = 2*i+1;
const int id2 = 2*(i+1);
const int i2 = pMP2->GetIndexInKeyFrame(pKF2);
//如果比對的兩幀看到的地圖點都存在
if(pMP1 && pMP2)
{
//判斷是不是好點
if(!pMP1->isBad() && !pMP2->isBad() && i2>=0)
{
//建立優化器 優化位姿1
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);
//優化位姿2
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++;
// Set edge x1 = S12*X2
//建立邊,就是計算重投影誤差
//提取像素坐标
Eigen::Matrix<double,2,1> obs1;
const cv::KeyPoint &kpUn1 = pKF1->mvKeysUn[i];
obs1 << kpUn1.pt.x, kpUn1.pt.y;
//建立邊,也就是誤差
g2o::EdgeSim3ProjectXYZ* e12 = new g2o::EdgeSim3ProjectXYZ();
//連接配接的兩個頂點
e12->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id2)));
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
//反向投影在進行優化
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();
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);
}
// Optimize!
//開始優化
optimizer.initializeOptimization();
optimizer.optimize(5);
// Check inliers
//清除外點和誤差過大的點
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;
if(nCorrespondences-nBad<10)
return 0;
// Optimize again only with inliers
//再次優化,隻優化内點
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++;
}
// Recover optimized Sim3
//恢複出位姿變化關系
g2o::VertexSim3Expmap* vSim3_recov = static_cast<g2o::VertexSim3Expmap*>(optimizer.vertex(0));
g2oS12= vSim3_recov->estimate();
return nIn;
}
當我們用第m幀推算了回環幀n的位姿時,使得n的位姿漂移誤差較小,但其實同時可以用第n幀來計算n-1幀的位姿,使n-1幀的位姿漂移誤差也減小。是以,這裡還要有一個位姿傳播。
另外我們可以優化所有的位姿,也就是進行一個位姿圖優化(由位姿變換建構位姿限制)。
最後,我們還可以進行一起全局所有變量的BA優化。
// 初始化
g2o::LinearSolverEigen< g2o::BlockSolver_6_3::PoseMatrixType >* linearSolver
= new g2o::LinearSolverEigen< g2o::BlockSolver_6_3::PoseMatrixType >();
g2o::BlockSolver_6_3* blockSolver = new g2o::BlockSolver_6_3(linearSolver);
g2o::OptimizationAlgorithmLevenberg* solver
= new g2o::OptimizationAlgorithmLevenberg( blockSolver );
optimizer.setAlgorithm( solver );
optimizer.setVerbose( false );
// 初始頂點
g2o::VertexSE3* v = new g2o::VertexSE3();
v->setId( cutIndex );
v->setEstimate( Eigen::Isometry3d::Identity() );
v->setFixed( true );
optimizer.addVertex( v )
// 添加新的節點(頂點)
g2o::VertexSE3* vNext = new g2o::VertexSE3();
vNext->setId(currFrame.frameID);
vNext->setEstimate( Eigen::Isometry3d::Identity());
optimizer.addVertex(vNext);
// 添加邊的資料
g2o::EdgeSE3* edgeNew = new g2o::EdgeSE3();
edgeNew->setVertex(0, optimizer.vertex(pousFrame.frameID));
edgeNew->setVertex(1, optimizer.vertex(currFrame.frameID));
edgeNew->setRobustKernel( new g2o::RobustKernelHuber() );
//edgeNew->setInformation( Eigen::Matrix2d::Identity()); //the size is worthy to research
// 資訊矩陣
// 兩個節點的轉換矩陣
Eigen::Isometry3d T = cvMat2Eigen( m_result.rotaMatrix, m_result.tranMatrix );
edgeNew->setMeasurement(T);
optimizer.addEdge(edgeNew);
// 開始優化
optimizer.initializeOptimization();
optimizer.optimize(10);
3. 參考連結
https://baijiahao.baidu.com/s?id=1627227355343777871&wfr=spider&for=pc