本篇博客主要讲解faster rcnn的基本结构及相关代码讲解。
总体结构介绍
从上图中我们可以看出,faster rcnn一共有三个部分,我们大致先说下:
1.第一个部分为特征提取部分,经过卷积层得到特征图,也就是feature map
2.第二个部分为RPN(region proposal network)区域候选网络,这是对fast rcnn重点改进的一部分,它的主要作用是得到感兴趣区域(ROI)。该网络会输出两个值,侯选框和分类(一张图像有许多候选框,分类这里分背景和物品也就是前景)
3.分类回归层,这是最终的输出环节,最终输出目标图片预测框和分类
这里可以看到1,3部分为主体结构;第2部分的主要作用是选择候选区域是一个辅助结构。
RPN以及fast-RCNN主体结构均有预测输出,所以整个loss由4个部分组成(RPN的侯选框和分类,fast-RCNN主体结构的侯选框和分类)。需要关注的两点是:
1.两者候选框的回归任务,预测都是误差值,然后用在初始框上的
2.由前面的讲述不难理解,最终的框得到了两次修正,先产生最初的框,RPN修正一次,后面的回归分类任务修正一次。
具体代码及意义讲解
def _build_network(self, is_training=True):
# select initializers
if cfg.TRAIN.TRUNCATED:
initializer = tf.truncated_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.truncated_normal_initializer(mean=0.0, stddev=0.001)
else:
initializer = tf.random_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.random_normal_initializer(mean=0.0, stddev=0.001)
#get feature map
net_conv = self._image_to_head(is_training)
with tf.variable_scope(self._scope, self._scope):
# build the anchors for the image
self._anchor_component()
# region proposal network
rois = self._region_proposal(net_conv, is_training, initializer)
# region of interest pooling
if cfg.POOLING_MODE == 'crop':
pool5 = self._crop_pool_layer(net_conv, rois, "pool5")
else:
raise NotImplementedError
fc7 = self._head_to_tail(pool5, is_training)
with tf.variable_scope(self._scope, self._scope):
# region classification
cls_prob, bbox_pred = self._region_classification(fc7, is_training,
initializer, initializer_bbox)
self._score_summaries.update(self._predictions)
return rois, cls_prob, bbox_pred
上述代码是总体框架:
前面是初始化内容不用看,我们从net_conv = self._image_to_head(is_training)看起。
1.net_conv = self._image_to_head(is_training)。
这其实就是前面的卷积层内容,输出feature map(特征图),一般输出的大小是1*60*40*512([图片个数,高度,宽度,通道数])。
需要提一句的是Faster RCNN首先是支持输入任意大小的图片的,比如输入的P*Q,进入网络之前对图片进行了规整化尺度的设定,如可设定图像短边不超过600,图像长边不超过1000,我们可以假定M*N=1000*600(如果图片少于该尺寸,可以边缘补0,即图像会有黑色边缘)
经过Conv layers,图片大小变成(M/16)*(N/16),即:60*40(1000/16≈60,600/16≈40);则,Feature Map就是60*40*512-d(注:VGG16是512-d,ZF是256-d),表示特征图的大小为60*40,数量为512
2.往下看self._anchor_component()。
这个函数主要产生anchor。这个东西是什么呢?
前面提到经过Conv layers后,图片大小变成了原来的1/16,令feat_stride=16。
然后我们来生成Anchors,首先要认识到特征图(60*40)上的一个点,可以对应到原图(1000*600)上一个16*16大小的区域(点位)(不难想象)。然后我们要对特征图上每一个点生成不同的矩形框。一般来说每个特征图上的点会生成9个矩形框,那么一共既是60*40*9(21600)个anchor box。这些anchor box会被映射到原图中,如下图:
当然特征图上每个点不可能随便生成9个anchor box,它是有一定规则的,根据长宽比和面积大小产生,就如上图一样。我们首先给定一个base anchor大小为[16,16](特征图与原图比例为1:16),我们先看下面积保持不变,长、宽比分别为ratios=[0.5, 1, 2]。产生的Anchors box,如下图:
接下来我们再看下面积大小变化scales=[8, 16, 32]。
综合以上3*3种情况,如图:
特征图每一个点位都照此处理。
详细的代码过程讲解(建议看一下,有助于理解):3.rois = self._region_proposal(net_conv, is_training, initializer)
这里就是实现区域候选网络(RPN)。
rpn如上图feature map经过一个3*3卷积之后,特征图保存不变,然后出现了两个1*1的卷积。
上面1*1卷积是预测类别的(背景还是物体,是一个二分类[0,1],[1,0]表示),特征图大小变成了[1,h(60),w(40),2*9],其实就是对以上60*40*9个anchor进行预测。所以图上的18就是每个点位中9个锚的二分类情况。
下面的1*1卷积是用来预测anchor box与真实框之间的偏移量,特征图大小变成了[1,h(60),w(40),4*9]。这个偏移量样本是如何计算的呢?看以下公式:
anchor box: 中心点位置坐标x_a,y_a和宽高w_a,h_a
ground truth:标定的框也对应一个中心点位置坐标x*,y*和宽高w*,h*
所以,偏移量:
△x=(x*-x_a)/w_a △y=(y*-y_a)/h_a
△w=log(w*/w_a) △h=log(h*/h_a)
通过ground truth box与预测的anchor box之间的差异来进行学习,从而是RPN网络中的权重能够学习到预测box的能力
生成的60*40*9个anchor box,然后累加上训练好的△x, △y, △w, △h,从而得到了相较于之前更加准确的预测框region proposal,进一步对预测框进行越界剔除和使用nms非最大值抑制,剔除掉重叠的框;比如,设定IoU为0.7的阈值,即仅保留覆盖率不超过0.7的局部最大分数的box(粗筛)。最后留下大约2000个anchor,然后再取前N个box(比如300个);这样,进入到下一层ROI Pooling时region proposal大约只有300个
综上所述,这段函数其实就是做分类,回归和筛选,得到proposal。
详情看代码详解
RPN网络
4.pool5 = self._crop_pool_layer(net_conv, rois, “pool5”)
通过以上代码,我们得到了ROI,然后我们就要将ROI映射到feature map上(因为ROI的大小是在原图的基础上)。这个做法就和fast rcnn的做法差不多一致。通过这个方法得到了一个一个特征图。我们详细看下:
我们了解一下它的输入吧:
bottom:特征图,shape=(1,60,40,512)
rois:筛选出来的roi,shape=(N,5)
作用:根据roi得到对应的特征图部分
def _crop_pool_layer(self, bottom, rois, name):
with tf.variable_scope(name) as scope:
#得到索引
batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])
# Get the normalized coordinates of bounding boxes
bottom_shape = tf.shape(bottom)
height = (tf.to_float(bottom_shape[1]) - 1.) * np.float32(self._feat_stride[0])
width = (tf.to_float(bottom_shape[2]) - 1.) * np.float32(self._feat_stride[0])
x1 = tf.slice(rois, [0, 1], [-1, 1], name="x1") / width
y1 = tf.slice(rois, [0, 2], [-1, 1], name="y1") / height
x2 = tf.slice(rois, [0, 3], [-1, 1], name="x2") / width
y2 = tf.slice(rois, [0, 4], [-1, 1], name="y2") / height
#以上其实是在归一化,下面函数的要求,会讲到。
# Won't be back-propagated to rois anyway, but to save time
bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
pre_pool_size = cfg.POOLING_SIZE * 2
crops = tf.image.crop_and_resize(bottom, bboxes, tf.to_int32(batch_ids), [pre_pool_size, pre_pool_size], name="crops")
#可以看下这篇博客
return slim.max_pool2d(crops, [2, 2], padding='SAME')
5.fc7 = self._head_to_tail(pool5, is_training)
cls_prob, bbox_pred =self._region_classification(fc7,is_training, initializer, initializer_bbox)
这里两行代码一起讲,第一行代码将pool5变成fc的结构,之后送入**_region_classification**,得到最终的分类和偏移量。
这个过程就是以下第三部份。