接着上面,继续分析,下面接着rpn之后的内容开始分析。
前面,我们分析了RPN,得到了一些框和背景。按照下图,把RPN的输出输入给RoI pooling进行一系列操作。
① 定义输入数据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之后,可能会从一张图片中找出很多个可能是物体的矩形框,然后为每个矩形框为做类别分类概率:
以上面的图片为例,目标是要定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的意思就是:先假设有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有四个损失函数:
• 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()是什么呢?请看下图:
这里,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.jpg | x11 | y11 | x21 | y21 | dog |
xxx.jpg | x12 | y12 | x22 | y22 | cat |
② 经过VGG/Resnet等分类模型产生特征图后,进行RPN网络的训练:
注意:这里重点来了,RPN网络的输入
data_generators里面对应的get_anchor_gt生产的新的label,而不仅仅是①中的两个Bbox。
这步产生的输出可能是:(其中:绿色代表狗,红色代表猫,紫色代表背景。)
注意:RPN的回归是回归这些乱78糟的由锚点生产的框,而不是回归原始label对应的框!
③ 经过一系列处理(包括非极大值抑制),得到合适的框和标签:
这一步见之前函数calc_iou的返回值。
④ 最后经过把rpn和roiPoolingConv合并起来的Faster-RCNN来进行判别和修正:
此步将不展示背景:
关于损失函数和RoiPoolingConv等内容,这里不再细述。希望这两篇文章对大家有帮助!
参考资料
[1] 深度学习(十八)基于R-CNN的物体检测
[2] Keras TimeDistributed
[3] keras版faster-rcnn算法详解(2.roi计算及其他)