ORB特征点提取与均匀化——ORBSLAM2源码讲解(一)
文章目錄
- 前言
- 一、基礎知識
- 二、ORB特征均勻化策略對性能的影響
- 三、ORB特征金字塔
- 四、ORB提取擴展圖像
- 五、ORB特征均勻化
- 總結
前言
本博客結合嗶哩大學視頻ORBSLAM2【ORBSLAM2源碼講解專題一】ORB特征點提取與均勻化策略和高翔的《視覺SLAM十四講》總結。
代碼參照github的ORB_SLAM2_detailed_comments
ORBSLAM2代碼很經典,而且代碼量大,會分成多個博客研究,下為slam專題的其他鏈接:
代碼要求環境做了總結,解決了配置中的一些常見問題:slam的環境配置大全–保姆教學
代碼如何運行記錄:ORB_SLAM2代碼的簡介安裝運行
一、基礎知識
特征點法:我們需要從圖像中選取比較有代表性的點,這些點在相機發生少量視角變化時會保持不變,在此基礎上討論相機位姿估計問題,在經典的SLAM模型中我們稱他們為路標,在視覺SLAM中路標則是圖像特征(feature)。
特征點: 特征點是圖像里一些特別的地方,我們可以把圖像中的角點、邊緣和區塊都當成圖像中的代表性的地方。為此,計算機視覺領域研究出了很多特征點的提取方法,如著名的SIFT、SURF、ORB,這些人工設計的特征點有一下特點:
- 可重復性:形同的“區域”可以在不同的圖像中找到。
- 可區別性:不同的“區域”有不同的表達。
- 高效率:同一圖像中,特征點的數量遠小于像素的數量。
- 本地性:特征僅與一小片圖像區域有關。
關鍵點和描述子: 特征點由關鍵點(Key-point)和描述子(Descriptor)兩部分組成,比如當要談論SIFT特征時,是指“提取SIFT關鍵點,并計算SIFT描述子”兩件事情。關鍵點是指該特征點在圖像里的位置,有些特征點還具有朝向、大小等信息。描述子通常是一個向量,按照認為的方式描述該點周圍像素的信息。描述子是按照“外觀相似的特征應該有相似的描述子”的原則設計的。
ORB特征: 不同的圖像特征特點不同。例如SIFT充分考慮了圖像變換中的關照、尺度等變化,很精確但計算量很大,普通的CPU無法實時計算SIFT特征,進行定位建圖。ORB適當的降低了精度和健壯性以提升計算速度,在同一圖像中提取1000個特征點,ORB要15.3ms,SURF要217.3ms,SIFT約花費5228.7ms。ORB特征也是由關鍵點和描述子組成,他的關鍵點稱為“Oriented FAST”,Oriented FAST的詳細描述在我的另一篇博客Oriented Fast神奇高效的代碼實現方式——ORBSLAM2源碼講解(二)。他的描述子稱為BRIEF(Binary Robust Independent Elementary Feature)。因此,提取ORB特征分為如下兩個步驟:
-
FAST角點提取:找出圖像中的“角點”。相較于原版的FAST,ORB中計算了特征點的主方向,為后續的BRIEF描述子增加了旋轉不變的特性。
-
BRIEF描述子:對前一步提取出特征點的周圍圖像區域進行描述。
其中ORB特征的描述為本博客的主要內容。
二、ORB特征均勻化策略對性能的影響
ORB特征提取相比較與其他的特征提取法有什么優勢呢?
ORB-SLAM2中的ORB特征均勻化提取方法 vs. OpenCV中的ORB函數: ? 提高了ORB-SLAM2的軌跡精度和魯棒性 ? 兩者運行時間差別不大 ? 增加特征提取的均勻性可以提高系統精度 ? 但是可能會降低特征提取的重復性ORB-SLAM2與opencv對比如圖:
有一篇知乎的文章對此做了總結:[ORB-SLAM2] ORB特征提取策略對ORB-SLAM2性能的影響
綜上沒有直接用opencv里的提取函數,而是采用ORB提取,如何實現這些優質的特性呢?和opencv有什么區別呢?詳見下文。
三、ORB特征金字塔
這個圖像金字塔的作用是實現尺度的不變性,如下圖所示:
藍色的是圖像,每往上走一層就長寬縮小二分之一,左一的紅圈是實際中的一個區域在藍色圖中的成像,相機就在虛線位置。若鏡頭往前推,則這個點就變大了,每一層都跟著變大,但第二列變大后的第二層和第一列的第一層是一樣大的,我們做特征匹配時,就可以拿第二列的第二層和第一列的第一層相匹配,這倆差距不大。若拿第一列的第一層和第二列的第一層匹配,因為差距過大是匹配不上的。同樣第三列的第一層和第一列的第二層相匹配。由此可以實現無論鏡頭怎么移動,都可以相匹配。
四、ORB提取擴展圖像
如下圖所示,灰色區域的是原圖,FAST特征點提取的時候需要以3pixels的半徑的特征點去提取,所以對外擴充一個3的邊,形成中間的方框。為了進行高斯模糊,所以又擴充了一個最大的框。
這一擴充功能對應的代碼在src包的ORBextractor.cc的1682行。其注釋如下:
五、ORB特征均勻化
如何實現特征點的均勻化呢?如下圖中要提取25個特征點,如何讓這個25個特征點更均勻?
如下長圖分五步實現:第一步只有一個node,整個就是一個節點。第二步,我們需要進行分裂,分成四份,總共有四個節點。因為我們想要的到25個特征,每個特征在一個節點內,所以4<25,還要繼續分。第三步,繼續分,16個節點出現一個節點內沒有特征點,無效刪除,16<25繼續分。第四步繼續分,本來有64個,除去無效的,30>25,所以結束分裂。第五步,從每個node里選擇一個質量最好的點,比如左上有唯一特征點,不用挑了。左側第二個,有兩個特征點,選擇響應值比較高的,最后選出來就是第五步圖所示。
此處借助知乎的一篇文章:ORB-SLAM中的ORB特征(提取)
提取流程概覽:
原理比較簡單,代碼實現起來比較復雜,上面方法的代碼注釋解析如下(在src文件下的ORBextractor.cc的715行開始):
vector<cv::KeyPoint> ORBextractor::DistributeOctTree(const vector<cv::KeyPoint>& vToDistributeKeys, const int &minX,const int &maxX, const int &minY, const int &maxY, const int &N, const int &level) {// Compute how many initial nodes// Step 1 根據寬高比確定初始節點數目//計算應該生成的初始節點個數,根節點的數量nIni是根據邊界的寬高比值確定的,一般是1或者2// ! bug: 如果寬高比小于0.5,nIni=0, 后面hx會報錯const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY)); //此處的maxX等在“ORB提取擴展圖像”的圖片上,round為取整。//一個初始的節點的x方向有多少個像素const float hX = static_cast<float>(maxX-minX)/nIni;//存儲有提取器節點的鏈表list<ExtractorNode> lNodes;//存儲初始提取器節點指針的vectorvector<ExtractorNode*> vpIniNodes;//重新設置其大小vpIniNodes.resize(nIni);// Step 2 生成初始提取器節點for(int i=0; i<nIni; i++){ //生成一個提取器節點ExtractorNode ni;//設置提取器節點的圖像邊界//注意這里和提取FAST角點區域相同,都是“半徑擴充圖像”,特征點坐標從0 開始 ni.UL = cv::Point2i(hX*static_cast<float>(i),0); //UpLeftni.UR = cv::Point2i(hX*static_cast<float>(i+1),0); //UpRightni.BL = cv::Point2i(ni.UL.x,maxY-minY); //BottomLeftni.BR = cv::Point2i(ni.UR.x,maxY-minY); //BottomRight//重設vkeys大小ni.vKeys.reserve(vToDistributeKeys.size());//將剛才生成的提取節點添加到鏈表中//雖然這里的ni是局部變量,但是由于這里的push_back()是拷貝參數的內容到一個新的對象中然后再添加到列表中//所以當本函數退出之后這里的內存不會成為“野指針”lNodes.push_back(ni);//存儲這個初始的提取器節點句柄vpIniNodes[i] = &lNodes.back();}//Associate points to childs// Step 3 將特征點分配到子提取器節點中for(size_t i=0;i<vToDistributeKeys.size();i++){//獲取這個特征點對象const cv::KeyPoint &kp = vToDistributeKeys[i];//按特征點的橫軸位置,分配給屬于那個圖像區域的提取器節點(最初的提取器節點)vpIniNodes[kp.pt.x/hX]->vKeys.push_back(kp);}// Step 4 遍歷此提取器節點列表,標記那些不可再分裂的節點,刪除那些沒有分配到特征點的節點// ? 這個步驟是必要的嗎?感覺可以省略,通過判斷nIni個數和vKeys.size() 就可以吧list<ExtractorNode>::iterator lit = lNodes.begin();while(lit!=lNodes.end()){//如果初始的提取器節點所分配到的特征點個數為1if(lit->vKeys.size()==1){//那么就標志位置位,表示此節點不可再分lit->bNoMore=true;//更新迭代器lit++;}///如果一個提取器節點沒有被分配到特征點,那么就從列表中直接刪除它else if(lit->vKeys.empty())//注意,由于是直接刪除了它,所以這里的迭代器沒有必要更新;否則反而會造成跳過元素的情況lit = lNodes.erase(lit); else//如果上面的這些情況和當前的特征點提取器節點無關,那么就只是更新迭代器 lit++;}//結束標志位清空bool bFinish = false;//記錄迭代次數,只是記錄,并未起到作用int iteration = 0;//聲明一個vector用于存儲節點的vSize和句柄對//這個變量記錄了在一次分裂循環中,那些可以再繼續進行分裂的節點中包含的特征點數目和其句柄vector<pair<int,ExtractorNode*> > vSizeAndPointerToNode;//調整大小,這里的意思是一個初始化節點將“分裂”成為四個vSizeAndPointerToNode.reserve(lNodes.size()*4);// Step 5 利用四叉樹方法對圖像進行劃分區域,均勻分配特征點while(!bFinish){//更新迭代次數計數器,只是記錄,并未起到作用iteration++;//保存當前節點個數,prev在這里理解為“保留”比較好int prevSize = lNodes.size();//重新定位迭代器指向列表頭部lit = lNodes.begin();//需要展開的節點計數,這個一直保持累計,不清零int nToExpand = 0;//因為是在循環中,前面的循環體中可能污染了這個變量,所以清空//這個變量也只是統計了某一個循環中的點//這個變量記錄了在一次分裂循環中,那些可以再繼續進行分裂的節點中包含的特征點數目和其句柄vSizeAndPointerToNode.clear();// 將目前的子區域進行劃分//開始遍歷列表中所有的提取器節點,并進行分解或者保留while(lit!=lNodes.end()){//如果提取器節點只有一個特征點,if(lit->bNoMore){// If node only contains one point do not subdivide and continue//那么就沒有必要再進行細分了lit++;//跳過當前節點,繼續下一個continue;}else{// If more than one point, subdivide//如果當前的提取器節點具有超過一個的特征點,那么就要進行繼續分裂ExtractorNode n1,n2,n3,n4;//再細分成四個子區域,DivideNode函數是進行結點劃分lit->DivideNode(n1,n2,n3,n4); // Add childs if they contain points//如果這里分出來的子區域中有特征點,那么就將這個子區域的節點添加到提取器節點的列表中//注意這里的條件是,有特征點即可if(n1.vKeys.size()>0){//注意這里也是添加到列表前面的lNodes.push_front(n1); //再判斷其中子提取器節點中的特征點數目是否大于1if(n1.vKeys.size()>1){//如果有超過一個的特征點,那么待展開的節點計數加1nToExpand++;//保存這個特征點數目和節點指針的信息vSizeAndPointerToNode.push_back(make_pair(n1.vKeys.size(),&lNodes.front()));//?這個訪問用的句柄貌似并沒有用到?// lNodes.front().lit 和前面的迭代的lit 不同,只是名字相同而已// lNodes.front().lit是node結構體里的一個指針用來記錄節點的位置// 迭代的lit 是while循環里作者命名的遍歷的指針名稱lNodes.front().lit = lNodes.begin();}}//后面的操作都是相同的,這里不再贅述if(n2.vKeys.size()>0){lNodes.push_front(n2);if(n2.vKeys.size()>1){nToExpand++;vSizeAndPointerToNode.push_back(make_pair(n2.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}if(n3.vKeys.size()>0){lNodes.push_front(n3);if(n3.vKeys.size()>1){nToExpand++;vSizeAndPointerToNode.push_back(make_pair(n3.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}if(n4.vKeys.size()>0){lNodes.push_front(n4);if(n4.vKeys.size()>1){nToExpand++;vSizeAndPointerToNode.push_back(make_pair(n4.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}//當這個母節點expand之后就從列表中刪除它了,能夠進行分裂操作說明至少有一個子節點的區域中特征點的數量是>1的// 分裂方式是后加的節點先分裂,先加的后分裂lit=lNodes.erase(lit);//繼續下一次循環,其實這里加不加這句話的作用都是一樣的continue;}//判斷當前遍歷到的節點中是否有超過一個的特征點}//遍歷列表中的所有提取器節點// Finish if there are more nodes than required features or all nodes contain just one point//停止這個過程的條件有兩個,滿足其中一個即可://1、當前的節點數已經超過了要求的特征點數//2、當前所有的節點中都只包含一個特征點if((int)lNodes.size()>=N //判斷是否超過了要求的特征點數|| (int)lNodes.size()==prevSize) //prevSize中保存的是分裂之前的節點個數,如果分裂之前和分裂之后的總節點個數一樣,說明當前所有的//節點區域中只有一個特征點,已經不能夠再細分了{//停止標志置位bFinish = true;}// Step 6 當再劃分之后所有的Node數大于要求數目時,就慢慢劃分直到使其剛剛達到或者超過要求的特征點個數//可以展開的子節點個數nToExpand x3,是因為一分四之后,會刪除原來的主節點,所以乘以3/*** //?BUG 但是我覺得這里有BUG,雖然最終作者也給誤打誤撞、稀里糊涂地修復了* 注意到,這里的nToExpand變量在前面的執行過程中是一直處于累計狀態的,如果因為特征點個數太少,跳過了下面的else-if,又進行了一次上面的遍歷* list的操作之后,lNodes.size()增加了,但是nToExpand也增加了,尤其是在很多次操作之后,下面的表達式:* ((int)lNodes.size()+nToExpand*3)>N* 會很快就被滿足,但是此時只進行一次對vSizeAndPointerToNode中點進行分裂的操作是肯定不夠的;* 理想中,作者下面的for理論上只要執行一次就能滿足,不過作者所考慮的“不理想情況”應該是分裂后出現的節點所在區域可能沒有特征點,因此將for* 循環放在了一個while循環里面,通過再次進行for循環、再分裂一次解決這個問題。而我所考慮的“不理想情況”則是因為前面的一次對vSizeAndPointerToNode* 中的特征點進行for循環不夠,需要將其放在另外一個循環(也就是作者所寫的while循環)中不斷嘗試直到達到退出條件。 * */else if(((int)lNodes.size()+nToExpand*3)>N){//如果再分裂一次那么數目就要超了,這里想辦法盡可能使其剛剛達到或者超過要求的特征點個數時就退出//這里的nToExpand和vSizeAndPointerToNode不是一次循環對一次循環的關系,而是前者是累計計數,后者只保存某一個循環的//一直循環,直到結束標志位被置位while(!bFinish){//獲取當前的list中的節點個數prevSize = lNodes.size();//保留那些還可以分裂的節點的信息, 這里是深拷貝vector<pair<int,ExtractorNode*> > vPrevSizeAndPointerToNode = vSizeAndPointerToNode;//清空vSizeAndPointerToNode.clear();// 對需要劃分的節點進行排序,對pair對的第一個元素進行排序,默認是從小到大排序// 優先分裂特征點多的節點,使得特征點密集的區域保留更少的特征點//! 注意這里的排序規則非常重要!會導致每次最后產生的特征點都不一樣。建議使用 stable_sortsort(vPrevSizeAndPointerToNode.begin(),vPrevSizeAndPointerToNode.end());//遍歷這個存儲了pair對的vector,注意是從后往前遍歷for(int j=vPrevSizeAndPointerToNode.size()-1;j>=0;j--){ExtractorNode n1,n2,n3,n4;//對每個需要進行分裂的節點進行分裂vPrevSizeAndPointerToNode[j].second->DivideNode(n1,n2,n3,n4);// Add childs if they contain points//其實這里的節點可以說是二級子節點了,執行和前面一樣的操作if(n1.vKeys.size()>0){lNodes.push_front(n1);if(n1.vKeys.size()>1){//因為這里還有對于vSizeAndPointerToNode的操作,所以前面才會備份vSizeAndPointerToNode中的數據//為可能的、后續的又一次for循環做準備vSizeAndPointerToNode.push_back(make_pair(n1.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}if(n2.vKeys.size()>0){lNodes.push_front(n2);if(n2.vKeys.size()>1){vSizeAndPointerToNode.push_back(make_pair(n2.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}if(n3.vKeys.size()>0){lNodes.push_front(n3);if(n3.vKeys.size()>1){vSizeAndPointerToNode.push_back(make_pair(n3.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}if(n4.vKeys.size()>0){lNodes.push_front(n4);if(n4.vKeys.size()>1){vSizeAndPointerToNode.push_back(make_pair(n4.vKeys.size(),&lNodes.front()));lNodes.front().lit = lNodes.begin();}}//刪除母節點,在這里其實應該是一級子節點lNodes.erase(vPrevSizeAndPointerToNode[j].second->lit);//判斷是是否超過了需要的特征點數?是的話就退出,不是的話就繼續這個分裂過程,直到剛剛達到或者超過要求的特征點個數//作者的思想其實就是這樣的,再分裂了一次之后判斷下一次分裂是否會超過N,如果不是那么就放心大膽地全部進行分裂(因為少了一個判斷因此//其運算速度會稍微快一些),如果會那么就引導到這里進行最后一次分裂if((int)lNodes.size()>=N)break;}//遍歷vPrevSizeAndPointerToNode并對其中指定的node進行分裂,直到剛剛達到或者超過要求的特征點個數//這里理想中應該是一個for循環就能夠達成結束條件了,但是作者想的可能是,有些子節點所在的區域會沒有特征點,因此很有可能一次for循環之后//的數目還是不能夠滿足要求,所以還是需要判斷結束條件并且再來一次//判斷是否達到了停止條件if((int)lNodes.size()>=N || (int)lNodes.size()==prevSize)bFinish = true; }//一直進行nToExpand累加的節點分裂過程,直到分裂后的nodes數目剛剛達到或者超過要求的特征點數目}//當本次分裂后達不到結束條件但是再進行一次完整的分裂之后就可以達到結束條件時}// 根據興趣點分布,利用4叉樹方法對圖像進行劃分區域// Retain the best point in each node// Step 7 保留每個區域響應值最大的一個興趣點//使用這個vector來存儲我們感興趣的特征點的過濾結果vector<cv::KeyPoint> vResultKeys;//調整容器大小為要提取的特征點數目vResultKeys.reserve(nfeatures);//遍歷這個節點鏈表for(list<ExtractorNode>::iterator lit=lNodes.begin(); lit!=lNodes.end(); lit++){//得到這個節點區域中的特征點容器句柄vector<cv::KeyPoint> &vNodeKeys = lit->vKeys;//得到指向第一個特征點的指針,后面作為最大響應值對應的關鍵點cv::KeyPoint* pKP = &vNodeKeys[0];//用第1個關鍵點響應值初始化最大響應值float maxResponse = pKP->response;//開始遍歷這個節點區域中的特征點容器中的特征點,注意是從1開始喲,0已經用過了for(size_t k=1;k<vNodeKeys.size();k++){//更新最大響應值if(vNodeKeys[k].response>maxResponse){//更新pKP指向具有最大響應值的keypointspKP = &vNodeKeys[k];maxResponse = vNodeKeys[k].response;}}//將這個節點區域中的響應值最大的特征點加入最終結果容器vResultKeys.push_back(*pKP);}//返回最終結果容器,其中保存有分裂出來的區域中,我們最感興趣、響應值最大的特征點return vResultKeys; }總結
總結
以上是生活随笔為你收集整理的ORB特征点提取与均匀化——ORBSLAM2源码讲解(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Jmeter在win10上字体界面小永久
- 下一篇: Kudu模式设计