ORBSLAM的ORB特徵提取

ORBSLAM中的主要使用了ORB特徵,也就是FAST特徵+BRIEF描述子的組合,具體這兩種方法就不詳細介紹了,這裏主要說一下每個特徵對應的描述子在ORBSLAM中的維護方式;

首先需要說明的是每個frame都有自己對應的找到的feature,在進行特徵提取前會先初始化一個Extractor,也就是:

void Frame::ExtractORB(int flag, const cv::Mat &im)
{
    if(flag==0)
        (*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
    else
        (*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

第二步:初始化完成後就是提取,這裏的入口函數是:
 

// 計算ORB特徵,_keypoints中的座標與scale已經無關
/* _image: 原圖
 * mvImagePyramid:ComputePyramid() 的結果,不同大小的圖片
 * allKeypoints:對應mvImagePyramid中每一層圖像的特徵點,是與金字塔圖像座標對應的,就是在原圖像的基礎上經過縮放的
 * _keypoints:對allKeypoints中每一個點找到對應的描述子後,再進行scale,
 *             作爲後面mvkeys;
 */
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,OutputArray _descriptors)

_keypoints 和_descriptors也就是我們最後需要的變量;即將一張圖片用一個vector的特徵點以及這些特徵點對應的描述子組成的Mat 來表示;具體由是如何推算的,可以用下面的步驟表示:
 

1.  構建高斯金字塔:

ComputePyramid(image);

 第一層爲原圖像,往上依次遞減,先用高斯函數進行模糊,再縮放,   將圖像保存至mvImagePyramid​[]中

   因此這個函數主要輸出的變量就是 std::vector<cv::Mat> mvImagePyramid;

2. 計算每層圖像的興趣點

void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)

這個函數主要輸出的是vector < vector<KeyPoint> > allKeypoints; 最外層的vector對應金字塔的每張圖片,內層vector對應當前圖像中的所有特徵點;因此這個變量中存儲的特徵點的座標是與金字塔圖像對應的;

主要步驟爲:

1. 對金字塔圖像進行遍歷,將圖像分割成nCols × nRows 個30*30的小塊cell
   for (int level = 0; level < nlevels; ++level)
   const int nCols = width/W;
   const int nRows = height/W; // W=30 
2. 對每個小塊進行FAST興趣點能提取,並將提取到的特徵點保存在這個cell對應的vKeysCell中
   vector<cv::KeyPoint> vKeysCell;
   FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
                     vKeysCell,iniThFAST,true);
3. 將各個小塊對應的vKeysCell進行整合,放入到vector<cv::KeyPoint> vToDistributeKeys 中;
4. 對vToDistributeKeys​中的特徵點進行四叉樹節點分配D;
   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)

(主要思路是該區域作爲均分爲n大塊,然後作爲n個根節點,遍歷每一個節點,如果該節點有多於一個的特徵點keypoints,則將該節點所處的區域均勻劃分成四份,依次類推,直到每個子節點區域只含有一個keypoints,如果該節點區域沒有kp則會被erase掉)

這裏詳細說一下具體的Keypoint計算過程,也就是特徵點是怎麼由座標變成一個關鍵點類的;

特徵點KeyPoint的構造過程:

KeyPoint的構造是首先在FAST中完成的:

FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),vKeysCell,iniThFAST,true);
其中mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX) 是當前金字塔層的某個圖像塊;
vKeysCell  用於保存提取的特徵點 vector<cv::KeyPoint>
iniThFAST  提取特徵時用到的閾值;
true爲是否使用極大值抑制;

而具體的FAST特徵提取時使用的是9_16的fast特徵: 

FASTX(_img, keypoints, threshold, nonmax_suppression, FastFeatureDetector::TYPE_9_16);

具體可參考opencv源碼中/modules/features2d/src/fast.cpp文件;

在經過一系列的判斷,確定當前點爲特徵點後,對當前座標進行了KeyPoint類構造:

keypoints.push_back(KeyPoint((float)j, (float)(i-1), 7.f, -1, (float)score));   

構造時,Keypoint的角度初始值,也就是倒數第二個變量設置爲-1;詳見OpenCV對KeyPOint類的構造:

CV_WRAP KeyPoint() : pt(0,0), size(0), angle(-1), response(0), octave(0), class_id(-1) {}

經過ComputeKeyPointsOctTree​()中的FAST特徵提取後,所有特徵點的xy座標,octave都已經確定,接下來就是計算每一個特徵點對應的角度,也就是方向值:

for (int level = 0; level < nlevels; ++level)
        computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
//計算當前keypoint的角度
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)
    {
        keypoint->angle = IC_Angle(image, keypoint->pt, umax);
    }
}

參考: 

How to distribute keypoints on every level image: https://zhuanlan.zhihu.com/p/61738607

https://blog.csdn.net/yang843061497/article/details/38553765

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章