目錄
一、各成員函式變數
1、定義一個列舉型別用于表示使用HARRIS回應值還是使用FAST回應值
2、行內函式都是用來直接獲取類的成員變數的
3、保護成員
二、計算特征點的方向 computeOrientation()
2.1、灰度質心法演算法步驟
1、計算一個半徑為15的近似圓
2、計算特征點角度
3、IC_Angle 計算技巧
4、灰度質心法計算公式
5、計算特征點的方向(computeOrientation)
三、FAST描述子
BRIEF描述子生成步驟
五、金字塔的計算(ORBextractor::ComputePyramid)
六、提取FAST特征點
6.1、分cell搜索特征點
6.2、提取特征點
6.3、四叉樹篩選特征點: DistributeOctTree()
6.4、 最后計算這些特征點的方向資訊
七、總結
一、各成員函式變數
在閱讀代碼之前我們先來介紹變數的命名規則
1、定義一個列舉型別用于表示使用HARRIS回應值還是使用FAST回應值
nfeatures | 指定要提取出來的特征點數目 |
scaleFactor | 影像金字塔的縮放系數 |
nlevels | 指定需要提取特征點的影像金字塔層 |
| iniThFAST | 初始的默認FAST回應值閾值 |
minThFAST | 較小的FAST回應值閾值 |
2、行內函式都是用來直接獲取類的成員變數的
GetScaleFactor() | 獲取當前提取器所在的影像的縮放因子 |
mvScaleFactor | 影像金字塔中每個圖層相對于底層影像的縮放因子 |
GetInverseScaleFactors() | 獲取上面的那個縮放因子s的倒數 |
GetScaleSigmaSquares() | 獲取sigma^2,就是每層影像相對于初始影像縮放因子的平方 |
GetInverseScaleSigmaSquares() | 獲取上面sigma平方的倒數 |
mvImagePyramid | 用來存盤影像金字塔的變數,一個元素存盤一層影像 |
3、保護成員
保護成員就是私有的別人不可以呼叫
ComputePyramid | 計算其影像金字塔 |
ComputeKeyPointsOctTree | 以八叉樹分配特征點的方式,計算影像金字塔中的特征點 |
vToDistributeKeys | 等待分配的特征點 |
mnFeaturesPerLevel | 分配到每層影像中,要提取的特征點數目 |
umax | 計算特征點方向的時候,有個圓形的影像區域,這個vector中存盤了每行u軸的邊界(四分之一,其他部分通過對稱獲得) |
二、計算特征點的方向 computeOrientation()
計算特征點的方向是為了使得提取的特征點具有旋轉不變性
方法是灰度質心法:以幾何中心和灰度質心的連線作為該特征點方向
2.1、灰度質心法演算法步驟
1、計算一個半徑為15的近似圓

后面計算的是特征點主方向上的描述子,計算程序中要將特征點周圍像素旋轉到主方向上,因此計算一個半徑為16的圓的近似坐標,用于后面計算描述子時進行旋轉操作.

PATCH_SIZE | 影像塊的大小,或者說是直徑 | 31 |
HALF_PATCH_SIZE | 上面這個大小的一半,或者說是半徑 | 15 |
EDGE_THRESHOLD | 演算法生成的影像邊 | 19 |
u_max | 影像塊的每一行的坐標邊界 | |
| float | 回傳特征點的角度,范圍為[0,360)角度,精度為0.3° |
int vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1); // 45°射線與圓周交點的縱坐標
int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2); // 45°射線與圓周交點的縱坐標
// 先計算下半45度的umax
for (int v = 0; v <= vmax; ++v) {
umax[v] = cvRound(sqrt(15 * 15 - v * v));
}
// 根據對稱性補出上半45度的umax
for (int v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v) {
while (umax[v0] == umax[v0 + 1])
++v0;
umax[v] = v0;
++v0;
}

2、計算特征點角度
點v 繞 原點旋轉θ 角,得到點v’,假設 v點的坐標是(x, y) ,那么可以推導得到 v’點的坐標(x’, y’)

float angle = (float)kpt.angle*factorPI;
float a = (float)cos(angle), b = (float)sin(angle);
const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
const int step = (int)img.step;
// 旋轉公式
// x'= xcos(θ) - ysin(θ)
// y'= xsin(θ) + ycos(θ)
#define GET_VALUE(idx) \
center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)]
3、IC_Angle 計算技巧
在一個圓域中算出m10(x坐標)和m01(y坐標),計算步驟是先算出中間紅線的m10,然后在平行于 x軸算出m10和m01,一次計算相當于影像中的同個顏色的兩個line

4、灰度質心法計算公式

static float IC_Angle(const Mat& image, Point2f pt, const vector<int> & u_max)
{
//影像的矩,前者是按照影像塊的y坐標加權,后者是按照影像塊的x坐標加權
int m_01 = 0, m_10 = 0;
//獲得這個特征點所在的影像塊的中心點坐標灰度值的指標center
const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));
// Treat the center line differently, v=0
//這條v=0中心線的計算需要特殊對待
//后面是以中心行為對稱軸,成對遍歷行數,所以PATCH_SIZE必須是奇數
for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
//注意這里的center下標u可以是負的!中心水平線上的像素按x坐標(也就是u坐標)加權
m_10 += u * center[u];
// Go line by line in the circular patch
//這里的step1表示這個影像一行包含的位元組總數,參考[https://blog.csdn.net/qianqing13579/article/details/45318279]
int step = (int)image.step1();
//注意這里是以v=0中心線為對稱軸,然后對稱地每成對的兩行之間進行遍歷,這樣處理加快了計算速度
for (int v = 1; v <= HALF_PATCH_SIZE; ++v)
{
// Proceed over the two lines
//本來m_01應該是一列一列地計算的,但是由于對稱以及坐標x,y正負的原因,可以一次計算兩行
int v_sum = 0;
// 獲取某行像素橫坐標的最大范圍,注意這里的影像塊是圓形的!
int d = u_max[v];
//在坐標范圍內挨個像素遍歷,實際是一次遍歷2個
// 假設每次處理的兩個點坐標,中心線下方為(x,y),中心線上方為(x,-y)
// 對于某次待處理的兩個點:m_10 = Σ x*I(x,y) = x*I(x,y) + x*I(x,-y) = x*(I(x,y) + I(x,-y))
// 對于某次待處理的兩個點:m_01 = Σ y*I(x,y) = y*I(x,y) - y*I(x,-y) = y*(I(x,y) - I(x,-y))
for (int u = -d; u <= d; ++u)
{
//得到需要進行加運算和減運算的像素灰度值
//val_plus:在中心線下方x=u時的的像素灰度值
//val_minus:在中心線上方x=u時的像素灰度值
int val_plus = center[u + v*step], val_minus = center[u - v*step];
//在v(y軸)上,2行所有像素灰度值之差
v_sum += (val_plus - val_minus);
//u軸(也就是x軸)方向上用u坐標加權和(u坐標也有正負符號),相當于同時計算兩行
m_10 += u * (val_plus + val_minus);
}
//將這一行上的和按照y坐標加權
m_01 += v * v_sum;
}
//為了加快速度還使用了fastAtan2()函式,輸出為[0,360)角度,精度為0.3°
return fastAtan2((float)m_01, (float)m_10);
}
///乘數因子,一度對應著多少弧度
const float factorPI = (float)(CV_PI/180.f);
5、計算特征點的方向(computeOrientation)
static void computeOrientation(const Mat& image, vector<KeyPoint>& keypoints, const vector<int>& umax)
{
// 遍歷所有的特征點
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
{
// 呼叫IC_Angle 函式計算這個特征點的方向
keypoint->angle = IC_Angle(image, //特征點所在的圖層的影像
keypoint->pt, //特征點在這張影像中的坐標
umax); //每個特征點所在影像區塊的每行的邊界 u_max 組成的vector
}
}
三、FAST描述子
BRIEF演算法的核心思想是在關鍵點P的周圍以一定模式選取N個點對,把這N個點對的比較結果組合起來作為描述子,

BRIEF描述子生成步驟
1.以關鍵點P為圓心,以d為半徑做圓O,
2.在圓O內某一模式選取N個點對,這里為方便說明,N=4,實際應用中N可以取512.
假設當前選取的4個點對如上圖所示分別標記為:
3.定義操作T

4.分別對已選取的點對進行T操作,將得到的結果進行組合,
假如:

原始的BRIEF描述子沒有方向不變性,通過加入關鍵點的方向來計算描述子,稱之為Steer BRIEF,具有較好旋轉不變特性
具體地,在計算的時候需要將這里選取的采樣模板中點的x軸方向旋轉到特征點的方向,
獲得采樣點中某個idx所對應的點的灰度值,這里旋轉前坐標為(x,y), 旋轉后坐標(x',y'),他們的變換關系:
x'= xcos(θ) - ysin(θ), y'= xsin(θ) + ycos(θ)
下面表示 y'* step + x'
#define GET_VALUE(idx) center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)]
//brief描述子由32*8位組成
//其中每一位是來自于兩個像素點灰度的直接比較,所以每比較出8bit結果,需要16個隨機點,這也就是為什么pattern需要+=16的原因
for (int i = 0; i < 32; ++i, pattern += 16)
{
int t0, //參與比較的第1個特征點的灰度值
t1, //參與比較的第2個特征點的灰度值
val; //描述子這個位元組的比較結果,0或1
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
val = t0 < t1; //描述子本位元組的bit0
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1; //描述子本位元組的bit1
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
val |= (t0 < t1) << 2; //描述子本位元組的bit2
t0 = GET_VALUE(6); t1 = GET_VALUE(7);
val |= (t0 < t1) << 3; //描述子本位元組的bit3
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
val |= (t0 < t1) << 4; //描述子本位元組的bit4
t0 = GET_VALUE(10); t1 = GET_VALUE(11);
val |= (t0 < t1) << 5; //描述子本位元組的bit5
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
val |= (t0 < t1) << 6; //描述子本位元組的bit6
t0 = GET_VALUE(14); t1 = GET_VALUE(15);
val |= (t0 < t1) << 7; //描述子本位元組的bit7
//保存當前比較的出來的描述子的這個位元組
desc[i] = (uchar)val;
}
//為了避免和程式中的其他部分沖突在,在使用完成之后就取消這個宏定義
#undef GET_VALUE
}
五、金字塔的計算(ORBextractor::ComputePyramid)
金字塔是為了實作尺度不變性

具體實作方式如上圖所示,當攝像機靠近影像,特征點變大,能提取的特征點變少;當攝像機遠離影像時特征點變小,能提取到的特征點變多,我們可以觀察到攝像機在正常位置時,第0層的特征點與攝像機往前移動第1層的特征點差不多大,利用這個特性我們可以實作尺度不變性,
iniThFAST | 指定初始的FAST特征點提取引數,可以提取出最明顯的角點 |
minThFAST | 如果初始閾值沒有檢測到角點,降低到這個閾值提取出弱一點的角點 |
ORBextractor::ORBextractor(int _nfeatures, //指定要提取的特征點數目
float _scaleFactor, //指定影像金字塔的縮放系數
int _nlevels, //指定影像金字塔的層數
int _iniThFAST, //指定初始的FAST特征點提取引數,可以提取出最明顯的角點
int _minThFAST): //如果初始閾值沒有檢測到角點,降低到這個閾值提取出弱一點的角點
nfeatures(_nfeatures), scaleFactor(_scaleFactor), nlevels(_nlevels),
iniThFAST(_iniThFAST), minThFAST(_minThFAST)//設定這些引數
{
//存盤每層影像縮放系數的vector調整為符合圖層數目的大小
mvScaleFactor.resize(nlevels);
//存盤這個sigma^2,其實就是每層影像相對初始影像縮放因子的平方
mvLevelSigma2.resize(nlevels);
//對于初始影像,這兩個引數都是1
mvScaleFactor[0]=1.0f;
mvLevelSigma2[0]=1.0f;
函式void ORBextractor::ComputePyramid(cv::Mat image)逐層計算影像金字塔,對于每層影像進行以下兩步:
1、先進行圖片縮放,縮放到mvInvScaleFactor對應尺寸.
2、在影像外補一圈厚度為19的padding(提取FAST特征點需要特征點周圍半徑為3的圓域,計算ORB描述子需要特征點周圍半徑為16的圓域).
下圖表示影像金字塔每層結構:
深灰色為縮放后的原始影像.
包含綠色邊界在內的矩形用于提取FAST特征點.
包含淺灰色邊界在內的整個矩形用于計算ORB描述子.
//計算這層影像的坐標邊界, NOTICE 注意這里是坐標邊界,EDGE_THRESHOLD指的應該是可以提取特征點的有效影像邊界,后面會一直使用“有效影像邊界“這個自創名詞
const int minBorderX = EDGE_THRESHOLD-3; //這里的3是因為在計算FAST特征點的時候,需要建立一個半徑為3的圓
const int minBorderY = minBorderX; //minY的計算就可以直接拷貝上面的計算結果了
const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
void ORBextractor::ComputePyramid(cv::Mat image) {
for (int level = 0; level < nlevels; ++level) {
// 計算縮放+補padding后該層影像的尺寸
float scale = mvInvScaleFactor[level];
Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
Size wholeSize(sz.width + EDGE_THRESHOLD * 2, sz.height + EDGE_THRESHOLD * 2);
Mat temp(wholeSize, image.type());
// 縮放影像并復制到對應圖層并補邊
mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
if( level != 0 ) {
resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, cv::INTER_LINEAR);
copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101+BORDER_ISOLATED);
} else {
copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101);
}
}
}
opyMakeBorder函式實作了復制和padding填充,其引數BORDER_REFLECT_101引數指定對padding進行鏡像填充

六、提取FAST特征點

6.1、分cell搜索特征點
分CELL搜索特征點,若某CELL內特征點回應值普遍較小的話就降低分數線再搜索一遍.
CELL搜索的示意圖如下,每個CELL的大小約為30?30,搜索到邊上,剩余尺寸不夠大的時候,最后一個CELL有多大就用多大的區域.

需要注意的是相鄰的CELL之間會有6像素的重疊區域,因為提取FAST特征點需要計算特征點周圍半徑為3的圓周上的像素點資訊,實際上產生特征點的區域比傳入的搜索區域小3像素.

//遍歷所有影像
for (int level = 0; level < nlevels; ++level)
{
//計算這層影像的坐標邊界, NOTICE 注意這里是坐標邊界,EDGE_THRESHOLD指的應該是可以提取特征點的有效影像邊界,后面會一直使用“有效影像邊界“這個自創名詞
const int minBorderX = EDGE_THRESHOLD-3; //這里的3是因為在計算FAST特征點的時候,需要建立一個半徑為3的圓
const int minBorderY = minBorderX; //minY的計算就可以直接拷貝上面的計算結果了
const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
//存盤需要進行平均分配的特征點
vector<cv::KeyPoint> vToDistributeKeys;
//一般地都是過量采集,所以這里預分配的空間大小是nfeatures*10
vToDistributeKeys.reserve(nfeatures*10);
//計算進行特征點提取的影像區域尺寸
const float width = (maxBorderX-minBorderX);
const float height = (maxBorderY-minBorderY);
//計算網格在當前層的影像有的行數和列數
const int nCols = width/W;
const int nRows = height/W;
//計算每個影像網格所占的像素行數和列數
const int wCell = ceil(width/nCols);
const int hCell = ceil(height/nRows);
//開始遍歷影像網格,還是以行開始遍歷的
for(int i=0; i<nRows; i++)
{
//計算當前網格初始行坐標
const float iniY =minBorderY+i*hCell;
//計算當前網格最大的行坐標,這里的+6=+3+3,即考慮到了多出來3是為了cell邊界像素進行FAST特征點提取用
//前面的EDGE_THRESHOLD指的應該是提取后的特征點所在的邊界,所以minBorderY是考慮了計算半徑時候的影像邊界
//目測一個影像網格的大小是25*25啊
float maxY = iniY+hCell+6;
//如果初始的行坐標就已經超過了有效的影像邊界了,這里的“有效影像”是指原始的、可以提取FAST特征點的影像區域
if(iniY>=maxBorderY-3)
//那么就跳過這一行
continue;
//如果影像的大小導致不能夠正好劃分出來整齊的影像網格,那么就要委屈最后一行了
if(maxY>maxBorderY)
maxY = maxBorderY;
//開始列的遍歷
for(int j=0; j<nCols; j++)
{
//計算初始的列坐標
const float iniX =minBorderX+j*wCell;
//計算這列網格的最大列坐標,+6的含義和前面相同
float maxX = iniX+wCell+6;
//判斷坐標是否在影像中
//如果初始的列坐標就已經超過了有效的影像邊界了,這里的“有效影像”是指原始的、可以提取FAST特征點的影像區域,
//并且應該同前面行坐標的邊界對應,都為-3
//!BUG 正確應該是maxBorderX-3
if(iniX>=maxBorderX-6)
continue;
//如果最大坐標越界那么委屈一下
if(maxX>maxBorderX)
maxX = maxBorderX;
這里指的應該是FAST角點可以存在的坐標位置范圍,其實就是原始影像的坐標范圍 注意這里沒有提前進行+3的操作,而是在后面計算每個網格的區域的時候使用-3的操作來處理FAST角點半徑問題 本質上和前面的思想是一樣的
//計算這個容許坐標區域的寬度和高度
const int W = maxBorderX - minBorderX;
const int H = maxBorderY - minBorderY;
//同時計算每個影像cell的寬度和高度
const int cellW = ceil((float)W/levelCols);
const int cellH = ceil((float)H/levelRows);
//計算本層影像中的總cell個數
const int nCells = levelRows*levelCols;
//ceil:回傳大于或者等于運算式的最小整數,向上取整
//這里計算了每個cell中需要提取出來的特征點數量,由于存在小數取整問題,所以都是往多了取整
const int nfeaturesCell = ceil((float)nDesiredFeatures/nCells);
6.2、提取特征點
FAST提取興趣點, 自適應閾值 并且這個向量存盤這個cell中的特征點
//這個向量存盤這個cell中的特征點
vector<cv::KeyPoint> vKeysCell;
//呼叫opencv的庫函式來檢測FAST角點
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX), //待檢測的影像,這里就是當前遍歷到的影像塊
vKeysCell, //存盤角點位置的容器
iniThFAST, //檢測閾值
true); //使能非極大值抑制
//如果這個影像塊中使用默認的FAST檢測閾值沒有能夠檢測到角點
if(vKeysCell.empty())
{
//那么就使用更低的閾值來進行重新檢測
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX), //待檢測的影像
vKeysCell, //存盤角點位置的容器
minThFAST, //更低的檢測閾值
true); //使能非極大值抑制
}
//得到的特征點的坐標,依舊是在當前圖層下來講的
keypoints = DistributeOctTree(vToDistributeKeys, //當前圖層提取出來的特征點,也即是等待剔除的特征點
//NOTICE 注意此時特征點所使用的坐標都是在“半徑擴充影像”下的
minBorderX, maxBorderX, //當前圖層影像的邊界,而這里的坐標卻都是在“邊緣擴充影像”下的
minBorderY, maxBorderY,
mnFeaturesPerLevel[level], //希望保留下來的當前層影像的特征點個數
level); //當前層影像所在的圖層
//PATCH_SIZE是對于底層的初始影像來說的,現在要根據當前圖層的尺度縮放倍數進行縮放得到縮放后的PATCH大小 和特征點的方向計算有關
const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];
// Add border to coordinates and scale information
//獲取剔除程序后保留下來的特征點數目
const int nkps = keypoints.size();
//然后開始遍歷這些特征點,恢復其在當前圖層影像坐標系下的坐標
for(int i=0; i<nkps ; i++)
{
//對每一個保留下來的特征點,恢復到相對于當前圖層“邊緣擴充影像下”的坐標系的坐標
keypoints[i].pt.x+=minBorderX;
keypoints[i].pt.y+=minBorderY;
//記錄特征點來源的影像金字塔圖層
keypoints[i].octave=level;
//記錄計算方向的patch,縮放后對應的大小, 又被稱作為特征點半徑
keypoints[i].size = scaledPatchSize;
}
}
// compute orientations
//然后計算這些特征點的方向資訊,注意這里還是分層計算的
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], //對應的圖層的影像
allKeypoints[level], //這個圖層中提取并保留下來的特征點容器
umax); //以及PATCH的橫坐標邊界
}
6.3、四叉樹篩選特征點: DistributeOctTree()
將提取器節點分成4個子節點,同時也完成影像區域的劃分、特征點歸屬的劃分,以及相關標志位的置位

void ExtractorNode::DivideNode(ExtractorNode &n1,
ExtractorNode &n2,
ExtractorNode &n3,
ExtractorNode &n4)
{
//得到當前提取器節點所在影像區域的一半長寬,當然結果需要取整
const int halfX = ceil(static_cast<float>(UR.x-UL.x)/2);
const int halfY = ceil(static_cast<float>(BR.y-UL.y)/2);
//Define boundaries of childs
//下面的操作大同小異,將一個影像區域再細分成為四個小影像區塊
//n1 存盤左上區域的邊界
n1.UL = UL;
n1.UR = cv::Point2i(UL.x+halfX,UL.y);
n1.BL = cv::Point2i(UL.x,UL.y+halfY);
n1.BR = cv::Point2i(UL.x+halfX,UL.y+halfY);
//用來存盤在該節點對應的影像網格中提取出來的特征點的vector
n1.vKeys.reserve(vKeys.size());
//n2 存盤右上區域的邊界
n2.UL = n1.UR;
n2.UR = UR;
n2.BL = n1.BR;
n2.BR = cv::Point2i(UR.x,UL.y+halfY);
n2.vKeys.reserve(vKeys.size());
//n3 存盤左下區域的邊界
n3.UL = n1.BL;
n3.UR = n1.BR;
n3.BL = BL;
n3.BR = cv::Point2i(n1.BR.x,BL.y);
n3.vKeys.reserve(vKeys.size());
//n4 存盤右下區域的邊界
n4.UL = n3.UR;
n4.UR = n2.BR;
n4.BL = n3.BR;
n4.BR = BR;
n4.vKeys.reserve(vKeys.size());
//Associate points to childs
//遍歷當前提取器節點的vkeys中存盤的特征點
for(size_t i=0;i<vKeys.size();i++)
{
//獲取這個特征點物件
const cv::KeyPoint &kp = vKeys[i];
//判斷這個特征點在當前特征點提取器節點影像的哪個區域,更嚴格地說是屬于那個子影像區塊
//然后就將這個特征點追加到那個特征點提取器節點的vkeys中
//NOTICE BUG REVIEW 這里也是直接進行比較的,但是特征點的坐標是在“半徑擴充影像”坐標系下的,而節點區域的坐標則是在“邊緣擴充影像”坐標系下的
if(kp.pt.x<n1.UR.x)
{
if(kp.pt.y<n1.BR.y)
n1.vKeys.push_back(kp);
else
n3.vKeys.push_back(kp);
}
else if(kp.pt.y<n1.BR.y)
n2.vKeys.push_back(kp);
else
n4.vKeys.push_back(kp);
}//遍歷當前提取器節點的vkeys中存盤的特征點
step1.如果圖片的寬度比較寬,就先把分成左右w/h份,一般的640×480的影像開始的時候只有一個 node,
step2.如果node里面的點數>1,把每個node分成四個node,如果node里面的特征點為空,就不要了, 刪掉,
step3.新分的node的點數>1,就再分裂成4個node,如此,一直分裂,
step4.終止條件為:node的總數量> [公式] ,或者無法再進行分裂,
step5.然后從每個node里面選擇一個質量最好的FAST點

//這里判斷是否數目等于1的目的是確定這個節點還能不能再向下進行分裂
if(n1.vKeys.size()==1)
n1.bNoMore = true;
if(n2.vKeys.size()==1)
n2.bNoMore = true;
if(n3.vKeys.size()==1)
n3.bNoMore = true;
if(n4.vKeys.size()==1)
n4.bNoMore = true;
6.4、 最后計算這些特征點的方向資訊
//遍歷影像金字塔中的每個圖層
for (int level = 0; level < nlevels; ++level)
//計算這個圖層所有特征點的方向資訊
computeOrientation(mvImagePyramid[level], //這個圖層的影像
allKeypoints[level], //這個圖層的特征點物件vector容器
umax); //patch區域的邊界
}
//注意這是一個不屬于任何類的全域靜態函式,static修飾符限定其只能夠被本檔案中的函式呼叫
/**
* @brief 計算某層金字塔影像上特征點的描述子
*
* @param[in] image 某層金字塔影像
* @param[in] keypoints 特征點vector容器
* @param[out] descriptors 描述子
* @param[in] pattern 計算描述子使用的固定隨機點集
*/
static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
const vector<Point>& pattern)
{
//清空保存描述子資訊的容器
descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);
//開始遍歷特征點
for (size_t i = 0; i < keypoints.size(); i++)
//計算這個特征點的描述子
computeOrbDescriptor(keypoints[i], //要計算描述子的特征點
image, //以及其影像
&pattern[0], //隨機點集的首地址
descriptors.ptr((int)i)); //提取出來的描述子的保存位置
}
七、總結
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
OutputArray _descriptors)
{
// Step 1 檢查影像有效性,如果影像為空,那么就直接回傳
if(_image.empty())
return;
//獲取影像的大小
Mat image = _image.getMat();
//判斷影像的格式是否正確,要求是單通道灰度值
assert(image.type() == CV_8UC1 );
// Pre-compute the scale pyramid
// Step 2 構建影像金字塔
ComputePyramid(image);
// Step 3 計算影像的特征點,并且將特征點進行均勻化,均勻的特征點可以提高位姿計算精度
// 存盤所有的特征點,注意此處為二維的vector,第一維存盤的是金字塔的層數,第二維存盤的是那一層金字塔影像里提取的所有特征點
vector < vector<KeyPoint> > allKeypoints;
//使用四叉樹的方式計算每層影像的特征點并進行分配
ComputeKeyPointsOctTree(allKeypoints);
//使用傳統的方法提取并平均分配影像的特征點,作者并未使用
//ComputeKeyPointsOld(allKeypoints);
// Step 4 拷貝影像描述子到新的矩陣descriptors
Mat descriptors;
//統計整個影像金字塔中的特征點
int nkeypoints = 0;
//開始遍歷每層影像金字塔,并且累加每層的特征點個數
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
//如果本影像金字塔中沒有任何的特征點
if( nkeypoints == 0 )
//通過呼叫cv::mat類的.realse方法,強制清空矩陣的參考計數,這樣就可以強制釋放矩陣的資料了
//參考[https://blog.csdn.net/giantchen547792075/article/details/9107877]
_descriptors.release();
else
{
//如果影像金字塔中有特征點,那么就創建這個存盤描述子的矩陣,注意這個矩陣是存盤整個影像金字塔中特征點的描述子的
_descriptors.create(nkeypoints, //矩陣的行數,對應為特征點的總個數
32, //矩陣的列數,對應為使用32*8=256位描述子
CV_8U); //矩陣元素的格式
//獲取這個描述子的矩陣資訊
// ?為什么不是直接在引數_descriptors上對矩陣內容進行修改,而是重新新建了一個變數,復制矩陣后,在這個新建變數的基礎上進行修改?
descriptors = _descriptors.getMat();
}
//清空用作回傳特征點提取結果的vector容器
_keypoints.clear();
//并預分配正確大小的空間
_keypoints.reserve(nkeypoints);
//因為遍歷是一層一層進行的,但是描述子那個矩陣是存盤整個影像金字塔中特征點的描述子,所以在這里設定了Offset變數來保存“尋址”時的偏移量,
//輔助進行在總描述子mat中的定位
int offset = 0;
//開始遍歷每一層影像
for (int level = 0; level < nlevels; ++level)
{
//獲取在allKeypoints中當前層特征點容器的句柄
vector<KeyPoint>& keypoints = allKeypoints[level];
//本層的特征點數
int nkeypointsLevel = (int)keypoints.size();
//如果特征點數目為0,跳出本次回圈,繼續下一層金字塔
if(nkeypointsLevel==0)
continue;
// preprocess the resized image
// Step 5 對影像進行高斯模糊
// 深拷貝當前金字塔所在層級的影像
Mat workingMat = mvImagePyramid[level].clone();
// 注意:提取特征點的時候,使用的是清晰的原影像;這里計算描述子的時候,為了避免影像噪聲的影響,使用了高斯模糊
GaussianBlur(workingMat, //源影像
workingMat, //輸出影像
Size(7, 7), //高斯濾波器kernel大小,必須為正的奇數
2, //高斯濾波在x方向的標準差
2, //高斯濾波在y方向的標準差
BORDER_REFLECT_101);//邊緣拓展點插值型別
// Compute the descriptors 計算描述子
// desc存盤當前圖層的描述子
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
// Step 6 計算高斯模糊后影像的描述子
computeDescriptors(workingMat, //高斯模糊之后的圖層影像
keypoints, //當前圖層中的特征點集合
desc, //存盤計算之后的描述子
pattern); //隨機采樣模板
// 更新偏移量的值
offset += nkeypointsLevel;
// Scale keypoint coordinates
// Step 6 對非第0層影像中的特征點的坐標恢復到第0層影像(原影像)的坐標系下
// ? 得到所有層特征點在第0層里的坐標放到_keypoints里面
// 對于第0層的影像特征點,他們的坐標就不需要再進行恢復了
if (level != 0)
{
// 獲取當前圖層上的縮放系數
float scale = mvScaleFactor[level];
// 遍歷本層所有的特征點
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
// 特征點本身直接乘縮放倍數就可以了
keypoint->pt *= scale;
}
// And add the keypoints to the output
// 將keypoints中內容插入到_keypoints 的末尾
// keypoint其實是對allkeypoints中每層影像中特征點的參考,這樣allkeypoints中的所有特征點在這里被轉存到輸出的_keypoints
_keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
}
參考文獻:
ORB_SLAM2原始碼決議
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296582.html
標籤:其他
下一篇:鏈表常見面試題( 建議收藏!!)
