最近在尝试将ORB提取特征点机制放置于芯片上,便于后期调用,减小CPU的计算负担,故最近对ORB特征点提取代码进行了研读,记录一下,有不对的还请指教;
首先在Frame()初始化中,含有ExtractORB(0,imGray); 这次我们研究的全在这个特征点提取函数里面;
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);
}
这里用了()的重载,直接跳转 *mpORBextractorLeft ; 跳转至void ORBextractor::operator();
特征点的提取均在此函数实现;
if(_image.empty())
return;
Mat image = _image.getMat();
assert(image.type() == CV_8UC1 );
// Pre-compute the scale pyramid
// 构建图像金字塔
ComputePyramid(image);
读取图片,为空返回; 不然建立金字塔,过程不再阐述;
// 计算每层图像的兴趣点 //计算关键点并生成四叉树
vector < vector<KeyPoint> > allKeypoints; // vector>
ComputeKeyPointsOctTree(allKeypoints);
//ComputeKeyPointsOld(allKeypoints);
首先开辟一个vector < vector > allKeypoints 空间,存放提取的特征点,后面进入 ComputeKeyPointsOctTree, 提取特征点,并采用四叉树的方法对提取的特征点进行均匀化;
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
allKeypoints.resize(nlevels);
const float W = 30;
// 对每一层图像做处理
for (int level = 0; level < nlevels; ++level)
{
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;
vector<cv::KeyPoint> vToDistributeKeys;
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;
float maxY = iniY+hCell+6;
if(iniY>=maxBorderY-3)
continue;
if(maxY>maxBorderY)
maxY = maxBorderY;
for(int j=0; j<nCols; j++)
{
const float iniX =minBorderX+j*wCell;
float maxX = iniX+wCell+6;
if(iniX>=maxBorderX-6)
continue;
if(maxX>maxBorderX)
maxX = maxBorderX;
// FAST提取兴趣点, 自适应阈值
vector<cv::KeyPoint> vKeysCell;
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,iniThFAST,true);
if(vKeysCell.empty())
{
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,minThFAST,true);
}
if(!vKeysCell.empty())
{
for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
{
(*vit).pt.x+=j*wCell;
(*vit).pt.y+=i*hCell;
vToDistributeKeys.push_back(*vit);
}
}
}
}
vector<KeyPoint> & keypoints = allKeypoints[level];
keypoints.reserve(nfeatures);
// 根据mnFeaturesPerLevel,即该层的兴趣点数,对特征点进行剔除
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);
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;
keypoints[i].size = scaledPatchSize;
}
}
// compute orientations
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}
首先根据金字塔层数开辟空间; w=30;在提取特征点过程中,将图像分割成很多分,w=30,为分割的每个小格子的大小;后面进入第一个for循环,对每一层进行同样操作;
vToDistributeKeys 为当前层提取的特征点个数,为最终提取的10倍;
后面按照对应的策略,分成跟多小块进行提出处理;
进入两个for循环,开始对每个小块进行处理;
// FAST提取兴趣点, 自适应阈值
vector<cv::KeyPoint> vKeysCell;
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,iniThFAST,true);
提取FAST特征点,此时提取的数量很多,后续需要均匀化处理;
// 根据mnFeaturesPerLevel,即该层的兴趣点数,对特征点进行剔除
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);
DistributeOctTree 采用四叉树的方法对提取的特征点进行提取,从而提高特征点的精度以及控制数量;
四叉树的具体原理,我推荐看这个:https://zhuanlan.zhihu.com/p/61738607
在 DistributeOctTree 中 :
list lNodes; 建立了list 存储需要处理的节点;
如果image为640*480,则一开始 节点数nIni = 1 ;即 lNodes.size() = 1;
后面进入第一个 大 while 循环, 建立缓存区; 后面进入里面的第一个 while 循环;
// If more than one point, subdivide
ExtractorNode n1,n2,n3,n4;
lit->DivideNode(n1,n2,n3,n4); // 再细分成四个子区域
当拆分的节点数不满足要求时,采用 DivideNode 进行1分4的拆分,同时处理每个节点时的数据存在 vSizeAndPointerToNode 中;每当1分4后处理后,此节点删除;
if(n1.vKeys.size()>0)
{
lNodes.push_front(n1);
if(n1.vKeys.size()>1)
{
nToExpand++;
vSizeAndPointerToNode.push_back(make_pair(n1.vKeys.size(),&lNodes.front()));
lNodes.front().lit = lNodes.begin();
}
}
对于刚分的节点,
如果此节点中没有特征点,则删除此节点,后续不在关注;
如果此节点中有特征点,且则将节点至于 lNodes 最前端;
如果此节点中特征点个数>1,则nToExpand+1,后续可能对其继续进行分割;
在此while 中将 lNodes 的 节点全都处理一次,后面观察是否满足要求:
if((int)lNodes.size()>=N || (int)lNodes.size()==prevSize)
{
bFinish = true;
}
// 当再划分之后所有的Node数大于要求数目时
else if(((int)lNodes.size()+nToExpand*3)>N)
{}
如果不满足数目要求,则返回上面的while循环,将上一次分割的节点继续分割;直到满足 else if(((int)lNodes.size()+nToExpand*3)>N) , 则运行第二个while 循环;完成最后一轮的节点分割;
// Retain the best point in each node
// 保留每个区域响应值最大的一个兴趣点
vector<cv::KeyPoint> vResultKeys;
vResultKeys.reserve(nfeatures);
for(list<ExtractorNode>::iterator lit=lNodes.begin(); lit!=lNodes.end(); lit++)
{
vector<cv::KeyPoint> &vNodeKeys = lit->vKeys;
cv::KeyPoint* pKP = &vNodeKeys[0];
float maxResponse = pKP->response;
for(size_t k=1;k<vNodeKeys.size();k++)
{
if(vNodeKeys[k].response>maxResponse)
{
pKP = &vNodeKeys[k];
maxResponse = vNodeKeys[k].response;
}
}
vResultKeys.push_back(*pKP);
}
按照上述分配的节点,提取每个节点中得分较大的特征点;完成返回;
返回上层后计算对应的描述子和 计算方向等事宜,比较简单,不在阐述;重点在如何采用四叉树来对提取的特征点进行均匀化;
大概写了下,大家将就看吧 哈哈
ORB-SLAM中的ORB特征(提取) 均匀化 : https://zhuanlan.zhihu.com/p/61738607
ORB特征均匀提取策略对性能的提升有多大? : https://www.sohu.com/a/341157217_100007727