Faster R-CNN

Faster R-CNN

Faster R-CNN 原理简述

在这里插入图片描述
上面就是Faster R-CNN的原理图:

  • 首先搭建一个faster rcnn的基础模型,搭建一个全卷积网络。

     全卷积网络会对原始的image进行maxpooling,vgg16进行2x2x2x2的maxpooling,最后把图片进行1/16倍的缩放。
    

    3.全卷积网络最后一层分为两个通道,(这里使用net称呼最后一层的feature map,程序里就是使用的net)一个net送入RPN进行区域推荐,得到的是box的坐标和坐标的分数(二分类,包不包含有物体)。在得到box之后,box就会在另一个net上进行特征的裁剪,再把所有裁剪下来的feature map进行尺寸归一到固定尺寸这就是所谓的ROIs。

    4.得到所有的rois区域之后,把这些rois区域进行拉平输出到两个地方,一个是进行物体的分类,一个是进行box坐标的回归(相当与在之前的box上进行的微调)。
    得到精确的box坐标和物体类别分数。

    6.记住,记住这里有坑,就是关于训练和非训练是的参数设置是不一样的。在这里提一个醒,就是在进行正样本和负样本训练的时候,这里会和ground truth(box的真实位置)进行对比(就在这里),非训练是没有ground truth,所以这里不进行ground truth的操作,而是直接使用top和nms进行。

    7.注意这里有两个回归和两个分类,第一个回归和分类在RPN网络,进行box的粗糙选取和是否含有物体的二分类,第二的回归和分类在ROIs之后的predication网络,这里的分类是box的精确回归和物体的分类(20分类)。

    (备注: faser rcnn 的原理很简单,但是这里面最最最复杂的是数据的处理,这些数据处理没有训练参数,但是,却占据了90%的代码量)

代码整理

这是我的工程项目文档,在github上下载的,网上的版本太多,我不想一一去看了,本来是入坑Google 的object detection api 的,但还是需要看一看这种稍微简单一点的源码才能理顺思路。

代码地址https://github.com/endernewton/tf-faster-rcnn/blob/a3279943cbe6b880be34b53329a4fe3f971c2c37/lib/layer_utils/proposal_target_layer.py#L18

在这里插入图片描述

代码前言

faster rcnn的整个网络是由一个叫做network.py的文件中的基类Network进行操作,所有的流程被这个叫Network的子类实现,所以,可以通过构建多个Network子类构建多个物体检测的子类了,源码里面有实现了两个子类vgg16和resnet子类。

# vgg16.py
class vgg16(Network):
    def __init__(self, batch_size=1):
        Network.__init__(self, batch_size=batch_size)

# resnetv1.py
class resnetv1(Network):
  def __init__(self, batch_size=1, num_layers=50):
    Network.__init__(self, batch_size=batch_size)
    self._num_layers = num_layers
    self._resnet_scope = 'resnet_v1_%d' % num_layers

开始demo.py和train.py

  • 1.demo.py

构建一个基于vgg16的faster rcnn模型,把训练好的模型参数从cptk文件中回复,输入img进行检测。

  • train.py

构建一个基于vgg16的faster rcnn模型,,把预训练的模型参数从cptk文件中回复,输入img进行检测。
其实在demo.py和train.py中都有这么一句代码:

# demo.py
net.create_architecture(sess, "TEST", 21,
                            tag='default', anchor_scales=[8, 16, 32])

# train.py
layers = self.net.create_architecture(sess, "TRAIN", 
                            self.imdb.num_classes, tag='default')

这就是构建faster rcnn进行计算图的操作。
请记住这个模型在进行模型参数恢复时,train.py和demo.py的不一样,demo.py时把所的参数进行恢复并赋值。而train.py只恢复到fc7,fc7输出时4096,在fc7后面接了两个输出,就是box坐标和classes。如果我们自己的训练数据并不是20或者99,我们在train.py的时候只需要更改num_classes既可以了,fc7后面的层就是合适新的分类任务所需的。

开始分析vgg16():

铺垫这么多,只为了在后面进行分析时候能有个索引。
vgg16这个类对外面调用的类似乎只有少数几个方法。vgg16框架

import tensorflow as tf
import tensorflow.contrib.slim as slim

import lib.config.config as cfg
from lib.nets.network import Network

class vgg16(Network):
    def __init__(self, batch_size=1):
        Network.__init__(self, batch_size=batch_size)

    def build_network(self, sess, is_training=True):
             。。。。
            
            # rois 所有的rois框的坐标的分类得分
            # cls_prob 进行_num_classes的分类得分,经过softmax
            # bbox_prediction 进行 box的回归
            return rois, cls_prob, bbox_pred

    def get_variables_to_restore(self, variables, var_keep_dic):
        

        return variables_to_restore

    def fix_variables(self, sess, pretrained_model):
        ....

    def build_head(self, is_training):
        # 全卷積網絡爲五個層,每層有一個卷積,一個池化操作,但是,最後一層操作中,僅
        # 有一個卷積操作,無池化操作。
        .....
        #输出的图片被 缩短/16
        return net

    def build_rpn(self, net, is_training, initializer):

        # Build anchor component
        # 用來生成九個框的函數
        。。。。
        
        # 二分類操作和迴歸操作是並行的,於是用同樣1×1的卷積去操作原來的future map,
        # 生成長度爲4×k,即_num_anchors×4的長度
        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')
        # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
        # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
        # rpn_cls_score,  是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
        # rpn_cls_score_reshape ,是shape=[None, 2]的框分数
        return rpn_cls_prob, rpn_bbox_pred, rpn_cls_score, rpn_cls_score_reshape

    def build_proposals(self, is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score):
        # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
        # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
        # rpn_cls_score,  是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
        # 获得合适的roi
        # 对坐标的操作,rios为筛选出来的合适的框,roi_scores为
        。。。。。。
        return rois

    def build_predictions(self, net, rois, is_training, initializer, initializer_bbox):

        # Crop image ROIs
        # 构建固定大小的rois窗口
        .......
        
        # 通过fc7进行box框的分类
        bbox_prediction = slim.fully_connected(fc7, self._num_classes * 4, weights_initializer=initializer_bbox, trainable=is_training, activation_fn=None, scope='bbox_pred')
        # cls_score 进行_num_classes的分类得分
        # cls_prob 进行_num_classes的分类得分,经过softmax
        # bbox_prediction 进行 box的回归
        return cls_score, cls_prob, bbox_prediction


从上面看vgg16似乎只有7个可用的方法,但是记住vgg16时继承了Network的所有的方法,也就是说Network的所有方法vgg16都有。那我们开始抽丝剥茧吧先从create_architecture()开始:

create_architecture()

def create_architecture(self, sess, mode, num_classes, tag=None, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
        self._image = tf.placeholder(tf.float32, shape=[self._batch_size, None, None, 3])
        self._im_info = tf.placeholder(tf.float32, shape=[self._batch_size, 3])
        self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])
        self._tag = tag

        self._num_classes = num_classes
        self._mode = mode
        self._anchor_scales = anchor_scales
        self._num_scales = len(anchor_scales)

        self._anchor_ratios = anchor_ratios
        self._num_ratios = len(anchor_ratios)
        
        # K 个box框
        self._num_anchors = self._num_scales * self._num_ratios

        training = mode == 'TRAIN'
        testing = mode == 'TEST'

        assert tag != None

        # handle most of the regularizer here
        weights_regularizer = tf.contrib.layers.l2_regularizer(cfg.FLAGS.weight_decay)
        if cfg.FLAGS.bias_decay:
            biases_regularizer = weights_regularizer
        else:
            biases_regularizer = tf.no_regularizer

        # list as many types of layers as possible, even if they are not used now
        with arg_scope([slim.conv2d, slim.conv2d_in_plane,
                        slim.conv2d_transpose, slim.separable_conv2d, slim.fully_connected],
                       weights_regularizer=weights_regularizer,
                       biases_regularizer=biases_regularizer,
                       biases_initializer=tf.constant_initializer(0.0)):
            # 前面指定了一系列卷積,反捲積的參數,核心代碼爲295行
            # rois爲roi pooling層得到的框,
            # cls_prob得到的是最後全連接層的分類score,
            # bbox_pred得到的是二十一分類之後的分類標籤。
            rois, cls_prob, bbox_pred = self.build_network(sess, training)

        layers_to_output = {'rois': rois}
        layers_to_output.update(self._predictions)

        for var in tf.trainable_variables():
            self._train_summaries.append(var)

        if mode == 'TEST':
            stds = np.tile(np.array(cfg.FLAGS2["bbox_normalize_stds"]), (self._num_classes))
            means = np.tile(np.array(cfg.FLAGS2["bbox_normalize_means"]), (self._num_classes))
            self._predictions["bbox_pred"] *= stds
            self._predictions["bbox_pred"] += means
        else:
            self._add_losses()
            layers_to_output.update(self._losses)

        val_summaries = []
        with tf.device("/cpu:0"):
            val_summaries.append(self._add_image_summary(self._image, self._gt_boxes))
            for key, var in self._event_summaries.items():
                val_summaries.append(tf.summary.scalar(key, var))
            for key, var in self._score_summaries.items():
                self._add_score_summary(key, var)
            for var in self._act_summaries:
                self._add_act_summary(var)
            for var in self._train_summaries:
                self._add_train_summary(var)

        self._summary_op = tf.summary.merge_all()
        if not testing:
            self._summary_op_val = tf.summary.merge(val_summaries)

        return layers_to_output


在create_architecture中先是定义了输入如下:
包括img(图片),im_info(img的尺寸),_gt_boxes(坐标标签),_tag(类别标签)

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

其他就是网络的参数,需要在构建网络是进行设置,如下:

self._num_classes = num_classes(类别数)
self._mode = mode(训练还是,非训练)
self._anchor_scales = anchor_scales(框的尺寸,预测)
self._num_scales = len(anchor_scales)

self._anchor_ratios = anchor_ratios
self._num_ratios = len(anchor_ratios)
        
        # K 个box框
self._num_anchors = self._num_scales * self._num_ratios(多少个框==9)

training = mode == 'TRAIN'
testing = mode == 'TEST'

接下来开始进行网络的运行build_network()

# 前面指定了一系列卷積,反捲積的參數,核心代碼爲295行
# rois爲roi pooling層得到的框,
# cls_prob得到的是最後全連接層的分類score,
# bbox_pred得到的是二十一分類之後的分類標籤。
rois, cls_prob, bbox_pred = self.build_network(sess, training)

build_network产生了img经过网络之后的输出,
rois为roi pooling层得到的框,
cls_prob得到的是最后全全连接层的score,
bbox_pred得到的是二十一分类之后的分类目标。
什么!!!就做完了,过程呢!!!!

接下来看看build_network发生了什么啊,

build_network()

build_network()在vgg16中实现了

def build_network(self, sess, is_training=True):
        with tf.variable_scope('vgg_16', 'vgg_16'):
            """
            分爲了幾段,build head,buildrpn,build proposals,build predictions
            對應的剛好是我們所剛剛敘述的全卷積層,RPN層,Proposal Layer,和最後經過的全連接層。
            """
            # select initializer
            if cfg.FLAGS.initializer == "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)

            # Build head
            # 全卷積網絡層的建立(build head)
            # 输出的图片被 缩短/16
            net = self.build_head(is_training)

            # Build rpn
            # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
            # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
            # rpn_cls_score,  是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
            # rpn_cls_score_reshape ,是shape=[None, 2]的框分数
            rpn_cls_prob, rpn_bbox_pred, rpn_cls_score, rpn_cls_score_reshape = self.build_rpn(net, is_training, initializer)

            # Build proposals
            # 还是筛选框rois,选择合适的框
            rois = self.build_proposals(is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score)

            # Build predictions
            # cls_score 进行_num_classes的分类得分
            # cls_prob 进行_num_classes的分类得分,经过softmax
            # bbox_prediction 进行 box的回归
            cls_score, cls_prob, bbox_pred = self.build_predictions(net, rois, is_training, initializer, initializer_bbox)

            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_bbox_pred"] = rpn_bbox_pred
            self._predictions["cls_score"] = cls_score
            self._predictions["cls_prob"] = cls_prob
            self._predictions["bbox_pred"] = bbox_pred
            self._predictions["rois"] = rois

            self._score_summaries.update(self._predictions)
            
            # rois 所有的rois框的坐标的分类得分
            # cls_prob 进行_num_classes的分类得分,经过softmax
            # bbox_prediction 进行 box的回归
            return rois, cls_prob, bbox_pred

所以说,就是从上面的几个函数进行如下
在这里插入图片描述

  • 1.build_head()函数: 构建CNN基层网络

  • 2.build_rpn()函数: 在feature map上生成box的坐标和判断是否有物体

  • 3.build_proposas()函数: 对box进行判断,挑选合适的box,其中进行iou和nms操作,这里没有训练参数的生成。

  • 4.build_predictions():这里进行最后的类别分类和box框回归之前会有一个rois网络层,该网络会把所有的feature map进行尺寸resize到固定的尺寸,之后进行拉伸。这里有两路输出,一个是box的坐标,另一个是类别的分数。

这样就可以进行代码的深入分析了:
先从build_head()开始:

build_head()

def build_head(self, is_training):
        # 全卷積網絡爲五個層,每層有一個卷積,一個池化操作,但是,最後一層操作中,僅
        # 有一個卷積操作,無池化操作。
        # Main network
        # Layer  1
        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')

        # Layer 2
        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')

        # Layer 3
        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')

        # Layer 4
        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')

        # Layer 5
        net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], trainable=is_training, scope='conv5')

        # Append network to summaries
        self._act_summaries.append(net)

        # Append network as head layer
        self._layers['head'] = net
        #输出的图片被 缩短/16
        return net

这个函数没有什么太大的问题,把一张图片输入到网络进行特征提取。之后把net输出。net代表了网络的最后一层的输出。

build_rpn()

def build_rpn(self, net, is_training, initializer):

        # Build anchor component
        # 用來生成九個框的函數
        self._anchor_component()

        # Create RPN Layer
        # 首先經過了一個3×3的卷積,之後用1×1的卷積去進行迴歸操作,分出前景或是背景,形成分數值
        rpn = slim.conv2d(net, 512, [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')
        
        # 进行softmax
        rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
        
        # 
        rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
        
        # 二分類操作和迴歸操作是並行的,於是用同樣1×1的卷積去操作原來的future map,
        # 生成長度爲4×k,即_num_anchors×4的長度
        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')
        # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
        # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
        # rpn_cls_score,  是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
        # rpn_cls_score_reshape ,是shape=[None, 2]的框分数
        return rpn_cls_prob, rpn_bbox_pred, rpn_cls_score, rpn_cls_score_reshape

build_rpn函数就似乎进行feature map的box的提取,其输出如下:

  • 1.rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框
  • 2.的分数是否有物体,进行二分类经过了softmax
  • 3.rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
  • 4.rpn_cls_score, 是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
  • 5.rpn_cls_score_reshape ,是shape=[None, 2]的框分数

注意:这个函数在内部调用了_anchor_component(),这个函数用來生成对所有的点生成九个框,一共会生成W x H x 9个框。

_anchor_component()

# 
def _anchor_component(self):
        with tf.variable_scope('ANCHOR_' + 'default'):
            # generate_anchors()產生位置
            # just to get the shape right
            # feat_stride爲原始圖像與這裏圖像的倍數關係,feat_stride在這裏爲16
            #_im_info[0, 0]原始图片的尺寸
            height = tf.to_int32(tf.ceil(self._im_info[0, 0] / np.float32(self._feat_stride[0])))
            width = tf.to_int32(tf.ceil(self._im_info[0, 1] / np.float32(self._feat_stride[0])))
            
            # snippit()中相關代碼
            # 这里产生了所有的图片产生的框,如果feature map大小是 W x H x 9个框,每个框大小已经被映射到原图,
            # 也就是乘上了16
            # 
            anchors, anchor_length = tf.py_func(generate_anchors_pre,
                                                [height, width,
                                                 self._feat_stride, self._anchor_scales, self._anchor_ratios],
                                                [tf.float32, tf.int32], name="generate_anchors")
            anchors.set_shape([None, 4])
            anchor_length.set_shape([])
            self._anchors = anchors
            self._anchor_length = anchor_length

在_anchor_component()内部调generate_anchors_pre()这个函数,才是生成所有的框的函数。

generate_anchors_pre()

def generate_anchors_pre(height, width, feat_stride, anchor_scales=(8,16,32), anchor_ratios=(0.5,1,2)):
  """ A wrapper function to generate anchors given different scales
    Also return the number of anchors in variable 'length'
  """
  """生成anchor的预处理方法,generate_anchors方法就是直接产生各种大小的anchor box,generate_anchors_pre方法
     是把每一个anchor box对应到原图上
      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])))
      feat_stride: 经过VGG或者ZF后特征图相对于原图的在长或者宽上的缩放倍数,也就是说height和width对应于特征图长宽
      anchor_scales:anchor尺寸
      anchor_ratios: anchor长宽比
  """
  # 只有9个框
  anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales)) # 产生各种大小的anchor box
  A = anchors.shape[0] # anchor的种数
  shift_x = np.arange(0, width) * feat_stride # 特征图相对于原图的偏移
  shift_y = np.arange(0, height) * feat_stride # 特征图相对于原图的偏移
  shift_x, shift_y = np.meshgrid(shift_x, shift_y) # 返回坐标矩阵
  shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
  K = shifts.shape[0]
  # width changes faster, so here it is H, W, C
  anchors = anchors.reshape((1, A, 4)) + shifts.reshape((1, K, 4)).transpose((1, 0, 2)) 
  # K x A x 4 想相当与把 anchor box加载featu map上,现在fe'a
  # anchor坐标加上anchor box大小
  # H x W x 9个框
  anchors = anchors.reshape((K * A, 4)).astype(np.float32, copy=False)
  length = np.int32(anchors.shape[0]) 
  return anchors, length

当然,这里面调用了一个函数,就是generate_anchors()函数,generate_anchors()就是对一个点产生固定大小的的框,按照输入的参数,就可以在原图生成9个框了。:

generate_anchors():

def generate_anchors(base_size=16, ratios=[0.5, 1, 2],
                     scales=2 ** np.arange(3, 6)):
    """
    Generate anchor (reference) windows by enumerating aspect ratios X
    scales wrt a reference (0, 0, 15, 15) window.
    """

    base_anchor = np.array([1, 1, base_size, base_size]) - 1
    ratio_anchors = _ratio_enum(base_anchor, ratios)
    anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)
                         for i in range(ratio_anchors.shape[0])])
    return anchors

这个函数仅仅是对feature map的处理,没有参数的训练,
这里可以直接test。
这里在一张1024的图上,产生了9个框,坐标点位是(365,365)

import time
    import numpy as np
    import cv2

    # Create a black image
    
    t = time.time()
    a = generate_anchors()
    print(time.time() - t)
    print(a)
    img = np.zeros((1024,1024,3), np.uint8)
    for i in a:
        i = np.array(i) + 365
        cv2.rectangle(img,(int(i[0]),int(i[1])),(int(i[2]),int(i[3])),(0,255,0),3)

    cv2.imshow('line',img)
    cv2.waitKey()
    cv2.waitKey()

这里的框如下:
在这里插入图片描述上面的坐标为:
这里的赋值,就是中心(365,365)的偏移值。

[[ -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.]]

现在可以往回走了。
回到generate_anchors_pre()吧!

  shift_x, shift_y = np.meshgrid(shift_x, shift_y) # 返回坐标矩阵
  shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
  K = shifts.shape[0]
  # width changes faster, so here it is H, W, C
  anchors = anchors.reshape((1, A, 4)) + shifts.reshape((1, K, 4)).transpose((1, 0, 2)) 
  # K x A x 4 想相当与把 anchor box加载featu map上,现在fe'a
  # anchor坐标加上anchor box大小
  # H x W x 9个框
  anchors = anchors.reshape((K * A, 4)).astype(np.float32, copy=False)
  length = np.int32(anchors.shape[0])

这几步操作,就是把对单点的框,扩展到整个feature map,这里的anchors和length,是函数的最终返回值,anchors是shape=[HxWx9,4]的大小,这里的每个点在原图中对应16个点的视野,这里的[2,2]在原图中对应了[32,32]的视野。这里还没有batch size的概念,这里只是对一张feature map产生框。
再回到_anchor_component():

anchors.set_shape([None, 4])
anchor_length.set_shape([])
self._anchors = anchors
self._anchor_length = anchor_length

在这里的anchors被设置到([None, 4]),同时也拿到了anchor_length数量,这里是WxHx9.
再回到build_rpn()
在构建了框之后,net就经过了[3,3]的卷积,

rpn = slim.conv2d(net, 512, [3, 3], trainable=is_training, weights_initializer=initializer, scope="rpn_conv/3x3")


再经过[1,1]卷积,判断出每个feature map点上是否有物体,这里使用2分类。

# 分出前景或是背景,形成分數值
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')

使用[1,1]卷积,判断出每个feature map点上是否有物体的box坐标,每个坐标包含4个值,左上和右下坐标。

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')

我们再次回到build_network()
在上一步说到,特征图中每个点的9个框搞定,同时网络给定了在每个点的预测结果(是否为背景),也是每个点预测9个框的分数。每张图片的框时20000个左右,这里的框有点多。接下来,进行训练和预测时,需要挑选合适的框进行预测。
build_proposals就是构建(选择)合适的框,进行下一步的推断。

# 筛选框rois,选择合适的框
rois = self.build_proposals(is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score)

build_proposals()

def build_proposals(self, is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score):
        # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
        # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
        # rpn_cls_score,  是shape=[None,512,w,h,self._num_anchors * 2]的框的分数是否有物体,进行没有二分类经过了softmax
        # 获得合适的roi
        if is_training:
            # 对坐标的操作,rios为筛选出来的合适的框,roi_scores为
            rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            #篩出來IOU大於70%的框
            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.FLAGS.test_mode == 'nms':
                rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            elif cfg.FLAGS.test_mode == 'top':
                rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            else:
                raise NotImplementedError
        return rois

在这里插入图片描述

_proposal_layer()

_proposal_layer调用了proposal_layer()那就直接看proposal_layer()

def proposal_layer(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
    # rpn_cls_prob, 是shape=[None, self._num_anchors * 2]的框的分数是否有物体,进行二分类经过了softmax
    # rpn_bbox_pred, 是shape=[None,512,w,h,self._num_anchors * 4] 是框的坐标,进行坐标回归
    """A simplified version compared to fast/er RCNN
       For details please see the technical report
    """
    
    """
        proposal_layer中做的事情:实际上上,在proposal_layer中的任務主要就是篩選合適的框,
        縮小檢測範圍,那麼,在前文回憶部分的步驟⑤中我們已經說到:第一,篩選與ground truth中,
        重疊率大於70%的候選框,篩掉其他的候選框,縮小範圍;第二,用NMS非極大值抑制,
        篩選二分類中前n個score值的候選框;第三,篩掉越界框後,
        再來從前n個從大到小排序的值中篩選一次
    """
    
    if type(cfg_key) == bytes:
        cfg_key = cfg_key.decode('utf-8')

    if cfg_key == "TRAIN":
        pre_nms_topN = cfg.FLAGS.rpn_train_pre_nms_top_n
        post_nms_topN = cfg.FLAGS.rpn_train_post_nms_top_n
        nms_thresh = cfg.FLAGS.rpn_train_nms_thresh
    else:
        pre_nms_topN = cfg.FLAGS.rpn_test_pre_nms_top_n
        post_nms_topN = cfg.FLAGS.rpn_test_post_nms_top_n
        nms_thresh = cfg.FLAGS.rpn_test_nms_thresh

    im_info = im_info[0]
    # Get the scores and bounding boxes
    scores = rpn_cls_prob[:, :, :, num_anchors:]
    rpn_bbox_pred = rpn_bbox_pred.reshape((-1, 4))
    scores = scores.reshape((-1, 1))
    
    # 先進行了整體平移,再進行了整體縮放,所以,在求出變換因子之後,
    # 求出,pred_ctr_x, pred_ctr_y, pred_w以及pred_h
    proposals = bbox_transform_inv(anchors, rpn_bbox_pred)
    proposals = clip_boxes(proposals, im_info[:2])

    # Pick the top region proposals
    order = scores.ravel().argsort()[::-1]
    if pre_nms_topN > 0:
        order = order[:pre_nms_topN]
    proposals = proposals[order, :]
    scores = scores[order]

    # Non-maximal suppression
    keep = nms(np.hstack((proposals, scores)), nms_thresh)

    # Pick th top region proposals after NMS
    if post_nms_topN > 0:
        keep = keep[:post_nms_topN]
    proposals = proposals[keep, :]
    scores = scores[keep]

    # Only support single image as input
    batch_inds = np.zeros((proposals.shape[0], 1), dtype=np.float32)
    blob = np.hstack((batch_inds, proposals.astype(np.float32, copy=False)))

    return blob, scores

bbox_transform_inv()坐标的变换

bbox_transform_inv函数结合RPN的输出对所有初始框进行了坐标变换

def bbox_transform_inv(boxes, deltas):
    '''
    Applies deltas to box coordinates to obtain new boxes, as described by 
    deltas
    '''   
    if boxes.shape[0] == 0:
        return np.zeros((0, deltas.shape[1]), dtype=deltas.dtype)
 
    boxes = boxes.astype(deltas.dtype, copy=False)
    
    #获得初始proposal的中心和长宽信息
    widths = boxes[:, 2] - boxes[:, 0] + 1.0
    heights = boxes[:, 3] - boxes[:, 1] + 1.0
    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]
 
    #得到改变后的proposal的中心和长宽信息
    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]
 
    #将改变后的proposal的中心和长宽信息还原成左上角和右下角的版本
    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


在这里插入图片描述使用clip_boxes函数将改变坐标信息后超过图像边界的框的边框裁剪一下,使之在图像边界之内。clip_boxes函数如下所示

clip_boxes()

def clip_boxes(boxes, im_shape):
    """
    Clip boxes to image boundaries.
    """
 
    #严格限制proposal的四个角在图像边界内
    # x1 >= 0
    boxes[:, 0::4] = np.maximum(np.minimum(boxes[:, 0::4], im_shape[1] - 1), 0)
    # y1 >= 0
    boxes[:, 1::4] = np.maximum(np.minimum(boxes[:, 1::4], im_shape[0] - 1), 0)
    # x2 < im_shape[1]
    boxes[:, 2::4] = np.maximum(np.minimum(boxes[:, 2::4], im_shape[1] - 1), 0)
    # y2 < im_shape[0]
    boxes[:, 3::4] = np.maximum(np.minimum(boxes[:, 3::4], im_shape[0] - 1), 0)
    return boxes


对所有的框按照前景分数进行排序,选择排序后的前pre_nms_topN和框。

order = scores.ravel().argsort()[::-1]
 if pre_nms_topN > 0:
      order = order[:pre_nms_topN]
proposals = proposals[order, :]
scores = scores[order]

对于上一步选择出来的框,用nms算法根据阈值排除掉重叠的框。

keep = nms(np.hstack((proposals, scores)), nms_thresh)


nms()

def py_cpu_nms(dets, thresh):
    """Pure Python NMS baseline."""
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    x2 = dets[:, 2]
    y2 = dets[:, 3]
    scores = dets[:, 4]

    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)

        inds = np.where(ovr <= thresh)[0]
        order = order[inds + 1]

    return keep

对于剩下的框,选择post_nms_topN个最终的框。

# Pick th top region proposals after NMS
if post_nms_topN > 0:
     keep = keep[:post_nms_topN]
proposals = proposals[keep, :]
scores = scores[keep]

所有选出的框之后,需要在feature map 上 插入索引,由于batch size为1,因此都插入0。

batch_inds = np.zeros((proposals.shape[0], 1), dtype=np.float32)
blob = np.hstack((batch_inds, proposals.astype(np.float32, copy=False)))

返回build_proposals()中在进行_proposal_layer之后还需要进行正负样本处理,筛选出來IOU大於70%的框

    def _anchor_target_layer(self, rpn_cls_score, name):
        with tf.variable_scope(name):
            rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = tf.py_func(
                anchor_target_layer,
                [rpn_cls_score, self._gt_boxes, self._im_info, self._feat_stride, self._anchors, self._num_anchors],
                [tf.float32, tf.float32, tf.float32, tf.float32])


在这里插入图片描述

cls_score, cls_prob, bbox_pred = self.build_predictions(net, rois, is_training, initializer, initializer_bbox)

build_predictions()

def build_predictions(self, net, rois, is_training, initializer, initializer_bbox):

        # Crop image ROIs
        # 构建固定大小的rois窗口
        pool5 = self._crop_pool_layer(net, rois, "pool5")
        pool5_flat = slim.flatten(pool5, scope='flatten')

        # Fully connected layers
        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')

        # Scores and predictions
        # 通过fc7进行_num_classes的分类
        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")
        
        # 通过fc7进行box框的分类
        bbox_prediction = slim.fully_connected(fc7, self._num_classes * 4, weights_initializer=initializer_bbox, trainable=is_training, activation_fn=None, scope='bbox_pred')
        # cls_score 进行_num_classes的分类得分
        # cls_prob 进行_num_classes的分类得分,经过softmax
        # bbox_prediction 进行 box的回归
        return cls_score, cls_prob, bbox_prediction

把rois(框的坐标,还未进行尺寸处理,pool5才是固定尺寸的feature map)特征图输入到网络。进行最后的分类和定位
这里的_crop_pool_layer()函数,就是crop_pool_layer了,利用box框的坐标,在net上找到对应的feature map区域。
返回的是固定大小的feature map==pool5.

def _crop_pool_layer(self, bottom, rois, name):
        #固定大小的窗口
        with tf.variable_scope(name):
            batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])
            # Get the normalized coordinates of bboxes
            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 backpropagated to rois anyway, but to save time
            bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
            pre_pool_size = cfg.FLAGS.roi_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')

这里pool5被拉平,之后送入fc6----->fc7,这里的fc7是一个公共层。fc7会有俩个输出,一个是进分类,另外一个是进行box的坐标回归。
分类:

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")

box回归:

# 通过fc7进行box框的分类
bbox_prediction = slim.fully_connected(fc7, self._num_classes * 4, weights_initializer=initializer_bbox, trainable=is_training, activation_fn=None, scope='bbox_pred')
        

到这里所有网络的object detection 网络所干的事就干完了。
而在train.py中,网络会进行判断是否在TEST和TRIAN。
TEST的话就结束计算了,而TRIAN还需要进行loss计算

def _add_losses(self, sigma_rpn=3.0):
        with tf.variable_scope('loss_' + self._tag):
            # RPN, class loss
            rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2])
            rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1])
            rpn_select = tf.where(tf.not_equal(rpn_label, -1))
            rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2])
            rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1])
            rpn_cross_entropy = tf.reduce_mean(
                tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label))

            # RPN, bbox loss
            rpn_bbox_pred = self._predictions['rpn_bbox_pred']
            rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets']
            rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights']
            rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights']

            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
            cls_score = self._predictions["cls_score"]
            label = tf.reshape(self._proposal_targets["labels"], [-1])

            cross_entropy = tf.reduce_mean(
                tf.nn.sparse_softmax_cross_entropy_with_logits(
                    logits=tf.reshape(cls_score, [-1, self._num_classes]), labels=label))

            # RCNN, bbox loss
            bbox_pred = self._predictions['bbox_pred']
            bbox_targets = self._proposal_targets['bbox_targets']
            bbox_inside_weights = self._proposal_targets['bbox_inside_weights']
            bbox_outside_weights = self._proposal_targets['bbox_outside_weights']

            loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)

            self._losses['cross_entropy'] = cross_entropy
            self._losses['loss_box'] = loss_box
            self._losses['rpn_cross_entropy'] = rpn_cross_entropy
            self._losses['rpn_loss_box'] = rpn_loss_box

            loss = cross_entropy + loss_box + rpn_cross_entropy + rpn_loss_box
            self._losses['total_loss'] = loss

            self._event_summaries.update(self._losses)

        return loss

从整个网路进行分析,可以发现网络有四个输出。分别是RPN box 和RPBN class ,RCNN box 和 RCNN class。box使用的是回归损失,class使用的是交叉熵损失。把所有的loss进行相加,可以进行联合训练。

2018.10.25 更新

在train.py这里的代码好像和论文不一样,毕竟不是原作者的写的。这里的模型其实是RPN网络与Fast RNN直接进行联合训练。如下:
train_op就是集合所有的loss,没有分阶段训练。

layers = self.net.create_architecture(sess, "TRAIN", self.imdb.num_classes, tag='default')
            loss = layers['total_loss']
            lr = tf.Variable(cfg.FLAGS.learning_rate, trainable=False)
            momentum = cfg.FLAGS.momentum
            optimizer = tf.train.MomentumOptimizer(lr, momentum)

            gvs = optimizer.compute_gradients(loss)

            # Double bias
            # Double the gradient of the bias if set
            if cfg.FLAGS.double_bias:
                final_gvs = []
                with tf.variable_scope('Gradient_Mult'):
                    for grad, var in gvs:
                        scale = 1.
                        if cfg.FLAGS.double_bias and '/biases:' in var.name:
                            scale *= 2.
                        if not np.allclose(scale, 1.0):
                            grad = tf.multiply(grad, scale)
                        final_gvs.append((grad, var))
                train_op = optimizer.apply_gradients(final_gvs)
            else:
                train_op = optimizer.apply_gradients(gvs)

                ....................................................

   
            rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, total_loss = self.net.train_step(sess, blobs, train_op)

参考:详细的Faster R-CNN源码解析之proposal_layer和proposal_target_layer源码解析
基于Tensorflow的目标检测(Detection)的代码案例详解

注:转发自简书

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值