Faster RCNN(2)代码分析

目录

运行代码

代码分析

运行代码

原作者的代码实现py-faster-rcnn,用的框架是caffe,由于对caffe不熟悉,所以在github上找了一个tensorflow版本的代码实现,地址是tf-faster-rcnn

在github上阅读代码之前,肯定是要先读一遍readme,根据作者写的说明将代码运行起来,这样也便于后面在代码中添加log来分析代码。

1.安装环境

下载代码

git clone https://github.com/endernewton/tf-faster-rcnn.git

保证除了tensorflow外还需要cythonopencv-pythoneasydict这三个包。

sudo pip install Cython
sudo pip install opencv-python
sudo pip install easydict

根据自己的电脑配置来修改setup.py

cd tf-faster-rcnn/lib
vim setup.py

比如根据我的电脑是GTX 1080 (Ti),所以修改了-arch为sm_61,具体的型号可以在README中查看。

        extra_compile_args={'gcc': ["-Wno-unused-function"],
                            'nvcc': ['-arch=sm_61',
                                     '--ptxas-options=-v',
                                     '-c',
                                     '--compiler-options',
                                     "'-fPIC'"]},
        include_dirs = [numpy_include, CUDA['include']]

如果我们的训练电脑只有CPU,那么可以把./lib/model/config.py中的__C.USE_GPU_NMS = True改为False。

然后运行make编译出gpu_nms.so和cpu_nms.so,这部分是原作者为nms做GPU加速而设计出来的代码。

2.准备数据

下载VOCdevkit

wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCdevkit_08-Jun-2007.tar

tar xvf VOCtrainval_06-Nov-2007.tar
tar xvf VOCtest_06-Nov-2007.tar
tar xvf VOCdevkit_08-Jun-2007.tar

创建软链接

cd $FRCN_ROOT/data
ln -s $VOCdevkit VOCdevkit2007

3.下载Pre-trained Weights

./data/scripts/fetch_faster_rcnn_models.sh

下载使用Resnet101网络对VOC07+12数据集训练出来的weights。如果无法下载可以试试作者提供的google drive下载。

下载的文件解压后放在data目录下,然后创建软链接

NET=res101
TRAIN_IMDB=voc_2007_trainval+voc_2012_trainval
mkdir -p output/${NET}/${TRAIN_IMDB}
cd output/${NET}/${TRAIN_IMDB}
ln -s ../../../data/voc_2007_trainval+voc_2012_trainval ./default
cd ../../..

我们准备的数据是VOC2007,而下载的weights是根据2007和2012进行训练的,不过我们只是需要将流程跑通,不下载VOC2012关系不大。

4.运行demo和test

运行demo

GPU_ID=0
CUDA_VISIBLE_DEVICES=${GPU_ID} ./tools/demo.py

可以看到对data/demo中的图片都进行了预测。

运行test

GPU_ID=0
./experiments/scripts/test_faster_rcnn.sh $GPU_ID pascal_voc_0712 res101

可以得到对每个类别预测的准确率

Saving cached annotations to /local/share/DeepLearning/stesha/tf-faster-rcnn-master/data/VOCdevkit2007/VOC2007/ImageSets/Main/test.txt_annots.pkl
AP for aeroplane = 0.8300
AP for bicycle = 0.8684
AP for bird = 0.8129
AP for boat = 0.7411
AP for bottle = 0.6853
AP for bus = 0.8764
AP for car = 0.8805
AP for cat = 0.8830
AP for chair = 0.6231
AP for cow = 0.8683
AP for diningtable = 0.7080
AP for dog = 0.8852
AP for horse = 0.8727
AP for motorbike = 0.8297
AP for person = 0.8272
AP for pottedplant = 0.5319
AP for sheep = 0.8115
AP for sofa = 0.7767
AP for train = 0.8461
AP for tvmonitor = 0.7938
Mean AP = 0.7976
~~~~~~~~
Results:
0.830
0.868
0.813
0.741
0.685
0.876
0.880
0.883
0.623
0.868
0.708
0.885
0.873
0.830
0.827
0.532
0.811
0.777
0.846
0.794
0.798
~~~~~~~~

5.训练数据集

基于ImageNet的分类训练的权重来训练Faster RCNN,所以我们需要先下载ImageNet训练权重

mkdir -p data/imagenet_weights
cd data/imagenet_weights
wget -v http://download.tensorflow.org/models/vgg_16_2016_08_28.tar.gz
tar -xzvf vgg_16_2016_08_28.tar.gz
mv vgg_16.ckpt vgg16.ckpt
cd ../..

然后就可以训练了,也可以将数据集替换成自己的数据进行训练。

./experiments/scripts/train_faster_rcnn.sh 0 pascal_voc vgg16

代码分析

模型训练

1.配置和数据准备

当我们运行train_faster_rcnn.sh进行训练时实际上是运行python ./tools/trainval_net.py并且传入了一些参数。在trainval_net.py一开始会打印出所有参数

if __name__ == '__main__':
  args = parse_args()

  print('Called with args:')
  print(args)


output:
Called with args:
Namespace(cfg_file='experiments/cfgs/vgg16.yml', imdb_name='voc_2007_trainval', imdbval_name='voc_2007_test', max_iters=70000, net='vgg16', set_cfgs=['ANCHOR_SCALES', '[8,16,32]', 'ANCHOR_RATIOS', '[0.5,1,2]', 'TRAIN.STEPSIZE', '[50000]'], tag=None, weight='data/imagenet_weights/vgg16.ckpt')

然后读取cfg_file中的配置信息放入config.py,整理set_cfgs中的信息后也放入config.py。常用的配置信息一开始已经放入config.py中了,这步操作相当于是增加了一些网络特有的配置信息。代码如下

  if args.cfg_file is not None:
    cfg_from_file(args.cfg_file)
  if args.set_cfgs is not None:
    cfg_from_list(args.set_cfgs)

接着准备imdb和roidb

  imdb, roidb = combined_roidb(args.imdb_name)
  print('{:d} roidb entries'.format(len(roidb)))

imdb是imdb.py的类对象,便于后面使用imdb提供的方法。

roidb是通过_load_pascal_annotation解析xml文件,获取其中ground truth的boxes,gt_classes,gt_overlaps,flipped,seg_areas信息。

boxes的shape是(len(objs), 4),表示图片中每个元素有一组box信息:xmin,xmax,ymin,ymax

gt_classes的shape是len(objs),图片中每个元素有一个数字表示的类别信息cls

gt_overlaps的shape是(len(objs), num_classes),图片中每个类别有一个对应cls位置为1.0,其他位置都为0的矩阵。

flipped表示是原图还是翻转图True或者False。

seg_areas的shape是len(objs),每个类别有一个矩阵面积。

然后在prepare_roidb函数的调用过程中还会增加一些信息到roidb中:

image表示对应的image的路径

width表示图片的宽

height表示图片的长

max_overlaps表示gt_overlaps每一行最大的值。因为是ground truth的最大值,所以都是1.0,比如一张图片4个物体那么max_overlaps的值为[1. 1. 1. 1.]

max_classes表示gt_overlaps每一行最大值的位置。其实就是一张图片上每个物体的类别,比如一张图片上4个物体,max_classes的值为[15 15 18  9]

下面设置output_dir

  output_dir = get_output_dir(imdb, args.tag)
  print('Output will be saved to `{:s}`'.format(output_dir))
#./tensorboard/vgg16/voc_2007_trainval/default

再设置tensorboard路径

  #tensorboard/vgg16/voc_2007_trainval/default
  tb_dir = get_output_tb_dir(imdb, args.tag)
  print('TensorFlow summaries will be saved to `{:s}`'.format(tb_dir))

用同样的方式取出validation set的valroidb,但是不进行图片的翻转

  orgflip = cfg.TRAIN.USE_FLIPPED
  cfg.TRAIN.USE_FLIPPED = False
  _, valroidb = combined_roidb(args.imdbval_name)
  print('{:d} validation roidb entries'.format(len(valroidb)))
  cfg.TRAIN.USE_FLIPPED = orgflip

准备网络实例,以vgg16网络结构为例

net = vgg16()

最后一步就是训练网络,将前面准备的所有数据都传入了train_net函数。

  train_net(net, imdb, roidb, valroidb, output_dir, tb_dir,
            pretrained_model=args.weight,
            max_iters=args.max_iters)

2.训练网络train_net

对roidb和valroidb进行过滤,就是只保留max_overlaps大于等于0.5(前景),或者小于0.5大于等于0.1(背景)的值。我想对于ground truth的roidb而言,应该只能去掉一些图片中没有任何标记的例子。

  roidb = filter_roidb(roidb)
  valroidb = filter_roidb(valroidb)

创建SolverWrapper对象,将函数的参数都保存在SolverWrapper内部,便于后面调用train_model的时候直接使用。

    sw = SolverWrapper(sess, network, imdb, roidb, valroidb, output_dir, tb_dir,
                       pretrained_model=pretrained_model)

然后进行训练,max_iters是传入的值为70000

sw.train_model(sess, max_iters)

3.train_model

首先对roidb和valroidb初始化RoIDataLayer对象,并且调用_shuffle_roidb_inds打乱db index顺序,比如roidb的长度是10022,所以RoIDataLayer内部存储的self._perm是打乱[0, 1, 2, ......, 10019, 10020, 10021]这个数组的顺序的向量。另外因为cfg.TRAIN.ASPECT_GROUPING为False,所以调用的是np.random.permutation,这个方法和shuffle的区别是会返回一个打乱顺序的数组,但是不会改变原来的数组。

    self.data_layer = RoIDataLayer(self.roidb, self.imdb.num_classes)
    self.data_layer_val = RoIDataLayer(self.valroidb, self.imdb.num_classes, random=True)

接着调用construct_graph函数

    lr, train_op = self.construct_graph(sess)

在这个函数中调用了一个很重要的函数create_architecture,这个函数是搭建网络的核心,现在先跳过,后面重点分析这个函数。另外在construct_graph中取出了'total_loss',设定了learning_rate,指定了optimizer。

再回到train_model函数,接着判断是否有可以restore的snapshot,如果有就恢复最后一个snapshot

    # Find previous snapshots if there is any to restore from
    lsf, nfiles, sfiles = self.find_previous()

    # Initialize the variables or restore them from the last snapshot
    if lsf == 0:
      rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.initialize(sess)
    else:
      rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.restore(sess, 
                                                                            str(sfiles[-1]), 
                                                                            str(nfiles[-1]))

如果调用initialize,其实会将ImageNet中训练的vgg16的checkpoint恢复。恢复参数的代码在vgg16.py中的fix_variables函数中实现。

接着进入训练的循环中,blobs是每次取出来的一个minibatch,因为data_layer中的self._perm是已经打乱过顺序了,所以第一次取minibatch顺序就是乱的。另外因为TRAIN.IMS_PER_BATCH为1,所以我们每次只取一个roidb的数据,对应一张图片。

blobs = self.data_layer.forward()
  def forward(self):
    """Get blobs and copy them into this layer's top blob vector."""
    blobs = self._get_next_minibatch()
    return blobs

具体准备blobs的代码在get_minibatch函数中实现,blobs的内容有三部分:

blobs['data']:是预处理后的图片数据。1.图片减去均值,2.短边压缩到600,如果压缩后长边大于1000,那么长边压缩到1000

blobs['im_info']:三个数字,前两个是压缩后的图片宽高,第三个是图片的压缩比例

blobs['gt_boxes']:shape是(len(objs), 5),5个数字中前4个是boxes信息,第5个是对应obj的cls信息,过滤掉了background这个类别的数据。

然后调用train_step进行训练

        # Compute the graph without summary
        rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, total_loss = \
          self.net.train_step(sess, blobs, train_op)
  def train_step(self, sess, blobs, train_op):
    feed_dict = {self._image: blobs['data'], self._im_info: blobs['im_info'],
                 self._gt_boxes: blobs['gt_boxes']}
    rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss, _ = sess.run([self._losses["rpn_cross_entropy"],
                                                                        self._losses['rpn_loss_box'],
                                                                        self._losses['cross_entropy'],
                                                                        self._losses['loss_box'],
                                                                        self._losses['total_loss'],
                                                                        train_op],
                                                                       feed_dict=feed_dict)
    return rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss

训练是将rpn的loss和fast rcnn的loss相加作为total loss来进行优化的,跟论文中提到的交替训练方式不太一样。不过这样会让训练更加的方便。

4.搭建网络create_architecture

前面跳过了create_architecture,现在再来分析这个函数。

首先创建了三个placeholder,对应我们从minibatch中取出来的blobs的三个信息。因为传入图片的尺寸并不固定,所以self._image的长宽shape设置为None。

    self._image = tf.placeholder(tf.float32, shape=[1, None, None, 3])
    self._im_info = tf.placeholder(tf.float32, shape=[3])
    self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])

然后初始化了一些参数,self._num_anchors表示每个中心点anchor的个数,是9个。

self._num_anchors = self._num_scales * self._num_ratios

然后两个关键的函数就是self._build_networkself._add_losses,将搭建网络的中间参数和创建的loss最后都放入layers_to_output中做为create_architecture函数的返回值。

先看_build_network,在这个函数的实现中也有两个主要的部分,

1.调用_image_to_head,搭建了vgg网络,如果传入的图片尺寸是(1, 600, 800, 3),经过计算后成为(1, 38, 50, 512)

  def _image_to_head(self, is_training, reuse=None):
    with tf.variable_scope(self._scope, self._scope, reuse=reuse):
      net = slim.repeat(self._image, 2, slim.conv2d, 64, [3, 3],
                          trainable=False, scope='conv1')
      net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool1')
      net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3],
                        trainable=False, scope='conv2')
      net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool2')
      net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3],
                        trainable=is_training, scope='conv3')
      net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
      net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
                        trainable=is_training, scope='conv4')
      net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
      net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
                        trainable=is_training, scope='conv5')

    self._act_summaries.append(net)
    self._layers['head'] = net
    
    return net

2.调用self._anchor_component()

  def _anchor_component(self):
    with tf.variable_scope('ANCHOR_' + self._tag) as scope:
      # just to get the shape right
      # 用图片原本的长宽除以16,得到的就是经过vgg运算后的feature map的尺寸
      height = tf.to_int32(tf.ceil(self._im_info[0] / np.float32(self._feat_stride[0])))
      width = tf.to_int32(tf.ceil(self._im_info[1] / np.float32(self._feat_stride[0])))
      if cfg.USE_E2E_TF:
        # 生成了相对原图的所有anchors
        anchors, anchor_length = generate_anchors_pre_tf(
          height,
          width,
          self._feat_stride,
          self._anchor_scales,
          self._anchor_ratios
        )
      ......

关键函数是generate_anchors_pre_tf,这个函数的目的是生成相对原图坐标而言的所有anchors。以feature map的坐标还原到原图上的位置,然后以这些位置为中心点与相对尺寸的9个anchors相加,为每个中心点生成9个anchors。所以anchors的个数为38*50*9=17100,就是论文中说的,feature map的每个像素点对应9个anchors。

def generate_anchors_pre_tf(height, width, feat_stride=16, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
  # 将feature map的横向还原到原图,并且间隔16设置一个点
  shift_x = tf.range(width) * feat_stride # width
  # 将feature map的纵向还原到原图,并且间隔16设置一个点
  shift_y = tf.range(height) * feat_stride # height
  # 将x与y的坐标对应起来,生成原图上横纵都间隔16的网格点坐标sw和sy。
  shift_x, shift_y = tf.meshgrid(shift_x, shift_y)
  sx = tf.reshape(shift_x, shape=(-1,))
  sy = tf.reshape(shift_y, shape=(-1,))
  # sx和sy作为原图框的起始坐标和结束坐标,因为起始坐标和结束坐标相同,所以其实[sx,sy,sx,sy]框是一个面积为0的点
  shifts = tf.transpose(tf.stack([sx, sy, sx, sy]))
  K = tf.multiply(width, height)
  # shifts尺寸为(width*height, 1, 4)
  shifts = tf.transpose(tf.reshape(shifts, shape=[1, K, 4]), perm=(1, 0, 2))

  # 利用anchor_ratios和anchor_scales生成固定比例的框
  # array([[ -83.,  -39.,  100.,   56.],
  #       [-175.,  -87.,  192.,  104.],
  #       [-359., -183.,  376.,  200.],
  #       [ -55.,  -55.,   72.,   72.],
  #       [-119., -119.,  136.,  136.],
  #       [-247., -247.,  264.,  264.],
  #       [ -35.,  -79.,   52.,   96.],
  #       [ -79., -167.,   96.,  184.],
  #       [-167., -343.,  184.,  360.]])
  anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales))
  A = anchors.shape[0]
  # anchor_constant尺寸为(1, 9, 4)
  anchor_constant = tf.constant(anchors.reshape((1, A, 4)), dtype=tf.int32)

  length = K * A
  # (1, 9, 4) + (width*height, 1, 4),按照广播原则相加得到的尺寸为(width*height, 9, 4),然后reshape成(width*height*9, 4),这就是相对原图上的所有anchors
  anchors_tf = tf.reshape(tf.add(anchor_constant, shifts), shape=(length, 4))
  
  return tf.cast(anchors_tf, dtype=tf.float32), length

3.调用self._region_proposal搭建rpn网络

  def _region_proposal(self, net_conv, is_training, initializer):
    rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3], trainable=is_training, weights_initializer=initializer,
                        scope="rpn_conv/3x3")
    self._act_summaries.append(rpn)
    rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training,
                                weights_initializer=initializer,
                                padding='VALID', activation_fn=None, scope='rpn_cls_score')
    # change it so that the score has 2 as its channel size
    rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
    rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
    rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred")
    rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
    rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training,
                                weights_initializer=initializer,
                                padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
    if is_training:
      rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
      # Try to have a deterministic order for the computing graph, for reproducibility
      with tf.control_dependencies([rpn_labels]):
        rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
    ......

    self._predictions["rpn_cls_score"] = rpn_cls_score
    self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape
    self._predictions["rpn_cls_prob"] = rpn_cls_prob
    self._predictions["rpn_cls_pred"] = rpn_cls_pred
    self._predictions["rpn_bbox_pred"] = rpn_bbox_pred
    self._predictions["rois"] = rois

    return rois

以feature map尺寸为(38, 50, 512)为例,下图是这些变量之间的生成关系与尺寸。

里面调用的三个比较重要的函数需要解说一下

# 由于计算出来的rpn_bbox_pred中的dx,dy,dw,dh都是相对anchor的偏移
# 所以这个函数就是相对anchors计算出来pred_boxes的坐标,将超出范围的pred_box进行裁剪
# 然后先取出分数排行前12000的scores和boxes,再用nms取出2000个boxes和对应的scroes
# 剩下proposals的shape是(2000, 4),返回的rois是对proposals第一列增加了一个数字全为0的列
# 所以rois的shape是(2000, 5),roi_scores的shape是(2000,)
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
# 这个函数实际上是调用了anchor_target_layer函数
# anchor_target_layer函数的实现比较复杂,是为了生成四个返回值
# rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights
# 1.所有anchors中超出图片范围的剔除,生成在图片内部的anchor列表,假设是5944个
# 2.创建labels向量,长度为5944,里面的值默认全部设置为-1
# 3.计算每一个anchor与每一个gt_box之间的IOU,假设有4个gt_box,那么生成的overlaps尺寸为(5944, 4)
# 4.每个anchor与4个gt_box之间的IOU取出最大值,如果最大值小于0.3,那么这个anchor对应的label标记为0(负样本)
# 5.每个anchor与4个gt_box之间的IOU取出最大值,如果最大值小于0.7,那么这个anchor对应的label标记为1(正样本)
# 6.为了防止最大值没有大于0.7的情况,取出overlaps中列最大值,如果有相同的最大值也一起取出,对应的label标记为1
# 7. 筛选正负样本,使它们的个数都不超过128个,没有被筛选上的anchor对应的label标记为-1(不关心的值)
# 8. gt_boxes[argmax_overlaps, :]表示取gt_boxes的哪一组值来进行计算,是根据anchor跟哪一个box的IOU最大决定的
# 9. bbox_targets的数据是anchor相对gt_box的dx,dy,dw,dh,是偏移压缩比例。(5944, 4)
# 10. bbox_inside_weights是(5944, 4)的全0矩阵,将label为1的位置的数据改为[1.0,1.0,1.0,1.0]
# 11. bbox_outside_weights是(5944, 4)的全0矩阵,将label为1和为0的位置的数据改为[0.00390625 0.00390625 0.00390625 0.00390625](用1除以正负样本总和算出来的)
# 12. 把长度5944的label还原长度为17100的label,用-1来补新增位置的值
# 13. 把尺寸为(5944, 4)的bbox_targets,还原到尺寸为(17100, 4),用0来补新增位置的值
# 14. bbox_inside_weights和bbox_outside_weights同样操作,尺寸都成为(17100, 4)
# 15. rpn_labels是对labels进行reshape,尺寸为(1, 1, 9*38, 50)
# 16. rpn_bbox_targets是对bbox_targets进行reshape,尺寸为(1, 38, 50, 36)
# 17. rpn_bbox_inside_weights和rpn_bbox_outside_weights同上一样,尺寸为(1, 38, 50, 36)
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
# 这个函数实际上是调用了proposal_target_layer,然后调用_sample_rois
# 因为anchors已经准备了256个正负样本参与计算,而proposal还是2000个,所以要进一步过滤proposal的个数
# 1.overlaps是计算出来的大约2000个proposal与gt_boxes之间的IOU,(2000, 4)
# 2.gt_assignment表示这2000个proposal跟哪个obj的overlap最大,(2000,)
# 3.labels = gt_boxes[gt_assignment, 4],gt_boxes的尺寸是(len(objs), 5),所以labels是每一个proposal对应的object的cls,(2000,)
# 4.fg_inds是最大IOU大于等于0.5的index,bg_inds是最大IOU大于等于0.1小于0.5的index
# 5.调整fg_inds和bg_inds,使他们的个数相加为rois_per_image(256)
# 6.保留labels中的fg_inds和bg_inds,(256,)
# 7.从fg_rois_per_image到结束是背景位置,设置label的值为0,而此时前景的label仍然是cls id
# 8.计算出proposal与gt_box的相对位移dx,dy,dw,dh,放入bbox_target_data并将label列放入第一列的位置,尺寸是(256, 5)
# 9.创建尺寸为(256, 84)的bbox_targets和bbox_inside_weights,84是因为有21个类,每个类留4个位置
# 经过计算后返回的rois尺寸为(256, 5)
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")

4.调用_crop_pool_layer实现RoI Pooling层,因为proposal的尺寸各不相同,如果要送入region calssification会计算出问题,所以这里将他们统一尺寸。原理上是将每个proposal对应到feature map上的位置,然后划分成同样的尺寸比如7*7,这样在49个区域里面进行max pooling,就可以将所有proposal转成7x7的尺寸。最后的返回值是(256, 7, 7, 512)

5.调用_head_to_tail继续构建网络,最后的fc7尺寸为(256, 4096)

  def _head_to_tail(self, pool5, is_training, reuse=None):
    with tf.variable_scope(self._scope, self._scope, reuse=reuse):
      pool5_flat = slim.flatten(pool5, scope='flatten')
      fc6 = slim.fully_connected(pool5_flat, 4096, scope='fc6')
      if is_training:
        fc6 = slim.dropout(fc6, keep_prob=0.5, is_training=True, 
                            scope='dropout6')
      fc7 = slim.fully_connected(fc6, 4096, scope='fc7')
      if is_training:
        fc7 = slim.dropout(fc7, keep_prob=0.5, is_training=True, 
                            scope='dropout7')

    return fc7

6.调用_region_classification搭建proposal的分类网络,cls_score的尺寸为(256, 21),cls_prob的尺寸为(256, 21),cls_pred的尺寸为(256,),bbox_pred的尺寸为(256, 84)

  def _region_classification(self, fc7, is_training, initializer, initializer_bbox):
    cls_score = slim.fully_connected(fc7, self._num_classes, 
                                       weights_initializer=initializer,
                                       trainable=is_training,
                                       activation_fn=None, scope='cls_score')
    cls_prob = self._softmax_layer(cls_score, "cls_prob")
    cls_pred = tf.argmax(cls_score, axis=1, name="cls_pred")
    bbox_pred = slim.fully_connected(fc7, self._num_classes * 4, 
                                     weights_initializer=initializer_bbox,
                                     trainable=is_training,
                                     activation_fn=None, scope='bbox_pred')

    self._predictions["cls_score"] = cls_score
    self._predictions["cls_pred"] = cls_pred
    self._predictions["cls_prob"] = cls_prob
    self._predictions["bbox_pred"] = bbox_pred

    return cls_prob, bbox_pred

至此,_build_network的所有工作就完成了,返回值是rois, cls_prob, bbox_pred,但是还保存了很多中间变量放在了self._predictions中。

再看_add_losses

首先是RPN class loss,RPN的loss都是相对anchors来计算的。

      # 是RPN网络中由feature map通过卷积计算生成的(1, 38, 50, 18)reshape得到的,尺寸是(9*38*50, 2)
      rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2])
      # (9*38*50,)
      rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1])
      # 找出rpn_label中不为-1的部分,-1表示not care的数据
      rpn_select = tf.where(tf.not_equal(rpn_label, -1))
      # 根据rpn_select找出rpn_cls_score的对应位置
      rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2])
      # 根据rpn_select找出rpn_label的对应位置
      rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1])
      # 计算cross entropy loss
      rpn_cross_entropy = tf.reduce_mean(
        tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label))

其次是RPN bbox loss

      # (1, 38, 50, 36)
      rpn_bbox_pred = self._predictions['rpn_bbox_pred']
      # (1, 38, 50, 36)
      rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets']
      # (1, 38, 50, 36)
      rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights']
      # (1, 38, 50, 36)
      rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights']
      # l1 loss
      rpn_loss_box = self._smooth_l1_loss(rpn_bbox_pred, rpn_bbox_targets, rpn_bbox_inside_weights,
                                          rpn_bbox_outside_weights, sigma=sigma_rpn, dim=[1, 2, 3])

接着是RCNN class loss,RCNN的loss都是相对proposal

      # (256, 21)
      cls_score = self._predictions["cls_score"]
      # (256,)
      label = tf.reshape(self._proposal_targets["labels"], [-1])
      # cross entropy loss
      cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=cls_score, labels=label))

最后是RCNN bbox loss

      # (256, 84)
      bbox_pred = self._predictions['bbox_pred']
      # (256, 84)
      bbox_targets = self._proposal_targets['bbox_targets']
      # (256, 84)
      bbox_inside_weights = self._proposal_targets['bbox_inside_weights']
      # (256, 84)
      bbox_outside_weights = self._proposal_targets['bbox_outside_weights']
      # l1 loss
      loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)

total_loss就是将以上4个loss相加,我们要优化的就是total_loss的值。

总结一下RPN和RCNN的loss:

模型预测

1.demo.py

从demo.py入手看如何预测一张图片。

当运行demo.py脚本的时候,会运行main下面的代码。

主要是准备session,调用create_architecture构建网络,restore网络,调用demo预测

2.构建网络

函数create_architecture的实现主体和training时一样,有以下几个区别。

1.在构建网络的时候_region_proposal中只需要调用_proposal_layer,因为我们不需要构建loss,所以training中后面的步骤不需要。

    if is_training:
      rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
      # Try to have a deterministic order for the computing graph, for reproducibility
      with tf.control_dependencies([rpn_labels]):
        rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
    else:
      if cfg.TEST.MODE == 'nms':
        rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      elif cfg.TEST.MODE == 'top':
        rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
      else:
        raise NotImplementedError

另外在_proposal_layer函数中,因为cfg_key为test,所以pre_nms_topN为6000,pos_nms_topN为300,因此我们的proposal有300个。

def proposal_layer(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
  ......
  pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N
  post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N
  nms_thresh = cfg[cfg_key].RPN_NMS_THRESH

2.在create_architecture中需要对预测数据做处理

    if testing:
      stds = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS), (self._num_classes))
      means = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS), (self._num_classes))
      self._predictions["bbox_pred"] *= stds
      self._predictions["bbox_pred"] += means

做这些处理是因为我们对target做过相反的处理

  if cfg.TRAIN.BBOX_NORMALIZE_TARGETS_PRECOMPUTED:
    # Optionally normalize targets by a precomputed mean and stdev
    targets = ((targets - np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS))
               / np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS))

self._predictions["bbox_pred"]的尺寸是(300, 84),就是我们前面选出来的300个proposal。

3.demo函数

im_detect就是进行预测的主要代码,生成的scores shape是(300, 21),boxes的shape是(300, 84)

scores, boxes = im_detect(sess, net, im)

然后对除了__background__以外的每个类别单独分析

    for cls_ind, cls in enumerate(CLASSES[1:]):
        cls_ind += 1 # because we skipped background
        #将对应class的box挑选出来,(300, 4)
        cls_boxes = boxes[:, 4*cls_ind:4*(cls_ind + 1)]
        #将对应class的分数挑选出来,(300, 1)
        cls_scores = scores[:, cls_ind]
        #合并成(300, 5)的数据score放在最后
        dets = np.hstack((cls_boxes,
                          cls_scores[:, np.newaxis])).astype(np.float32)
        #keep表示通过nms挑选出来的index,比如挑选出来30个
        keep = nms(dets, NMS_THRESH)
        #取出挑选出来的dets,(30, 5)
        dets = dets[keep, :]
        #将dets中score大于0.8的框保留下来画在图片上,这样就拿到了bbox和score和class id
        im = vis_detections(im, cls, dets, thresh=CONF_THRESH)

4.im_detect预测

调用net.test_image进行预测

  def test_image(self, sess, image, im_info):
    feed_dict = {self._image: image,
                 self._im_info: im_info}

    cls_score, cls_prob, bbox_pred, rois = sess.run([self._predictions["cls_score"],
                                                     self._predictions['cls_prob'],
                                                     self._predictions['bbox_pred'],
                                                     self._predictions['rois']],
                                                    feed_dict=feed_dict)
    return cls_score, cls_prob, bbox_pred, rois

然后再根据相对坐标计算出真实坐标

  if cfg.TEST.BBOX_REG:
    # Apply bounding-box regression deltas
    box_deltas = bbox_pred
    pred_boxes = bbox_transform_inv(boxes, box_deltas)
    pred_boxes = _clip_boxes(pred_boxes, im.shape)

预测完成。

以上为本文所有内容,感谢阅读,欢迎留言。

猜你喜欢

转载自blog.csdn.net/stesha_chen/article/details/82965283