讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解的(01)ORB-SLAM2源码无死角解析链接如下(本文内容来自计算机视觉life ORB-SLAM2 课程课件):
(01)ORB-SLAM2源码无死角解析-(00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/123092196
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX→官方认证
上一篇博客,我们对 ORBextractor::operator() 中调用的ComputePyramid(image)函数进行了讲解,那么接下来我们就是要分析紧跟在其后面的 ComputeKeyPointsOctTree(allKeypoints) 函数进行讲解。其代码实现位于src/ORBextractor.cc文件中。在讲解代码之前,我们先来具体的了解一下ORC特征,前面的博客我们提及到ORB特征主要由 Oriented FAST 关键点与 Rotated BRIEF 描述子两个部分组成。
我们先来了解一下什么四FAST关键。判断一个像素点是否为FAST关键点,主要流程如下:
1、选取像素p,假设它的亮度为Ip;
2、设置一个阈值T(比如Ip的20%);
3、以像素p为中心,选取半径为3的圆上的16个像素点;
4、假如选取的圆上,有连续的N个点的亮度大于Ip+T或小于Ip-T,那么像素p可以被认为是关键点;
通常取12,即为FAST-12。其他常用的N取值为9和11,它们分别被称为FAST-9,FAST-11)。
5、循环以上4步,对每一个像素执行相同操作。
简单的图示如下:
FAST是一种角点,主要检测局部像素灰度变化明显的地方,以速度快著称。它的思想是:如果一个像素与它邻域的像素差别较大(过亮或过暗),那它更可能是角点。
在FAST-12算法中,为了更高效,可以添加一项预测试操作,以快速地排除绝大多数不是角点的像素。具体操作为,对于每个像素,直接检测邻域圆上的第1,5,9,13个像素的亮度。只有当这四个像素中有三个同时大于Ip+T或小于Ip-T时,当前像素才有可能是一个角点,否则应该直接排除。这样的预测试操作大大加速了角点检测。此外,原始的FAST角点经常出现“扎堆”的现象。所以在第一遍检测之后,还需要用非极大值抑制(Non-maximal suppression),在一定区域内仅保留响应极大值的角点, 避免角点集中的问题。
FAST特征点的计算仅仅是比较像素间亮度的差异,速度非常快,但它也有一些问题如下:
F A S T 特征点数量很大且不确定 → \color{red}{FAST特征点数量很大且不确定→} FAST特征点数量很大且不确定→FAST特征点数量很大且不确定,而我们往往希望对图像提取固定数量的特征。因此,在ORB中,对原始的FAST算法进行了改进。 我们可以指定最终要提取的角点数量N,对原始FAST角点分别计算Harris响应值,然后选取前N NN个具有最大响应值的角点,作为最终的角点集合。
F A S T 角点不具有方向信息 → \color{red}{FAST角点不具有方向信息→} FAST角点不具有方向信息→由于它固定取半径为3的圆,存在尺度问题 :远处看着像是角点的地方,接近后看可能就不是角点了。针对FAST角点不具有方向性和尺度的弱点,ORB添加了尺度和旋转的描述。尺度不变性由构建图像金字塔,并在金字塔的每一层上检测角点来实现。而特征的旋转是由灰度质心法(Intensity Centroid)实现的
金字塔我们已经在前面进行了讲解,对于灰度质心法(Intensity Centroid)稍后会进行讲解
//用于存储所有的关键点信息
allKeypoints.resize(nlevels);
//循环对图像金字塔进行处理
for (int level = 0; level < nlevels; ++level)
1、把图像分割成栅格,栅格为正方形,边长像素为W=30
2、循环遍历每个网格
for(int i=0; i<nRows; i++)
for(int j=0; j<nCols; j++)
3、设定初始对单个栅格进行FAST关键点提取
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),vKeysCell,iniThFAST,true);
4、如果没有检测到任何关键点,则降低阈值再进行检测
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),vKeysCell,minThFAST,true);
其上大家要注意一个点,就是 为什么要划分成栅格的形式进行关键点提取 \color{red}{为什么要划分成栅格的形式进行关键点提取} 为什么要划分成栅格的形式进行关键点提取,而不是直接送入整张图像进行检测。因为一张图像的各个区域的可区分度是一样的,有的区域像素值差距比较大,直接使用 iniThFAST 作为检测阈值即可,但是有的区域的像素值差距比较小,这时可区分度是比较小的,所以需要设定比较小的阈值。该是一个比较细节的地方,大家需要多思考一下。
代码实现位于src/ORBextractor.cc文件中,该函数没有全部粘贴,只给出了关于FAST的相关部分
//计算四叉树的特征点,函数名字后面的OctTree只是说明了在过滤和分配特征点时所使用的方式
void ORBextractor::ComputeKeyPointsOctTree(
vector<vector<KeyPoint> >& allKeypoints) //所有的特征点,这里第一层vector存储的是某图层里面的所有特征点,
//第二层存储的是整个图像金字塔中的所有图层里面的所有特征点
{
//重新调整图像层数
allKeypoints.resize(nlevels);
//图像cell的尺寸,是个正方形,可以理解为边长in像素坐标
const float W = 30;
// 对每一层图像做处理
//遍历所有图像
for (int level = 0; level < nlevels; ++level)
{
#ifdef _DEBUG
// UNDONE:
ostringstream buffer;
buffer << "mvImagePyramid_" << level << ".jpg";
string imageFile = buffer.str();
string imagePath = "result_images/" + imageFile;
//cv::imshow(imageFile, mvImagePyramid[level]);
system("mkdir -p result_images");
cv::imwrite(imagePath, mvImagePyramid[level]);
//cv::waitKey();
#endif
//计算这层图像的坐标边界, 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提取兴趣点, 自适应阈值
//这个向量存储这个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); //使能非极大值抑制
}
//当图像cell中检测到FAST角点的时候执行下面的语句
if(!vKeysCell.empty())
{
//遍历其中的所有FAST角点
for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
{
//NOTICE 到目前为止,这些角点的坐标都是基于图像cell的,现在我们要先将其恢复到当前的【坐标边界】下的坐标
//这样做是因为在下面使用八叉树法整理特征点的时候将会使用得到这个坐标
//在后面将会被继续转换成为在当前图层的扩充图像坐标系下的坐标
(*vit).pt.x+=j*wCell;
(*vit).pt.y+=i*hCell;
//然后将其加入到”等待被分配“的特征点容器中
vToDistributeKeys.push_back(*vit);
}//遍历图像cell中的所有的提取出来的FAST角点,并且恢复其在整个金字塔当前层图像下的坐标
}//当图像cell中检测到FAST角点的时候执行下面的语句
}//开始遍历图像cell的列
}//开始遍历图像cell的行
//声明一个对当前图层的特征点的容器的引用
vector<KeyPoint> & keypoints = allKeypoints[level];
//并且调整其大小为欲提取出来的特征点个数(当然这里也是扩大了的,因为不可能所有的特征点都是在这一个图层中提取出来的)
keypoints.reserve(nfeatures);
// 根据mnFeatuvector & keypoints = allKeypoints[level];resPerLevel,即该层的兴趣点数,对特征点进行剔除
//返回值是一个保存有特征点的vector容器,含有剔除后的保留下来的特征点
//得到的特征点的坐标,依旧是在当前图层下来讲的
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的横坐标边界
}
在代码中还有一个比较细节的问题,我们可以看到其进行了两次 FAST关键点提取,第一次使用 iniThFAST 为检测阈值,第二次使用 minThFAST 为检测阈值。他的用意也是十分简单的,单使用 iniThFAST 为检测阈值的时候,如果没有检测到任何关键点,即 vKeysCell.empty() 成立,说明当前图像的区分度比较小,我们需要降低 iniThFAST,使用minThFAST 为阈值来进行检测。
另外我们可以看到代码中的 mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX) 的操作,该操作就是先类似于图片剪切的效果。本人保存了一些截图之后的图像如下:
可以很明显的知道其是下图左上角的一部分:
上面是关于 FAST关键点提取的讲解,但是这里存在一个问题,就是一张图像提取出来的特征是分布不均匀的,有的可能特别密集,有的可能很稀疏。比如一共需要提取300个关键点,如果很多关键点都集中在一个区域,其他区域关键点很少。那么我们这300个关键点,肯定大部分都来自于密集区域吗,那么这种时候我们应该如何处理呢?
其上有个地方,我们可以看到,提取出来的关键点都存储在变量 vToDistributeKeys 变量之中,从命令可以看到这些关键点还需要进行重新分配(均匀分配)。也就是随后调用的 DistributeOctTree() 函数,在下一篇博客我们会对齐进行详细的讲解。
本文内容来自计算机视觉life ORB-SLAM2 课程课件