特征点提取器的构造函数
ORBextractor::ORBextractor(int _nfeatures,float _scaleFactor,int _nlevels,int _iniThFAST,int _minThFAST )
- 初始化下面参数:
int nFeatures = 1000; //特征点数
float fScaleFactor = 1.2; //高斯金字塔的放大系数
int nLevels = 8; //高斯金字塔的层数
int fIniThFAST = 40; //FAST特征的第一个参数 - 然后逐层计算图像金字塔中图像相当于初始图像的缩放系数
- 再开始逐层计算要分配的特征点个数,顶层图像除外(看循环后面)会导致剩余一些特征点个数没有被分配,所以这里就将这个余出来的特征点分配到最高的图层中
//开始逐层计算要分配的特征点个数,顶层图像除外(看循环后面) for( int level = 0; level < nlevels-1; level++ ) { //分配 cvRound : 返回个参数最接近的整数值 mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale); //累计 sumFeatures += mnFeaturesPerLevel[level]; //乘系数 nDesiredFeaturesPerScale *= factor; } //由于前面的特征点个数取整操作,可能会导致剩余一些特征点个数没有被分配,所以这里就将这个余出来的特征点分配到最高的图层中 mnFeaturesPerLevel[nlevels-1] = std::max(nfeatures - sumFeatures, 0);
- 下来要考虑的是计算特征点的方向。本项目中采用的是在一个圆形区域中计算,为了能够快速计算该区域,采用的是首先生成蒙版的方法。所以首先初始化的过程中就将该圆形区域的相对坐标存储在umax数组中。至此,初始化过程结束。
//利用圆的方程计算每行像素的u坐标边界(max) for (v = 0; v <= vmax; ++v) umax[v] = cvRound(sqrt(hp2 - v * v)); //结果都是大于0的结果,表示x坐标在这一行的边界 // Make sure we are symmetric //这里其实是使用了对称的方式计算上八分之一的圆周上的umax,目的也是为了保持严格的对称(如果按照常规的想法做,由于cvRound就会很容易出现不对称的情况, //同时这些随机采样的特征点集也不能够满足旋转之后的采样不变性了) for (v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v) { while (umax[v0] == umax[v0 + 1]) ++v0; umax[v] = v0; ++v0; }
构造图像金字塔
void ORBextractor::ComputePyramid(cv::Mat image)
直接通过图像的缩放就可以达到目的。主要使用的函数就是如下所示的resize函数
resize(mvImagePyramid[level - 1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
特征点提取与分配
为了使接下来计算PnP时能够更加准确,需要保持的原则就是尽量使得特征点能够均匀分布在整个图像中,所以需要将图像分成更小的部分分别提取特征点,并用某种机制来分配。在本项目中,作者提供了两种分配方案分别是八叉树的方法和传统的方法。考虑到在室内场景中并不需要提供太多的特征点,而在1000个特征点以下时,传统的方法表现要由于建造八叉树的方法。所以在实际应用中,都是采用传统的方法,而不去花费大量收时间用于建造八叉树。
传统方法:void ORBextractor::ComputeKeyPointsOld(
std::vector<std::vector<KeyPoint> > &allKeypoints) //输出,所有图层上的所有特征点
传统方法的第一步就是计算需要将图像分割成多少个元包(cell),对于每个元包分别提取特征点。元包的计算方法为,根据需要提取的特征点数目,假设每个元包中需要提取5个特征点,以此来进行计算需要的cell数目。
接着对上面计算好的元包分别进行特征点的提取。
//调用opencv的库函数来检测FAST角点
FAST(cellImage, //cell中的图像
cellKeyPoints[i][j], //用于保存提取出来的特征点的vector容器
iniThFAST, //初步的FAST检测阈值
true); //使能非极大值抑制
这里注意,由于FAST特征在计算角点时向内缩进了3个像素才开始计算,所以在使用FAST之前还需要对图像加一条宽度为3像素的边。然后就要用到之前初始化的两个阈值参数,首先使用阈值较大的参数作为FAST特征点检测的阈值,如果提取到的特征点数目足够多,那么直接计算下一个元包即可,否则就要使用较小的参数重新提取。在本项目中,特征点数目的阈值设定为3.
//如果当前cell中提取出来的特征点的个数小于3
if(cellKeyPoints[i][j].size()<=3)
{
//那么首先清空刚才提取出来的特征点
cellKeyPoints[i][j].clear();
//然后使用更小的参数阈值,进行重新提取
FAST(cellImage, //cell中的图像
cellKeyPoints[i][j], //输出变量,用于保存提取出来的特征点的vector
minThFAST, //较小的那个FAST阈值
true); //使能非极大值抑制
}
然后就涉及到了特征点的数目分配问题。由于图像中不可避免的存在纹理丰富和纹理较浅的区域,在纹理较丰富的区域,角点的数目可能提取很多,而在纹理不丰富的区域,角点的数目可能很少。而在分配各区域选取的特征点数目时,就要考虑前面提到的极可能均匀的问题。所以采用的方法是循环将特征点数目不足的元包中的剩余数目分配到其他所有元包中,知道最后取得足够数量的特征点。当然,如果最初提取的特征点数目就不足预期,那么直接全部选取即可。所以这种方法并不能保证最终得到的特征点数目一定能达到1000。
对于那些特征点数目特别多的元包,采用的是对各个角点的质量进行排序,选择最好的前n个特征点作为最终结果。
计算方向
为了使得提取的特征点具有旋转不变性,需要计算每个特征点的方向。方法是计算以特征点为中心以像素为权值的圆形区域上的重心,以中心和重心的连线作为该特征点的方向。具体计算过程在IC_Angle
static void computeOrientation(
const Mat& image, //特征点所在的图像(其实就是图像金字塔中每一层的图像)
vector<KeyPoint>& keypoints, //存储有特征点的vector
const vector<int>& umax) //以及每个特征点所在图像区块的每行的边界u_max组成的vector
{
//遍历完所有的特征点
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
{
//计算这个特征点的方向,其实就是简单的函数调用
keypoint->angle = IC_Angle(image, //特征点所在的图层的图像
keypoint->pt, //特征点在这张图像中的坐标subl
umax); //以及图像区块的边界
}//遍历完成所有的特征点
}
计算描述子
brief描述子由32*8位组成,其中每一位是来自于两个像素点灰度的直接比较,所以每比较出8bit结果,需要16个随机点,这也就是为什么pattern需要+=16的原因。
for (int i = 0; i < 32; ++i, pattern += 16)
{
int t0, //参与比较的一个特征点的灰度值
t1, //参与比较的另一个特征点的灰度值 //TODO 检查一下这里灰度值为int型???
val; //描述子这个字节的比较结果
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;
}//通过对随机点像素灰度的比较,得出BRIEF描述子,一共是32*8=256位