本文结合CVPR 2018论文"Structure Inference Net: Object Detection Using Scene-Level Context and Instance-Level Relationships",详细解析Faster RCNN(tensorflow版本)代码,以及该论文中的一些操作步骤。

  1. Faster RCNN整个的流程就是使用VGG等网络提取全图的feature map以及使用RPN网络预测一些object proposal(物体bbox的形式),使用ROI Pooling操作,提取出每个物体的特征图,然后输入到两层全连接神经网络进行物体类别以及bbox坐标的预测,抽象版的流程图可以看下面两幅图。

    Faster RCNN(tensorflow)代码详解

    Faster RCNN(tensorflow)代码详解

    为了更清楚的体现代码中的结构,按照代码中的流程绘制了下面这张图:(图太大截图不好看,可使用链接下载。)

    Faster RCNN(tensorflow)代码详解

  2. 下面分析代码是根据一次典型的实验的执行顺序来分析的,这样比较方便理解。首先贴出跑一次Faster RCNN的典型参数配置:

    'DEDUP_BOXES': 0.0625,

    'EPS': 1e-14,

    'EXP_DIR': 'faster_rcnn_end2end',

    'GPU_ID': 0,

    'IS_MULTISCALE': False,

    'MATLAB': 'matlab',

    'MODELS_DIR': 'XXX/SIN/models/pascal_voc',

    'PIXEL_MEANS': array([[[102.9801, 115.9465, 122.7717]]]),

    'RNG_SEED': 3,

    'ROOT_DIR': 'XXX/SIN',

    'TEST':

    {'BBOX_REG': True,

    'DEBUG_TIMELINE': False,

    'HAS_RPN': True,

    'MAX_SIZE': 1000,

    'NMS': 0.3,

    'PROPOSAL_METHOD': 'selective_search',

    'RPN_MIN_SIZE': 16,

    'RPN_NMS_THRESH': 0.7,

    'RPN_POST_NMS_TOP_N': 300,

    'RPN_PRE_NMS_TOP_N': 6000,

    'SCALES': [600],

    'SVM': False},

    'TRAIN':

    {'ASPECT_GROUPING': True,

    'BATCH_SIZE': 128,

    'BBOX_INSIDE_WEIGHTS': [1.0, 1.0, 1.0, 1.0],

    'BBOX_NORMALIZE_MEANS': [0.0, 0.0, 0.0, 0.0],

    'BBOX_NORMALIZE_STDS': [0.1, 0.1, 0.2, 0.2],

    'BBOX_NORMALIZE_TARGETS': True,

    'BBOX_NORMALIZE_TARGETS_PRECOMPUTED': True,

    'BBOX_REG': True,

    'BBOX_THRESH': 0.5,

    'BG_THRESH_HI': 0.5,

    'BG_THRESH_LO': 0.0,

    'DEBUG_TIMELINE': False,

    'DISPLAY': 10,

    'FG_FRACTION': 0.25,

    'FG_THRESH': 0.5,

    'GAMMA': 0.1,

    'HAS_RPN': True,

    'IMS_PER_BATCH': 1,

    'LEARNING_RATE': 0.0005,

    'MAX_SIZE': 1000,

    'MOMENTUM': 0.9,

    'PROPOSAL_METHOD': 'gt',

    'RPN_BATCHSIZE': 256,

    'RPN_BBOX_INSIDE_WEIGHTS': [1.0, 1.0, 1.0, 1.0],

    'RPN_CLOBBER_POSITIVES': False,

    'RPN_FG_FRACTION': 0.5,

    'RPN_MIN_SIZE': 16,

    'RPN_NEGATIVE_OVERLAP': 0.3,

    'RPN_NMS_THRESH': 0.7,

    'RPN_POSITIVE_OVERLAP': 0.7,

    'RPN_POSITIVE_WEIGHT': -1.0,

    'RPN_POST_NMS_TOP_N': 2000,

    'RPN_PRE_NMS_TOP_N': 12000,

    'SCALES': [600],

    'SNAPSHOT_INFIX': '',

    'SNAPSHOT_ITERS': 5000,

    'SNAPSHOT_PREFIX': 'VGGnet_fast_rcnn',

    'STEPSIZE': 80000,

    'USE_FLIPPED': True,

    'USE_PREFETCH': False},

    'USE_GPU_NMS': True}

    结合上述配置,下面代码用到的参数就可以很方便的在这里查阅。

  3. 数据准备部分:数据准备为整个模型提供图像数据以及roi的bbo信息。整体涉及到的代码文件如下:

    调用文件顺序:tools/train_net.py\(\rightarrow\)(combined_roidb)\(\rightarrow\)datasets/factory.py.get_imdb\(\rightarrow\)imdb.set_proposal_method\(\rightarrow\)lib/fast_rcnn/train.py(get_training_roidb)\(\rightarrow\)imdb.append_flipped_images\(\rightarrow\)pascal_voc.roidb\(\rightarrow\)pascal_voc.gt_roidb

    (1)通过从factory得到imdb对象,并且通过set_proposal_method将roi_db_handler设置为gt_roidb

    (2)调用train.py的get_training_roidb函数,其中要调用imdb.append_flipped_imags函数,这个是基类imdb的成员函数,这个函数里面,对所有图像的for循环里,调用了self.roidb,这个时候就会调用imdb的roidb函数,而roidb函数检查类成员_roidb是否为空,如果不是空就直接返回,否则调用roi_db_handler函数,因为上面已经设置成gt_roidb,实际是执行了gt_roidb()函数,而这个函数是在pascal_voc类中(其他数据集也是如此,每个数据集都具体实现了gt_roidb()函数)

    (3)gt_roidb读取pascal_voc的标注,形成的roidb格式为:

    list[

    ​ dict 1(for image 1):

    ​ {'boxes': (np.array,(N, 4),左上角顶点和右下角顶点坐标,从0开始),

    ​ 'gt_classes':(np.array, (N,),每个框类别号,21类),

    ​ 'gt_overlaps':(np.array, (N, 21),稀疏矩阵,每个框对应的那一类是1,其他是0),

    ​ 'seg_areas':(np.array, (N,),每个框的面积),

    ​ 'flipped':False},

    ​ dict 2(for image 2):{...}

    ​ ............

    ​ ]

    (4)applied_flipped_images就是对每张图像的每个框都作一个水平对称变换,即x坐标变,y坐标不变。结果就是gt_roidb的长度加倍。

    (5)接下来,还在get_training_roidb函数内,由于HAS_RPN=True, IS_MULTISCALE=False, 因此调用rdl_roidb的prepare_roidb函数,即roi_data_layer文件夹内的roidb文件中的函数,进一步添加roidb数据字段。注意,此步添加的字段在是不写入cache中的,是在每次程序运行时添加的。添加后的格式变为:

    list[

    ​ dict 1(for image 1):

    ​ {'boxes': (np.array,(N, 4),左上角顶点和右下角顶点坐标,从0开始),

    ​ 'gt_classes':(np.array, (N,),每个框类别号,21类),

    ​ 'gt_overlaps':(np.array, (N, 21),稀疏矩阵,每个框对应的那一类是1,其他是0),

    ​ 'seg_areas':(np.array, (N,),每个框的面积),

    ​ 'flipped':False,

    ​ 'image':当前图像的路径,

    ​ 'width':图像宽度,

    ​ 'height':图像高度,

    ​ 'max_classes':(np.array, (N,), 每个框与overlap最大的类别号,也就是类别标号),

    ​ 'max_overlaps':(np.array, (N,),全1)}

    ​ dict 2(for image 2):{...}

    ​ ............

    ]

    (6)至此,get_training_roidb函数执行完毕,返回了roidb,回到combined_roidb函数内,然后检查是否有多个数据集的roidb,比如pascal_voc_2007_trainval和pascal_voc_2012_trainval,将他们合并成一个roidb的list,最后返回imdb和roidb对象。
    (7)接下来,开始真正调用train.py文件中的train_net函数。roidb要先经过筛选,即检查每个图像至少有一个前景ROI(overlap大于0.5)或者一个背景ROI(overlap大于等于0小于0.5)。这样imdb和roidb就完全准备好了。开始调用train_net函数。

    (8)train_net函数中,需要构造solver对象,而solver类在初始化时需要继续对roidb添加更多的信息,涉及到的函数是roidb.py文件中的add_bbox_regression_targets函数。在该函数中可以看到为每个图像的roidb加入了"bbox_targets"字段,调用函数为_compute_targets,输入参数为

    ​ rois: 'boxes': (np.array,(N, 4),左上角顶点和右下角顶点坐标,从0开始)

    ​ max_overlaps: 'max_overlaps':(np.array, (N,),全1)

    ​ max_classes: 'max_classes':(np.array, (N,), 每个框与overlap最大的类别号,也就是类别标号)

    其实这里调用这个函数没什么意义,因为这里全是grondtruth的bbox,而这个_compute_targets函数我们稍后会看到他是计算一个回归的偏移量的,而gt的bbox传进去,计算的偏移量当然全部都是0。因此可以看到添加的bbox_targets字段是一个(N, 5)的矩阵,第一列是类别,后面四列全0,这里只要知道一下被添加了一个字段就好了。下面就开始调用solver的train_model函数。

    (9)roidb提供了标注信息,imdb提供了一个数据基类,里面有一些工具接口。那么实际网络跑起来的时候,也需要准备图像数据输入。因此接下来再关注solver的train_model成员函数中每次图像数据是如何生成的。
    (10)首先生成一个data_layer,根据参数设置,得到一个roi_data_layer,类初始化参数为上面得到的roidb和类别数,接下来是一系列的网络输出、损失定义,后面再看。我们看真正训练时的每次循环,一句blobs=data_layer.forward(),这句已经准备好了传给feed_dict的所有数据,因此很关键。
    (11)我们看这个forward是如何提供图像数据的。在初始化roi_data_layer对象时,同时生成了一个len(roidb)长度的标号乱序,比如有5张图像,那么一个标号乱序可以为[2, 3, 0, 1, 4],以及初始化当前游标为0。每次forward,都调用_get_next_minibatch函数,这个函数又调用_get_next_minibatch_inds函数,这个流程是:

    (HAS_RPN=True)如果当前游标已经到达标号乱序的尽头,即每张图像都遍历了一遍,就重新生成一个乱序,重置游标为0。然后截取乱序的[cur,cur+IMS_PER_BATCH)部分,并令游标向前推进IMS_PER_BATCH。Faster RCNN要求每个batch只有一张图像,因此IMS_PER_BATCH=1,这个后面会多次涉及。

    (HAS_RPN=False)只抽取有object的图像。

    然后,再调用minibatch.py/get_mini_batch函数,输入参数是抽取的那个batch图像的roidb形成的list和类别数。
    (12)get_minibatch函数:

    ​ random_scale_inds是为batch的每个图像随机产生的一个从SCALE(参数)中取scale的下标,这是一个list,和roidb list的长度相同。因为SCALE=[600],因此后面每个图像用的scale都是600.

    ​ BATCH_SIZE=128是ROI的batch,注意区分上面的IMS_PER_BATCH=1是图像的batch。前者要被后者整除。

    ​ rois_per_image=BATCH_SIZE/IMS_PER_BATCH=128

    ​ fg_rois_per_image=128$\times$0.25=32

    ​ 调用_get_image_blob函数,输入参数是roidb和random_scale_inds,返回im_blob和im_scales。首先根据roidb读入图像,如果roidb的flipped属性是True则水平翻转,注意要看是用什么工具读图,如果是opencv通道顺序BGR,对应的PIXEL_MEANS要符合这个顺序。再调用prep_im_for_blob(im, cfg.PIXEL_MEANS, target_size, cfg.TRAIN.MAX_SIZE),其中target_size就是该图像对应的scale,即SCALE[random_scale_inds[i]],此处为600。MAX_SIZE=1000。这个函数先将图像减均值,然后尝试用target_size/短边长度,得到一个scale,再用这个scale乘长边,如果大于MAX_SIZE,则scale为MAX_SIZE/长边长度。再使用opencv将图像resize。最后得到的图像结果是:要么长边=1000,短边小于600;要么短边=600,长边<=1000。这个函数返回处理后的图像和该图resize使用的scale。这样循环调用prep_im_for_blob后,得到processed_ims和im_scales的list。然后将processed_ims传给im_list_to_blob函数,这个函数先找到这批图像的最大短边和最大长边,作为整个blob的高和宽,初始化一个BATCH \(\times\) H \(\times\) W \(\times\) 3的blob,依次将每个图像填入,这就是blob。空白位置用0填充。因为这里每个图像batch只有一张图像,因此没有空白位置。
    ​ 回到get_minibatch函数,经过上述步骤,我们已经得到了关于图像batch的im_blob(BATCH \(\times\) H \(\times\) W \(\times\) 3)和每个图像resize使用的im_scale(list)。注意每个im_blob的长宽可能不一样,因为是根据图像而定的。
    ​ 返回的blobs是一个dict,它包含的字段为:'data':im_blob,这个就是上面返回的im_blob。另外,还要提取出不是背景的gt框,形成一个gt_boxes矩阵,他是从roidb的gt_classes字段中提出标签非0的框号,然后从boxes字段提出这些前景框,最后形成一个N \(\times\) 5的矩阵,前4是列框的坐标(左上角右下角),第5列是类别标号。另外一个im_info字段,前两维分别是im_blob的高和宽,第三维是scale。因此最后形成的blobs格式如下:

    blobs={

    ​ 'data':(np.array, (1\(\times\)H\(\times\)W$\times$3)),

    ​ 'im_info':(1$\times$3, 高和宽,scale),

    ​ 'gt_boxes'????(N$\times$5), 前景框,前四维坐标,最后一维类别)

    }

    至此,数据完全准备好,每次抽取1张图像提供blobs包含图像数据、高宽、scale,还有gt bboxes。

  4. 网络模型部分

    接下来会详细解析一些比较特殊的layer的代码,VGG部分的Conv,max pooling等就略过了。每一层的解析都是按照代码控制逻辑逐段逐段解释,最好对着代码一起看。

    • anchor_target_layer

      • 输入参数:

        rpn_cls_score: 1\(\times\)h\(\times\)w$\times$18(注意tensorflow和其他不一样,默认时通道维度是最后一维)

        gt_boxes:N$\times$5

        im_info: 1$\times$3

        data: 1\(\times\)H\(\times\)W$\times$3

      • 运行

        _anchors: 9$\times$4,使用3个ratio(0.5, 1, 2)和3个scale(8, 16, 32)对基础框([0,0,15,15])做变换,得到9种框。

        height, width: 缩小16倍后的特征图的高宽,即h, w

        shift_x, shift_y:

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

        该段代码首先将特征图上的每个点乘16对应回原图的位置,meshgrid将shift_x当成行向量,在0维度堆叠shift_y的长度那么多次,将shift_y当成列向量,在1维度堆叠shift_y那么多次,ravel()将矩阵按行展开。比如,height=4,width=3,那么生成的shifts如下:

        array([[ 0, 0, 0, 0], [16, 0, 16, 0], [32, 0, 32, 0], [ 0, 16, 0, 16], [16, 16, 16, 16], [32, 16, 32, 16], [ 0, 32, 0, 32], [16, 32, 16, 32], [32, 32, 32, 32], [ 0, 48, 0, 48], [16, 48, 16, 48], [32, 48, 32, 48]])

        从这个例子可以看到一些规律,0维度大小12,就是特征图面积大小,并且逐行逐行看,他是按行主序的顺序遍历特征图上的没一点的。如果将_anchors的每个框和这个shifts的每一行相加,就是得到了所有anchors在原图的位置。shifts就是anchor的偏移量。比如说,取定原图上的左上角第一个点(对应取shifts的第一行),分别加上_anchors的9行,就得到了这个位置的9个anchors,取定原图第一行第二列的点(对应shifts第二行),分别加上_anchors的9行,就又得到了这个位置的9个anchors。那么具体怎么使用矩阵操作相加呢?我们按照代码中的字母来将形状符号化,_anchors的形状是(9,4)=(A, 4),shifts形状是(h\(\times\)w, 4)=(K, 4),我们利用broadcast,先将shifts reshape成(1, K, 4),并转置成(K, 1, 4),_anchors reshape成(1, A, 4),根据broadcast这两个符合规则,可以相加,相加的结果形状是(K, A, 4),这个也是有顺序的,也就是K个通道分别表示特征图上的K个位置,而每个通道的A \(\times\) 4的矩阵就代表这个位置上的A个anchors。这样,我们最终得到了在原图上的A\(\times\)K个anchors的坐标,记为all_anchors。

        all_anchors需要筛选,控制一个边界参数,经过初步筛选,仍记为all_anchors,此时数目应该小于等于A \(\times\) K个。现在记all_anchors: Na \(\times\) 4, labels: (Na,),用-1填充 计算all_anchors和gt_boxes的overlaps(Na\(\times\)No),代码片段如下

        overlaps = bbox_overlaps( np.ascontiguousarray(anchors, dtype=np.float), np.ascontiguousarray(gt_boxes, dtype=np.float))

        argmax_overlaps = overlaps.argmax(axis=1)

        max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps] gt_argmax_overlaps = overlaps.argmax(axis=0)

        gt_max_overlaps = overlaps[gt_argmax_overlaps, np.arange(overlaps.shape[1])]

        gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]

        这段代码之后,argmax_overlaps计算每个anchor覆盖的gt最多的那个gt_box序号,max_overlaps记录每个anchor对应的这个最大overlap值。gt_max_overlaps记录每个gt,对应与之重合的最大overlap,gt_argmax_overlaps则记录每个gt box,对应的覆盖最大的anchor序号,注意,每个gt box可能有多个anchor与之对应。

        接下来,labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0 此句最大overlap都小于0.3阈值的anchor的label置为0; labels[gt_argmax_overlaps] = 1 将与每个gt_box overlap最大的anchor的label置为1,注意,可能会将上一步的标签纠正过来。labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 1将最大overlap大于0.7的anchor的label也置为1;

        然后如果正样本过多,需要进行正样本的采样。首先计算num_fg=RPN_FG_FRACTION\(\times\)RPN_BATCHSIZE=0.5$\times$256=128,fg_inds将labels=1的anchor序号抽出来,如果这个数目多于num_fg,那么随机抽取一些disabled掉,即将labels置为-1。然后,num_bg=RPN_BATCH_SIZE-sum(labels==1),同样也抽取labels=0的,多于num_bg的就disabled掉一些,设为-1。通常前景框数目远少于背景框,二者加起来一共256个,注意现在all_anchors的数目还是没有变,labels的大小也和all_anchors数目一样,只不过其中标1和标0的数目加起来是256,还有其他的没有被考虑的就标为-1。

        bbox_targets:对于上述的每个筛选后的anchor计算他和对应的最大IoU的gt box之间的偏移量,_compute_targets(调用bbox_transform)完成这个任务,返回的结果是Na$\times$4,分别是(dx, dy, dw, dh),其中dx和dy是(gt_ctr_x-ex_ctr_x)/ex_widths, 即相对偏差;dw和dh是np.log(gt_widths/ex_widths);注意,Faster RCNN回归的目标不是框的真实位置,而是anchor相对于gt box的偏差值,因此在预测时需要利用预测的偏差值计算出真正的框的位置。

        bbox_inside_weights是Na \(\times\) 4,正样例(labels=1)的行全是1;其他都是0。bbox_outside_weights也是Na \(\times\) 4,值分情况,如果参数RPN_POSITIVE_WEIGHT<0,那么所有正负样例的行都是1/(Np+Nn);否则如果在(0,1)间,那么正样例权重RPN_POSITIVE_WEIGHT/(Np),负样例权重(1-RPN_POSITIVE_WEIGHT)/(Nn)
        最后一步,因为上面的anchor经过筛选,一些在图像内的标号记为inds_inside,即有Na个,而一开始未经任何筛选时总共有K\(\times\)A个,因此我们还是要求回所有K\(\times\)A个anchors对应上述的所有量。_unmap函数完成这个任务,比如labels,它先生成一个K\(\times\)A大小的一维向量,然后向inds_inside的位置填入labels,其他被抛弃掉的不在图像内的anchors的label填充-1;同理,bbox_targets,bbox_inside_weights,bbox_outside_weights也全部都是这种操作,被抛弃掉的对应行填0;这样,这四个变量的形状分别是(K\(\times\)A, ),(K\(\times\)A, 4),(K\(\times\)A, 4),(K\(\times\)A, 4).
        再reshape一下,reshape也有技巧,上面已经说到anchors的排列顺序是有规律的,每个位置的A个anchor的信息先排,接下来是下面一个位置,也就是上面的0维度K\(\times\)A的大小可以看成有K组A,而位置变动是行主序的,因此reshape的时候先reshape成(1, h, w, A)或者(1, h, w, A$\times$4),再转置成(1, A, h, w)或者(1, A \(\times\) 4, h, w),labels比较特别,最后reshape成(1,1,A\(\times\)h, w)。最后返回的四个变量格式如下:

        ​ rpn_labels, (1,1,A\(\times\)h, w),表示每个位置的每个anchor是正样本(1)还是负样本(0)还是被忽略的(-1)

        ​ rpn_bbox_targets, (1, A$\times$4, h, w),通道表示每个位置的A个anchor的回归目标,无目标的填充0;

        ​ rpn_bbox_inside_weights, (1, A \(\times\) 4, h, w),未被忽略的,全1,包括正负样本;被忽略的,填0;

        ​ rpn_bbox_outside_weights, (1, A \(\times\) 4, h, w),未被忽略的,全都是1/(Np+Nn);被忽略的,填0。

        总结一下,这一层的功能就是给每个位置的9个anchor生成表示正负样本的label和回归的目标值,以及权重,提供给RPN进行训练。

    • reshape_layer

      • 输入参数:

        rpn_cls_score,(1, h, w, 18)

        通道数:d

      • 运行:

        代码模型中用到两个reshape layer。第一个reshape layer,d=2,将rpn_cls_score变成(1, 9h, w, 2),然后进行softmax,特别注意tensorflow默认最后一维是通道,softmax也是默认通道间进行,因此都将通道放在最后一维。输出rpn_cls_prob。 第二个reshape layer,d=18,紧接着将rpn_cls_prob还原成(1, h, w, 18),为rpn_cls_prob_reshape

    • proposal_layer

      • 输入参数:

        rpn_cls_prob_reshape,(1, h, w, 18),(1, 18, h, w)

        rpn_bbox_pred,(1, h, w, 36),层内转置成(1, 36, h, w)

        im_info: (1,3)

      • 运行:

        一些设置的参数如下,后面用到:

        _num_anchors = 9

        pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N=12000(train)/6000(test)

        post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N=2000(train)/300(test)

        if cfg_key == 'TEST':

        ​ post_nms_topN = 256【SIN论文的设置】

        nms_thresh = cfg[cfg_key].RPN_NMS_THRESH=0.7

        min_size = cfg[cfg_key].RPN_MIN_SIZE=16

        scores = rpn_cls_prob_reshape[:, _num_anchors:, :, :]取第二组9个通道为前景的概率值,即scores形状为(1,9,h,w)

        bbox_deltas = rpn_bbox_pred, (1, 36, h,w) 记住我们预测的是偏移值,因此叫做deltas没毛病。

        和anchor_target_layer一样,也每个位置产生9个anchor,堆叠成anchors, (K\(\times\)A, 4), 遍历顺序是先遍历完一个位置的所有anchor,然后宽度遍历,最后高度遍历,这种遍历顺序记作(h,w,a)

        bbox_deltas = bbox_deltas.transpose((0, 2, 3, 1)).reshape((-1, 4)),现在形状变成(9\(\times\)h\(\times\)w, 4),遍历顺序(h, w, a)

        scores = scores.transpose((0, 2, 3, 1)).reshape((-1, 1)),形状变成(9\(\times\)h\(\times\)w, 1),遍历顺序(h,w,a)

        proposals = bbox_transform_inv(anchors, bbox_deltas),回想anchor_target_layer,他给每个anchor产生的回归目标是到各个gt box的偏移量,bbox_transform函数完成这个计算。那么现在我们模型回归出bbox_deltas,因此只要在anchors基础上做一个bbox_transform_inv的逆运算,就可以计算出模型预测的proposals的框,形状是和anchors形状一样,(9\(\times\)h\(\times\)w, 4)=(K\(\times\)A, 4),左上角右下角顶点坐标值。

        进一步对proposals做后续处理,首先是clipped,即每个box的边界缩回到不超过原图边界;然后_filter_boxes, 通过上面的min_size\(\times\)scale,scale从im_info获得,限制每个框的最小高宽,返回保留的框的序号;proposals和scores都取序号索引的框;这时框的数目少于A\(\times\)K个。

        order = scores.ravel().argsort()[::-1]

        if pre_nms_topN > 0:

        order = order[:pre_nms_topN]

        proposals = proposals[order, :]

        scores = scores[order]

        order是将scores展开,并由大到小排序的标号,如果有pre_nms_topN的限制,就先截取分数最高的pre_nms_topN个框,比如12000个(注意如果少于这个数就是全部),然后proposals和scores都按照这个顺序将框排好。这个时候的框已经没有(h,w,a)的遍历顺序了。

        然后再做NMS。NMS的步骤就是对于分数由高到低排序的框,从分数高的开始,看他和后面每一个没有被扔掉的框的IoU是否大于阈值,是的话就将后面的这些框扔掉;

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

        delta = 0.05

        while (len(keep)) < 256:

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

        delta = delta + 0.05

        这段代码进行了nms操作,并且保证保留下来的框有至少256个,通过提高阈值实现。

        if post_nms_topN > 0:

        keep = keep[:post_nms_topN]

        proposals = proposals[keep, :]

        scores = scores[keep]

        batch_inds = np.zeros((proposals.shape[0], 1), dtype=np.float32)

        blob = np.hstack((batch_inds, proposals.astype(np.float32, copy=False)))

        最后,如果有post_nms_topN,就截取,SIN任务测试时是截256个。然后给proposals这个4列的矩阵在前面加一个全0列,表示batch_inds。因为只有一张图像,所以batch序号是全0.

        总结一下,proposal_layer就是将预测出的rpn_bbox_pred(框的偏移量)拿过来,经过一系列的操作,生成真正的proposals,形状是5列,注意这里是rpn的proposals,只有是否前景之分,没有对应的物体类别,这一层的用处是还原出真正的proposal信息,在test时用于prediction。

    • proposal target layer

      • 输入参数:

        rpn_rois: 5列,来自于proposal_layer的rpn预测出的bbox,第一列全0,表示batch id;

        gt_boxes:五列,最后一列是框的类别

        _num_classes: 类别数

      • 运行:

        all_rois = np.vstack((all_rois, np.hstack((zeros, gt_boxes[:, :-1])))) 将rois和gt_boxes在0维拼合在一起,数据还是五列,第一列全0,后四列是box坐标;

        num_images = 1

        rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images=128/1=128

        fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image)=0.25*128=32

        labels, rois, bbox_targets, bbox_inside_weights = _sample_rois( all_rois, gt_boxes, fg_rois_per_image, rois_per_image, _num_classes)

        具体看_sample_rois函数,传入参数all_rois, gt_boxes, 32, 128, 21。 该函数首先计算all_rois和gt_boxes的overlaps,得出每个roi bbox最大IoU的gt_box对应的类别标号,用labels表示。然后用前景阈值0.5筛选出前景框标号,记为fg_inds。

        fg_rois_per_this_image = int(min(fg_rois_per_image, fg_inds.size))

        if fg_inds.size > 0:

        fg_inds = npr.choice(fg_inds, size=fg_rois_per_this_image, replace=False)

        这段代码,当fg_inds的个数比fg_rois_per_image大时,就只筛选32个出来;否则,全部保留; 同样,bg框也是筛选,最后筛选出来前景+背景128个;

        相应的,labels设置,将背景框label置为0;接下来就和anchor_target_layer类似了,传入rois,gt_boxes给_compute_targets来计算要回归的偏移量,唯一不同的就是需要将偏移量Normalize,即减均值和除以标准差,返回的bbox_target_data有五列,第一列是label,后面四列是回归的目标;然后继续调用_get_bbox_regression_labels,主要目的是将bbox_target_data扩充成输入到网络的形式,即表示回归目标的4个元素值扩充成84维,只有class label对应的那4个位置填上目标值,其他位置为0。返回的bbox_targets和bbox_inside_weights都是84列,后者对应label的4个位置全1,其余全0.

        最后,这一层返回如下:

        rois:128$\times$5,第一列是全0,后面是框的左上角右下角坐标;

        labels: 128$\times$1,每个框的物体类别;

        bbox_targets: 128$\times$84,每个框回归的偏差值,经过了normalize

        bbox_inside_weights, bbox_outside_weights: 128$\times$84对应类别位置为1.

        总结:这一层就是将proposal_layer提供的roi加上物体类别标签和bbox的回归目标,并计算权重weights。注意上面的anchor_target_layer加上的标签和回归目标用于rpn训练,这里的用于目标检测训练。

    • Roi Pooling

      • 输入参数:

        特征图,例如VGG16的conv5-3,shape是(1, h, w, 512)

        rois: bbox,(N, 5),第一列的batch_id在此处用到了,来自于propoosal_target_layer

        输出特征图的大小,一般为7

        缩放比例:一般为1/16.

      • 运行:

        输出形状:(N, 7,7, 512)注意,ROI Pooling和max pooling相似,都是逐通道地区域最大值。

        先将ROI框的坐标比例缩小16倍(这里有四舍五入操作,会造成偏差,也是ROI pooling被改造成ROI align的原因),假设特征图大小为h\(\times\)w,然后将特征图的这个区域划分成H\(\times\)W个网格,每个网格高h/H个像素,宽w/W个像素,然后在这些小网格做Max Pooling,因此得到一个H\(\times\)W的统一大小的ROI的特征图,一般为7$\times$7。一般这里都是只有一张图像,因此batch_id全0;而如果有多张图像,就应该根据rois的batch_id(应该叫image_id更恰当)去找该图像的特征图,在这上面pooling,最后输出来的各个特征图按照rois的标号顺序在第0维堆叠。

    • 【SIN模块专属层】union_box_layer

      • 输入参数:

        rois:(128, 5),【注意:proposal_target_layer提供了四个输出,而network.py中定义的union_box_layer在一开始对输入做了处理,因为input一开始是(roi-data, im_info),而roi-data是来自proposal_target_layer的四个矩阵,是元组形式,因此只取了第一块,即rois这个128*5的矩阵】

        im_info:(1,3)

      • 运行:

        返回整个图的框,即(1, 5)矩阵,第一维是0,后四维实际就是整个图的框

    • 【SIN模块专属层】edge_box_layer

      • 输入参数:

        与union_box_layer相同。

      • 运行:

        主要作用:形成论文中的\(R^P_{j\rightarrow i}\)的12维向量关系,这是位置关系。实际上就是对所有128个rois进行二重遍历,计算IoU,如果IoU小于0.6,认为有关系,则产生一个12维向量,包括两个box各自的宽高面积(除以对应的值作单位化,比如宽除以原图宽,面积除以原图面积),这里有6维。然后是他们的相对位置关系,详细可见原文,注意是用接收信息的框减掉传过来信息的框,比如\(R^P_{j\rightarrow i}\),那么相对关系应该是\(i\)框减掉\(j\)框。这里也有6维,一共12维向量。对于IoU大于0.6的,不认为他们有关系,因此12维全0,最后返回的是(128*128, 12)的矩阵。

    • 【SIN模块专属层】structure_inference_spmm

      • 输入参数:

        fc6: (129, 2048),这是由物体框roi_pooling后的(128, 7,7,512)特征图和全图框roi_pooling后的(1,7,7,512)在0维连接后进行一个全连接得到的特征。

        edges: 由edge_box_layer得到的(128$\times$128, 12)bbox之间关系的特征。

      • 运行:

        n_steps=2

        n_boxes=128

        n_inputs=2048

        n_hidden_o=n_hidden_e=2048

        ofe = edges

        ofo, ofs=tf.split(input[0], [n_boxes, 1], 0) #将fc6在0维分成两份,得到ofo (128, 2048)和ofs (1, 2048)

        fo = tf.reshape(ofo, [n_boxes, n_inputs]) #(128, 2048),每个物体框的视觉特征

        fs = tf.reshape(ofs, [1, n_inputs])

        fs = tf.concat(n_boxes * [fs], 0)

        fs = tf.reshape(fs, [n_boxes, 1, n_inputs])

        fe = tf.reshape(ofe, [n_boxes * n_boxes, 12])

        这段最后得到的fs是(128, 1, 2048),因为是concat堆叠的,每个2048维都一样;表示由场景提供的context特征。

        fe是(128 \(\times\) 128, 12),表示物体之间位置关系的特征;

        u: 训练参数,(12, 1)

        W: 训练参数,(2048, 2048)

        接下来计算原文中的\(e_{j\rightarrow i}=relu(W_pR^p_{j\rightarrow i})*tanh(W_v[f_i, f_j])\),因为任意两个box之间的关系是一个标量值,下面要计算出的是矩阵,即进行矩阵化的操作。

        PE = fe与u相乘,即将128 \(\times\) 128个box pair的12维关系特征压缩成1维表示,然后reshape成(128, 128)的矩阵,再relu,这和原来的bbox顺序一样,因此可以reshape,这个就相当于公式中的relu部分。

        oinput=fs[:,0,:] (128, 2048) ,场景提供的context信息,在以后的迭代中每次输入都不变。

        hi=fo #(128, 2048) 输入到GRU的初始隐状态,即128个物体框的视觉特征

        开始GRU迭代两步:

        X: 是hi变成(128, 128, 2048),明确他的意义,这里有128通道,每个通道128 \(\times\) 2048矩阵,每个矩阵中的每一行就是每个物体框的特征,那么根据原文的描述,X是用来计算每个节点接受的信息;

        对应原文中E的计算,需要计算tanh部分,这里实现和文中稍有不同,计算视觉的相互影响关系是用\(tanh(YWY^T)\)的方式,其中W是对称正交矩阵,不管方式如何,得到VE是(128,128),因此计算E就是用PE\(\times\)VE,再进行一个softmax。E是(128,128),我们进一步记作Z。这里的每一个位置就是任意两个框之间的影响权重。

        接下来就要计算每个物体节点接收的关系信息m,结合上面说到的X,我们先看原文的公式,

        \[m_i^e=max_{j\in V}(e_{j\rightarrow i}f_j^v)
        \]

        对于物体i,他接收的关系是先让E中其他物体j对i的影响因子乘以j的视觉特征,这个实现起来非常巧妙,将Z变成(128, 128, 1),这样的话每个128 \(\times\) 1的矩阵就是所有其他框对当前框的影响因子,这个可以根据edge_box_layer的计算顺序推知。那么根据变形关系,对于Z的通道i,就是对应要计算的物体i,也是对应X的通道i,然后如果让Z\(\times\)X,利用了broadcast机制,比如我们看第1通道,分别抽出Z和X的第一通道,就是计算第一个物体的接收信息,那么128 \(\times\) 1的矩阵和128 \(\times\) 2048矩阵相乘,就是公式中的各自影响因子和各自视觉特征相乘。这样,我们得到了(128, 128, 2048)的tensor,再根据公式中的max,应该是在1维度进行取max操作,表示按照其他框的影响取最大值,因此最终得到M (128, 2048),表示128个框分别接收的信息。

        Faster RCNN(tensorflow)代码详解

        计算好由edge获得的信息einput(就是M)后,就可以输入两个GRU进行迭代了。Scene GRU的初始输入x是fs全图特征(128, 2048),这个每次迭代都不变,而edge GRU的输入是einput,他们共同的隐状态是hi,初始隐状态是(128, 2048)的物体特征,随着每次迭代,取两个GRU的更新后的隐状态的均值作为新的隐状态,并且重新计算einput,进行下一次迭代。最终返回一个(128, 2048)的物体特征矩阵,这个就是替代原始Faster RCNN输入到全连接层进行分类和定位的特征。

  5. 模型训练

    loss的构成:loss主要分成4个部分。

    • rpn分类损失

      从anchor_target_layer返回数据读取第一块rpn_label,排成一列,共有K\(\times\)A个,读取rpn_cls_score_reshape数据,reshape成(K\(\times\)A, 2)矩阵,然后根据label取出不为-1的行,共256行,然后输入给tf.nn.sparse_softmax_cross_entropy_with_logits计算分类损失。

    • rpn回归损失

      从anchor_target_layer返回数据读取第二到第四块数据,分别是回归的目标值,计算modified L1 loss的inside和outside weight,都reshape成(1, h, w, A*4),从而计算出L1 loss。

    • 目标检测分类损失

      读取最后的cls_score,(128, 21),从proposal_target_layer返回数据读取第二块labels,排成一列,然后计算tf.nn.sparse_softmax_cross_entropy_with_logits分类损失。

    • 目标检测回归损失

      读取最后的bbox_pred,(128, 84),从proposal_target_layer返回数据读取第三到第五块数据,同样计算L1 loss。

至此,全部主要的代码分析完毕,跟着数据走非常重要!!搞清楚每一层输出数据的形状!

参考资料:

【1】tensorflow版的Faster RCNN代码:https://github.com/smallcorgi/Faster-RCNN_TF.git

【2】SIN论文:Structure Inference Net: Object Detection Using Scene-Level Context and Instance-Level Relationships, Yong Liu, Ruiping Wang, Xilin Chen, CVPR 2018.

【3】Faster RCNN及SIN模型结构Visio绘图链接:https://pan.baidu.com/s/10GKd777lEE31ekoPWMja5A 密码:eo4j

对于原创博文:如需转载请注明出处http://www.cnblogs.com/Kenneth-Wong/