ORB-SLAM2源码逐行解析系列(四):ORB-SLAM2中的ORB特征点提取

本文深入剖析ORB-SLAM2的ORB特征点提取过程,包括构建图像金字塔、计算特征点及均匀分布、计算描述子等步骤。详细解释了ComputePyramid、ComputeKeyPointsOctTree(DistributeOctTree、computeOrientation)和computeDescriptors的实现逻辑,阐述了特征点检测、均匀分布优化和方向计算的原理与加速技巧。
摘要由CSDN通过智能技术生成

1. ExtractORB

代码实现

/**
 * @brief 提取图像的ORB特征点,提取的关键点存放在mvKeys,描述子存放在mDescriptors
 * 
 * @param[in] flag          标记是左图还是右图。0:左图  1:右图
 * @param[in] im            等待提取特征点的图像
 */
void Frame::ExtractORB(int flag, const cv::Mat &im)
{
   
    // 判断是左图还是右图
    if(flag==0)
        // 左图的话就套使用左图指定的特征点提取器,并将提取结果保存到对应的变量中 
        // 这里使用了仿函数来完成,重载了括号运算符 ORBextractor::operator() 
        // 仿函数又称为函数对象,是一个能行使函数功能的类,必须重载operator()运算符
        // 仿函数可以拥有自己的数据成员和成员变量,通常比一般函数速度更快
        (*mpORBextractorLeft)(im,				// 待提取特征点的图像
							  cv::Mat(),		// 掩摸图像, 实际没有用到
							  mvKeys,			// 输出变量,用于保存提取后的特征点
							  mDescriptors);	// 输出变量,用于保存特征点的描述子
    else
        // 右图的话就需要使用右图指定的特征点提取器,并将提取结果保存到对应的变量中 
        // 双目模式下,会分别使用mpORBextractorLeft和mpORBextractorRight提取ORB特征点
        (*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

仿函数实现

(1)大致流程

  • 检查输入图像的有效性
  • ComputePyramid:利用输入图像,构建图像金字塔
  • ComputeKeyPointsOctTree:计算图像金字塔中的特征点,并进行均匀分配
  • GaussianBlur:对图像金字塔中的每层图像进行高斯模糊操作
  • computeDescriptors:对高斯模糊后的每层图像计算描述子
  • 将图像金字塔中的特征点恢复到第0层,并保存到_keypoints中

(2)代码实现

// /src/ORBextractor.cc
/**
 * @brief 用仿函数(重载括号运算符)方法来计算图像特征点
 * 
 * @param[in] _image                    输入原始图的图像
 * @param[in] _mask                     掩膜mask
 * @param[in & out] _keypoints          存储特征点关键点的向量  输出
 * @param[in & out] _descriptors        存储特征点描述子的矩阵  输出
 */
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 );  // 判断图像的格式是否正确,要求是单通道灰度值

    // Step 2 构建图像金字塔
    ComputePyramid(image);

    // Step 3 计算图像的特征点,并且将特征点进行均匀化, 均匀的特征点可以提高位姿计算精度
    vector<vector<KeyPoint> > allKeypoints;  // 保存提取的特征点
    // 使用四叉树的方式计算每层图像的特征点并进行分配
    ComputeKeyPointsOctTree(allKeypoints);   // 特征点均匀化分配
    
    // Step 4 开辟特征点的描述子空间,并转为cv::Mat的格式descriptors
    Mat descriptors;  // 存储整个图像金字塔中所有特征点对应的描述子
    int nkeypoints = 0;  // 获取特征点个数
    for (int level = 0; level < nlevels; ++level)
        nkeypoints += (int)allKeypoints[level].size();
    if( nkeypoints == 0 )
        // 若特征点个数为0,则清空描述子
        _descriptors.release();
    else
    {
   
		// 若存在特征点,则开辟对应的描述子空间_descriptors
        _descriptors.create(nkeypoints,		// 矩阵的行数,对应为特征点的总个数
							32, 			// 矩阵的列数,对应为使用32*8=256位描述子
							CV_8U);			// 矩阵元素的格式
		// 将描述子空间转成cv::Mat的形式
        descriptors = _descriptors.getMat();
    }
    // 清空用作返回特征点提取结果的vector容器
    _keypoints.clear();
	// 并预分配正确大小的空间
    _keypoints.reserve(nkeypoints);
    
	// 寻址的偏移量,用于在总描述子mat中进行辅助定位
    int offset = 0;
	// 开始遍历每一层图像
    for (int level = 0; level < nlevels; ++level)
    {
   
        // 获取当前层的特征点
        vector<KeyPoint>& keypoints = allKeypoints[level];
		// 获取当前层的特征点个数
        int nkeypointsLevel = (int)keypoints.size();
		// 如果特征点数目为0,跳出本次循环,继续下一层金字塔
        if(nkeypointsLevel==0)
            continue;
        
        //  Step 5 为了避免图像噪声的影响, 对图像进行高斯模糊
        Mat workingMat = mvImagePyramid[level].clone();  // 深拷贝(.clone)当前层图像
        GaussianBlur(workingMat, 		   // 源图像
					 workingMat, 		   // 输出图像
					 Size(7, 7), 		   // 高斯滤波器kernel大小,必须为正的奇数
					 2, 				   // 高斯滤波在x方向的标准差
					 2, 				   // 高斯滤波在y方向的标准差
					 BORDER_REFLECT_101);  // 边缘拓展点插值类型
        
        Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
		// Step 6 计算高斯模糊后当前层图像的描述子
        computeDescriptors(workingMat, 	// 高斯模糊之后的当前层图像
						   keypoints, 	// 当前层图像中的特征点
						   desc, 		// 存储计算出来的当前层图像特征点的描述子
						   pattern);	// 计算描述子的模板

		// 更新偏移量的值,将上面计算出来的当前层描述子desc存储到descriptors的当前层相应位置处
        offset += nkeypointsLevel;
        
        // Step 7 将图像金字塔中的特征点恢复到第0层图像(原图像)上
        if (level != 0)
        {
   
			// 获取当前图层上的缩放系数
            float scale = mvScaleFactor[level];
            // 遍历本层所有的特征点
            for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
                 keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
				// 特征点本身直接乘缩放倍数就可以了
                keypoint->pt *= scale;
        }
        // 获取图像金字塔中恢复到第0层图像上的所有特征点
        _keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
    }
}                

1.1 构建图像金字塔ComputePyramid

(1)实现逻辑

​ 大致过程:将原始图像作为第0层,根据每层的缩放系数scale逐层缩放,获取每层的图像,以此构成图像金字塔。另外,在进行特征提取之前需要执行高斯滤波操作(目的是去除噪点),由于图像边界处无法进行高斯滤波,因此需要对图像金字塔的每层进行边界扩充,但在原始代码中并没有应用到边界扩充(应该是个bug)。

​ 在对图像金字塔的每层图像进行特征提取时,由于无法对边界部分进行特征提取操作,因此图像边界需要外扩3个像素以提取FAST角点(FAST-16需要半径为3个像素的圆)。于是,整体的图像描述如下:

最中间的灰色区域为图像的像素部分——称为有效像素图

附加绿色部分的minBorder边界的图像用于提取FAST角点——称为半径扩充图像

外扩EDGE_THRESHOLD的整个图像用于高斯模糊
图像金字塔边界管理
(2)代码实现

/**
 * 构建图像金字塔
 * @param image 输入原图像,这个输入图像所有像素都是有效的,也就是说都是可以在其上提取出FAST角点的
 */
void ORBextractor::ComputePyramid(cv::Mat image)
{
   
    // 遍历所有的图层
    for (int level = 0; level < nlevels; ++level)
    {
   
        // 获取本层图像的缩放系数
        float scale = mvInvScaleFactor[level];
		// 计算本层图像的像素尺寸大小
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
		// 全尺寸图像: 包括无效图像区域的大小。将图像进行“补边”,主要是为了进行高斯模糊
        // EDGE_THRESHOLD区域外的图像不进行FAST角点检测
        Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
		// 定义了两个变量:temp是扩展了边界的图像,masktemp并未使用
        Mat temp(wholeSize, image.type()), masktemp;
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, 
                                          sz.width, sz.height));
        
        // 对于非第0层图像,先根据缩放系数进行resize,然后使用copyMakeBorder逐层填充当前层图形
        if( level != 0 )
        {
   
            resize(mvImagePyramid[level-1],	 // 输入图像
				   mvImagePyramid[level], 	 // 输出图像
				   sz, 						 // 输出图像的尺寸
				   0, 						 // 水平方向上的缩放系数,留0表示自动计算
				   0,  						 // 垂直方向上的缩放系数,留0表示自动计算
				   cv::INTER_LINEAR);		 // 图像缩放的差值算法类型,这里的是线性插值算法
            
            copyMakeBorder(mvImagePyramid[level], 		   // 源图像
						   temp, 						   // 目标图像
						   EDGE_THRESHOLD,                 // top扩充的border大小
                           EDGE_THRESHOLD,                 // bottom扩展的border大小
						   EDGE_THRESHOLD,                 // left扩充的border大小
                           EDGE_THRESHOLD,		           // right扩展的border大小
                           BORDER_REFLECT_101+BORDER_ISOLATED);  // 扩充方式
        }
        else
        {
   
			// 对于第0层未缩放图像,直接将图像深拷贝到temp的中间
            // 并且对其周围进行边界扩展。此时temp就是对原图扩展后的图像
            copyMakeBorder(image,			//这里是原图像
						   temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, 
                           EDGE_THRESHOLD, BORDER_REFLECT_101);            
        }
        // ! 原代码mvImagePyramid 并未扩充,应该添加下面一行代码
        // mvImagePyramid[level] = temp;
    }
}

1.2 ComputeKeyPointsOctTree

(1)大致流程

  • 遍历图像金字塔的每一层,对于遍历到的每一层,执行如下操作:
    • 确定半径扩充的有效图像边界[minBorderX, maxBorderX, minBorderY, maxBorderY]
    • 在半径扩充的有效图像上划分网格cell
    • 以网格cell为单位,先行后列进行遍历。对于遍历到的网格cell,使用opencv提取FAST特征点(为防止提取不到,可降低阈值再次提取)
    • 将提取到的FAST特征点坐标恢复到基于半径扩充的有效图像上
    • DistributeOctTree:将提取到的FAST特征点使用四叉树实现特征点均匀化分布,并剔除冗余密集特征点
    • 将保留下来的基于半径扩充的有效图像区域的特征点恢复到基于EDGE_THRESHOLD外扩区域上
  • computeOrientation:计算所有层上特征点的主方向

(2)代码实现

void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)	
{
   
    allKeypoints.resize(nlevels);  // 重新调整图像层数
    const float W = 30;  // 划分的网格尺寸
    // 遍历所有层
    for (int level = 0; level < nlevels; ++level)
    {
   
        // 确定图形的有效边界,包括外扩3个像素便于提取FAST特征点
        //[minBorderX, minBorderY, maxBorderX, maxBorderY]为前面图像中的绿色有效区域
        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;
		// 一般地都是过量采集,所以这里预分配的空间大小是nfeatures*10
        vToDistributeKeys.reserve(nfeatures*10);

		// 计算进行特征点提取的图像区域尺寸(前面图像中的灰色区域尺寸)
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);

        const int nCols = width/W;   // 当前层的图像在列上的网格cell个数
        const int nRows = height/W;  // 当前层的图像在行上的网格cell个数

        const int wCell = ceil(width/nCols);   // 当前层的每个图像网格cell所占的像素列数
        const int hCell = ceil(height/nRows);  // 当前层的每个图像网格cell所占的像素行数
        
        // 以网格cell为单位,进行行遍历
        for(int i=0; i<nRows; i++)
        {
   
			// 计算当前网格初始行坐标(y)
            const float iniY =minBorderY+i*hCell;
/*
   当i = nRows时,遍历到最后一行,考虑到提取FAST特征点的外扩6个像素,理论最大Y边界为:minBorderY+nRows*hCell+6, 需要与实际物理最大Y边界maxBorderY进行比较,以确定有效区域
*/
            float maxY = iniY+hCell+6;
            // 如果初始的行坐标就已经超出有效的图像区域(绿色范围), 则跳过这一行
            if(iniY>=maxBorderY-3)
                continue;
  // 如果图像的大小导致最后一行网格不能完整划分,导致Y边界超过有效区域,则最后一行网格的最大行坐标为有效区域Y边界maxBorderY
            if(maxY>maxBorderY)
                maxY = maxBorderY;
            // 以网格cell为单位,进行列遍历
            for(int j=0; j<nCols; j++)
            {
   
                // 计算初始的列坐标(x)
                const float iniX =minBorderX+j*wCell;
				// 计算这列网格的最大列坐标,+6的含义和前面相同
                float maxX = iniX+wCell+6;
                // 同前面一样,判断初始列是否超过有效图像区域    !BUG  正确应该是maxBorderX-3
                if(iniX>=maxBorderX-6)
                    continue;
                // 同前面一样,最后一列网格的边界判断
                if(maxX>maxBorderX)
                    maxX = maxBorderX;
                
                vector<cv::KeyPoint> vKeysCell;  // 存储提取出来的FAST特征点
				// 调用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);			
                }
                
/*
  由于检测到的FAST特征点是基于对应网格cell的,即是在[iniX, iniY, maxX, maxY]区域上检测到的
  需要将其坐标恢复到图像有效区域上,即[minBorderX, maxBorderX, minBorderY, maxBorderY]上
*/
                if(!vKeysCell.empty())
                {
   
                    // 遍历其中的所有FAST角点
                    for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); 
                        vit!=vKeysCell.end();vit++)
                    {
   
                        // 当i,j均为0时,iniX = minBorderX, iniY = minBorderY
                        // 故是将特征点坐标恢复到基于有效图像区域上
						(*vit).pt.x+=j*wCell;
                        (*vit).pt.y+=i*hCell;
                        // 将提取到的特征点存储到vToDistributeKeys中
                        vToDistributeKeys.push_back(*vit);
                    }
                }
            }
        }

/*
  声明一个对当前图层的特征点的容器的引用, 并且调整其大小为欲提取出来的特征点个数(当然这里是扩大了的,因为不可能所有的特征点都是在这一个图层中提取出来的)
*/
        vector<KeyPoint> & keypoints = allKeypoints[level]; 
        keypoints.reserve(nfeatures);
        // 对存储到vToDistributeKeys中的当前层特征点使用四叉树实现特征点均匀化分别
        keypoints = DistributeOctTree(vToDistributeKeys, 		  // 待分配的特征点
									  minBorderX, maxBorderX,	  // 特征点区域
                                      minBorderY, maxBorderY,       
									  mnFeaturesPerLevel[level],  // 保留的当前层特征点个数
									  level);                     // 当前层
        // PATCH_SIZE是对于底层的初始图像来说的
        // 现在要根据当前图层的尺度缩放倍数进行缩放, 得到缩放后的PATCH大小 和特征点的方向计算有关
        const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];
		// 获取剔除过程后保留下来的特征点数目
        const int nkps = keypoints.size();
        // 将基于半径扩充的有效图像区域的特征点 转换到 基于EDGE_THRESHOLD外扩区域上
        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;  // 每层计算灰度质心时的半径
        }
    }
    
    // 计算每层图像上特征点的主方向
    for (int level = 0; level < nlevels; ++level)
        computeOrientation(mvImagePyramid[level],	// 对应图层的图像
						   allKeypoints[level], 	// 这个图层中提取并保留下来的特征点
						   umax);					// 在构造ORBextractor时提前计算出的
}

1.2.1 DistributeOctTree

(1)大致流程

  • 根据图像宽高比取整来确定初始的节点数目:假设图像的宽度 w w w大于高度 h h
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值