1.这个仿函数做了什么?
对输入的图像提取特征点、计算描述子。
2.代码解析
/** * @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)
2.1 检查图像有效性。如果图像为空,那么就直接返回
if(_image.empty()) return; //获取图像的大小 Mat image = _image.getMat(); //判断图像的格式是否正确,要求是单通道灰度值 assert(image.type() == CV_8UC1 );
2.2 构建图像金字塔
ComputePyramid(image);
ORB-SLAM2 ---- ORBextractor::ComputePyramid函数_Courage2022的博客-CSDN博客
2.3 计算图像的特征点,并且将特征点进行均匀化。均匀的特征点可以提高位姿计算精度
// 存储所有的特征点,注意此处为二维的vector,第一维存储的是金字塔的层数,第二维存储的是那一层金字塔图像里提取的所有特征点 vector < vector<KeyPoint> > allKeypoints; //使用四叉树的方式计算每层图像的特征点并进行分配 ComputeKeyPointsOctTree(allKeypoints);
ORB-SLAM2 ---- ORBextractor::ComputeKeyPointsOctTree函数_Courage2022的博客-CSDN博客
2.4 拷贝图像描述子到新的矩阵descriptors
Mat descriptors; //统计整个图像金字塔中的特征点 int nkeypoints = 0; //开始遍历每层图像金字塔,并且累加每层的特征点个数 for (int level = 0; level < nlevels; ++level) nkeypoints += (int)allKeypoints[level].size(); //如果本图像金字塔中没有任何的特征点 if( nkeypoints == 0 ) //通过调用cv::mat类的.realse方法,强制清空矩阵的引用计数,这样就可以强制释放矩阵的数据了 //参考[https://blog.csdn.net/giantchen547792075/article/details/9107877] _descriptors.release(); else { //如果图像金字塔中有特征点,那么就创建这个存储描述子的矩阵,注意这个矩阵是存储整个图像金字塔中特征点的描述子的 _descriptors.create(nkeypoints, //矩阵的行数,对应为特征点的总个数 32, //矩阵的列数,对应为使用32*8=256位描述子 CV_8U); //矩阵元素的格式 //获取这个描述子的矩阵信息 // ?为什么不是直接在参数_descriptors上对矩阵内容进行修改,而是重新新建了一个变量,复制矩阵后,在这个新建变量的基础上进行修改? descriptors = _descriptors.getMat(); } //清空用作返回特征点提取结果的vector容器 _keypoints.clear(); //并预分配正确大小的空间 _keypoints.reserve(nkeypoints); //因为遍历是一层一层进行的,但是描述子那个矩阵是存储整个图像金字塔中特征点的描述子,所以在这里设置了Offset变量来保存“寻址”时的偏移量, //辅助进行在总描述子mat中的定位 int offset = 0; //开始遍历每一层图像 for (int level = 0; level < nlevels; ++level) { //获取在allKeypoints中当前层特征点容器的句柄 vector<KeyPoint>& keypoints = allKeypoints[level]; //本层的特征点数 int nkeypointsLevel = (int)keypoints.size(); //如果特征点数目为0,跳出本次循环,继续下一层金字塔 if(nkeypointsLevel==0) continue; // preprocess the resized image
这里我们新建了一个描述子矩阵descriptors,统计所有层的已提取特征点数目存放在nkeypoints里(为什么要建立这个变量,配置文件里面不是已经有要分配的特征点数量了吗?ORBextractor.nFeatures: 1000?因为可能由于图片原因未必能找出那么多特征点....), 在判断特征点数目是否为空?(SLAM系统BUG或者图片原因);
若为空则清空用作返回特征点提取结果的vector容器,若不为空那么就创建这个存储描述子的矩阵,注意这个矩阵是存储整个图像金字塔中特征点的描述子的。
对每层图像金子塔进行遍历,取出当前层的特征点存储到keypoints容器中,并存储该层金字塔中特征点的数量到变量nkeypointsLevel 中,如果特征点数目 = 0,跳过该层金字塔。
2.5 对图像进行高斯模糊
Mat workingMat = mvImagePyramid[level].clone(); // 注意:提取特征点的时候,使用的是清晰的原图像;这里计算描述子的时候,为了避免图像噪声的影响,使用了高斯模糊 GaussianBlur(workingMat, //源图像 workingMat, //输出图像 Size(7, 7), //高斯滤波器kernel大小,必须为正的奇数 2, //高斯滤波在x方向的标准差 2, //高斯滤波在y方向的标准差 BORDER_REFLECT_101);//边缘拓展点插值类型
高斯模糊意义:去掉干扰点
参数意义:我们高斯模糊原图像(金字塔第i层的图像,深拷贝到了workingMat 变量中),并将高斯模糊后的图像也放入到该变量中。
2.6 计算高斯模糊后图像的描述子
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel); computeDescriptors(workingMat, //高斯模糊之后的图层图像 keypoints, //当前图层中的特征点集合 desc, //存储计算之后的描述子 pattern); //随机采样模板 // 更新偏移量的值 offset += nkeypointsLevel;
2.7 对非第0层图像中的特征点的坐标恢复到第0层图像(原图像)的坐标系下
// 对于第0层的图像特征点,他们的坐标就不需要再进行恢复了 if (level != 0) { // 获取当前图层上的缩放系数 float scale = mvScaleFactor[level]; // 遍历本层所有的特征点 for (vector<KeyPoint>::iterator keypoint = keypoints.begin(), keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint) // 特征点本身直接乘缩放倍数就可以了 keypoint->pt *= scale; } // And add the keypoints to the output // 将keypoints中内容插入到_keypoints 的末尾 // keypoint其实是对allkeypoints中每层图像中特征点的引用,这样allkeypoints中的所有特征点在这里被转存到输出的_keypoints _keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
将特征点乘以缩放系数得到对应层的特征点坐标。
将提取到的特征点放入_keypoints容器交付上层。