Faster-RCNN·代码解读系列02:一口气拿下Faster RCNN模型及其设计原理

一口气拿下Faster RCNN模型及其设计原理

5.3 一口气拿下Faster RCNN模型及其设计原理

5.3.1 简介

经过R-CNN和Fast RCNN的积淀,Ross B. Girshick在2016年提出了新的Faster RCNN。最大不同在于:region proposal的提取方式的改变。

Fast R-CNN虽然提出了ROI Pooling的特征提取方式,很好地解决了传统R-CNN中将Region Proposal区域分别输入CNN网络中的弊端。但是!!!始终都是用的传统Selective Search搜索方式确定Region Proposal,训练和测试时消耗了大量时间在RP搜索上。而Faster R-CNN突破性地使用了RPN网络直接提取出RP,并将其融入进整体网络中,使得综合性能有较大提高,在检测速度方面尤为明显。

①学习一个模型的常规路径

测试(Test):首先理清测试逻辑,即模型已经训练好了,扔进去一张图片,是如何一步步生成带有检测框和置信度的检测结果的。

训练(Train):然后再看训练过程,检测和训练过程通常架构都是一样的,训练主要是加入LOSS来学习各个地方的参数,因此关键在于所以这步主要就是搞清LOSS是如何运作的。
在这里插入图片描述

5.3.2 三代对比图

在这里插入图片描述

观察图4,可以发现目标检测的框架中包含4个关键模块,包括region proposal(生成ROI)、feature extraction(特征提取网络)、classification(ROI分类)、regression(ROI回归)。而faster-rcnn利用一个神经网络将这4个模块结合起来,训练一个端到端的网络。通过观察图1、图2、图3,可以得到如下的结论:Faster-rcnn主要包括4个关键模块,特征提取网络、生成ROI、ROI分类、ROI回归。

  1. 特征提取网络:它用来从大量的图片中提取出一些不同目标的重要特征,通常由conv+relu+pool层构成,常用一些预训练好的网络(VGG、Inception、Resnet等),获得的结果叫做特征图;

  2. 生成ROI:在获得的特征图的每一个点上做多个候选ROI(这里是9),然后利用分类器将这些ROI区分为背景和前景,同时利用回归器对这些ROI的位置进行初步的调整;

  3. ROI分类:在RPN阶段,用来区分前景(于真实目标重叠并且其重叠区域大于0.5)和背景(不与任何目标重叠或者其重叠区域小于0.1);在Fast-rcnn阶段,用于区分不同种类的目标(猫、狗、人等);

  4. ROI回归:在RPN阶段,进行初步调整;在Fast-rcnn阶段进行精确调整;

总之,其整体流程如下所示:

①首先对输入的图片进行裁剪操作,并将裁剪后的图片送入预训练好的分类网络中获取该图像对应的特征图;

②在特征图上的每一个锚点上取9个候选的ROI(3个不同尺度,3个不同长宽比),并根据相应的比例将其映射到原始图像中(因为特征提取网络一般有conv和pool组成,但是只有pool会改变特征图的大小,因此最终的特征图大小和pool的个数相关);

③将这些候选的ROI输入到RPN网络中,RPN网络对这些ROI进行分类(即确定这些ROI是前景还是背景)同时对其进行初步回归(即计算这些前景ROI与真实目标之间的BB的偏差值,包括Δx、Δy、Δw、Δh),然后做NMS(非极大值抑制,即根据分类的得分对这些ROI进行排序,然后选择其中的前N个ROI);

④对这些不同大小的ROI进行ROI Pooling操作(即将其映射为特定大小的feature_map,文中是7x7),输出固定大小的feature_map;

⑤最后将其输入简单的检测网络中,然后利用1x1的卷积进行分类(区分不同的类别,N+1类,多余的一类是背景,用于删除不准确的ROI),同时进行BB回归(精确的调整预测的ROI和GT的ROI之间的偏差值),从而输出一个BB集合。

5.3.3 基本概念

5.3.3.1 锚点

即特征图上的最小单位点,比如原始图像的大小是256x256,特征提取网络中含有4个pool层,然后最终获得的特征图的大小为 256/16 x 256/16,即获得一个16x16的特征图,该图中的最小单位即是锚点,由于特征图和原始图像之间存在比例关系,在特征图上面密集的点对应到原始图像上面是有16个像素的间隔,如下图所示:

在这里插入图片描述
图5 原始图像中对应的锚点

5.3.3.2 什么是框回归

5.3.3.2.1 为何要做边框回归?

在这里插入图片描述

如上图所示,绿色的框代表实际飞机的Ground Truth,红色的框代表算法提取的Region Proposal。那么即便红色的框被分类器识别为飞机,但是由于红色的框定位不准(IoU<0.5),那么这张图相当于没有正确的检测出飞机。如果我们能对红色的框进行微调,使得经过微调后的窗口跟Ground Truth更接近,这样岂不是定位会更准确。确实,Bounding-box regression 就是用来微调这个窗口的。

一句话总结:Bounding-box regression 是用来对算法提取的预测框Region Proposal进行微调,使其更加接近于物体的真实标注框Ground Truth。

5.3.3.2.2 什么是边框回归?

在这里插入图片描述
在这里插入图片描述

比较简单的思路就是: 位置平移+尺度放缩

在这里插入图片描述

当输入的Region Proposal 与 Ground Truth 相差较小时(RCNN设置是IOU>0.6),可以认为这种变换是一种线性变换,那么我们就可以使用线性回归来建模对窗口进行微调。

#注意:只有当Proposal和Ground Truth比较接近时(线性问题),我们才能将其作为训练样本训练我们的线性回归模型,
#否则会导致训练的回归模型不work(当Proposal跟GT离得较远,就是复杂的非线性问题了,此时用线性回归建模显然不合理)。
#这个也是G-CNN: an Iterative Grid Based Object Detector多次迭代实现目标准确定位的关键。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.3.2.3 边框回归为什么使用相对坐标差?

CNN具有尺度不变性
在这里插入图片描述

①x,y 坐标除以宽、高的原因:
进行尺度归一化,解决输入图像的尺寸大小不等的问题。 如:上图中两个人的尺度不同,但他都是人。所以,根据CNN具有尺度不变性,可知:我们得到的特征相同。
在这里插入图片描述

②宽、高坐标使用Log形式的原因:
保证缩放尺度的非负性。 要得到一个放缩的尺度,这里必须限制尺度大于0。那么,我们学习的 tw , th 怎么保证满足大于0呢?最直观的想法就是引入EXP函数,如公式(3), (4)所示,那么反过来推导就是Log函数的来源了。
③为什么IoU较大时,可以认为是线性变换?
当输入的 Proposal 与 Ground Truth 相差较小时(RCNN 设置的是 IoU>0.6), 可以认为这种变换是一种线性变换, 那么我们就可以用线性回归来建模对窗口进行微调, 否则会导致训练的回归模型不 work(当 Proposal跟 GT 离得较远,就是复杂的非线性问题了,此时用线性回归建模显然不合理)。
在这里插入图片描述

5.3.3.2 候选的锚点框

针对每一个锚点,然后根据不同的尺度(128、256、512pixel)和不同的长宽比(1:1、0.5:1、1:0.5)产生9个锚点框,如下图所示,对于16x16的特征图,最终产生16x16x9个候选的ROI
在这里插入图片描述

图6 左侧:锚点、中心:特征图空间单一锚点在原图中的表达,右侧:所有锚点在原图中的表达

5.3.3.2 什么是RPN

在这里插入图片描述

RPN(如图3所示) 是用完全卷积的方式高效实现的,用基础网络返回的卷积特征图作为输入。首先,我们使用一个有 512个通道和 3x3 卷积核大小的卷积层,然后我们有两个使用 1x1 卷积核的并行卷积层,其通道数量取决于每个点的锚点数量。

对于分类层,我们对每个锚点输出两个预测值:它是背景(不是目标)的分数,和它是前景(实际的目标)的分数。

对于回归或边框调整层,我们输出四个预测值:Δx、Δy、Δw、Δh我们将会把这些值用到锚点中来得到最终的建议。

1、为何提出RPN?

RPN 全称是Region Proposal Network。Region Proposal的中文意思是“区域选取”,也就是“提取候选框”的意思。RPN 首次在Faster RCNN结构中提出。RPN的目标是代替Selective Search实现候选框的提取。一方面RPN耗时少,另一方面RPN很容易结合到Fast RCNN中,成为一个整体。

所以,RPN的核心功能: 专门用来提取候选框。

2、RPN与Faster RCNN网络的关联

RPN的引入真正意义上把物体检测整个流程融入到一个神经网络中,这个网络结构叫做 Faster RCNN
在这里插入图片描述
图1为 Faster RCNN网络的整体结构示意图
在这里插入图片描述

图2展示了python版本的 VGG16模型中的Faster RCNN的网络结构 ,可以清晰的看到该网络对于任意一张 P × Q 大小的图像,将执行如下操作:
在这里插入图片描述
在这里插入图片描述

3、什么是RPN?

RPN 的总体结构: 1、生成Anchors ⇒ Softmax分类器提取Positvie Anchors ⇒ Bounding Box Reg回归微调Positive Anchors ⇒ Proposal Layer生成proposals。
在这里插入图片描述

最后的Proposal Layer层负责综合positive anchors和对应bounding box regression偏移量获取proposals,同时剔除重叠和超出边界的proposals。其实整个网络到了Proposal Layer这里,就完成了相当于目标定位的功能。

RPN 涉及的几个重要问题:

1、RPN的 input 特征图指的是哪个特征图?
Answert: RPN的输入特征图是指Faster RCNN的公共Feature Map,也称“共享Feature Map”,主要用以RPN和RoI Pooling共享。

2、为什么是用sliding window?文中不是说用CNN么?

我们可以把 3 × 3 3×33×3 的sliding window看作是对特征图做了一次 3 × 3 的卷积操作,最后得到了一个channel数目是256的特征图,尺寸和公共特征图相同,我们假设是256 × ( H × W )

3、256维特征向量如何获得的?

我们可以近似的把这个特征图看作有H × W 个向量,每个向量是256维,那么图中的256维指的就是其中一个向量,然后我们要对每个特征向量做两次全连接操作,一个得到2个分数(前景和背景概率),一个得到4个坐标,由于我们要对每个向量做同样的全连接操作,等同于对整个特征图做两次1 × 1的卷积,得到一个 2 × H × W 和 一个 4 × H × W 大小的特征图,换句话说,有H × W个结果,每个结果包含2个分数和4个坐标;

4、2k和4k中的k指的是什么?
是指由锚点产生的K个框;

5、图右侧不同形状的矩形和Anchors又是如何得到的?

首先我们知道有H x W个结果,我们随机取一点,它跟原图肯定是有映射关系的,由于原图和特征图大小不同,所以特征图上的一个点对应原图肯定是一个框,然而这个框很小,比如说8 x 8,这里8是指原图和特征图的比例,所以这个并不是我们想要的框,那我们不妨把框的左上角或者框的中心作为锚点(Anchor),然后想象出一堆框,具体多少,聪明的读者肯定已经猜到,K个,这也就是图中所说的K anchor boxes(由锚点产生的K个框);

换句话说,H x W个点,每个点对应原图有K个框,那么就有H x W x k个框显示在原图上,那RPN的结果其实就是判断这些框是不是物体以及他们的偏移;那么K个框到底有多大,长宽比是多少?这里是预先设定好的,共有9种组合,所以k等于9,最后我们的结果是针对这9种组合的,所以有H x W x 9个结果,也就是18个分数和36个坐标;
在这里插入图片描述

6、关于256维特征中256由来的理解?

如下图,一个特征图经过sliding window处理,得到256维特征,然后通过两次全连接得到结果2k个分数和4k个坐标;
在这里插入图片描述

Answert: 在原文中使用的是ZFNet网络中,其Conv Layers中最后的conv5层num_output=256,对应生成256张特征图,所以相当于feature map每个点都是256-dimensions。

在conv5之后,做了rpn_conv/3x3卷积且num_output=256,相当于每个点又融合了周围3x3的空间信息(猜测这样做也许更鲁棒?),同时256-d不变

假设在conv5 feature map中每个点上有k个anchor(默认k=9),而每个anhcor要分positive和negative,所以每个点由256d feature转化为cls=2k scores;而每个anchor都有(x, y, w, h)对应4个偏移量,所以reg=4k coordinates。

注意: 如果使用的是VGG网络,最后的 conv5 num_output=512,所以是512d,其他类似。所以,256-d并不是固定不变的,而是随着选取网络结构不同,随之发生改变。

4、总结

其实RPN最终就是在原图尺度上,设置了密密麻麻的候选Anchor。然后用cnn去判断哪些Anchor是里面有目标的positive anchor,哪些是没目标的negative anchor。所以,仅仅是个二分类而已!
那么Anchor一共有多少个?原图800x600,VGG下采样16倍,feature map每个点设置9个Anchor,所以:
在这里插入图片描述

其中ceil()表示向上取整,是因为VGG输出的feature map size= 50*38。
在这里插入图片描述

参考:https://gemini-yang.blog.csdn.net/article/details/110421829

5.3.3.4 什么是RoI Pooling?

ROI pooling是简化版SPP(全称:Spatial Pyramid Pooling),在Fast RCNN和Faster RCNN中使用。

ROI 是Region of Interest的简写,指的是在“特征图上的框” 。具体而言:

① 在SPP Net和Fast RCNN网络中, ROIS 是指Selective Search(选择搜索算法)完成后得到的所有“候选框”在特征图上的映射;

② 在Faster RCNN网络中,候选框是经过RPN算法产生的,然后再把 各个“候选框”映射到特征图上,得到ROIS。
在这里插入图片描述
在这里插入图片描述

问题1、以输入图片为参考坐标的候选框在feature maps上如何映射?
在这里插入图片描述

问题2、如何把形状和大小各异的ROIS归一化为固定大小的目标识别区域?
在这里插入图片描述

RoI Pooling的计算过程

在这里插入图片描述
针对上图,RoI Pooling的实现原理 如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ROI Pooling的作用是通过最大池化操作将特征图上面的ROI固定为特定大小的特征图(7x7),以便进行后续的分类和包围框回归操作。

因此上述存在的问题:由于预选ROI的位置通常是有模型回归得到的,一般来说是浮点数,而赤化后的特征图要求尺度固定,因此ROI Pooling这个操作存在两次数据量化的过程。1)将候选框边界量化为整数点坐标值;2)将量化后的边界区域平均分割成kxk个单元,对每个单元的边界进行量化。事实上,经过上面的两次量化操作,此时的ROI已经和最开始的ROI之间存在一定的偏差,这个偏差会影响检测的精确度。
在这里插入图片描述

下面用直观的例子具体分析一下区域不匹配问题,如图所示,这是一个Faster-rcnn检测框架。输入一张800x800的图片,图片中有一个665x665的BB中框这一只狗。图片经过特征提取网络之后,整个图片的特征图变为800/32 * 800/32,即25x25,但是665/32=20.87,带有小数,ROI Pooling直接将它量化为20。在这里引入了一次偏差。由于最终的特征映射的大小为7x7,即需要将20x20的区域映射成7x7,矩形区域的边长为2.86,又一次将其量化为2。这里再次引入了一次量化误差。经过这两次的量化,候选ROI已经出现了严重的偏差(如图中绿色部分所示)。更重要的是,在特征图上差0.1个像素,对应到原图上就是3.2个像素。

举例说明:Region of interest pooling — example

《Region of interest pooling explained》的原文如下:

附Let’s consider a small example to see how it works. We’re going to perform region of interest pooling on a single 8×8 feature map, one region of interest and an output size of 2×2.
(1)Our input feature map looks like this:
(2)Let’s say we also have a region proposal (top left, bottom right coordinates): (0, 3), (7, 8). In the picture it would look like this:
Normally, there’d be multiple feature maps and multiple proposals for each of them, but we’re keeping things simple for the example.
(3)By dividing it into (2×2) sections (because the output size is 2×2) we get:
Notice that the size of the region of interest doesn’t have to be perfectly divisible by the number of pooling sections (in this case our RoI is 7×5 and we have 2×2 pooling sections).(即:RoI的size不必保证恰好被pooling sections的size整除)
(4)The max values in each of the sections are:
And that’s the output from the Region of Interest pooling layer. Here’s our example presented in form of a nice animation:
What are the most important things to remember about RoI Pooling?
It’s used for object detection tasks
It allows us to reuse the feature map from the convolutional network
It can significantly speed up both train and test time
It allows to train object detection systems in an end-to-end manner

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

5.3.3.5 RoIAlign的提出和原理

1、RoIAlign提出的原因

在这里插入图片描述

2、什么是RoIAlign

在这里插入图片描述

假定采样点数为4,则ROI Align的具体执行步骤如下:

1、假设需要输出的固定尺度的feature map为 2 × 2 ;
2、先将ROI切分成 2 × 2 2×22×2 的单元格;
3、假设采样点数是4,将每个单元格子均分成四个小方格,以每个小方格的中心作为采样点;
4、对采样点像素进行双线性插值,得到该采样点的像素值;
5、对每个单元格内的 4 个采样点进行 max pooling,得到最终的 ROI Align 结果。

3、如何实现RoIAlign

在这里插入图片描述
在这里插入图片描述

综上可知:RoI Align主要解决了RoI pooling在回归框定位时出现的偏差,在对于小目标的精细回归问题上,RoI Align精度更高。所以,在以后的项目中可以根据实际情况进行方案的选择;对于检测图片中大目标物体时,两种方案的差别不大,而如果是图片中有较多小目标物体需要检测,则优先选择RoiAlign。
在这里插入图片描述

4、ROI-Pooling与ROI-Align的异同

ROI-Pooling与ROI-Align的相同点:

RoI pooling与RoI Align都是在ROI生成的feature map上进行下采样,使得固定输出特定尺寸(一般为7 × 7 7×77×7)的新的feature map,再进行后续的分类与框的回归。由于要进行框的回归,所以位置的准确度就尤为重要。

ROI-Pooling与ROI-Align的不同点:
在这里插入图片描述

在Faster RCNN中,步骤二和步骤三要经过量化处理,来对齐特征图坐标。每经过一次量化,就会造成原图目标的像素丢失。

在特征图中,只有整数坐标的位置会有像素值,而浮点坐标没有对应像素值,所以需要向下取整,对齐特征图。
在这里插入图片描述

在Mask RCNN中,步骤二和步骤三不做量化处理,保留浮点数坐标位置不变,经过双线性插值为浮点坐标匹配特征值。

在特征图中,整数坐标和浮点坐标的位置都会有特征值,而浮点坐标的特征值需要通过整数坐标的特征值做双线性插值处理。

上图中,每一个小方格有两层含义:1、特征的坐标;2、表示的特征值

5.3.3.5 ROI Pooling后的检测网络

在这里插入图片描述

如上图所示,在获得7x7x512的特征映射之后,通过Flatten操作将其展开为一维向量,然后经过两个全连接层对其进行分类和回归。一个有 N+1 个单元的全连接层,其中 N 是类的总数,另外一个是背景类。一个有 4N 个单元的全连接层。我们希望有一个回归预测,因此对 N 个类别中的每一个可能的类别,我们都需要 Δx、Δy、Δw、Δh。

5.3.3.6 NMS(非极大值抑制)

由于锚点经常重叠,因此建议最终也会在同一个目标上重叠。为了解决重复建议的问题,我们使用一个简单的算法,称为非极大抑制(NMS)。NMS 获取按照分数排序的建议列表并对已排序的列表进行迭代,丢弃那些 IoU 值大于某个预定义阈值的建议,并提出一个具有更高分数的建议。总之,抑制的过程是一个迭代-遍历-消除的过程。如下图所示:

由于锚点经常重叠,因此建议最终也会在同一个目标上重叠。为了解决重复建议的问题,我们使用一个简单的算法,称为非极大抑制(NMS)。NMS 获取按照分数排序的建议列表并对已排序的列表进行迭代,丢弃那些 IoU 值大于某个预定义阈值的建议,并提出一个具有更高分数的建议。总之,抑制的过程是一个迭代-遍历-消除的过程。如下图所示:
在这里插入图片描述

遍历其余的框,如果它和当前最高得分框的重叠面积大于一定的阈值,我们将其删除
在这里插入图片描述

从没有处理的框中继续选择一个得分最高的,重复上述过程
在这里插入图片描述

5.3.3.6 损失计算组成

①简约流程

(1)同样的用CNN提取输入图像的特征,得到feature map
(2)通过RPN网络从feature map得到候选框ROIs,并对ROIs进行二分类,判别候选框内容是前景还是背景,留下前景的候选框,抛弃背景候选框,并通过回归微调前景的BBox与标注gt接近。
(3)ROI pooling得到等size的feature,再送入多分类器
(4)通过多分类器对候选框进行多分类,即gt有几类,这个就会分成相应的类别
(5)再进行BBox回归

②loss的组成部分:

由上述总体流程,可以得知faster rcnn整体的loss由四个部分组成:

(1)RPN网络进行二分类的loss(候选框是前景 or 背景)
(2)RPN网络前景BBox与gt微调的loss,BBox reg loss
(3)多分类的loss,从RPN网络得到的bbox进行多类别判断的loss
(4)最后每个类别BBox reg微调的loss

5.3.3.7 正负样本划分

①对每个标定的ground true box区域,与其重叠比例最大的anchor记为 正样本 (保证每个ground true 至少对应一个正样本anchor);
②对1)剩余的anchor,如果其与某个标定区域重叠比例大于0.7,记为正样本(每个ground true box可能会对应多个正样本anchor。但每个正样本anchor 只可能对应一个grand true box);如果其与任意一个标定的重叠比例都小于0.3,记为负样本。
③对上面两步剩余的anchor,弃去不用;跨越图像边界的anchor弃去不用。

5.3.2 Faster RCNN模型详解

在这里插入图片描述

如图1,Faster RCNN其实可以分为4个主要内容:

①Conv layers【特征提取网络,用于提取特征】:作为一种CNN网络目标检测方法,Faster RCNN首先使用一组基础conv+relu+pooling层提取image的Feature Map,以被共享用于后续RPN层和全连接层。

②RPN(Region Proposal Network)【区域候选网络】:RPN网络用于生成region proposals。该层通过softmax判断anchors属于positive或者negative(即anchor内是否有目标,二分类),再利用bounding box regression修正anchors获得精确的proposals。因此,RPN网络相当于提前做部分检测,即判断是否有目标(具体什么类别这里不判),以及修正anchor使框的更准一些。

③Roi Pooling【兴趣域池化】,即(SPP net中的空间金字塔池化),用于收集RPN生成的proposals(每个框的坐标),并从①中的feature maps的对应位置提取出来,生成proposals feature maps送入后续全连接层继续做分类(具体是哪一类别)和回归。

④Classification。利用proposals feature maps计算出proposal的具体类别,同时再做一次bounding box regression获得检测框最终的精确位置。

5.3.3 Conv layers [conv+pooling+relu]

在这里插入图片描述

以python版本中的VGG16模型中的faster_rcnn_test.pt的网络结构为例,如图2,Conv layers部分共有13个conv层,13个relu层,4个pooling层。这里有一个非常容易被忽略但是又无比重要的信息,在Conv layers中:

所有的conv层都是:kernel_size=3,pad=1,stride=1
所有的pooling层都是:kernel_size=2,pad=0,stride=2

为何重要?在Faster RCNN Conv layers中对所有的卷积都做了扩边处理( pad=1,即填充一圈0),导致原图大小变为 (M+2)×(N+2),再做3x3卷积后输出M×N 。正是这种设置,导致Conv layers中的conv层不改变输入和输出矩阵大小。如图3:

在这里插入图片描述

类似的是,Conv layers中的pooling层kernel_size=2,stride=2。这样每个经过pooling层的M×N矩阵,都会变为(M/2)×(N/2)大小。综上所述,在整个Conv layers中,conv和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的1/2。

那么,一个MxN大小的矩阵经过Conv layers固定变为(M/16)×(N/16),这样Conv layers生成的feature map中都可以和原图对应起来。

利用卷积公示证明以上观点:

根据卷积和池化公式可得,经过每个conv层后,feature map大小都不变;经过每个pooling层后,feature map的宽高变为之前的一半。(经过relu层也不变)。
在这里插入图片描述

综上,一个M×N大小的图片经过Conv layers之后生成的feature map大小为(M/16)×(N/16)

5.3.4 Region Proposal Networks(RPN)

5.3.4.1 一句话概括Region Proposal Networks(RPN)

Faster-RCNN相对于Fast-RCNN的一个改进是引入了RPN网络,RPN用于区域推荐,替换了此前的SS算法使得网络在整体上更加的CNN化。

RPN先列举出数万个矩形框,然后用卷积+softmax将这些矩形框分为前景和背景两类,同时对矩形框的边界进行回归修正,而这些矩形框就是anchor。

5.3.4.2 RPN流程

(1)RPN网络的输入是CNN得到的feature map,RPN在feature map上用3*3的滑动窗得到对应原图的多个候选框。

feature map上的每一个点,都可以映射到原图的一个区域(即感受野)。对feature map每个点,对应原图的区域,进行不同的形状大小变换,得到不同大小形状的框就是anchor box。这些框的尺寸和大小是预先设置好的。feature map上的每一个点就是原图区域anchoe box的中心点。为了使得覆盖范围更广,检测的物体更多,并保证效率,faster rcnn基于3个不同尺寸大小,3种不同尺度变换的anchor box做变换,对于feature map的每一个点,在原图区域得到9个anchor box。

(2)去掉超出图像边界的anchor box,把所有anchor box送入二分类层,判断每个anchor box是前景还是背景;同时把anchor box送入回归层,调整box与gt接近。

(1)cls和reg层都是全卷积网络,用1*1的卷积核代替全连接

(2)anchor 具有平移不变性

(3)anchor的二分类时,有两个方法判断为正例:与gt有最大的IOU的anchor认为是正例;与gt的IOU大于0.7的anchor认为是正例

(4)对重复的box,用nms进行合并进行筛选。若连接的box1和box2重复IOU大于0.7,看box1与box2哪个二分类正例的得分高,谁高保留谁

5.3.4.1 RPN网络的具体结构

Faster RCNN则抛弃了传统的OpenCV滑动窗口和R-CNN使用SS(Selective Search)方法,直接使用RPN生成检测框,这也是Faster R-CNN的巨大优势,能极大提升检测框的生成速度。

在这里插入图片描述

在图4的RPN网络的具体结构中,到达RPN先做一个卷积(不改变尺寸),再依次经过

第一个功能模块是利用softmax二分类给每个anchor打出前景得分,获得positive和negative分类,并利用回归计算出每个anchor与其对应的GT间的四个微调参数。深度是2×k(anchor数量,一般是9),也就是说每个anchor有两个分类。

第二个功能模块则是计算对于anchors的bounding box regression偏移量,以获得精确的proposal。另一条是4×k,对anchor的位置做调整。

最后的Proposal层则负责综合positive anchors和对应bounding box regression偏移量获取修正后的proposals,同时剔除太小和超出边界的proposals。其实整个网络到了Proposal Layer这里,就完成了相当于目标定位的功能。(只差分具体类别,还有更精准的再次框回归)

其实RPN只有第一个功能模块需要训练,第二个模块都是基本的选择运算,没有参数需要训练,只是为RoiHead选出训练和测试要用到的region proposal。

5.3.4.2 补充:多通道图像卷积的计算方法

  1. 对于单通道图像+单卷积核做卷积,计算方式如下;

    在这里插入图片描述

  2. 对于多通道图像+多卷积核做卷积,计算方式如下:
    在这里插入图片描述
    如上图所示,输入有3个通道,同时有2个卷积核。对于每个卷积核,先在输入3个通道分别作卷积,再将3个通道结果加起来得到卷积输出。所以对于某个卷积层,即输入图像有多少个通道,输出图像通道数总是等于卷积核数量。

对多通道图像做1x1卷积,其实就是将输入图像于每个通道乘以卷积系数后加在一起,即相当于把原图像中本来各个独立的通道“联通”在了一起。

5.3.4.3 anchors

RPN网络中的所谓anchors,实际上就是一组由rpn/generate_anchors.py生成的矩形。

[[ -84.  -40.   99.   55.]
 [-176.  -88.  191.  103.]
 [-360. -184.  375.  199.]
 [ -56.  -56.   71.   71.]
 [-120. -120.  135.  135.]
 [-248. -248.  263.  263.]
 [ -36.  -80.   51.   95.]
 [ -80. -168.   95.  183.]
 [-168. -344.  183.  359.]]

其中每行的4个值 (x1,y1,x2,y2) 表矩形左上和右下角点坐标。9个矩形共有3种形状,长宽比为大约为width:height∈{1:1,1:2,2:1} 三种,如图6。实际上通过anchors就引入了检测中常用到的多尺度方法
在这里插入图片描述

注:anchors size是根据检测图像设置的。在python demo中,会把任意大小的输入图像reshape成800x600(即图2中的M=800,N=600)。再回头来看anchors的大小,anchors中长宽1:2中最大为352x704,长宽2:1中最大736x384,基本是cover掉800x600的各个尺度和形状。如图6所示,在这个feature map上,对于每一个像素点,设置9个预设anchor(作者设置的9个)。这9个anchor的大小按照三种长宽比ratio[1:1,1:2,2:1]设置,具体大小根据输入图像的原始目标大小灵活设置。

那么这9个anchors是做什么的呢?借用Faster RCNN论文中的原图,如图7,遍历Conv layers计算获得的feature maps,为每一个点都配备这9种anchors作为初始的检测框。这样做获得检测框很不准确,不用担心,后面还有2次bounding box regression可以修正检测框位置。
在这里插入图片描述

其实RPN最终就是在原图尺度上,设置了密密麻麻的候选Anchor。然后用cnn去判断哪些Anchor是里面有目标的positive anchor,哪些是没目标的negative anchor。所以,仅仅是个二分类而已!

假设原始图片输入大小是MxN,则RPN的输入feature map大小为(M/16)x(N/16),那么Anchor一共有多少个?原图800x600,VGG下采样16倍,feature map每个点设置9个Anchor,所以:

在这里插入图片描述

其中ceil()表示向上取整,是因为VGG输出的feature map size= 50*38。

设置anchor是为了覆盖图像上各个位置各种大小的目标,那么原图上anchor的数量就是(M/16) x (N/16) x 9。这么多anchor,第一肯定不准确,第二肯定不能要这么多,所以后续还会淘汰一大批以及修正anchor位置。图8可视化后更清晰,这些anchor都会用于后续的分类和回归。
在这里插入图片描述

5.3.4.4 softmax判定positive与negative

参照上面原文中的图来讲,首先,在拿到conv layers的feature map后,先经过一个3x3卷积(卷积核个数为256)红色框是一个anchor,所以通过这个卷积层后feature map的通道数也是256,k是anchor个数(文中默认是9)。

在这里插入图片描述

(M/16)x(N/16)x256的特征通过1x1卷积得到(M/16)x(N/16)x2k的输出,因为这里是二分类判断positive和negative,所以该feature map上每个点的每个anchor对应2个值,表示目标和背景的概率(为什么有2个,是因为这里是用的softmax,这两个值加起来等于1;也可以用sigmoid,就只需要1个值了)

首先,在拿到conv layers的feature map后,先经过一个3x3卷积(卷积核个数为256)红色框是一个anchor,所以通过这个卷积层后feature map的通道数也是256,k是anchor个数(文中默认是9)。

一副M×N大小的矩阵送入Faster RCNN网络后,到RPN网络变为(M/16)×(N/16),不妨设 W=M/16,H=N/16。在进入reshape与softmax之前,先做了1x1卷积,如图9:

在这里插入图片描述

5.3.4.5 bounding box regression原理

如图9所示绿色框为飞机的Ground Truth(GT),红色为提取的positive anchors,即便红色的框被分类器识别为飞机,但是由于红色的框定位不准,这张图相当于没有正确的检测出飞机。所以希望对红色的框进行微调,使得positive anchors和GT更加接近。
在这里插入图片描述

对于窗口一般使用四维向量 (x,y,w,h) 表示,分别表示窗口的中心点坐标和宽高。对于红色的框A代表原始的positive Anchors,绿色的框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的anchor A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即:

  • 给定anchor A=(Ax,Ay,Aw,Ah) 和 GT=[Gx,Gy,Gw,Gh]
  • 寻找一种变换**F,**使得:F(Ax,Ay,Aw,Ah)=(Gx′,Gy′,Gw′,Gh′),其中(Gx′,Gy′,Gw′,Gh′)≈(Gx,Gy,Gw,Gh)

先做平移
在这里插入图片描述

再做缩放
在这里插入图片描述

观察上面4个公式发现,需要学习的是 dx(A),dy(A),dw(A),dh(A) 这四个变换。当输入的anchor A与GT相差较小时,可以认为这种变换是一种线性变换, 那么就可以用线性回归来建模对窗口进行微调(注意,只有当anchors A和GT比较接近时,才能使用线性回归模型,否则就是复杂的非线性问题了)。

接下来的问题就是如何通过线性回归获得 dx(A),dy(A),dw(A),dh(A) 了。线性回归就是给定输入的特征向量X, 学习一组参数W, 使得经过线性回归后的值跟真实值Y非常接近,即Y=WX。对于该问题,输入X是cnn feature map,定义为Φ;同时还有训练传入A与GT之间的变换量,即(tx,ty,tw,th)。输出是dx(A),dy(A),dw(A),dh(A)四个变换。那么目标函数可以表示为:
在这里插入图片描述

其中 ϕ(A) 是对应anchor的feature map组成的特征向量, W是需要学习的参数, d(A) 是得到的预测值(*表示 x,y,w,h,也就是每一个变换对应一个上述目标函数)。为了让预测值 d(A) 与真实值 t差距最小,设计L1损失函数:
在这里插入图片描述

函数优化目标为:
在这里插入图片描述

Tip:为了方便描述,这里以L1损失为例介绍,而真实情况中一般使用soomth-L1损失。需要说明,只有在GT与需要回归框位置比较接近时,才可近似认为上述线性变换成立。

说完原理,对应于Faster RCNN原文,positive anchor与ground truth之间的平移量 (tx,ty) 与尺度因子 (tw,th) 如下:
在这里插入图片描述

对于训练bouding box regression网络回归分支,输入是cnn feature Φ,监督信号是Anchor与GT的差距 (tx,ty,tw,th),即训练目标是:输入 Φ的情况下使网络输出与监督信号尽可能接近。那么当bouding box regression工作时,再输入Φ时,回归网络分支的输出就是每个Anchor的平移量和变换尺度(tx,ty,tw,th),即可用来修正Anchor位置了。

5.3.4.6 对proposals进行bounding box regression(第二个模块)

在这里插入图片描述

(M/16)×(N/16)×256的特征通过1x1卷积得到(M/16)×(N/16)×4k的输出,因为这里是生成每个anchor的坐标偏移量(用于修正anchor),[tx,ty,tw,th] 共4个,所以是4k。注意,这里输出的是坐标偏移量,不是坐标本身,要得到修正后的anchor还要用原坐标和这个偏移量运算一下才行。

偏移值计算公式:
在这里插入图片描述

其中[xa,ya,wa,ha]是anchor的中心点坐标和宽高,[tx.ty,tw,th]是这个回归层预测的偏移量,通过这个公式计算出修正后的anchor坐标[x,y,w,h]。计算如下:
在这里插入图片描述

[px,py,pw,ph]表示原始anchor的坐标
[dx,dy,dw,dh]表示RPN网络预测的坐标偏移
[gx,gy,gw,gh]表示修正后的anchor坐标。

在这里插入图片描述

可能会有的疑问:
1、为什么不直接预测修正后的anchor坐标,而是预测偏移量?
(1)如果直接预测修正后的anchor坐标了,那要这个预设anchor有何用?正是因为预测了偏移量,才能和预设anchor联动起来生成修正后的anchor
(2)直接预测框坐标,数量级比较大,难以训练
(3)坐标偏移一方面大小较小,且偏移具有较好的数学公式,求导方便

5.3.4.7 Proposal Layer

在这里插入图片描述

如上图Proposal层,这是RPN里最后一个步骤,输入有三个:

  • cls层生成的(M/16)×(N/16)×2k向量,positive vs negative anchors分类器结果rpn_cls_prob_reshape
  • reg层生成的(M/16)×(N/16)×4k向量,bbox reg的[ dx(A),dy(A),dw(A),dh(A) ] 变换量rpn_bbox_pred
  • im_info=[M, N,scale_factor]

Proposal Layer综合上述输入,计算出精准的proposal,送入后续RoI Pooling Layer。另外还有参数feat_stride=16,这和下图是对应的。
在这里插入图片描述

首先解释im_info。对于一副任意大小P×Q图像,传入Faster RCNN前,首先reshape到固定M×N,im_info=[M, N, scale_factor]则保存了此次缩放的所有信息。然后经过Conv Layers,经过4次pooling变为W×H=(M/16)×(N/16)大小,其中feature_stride=16则保存了该信息,用于计算anchor偏移量。

在这里插入图片描述

Proposal Layer forward(caffe layer的前传函数)按照以下顺序依次处理:
①生成anchors,利用reg层的偏移量[dx(A),dy(A),dw(A),dh(A)]对所有的anchors做bbox regression回归(这里的anchors生成和训练时完全一致),即对所有的原始anchor进行修正
②按照positive softmax scores由大到小排序所有anchors,取前topN(比如6000个)个anchors,即提取修正位置后的positive anchors
③边界处理,将anchors映射回原图判断是否超出边界,把超出图像边界的positive anchor超出的部分收拢到图像边界处,防止后续RoI pooling时proposals超出边界。
④剔除尺寸非常小的positive anchors
⑤对剩余的positive anchors进行NMS(非极大抑制)
⑥最后输出一堆proposals左上角和右下角坐标值([x1,y1,x2,y2]对应原图MxN尺度),注意,由于在第三步中将anchors映射回原图判断是否超出边界,所以这里输出的proposal是对应M×N输入图像尺度的,这点在后续网络中有用。

RPN网络结构就介绍到这里,总结起来就是:

生成anchors -> softmax分类器提取positvie anchors -> bbox reg回归positive anchors -> Proposal Layer生成proposals

5.3.5 RoI pooling

RoI Pooling层则负责收集proposal,并计算出proposal feature maps(从conv layers后的feature map中扣出对应位置),送入后续网络。

输入有两个:

(1)conv layers提出的原始特征feature map,大小(M/16)x(N/16)
(2)RPN网络生成的Proposals,大小各不相同。一堆坐标([x1,y1,x2,y2])
在这里插入图片描述

5.3.5.1 为何需要RoI Pooling

对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector or matrix。如果输入图像大小不定,这个问题就变得比较麻烦。

有2种解决办法:

①从图像中crop一部分传入网络
②将图像warp成需要的大小后传入网络

在这里插入图片描述

两种办法的示意图如图14,可以看到无论采取那种办法都不好,要么crop后破坏了图像的完整结构,要么warp破坏了图像原始形状信息

RPN网络生成的proposals的方法:对positive anchors进行bounding box regression,那么这样获得的proposals也是大小形状各不相同,即也存在上述问题。所以Faster R-CNN中提出了RoI Pooling解决这个问题。

5.3.5.2 RoI Pooling原理

RoI Pooling layer forward过程:

RoI pooling会有一个预设的pooled_w和pooled_h,表明要把每个proposal特征都统一为这么大的feature map

  • 由于proposal是对应MxN尺度的,所以首先使用spatial_scale参数将其映射回(M/16)×(N/16)大小的feature map尺度;
  • 再将每个proposal对应的feature map区域水平分为 pooled_w × pooled_h 的网格;
  • 对网格的每一份都进行max pooling处理。
  • 这样处理后,即使大小不同的proposal输出结果都是pooled_w × pooled_h固定大小,实现了固定长度输出

这样处理后,即使大小不同的proposal输出结果都是 pooled_w × pooled_h 固定大小,实现了固定长度输出。
在这里插入图片描述

5.3.5.3 举例说明RoI Pooling

roi的输入由两部分:

第一部分是特征图:在fast rcnn中,位于roi pooling之前,在faster rcnn中是与rpn共享那个特征图。
第二部分是rois:在fast rcnn中,指的是selective search的输出,在faster rcnn中指的是rpn的输出,一堆矩形候选框,形状是1 * 5 * 1 * 1.(4个坐标+1个index)。坐标的参考系不是针对feature map这张图的,而是针对原图的(神经网络最开始的输入)。

roi pooling的过程可以详解如下三步:

第一步:根据输入的image,将roi映射到feature map的对应位置。
第二步:将映射后的区域划分为相同大小的sections(sections的数量与输出维度相同)。
第三步:由于输入图片大小不一,在pooling的过程中需要计算pooling后的结果对应到feature map上所占的范围,然后在那个范围上取max或者取average。

举例说明

考虑一个8 * 8的feature map,一个roi,以及输出大小为2 * 2

第一步:下图是一个8 * 8的feature map
在这里插入图片描述

第二步:region proposal投影,投影之后的位置在上图的feature map上表示出来就是下图中的黑框框出的部分。坐标(0,3),(7,8)给出的左下角坐标和右上角坐标
在这里插入图片描述

第三步:把投影之后的区域划分为(2 * 2)个sections,因为输出大小是2 * 2.
在这里插入图片描述

第四步:对每个sections做max pooling,就是取划分后的小区域里的最大值,得到结果如下:
在这里插入图片描述

roi pooling的作用:
(1)用于目标检测任务
(2)允许对cnn中的feature map进行reuse
(3)显著加速训练和测试的速度
(4)允许端到端的形式训练目标检测系统

5.3.6 Classification(多分类)

注意这里的分类和RPN中的分类不同,RPN中只是二分类,区分目标还是背景;这里的分类是要对之前的所有positive anchors识别其具体属于哪一类。

Classification部分网络结构如图16。
在这里插入图片描述

从RoI pooling处获取到pooled_w × pooled_h大小的proposal feature map后,送入后续网络,做两件事:

①利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于那个类别(如人,车,电视等),输出cls_prob概率向量,这实际上已经是识别的范畴了

举例说明:
假设pooled_w和pooled_h都为7,那么这些proposals在经过RoI pooling后的特征向量维度为[7, 7, 256],假设一共输出了300个proposals,那么所有的proposals组合起来维度就是[300,7,7,256],经过最后一个全连接层之后(会有拉平操作),维度应该是[300, 类别数],则该向量就能反应出每个proposal属于每一类的概率有多大。最终就知道每个proposal是属于哪一类,根据proposal索引来找到具体是图上哪个proposal。

②同时对proposals再次利用bounding box regression获得每个proposal的位置偏移量bbox_pred,获取更高精度的最终的predicted box目标检测框。

举例说明:
同上,假设一共输出了300个proposals,回归这里的全连接层输出维度应该是[300, 4],4还是代表偏移量。最终用proposal原始坐标加上偏移量,修正得到最最最终的predicted box结果。

这里来看看全连接层InnerProduct layers,简单的示意图如图17
在这里插入图片描述

其计算公式如下:
在这里插入图片描述

其中W和bias B都是预先训练好的,即大小是固定的,当然输入X和输出Y也就是固定大小。所以,这也就印证了之前Roi Pooling的必要性。

5.3.7 Faster RCNN训练

5.3.7.1 训练过程流程图

Faster RCNN由于是two-stage检测器,训练要分为两个部分进行,一个是训练RPN网络,一个是训练后面的分类网络。为了清晰描述整个训练过程,首先明确如下两个事实:

RPN网络 = 特征提取conv层(下面简称共享conv层) + RPN特有层(3x3卷积、1x1卷积等)
Faster RCNN网络 = 共享conv层 + Faster RCNN特有层(全连接层)
在这里插入图片描述

5.3.7.2 Faster R-CNN的训练步骤

在已经训练好的model(如VGG_CNN_M_1024,VGG,ZF)的基础上继续进行训练。实际中训练过程分为6个步骤:

  1. 先使用ImageNet的预训练权重初始化RPN网络的共享conv层(RPN特有层可随机初始化),然后训练RPN网络。训练完后,共享conv层和RPN特有层的权重都更新了。对应stage1_rpn_train.pt,
  2. 利用第一步中训练好的RPN网络,收集proposals(和测试过程一样),对应rpn_test.pt
  3. 再次使用第一步中ImageNet的预训练权重初始化Faster RCNN网络的贡献conv层(Faster RCNN特有层随机初始化),然后训练Faster RCNN网络。训练完后,共享conv层和Faster RCNN特有层的权重都更新了。
  4. 使用第三步训练好的共享conv层和第一步训练好的RPN特有层来初始化RPN网络,进行RPN网络第二次训练。但这次要把共享conv层的权重固定,训练过程中保持不变,只训练RPN特有层的权重。对应stage2_rpn_train.pt,
  5. 再次利用步骤4中训练好的RPN网络,收集proposals(和测试过程一样),对应rpn_test.pt
  6. 依然使用第三步训练好的共享conv层和第三步训练好的Faster RCNN特有层来初始化Faster RCNN网络,第二次训练Faster RCNN网络。同样,固定conv层,只fine tune特有部分。

可以看到训练过程类似于一种“迭代”的过程,不过只循环了2次。至于只循环了2次的原因是应为作者提到:“A similar alternating training can be run for more iterations, but we have observed negligible improvements”,即循环更多次没有提升了。接下来本章以上述6个步骤讲解训练过程。

5.3.7.3 训练RPN网络

RPN网络训练有两个Loss

  • Lcls:softmax loss,用于分类anchors属于前景还是背景(也有说用二分类交叉熵Loss的)
  • Lreg:smooth L1 loss,用于修正anchor框,前面乘了一个pi*表示只回归有目标的框
    在这里插入图片描述
    在这里插入图片描述

smooth L1 loss如下:
在这里插入图片描述

5.3.7.4 训练Faster RCNN网络

由于两块网络的loss用的是一样的,所以过程一样,只不过这里是多分类向量维度上有所变化,其他一样。

5.3.8 QA

  • 为什么Anchor坐标中有负数

回顾anchor生成步骤:首先生成9个base anchor,然后通过坐标偏移在 50∗38 大小的 1/16 下采样FeatureMap每个点都放上这9个base anchor,就形成了 50∗38∗k 个anhcors。至于这9个base anchor坐标是什么其实并不重要,不同代码实现也许不同。显然这里面有一部分边缘anchors会超出图像边界,而真实中不会有超出图像的目标,所以会有clip anchor步骤。
在这里插入图片描述

  • Anchor到底与网络输出如何对应

VGG输出 50∗38∗512 的特征,对应设置 50∗38∗k 个anchors,而RPN输出 50∗38∗2k 的分类特征矩阵和 50∗38∗4k 的坐标回归特征矩阵。

其实在实现过程中,每个点的 2k 个分类特征与 4k 回归特征,与 k 个anchor逐个对应即可,这实际是一种“人为设置的逻辑映射”。当然,也可以不这样设置,但是无论如何都需要保证在训练和测试过程中映射方式必须一致
在这里插入图片描述

  • 为何有ROI Pooling还要把输入图片resize到固定大小的MxN

由于引入ROI Pooling,从原理上说Faster R-CNN确实能够检测任意大小的图片。但是由于在训练的时候需要使用大batch训练网络,而不同大小输入拼batch在实现的时候代码较为复杂,所以作者选择了把输入图片resize到固定大小的800x600。这应该算是历史遗留问题。

  • 从另一个角度理解Anchor

Faster-RCNN把anchor解释为预选框,后面的分类和定位都是基于anchor来做的,其中定位就是对anchor box做一个线性变换。也可以将anchor box 理解为连接预测框和GT的桥梁。对于每个anchor,有四个代表位置信息的预测值,在yolo v3中这四个值分别是对anchor中心点的偏移量和宽高缩放量。根据anchor坐标和这四个值就可以计算出预测框的位置,然后用预测框和GT做损失计算,这种理解更符合“学习”的思维方式:正向传播、计算损失、反向传播、梯度下降、正向传播。
在这里插入图片描述
以yolo-v1为例,最后输出一个7×7的网络,然后每个网格预测2个结果,注意不管这里预测多少个结果,其分类只有一个。换句话说每个网格只会负责一个分类,至于边界框可以有多个。
所以,对于anchor-free而言,在特征图中,每个网格负责预测出一个分类,而anchor-base的网络,在特征图中每个节点存储的是以该节点为中心的数个子图的特征,这些特征分别进行分类。

本质上来讲:anchor-free根据网格代表的那块图像的特征计算出了分类,而anchor-base则根据周围预设的子图的特征计算分类,而达到这点的手段就是groundtruth的设置规则。

在anchor-free中,物体落到哪个网格,哪个网格就是正样本,其余都是负样本。anchor-base则计算每个anchor和gt的IoU,超过多少阈值就算正。

  • Faster RCNN与SSD的anchor区别在于:
    (1)前者在一个特征图上预设了anchor,先进行初步修正与筛选,之后再进行分类与回归
    (2)后者在多个特征图上预设了anchor(多尺度),并直接在所有这些anchor上进行分类与回归(单阶段)

参考文献:
https://zhuanlan.zhihu.com/p/31426458

https://blog.csdn.net/weixin_43750626/article/details/109722034

https://www.bilibili.com/video/BV1of4y1m7nj

https://blog.csdn.net/weixin_42310154/article/details/119889682

https://blog.csdn.net/program_developer/article/details/80958716

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布尔大学士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值