整體架構
構造函數進行初始化,傳入設定幾個重要的成員變量。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() 函數中計算特征點的描述子。