ORB-SLAM2 ---- ORBextractor::operator()

一、函数作用

此函数为特征点提取中的主函数,也是最重要的一个函数,这是一个仿函数,会在下文中讲到。之所以说本函数是主函数,因为他直接或间接的调用了除构造函数外的所有函数,它完整的实现了特征提取功能(金字塔构建、给每层金字塔分配特征点数、给金字塔每层图像扩充图像边界,提取/分配/筛选特征点,计算特征点的方向信息和描述子等)

二、源码及注释

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()运算符的类或结构体,使得其对象可以像普通函数一样被调用。
特点:

  1. 本质:是一个类或结构体的对象,通过重载()运算符模仿函数的调用行为。
  2. 状态:仿函数可以保存状态(成员变量),与普通函数相比更加灵活。
  3. 用法:可以像函数一样调用对象,常用于STL算法(如std::sort)。
  4. 示例(调用仿函数用类的名称,而不是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());
    }

四、总结

此函数代码部分很简单,没有理解上的难点,但需要所有其他函数的基础,其他函数学习不牢固,会出现不能将主线串起来的问题,这时建议回头查缺补漏,自此特征提取的内容全部结束,大家有什么意见或者建议欢迎评论交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值