主頁 >  其他 > ORB_SLAM2 原始碼決議 ORB特征提取(二)

ORB_SLAM2 原始碼決議 ORB特征提取(二)

2021-09-01 14:30:45 其他

目錄

一、各成員函式變數

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

標籤:其他

上一篇:【資料結構】 佇列的簡單理解和基本操作

下一篇:鏈表常見面試題( 建議收藏!!)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more