讲解关于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) τ(p;x,y)={0,1,if p(x)<p(y)otherwise
以关键点为中心,取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)
步骤 ( 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对随机点如下(一条线段的两个端点是一对):
经过上面的特征提取算法,对于一幅图中的每一个特征点,都得到了一个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 课程课件