文章目录
- 一、函数作用
- 二、源码及注释
- 三、函数讲解
- 1.什么是仿函数
- 2. 判断图像是否为空,不空则将其转换为灰度图像
- 3. 调用函数[ComputePyramid](https://blog.csdn.net/uzi_ccc/article/details/142750378?spm=1001.2014.3001.5501)预先构建函数金字塔
- 4. 调用函数[ComputeKeyPointsOctTree](https://blog.csdn.net/uzi_ccc/article/details/142768805?spm=1001.2014.3001.5501),使用八叉树 (OctTree) 方法计算每一层的特征点
- 5. 统计每层特征点数量,并准备一些容器,在后续处理特征点时使用
- 6. 处理特征点
- 四、总结
一、函数作用
此函数为特征点提取中的主函数,也是最重要的一个函数,这是一个仿函数,会在下文中讲到。之所以说本函数是主函数,因为他直接或间接的调用了除构造函数外的所有函数,它完整的实现了特征提取功能(金字塔构建、给每层金字塔分配特征点数、给金字塔每层图像扩充图像边界,提取/分配/筛选特征点,计算特征点的方向信息和描述子等)
二、源码及注释
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
OutputArray _descriptors)
{
// 判断图像是否为空
if(_image.empty())
return;
// 将输入的 _image 转换为 Mat 类型,并确保它是灰度图像 (CV_8UC1)
Mat image = _image.getMat();
// 括号内的为真则继续进行,为假则输出报错信息
assert(image.type() == CV_8UC1 );
// Pre-compute the scale pyramid
// 预先计算图像的金字塔 (图像金字塔用于多尺度特征检测)
ComputePyramid(image);
// 用于存储所有金字塔层的特征点
vector < vector<KeyPoint> > allKeypoints;
// 使用八叉树 (OctTree) 方法计算每一层的特征点
ComputeKeyPointsOctTree(allKeypoints);
//ComputeKeyPointsOld(allKeypoints);
// 初始化描述子矩阵
Mat descriptors;
// 统计所有金字塔层中的关特征点总数
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
// 如果没有找到特征点,释放描述子矩阵并返回
if( nkeypoints == 0 )
_descriptors.release();
else
{
// 为描述子矩阵分配空间,描述子的大小为 nkeypoints × 32,类型为 CV_8U
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
// 清空并准备特征点容器
_keypoints.clear();
_keypoints.reserve(nkeypoints);
// 用于描述子的偏移量,用于遍历金字塔层时对每层的关键点处理
int offset = 0;
// 遍历每一层的金字塔
for (int level = 0; level < nlevels; ++level)
{
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size();
// 如果当前层没有特征点,跳过该层
if(nkeypointsLevel==0)
continue;
// preprocess the resized image
// 对当前层的图像进行预处理,先克隆,再使用高斯模糊 (GaussianBlur) 平滑图像
Mat workingMat = mvImagePyramid[level].clone();
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
// Compute the descriptors
// 计算当前层的描述子,将其存储到 descriptors 矩阵中
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern);
// 更新偏移量,指向下一个金字塔层的位置
offset += nkeypointsLevel;
// Scale keypoint coordinates
// 对除第一层外的所有层,按照比例缩放特征点坐标
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.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
}
三、函数讲解
因为此函数代码量较大,我们分几段来讲解
1.什么是仿函数
仿函数(函数对象,Functor)是在C++中通过重载operator()运算符的类或结构体,使得其对象可以像普通函数一样被调用。
特点:
- 本质:是一个类或结构体的对象,通过重载()运算符模仿函数的调用行为。
- 状态:仿函数可以保存状态(成员变量),与普通函数相比更加灵活。
- 用法:可以像函数一样调用对象,常用于STL算法(如std::sort)。
- 示例(调用仿函数用类的名称,而不是operator()):
#include <iostream>
// 定义一个仿函数类
class Add {
public:
// 重载()运算符
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Add add; // 创建仿函数对象
int result = add(3, 4); // 像函数一样调用仿函数对象
std::cout << "Result: " << result << std::endl; // 输出结果7
return 0;
}
2. 判断图像是否为空,不空则将其转换为灰度图像
将图象转换为灰度图像的原因是,后续特征提取和匹配时用的是灰度图像(也可以说灰度图像提取特征点更容易)
// 判断图像是否为空
if(_image.empty())
return;
// 将输入的 _image 转换为 Mat 类型,并确保它是灰度图像 (CV_8UC1)
Mat image = _image.getMat();
// 括号内的为真则继续进行,为假则输出报错信息
assert(image.type() == CV_8UC1 );
3. 调用函数ComputePyramid预先构建函数金字塔
4. 调用函数ComputeKeyPointsOctTree,使用八叉树 (OctTree) 方法计算每一层的特征点
此函数中调用了FAST()角点提取器函数,先提取特征点,然后将特征点进行分配和过滤,后计算每个特征点的方向信息
5. 统计每层特征点数量,并准备一些容器,在后续处理特征点时使用
// 初始化描述子矩阵
Mat descriptors;
// 统计所有金字塔层中的关特征点总数
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
// 如果没有找到特征点,释放描述子矩阵并返回
if( nkeypoints == 0 )
_descriptors.release();
else
{
// 为描述子矩阵分配空间,描述子的大小为 nkeypoints × 32,类型为 CV_8U
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
// 清空并准备特征点容器
_keypoints.clear();
_keypoints.reserve(nkeypoints);
// 用于描述子的偏移量,用于遍历金字塔层时对每层的关键点处理
int offset = 0;
6. 处理特征点
遍历每个金字塔图层,并对其作高斯模糊,然后计算每个特征点的描述子,最后将处理好的特征点放入容器中。本人认为这段代码的处理并不好,应当将高斯模糊和计算描述子放入ComputeKeyPointsOctTree函数中,这样可以将处理特征点的工作集中到一个函数中,让代码看起来不那么凌乱,也避免ComputeKeyPointsOctTree中遍历后主函数中再次遍历。
for (int level = 0; level < nlevels; ++level)
{
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size();
// 如果当前层没有特征点,跳过该层
if(nkeypointsLevel==0)
continue;
// preprocess the resized image
// 对当前层的图像进行预处理,先克隆,再使用高斯模糊 (GaussianBlur) 平滑图像
Mat workingMat = mvImagePyramid[level].clone();
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
// Compute the descriptors
// 计算当前层的描述子,将其存储到 descriptors 矩阵中
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern);
// 更新偏移量,指向下一个金字塔层的位置
offset += nkeypointsLevel;
// Scale keypoint coordinates
// 对除第一层外的所有层,按照比例缩放特征点坐标
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.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
四、总结
此函数代码部分很简单,没有理解上的难点,但需要所有其他函数的基础,其他函数学习不牢固,会出现不能将主线串起来的问题,这时建议回头查缺补漏,自此特征提取的内容全部结束,大家有什么意见或者建议欢迎评论交流。