ORB_SLAM2 源碼閱讀 ORB_SLAM2::ORBextractor


整體架構

構造函數進行初始化,傳入設定幾個重要的成員變量。nfeatures(特征點的個數)、nlevels(構造金字塔的層數)、scaleFactor(金字塔中相鄰層圖像的比例系數)、iniThFAST(檢測 FAST 角點的閾值)、minThFAST(在 iniThFAST 沒有檢測到角點的前提下,降低的閾值)。

括號運算符對輸入的圖像進行角點檢測。1. ComputePyramid 函數構造金字塔。2. ComputeKeyPointsOctTree 對金字塔圖像進行角點檢測。3. 計算角點的描述子,輸出。

括號運算符 operator()

括號運算符輸入圖像,並且傳入引用參數 _keypoints, _descriptors 用於存儲計算得到的特征點及其描述子。

這種設計使得只需要構造一次 ORBextractor 就可以為所有圖像生成特征點。

ComputePyramid 函數

我認為 ComputePyramid 函數中調用的 OpenCV 函數 copyMakeBorder 是沒有意義的。

當 level == 0 時,或許有用,mvImagePyramid[level] 指向 temp 圖像的中間,將 image 復制了進去。

當 level != 0 是,resize 函數就已經完成了工作。

copyMakeBorder 只是對 temp 圖像的外圍進行了修改,而 temp 圖像的外圍並不存在於影像金字塔中。

ComputeKeyPointsOctTree 函數

對影像金字塔中的每一層圖像進行特征點的計算。具體的計算過程是將影像格網分割為小區域,每一個小區域獨立使用 FAST 角點檢測。檢測完成之后使用 DistributeOctTree 函數對檢測得到的所有角點進行篩選,使得角點分布均勻。

划分格網計算

I 確定計算角點的范圍:

對影像“裁邊”周圍 EDGE_THRESHOLD-3 的像素不進行角點檢測。

const int minBorderX = EDGE_THRESHOLD-3;
const int minBorderY = minBorderX;
const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;

II 確定確定格網的數量:

nCols, nRows 是寬高方向上格網的的數量,wCell, hCell 是每個格網的寬和高。

使用這種方式計算能夠使得最后不完整的 Cell 盡可能不會太小。W 是預估 Cell 的寬高。

const float W = 30;

const int nCols = width/W;
const int nRows = height/W;
const int wCell = ceil(width/nCols);
const int hCell = ceil(height/nRows);

如 640*480 的影像,進行裁邊之后是 608*448。計算得到:

nCols == 20, wCell == 31;
nRows == 14, hCell == 32;
nCols * wCell == 620;
620 % 31 == 19; // constrast to 608 % 30 == 8
nRows * hCell == 448;

III 循環生成每個 Cell 的角點:

最后計算的 Cell 的寬或高不會小於7。因為 FAST 計算的鄰域是直徑為7的 BressenHam 圓。

數字7與代碼中出現的數字6對應。

最后生成的 FAST 角點存放在 vToDistributeKeys 中,坐標是以 (minBorderX, minBorderY) 為原點(左上)的坐標。

八叉樹分配

DistributeOctTree 這個是八叉樹分配的意思,但是函數中每次對節點進行分裂都是分裂成4個。

// If more than one point, subdivide
ExtractorNode n1,n2,n3,n4;
lit->DivideNode(n1,n2,n3,n4);

Excuse me,這東西叫 DistributeQuadTree 好不好?畢竟是在平面上划分。

I 水平划分:

將影像進行水平划分,nIni 是水平划分的數量,hX 是水平划分格子的寬度。

const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));
const float hX = static_cast<float>(maxX-minX)/nIni;

注:這里有 bug,如果輸入的是一張 寬高比 小於 0.5 的圖像,nIni 計算得到 0,計算 hX 會報錯。

將所有的特征點分配到 lNodes 的 node 中,vpIniNodes 存在的意義只是分配特征點。

對於每個 node 而言,若其只有一個特征點,bNoMore 為 true,表明其不用再繼續划分。

II Node 四叉分裂:

進入循環

while(!bFinish)
{
...
}

i. 循環首先遍歷 lNodes 中的每一個節點,對節點進行分裂:

ExtractorNode n1,n2,n3,n4;
lit->DivideNode(n1,n2,n3,n4);

分裂成 n1, n2, n3, n4 四個節點,將這四個節點中含有特征點的節點存入 lNodes 的最前面,因為是向后遍歷的,不希望一次遍歷下來會遍歷到前面加進來的節點。如果分裂出的節點中特征點的個數大於1,說明還可以分裂,將特征點數、節點的指針存入 vSizeAndPointerToNode 中,nToExpand 計數器加1,后面會用於判斷可分裂的能力。

ii. 在分裂成功之后進行判斷

判斷如果當前節點數量大於等於需要的特征點數量(N),或者分裂過程並沒有增加節點的數量,說明不需要再進行分裂了,bFinish 設置為 true,可以跳出循環。

這個判斷好像不是很充分,因為前面的分裂是整體的,只用一個數量參數來判斷,有點牽強,有些分裂出的節點可以繼續分裂。

如果上面的條件不滿足,判斷條件

((int)lNodes.size()+nToExpand*3)>N

是否滿足。滿足表明了再如果所有的節點再進行一次完全分裂(所有節點都能分裂成4個節點),可以滿足特征點數量的要求。

vSizeAndPointerToNode 是前面分裂出來的子節點(n1, n2, n3, n4)中可以分裂的節點。按照它們特征點的排序,先從特征點多的開始分裂,分裂的結果繼續存儲在 lNodes 中。每分裂一個節點都會進行一次判斷,如果 lNodes 中的節點數量大於所需要的特征點數量,退出整個 while(!bFinish) 循環。

如果進行了一次分裂,並沒有增加節點數量,不玩了,退出整個 while(!bFinish) 循環。

III 取最大響應點:

在前面的工作完成時 lNodes 中節點的數量應該大於所需要的特征點數量 N,如果不是大於,那么很抱歉,也只能這樣子了。

取出每一個節點中最大響應的特征點,存儲進 vResultKeys 中。

函數返回。

總結

剩下的代碼是在 ComputeKeyPointsOctTree 函數中對過濾出的特征點坐標進行調整,調整到整個圖像的坐標,計算特征點的方向。

在 operator() 函數中計算特征點的描述子。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM