Faster-RCNN代码+理论——2

接着上面,继续分析,下面接着rpn之后的内容开始分析。

前面,我们分析了RPN,得到了一些框和背景。按照下图,把RPN的输出输入给RoI pooling进行一系列操作。
《Faster-RCNN代码+理论——2》

① 定义输入数据RPN,将RPN的输出输入到RoI

#coding:UTF-8
from __future__ import division
import random
import pprint
import sys
import time
import numpy as np
from optparse import OptionParser
import pickle

from keras import backend as K
from keras.optimizers import Adam, SGD, RMSprop
from keras.layers import Input
from keras.models import Model
from keras_frcnn import config, data_generators
from keras_frcnn import losses as losses
import keras_frcnn.roi_helpers as roi_helpers
from keras.utils import generic_utils

# 输入尺度(以backend为Tensorflow为例)
input_shape_img = (None, None, 3)
img_input = Input(shape=input_shape_img)
# 关于rpn函数的内容,请查看Faster-RCNN代码+理论——1
rpn = nn.rpn(shared_layers, num_anchors)
# 定义model_rpn
model_rpn = Model(img_input, rpn[:2])

...
# 简化的训练过程(这里相比keras代码的内容进行了简化)
num_epochs = 2000
for epoch_num in range(num_epochs):

    # Progbar是生成进度条(这是一个武大的兄弟告诉我的,表示感谢)
    progbar = generic_utils.Progbar(epoch_length)
    print('Epoch {}/{}'.format(epoch_num + 1, num_epochs))

    while True:
        # data_gen_train是一个迭代器。返回的是 np.copy(x_img), [np.copy(y_rpn_cls), np.copy(y_rpn_regr)], img_data_aug(我们这里假设数据没有进行水平翻转等操作。那么,x_img = img_data_aug),y_rpn_cls和y_rpn_regr是RPN的两个损失函数。
        X, Y, img_data = next(data_gen_train)

        loss_rpn = model_rpn.train_on_batch(X, Y)
        P_rpn = model_rpn.predict_on_batch(X)

        # 得到了region proposals,接下来另一个重要的思想就是ROI,
        # 可将不同shape的特征图转化为固定shape,送到全连接层进行最终的预测。
        # rpn_to_roi接收的是每张图片的预测输出,返回的R = [boxes, probs]
        R = roi_helpers.rpn_to_roi(P_rpn[0], P_rpn[1], C, K.image_dim_ordering(), use_regr=True, overlap_thresh=0.7, max_boxes=300)
        # note: calc_iou converts from (x1,y1,x2,y2) to (x,y,w,h) format
        # 通过calc_iou()找出剩下的不多的region对应ground truth里重合度最高的bbox,从而获得model_classifier的数据和标签。
        # X2保留所有的背景和match bbox的框; Y1 是类别one-hot转码; Y2是对应类别的标签及回归要学习的座标位置; IouS是debug用的。
        X2, Y1, Y2, IouS = roi_helpers.calc_iou(R, img_data, C, class_mapping)

下面简单叙述一下rpn_to_roi和calc_iou的作用。

② 函数rpn_to_roi & calc_iou分析

从上面的代码可以看出,rpn_to_roi输出作为calc_iou的输入。那么按照顺序先来分析一下rpn_to_roi,此函数的主要作用是:把由RPN输出的所有可能的框过滤掉重合度高的框,降低计算复杂度。

其中,涉及到一个算法:non_max_suppression(非极大值抑制)

下面关于非极大值抑制这个算法的介绍来自参考资料[1]

因为经过RPN之后,可能会从一张图片中找出很多个可能是物体的矩形框,然后为每个矩形框为做类别分类概率:

《Faster-RCNN代码+理论——2》
以上面的图片为例,目标是要定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的意思就是:先假设有6个矩形框,根据分类器类别分类概率做排序,从小到大分别属于车辆的概率分别为A、B、C、D、E、F。

(1)从最大概率矩形框F开始,分别判断A~E与F的重叠度IOU是否大于某个设定的阈值;

(2)假设B、D与F的重叠度超过阈值,那么就扔掉B、D;并标记第一个矩形框F,是我们保留下来的。

(3)从剩下的矩形框A、C、E中,选择概率最大的E,然后判断E与A、C的重叠度,重叠度大于一定的阈值,那么就扔掉;并标记E是我们保留下来的第二个矩形框。

就这样一直重复,找到所有被保留下来的矩形框。

而calc_iou的作用是,通过calc_iou()找出剩下的不多的region对应ground truth里重合度最高的bbox,从而获得model_classifier的数据和标签。
X2保留所有的背景和match bbox的框; Y1 是类别one-hot转码; Y2是对应类别的标签及回归要学习的座标位置; IouS是debug用的。
X2, Y1, Y2, IouS = roi_helpers.calc_iou(R, img_data, C, class_mapping)

# 通过calc_iou()找出剩下的不多的region对应ground truth里重合度最高的bbox,
# 从而获得model_classifier的目标和标签。
def calc_iou(R, img_data, C, class_mapping):

    bboxes = img_data['bboxes']
    (width, height) = (img_data['width'], img_data['height'])
    # get image dimensions for resizing
    (resized_width, resized_height) = data_generators.get_new_img_size(width, height, C.im_size)

    # 这里跟calc_rpn基本一致
    gta = np.zeros((len(bboxes), 4))

    for bbox_num, bbox in enumerate(bboxes):
        # get the GT box coordinates, and resize to account for image resizing
        gta[bbox_num, 0] = int(round(bbox['x1'] * (resized_width / float(width))/C.rpn_stride))
        gta[bbox_num, 1] = int(round(bbox['x2'] * (resized_width / float(width))/C.rpn_stride))
        gta[bbox_num, 2] = int(round(bbox['y1'] * (resized_height / float(height))/C.rpn_stride))
        gta[bbox_num, 3] = int(round(bbox['y2'] * (resized_height / float(height))/C.rpn_stride))

    x_roi = []
    y_class_num = []
    y_class_regr_coords = []
    y_class_regr_label = []
    IoUs = [] # for debugging only

    # R = [boxes, probs]
    for ix in range(R.shape[0]):
        (x1, y1, x2, y2) = R[ix, :]
        x1 = int(round(x1))
        y1 = int(round(y1))
        x2 = int(round(x2))
        y2 = int(round(y2))

        best_iou = 0.0
        best_bbox = -1
        for bbox_num in range(len(bboxes)):
            # x1 x2 y1 y2是生成的框,gta是相对于原图缩小比例的bbox
            curr_iou = data_generators.iou([gta[bbox_num, 0], gta[bbox_num, 2], gta[bbox_num, 1], gta[bbox_num, 3]], [x1, y1, x2, y2])
            if curr_iou > best_iou:
                best_iou = curr_iou
                best_bbox = bbox_num

        # 如果对于某个框,其匹配现有的bbox重叠率小于0.3,那么这个框就扔掉
        if best_iou < C.classifier_min_overlap:
                continue
        else:
            w = x2 - x1
            h = y2 - y1
            x_roi.append([x1, y1, w, h])
            IoUs.append(best_iou)

            if C.classifier_min_overlap <= best_iou < C.classifier_max_overlap:
                # hard negative example
                cls_name = 'bg'
            elif C.classifier_max_overlap <= best_iou:
                cls_name = bboxes[best_bbox]['class']
                cxg = (gta[best_bbox, 0] + gta[best_bbox, 1]) / 2.0
                cyg = (gta[best_bbox, 2] + gta[best_bbox, 3]) / 2.0

                cx = x1 + w / 2.0
                cy = y1 + h / 2.0

                tx = (cxg - cx) / float(w)
                ty = (cyg - cy) / float(h)
                tw = np.log((gta[best_bbox, 1] - gta[best_bbox, 0]) / float(w))
                th = np.log((gta[best_bbox, 3] - gta[best_bbox, 2]) / float(h))
            else:
                print('roi = {}'.format(best_iou))
                raise RuntimeError

        # 找到class对应的类别的数字标签:0,1,2...
        class_num = class_mapping[cls_name]
        # One-Hot
        class_label = len(class_mapping) * [0]
        class_label[class_num] = 1
        y_class_num.append(copy.deepcopy(class_label))
        coords = [0] * 4 * (len(class_mapping) - 1)
        labels = [0] * 4 * (len(class_mapping) - 1)
        if cls_name != 'bg':
            label_pos = 4 * class_num
            sx, sy, sw, sh = C.classifier_regr_std
            # coords: 座标调整:相当于coords是回归要学习的内容
            coords[label_pos:4+label_pos] = [sx*tx, sy*ty, sw*tw, sh*th]
            labels[label_pos:4+label_pos] = [1, 1, 1, 1]
            y_class_regr_coords.append(copy.deepcopy(coords))
            y_class_regr_label.append(copy.deepcopy(labels))
        else:
            y_class_regr_coords.append(copy.deepcopy(coords))
            y_class_regr_label.append(copy.deepcopy(labels))

    if len(x_roi) == 0:
        return None, None, None, None

    # X保留所有的背景和match bbox的框; Y1 是类别one-hot转码; Y2是对应类别的标签及回归要学习的座标位置
    X = np.array(x_roi)
    Y1 = np.array(y_class_num)
    Y2 = np.concatenate([np.array(y_class_regr_label),np.array(y_class_regr_coords)],axis=1)

    # expand_dims:增加一个通道
    return np.expand_dims(X, axis=0), np.expand_dims(Y1, axis=0), np.expand_dims(Y2, axis=0), IoUs

③ 总训练(结合四个损失函数)

《Faster-RCNN代码+理论——2》
如图,因为Faster-RCNN有四个损失函数:
  • RPN calssification(anchor good.bad)
  • RPN regression(anchor->propoasal)
  • Fast R-CNN classification(over classes)
  • Fast R-CNN regression(proposal ->box)
现在,我们结合第②步的输出和原始输入,来训练总的网络。

# sel_samples表示所有匹配Bbox的框(pos)及背景(neg)
sel_samples = selected_pos_samples + selected_neg_samples

loss_class = model_classifier.train_on_batch([X, X2[:, sel_samples, :]], [Y1[:, sel_samples, :], Y2[:, sel_samples, :]])

这里,

# 输入
roi_input = Input(shape=(None, 4)) # roi框的位置,故为4
input_shape_img = (None, None, 3)
img_input = Input(shape=input_shape_img)

# classifier是什么?
# classes_count {} 每一个类的数量:{'cow': 4, 'dog': 10, ...}
# C.num_rois每次取的感兴趣区域,默认为32
# roi_input = Input(shape=(None, 4)) 框框
# classifier是faster rcnn的两个损失函数[out_class, out_reg]
# shared_layers是Faster-RCNN代码+理论——1里面vgg的输出feature map
classifier = nn.classifier(shared_layers, roi_input, C.num_rois, nb_classes=len(classes_count), trainable=True)

model_classifier = Model([img_input, roi_input], classifier)

那么,这个nn.classifier()是什么呢?请看下图:
《Faster-RCNN代码+理论——2》

这里,RoiPoolingConv一个自定义的keras layer,下面大家可能会问,为什么用TimeDistributed这个DD呢?这个不是用在RNN里面的吗?
答:
在最后Faster RCNN的结构中进行类别判断和bbox框的回归时,需要对设置的num_rois个感兴趣区域进行回归处理,由于每一个区域的处理是相对独立的,便等价于此时的时间步为num_rois,因此用TimeDistributed来wrap。

最后,产生num_rois个out_class和out_reg。也就是上面的四个损失函数中的下面两个:Fast R-CNN classification和Fast R-CNN regression(proposal ->box)。

总结

这里,我将结合图片来解释一下流程:

① 输入数据:

图片地址左上角横座标左上角纵座标右下角横座标右下角纵座标Label
xxx.jpgx11y11x21y21dog
xxx.jpgx12y12x22y22cat

《Faster-RCNN代码+理论——2》

② 经过VGG/Resnet等分类模型产生特征图后,进行RPN网络的训练:

注意:这里重点来了,RPN网络的输入 X 是原图:xxx.jpg;而其对应的label Y 则是由 keras代码
data_generators里面对应的get_anchor_gt生产的新的label,而不仅仅是①中的两个Bbox。
这步产生的输出可能是:(其中:绿色代表狗,红色代表猫,紫色代表背景。
《Faster-RCNN代码+理论——2》
注意:RPN的回归是回归这些乱78糟的由锚点生产的框,而不是回归原始label对应的框!

③ 经过一系列处理(包括非极大值抑制),得到合适的框和标签:

这一步见之前函数calc_iou的返回值。
《Faster-RCNN代码+理论——2》

④ 最后经过把rpn和roiPoolingConv合并起来的Faster-RCNN来进行判别和修正:

此步将不展示背景:
《Faster-RCNN代码+理论——2》

关于损失函数和RoiPoolingConv等内容,这里不再细述。希望这两篇文章对大家有帮助!

参考资料

[1] 深度学习(十八)基于R-CNN的物体检测
[2] Keras TimeDistributed
[3] keras版faster-rcnn算法详解(2.roi计算及其他)

点赞