ORB_SLAM2 源码解析 ORB特征提取(二)

目录

一、各成员函数变量

1、定义一个枚举类型用于表示使用HARRIS响应值还是使用FAST响应值

2、内联函数都是用来直接获取类的成员变量的

3、保护成员

二、计算特征点的方向 computeOrientation()

2.1、灰度质心法算法步骤

1、计算一个半径为15的近似圆

2、计算特征点角度

3、IC_Angle 计算技巧

4、灰度质心法计算公式

5、计算特征点的方向(computeOrientation)

三、FAST描述子

BRIEF描述子生成步骤

五、金字塔的计算(ORBextractor::ComputePyramid)

六、提取FAST特征点

6.1、分cell搜索特征点

6.2、提取特征点

6.3、四叉树筛选特征点: DistributeOctTree()

6.4、 最后计算这些特征点的方向信息

七、总结

一、各成员函数变量

在阅读代码之前我们先来介绍变量的命名规则

1、定义一个枚举类型用于表示使用HARRIS响应值还是使用FAST响应值

nfeatures
指定要提取出来的特征点数目
scaleFactor
图像金字塔的缩放系数
nlevels
指定需要提取特征点的图像金字塔层

iniThFAST

初始的默认FAST响应值阈值
minThFAST
较小的FAST响应值阈值

2、内联函数都是用来直接获取类的成员变量的

GetScaleFactor()
获取当前提取器所在的图像的缩放因子
mvScaleFactor
图像金字塔中每个图层相对于底层图像的缩放因子
GetInverseScaleFactors()
获取上面的那个缩放因子s的倒数
GetScaleSigmaSquares()
获取sigma^2,就是每层图像相对于初始图像缩放因子的平方
GetInverseScaleSigmaSquares()
获取上面sigma平方的倒数
mvImagePyramid
用来存储图像金字塔的变量,一个元素存储一层图像

3、保护成员

保护成员就是私有的别人不可以调用

ComputePyramid
计算其图像金字塔
ComputeKeyPointsOctTree
以八叉树分配特征点的方式,计算图像金字塔中的特征点
vToDistributeKeys
等待分配的特征点
mnFeaturesPerLevel
分配到每层图像中,要提取的特征点数目
umax
计算特征点方向的时候,有个圆形的图像区域,这个vector中存储了每行u轴的边界(四分之一,其他部分通过对称获得)

二、计算特征点的方向 computeOrientation()

计算特征点的方向是为了使得提取的特征点具有旋转不变性

方法是灰度质心法:以几何中心和灰度质心的连线作为该特征点方向

2.1、灰度质心法算法步骤

1、计算一个半径为15的近似圆

ORB_SLAM2 源码解析 ORB特征提取(二)

后面计算的是 特征点主方向上的描述子,计算过程中要将特征点周围像素旋转到主方向上,因此计算一个半径为 16的圆的近似坐标,用于后面计算描述子时进行旋转操作.

ORB_SLAM2 源码解析 ORB特征提取(二)
PATCH_SIZE
图像块的大小,或者说是直径

31

HALF_PATCH_SIZE
上面这个大小的一半,或者说是半径

15

EDGE_THRESHOLD
算法生成的图像边

19

u_max
图像块的每一行的坐标边界

float

返回特征点的角度,范围为[0,360)角度,精度为0.3°
int vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1);    // 45°射线与圆周交点的纵坐标
int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);         // 45°射线与圆周交点的纵坐标

// 先计算下半45度的umax
for (int v = 0; v <= 15 vmax; ++v) { umax[v]="cvRound(sqrt(15" * - v v)); } 根据对称性补出上半45度的umax for (int v0="0;">= vmin; --v) {
    while (umax[v0] == umax[v0 + 1])
        ++v0;
    umax[v] = v0;
    ++v0;
}</=>

ORB_SLAM2 源码解析 ORB特征提取(二)

2、计算特征点角度

点v 绕 原点旋转θ 角,得到点v’,假设 v点的坐标是(x, y) ,那么可以推导得到 v’点的坐标(x’, y’)

ORB_SLAM2 源码解析 ORB特征提取(二)
 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;

    // &#x65CB;&#x8F6C;&#x516C;&#x5F0F;
    // x'= xcos(&#x3B8;) - ysin(&#x3B8;)
    // y'= xsin(&#x3B8;) + ycos(&#x3B8;)

#define GET_VALUE(idx) \
    center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)] </uchar>

3、IC_Angle 计算技巧

在一个圆域中算出m10(x坐标)和m01(y坐标),计算步骤是先算出中间红线的m10,然后在平行于 x轴算出m10和m01,一次计算相当于图像中的同个颜色的两个line

ORB_SLAM2 源码解析 ORB特征提取(二)

4、灰度质心法计算公式

ORB_SLAM2 源码解析 ORB特征提取(二)
static float IC_Angle(const Mat& image, Point2f pt,  const vector<int> & u_max)
{
    //&#x56FE;&#x50CF;&#x7684;&#x77E9;&#xFF0C;&#x524D;&#x8005;&#x662F;&#x6309;&#x7167;&#x56FE;&#x50CF;&#x5757;&#x7684;y&#x5750;&#x6807;&#x52A0;&#x6743;&#xFF0C;&#x540E;&#x8005;&#x662F;&#x6309;&#x7167;&#x56FE;&#x50CF;&#x5757;&#x7684;x&#x5750;&#x6807;&#x52A0;&#x6743;
    int m_01 = 0, m_10 = 0;
    //&#x83B7;&#x5F97;&#x8FD9;&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x6240;&#x5728;&#x7684;&#x56FE;&#x50CF;&#x5757;&#x7684;&#x4E2D;&#x5FC3;&#x70B9;&#x5750;&#x6807;&#x7070;&#x5EA6;&#x503C;&#x7684;&#x6307;&#x9488;center
    const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));
    // Treat the center line differently, v=0
    //&#x8FD9;&#x6761;v=0&#x4E2D;&#x5FC3;&#x7EBF;&#x7684;&#x8BA1;&#x7B97;&#x9700;&#x8981;&#x7279;&#x6B8A;&#x5BF9;&#x5F85;
    //&#x540E;&#x9762;&#x662F;&#x4EE5;&#x4E2D;&#x5FC3;&#x884C;&#x4E3A;&#x5BF9;&#x79F0;&#x8F74;&#xFF0C;&#x6210;&#x5BF9;&#x904D;&#x5386;&#x884C;&#x6570;&#xFF0C;&#x6240;&#x4EE5;PATCH_SIZE&#x5FC5;&#x987B;&#x662F;&#x5947;&#x6570;
    for (int u = -HALF_PATCH_SIZE; u <= half_patch_size; ++u) 注意这里的center下标u可以是负的!中心水平线上的像素按x坐标(也就是u坐标)加权 m_10 +="u" * center[u]; go line by in the circular patch 这里的step1表示这个图像一行包含的字节总数。参考[https: blog.csdn.net qianqing13579 article details 45318279] int step="(int)image.step1();" 注意这里是以v="0&#x4E2D;&#x5FC3;&#x7EBF;&#x4E3A;&#x5BF9;&#x79F0;&#x8F74;&#xFF0C;&#x7136;&#x540E;&#x5BF9;&#x79F0;&#x5730;&#x6BCF;&#x6210;&#x5BF9;&#x7684;&#x4E24;&#x884C;&#x4E4B;&#x95F4;&#x8FDB;&#x884C;&#x904D;&#x5386;&#xFF0C;&#x8FD9;&#x6837;&#x5904;&#x7406;&#x52A0;&#x5FEB;&#x4E86;&#x8BA1;&#x7B97;&#x901F;&#x5EA6;" for (int v="1;" <="HALF_PATCH_SIZE;" ++v) { proceed over two lines 本来m_01应该是一列一列地计算的,但是由于对称以及坐标x,y正负的原因,可以一次计算两行 v_sum="0;" 获取某行像素横坐标的最大范围,注意这里的图像块是圆形的! d="u_max[v];" 在坐标范围内挨个像素遍历,实际是一次遍历2个 假设每次处理的两个点坐标,中心线下方为(x,y),中心线上方为(x,-y) 对于某次待处理的两个点:m_10="&#x3A3;" x*i(x,y)="x*I(x,y)" x*i(x,-y)="x*(I(x,y)" i(x,-y)) 对于某次待处理的两个点:m_01="&#x3A3;" y*i(x,y)="y*I(x,y)" - y*i(x,-y)="y*(I(x,y)" u="-d;" 得到需要进行加运算和减运算的像素灰度值 val_plus:在中心线下方x="u&#x65F6;&#x7684;&#x7684;&#x50CF;&#x7D20;&#x7070;&#x5EA6;&#x503C;" val_minus:在中心线上方x="u&#x65F6;&#x7684;&#x50CF;&#x7D20;&#x7070;&#x5EA6;&#x503C;" val_plus="center[u" v*step], val_minus="center[u" v*step]; 在v(y轴)上,2行所有像素灰度值之差 val_minus); u轴(也就是x轴)方向上用u坐标加权和(u坐标也有正负符号),相当于同时计算两行 (val_plus } 将这一行上的和按照y坐标加权 m_01 v_sum; 为了加快速度还使用了fastatan2()函数,输出为[0,360)角度,精度为0.3° return fastatan2((float)m_01, (float)m_10); 乘数因子,一度对应着多少弧度 const float factorpi="(float)(CV_PI/180.f);" code></=></uchar></int>

5、计算特征点的方向( computeOrientation )

static void computeOrientation(const Mat& image, vector<keypoint>& keypoints, const vector<int>& umax)
{
    // &#x904D;&#x5386;&#x6240;&#x6709;&#x7684;&#x7279;&#x5F81;&#x70B9;
    for (vector<keypoint>::iterator keypoint = keypoints.begin(),
         keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
    {
        // &#x8C03;&#x7528;IC_Angle &#x51FD;&#x6570;&#x8BA1;&#x7B97;&#x8FD9;&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65B9;&#x5411;
        keypoint->angle = IC_Angle(image,           //&#x7279;&#x5F81;&#x70B9;&#x6240;&#x5728;&#x7684;&#x56FE;&#x5C42;&#x7684;&#x56FE;&#x50CF;
                                   keypoint->pt,    //&#x7279;&#x5F81;&#x70B9;&#x5728;&#x8FD9;&#x5F20;&#x56FE;&#x50CF;&#x4E2D;&#x7684;&#x5750;&#x6807;
                                   umax);           //&#x6BCF;&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x6240;&#x5728;&#x56FE;&#x50CF;&#x533A;&#x5757;&#x7684;&#x6BCF;&#x884C;&#x7684;&#x8FB9;&#x754C; u_max &#x7EC4;&#x6210;&#x7684;vector
    }
}</keypoint></int></keypoint>

三、FAST描述子

BRIEF算法的核心思想是在关键点P的周围以一定模式选取N个点对,把这N个点对的比较结果组合起来作为描述子。

ORB_SLAM2 源码解析 ORB特征提取(二)

BRIEF描述子生成步骤

1.以关键点P为圆心,以d为半径做圆O。
2.在圆O内某一模式选取N个点对。这里为方便说明,N=4,实际应用中N可以取512.

假设当前选取的4个点对如上图所示分别标记为:

ORB_SLAM2 源码解析 ORB特征提取(二)

3.定义操作T

ORB_SLAM2 源码解析 ORB特征提取(二)

4.分别对已选取的点对进行T操作,将得到的结果进行组合。
假如:

ORB_SLAM2 源码解析 ORB特征提取(二)

原始的 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&#x63CF;&#x8FF0;&#x5B50;&#x7531;32*8&#x4F4D;&#x7EC4;&#x6210;
    //&#x5176;&#x4E2D;&#x6BCF;&#x4E00;&#x4F4D;&#x662F;&#x6765;&#x81EA;&#x4E8E;&#x4E24;&#x4E2A;&#x50CF;&#x7D20;&#x70B9;&#x7070;&#x5EA6;&#x7684;&#x76F4;&#x63A5;&#x6BD4;&#x8F83;&#xFF0C;&#x6240;&#x4EE5;&#x6BCF;&#x6BD4;&#x8F83;&#x51FA;8bit&#x7ED3;&#x679C;&#xFF0C;&#x9700;&#x8981;16&#x4E2A;&#x968F;&#x673A;&#x70B9;&#xFF0C;&#x8FD9;&#x4E5F;&#x5C31;&#x662F;&#x4E3A;&#x4EC0;&#x4E48;pattern&#x9700;&#x8981;+=16&#x7684;&#x539F;&#x56E0;
for (int i = 0; i < 32; ++i, pattern += 16)
{

        int t0,     //&#x53C2;&#x4E0E;&#x6BD4;&#x8F83;&#x7684;&#x7B2C;1&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x7070;&#x5EA6;&#x503C;
            t1,     //&#x53C2;&#x4E0E;&#x6BD4;&#x8F83;&#x7684;&#x7B2C;2&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x7070;&#x5EA6;&#x503C;
            val;    //&#x63CF;&#x8FF0;&#x5B50;&#x8FD9;&#x4E2A;&#x5B57;&#x8282;&#x7684;&#x6BD4;&#x8F83;&#x7ED3;&#x679C;&#xFF0C;0&#x6216;1

        t0 = GET_VALUE(0); t1 = GET_VALUE(1);
        val = t0 < t1;                          //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit0
        t0 = GET_VALUE(2); t1 = GET_VALUE(3);
        val |= (t0 < t1) << 1;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit1
        t0 = GET_VALUE(4); t1 = GET_VALUE(5);
        val |= (t0 < t1) << 2;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit2
        t0 = GET_VALUE(6); t1 = GET_VALUE(7);
        val |= (t0 < t1) << 3;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit3
        t0 = GET_VALUE(8); t1 = GET_VALUE(9);
        val |= (t0 < t1) << 4;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit4
        t0 = GET_VALUE(10); t1 = GET_VALUE(11);
        val |= (t0 < t1) << 5;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit5
        t0 = GET_VALUE(12); t1 = GET_VALUE(13);
        val |= (t0 < t1) << 6;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit6
        t0 = GET_VALUE(14); t1 = GET_VALUE(15);
        val |= (t0 < t1) << 7;                  //&#x63CF;&#x8FF0;&#x5B50;&#x672C;&#x5B57;&#x8282;&#x7684;bit7

        //&#x4FDD;&#x5B58;&#x5F53;&#x524D;&#x6BD4;&#x8F83;&#x7684;&#x51FA;&#x6765;&#x7684;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x8FD9;&#x4E2A;&#x5B57;&#x8282;
        desc[i] = (uchar)val;
    }

    //&#x4E3A;&#x4E86;&#x907F;&#x514D;&#x548C;&#x7A0B;&#x5E8F;&#x4E2D;&#x7684;&#x5176;&#x4ED6;&#x90E8;&#x5206;&#x51B2;&#x7A81;&#x5728;&#xFF0C;&#x5728;&#x4F7F;&#x7528;&#x5B8C;&#x6210;&#x4E4B;&#x540E;&#x5C31;&#x53D6;&#x6D88;&#x8FD9;&#x4E2A;&#x5B8F;&#x5B9A;&#x4E49;
    #undef GET_VALUE
}

五、金字塔的计算( ORBextractor::ComputePyramid )

金字塔是为了实现尺度不变性

ORB_SLAM2 源码解析 ORB特征提取(二)

具体实现方式如上图所示,当摄像机靠近图像,特征点变大,能提取的特征点变少;当摄像机远离图像时特征点变小,能提取到的特征点变多。我们可以观察到摄像机在正常位置时,第0层的特征点与摄像机往前移动第1层的特征点差不多大,利用这个特性我们可以实现尺度不变性。

iniThFAST
指定初始的FAST特征点提取参数,可以提取出最明显的角点
minThFAST
如果初始阈值没有检测到角点,降低到这个阈值提取出弱一点的角点
ORBextractor::ORBextractor(int _nfeatures,      //&#x6307;&#x5B9A;&#x8981;&#x63D0;&#x53D6;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x6570;&#x76EE;
                           float _scaleFactor,  //&#x6307;&#x5B9A;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x7684;&#x7F29;&#x653E;&#x7CFB;&#x6570;
                           int _nlevels,        //&#x6307;&#x5B9A;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x7684;&#x5C42;&#x6570;
                           int _iniThFAST,      //&#x6307;&#x5B9A;&#x521D;&#x59CB;&#x7684;FAST&#x7279;&#x5F81;&#x70B9;&#x63D0;&#x53D6;&#x53C2;&#x6570;&#xFF0C;&#x53EF;&#x4EE5;&#x63D0;&#x53D6;&#x51FA;&#x6700;&#x660E;&#x663E;&#x7684;&#x89D2;&#x70B9;
                           int _minThFAST):     //&#x5982;&#x679C;&#x521D;&#x59CB;&#x9608;&#x503C;&#x6CA1;&#x6709;&#x68C0;&#x6D4B;&#x5230;&#x89D2;&#x70B9;&#xFF0C;&#x964D;&#x4F4E;&#x5230;&#x8FD9;&#x4E2A;&#x9608;&#x503C;&#x63D0;&#x53D6;&#x51FA;&#x5F31;&#x4E00;&#x70B9;&#x7684;&#x89D2;&#x70B9;
    nfeatures(_nfeatures), scaleFactor(_scaleFactor), nlevels(_nlevels),
    iniThFAST(_iniThFAST), minThFAST(_minThFAST)//&#x8BBE;&#x7F6E;&#x8FD9;&#x4E9B;&#x53C2;&#x6570;
{
    //&#x5B58;&#x50A8;&#x6BCF;&#x5C42;&#x56FE;&#x50CF;&#x7F29;&#x653E;&#x7CFB;&#x6570;&#x7684;vector&#x8C03;&#x6574;&#x4E3A;&#x7B26;&#x5408;&#x56FE;&#x5C42;&#x6570;&#x76EE;&#x7684;&#x5927;&#x5C0F;
    mvScaleFactor.resize(nlevels);
    //&#x5B58;&#x50A8;&#x8FD9;&#x4E2A;sigma^2&#xFF0C;&#x5176;&#x5B9E;&#x5C31;&#x662F;&#x6BCF;&#x5C42;&#x56FE;&#x50CF;&#x76F8;&#x5BF9;&#x521D;&#x59CB;&#x56FE;&#x50CF;&#x7F29;&#x653E;&#x56E0;&#x5B50;&#x7684;&#x5E73;&#x65B9;
    mvLevelSigma2.resize(nlevels);
    //&#x5BF9;&#x4E8E;&#x521D;&#x59CB;&#x56FE;&#x50CF;&#xFF0C;&#x8FD9;&#x4E24;&#x4E2A;&#x53C2;&#x6570;&#x90FD;&#x662F;1
    mvScaleFactor[0]=1.0f;
    mvLevelSigma2[0]=1.0f;

函数void ORBextractor::ComputePyramid(cv::Mat image)逐层计算图像金字塔,对于每层图像进行以下两步:

1、先进行图片缩放,缩放到mvInvScaleFactor对应尺寸.

2、在图像外补一圈厚度为19的padding(提取FAST特征点需要特征点周围半径为3的圆域,计算ORB描述子需要特征点周围半径为16的圆域).

下图表示图像金字塔每层结构:

深灰色为缩放后的原始图像.

包含绿色边界在内的矩形用于提取FAST特征点.

包含浅灰色边界在内的整个矩形用于计算ORB描述子.

ORB_SLAM2 源码解析 ORB特征提取(二)
//&#x8BA1;&#x7B97;&#x8FD9;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x5750;&#x6807;&#x8FB9;&#x754C;&#xFF0C; NOTICE &#x6CE8;&#x610F;&#x8FD9;&#x91CC;&#x662F;&#x5750;&#x6807;&#x8FB9;&#x754C;&#xFF0C;EDGE_THRESHOLD&#x6307;&#x7684;&#x5E94;&#x8BE5;&#x662F;&#x53EF;&#x4EE5;&#x63D0;&#x53D6;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x6709;&#x6548;&#x56FE;&#x50CF;&#x8FB9;&#x754C;&#xFF0C;&#x540E;&#x9762;&#x4F1A;&#x4E00;&#x76F4;&#x4F7F;&#x7528;&#x201C;&#x6709;&#x6548;&#x56FE;&#x50CF;&#x8FB9;&#x754C;&#x201C;&#x8FD9;&#x4E2A;&#x81EA;&#x521B;&#x540D;&#x8BCD;
        const int minBorderX = EDGE_THRESHOLD-3;            //&#x8FD9;&#x91CC;&#x7684;3&#x662F;&#x56E0;&#x4E3A;&#x5728;&#x8BA1;&#x7B97;FAST&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x9700;&#x8981;&#x5EFA;&#x7ACB;&#x4E00;&#x4E2A;&#x534A;&#x5F84;&#x4E3A;3&#x7684;&#x5706;
        const int minBorderY = minBorderX;                  //minY&#x7684;&#x8BA1;&#x7B97;&#x5C31;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x62F7;&#x8D1D;&#x4E0A;&#x9762;&#x7684;&#x8BA1;&#x7B97;&#x7ED3;&#x679C;&#x4E86;
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;

void ORBextractor::ComputePyramid(cv::Mat image) {
    for (int level = 0; level < nlevels; ++level) {
        // &#x8BA1;&#x7B97;&#x7F29;&#x653E;+&#x8865;padding&#x540E;&#x8BE5;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x5C3A;&#x5BF8;
        float scale = mvInvScaleFactor[level];
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
        Size wholeSize(sz.width + EDGE_THRESHOLD * 2, sz.height + EDGE_THRESHOLD * 2);
        Mat temp(wholeSize, image.type());

        // &#x7F29;&#x653E;&#x56FE;&#x50CF;&#x5E76;&#x590D;&#x5236;&#x5230;&#x5BF9;&#x5E94;&#x56FE;&#x5C42;&#x5E76;&#x8865;&#x8FB9;
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
        if( level != 0 ) {
            resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, cv::INTER_LINEAR);
            copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101+BORDER_ISOLATED);
        } else {
            copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101);
        }
    }
}

opyMakeBorder 函数实现了复制和 padding 填充,其参数 BORDER_REFLECT_101 参数指定对padding进行镜像填充

ORB_SLAM2 源码解析 ORB特征提取(二)

六、提取FAST特征点

ORB_SLAM2 源码解析 ORB特征提取(二)

6.1、分cell搜索特征点

CELL搜索特征点,若某 CELL内特征点响应值普遍较小的话就降低分数线再搜索一遍.

CELL搜索的示意图如下,每个 CELL的大小约为 30&#x2716;30,搜索到边上,剩余尺寸不够大的时候,最后一个 CELL有多大就用多大的区域.

ORB_SLAM2 源码解析 ORB特征提取(二)

需要注意的是相邻的 CELL之间会有 6像素的重叠区域,因为提取 FAST特征点需要计算特征点周围半径为 3的圆周上的像素点信息,实际上产生特征点的区域比传入的搜索区域小 3像素.

ORB_SLAM2 源码解析 ORB特征提取(二)
//&#x904D;&#x5386;&#x6240;&#x6709;&#x56FE;&#x50CF;
    for (int level = 0; level < nlevels; ++level)
    {
        //&#x8BA1;&#x7B97;&#x8FD9;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x5750;&#x6807;&#x8FB9;&#x754C;&#xFF0C; NOTICE &#x6CE8;&#x610F;&#x8FD9;&#x91CC;&#x662F;&#x5750;&#x6807;&#x8FB9;&#x754C;&#xFF0C;EDGE_THRESHOLD&#x6307;&#x7684;&#x5E94;&#x8BE5;&#x662F;&#x53EF;&#x4EE5;&#x63D0;&#x53D6;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x6709;&#x6548;&#x56FE;&#x50CF;&#x8FB9;&#x754C;&#xFF0C;&#x540E;&#x9762;&#x4F1A;&#x4E00;&#x76F4;&#x4F7F;&#x7528;&#x201C;&#x6709;&#x6548;&#x56FE;&#x50CF;&#x8FB9;&#x754C;&#x201C;&#x8FD9;&#x4E2A;&#x81EA;&#x521B;&#x540D;&#x8BCD;
        const int minBorderX = EDGE_THRESHOLD-3;            //&#x8FD9;&#x91CC;&#x7684;3&#x662F;&#x56E0;&#x4E3A;&#x5728;&#x8BA1;&#x7B97;FAST&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x9700;&#x8981;&#x5EFA;&#x7ACB;&#x4E00;&#x4E2A;&#x534A;&#x5F84;&#x4E3A;3&#x7684;&#x5706;
        const int minBorderY = minBorderX;                  //minY&#x7684;&#x8BA1;&#x7B97;&#x5C31;&#x53EF;&#x4EE5;&#x76F4;&#x63A5;&#x62F7;&#x8D1D;&#x4E0A;&#x9762;&#x7684;&#x8BA1;&#x7B97;&#x7ED3;&#x679C;&#x4E86;
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;

        //&#x5B58;&#x50A8;&#x9700;&#x8981;&#x8FDB;&#x884C;&#x5E73;&#x5747;&#x5206;&#x914D;&#x7684;&#x7279;&#x5F81;&#x70B9;
        vector<cv::keypoint> vToDistributeKeys;
        //&#x4E00;&#x822C;&#x5730;&#x90FD;&#x662F;&#x8FC7;&#x91CF;&#x91C7;&#x96C6;&#xFF0C;&#x6240;&#x4EE5;&#x8FD9;&#x91CC;&#x9884;&#x5206;&#x914D;&#x7684;&#x7A7A;&#x95F4;&#x5927;&#x5C0F;&#x662F;nfeatures*10
        vToDistributeKeys.reserve(nfeatures*10);

        //&#x8BA1;&#x7B97;&#x8FDB;&#x884C;&#x7279;&#x5F81;&#x70B9;&#x63D0;&#x53D6;&#x7684;&#x56FE;&#x50CF;&#x533A;&#x57DF;&#x5C3A;&#x5BF8;
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);

        //&#x8BA1;&#x7B97;&#x7F51;&#x683C;&#x5728;&#x5F53;&#x524D;&#x5C42;&#x7684;&#x56FE;&#x50CF;&#x6709;&#x7684;&#x884C;&#x6570;&#x548C;&#x5217;&#x6570;
        const int nCols = width/W;
        const int nRows = height/W;
        //&#x8BA1;&#x7B97;&#x6BCF;&#x4E2A;&#x56FE;&#x50CF;&#x7F51;&#x683C;&#x6240;&#x5360;&#x7684;&#x50CF;&#x7D20;&#x884C;&#x6570;&#x548C;&#x5217;&#x6570;
        const int wCell = ceil(width/nCols);
        const int hCell = ceil(height/nRows);

        //&#x5F00;&#x59CB;&#x904D;&#x5386;&#x56FE;&#x50CF;&#x7F51;&#x683C;&#xFF0C;&#x8FD8;&#x662F;&#x4EE5;&#x884C;&#x5F00;&#x59CB;&#x904D;&#x5386;&#x7684;
        for(int i=0; i<nrows; i++) { 计算当前网格初始行坐标 const float iniy="minBorderY+i*hCell;" 计算当前网格最大的行坐标,这里的+6="+3+3&#xFF0C;&#x5373;&#x8003;&#x8651;&#x5230;&#x4E86;&#x591A;&#x51FA;&#x6765;3&#x662F;&#x4E3A;&#x4E86;cell&#x8FB9;&#x754C;&#x50CF;&#x7D20;&#x8FDB;&#x884C;FAST&#x7279;&#x5F81;&#x70B9;&#x63D0;&#x53D6;&#x7528;" 前面的edge_threshold指的应该是提取后的特征点所在的边界,所以minbordery是考虑了计算半径时候的图像边界 目测一个图像网格的大小是25*25啊 maxy="iniY+hCell+6;" 如果初始的行坐标就已经超过了有效的图像边界了,这里的"有效图像"是指原始的、可以提取fast特征点的图像区域 if(iniy>=maxBorderY-3)
                //&#x90A3;&#x4E48;&#x5C31;&#x8DF3;&#x8FC7;&#x8FD9;&#x4E00;&#x884C;
                continue;
            //&#x5982;&#x679C;&#x56FE;&#x50CF;&#x7684;&#x5927;&#x5C0F;&#x5BFC;&#x81F4;&#x4E0D;&#x80FD;&#x591F;&#x6B63;&#x597D;&#x5212;&#x5206;&#x51FA;&#x6765;&#x6574;&#x9F50;&#x7684;&#x56FE;&#x50CF;&#x7F51;&#x683C;&#xFF0C;&#x90A3;&#x4E48;&#x5C31;&#x8981;&#x59D4;&#x5C48;&#x6700;&#x540E;&#x4E00;&#x884C;&#x4E86;
            if(maxY>maxBorderY)
                maxY = maxBorderY;

            //&#x5F00;&#x59CB;&#x5217;&#x7684;&#x904D;&#x5386;
            for(int j=0; j<ncols; j++) { 计算初始的列坐标 const float inix="minBorderX+j*wCell;" 计算这列网格的最大列坐标,+6的含义和前面相同 maxx="iniX+wCell+6;" 判断坐标是否在图像中 如果初始的列坐标就已经超过了有效的图像边界了,这里的"有效图像"是指原始的、可以提取fast特征点的图像区域。 并且应该同前面行坐标的边界对应,都为-3 !bug 正确应该是maxborderx-3 if(inix>=maxBorderX-6)
                    continue;
                //&#x5982;&#x679C;&#x6700;&#x5927;&#x5750;&#x6807;&#x8D8A;&#x754C;&#x90A3;&#x4E48;&#x59D4;&#x5C48;&#x4E00;&#x4E0B;
                if(maxX>maxBorderX)
                    maxX = maxBorderX;
</ncols;></nrows;></cv::keypoint>
这里指的应该是FAST角点可以存在的坐标位置范围,其实就是原始图像的坐标范围
注意这里没有提前进行+3的操作,而是在后面计算每个网格的区域的时候使用-3的操作来处理FAST角点半径问题
本质上和前面的思想是一样的
//&#x8BA1;&#x7B97;&#x8FD9;&#x4E2A;&#x5BB9;&#x8BB8;&#x5750;&#x6807;&#x533A;&#x57DF;&#x7684;&#x5BBD;&#x5EA6;&#x548C;&#x9AD8;&#x5EA6;
        const int W = maxBorderX - minBorderX;
        const int H = maxBorderY - minBorderY;
        //&#x540C;&#x65F6;&#x8BA1;&#x7B97;&#x6BCF;&#x4E2A;&#x56FE;&#x50CF;cell&#x7684;&#x5BBD;&#x5EA6;&#x548C;&#x9AD8;&#x5EA6;
        const int cellW = ceil((float)W/levelCols);
        const int cellH = ceil((float)H/levelRows);

        //&#x8BA1;&#x7B97;&#x672C;&#x5C42;&#x56FE;&#x50CF;&#x4E2D;&#x7684;&#x603B;cell&#x4E2A;&#x6570;
        const int nCells = levelRows*levelCols;
        //ceil:&#x8FD4;&#x56DE;&#x5927;&#x4E8E;&#x6216;&#x8005;&#x7B49;&#x4E8E;&#x8868;&#x8FBE;&#x5F0F;&#x7684;&#x6700;&#x5C0F;&#x6574;&#x6570;&#xFF0C;&#x5411;&#x4E0A;&#x53D6;&#x6574;
        //&#x8FD9;&#x91CC;&#x8BA1;&#x7B97;&#x4E86;&#x6BCF;&#x4E2A;cell&#x4E2D;&#x9700;&#x8981;&#x63D0;&#x53D6;&#x51FA;&#x6765;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x6570;&#x91CF;&#xFF0C;&#x7531;&#x4E8E;&#x5B58;&#x5728;&#x5C0F;&#x6570;&#x53D6;&#x6574;&#x95EE;&#x9898;&#xFF0C;&#x6240;&#x4EE5;&#x90FD;&#x662F;&#x5F80;&#x591A;&#x4E86;&#x53D6;&#x6574;
        const int nfeaturesCell = ceil((float)nDesiredFeatures/nCells);

6.2、提取特征点

FAST提取兴趣点, 自适应阈值 并且这个向量存储这个cell中的特征点

//&#x8FD9;&#x4E2A;&#x5411;&#x91CF;&#x5B58;&#x50A8;&#x8FD9;&#x4E2A;cell&#x4E2D;&#x7684;&#x7279;&#x5F81;&#x70B9;
vector<cv::keypoint> vKeysCell;
//&#x8C03;&#x7528;opencv&#x7684;&#x5E93;&#x51FD;&#x6570;&#x6765;&#x68C0;&#x6D4B;FAST&#x89D2;&#x70B9;
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX), //&#x5F85;&#x68C0;&#x6D4B;&#x7684;&#x56FE;&#x50CF;&#xFF0C;&#x8FD9;&#x91CC;&#x5C31;&#x662F;&#x5F53;&#x524D;&#x904D;&#x5386;&#x5230;&#x7684;&#x56FE;&#x50CF;&#x5757;
     vKeysCell,         //&#x5B58;&#x50A8;&#x89D2;&#x70B9;&#x4F4D;&#x7F6E;&#x7684;&#x5BB9;&#x5668;
     iniThFAST,         //&#x68C0;&#x6D4B;&#x9608;&#x503C;
     true);             //&#x4F7F;&#x80FD;&#x975E;&#x6781;&#x5927;&#x503C;&#x6291;&#x5236;

//&#x5982;&#x679C;&#x8FD9;&#x4E2A;&#x56FE;&#x50CF;&#x5757;&#x4E2D;&#x4F7F;&#x7528;&#x9ED8;&#x8BA4;&#x7684;FAST&#x68C0;&#x6D4B;&#x9608;&#x503C;&#x6CA1;&#x6709;&#x80FD;&#x591F;&#x68C0;&#x6D4B;&#x5230;&#x89D2;&#x70B9;
if(vKeysCell.empty())
{
//&#x90A3;&#x4E48;&#x5C31;&#x4F7F;&#x7528;&#x66F4;&#x4F4E;&#x7684;&#x9608;&#x503C;&#x6765;&#x8FDB;&#x884C;&#x91CD;&#x65B0;&#x68C0;&#x6D4B;
  FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),   //&#x5F85;&#x68C0;&#x6D4B;&#x7684;&#x56FE;&#x50CF;
       vKeysCell,       //&#x5B58;&#x50A8;&#x89D2;&#x70B9;&#x4F4D;&#x7F6E;&#x7684;&#x5BB9;&#x5668;
       minThFAST,       //&#x66F4;&#x4F4E;&#x7684;&#x68C0;&#x6D4B;&#x9608;&#x503C;
       true);           //&#x4F7F;&#x80FD;&#x975E;&#x6781;&#x5927;&#x503C;&#x6291;&#x5236;
}
//&#x5F97;&#x5230;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x5750;&#x6807;&#xFF0C;&#x4F9D;&#x65E7;&#x662F;&#x5728;&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x4E0B;&#x6765;&#x8BB2;&#x7684;
        keypoints = DistributeOctTree(vToDistributeKeys,            //&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x63D0;&#x53D6;&#x51FA;&#x6765;&#x7684;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x4E5F;&#x5373;&#x662F;&#x7B49;&#x5F85;&#x5254;&#x9664;&#x7684;&#x7279;&#x5F81;&#x70B9;
                                                                    //NOTICE &#x6CE8;&#x610F;&#x6B64;&#x65F6;&#x7279;&#x5F81;&#x70B9;&#x6240;&#x4F7F;&#x7528;&#x7684;&#x5750;&#x6807;&#x90FD;&#x662F;&#x5728;&#x201C;&#x534A;&#x5F84;&#x6269;&#x5145;&#x56FE;&#x50CF;&#x201D;&#x4E0B;&#x7684;
                                      minBorderX, maxBorderX,       //&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x8FB9;&#x754C;&#xFF0C;&#x800C;&#x8FD9;&#x91CC;&#x7684;&#x5750;&#x6807;&#x5374;&#x90FD;&#x662F;&#x5728;&#x201C;&#x8FB9;&#x7F18;&#x6269;&#x5145;&#x56FE;&#x50CF;&#x201D;&#x4E0B;&#x7684;
                                      minBorderY, maxBorderY,
                                      mnFeaturesPerLevel[level],    //&#x5E0C;&#x671B;&#x4FDD;&#x7559;&#x4E0B;&#x6765;&#x7684;&#x5F53;&#x524D;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x4E2A;&#x6570;
                                      level);                       //&#x5F53;&#x524D;&#x5C42;&#x56FE;&#x50CF;&#x6240;&#x5728;&#x7684;&#x56FE;&#x5C42;
//PATCH_SIZE&#x662F;&#x5BF9;&#x4E8E;&#x5E95;&#x5C42;&#x7684;&#x521D;&#x59CB;&#x56FE;&#x50CF;&#x6765;&#x8BF4;&#x7684;&#xFF0C;&#x73B0;&#x5728;&#x8981;&#x6839;&#x636E;&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x7684;&#x5C3A;&#x5EA6;&#x7F29;&#x653E;&#x500D;&#x6570;&#x8FDB;&#x884C;&#x7F29;&#x653E;&#x5F97;&#x5230;&#x7F29;&#x653E;&#x540E;&#x7684;PATCH&#x5927;&#x5C0F; &#x548C;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65B9;&#x5411;&#x8BA1;&#x7B97;&#x6709;&#x5173;
        const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];

        // Add border to coordinates and scale information
        //&#x83B7;&#x53D6;&#x5254;&#x9664;&#x8FC7;&#x7A0B;&#x540E;&#x4FDD;&#x7559;&#x4E0B;&#x6765;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x6570;&#x76EE;
        const int nkps = keypoints.size();
        //&#x7136;&#x540E;&#x5F00;&#x59CB;&#x904D;&#x5386;&#x8FD9;&#x4E9B;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x6062;&#x590D;&#x5176;&#x5728;&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x56FE;&#x50CF;&#x5750;&#x6807;&#x7CFB;&#x4E0B;&#x7684;&#x5750;&#x6807;
        for(int i=0; i<nkps ; i++) { 对每一个保留下来的特征点,恢复到相对于当前图层"边缘扩充图像下"的坐标系的坐标 keypoints[i].pt.x+="minBorderX;" keypoints[i].pt.y+="minBorderY;" 记录特征点来源的图像金字塔图层 keypoints[i].octave="level;" 记录计算方向的patch,缩放后对应的大小, 又被称作为特征点半径 keypoints[i].size="scaledPatchSize;" } compute orientations 然后计算这些特征点的方向信息,注意这里还是分层计算的 for (int level="0;" < nlevels; ++level) computeorientation(mvimagepyramid[level], 对应的图层的图像 allkeypoints[level], 这个图层中提取并保留下来的特征点容器 umax); 以及patch的横坐标边界 }< code></nkps></cv::keypoint>

6.3、四叉树筛选特征点 : DistributeOctTree()

将提取器节点分成4个子节点,同时也完成图像区域的划分、特征点归属的划分,以及相关标志位的置位

ORB_SLAM2 源码解析 ORB特征提取(二)
void ExtractorNode::DivideNode(ExtractorNode &n1,
                               ExtractorNode &n2,
                               ExtractorNode &n3,
                               ExtractorNode &n4)
{
    //&#x5F97;&#x5230;&#x5F53;&#x524D;&#x63D0;&#x53D6;&#x5668;&#x8282;&#x70B9;&#x6240;&#x5728;&#x56FE;&#x50CF;&#x533A;&#x57DF;&#x7684;&#x4E00;&#x534A;&#x957F;&#x5BBD;&#xFF0C;&#x5F53;&#x7136;&#x7ED3;&#x679C;&#x9700;&#x8981;&#x53D6;&#x6574;
    const int halfX = ceil(static_cast<float>(UR.x-UL.x)/2);
    const int halfY = ceil(static_cast<float>(BR.y-UL.y)/2);

    //Define boundaries of childs
    //&#x4E0B;&#x9762;&#x7684;&#x64CD;&#x4F5C;&#x5927;&#x540C;&#x5C0F;&#x5F02;&#xFF0C;&#x5C06;&#x4E00;&#x4E2A;&#x56FE;&#x50CF;&#x533A;&#x57DF;&#x518D;&#x7EC6;&#x5206;&#x6210;&#x4E3A;&#x56DB;&#x4E2A;&#x5C0F;&#x56FE;&#x50CF;&#x533A;&#x5757;
    //n1 &#x5B58;&#x50A8;&#x5DE6;&#x4E0A;&#x533A;&#x57DF;&#x7684;&#x8FB9;&#x754C;
    n1.UL = UL;
    n1.UR = cv::Point2i(UL.x+halfX,UL.y);
    n1.BL = cv::Point2i(UL.x,UL.y+halfY);
    n1.BR = cv::Point2i(UL.x+halfX,UL.y+halfY);
    //&#x7528;&#x6765;&#x5B58;&#x50A8;&#x5728;&#x8BE5;&#x8282;&#x70B9;&#x5BF9;&#x5E94;&#x7684;&#x56FE;&#x50CF;&#x7F51;&#x683C;&#x4E2D;&#x63D0;&#x53D6;&#x51FA;&#x6765;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x7684;vector
    n1.vKeys.reserve(vKeys.size());

    //n2 &#x5B58;&#x50A8;&#x53F3;&#x4E0A;&#x533A;&#x57DF;&#x7684;&#x8FB9;&#x754C;
    n2.UL = n1.UR;
    n2.UR = UR;
    n2.BL = n1.BR;
    n2.BR = cv::Point2i(UR.x,UL.y+halfY);
    n2.vKeys.reserve(vKeys.size());

    //n3 &#x5B58;&#x50A8;&#x5DE6;&#x4E0B;&#x533A;&#x57DF;&#x7684;&#x8FB9;&#x754C;
    n3.UL = n1.BL;
    n3.UR = n1.BR;
    n3.BL = BL;
    n3.BR = cv::Point2i(n1.BR.x,BL.y);
    n3.vKeys.reserve(vKeys.size());

    //n4 &#x5B58;&#x50A8;&#x53F3;&#x4E0B;&#x533A;&#x57DF;&#x7684;&#x8FB9;&#x754C;
    n4.UL = n3.UR;
    n4.UR = n2.BR;
    n4.BL = n3.BR;
    n4.BR = BR;
    n4.vKeys.reserve(vKeys.size());
 //Associate points to childs
    //&#x904D;&#x5386;&#x5F53;&#x524D;&#x63D0;&#x53D6;&#x5668;&#x8282;&#x70B9;&#x7684;vkeys&#x4E2D;&#x5B58;&#x50A8;&#x7684;&#x7279;&#x5F81;&#x70B9;
    for(size_t i=0;i<vkeys.size();i++) { 获取这个特征点对象 const cv::keypoint &kp="vKeys[i];" 判断这个特征点在当前特征点提取器节点图像的哪个区域,更严格地说是属于那个子图像区块 然后就将这个特征点追加到那个特征点提取器节点的vkeys中 notice bug review 这里也是直接进行比较的,但是特征点的坐标是在"半径扩充图像"坐标系下的,而节点区域的坐标则是在"边缘扩充图像"坐标系下的 if(kp.pt.x<n1.ur.x) if(kp.pt.y<n1.br.y) n1.vkeys.push_back(kp); else n3.vkeys.push_back(kp); } n2.vkeys.push_back(kp); n4.vkeys.push_back(kp); 遍历当前提取器节点的vkeys中存储的特征点 < code></vkeys.size();i++)></float></float>

step1.如果图片的宽度比较宽,就先把分成左右w/h份。一般的640×480的图像开始的时候只有一个 node。

step2.如果node里面的点数>1,把每个node分成四个node,如果node里面的特征点为空,就不要了, 删掉。

step3.新分的node的点数>1,就再分裂成4个node。如此,一直分裂。

step4.终止条件为:node的总数量> [公式] ,或者无法再进行分裂。

step5.然后从每个node里面选择一个质量最好的FAST点

ORB_SLAM2 源码解析 ORB特征提取(二)
 //&#x8FD9;&#x91CC;&#x5224;&#x65AD;&#x662F;&#x5426;&#x6570;&#x76EE;&#x7B49;&#x4E8E;1&#x7684;&#x76EE;&#x7684;&#x662F;&#x786E;&#x5B9A;&#x8FD9;&#x4E2A;&#x8282;&#x70B9;&#x8FD8;&#x80FD;&#x4E0D;&#x80FD;&#x518D;&#x5411;&#x4E0B;&#x8FDB;&#x884C;&#x5206;&#x88C2;
    if(n1.vKeys.size()==1)
        n1.bNoMore = true;
    if(n2.vKeys.size()==1)
        n2.bNoMore = true;
    if(n3.vKeys.size()==1)
        n3.bNoMore = true;
    if(n4.vKeys.size()==1)
        n4.bNoMore = true;

6.4、 最后计算这些特征点的方向信息

//&#x904D;&#x5386;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x7684;&#x6BCF;&#x4E2A;&#x56FE;&#x5C42;
    for (int level = 0; level < nlevels; ++level)
        //&#x8BA1;&#x7B97;&#x8FD9;&#x4E2A;&#x56FE;&#x5C42;&#x6240;&#x6709;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65B9;&#x5411;&#x4FE1;&#x606F;
        computeOrientation(mvImagePyramid[level],   //&#x8FD9;&#x4E2A;&#x56FE;&#x5C42;&#x7684;&#x56FE;&#x50CF;
                           allKeypoints[level],     //&#x8FD9;&#x4E2A;&#x56FE;&#x5C42;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x5BF9;&#x8C61;vector&#x5BB9;&#x5668;
                           umax);                   //patch&#x533A;&#x57DF;&#x7684;&#x8FB9;&#x754C;
}

//&#x6CE8;&#x610F;&#x8FD9;&#x662F;&#x4E00;&#x4E2A;&#x4E0D;&#x5C5E;&#x4E8E;&#x4EFB;&#x4F55;&#x7C7B;&#x7684;&#x5168;&#x5C40;&#x9759;&#x6001;&#x51FD;&#x6570;&#xFF0C;static&#x4FEE;&#x9970;&#x7B26;&#x9650;&#x5B9A;&#x5176;&#x53EA;&#x80FD;&#x591F;&#x88AB;&#x672C;&#x6587;&#x4EF6;&#x4E2D;&#x7684;&#x51FD;&#x6570;&#x8C03;&#x7528;
/**
 * @brief &#x8BA1;&#x7B97;&#x67D0;&#x5C42;&#x91D1;&#x5B57;&#x5854;&#x56FE;&#x50CF;&#x4E0A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x63CF;&#x8FF0;&#x5B50;
 *
 * @param[in] image                 &#x67D0;&#x5C42;&#x91D1;&#x5B57;&#x5854;&#x56FE;&#x50CF;
 * @param[in] keypoints             &#x7279;&#x5F81;&#x70B9;vector&#x5BB9;&#x5668;
 * @param[out] descriptors          &#x63CF;&#x8FF0;&#x5B50;
 * @param[in] pattern               &#x8BA1;&#x7B97;&#x63CF;&#x8FF0;&#x5B50;&#x4F7F;&#x7528;&#x7684;&#x56FA;&#x5B9A;&#x968F;&#x673A;&#x70B9;&#x96C6;
 */
static void computeDescriptors(const Mat& image, vector<keypoint>& keypoints, Mat& descriptors,
                               const vector<point>& pattern)
{
    //&#x6E05;&#x7A7A;&#x4FDD;&#x5B58;&#x63CF;&#x8FF0;&#x5B50;&#x4FE1;&#x606F;&#x7684;&#x5BB9;&#x5668;
    descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);

    //&#x5F00;&#x59CB;&#x904D;&#x5386;&#x7279;&#x5F81;&#x70B9;
    for (size_t i = 0; i < keypoints.size(); i++)
        //&#x8BA1;&#x7B97;&#x8FD9;&#x4E2A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x63CF;&#x8FF0;&#x5B50;
        computeOrbDescriptor(keypoints[i],              //&#x8981;&#x8BA1;&#x7B97;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x7279;&#x5F81;&#x70B9;
                             image,                     //&#x4EE5;&#x53CA;&#x5176;&#x56FE;&#x50CF;
                             &pattern[0],               //&#x968F;&#x673A;&#x70B9;&#x96C6;&#x7684;&#x9996;&#x5730;&#x5740;
                             descriptors.ptr((int)i));  //&#x63D0;&#x53D6;&#x51FA;&#x6765;&#x7684;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x4FDD;&#x5B58;&#x4F4D;&#x7F6E;
}
</point></keypoint>

七、总结

void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<keypoint>& _keypoints,
                      OutputArray _descriptors)
{
    // Step 1 &#x68C0;&#x67E5;&#x56FE;&#x50CF;&#x6709;&#x6548;&#x6027;&#x3002;&#x5982;&#x679C;&#x56FE;&#x50CF;&#x4E3A;&#x7A7A;&#xFF0C;&#x90A3;&#x4E48;&#x5C31;&#x76F4;&#x63A5;&#x8FD4;&#x56DE;
    if(_image.empty())
        return;

    //&#x83B7;&#x53D6;&#x56FE;&#x50CF;&#x7684;&#x5927;&#x5C0F;
    Mat image = _image.getMat();
    //&#x5224;&#x65AD;&#x56FE;&#x50CF;&#x7684;&#x683C;&#x5F0F;&#x662F;&#x5426;&#x6B63;&#x786E;&#xFF0C;&#x8981;&#x6C42;&#x662F;&#x5355;&#x901A;&#x9053;&#x7070;&#x5EA6;&#x503C;
    assert(image.type() == CV_8UC1 );

    // Pre-compute the scale pyramid
    // Step 2 &#x6784;&#x5EFA;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;
    ComputePyramid(image);

    // Step 3 &#x8BA1;&#x7B97;&#x56FE;&#x50CF;&#x7684;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x5E76;&#x4E14;&#x5C06;&#x7279;&#x5F81;&#x70B9;&#x8FDB;&#x884C;&#x5747;&#x5300;&#x5316;&#x3002;&#x5747;&#x5300;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x53EF;&#x4EE5;&#x63D0;&#x9AD8;&#x4F4D;&#x59FF;&#x8BA1;&#x7B97;&#x7CBE;&#x5EA6;
    // &#x5B58;&#x50A8;&#x6240;&#x6709;&#x7684;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x6CE8;&#x610F;&#x6B64;&#x5904;&#x4E3A;&#x4E8C;&#x7EF4;&#x7684;vector&#xFF0C;&#x7B2C;&#x4E00;&#x7EF4;&#x5B58;&#x50A8;&#x7684;&#x662F;&#x91D1;&#x5B57;&#x5854;&#x7684;&#x5C42;&#x6570;&#xFF0C;&#x7B2C;&#x4E8C;&#x7EF4;&#x5B58;&#x50A8;&#x7684;&#x662F;&#x90A3;&#x4E00;&#x5C42;&#x91D1;&#x5B57;&#x5854;&#x56FE;&#x50CF;&#x91CC;&#x63D0;&#x53D6;&#x7684;&#x6240;&#x6709;&#x7279;&#x5F81;&#x70B9;
    vector < vector<keypoint> > allKeypoints;
    //&#x4F7F;&#x7528;&#x56DB;&#x53C9;&#x6811;&#x7684;&#x65B9;&#x5F0F;&#x8BA1;&#x7B97;&#x6BCF;&#x5C42;&#x56FE;&#x50CF;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x5E76;&#x8FDB;&#x884C;&#x5206;&#x914D;
    ComputeKeyPointsOctTree(allKeypoints);

    //&#x4F7F;&#x7528;&#x4F20;&#x7EDF;&#x7684;&#x65B9;&#x6CD5;&#x63D0;&#x53D6;&#x5E76;&#x5E73;&#x5747;&#x5206;&#x914D;&#x56FE;&#x50CF;&#x7684;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x4F5C;&#x8005;&#x5E76;&#x672A;&#x4F7F;&#x7528;
    //ComputeKeyPointsOld(allKeypoints);

    // Step 4 &#x62F7;&#x8D1D;&#x56FE;&#x50CF;&#x63CF;&#x8FF0;&#x5B50;&#x5230;&#x65B0;&#x7684;&#x77E9;&#x9635;descriptors
    Mat descriptors;

    //&#x7EDF;&#x8BA1;&#x6574;&#x4E2A;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x7684;&#x7279;&#x5F81;&#x70B9;
    int nkeypoints = 0;
    //&#x5F00;&#x59CB;&#x904D;&#x5386;&#x6BCF;&#x5C42;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#xFF0C;&#x5E76;&#x4E14;&#x7D2F;&#x52A0;&#x6BCF;&#x5C42;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x4E2A;&#x6570;
    for (int level = 0; level < nlevels; ++level)
        nkeypoints += (int)allKeypoints[level].size();

    //&#x5982;&#x679C;&#x672C;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x6CA1;&#x6709;&#x4EFB;&#x4F55;&#x7684;&#x7279;&#x5F81;&#x70B9;
    if( nkeypoints == 0 )
        //&#x901A;&#x8FC7;&#x8C03;&#x7528;cv::mat&#x7C7B;&#x7684;.realse&#x65B9;&#x6CD5;&#xFF0C;&#x5F3A;&#x5236;&#x6E05;&#x7A7A;&#x77E9;&#x9635;&#x7684;&#x5F15;&#x7528;&#x8BA1;&#x6570;&#xFF0C;&#x8FD9;&#x6837;&#x5C31;&#x53EF;&#x4EE5;&#x5F3A;&#x5236;&#x91CA;&#x653E;&#x77E9;&#x9635;&#x7684;&#x6570;&#x636E;&#x4E86;
        //&#x53C2;&#x8003;[https://blog.csdn.net/giantchen547792075/article/details/9107877]
        _descriptors.release();
    else
    {
        //&#x5982;&#x679C;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x6709;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x90A3;&#x4E48;&#x5C31;&#x521B;&#x5EFA;&#x8FD9;&#x4E2A;&#x5B58;&#x50A8;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x77E9;&#x9635;&#xFF0C;&#x6CE8;&#x610F;&#x8FD9;&#x4E2A;&#x77E9;&#x9635;&#x662F;&#x5B58;&#x50A8;&#x6574;&#x4E2A;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x63CF;&#x8FF0;&#x5B50;&#x7684;
        _descriptors.create(nkeypoints,     //&#x77E9;&#x9635;&#x7684;&#x884C;&#x6570;&#xFF0C;&#x5BF9;&#x5E94;&#x4E3A;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x603B;&#x4E2A;&#x6570;
                            32,             //&#x77E9;&#x9635;&#x7684;&#x5217;&#x6570;&#xFF0C;&#x5BF9;&#x5E94;&#x4E3A;&#x4F7F;&#x7528;32*8=256&#x4F4D;&#x63CF;&#x8FF0;&#x5B50;
                            CV_8U);         //&#x77E9;&#x9635;&#x5143;&#x7D20;&#x7684;&#x683C;&#x5F0F;
        //&#x83B7;&#x53D6;&#x8FD9;&#x4E2A;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x77E9;&#x9635;&#x4FE1;&#x606F;
        // ?&#x4E3A;&#x4EC0;&#x4E48;&#x4E0D;&#x662F;&#x76F4;&#x63A5;&#x5728;&#x53C2;&#x6570;_descriptors&#x4E0A;&#x5BF9;&#x77E9;&#x9635;&#x5185;&#x5BB9;&#x8FDB;&#x884C;&#x4FEE;&#x6539;&#xFF0C;&#x800C;&#x662F;&#x91CD;&#x65B0;&#x65B0;&#x5EFA;&#x4E86;&#x4E00;&#x4E2A;&#x53D8;&#x91CF;&#xFF0C;&#x590D;&#x5236;&#x77E9;&#x9635;&#x540E;&#xFF0C;&#x5728;&#x8FD9;&#x4E2A;&#x65B0;&#x5EFA;&#x53D8;&#x91CF;&#x7684;&#x57FA;&#x7840;&#x4E0A;&#x8FDB;&#x884C;&#x4FEE;&#x6539;&#xFF1F;
        descriptors = _descriptors.getMat();
    }

    //&#x6E05;&#x7A7A;&#x7528;&#x4F5C;&#x8FD4;&#x56DE;&#x7279;&#x5F81;&#x70B9;&#x63D0;&#x53D6;&#x7ED3;&#x679C;&#x7684;vector&#x5BB9;&#x5668;
    _keypoints.clear();
    //&#x5E76;&#x9884;&#x5206;&#x914D;&#x6B63;&#x786E;&#x5927;&#x5C0F;&#x7684;&#x7A7A;&#x95F4;
    _keypoints.reserve(nkeypoints);

    //&#x56E0;&#x4E3A;&#x904D;&#x5386;&#x662F;&#x4E00;&#x5C42;&#x4E00;&#x5C42;&#x8FDB;&#x884C;&#x7684;&#xFF0C;&#x4F46;&#x662F;&#x63CF;&#x8FF0;&#x5B50;&#x90A3;&#x4E2A;&#x77E9;&#x9635;&#x662F;&#x5B58;&#x50A8;&#x6574;&#x4E2A;&#x56FE;&#x50CF;&#x91D1;&#x5B57;&#x5854;&#x4E2D;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x63CF;&#x8FF0;&#x5B50;&#xFF0C;&#x6240;&#x4EE5;&#x5728;&#x8FD9;&#x91CC;&#x8BBE;&#x7F6E;&#x4E86;Offset&#x53D8;&#x91CF;&#x6765;&#x4FDD;&#x5B58;&#x201C;&#x5BFB;&#x5740;&#x201D;&#x65F6;&#x7684;&#x504F;&#x79FB;&#x91CF;&#xFF0C;
    //&#x8F85;&#x52A9;&#x8FDB;&#x884C;&#x5728;&#x603B;&#x63CF;&#x8FF0;&#x5B50;mat&#x4E2D;&#x7684;&#x5B9A;&#x4F4D;
    int offset = 0;
    //&#x5F00;&#x59CB;&#x904D;&#x5386;&#x6BCF;&#x4E00;&#x5C42;&#x56FE;&#x50CF;
    for (int level = 0; level < nlevels; ++level)
    {
        //&#x83B7;&#x53D6;&#x5728;allKeypoints&#x4E2D;&#x5F53;&#x524D;&#x5C42;&#x7279;&#x5F81;&#x70B9;&#x5BB9;&#x5668;&#x7684;&#x53E5;&#x67C4;
        vector<keypoint>& keypoints = allKeypoints[level];
        //&#x672C;&#x5C42;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x6570;
        int nkeypointsLevel = (int)keypoints.size();

        //&#x5982;&#x679C;&#x7279;&#x5F81;&#x70B9;&#x6570;&#x76EE;&#x4E3A;0&#xFF0C;&#x8DF3;&#x51FA;&#x672C;&#x6B21;&#x5FAA;&#x73AF;&#xFF0C;&#x7EE7;&#x7EED;&#x4E0B;&#x4E00;&#x5C42;&#x91D1;&#x5B57;&#x5854;
        if(nkeypointsLevel==0)
            continue;

        // preprocess the resized image
        //  Step 5 &#x5BF9;&#x56FE;&#x50CF;&#x8FDB;&#x884C;&#x9AD8;&#x65AF;&#x6A21;&#x7CCA;
        // &#x6DF1;&#x62F7;&#x8D1D;&#x5F53;&#x524D;&#x91D1;&#x5B57;&#x5854;&#x6240;&#x5728;&#x5C42;&#x7EA7;&#x7684;&#x56FE;&#x50CF;
        Mat workingMat = mvImagePyramid[level].clone();

        // &#x6CE8;&#x610F;&#xFF1A;&#x63D0;&#x53D6;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x4F7F;&#x7528;&#x7684;&#x662F;&#x6E05;&#x6670;&#x7684;&#x539F;&#x56FE;&#x50CF;&#xFF1B;&#x8FD9;&#x91CC;&#x8BA1;&#x7B97;&#x63CF;&#x8FF0;&#x5B50;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x4E3A;&#x4E86;&#x907F;&#x514D;&#x56FE;&#x50CF;&#x566A;&#x58F0;&#x7684;&#x5F71;&#x54CD;&#xFF0C;&#x4F7F;&#x7528;&#x4E86;&#x9AD8;&#x65AF;&#x6A21;&#x7CCA;
        GaussianBlur(workingMat,        //&#x6E90;&#x56FE;&#x50CF;
                     workingMat,        //&#x8F93;&#x51FA;&#x56FE;&#x50CF;
                     Size(7, 7),        //&#x9AD8;&#x65AF;&#x6EE4;&#x6CE2;&#x5668;kernel&#x5927;&#x5C0F;&#xFF0C;&#x5FC5;&#x987B;&#x4E3A;&#x6B63;&#x7684;&#x5947;&#x6570;
                     2,                 //&#x9AD8;&#x65AF;&#x6EE4;&#x6CE2;&#x5728;x&#x65B9;&#x5411;&#x7684;&#x6807;&#x51C6;&#x5DEE;
                     2,                 //&#x9AD8;&#x65AF;&#x6EE4;&#x6CE2;&#x5728;y&#x65B9;&#x5411;&#x7684;&#x6807;&#x51C6;&#x5DEE;
                     BORDER_REFLECT_101);//&#x8FB9;&#x7F18;&#x62D3;&#x5C55;&#x70B9;&#x63D2;&#x503C;&#x7C7B;&#x578B;

        // Compute the descriptors &#x8BA1;&#x7B97;&#x63CF;&#x8FF0;&#x5B50;
        // desc&#x5B58;&#x50A8;&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x7684;&#x63CF;&#x8FF0;&#x5B50;
        Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
        // Step 6 &#x8BA1;&#x7B97;&#x9AD8;&#x65AF;&#x6A21;&#x7CCA;&#x540E;&#x56FE;&#x50CF;&#x7684;&#x63CF;&#x8FF0;&#x5B50;
        computeDescriptors(workingMat,  //&#x9AD8;&#x65AF;&#x6A21;&#x7CCA;&#x4E4B;&#x540E;&#x7684;&#x56FE;&#x5C42;&#x56FE;&#x50CF;
                           keypoints,   //&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x4E2D;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x96C6;&#x5408;
                           desc,        //&#x5B58;&#x50A8;&#x8BA1;&#x7B97;&#x4E4B;&#x540E;&#x7684;&#x63CF;&#x8FF0;&#x5B50;
                           pattern);    //&#x968F;&#x673A;&#x91C7;&#x6837;&#x6A21;&#x677F;

        // &#x66F4;&#x65B0;&#x504F;&#x79FB;&#x91CF;&#x7684;&#x503C;
        offset += nkeypointsLevel;

        // Scale keypoint coordinates
        // Step 6 &#x5BF9;&#x975E;&#x7B2C;0&#x5C42;&#x56FE;&#x50CF;&#x4E2D;&#x7684;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x5750;&#x6807;&#x6062;&#x590D;&#x5230;&#x7B2C;0&#x5C42;&#x56FE;&#x50CF;&#xFF08;&#x539F;&#x56FE;&#x50CF;&#xFF09;&#x7684;&#x5750;&#x6807;&#x7CFB;&#x4E0B;
        // ? &#x5F97;&#x5230;&#x6240;&#x6709;&#x5C42;&#x7279;&#x5F81;&#x70B9;&#x5728;&#x7B2C;0&#x5C42;&#x91CC;&#x7684;&#x5750;&#x6807;&#x653E;&#x5230;_keypoints&#x91CC;&#x9762;
        // &#x5BF9;&#x4E8E;&#x7B2C;0&#x5C42;&#x7684;&#x56FE;&#x50CF;&#x7279;&#x5F81;&#x70B9;&#xFF0C;&#x4ED6;&#x4EEC;&#x7684;&#x5750;&#x6807;&#x5C31;&#x4E0D;&#x9700;&#x8981;&#x518D;&#x8FDB;&#x884C;&#x6062;&#x590D;&#x4E86;
        if (level != 0)
        {
            // &#x83B7;&#x53D6;&#x5F53;&#x524D;&#x56FE;&#x5C42;&#x4E0A;&#x7684;&#x7F29;&#x653E;&#x7CFB;&#x6570;
            float scale = mvScaleFactor[level];
            // &#x904D;&#x5386;&#x672C;&#x5C42;&#x6240;&#x6709;&#x7684;&#x7279;&#x5F81;&#x70B9;
            for (vector<keypoint>::iterator keypoint = keypoints.begin(),
                 keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
                // &#x7279;&#x5F81;&#x70B9;&#x672C;&#x8EAB;&#x76F4;&#x63A5;&#x4E58;&#x7F29;&#x653E;&#x500D;&#x6570;&#x5C31;&#x53EF;&#x4EE5;&#x4E86;
                keypoint->pt *= scale;
        }

        // And add the keypoints to the output
        // &#x5C06;keypoints&#x4E2D;&#x5185;&#x5BB9;&#x63D2;&#x5165;&#x5230;_keypoints &#x7684;&#x672B;&#x5C3E;
        // keypoint&#x5176;&#x5B9E;&#x662F;&#x5BF9;allkeypoints&#x4E2D;&#x6BCF;&#x5C42;&#x56FE;&#x50CF;&#x4E2D;&#x7279;&#x5F81;&#x70B9;&#x7684;&#x5F15;&#x7528;&#xFF0C;&#x8FD9;&#x6837;allkeypoints&#x4E2D;&#x7684;&#x6240;&#x6709;&#x7279;&#x5F81;&#x70B9;&#x5728;&#x8FD9;&#x91CC;&#x88AB;&#x8F6C;&#x5B58;&#x5230;&#x8F93;&#x51FA;&#x7684;_keypoints
        _keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
    }
}

</keypoint></keypoint></keypoint></keypoint>

参考文献:

ORB_SLAM2源码解析

Original: https://blog.csdn.net/m0_58173801/article/details/120014453
Author: 小负不负
Title: ORB_SLAM2 源码解析 ORB特征提取(二)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/719362/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球