本文写于看完《视觉slam十四讲》,orb-slam2代码和论文后,以此来复盘过去几个月所学内容,有理解错误的地方欢迎大家指正批评!有问题也可以在评论区交流。
关键点和描述子的计算见ORB-SLAM2源码分析和学习笔记03:ORB特征提取(1)
特征点均匀化
通过计算关键点和描述子可以得到一张图像中的所有特征点,但这些特征点可能在图片纹理丰富的区域过于集中,而在纹理稀疏的地方很少,这就会造成特征点分布不均匀,从而影响后续的特征点跟踪,故会对这些特征点采用四叉树筛选使其分布均匀。
四叉树筛选
四叉树原理可以参考四叉树
具体步骤如下:
代码实现比较复杂,个人认为理解上图即可
/**
* @brief 八叉树筛选特征点
* @param vToDistributeKeys 等待进行筛选到特征点
* @param minX 当前图层的图像的边界,坐标都是在“半径扩充图像”坐标系下的坐标
* @param maxX
* @param minY
* @param maxY
* @param N 希望提取出的特征点个数
* @param level 指定的金字塔图层,并未使用
* @return 筛选过的特征点容器vector<cv::KeyPoint> vResultKeys;
*/
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)
{
// Compute how many initial nodes
const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));
const float hX = static_cast<float>(maxX-minX)/nIni;
list<ExtractorNode> lNodes;
vector<ExtractorNode*> vpIniNodes;
vpIniNodes.resize(nIni);
for(int i=0; i<nIni; i++)
{
ExtractorNode ni;
ni.UL = cv::Point2i(hX*static_cast<float>(i),0);
ni.UR = cv::Point2i(hX*static_cast<float>(i+1),0);
ni.BL = cv::Point2i(ni.UL.x,maxY-minY);
ni.BR = cv::Point2i(ni.UR.x,maxY-minY);
ni.vKeys.reserve(vToDistributeKeys.size());
lNodes.push_back(ni);
vpIniNodes[i] = &lNodes.back();
}
//Associate points to childs
for(size_t i=0;i<vToDistributeKeys.size();i++)
{
const cv::KeyPoint &kp = vToDistributeKeys[i];
vpIniNodes[kp.pt.x/hX]->vKeys.push_back(kp);
}
list<ExtractorNode>::iterator lit = lNodes.begin();
while(lit!=lNodes.end())
{
if(lit->vKeys.size()==1)
{
lit->bNoMore=true;
lit++;
}
else if(lit->vKeys.empty())
lit = lNodes.erase(lit);
else
lit++;
}
…………
…………
}
特征点提取
作者重载了()运算符,使得其他类可以将ORBextractor类型变量当作函数来使用,这个函数调用了ORBextractor.cc中的其他函数,实现了特征点提取。
/**
* @brief 这个函数重载了() 运算符,使得其他类可以将ORBextractor类型变量当作函数来使用.
* @param _image 原始图的图像
* @param _mask 掩膜mask
* @param _keypoints 存储特征点关键点的向量
* @param _descriptors 存储特征点描述子的矩阵
* @note 该类最重要的函数,相当于其它类调用此函数时做了一次完整的特征点提取
*/
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
OutputArray _descriptors)
{
//1.检查图像有效性
if(_image.empty())
return;
Mat image = _image.getMat();//获取图像大小
assert(image.type() == CV_8UC1 );//判断图像的格式是否正确,要求是单通道灰度值
//2.构建图像金字塔
ComputePyramid(image);
vector < vector<KeyPoint> > allKeypoints;
//3.提取图像特征点并筛选
ComputeKeyPointsOctTree(allKeypoints);
//ComputeKeyPointsOld(allKeypoints);
//4.计算图像的描述子,并储存至descriptors
Mat descriptors;
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
if( nkeypoints == 0 )
_descriptors.release();
else
{
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
_keypoints.clear();//清空用作返回特征点提取结果的vector容器
_keypoints.reserve(nkeypoints);//预分配正确大小的空间
int offset = 0;
//开始遍历每一层图像
for (int level = 0; level < nlevels; ++level)
{
//获取在allKeypoints中当前层特征点容器的句柄
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size();//本层的特征点数
if(nkeypointsLevel==0)
continue;
Mat workingMat = mvImagePyramid[level].clone();// 深拷贝当前金字塔所在层级的图像
//5.对图像进行高斯模糊
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
//提取特征点的时候,使用的是清晰的原图像;这里计算描述子的时候,为了避免图像噪声的影响,使用了高斯模糊的图像
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
//6.计算高斯模糊后图像的描述子
computeDescriptors(workingMat, keypoints, desc, pattern);
offset += nkeypointsLevel;// 更新偏移量的值
//7.对非第0层图像中的特征点的坐标恢复到第0层图像(原图像)的坐标系下
if (level != 0)
{
float scale = mvScaleFactor[level]; //getScale(level, firstLevel, scaleFactor);
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
keypoint->pt *= scale;
}
//将keypoints中内容插入到_keypoints的末尾
//输出_keypoints
_keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
}
特征提取效果
这里用自己录得一段数据,orbslam做的特征点提取:
orbslam
最近也尝试用卷积网络和深度网络来做特征提取和匹配,这里做个对比:
卷积网络