faster-RCNN 流程详解

本文着重于faster_rcnn_pytorch工程的代码实现层面的流程框架。

pytorch_faster_rcnn

总框图

![](\images\faster-rcnn\Faster R-CNN.png)

1.image pre-processing

在测试或者训练之前,必须图像预处理,并且这些步骤在前向推理和训练时候都必须一致。均值向量($3\times1$)不是图像所有像素的均值,而是一个配置文件中的值,在训练和测试所有的图像上都是一致的。

  1. 读取图片

  2. 减均值,均值是一个固定的元祖

  3. rescale:

    a.$targetSize=600$:图片短边的维度

    b.$maxSize=1000$:图片维度不能超过的最大值

    c.$w=image width,h=image height,minDim=min(w,h),maxDim=max(w,h)$

    $scale=target/minDim$,如果$scale\times maxDim>maxSize,scale=maxSize/maxDim$.

  4. 输出图片

代码:

1
2
3
PIXEL_MEANS = np.array([[[102.9801, 115.9465, 122.7717]]])
SCALES = (600,)
MAX_SIZE = 1000
1
2
3
4
5
6
7
8
9
10
for target_size in self.SCALES:
im_scale = float(target_size) / float(im_size_min)
# Prevent the biggest axis from being more than MAX_SIZE
if np.round(im_scale * im_size_max) > self.MAX_SIZE:
im_scale = float(self.MAX_SIZE) / float(im_size_max)
im = cv2.resize(im_orig, None, None, fx=im_scale, fy=im_scale,interpolation=cv2.INTER_LINEAR)
#im.shape:427*2212*3->193*1000*3最长边变成1000

im_scale_factors.append(im_scale)
processed_ims.append(im)

2.network organization

RCNNs主要包括三种类型的网络:

  1. 特征提取网络
  2. RPN
  3. 分类网络

1.特征提取网络

通常称为head网络,使用预训练模型来初始化前网络的前几层,比如ResNet50这样的网络。

​ 能够使用一个不同问题不同数据集上训练的网络来初始化模型原因是,神经网络可以迁移学习Yosinski, Jason。网络的前几层可以学习到一般化的特征,比如边缘颜色斑点之类,在很多不同的问题上都具有辨识度的特征。后面更深层学习到的就是特定问题更加具体的特征。这些层可以去掉或者在后向传播中fine-tune他们的权重。

​ 【特征提取网络产生的卷积特征图然后被送入到RPN网络中,使用一系列卷积和全连接层来产生可能包含前景的感兴趣区域。这些ROIs然后再在特征图上crop出对应的区域,这些region再被从到分类网络来判断是否包含物体。】

​ ResNet初始化的时候是下面这种方法:

1
2
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))

原因参见论文tips in CNN

2.整体架构

3.training

训练RCNN时候分成了好几层,如下:

  • anchor generation layer: 这一层产生了固定数目的anchor,然后将这些anchor平移到均匀分布的空间格点上。
  • proposal layer :根据Bbox回归的系数来得到变换后的anchor。然后通过NMS来减少anchor。
  • anchor target layer: 这一层的输出就是产生回归的系数来训练RPN的,不用到分类层。给定anchor generation layer生成的一组anchor,这一层负责确定哪些是前景哪些是背景。这一层还会输出一组bbox的regressor。
  • RPNloss:训练RPN时候用到的,包括两部分:分类,回归
  • Proposal Target Layer: 这一层主要是为了修剪由proposal layer产生的一堆anchor,产生特别类别的bounding box回归targets,这样可以用来训练分类层并且产生目标类别和回归目标。

Anchor Generation Layer

这一层产生的一组anchor是各个尺寸和纵横比上的。对于所有图像来说,这些anchor都是一样的。其中一些会包含目标前景但是大部分都不包含。RPN网络的目的就是学会判定哪些box是好的-也就是包含目标前景并且能够产生目标回归系数,并能够用到anchor上使他变成bounding box。

Region Proposal Layer

目标检测方法需要输入一组特征。RCNN使用selective search方法来产生region proposal,在Faster RCNN里面,使用滑动窗来产生候选区域然后利用RPN网络来对region proposal进行打分。这一层两个目的:

  1. 从一系列的anchor中,确定前景anchor和 背景anchor,
  2. 通过一组回归系数来修正位置,宽高,来提高anchor的质量。

这一层包含了3各层,proposal layer,anchor target layer,proposal target layer。

Region Proposal Network

region proposal layer在head network产生的feature map上使用一个卷积层跑,跟着一个relu激活。输出再送入两个$1\times1$的卷积层,产生前景和背景的类别分数和对应的Bbox回归系数,head network的步长赌赢了产生anchor时的步长,所以anchor的数目和region proposal网络产生的anchor是一一对应的。(anchor box数目=class score数目=Bbox 回归系数的数目=$\frac{w}{16}\times \frac{h}{16}\times9$)

Proposal layer

proposal layer 的输入是anchor generation layer产生的anchor box,然后通过前景分数来进行NMS减少anchor的数目。同时也产生了变换后的bounding box通过RPN产生的回归系数。

具体步骤:

  1. Generate proposals from bbox deltas and shifted anchors
  2. clip predicted boxes to image
  3. remove predicted boxes with either height or width < threshold
  4. sort all (proposal, score) pairs by score from highest to lowest
  5. take top pre_nms_topN (e.g. 6000)
  6. apply nms (e.g. threshold = 0.7)
  7. take after_nms_topN (e.g. 300)
  8. return the top proposals (-> RoIs top)

代码:shifted anchor+box delta变成proposal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def bbox_transform_inv(boxes, deltas):#boxes:base anchor,deltas:the predicted offset
if boxes.shape[0] == 0:
return np.zeros((0,), dtype=deltas.dtype)

boxes = boxes.astype(deltas.dtype, copy=False)#62*12*9=6696

widths = boxes[:, 2] - boxes[:, 0] + 1.0#6696-d
heights = boxes[:, 3] - boxes[:, 1] + 1.0#6696-d
ctr_x = boxes[:, 0] + 0.5 * widths
ctr_y = boxes[:, 1] + 0.5 * heights

dx = deltas[:, 0::4]
dy = deltas[:, 1::4]
dw = deltas[:, 2::4]
dh = deltas[:, 3::4]

pred_ctr_x = dx * widths[:, np.newaxis] + ctr_x[:, np.newaxis]
pred_ctr_y = dy * heights[:, np.newaxis] + ctr_y[:, np.newaxis]
pred_w = np.exp(dw) * widths[:, np.newaxis]
pred_h = np.exp(dh) * heights[:, np.newaxis]

pred_boxes = np.zeros(deltas.shape, dtype=deltas.dtype)
# x1
pred_boxes[:, 0::4] = pred_ctr_x - 0.5 * pred_w
# y1
pred_boxes[:, 1::4] = pred_ctr_y - 0.5 * pred_h
# x2
pred_boxes[:, 2::4] = pred_ctr_x + 0.5 * pred_w
# y2
pred_boxes[:, 3::4] = pred_ctr_y + 0.5 * pred_h

return pred_boxes

Anchor target layer

该层的目的是挑选合适的anchor用来训练RPN.

  1. 产生anchors;
  2. 并且给这些anchors分配标签;
  3. 计算anchors与ground truth boxes的偏移量,用于RPN层回归参数的学习。

4.Calculating RPN Loss

RPN层需要学习来给anchor box分类成前景还是背景,并计算回归系数用来你修正位置和宽高。RPN的损失就是基于此计算的。

RPN损失是分类和回归损失之和,
$$
RPNloss=L_{cls}+L_{loc}=L_{cls}+\sum_{u\in all foreground anchors}l_u\=L_{cls}+\\sum_{u\in all foreground anchors}\sum_{i\in x,y,w,h}smooth_{L_1}(u_i(predicted)-u_i(target))
$$
$$
smooth_{L_1}(x)=\begin{cases}\frac{\sigma^2x^2}{2}\qquad ||x||<\frac{1}{\theta^2}\||x||-\frac{0.5}{\sigma^2}\qquad otherwise\end{cases}
$$
$\sigma$取值随意,这里设为3.在python实现中,为前景anchor使用了一mask 数组(bbox_inside_weights),这样就能用向量来计算损失,而不是for-if循环。

所有为了计算损失,我们必须计算出下面几个东西:

  1. class label(background,foreground),anchor box的分数。
  2. foreground target的回归系数

跟着anchor target layer的实现来看这些量是怎么计算的:

首先挑选图像内的anchor box,然后计算和所有anchor box的IOU,通过重叠信息,以下两类会被认为是前景:

  1. 对于每个ground truth box,所有的那些和ground truth box有最大IOU的anchor。
  2. 和一些ground truth的最大IOU超过了阈值

​ anchor和GT的IOU超过了一个阈值的是前景box,这样是为了防止无休止的训练那些离GT太远的anchor的回归系数。低于阈值的被认为是负样本,标记为背景框。既不是前景也不是背景的框框不予考虑。

​ 这一层输入输出参数有以下这些:

  • TRAIN.RPN_POSITIVE_OVERLAP:用来挑选是否为前景的阈值(默认0.7)
  • TRAIN.RPN_POSITIVE_OVERLAP:如果anchor和groundtruth的IOU比阈值低,就是背景。默认0.3
  • TRAIN.RPN_BATCHSIZE:默认的背景和前景anchor的总数,256
  • TRAIN.RPN_FG_FRACTION:batch_size中正样本的比例,默认0.5,如果正样本比例超过0.5,则超过的就是无关的样本。

输入:

  • RPN网络输出(预测的前景/背景的类别标签,回归系数)
  • anchor box,(anchor generation layer产生的)
  • ground truth boxes

输出:

  • Good foreground/background boxes and associated class labels
  • Target regression coefficients

其他层:proposal target layer,ROIPooling layer和分类层是产生计算分类损失所需要的信息。

Calculating Classification Layer Loss

和RPN损失类似,分类层的损失由两部分组成:分类损失和边界框回归损失:

关键的不同是:RPN层处理的是两类—-前景和背景,分类层是处理的所有目标类别(加上背景)。

分类损失包括交叉熵损失:

边界框回归损失和RPN计算的类似,就是现在回归系数和类别有关了。 target regression coefficient只针对正确的类别,也就是和anchor box有着最大重叠面积的GT的目标类别。

计算损失的时候,会有一个mask来标记每个anchor用到的那个类别。对于不正确的类别,回归系数处理的时候直接忽略掉。这个mask使得损失计算直接变成矩阵相乘,不用for循环。

所以,计算这个分类损失需要以下几个量:

  1. 预测的类别标签和边界框回归系数(就是分类网络的输出)
  2. 每个anchor的类别标签
  3. target bounding box regression coefficients

这几个量都是在proposal layer和分类layer里面算的:

Proposal Target Layer

  1. 给PRN提供的proposals分配标签;
  2. 计算proposals和ground truth boxes的偏移量,用于网络最后一层(bbox_pred)回归参数的学习。

这一层的目的是挑选proposal layer输出的ROIs集合中的promising ROIs. 这些ROIs是用来在feature map上进行crop pooling,然后再送入后面的网络进行预测类别分数和边界框回归系数。

和anchor target layer类似,需要选出好的proposal送到分类层,否则,学习任务就是毫无希望的

开始的时候,使用每个ROI和GTbox的最大重叠率来分类前景样本和背景样本。IOU>0.5是前景,0.1<IOU<0.5是背景样本

还要确保前景和背景区域的总数是恒定的。如果找到的背景区域太少,它会尝试通过随机重复一些背景索引来弥补缺口。

然后,边界框回归的目标(targets)是计算每个ROI与其最近的匹配的ground truth之间得到的。(包括背景ROI,因为他们也和GT存在重叠)。这些回归的目标会扩展到每个类上。

bbox_inside_weights数组就像是一个掩模,对每个前景ROI,在正确类别处是1,其他地方是0,背景ROI也是0.因此,当计算分类层损失的bbox回归部分的时候,只会考虑前景region的回归系数。背景ROI属于background 类。

输入:

  • proposal 层产生的ROI
  • ground truth的信息

输出:

  • 挑选出的符合重叠标准的前景和背景ROIs
  • 每个ROIs指定类别的target 回归系数

参数:

  • TRAIN.FG_THRESH:默认0.5.用来挑选前景的ROIs.ROIs和ground truth的最大重叠超过该阈值的时候标记为前景。
  • TRAIN.BG_THRESH_HI:默认0.5
  • TRAIN.BG_THRESH_LO:默认0.1。这两个阈值用来挑选背景ROIs.ROI的最大重叠面积落在这BG_THRESH_HI和BG_THRESH_LO之间的被标记为背景。
  • TRAIN.BATCH_SIZE:默认128.挑选的前景和背景box的总数
  • TRAIN.FG_FRACTION:默认0.25,前景box的数目不能超过BATCH_SIZE*FG_FRACTION

Crop Pooling

proposal target layer产生了promising 的ROIs(最大重叠面积符合标准的)。下一步就是提取出这些ROI在卷积特征图上对应的区域。提取出来的特征图然后送入后半段网络来产生每个ROI的类别概率分布和回归系数。Crop pooling的作用就是执行在卷积特征图上crop对应的区域。

crop pooling的关键思想是论文Spatial Transform Newworks .其目的是应用一个函数($2\times3$的仿射变换矩阵)来变换输入的特征图。如下图所示:

crop pooling包含两步:

  1. 对应一组坐标,应用仿射变换产生一组源坐标。
    $$\bigl[\begin{smallmatrix}{x_i^s}\{y_i^s} \end{smallmatrix}\bigr]=\bigl[\begin{smallmatrix}{\theta_{11}\quad{\theta_{12}}\quad{\theta_{13}}} \{\theta_{21}}\quad{\theta_{22}}\quad{\theta_{23}}\end{smallmatrix}\bigr]\bigl[\begin{smallmatrix}{x_i^t}\{y_i^t}\1\end{smallmatrix}\bigr]$$
    其中$x_i^s,y_i^s,x_i^t,y_i^t​$是经过宽高归一化之后的坐标,即范围在(-1,1)之间。
  2. 第二步,在源坐标上采样输入的特征图来产生输出特征图。每个($x_i^s,y_i^s$)表示输入的空间位置,通过一个采样核来得到输出特征图上的像素值。

pytorch中实现crop pooling的函数:torch.nn.functional.affine_grid,torch.nn.functional.grid_sample 后向传播的梯度可以由Pytroch自己计算。

我们需要做的是:

  1. 把ROI的坐标除以head 网络的步长(16)。因为ROIs的坐标是原始图片的领域的。
  2. 为了使用pytorch的API,需要使用仿射变换矩阵。计算如下。
  3. 还需要在目标特征图上$x,y$维度的点数,配置文件中cfg.POOLING_SIZE(默认7)。

Classification Layer

crop pooling输入proposal target layer产生的ROIbox,和head网络产生的特征图,输出方形的特征图。这些特征图然后被送入ResNet第四层,接着平均池化层。最后的结果(fc7)是一个一维的向量。

这个特征向量然后输入两个全连接层,bbox_pred_net,cls_score_net.分类层使用softmax来产生每个边界框的类别分数。回归层产生特定类别的边界框的回归系数,再和原始的bounding box的坐标结合产生最后的边界框。

RPN网络产生的回归系数和分类网络产生的回归系数的不同:

  1. 第一组是用来训练RPN来产生好的前景边界框(和目标的边界框更加贴合)。那么target回归系数就是用来绑定一个ROIbox和它最近的匹配上的GT边界框(由anchor target layer产生的)。
  2. 第二组是由分类层产生的。这些系数是特定类别的,也就是为每个ROIbox的每个目标类别产生的系数。这些target回归系数是由proposal target layer产生的。

在训练该分类层时,误差梯度也会后向传播到RPN网络。因为在crop pooling中使用的ROI box的坐标是RPN网络自己产生的,因为这些坐标是将RPN产生的回归系数应用到anchor box上去得到的。在后向传播中,误差梯度也会通过crop pooling传播到RPN层。就是faster rcnn中说的训练方法的第三种:非近似训练(考虑了ROI pooling层的梯度传播)

4.Implementation Details: Inference

前向推断的时候不会用到anchor target 和proposal target层。RPN网络已经能够分类前景和背景anchor了,并且能产生好的边界框回归系数。proposal layer可以直接使用这些回归系数应用到分数最高的anchor box上,再使用NMS来去除有很多重叠的box。最后的box再被送入到分类层,产生类别分数和特定类别的边界框回归系数。

5.Appendix

ResNet50

ROI Pooling.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
int roi_pooling_forward(int pooled_height, int pooled_width, float spatial_scale,
THFloatTensor * features, THFloatTensor * rois, THFloatTensor * output)
//features:[1,512,12,62],
//rois:[300,5],
//output:[300,512,7,7],
//argmax[300,512,7,7]
{
// Grab the input tensor
float * data_flat = THFloatTensor_data(features);
float * rois_flat = THFloatTensor_data(rois);

float * output_flat = THFloatTensor_data(output);

// Number of ROIs
int num_rois = THFloatTensor_size(rois, 0);//300
int size_rois = THFloatTensor_size(rois, 1);//5
// batch size
int batch_size = THFloatTensor_size(features, 0);//1
if(batch_size != 1)
{
return 0;
}
// data height
int data_height = THFloatTensor_size(features, 1);//12
// data width
int data_width = THFloatTensor_size(features, 2);//62
// Number of channels
int num_channels = THFloatTensor_size(features, 3);//512

// Set all element of the output tensor to -inf.
THFloatStorage_fill(THFloatTensor_storage(output), -1);

// For each ROI R = [batch_index x1 y1 x2 y2]: max pool over R
int index_roi = 0;
int index_output = 0;
int n;
for (n = 0; n < num_rois; ++n)//0:300
{
int roi_batch_ind = rois_flat[index_roi + 0];//rois[0]获取当前roi属于输入的feature map 中batch中第几张图

//获得roi区域对应的feature map上的区域,这里round是四舍五入求近似,spatial_scale:1/16
int roi_start_w = round(rois_flat[index_roi + 1] * spatial_scale);
int roi_start_h = round(rois_flat[index_roi + 2] * spatial_scale);
int roi_end_w = round(rois_flat[index_roi + 3] * spatial_scale);
int roi_end_h = round(rois_flat[index_roi + 4] * spatial_scale);
// CHECK_GE(roi_batch_ind, 0);
// CHECK_LT(roi_batch_ind, batch_size);

//获取roi区域对应到feature map上大小
int roi_height = fmaxf(roi_end_h - roi_start_h + 1, 1);
int roi_width = fmaxf(roi_end_w - roi_start_w + 1, 1);
//根据获一个roi映射到feature map上的区域的大小和roi-pool后的map
//大小,获取pool的map上一个元素在feature map上的大小
float bin_size_h = (float)(roi_height) / (float)(pooled_height);
float bin_size_w = (float)(roi_width) / (float)(pooled_width);

int index_data = roi_batch_ind * data_height * data_width * num_channels;
const int output_area = pooled_width * pooled_height;

int c, ph, pw;
for (ph = 0; ph < pooled_height; ++ph)
{
for (pw = 0; pw < pooled_width; ++pw)
{
int hstart = (floor((float)(ph) * bin_size_h));
int wstart = (floor((float)(pw) * bin_size_w));
int hend = (ceil((float)(ph + 1) * bin_size_h));
int wend = (ceil((float)(pw + 1) * bin_size_w));

hstart = fminf(fmaxf(hstart + roi_start_h, 0), data_height);
hend = fminf(fmaxf(hend + roi_start_h, 0), data_height);
wstart = fminf(fmaxf(wstart + roi_start_w, 0), data_width);
wend = fminf(fmaxf(wend + roi_start_w, 0), data_width);

const int pool_index = index_output + (ph * pooled_width + pw);
int is_empty = (hend <= hstart) || (wend <= wstart);
if (is_empty)
{
for (c = 0; c < num_channels * output_area; c += output_area)
{
output_flat[pool_index + c] = 0;
}
}
else
{
int h, w, c;
for (h = hstart; h < hend; ++h)
{
for (w = wstart; w < wend; ++w)
{
for (c = 0; c < num_channels; ++c)
{
const int index = (h * data_width + w) * num_channels + c;
if (data_flat[index_data + index] > output_flat[pool_index + c * output_area])
{
output_flat[pool_index + c * output_area] = data_flat[index_data + index];
}
}
}
}
}
}
}

// Increment ROI index
index_roi += size_rois;//5
index_output += pooled_height * pooled_width * num_channels;
}
return 1;
}

6.关于RPN网络和FastRCNN网络正负样本阈值选取的思考

reference

作者对faster rcnn这个两阶段框架的总结:a two-stage cascade consisting of class-agnostic proposals and class-specific detections

1.RPN:

二分类

RPN的正负样本确定的标准是和gt的iou大于0.7为正样本,小于0.3是负样本

首先,图像送入网络前进行了缩放,尺寸大概是$1000\times600$的样子,经过特征提取网络后,步长16,特征图尺寸下降16倍,所以anchor数量$w/16\times h/16\times9=21000$个,数量级为万

通常在proposal阶段,我们必须生成许多ROI。为什么?如果在第一阶段(区域提议)中未检测到对象,则无法在第二阶段对其进行正确分类。这就是为什么这个region proposal有很高的召回率非常重要。这是通过生成大量proposal(例如,每帧几千)来实现的。在检测算法的第二阶段中,大多数将被归类为背景。

选取正负样本是为了计算loss,选取样本过多会导致模型无法收敛,所以使用更加严苛的条件筛选样本,活得更少的样本参与loss计算,比如512个,利用收敛。

"leads to faster optimization and a more stable training"。来自SSD原文。

RPN原文中说达到至多1:1的比例。实际经验中1:3也是常用的技巧了。

2.per-region classifier

多分类与回归

精确训练阶段,rpn网络会生产大约2000个候选区域,这些候选区域不会都拿去训练的,比如有一些实现中,iou值大于0.5的,大概排序后选取32个,而对于负样本是选取iou小于0.5的,且是倒序选取最小的一些,大约96个,基本正负比例在1:3。这样选取后再进行精确的回归和分类。从上面也能看出此阶段的iou设置阈值为0.5是为了能获得更多的roi,毕竟总共才2000,有时iou大于0.5的anchor不够了,还要从排序结果中再选取一些,即使iou小于0.5

Reference

reference