Keras版Faster RCNN——test过程 (1)

code from GitHub: https://github.com/yhenon/keras-frcnn

1. 网络搭建之前

OptionParser模块

这个模块能提供处理命令行参数的方法
简单用法举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from optparse import OptionParser

parser.add_option("-p", "--path",
dest="test_path",
help="Path to test data.")

parser.add_option("-n", "--num_rois",
type="int",
dest="num_rois",
help="Number of ROIs per iteration. Higher means more memory use.",
default=32)

parser.add_option("--network",
dest="network",
help="Base network to use. Supports vgg or resnet50.",
default='resnet50')

(options, args) = parser.parse_args()

help参数后面接的是命令行的帮助信息,可以通过-h,--help参数查看:

1
2
<yourscript> -h  
<yourscript> --help

其中有一个参数action默认设置为action='store',表示要将命令行参数存储在dest参数定义的变量中。通过最后的(options, args) = parser.parse_args()依次传入这些值。
如果未传入某个想要的参数,可以这样判断并提供报错信息:

1
2
if not options.test_path:
parser.error('Error: path to test data must be specified. Pass --path to command line')

pickle模块

pickle是python语言的一个标准模块,安装python后已包含pickle库,不需要单独再安装。pickle模块使用的数据格式是python专用的,并且不同版本不向后兼容,同时也不能被其他语言识别。要和其他语言交互,可以使用内置的json包。

使用模块中的dump()函数保存数据,load()函数读取数据。

1
2
3
4
import pickle

with open(pickl_filename, 'rb') as f_in:
C = pickle.load(f_in)

访问其中的变量:

1
2
3
4
if C.network == 'resnet50':
import keras_frcnn.resnet as nn
elif C.network == 'vgg':
import keras_frcnn.vgg as nn

更多数据信息存放在config.py文件中。

numpy.random.randint

一般使用方法:numpy.random.randint(low, high=None, size=None, dtype='l')
np.random.randint(0, 255, 3)生成的是大小为(3,)的随机array,值在0-255之间。

代码中生成class_to_color随机变量

1
2
3
4
5
6
7
8
9
10
11
12
13
class_mapping: {0: 'person', 1: 'dog', 2: 'bottle', 3: 'motorbike', 4: 'train', 
5: 'bus', 6: 'car', 7: 'bird', 8: 'aeroplane', 9: 'sofa',
10: 'horse', 11: 'cat', 12: 'cow', 13: 'chair', 14: 'pottedplant',
15: 'sheep', 16: 'diningtable', 17: 'bicycle', 18: 'boat', 19: 'tvmonitor',
20: 'bg'}

class_to_color = {class_mapping[v]: np.random.randint(0, 255, 3)
for v in class_mapping}

class_to_color: {'person': array([240, 201, 251]),
'dog': array([118, 6, 29]),
...,
'bg': array([ 29, 194, 91])}

每个类对应一个随机像素点,最后用来决定在图像上的这一类物体的框的颜色。

2. 网络搭建过程

2.1 shared-layers

定义了: img_input:(None, None, 3) :

1
2
3
img_input = Input(shape=input_shape_img)

shared_layers = nn.nn_base(img_input, trainable=True)

nn.nn_base是搭建共享网络 (vgg16或者resnet50) 的函数,代码和keras/applications下的resnet50.py差不多,但是在模型的搭建上,有一些区别,比如vgg16去掉了后面的一层,resnet50去掉了后面的好几层。

resnet50网络搭建起来有点复杂,还是先看vgg16吧

2.2 rpn-layers

如论文中的3.1部分:

  1. We model this process (RPN) with a fully convolutional network.
  2. To generate region proposals, we slide a small network over the convolutional feature map output by the last shared convolutional layer.
    其实就是在shared layers后面加上个卷积层。
  3. Each sliding window is mapped to a lower-dimensional feature (256-d for ZF and 512-d for VGG, with ReLU following).
    简单描述:对于ZF网络,用256个卷积核,对于VGG16,用512个卷积核。kernel size:3*3
  4. This feature is fed into two sibling fully-connected layers—a box-regression layer (reg) and a box-classification layer (cls).
    这句话就比较有意思了。(1.)中说了rpn是“fully convolutional network”,这里又说是“fully-connected layers”,看到最后,有一句:This architecture is naturally implemented with an n × n convolutional layer followed by two sibling 1 × 1 convolutional layers (for reg and cls, respectively). 这个“fully-connected layers”是用kernel大小为1*1的卷积层来实现的。

在程序实现中,用的是k个scores代替2k个scores (logistic regression)。

RPN网络搭建代码:

1
2
3
4
5
6
7
8
9
10
11
def rpn(shared_layers, num_anchors):

x = Conv2D(512, (3, 3), padding='same', activation='relu',
kernel_initializer='normal', name='rpn_conv1')(shared_layers)

x_class = Conv2D(num_anchors, (1, 1), activation='sigmoid',
kernel_initializer='uniform', name='rpn_out_class')(x)
x_regr = Conv2D(num_anchors * 4, (1, 1), activation='linear',
kernel_initializer='zero', name='rpn_out_regress')(x)

return [x_class, x_regr, shared_layers]

num_anchors是9(9种anchor box)。
这个x_class,x_regr是通过1*1的卷积层的结果,所以不需要设置padding='same'(默认是padding='valid')就能保证输入输出的大小相同(深度不同)。

  • x_class是原大小,深度为9.
  • x_regr是原大小,深度为9*4=36.

2.3 classifier-layers

网络搭建:

1
2
3
4
5
roi_input = Input(shape=(C.num_rois, 4))
feature_map_input = Input(shape=input_shape_features)

classifier = nn.classifier(feature_map_input, roi_input, C.num_rois,
nb_classes=len(class_mapping), trainable=True)

这里用到了两个input: roi_input:(32, 4)feature_map_input:(None, None, 1024)

和Fast-RCNN的classifier一样,classifier有两个输出,如上图。
vgg16对应的classifier定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def classifier(base_layers, input_rois, num_rois, nb_classes = 21, trainable=False):

...

# make ROI same size
out_roi_pool = RoiPoolingConv(pooling_regions, num_rois)([base_layers, input_rois])

out = TimeDistributed(Flatten(name='flatten'))(out_roi_pool)
out = TimeDistributed(Dense(4096, activation='relu', name='fc1'))(out)
out = TimeDistributed(Dropout(0.5))(out)
out = TimeDistributed(Dense(4096, activation='relu', name='fc2'))(out)
out = TimeDistributed(Dropout(0.5))(out)

out_class = TimeDistributed(Dense(nb_classes, activation='softmax', kernel_initializer='zero'), name='dense_class_{}'.format(nb_classes))(out)
# note: no regression target for bg class
out_regr = TimeDistributed(Dense(4 * (nb_classes-1), activation='linear', kernel_initializer='zero'), name='dense_regress_{}'.format(nb_classes))(out)

return [out_class, out_regr]

RoiPoolingConv

是自定义的Keras层,定义方法参考:编写自己的层
参考资料:

简单说明:这一层的作用是pool这些ROI区域到相同大小(7*7). 输入的维度是(1, rows, cols, channels),输出的维度是(1, num_rois, channels, pool_size, pool_size),1是时间维度 (参考下面的TimeDistributed). 这里pool_size是7.

TimeDistributed

TimeDistributed是一个层封装器
如官方教程所说,输入大小是(32, 10, 16)的话,通过TimeDistributed连接到dense(8),输出的是(32, 10, 8)
这里的时间步是num_rois。

2.4 concatenate layers

model_rpn

1
model_rpn = Model(img_input, rpn_layers)

rpn_layers有两个输出,但是程序中rpn_layers = nn.rpn(shared_layers, num_anchors)返回的是[x_class, x_regr, base_layers],因为训练过程的模型只用到了rpn_layers[0:2]. 所以这里即使的最后一个base_layers没有实际作用。
model_rpn是一个单输入多输出模型。注意base_layers的最后一层是feature map.

由于需要先计算RPN网络的输出,所以将shared layers和rpn网络绑定,直接将feature map作为rpn网络的输入也可以,但是不如绑定后直接将img_input作为输入方便。

model_classifier

1
2
model_classifier_only = Model([feature_map_input, roi_input], classifier)
model_classifier = Model([feature_map_input, roi_input], classifier)

classifier_layers有两个输出,也就是model_classifier = Model([feature_map_input, roi_input], [out_class, out_regr]).
model_classifier是一个多输入多输出模型。

model_classifier_only在后面会用到。

目前为止model_rpnmodel_classifier两个模型在网络结构上面没有连接,他们之间的联系是feature map.

3. 载入参数和编译模型

1
2
3
4
print('Loading weights from {}'.format(C.model_path))

model_rpn.load_weights(C.model_path, by_name=True)
model_classifier.load_weights(C.model_path, by_name=True)

model.load_weights(filepath, by_name=False):从HDF5文件中加载权重到当前模型中, 默认情况下模型的结构将保持不变。如果想将权重载入不同的模型(有些层相同)中,则设置by_name=True,只有名字匹配的层才会载入权重。

1
2
model_rpn.compile(optimizer='sgd', loss='mse')
model_classifier.compile(optimizer='sgd', loss='mse')

C.model_pathconfig.pickle文件中导入的,base layers选用resnet50时,model_path的值为'model_frcnn.hdf5'.
关于mse损失函数,参考官方文档:损失函数,这里只做测试,loss可以随便写,甚至模型都不用compile,只load就可以。

4. 测试

4.1读取图片

一个小轮子:

1
2
3
4
5
6
7
8
9
for idx, img_name in enumerate(sorted(os.listdir(img_path))):
if not img_name.lower().endswith(('.bmp', '.jpeg', '.jpg', '.png', '.tif', '.tiff')):
continue

print(img_name)

filepath = os.path.join(img_path,img_name)

img = cv2.imread(filepath)

可以依次读入文件夹下的图片。

4.2 图片预处理

1
2
3
4
5
6
X, ratio = format_img(img, C)

def format_img(img, C):
img, ratio = format_img_size(img, C)
img = format_img_channels(img, C)
return img, ratio

主要涉及到两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def format_img_size(img, C):
img_min_side = float(C.im_size)
(height,width,_) = img.shape

if width <= height:
ratio = img_min_side/width
new_height = int(ratio * height)
new_width = int(img_min_side)
else:
ratio = img_min_side/height
new_width = int(ratio * width)
new_height = int(img_min_side)
img = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_CUBIC)
return img, ratio

def format_img_channels(img, C):
img = img[:, :, (2, 1, 0)]
img = img.astype(np.float32)
img[:, :, 0] -= C.img_channel_mean[0]
img[:, :, 1] -= C.img_channel_mean[1]
img[:, :, 2] -= C.img_channel_mean[2]
img /= C.img_scaling_factor
img = np.transpose(img, (2, 0, 1))
img = np.expand_dims(img, axis=0)
return img

format_img_size()函数用来调整图片大小,将图片的width和height较短的一边调整到600pixels大小(config.py文件中默认设置im_size = 600),另一边等比例调整,插值方法选择INTER_CUBIC
format_img_channels()函数调整了图片到RGB顺序(配合预训练模型的数据格式),再将像素值减去均值并缩放,img_scaling_factorconfig.py文件中给了默认值1.0,最后将通道数放在前面,并新增一个维度(在axis=0的位置增加,相当于在最前面加一个维度)。

1
2
if K.image_dim_ordering() == 'tf':
X = np.transpose(X, (0, 2, 3, 1))

使用tensorflow后端,再将通道数放在后面(变换前的维度顺序:new axis, [channels], height, width。变换后为:new axis, height, width, [channels]).

4.3 一个test实例

1
[Y1, Y2, F] = model_rpn.predict(X)

[Y1, Y2, F]对应RPN网络的输出x_class、x_regr、shared_layers(feature map)

查看中间过程张量的维度

测试过程中,输入一张height×width×channels: 375×500×3大小的图片。由上面所说的图片处理过程,处理之后的图片的短边height (375<500) 放大为600,长边width按比例放大成800 (600/375*500=800). 通过Debug,可以验证如下张量的维度:

  • X的维度: (1, 600, 800, 3)
  • Y1的维度: (1, 38, 50, 9)
  • Y2的维度: (1, 38, 50, 36)

Y1Y2的通道数分别是9和36,很好理解,因为定义RPN最后两个输出时分别使用了9 (anchor scale) 和9*4个卷积核。这两个通道数中的值对应着9种尺度下的objectness score及其4个对应平移缩放参数的box regression信息。的由于RPN过程中都是同尺度卷积(padding='same'kernel_size=(1, 1))。所以Y1Y2heightwidth维度和RPN的输入feature map的对应相同,为(38, 50)(即使输入图片大小不同,经过预处理,feature map的heightwidth总是(38, 50),shared layers为resnet50).

rpn_to_roi

1
R = roi_helpers.rpn_to_roi(Y1, Y2, C, K.image_dim_ordering(), overlap_thresh=0.7)

roi_helpers.py文件中的函数是RPN网络的关键,见Keras版Faster RCNN——test过程 (2) roi_helpers.

得到的R是筛选出按概率大小排序的box的坐标信息(feature map上的),shape:(selected_boxes_number, 4). 经过设置,这里的selected_boxes_number取值为300.

1
2
3
4
5
6
7
# convert from (x1,y1,x2,y2) to (x,y,w,h)
R[:, 2] -= R[:, 0]
R[:, 3] -= R[:, 1]

# apply the spatial pyramid pooling to the proposed regions
bboxes = {}
probs = {}

R中的(x1,y1,x2,y2)数据转化为(x,y,w,h),其实刚开始的数据格式就是(x,y,w,h),因为中间过程要计算IoU,才转换为(x1,y1,x2,y2).

bboxesprobs是最后用来在图像上显示的框信息,物体类别和概率值。

处理和映射ROIs

1
for jk in range(R.shape[0]//C.num_rois + 1):

batch思想,按批处理rois,C.num_rois设置为32;“+1”是为了处理最后一个批次。这个循环中的处理如下:

  1. 选择批次输入model_classifier_only进行预测:

    • 对于大小为32的batch,仅做如下处理

      1
      2
      3
      4
      ROIs = np.expand_dims(R[C.num_rois*jk:C.num_rois*(jk+1), :], axis=0)

      if ROIs.shape[1] == 0:
      break

      参考:numpy.expand_dims( ).
      增加了一个新维度后,ROIs的维度: (1, 32, 4).

    • 对于最后一个batch,大小可能小于32,额外进行如下处理

      1
      2
      3
      4
      5
      6
      7
      8
      if jk == R.shape[0]//C.num_rois:
      #pad R
      curr_shape = ROIs.shape
      target_shape = (curr_shape[0],C.num_rois,curr_shape[2])
      ROIs_padded = np.zeros(target_shape).astype(ROIs.dtype)
      ROIs_padded[:, :curr_shape[1], :] = ROIs
      ROIs_padded[0, curr_shape[1]:, :] = ROIs[0, 0, :]
      ROIs = ROIs_padded

      以默认的300个boxes举例,最有一个batch的数量是12,此时经过前面的ROIs = np.expand_dims()ROIs的shape是(1, 12, 4),注意:切片操作取超过index长度的数据时,自动取到最后一个。这里的目标是将当前12个boxes的batch重新填满(pad to 32)。前面12个为当前的boxes,剩下的boxes用当前batch的第一个box信息(ROIs[0, 0, :])填充。

    前面已经讲过model_classifier_only这个模型的构建,用它预测每个batch:

    1
    [P_cls, P_regr] = model_classifier_only.predict([F, ROIs])

    F是feature map。

    P_cls是前面介绍过的model_classifier_only网络输出的判断为每个类别的概率,维度为(1, 32, 21)。最后一个维度21是20类物体+1个类“bg”表示背景
    P_regr表示框回归信息,维度是(1, 32, 80),”bg”类没有框回归信息。

  2. 对每一个box筛选出预测结果中概率最高的物体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    for ii in range(P_cls.shape[1]):

    if np.max(P_cls[0, ii, :]) < bbox_threshold or \
    np.argmax(P_cls[0, ii, :]) == (P_cls.shape[2] - 1):
    continue

    cls_name = class_mapping[np.argmax(P_cls[0, ii, :])]

    if cls_name not in bboxes:
    bboxes[cls_name] = []
    probs[cls_name] = []

    (x, y, w, h) = ROIs[0, ii, :]

    cls_num = np.argmax(P_cls[0, ii, :])
    try:
    (tx, ty, tw, th) = P_regr[0, ii, 4*cls_num:4*(cls_num+1)]
    tx /= C.classifier_regr_std[0]
    ty /= C.classifier_regr_std[1]
    tw /= C.classifier_regr_std[2]
    th /= C.classifier_regr_std[3]
    x, y, w, h = roi_helpers.apply_regr(x, y, w, h, tx, ty, tw, th)
    except:
    pass
    bboxes[cls_name].append([C.rpn_stride*x, C.rpn_stride*y,
    C.rpn_stride*(x+w), C.rpn_stride*(y+h)])
    probs[cls_name].append(np.max(P_cls[0, ii, :]))

    注意这里的循环是针对一个box的预测结果

    if语句的作用是排除满足两种情况的box:(1)该box判断为某类的最大概率仍小于阈值(bbox_threshold这里设置为0.8)(2)对该box输出的概率结果中,背景类(在第[20]个位置)的概率最大。

    class_mapping是字典:{0: ‘person’, 1: ‘dog’, …, 20: ‘bg’},cls_name是代表当前box以最大概率分到的类别的字符串表示,如“bus”.

    model_classifier_only输出的reg层再对box的坐标,长宽进行修正,这次修正,得到的坐标,长宽信息就是在image上的对应位置了。tx /= C.classifier_regr_std[0]对应于训练过程的操作。

    apply_regr( )函数求预处理过的image上的对应位置坐标(feature map上的点信息经过回归参数调整后×C.rpn_strideapply_regr( )函数详情参考Keras版Faster RCNN——test过程 (2) roi_helpers

    最后将当前box对应到原图上的的proposal信息存起来,当前图片上只检测到了bus(其实只有一辆bus,但是有很多重叠框,后面会进行抑制):

    1
    2
    bboxes: {'bus': [[144, 80, 720, 496], [160, 80, 704, 464], [144, 80, 704, 480], [144, 48, 704, 512], [128, 64, 688, 480], [160, 64, 736, 496], [160, 64, 720, 496], [160, 80, 720, 496], [128, 32, 672, 496], [144, 80, 688, 480], [128, 64, 704, 496]]}
    probs: {'bus': [0.99375606, 0.90301329, 0.97631681, 0.94491804, 0.998245, 0.99156356, 0.9995597, 0.97257304, 0.90957648, 0.86869138, 0.98103034]}

绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for key in bboxes:
bbox = np.array(bboxes[key])

new_boxes, new_probs = roi_helpers.non_max_suppression_fast(bbox,
np.array(probs[key]),
overlap_thresh=0.5)
for jk in range(new_boxes.shape[0]):
(x1, y1, x2, y2) = new_boxes[jk,:]

(real_x1, real_y1, real_x2, real_y2) = get_real_coordinates(ratio, x1, y1, x2, y2)

cv2.rectangle(img,(real_x1, real_y1), (real_x2, real_y2),
(int(class_to_color[key][0]), int(class_to_color[key][1]),
int(class_to_color[key][2])),2)

textLabel = '{}: {}'.format(key,int(100*new_probs[jk]))
all_dets.append((key,100*new_probs[jk]))

(retval,baseLine) = cv2.getTextSize(textLabel,cv2.FONT_HERSHEY_COMPLEX,1,1)
textOrg = (real_x1, real_y1-0)

cv2.rectangle(img, (textOrg[0] - 5, textOrg[1]+baseLine - 5),
(textOrg[0]+retval[0] + 5, textOrg[1]-retval[1] - 5),
(0, 0, 0), 2)
cv2.rectangle(img, (textOrg[0] - 5,textOrg[1]+baseLine - 5),
(textOrg[0]+retval[0] + 5, textOrg[1]-retval[1] - 5),
(255, 255, 255), -1)
cv2.putText(img, textLabel, textOrg, cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 0), 1)

参考cv2.rectangle( )cv2.getTextSize( )cv2.putText( ).

注意:

  • 这里又进行了一次non_max_suppression_fast
  • 在预处理之前的image上绘制,需要使用get_real_coordinates( )函数将坐标除以缩放的比例(与预处理过程相反操作)
  • 三个cv2.rectangle( )函数的作用分别是,画出包围物体的框、画出text文字信息的框、使text的底色为白色(-1就是cv2.FILLED
1
2
3
4
print('Elapsed time = {}'.format(time.time() - st))
print(all_dets)
cv2.imshow('img', img)
cv2.waitKey(0)

最后,显示一些信息和图片,test过程至此结束!

提醒

  • 2.3 节的RoiPoolingConv过程中,自定义Keras层的方法还是不太了解。

  • 总共进行两次修正:RPN输出的reg是修正量,classifier输出的reg也是修正量(需要查看训练过程两个reg修正量的计算方式),RPN-reg是对feature map上框的修正,classifier是对预处理之后的image上框的修正。


----------over----------


文章标题:Keras版Faster RCNN——test过程 (1)

文章作者:Ge垚

发布时间:2018年05月21日 - 18:05

最后更新:2018年07月20日 - 16:07

原始链接:http://geyao1995.com/Faster_rcnn代码笔记_test_1/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。