(01)ORB-SLAM2源码无死角解析-(11)ORBextractor::operator()→BRIEF描述子

讲解关于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官方认证
 

一、前言

通过前面的博客,我们已经对FAST关键点做了详细的讲解。我们先来回顾一下之前的代码,在 ORBextractor::operator() 函数中,调用了比较重要的几个函数:

void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
                      OutputArray _descriptors){
    // Step 2 构建图像金字塔
    ComputePyramid(image);
    //使用四叉树的方式计算每层图像的特征点并进行分配
    ComputeKeyPointsOctTree(allKeypoints);
    // Step 6 计算高斯模糊后图像的描述子
    computeDescriptors(workingMat, 	//高斯模糊之后的图层图像
                       keypoints, 	//当前图层中的特征点集合
                       desc, 		//存储计算之后的描述子
                       pattern);	//随机采样模板
}

针对于 ComputePyramid(image); 以及 ComputeKeyPointsOctTree(allKeypoints); 已经做了比较详细的讲解,接下来我们就是就是对 computeDescriptors()进行讲解了。在代码讲解之前,我们先来了解一下BRIEF描述子的理论知识

 

二、理论基础

论文:BRIEF: Binary Robust Independent Elementary Features,如果需要详细了解的朋友可以查看这篇论文。
简要说明: 一种对已检测到的特征点进行描述的算法,它是一种二进制编码的描述子,在图像匹配时使用BRIEF能极大的提升匹配速度。
主要实现的思想逻辑如下:

步骤 ( 1 ) \color{blue}{步骤(1)} 步骤(1)
        为减少噪声干扰,先对图像进行高斯滤波.

步骤 ( 2 ) \color{blue}{步骤(2)} 步骤(2)
        以关键点为中心,取SxS的邻域大窗口(仅在该区域内为该关键点生成描述子)。大窗口中随机选取一对(两个)5x5的子窗口,当然也可以是随机两个像素点。比较子窗口内的像素和(可用积分图像完成),进行二进制赋值。(一般S=31),公式如下:
τ ( p ; x , y ) = { 0 , i f    p ( x ) < p ( y ) 1 , o t h e r w i s e \color{blue} \tau(p;x,y)= \begin{cases} 0,& if ~~p(x)τ(p;x,y)={0,1,if  p(x)<p(y)otherwise其上的公式很好理解,就是说两个窗口(像素)和进行比较,第一个小于第二个则为 0否则为1。其中, p ( x ) p(x) p(x) p ( y ) p(y) p(y) 分别取随机点 x = ( u 1 , v 1 ) x=(u1,v1) x=(u1,v1) x = ( u 2 , v 2 ) x=(u2,v2) x=(u2,v2)所在5x5子窗口的像素和

步骤 ( 3 ) \color{blue}{步骤(3)} 步骤(3)
在大窗口中随机选取N对子窗口,重复步骤2的二进制赋值,形成一个二进制编码,这个编码就是对特征点的描述,即特征描述子。(一般N=256),256对其结果就存在256位二进制,一字节8位,也就是四个字节,相当于一个32位 int 形。

其他 \color{blue}{其他} 其他
以上便是BRIEF特征描述算法的步骤。关于随机对点的选择方法,原作者测试了以下五种方式,其中方式(2)的效果比较好。

(1) x i , y i x_i,y_i xi,yi 都呈均匀分布 [ − S 2 , S 2 ] [-\frac{S}{2},\frac{S}{2}] [2S,2S]

(2) x i , y i x_i,y_i xi,yi 都呈高斯分布 [ 0 , 1 25 s 2 ] [0,\frac{1}{25}s^2] [0,251s2],准则采样服从各向同性的同一高斯分布。

(3) x i x_i xi 都呈高斯分布 [ 0 , 1 25 s 2 ] [0,\frac{1}{25}s^2] [0,251s2] y i y_i yi 都呈高斯分布 [ 0 , 1 100 s 2 ] [0,\frac{1}{100}s^2] [0,1001s2],采样分为两步进行:首先在原点处为 x i x_i xi 进行高斯采样,然后在中心为 x i x_i xi 处为 y i y_i yi 进行高斯采样。

(4) x i , y i x_i,y_i xi,yi 在空间量化极坐标的离散位置进行随机采样。

(5) x i = ( 0 , 0 ) τ x_i=(0,0)^\tau xi=(0,0)τ y i y_i yi 在空间量化极坐标的离散位置处进行随机采样。

这5种方法生成的256对随机点如下(一条线段的两个端点是一对):(01)ORB-SLAM2源码无死角解析-(11)ORBextractor::operator()→BRIEF描述子_第1张图片
经过上面的特征提取算法,对于一幅图中的每一个特征点,都得到了一个256bit的二进制编码。
 

三、代码流程

步骤 ( 1 ) \color{blue}{步骤(1)} 步骤(1)
为了每次计算的描述子的踩点是一致的,先固定采样模板,也就是使用固定的采样像素对。代码在src/ORBextractor.cc文件中的 bit_pattern_31_ 变量, 其是 256*4 的数组。一对坐标4个元素,所以这种共256对。

步骤 ( 2 ) \color{blue}{步骤(2)} 步骤(2)
获得关键点角度,单纯的 BRIEF 描述子是不具备方向信息的,所以需要与关键点的角度结合起来。

步骤 ( 3 ) \color{blue}{步骤(3)} 步骤(3)
分成32次循环,每次循环对比8对像素值,共完成 32x8=256 次对比,代码中还使用位移操作来完成计算,这样加快了代码运行速率。
 

四、代码注释

该核心代码为 src/ORBextractor.cc 中的 computeDescriptors()函数,注释如下:

//注意这是一个不属于任何类的全局静态函数,static修饰符限定其只能够被本文件中的函数调用
/**
 * @brief 计算某层金字塔图像上特征点的描述子
 * 
 * @param[in] image                 某层金字塔图像
 * @param[in] keypoints             特征点vector容器
 * @param[out] descriptors          描述子
 * @param[in] pattern               计算描述子使用的固定随机点集
 */
static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
                               const vector<Point>& pattern)
{
    //清空保存描述子信息的容器
    descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);

    //开始遍历特征点
    for (size_t i = 0; i < keypoints.size(); i++)
        //计算这个特征点的描述子
        computeOrbDescriptor(keypoints[i], 				//要计算描述子的特征点
                             image, 					//以及其图像
                             &pattern[0], 				//随机点集的首地址
                             descriptors.ptr((int)i));	//提取出来的描述子的保存位置
}



/**
 * @brief 计算ORB特征点的描述子。注意这个是全局的静态函数,只能是在本文件内被调用
 * @param[in] kpt       特征点对象
 * @param[in] img       提取特征点的图像
 * @param[in] pattern   预定义好的采样模板
 * @param[out] desc     用作输出变量,保存计算好的描述子,维度为32*8 = 256 bit
 */
static void computeOrbDescriptor(const KeyPoint& kpt, const Mat& img, const Point* pattern, uchar* desc)
{
    //得到特征点的角度,用弧度制表示。其中kpt.angle是角度制,范围为[0,360)度
    float angle = (float)kpt.angle*factorPI;
    //计算这个角度的余弦值和正弦值
    float a = (float)cos(angle), b = (float)sin(angle);

    //获得图像中心指针
    const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
    //获得图像的每行的字节数
    const int step = (int)img.step;

    //原始的BRIEF描述子没有方向不变性,通过加入关键点的方向来计算描述子,称之为Steer BRIEF,具有较好旋转不变特性
    //具体地,在计算的时候需要将这里选取的采样模板中点的x轴方向旋转到特征点的方向。
    //获得采样点中某个idx所对应的点的灰度值,这里旋转前坐标为(x,y), 旋转后坐标(x',y'),他们的变换关系:
    // x'= xcos(θ) - ysin(θ),  y'= xsin(θ) + ycos(θ)
    // 下面表示 y'* step + x'
    #define GET_VALUE(idx) center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)]        
    
    //brief描述子由32*8位组成
    //其中每一位是来自于两个像素点灰度的直接比较,所以每比较出8bit结果,需要16个随机点,这也就是为什么pattern需要+=16的原因
    for (int i = 0; i < 32; ++i, pattern += 16)
    {
        
        int t0, 	//参与比较的第1个特征点的灰度值
            t1,		//参与比较的第2个特征点的灰度值		
            val;	//描述子这个字节的比较结果,0或1
        
        t0 = GET_VALUE(0); t1 = GET_VALUE(1);
        val = t0 < t1;							//描述子本字节的bit0
        t0 = GET_VALUE(2); t1 = GET_VALUE(3);
        val |= (t0 < t1) << 1;					//描述子本字节的bit1
        t0 = GET_VALUE(4); t1 = GET_VALUE(5);
        val |= (t0 < t1) << 2;					//描述子本字节的bit2
        t0 = GET_VALUE(6); t1 = GET_VALUE(7);
        val |= (t0 < t1) << 3;					//描述子本字节的bit3
        t0 = GET_VALUE(8); t1 = GET_VALUE(9);
        val |= (t0 < t1) << 4;					//描述子本字节的bit4
        t0 = GET_VALUE(10); t1 = GET_VALUE(11);
        val |= (t0 < t1) << 5;					//描述子本字节的bit5
        t0 = GET_VALUE(12); t1 = GET_VALUE(13);
        val |= (t0 < t1) << 6;					//描述子本字节的bit6
        t0 = GET_VALUE(14); t1 = GET_VALUE(15);
        val |= (t0 < t1) << 7;					//描述子本字节的bit7

        //保存当前比较的出来的描述子的这个字节
        desc[i] = (uchar)val;
    }

    //为了避免和程序中的其他部分冲突在,在使用完成之后就取消这个宏定义
    #undef GET_VALUE
}

五、结语

到该章节为止,对于ORB特征提取的相关内容可以说是结束了。也就是 ORBextractor::operator() 函数已经完成了。该函数是在 Frame.cc 中的 Frame::Frame 中被调用的。如下:

Frame::Frame(const cv::Mat &imGray, const double &timeStamp, ORBextractor* extractor,ORBVocabulary* voc, cv::Mat &K, cv::Mat &distCoef, const float &bf, const float &thDepth)
    :mpORBvocabulary(voc),mpORBextractorLeft(extractor),mpORBextractorRight(static_cast<ORBextractor*>(NULL)),
     mTimeStamp(timeStamp), mK(K.clone()), mDistCoef(distCoef.clone()), mbf(bf), mThDepth(thDepth)
{
    // ORB extraction
	// Step 3 对图像进行提取特征点, 第一个参数0-左图, 1-右图
    ExtractORB(0,imGray);
	......
	......
}

Frame 的构造函数除了提取 ORB 特征之外,还做了其他的操作,如特征点畸变矫正等等。这些内容我们会在后面的博客进行讲解。

 
 
本文内容来自计算机视觉life ORB-SLAM2 课程课件

你可能感兴趣的:(#,自动驾驶,机器人,无人机,增强现实,ORB-SLAM2)