Keras版Faster RCNN——roi_helpers

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

这部分是对roi_helpers.py文件中函数的分析

rpn_to_roi( )

1. 参数

rpn_to_roi函数的定义参数和调用时传入的参数:

1
2
3
4
def rpn_to_roi(rpn_layer, regr_layer, C, dim_ordering, use_regr=True, max_boxes=300,overlap_thresh=0.9):
...

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

shared layers使用resnet50.

  • Y1的维度:(1, 38, 50, 9)
  • Y2的维度:(1, 38, 50, 36)

2. 内部分析

1
regr_layer = regr_layer / C.std_scaling

数值处理过程,C.std_scalingconfig.py文件中默认设置为4.0,训练过程中有对应的乘以C.std_scaling步骤。

1
2
anchor_sizes = C.anchor_box_scales
anchor_ratios = C.anchor_box_ratios

anchor box的大小和形状比例的scale,在config.py文件中:

  • anchor_box_scales = [128, 256, 512]
  • anchor_box_ratios = [[1, 1], [1./math.sqrt(2), 2./math.sqrt(2)], [2./math.sqrt(2), 1./math.sqrt(2)]]
1
2
3
4
5
assert rpn_layer.shape[0] == 1

(rows, cols) = rpn_layer.shape[1:3]

A = np.zeros((4, rpn_layer.shape[1], rpn_layer.shape[2], rpn_layer.shape[3]))

对于tensorflow后端,定义空的nd_array: A ,维度是: (4, 38, 50, 9).

1
2
for anchor_size in anchor_sizes:
for anchor_ratio in anchor_ratios:

开始遍历3×3=9种anchor box scale,对每个scale,都进行下面的操作。

anchor_x = (anchor_size * anchor_ratio[0])/C.rpn_stride 
anchor_y = (anchor_size * anchor_ratio[1])/C.rpn_stride

rpn_strideconfig.py文件中默认设置为16(shared layers用的是“resnet50”), 因为image_size(after scaled) / feature_map_size = 16,卷积核在feature map上移动1个像素点对应在输入image上移动16个像素点。

anchor_xanchor_y代表每种anchor box在feature map上的对应的widthheight。比如anchor_size: 128, anchor_ratio: [1, 1]的anchor box对应着(8, 8)大小的anchor_xanchor_y

regr = regr_layer[0, :, :, 4 * curr_layer:4 * curr_layer + 4]
regr = np.transpose(regr, (2, 0, 1))

regr_layer 的维度大小: (1, 38, 50, 36) 。将当前scale对应的4个坐标regression信息提取到变量regr中,regr的维度大小: (38, 50, 4);再调整一下维度顺序(channels放到第一个维度),最终regr的维度大小为:(4, 38, 50)

X, Y = np.meshgrid(np.arange(cols),np. arange(rows))

生成两个矩阵X、Y用来表示坐标,参考文档:numpy.meshgrid.

X、Y的维度均为(38, 50). X的每一列的值都相同,为当前列的indexY的每一行的值都相同,为当前行的index

A[0, :, :, curr_layer] = X - anchor_x/2
A[1, :, :, curr_layer] = Y - anchor_y/2
A[2, :, :, curr_layer] = anchor_x
A[3, :, :, curr_layer] = anchor_y

变量curr_layer初始值为0,在每次for循环中都+1,用来遍历9个scale。

(anchor_x/2,anchor_y/2)表示box在feature map上映射的左上角坐标。 。

if use_regr:
    A[:, :, :, curr_layer] = apply_regr_np(A[:, :, :, curr_layer], regr)

use_regr已经设置为True,表示在预测过程中使用RPN网络reg输出层的信息进行proposal位置调整。

页内跳转:apply_regr_np( )函数说明

此时,A中的box信息已经经过修正。

A[2, :, :, curr_layer] = np.maximum(1, A[2, :, :, curr_layer])
A[3, :, :, curr_layer] = np.maximum(1, A[3, :, :, curr_layer])
A[2, :, :, curr_layer] += A[0, :, :, curr_layer]
A[3, :, :, curr_layer] += A[1, :, :, curr_layer]

用到了np.maximum( )broadcasting机制,每个表示box长宽的元素和1比较,小于1则变为1(让box的widthheight不小于1)。

经过上面的计算,A[2]、A[3]存放的就是右下角坐标。

此时,A中的数据是box的左上角和右下角坐标(在feature map上映射的坐标)。

A[0, :, :, curr_layer] = np.maximum(0, A[0, :, :, curr_layer])
A[1, :, :, curr_layer] = np.maximum(0, A[1, :, :, curr_layer])
A[2, :, :, curr_layer] = np.minimum(cols-1, A[2, :, :, curr_layer])
A[3, :, :, curr_layer] = np.minimum(rows-1, A[3, :, :, curr_layer])

规定box的左上角和右下角坐标分别不能超过image的左上角和右下角。

至此,对每个scale的操作就完成了。

总结一下,关于A的计算过程:

  1. 修正之前,A中按维度顺序存放的是中心坐标$(x, y)$,宽度$w$,高度$h$

  2. 通过apply_regr_np()函数修正

  3. 每个box的左上角坐标为 ($x-w/2,y-h/2$),记作$(x_1,y_1)$,右下角坐标为$(x_1+w,y_1+h)$,记作$(x_2,y_2)$

  4. 将$(x_1,y_1)$、$(x_2,y_2)$按维度顺序重新存放到A

遍历完9个scale后,得到了所有box,个数为:$9×38×50=17100$ (shared layers是resnet50的情况下)。A的shape:(4, 38, 50, 9)

1
2
all_boxes = np.reshape(A.transpose((0, 3, 1,2)), (4, -1)).transpose((1, 0))
all_probs = rpn_layer.transpose((0, 3, 1, 2)).reshape((-1))

将shape为(4, 38, 50, 9)的坐标变成shape为(17100, 4)的array存放到all_boxes中。

将shape为(1, 38, 50, 9)的RPN-cls层输出的张量(表示objectness probability)变成shape为(17100, )的array存放到all_probs中。

注意这里的维度排序方法!先把维度顺序转变为(0, 3, 1, 2),坐标信息的shape变成(4, 9, 38, 50)再进行reshape(A, (4, -1))操作,会按第二个维度9展开,如果直接对(4, 38, 50, 9)进行reshape(A, (4, -1))操作,按第二个维度38展开就达不到目的。

1
2
3
4
5
6
7
8
9
x1 = all_boxes[:, 0]
y1 = all_boxes[:, 1]
x2 = all_boxes[:, 2]
y2 = all_boxes[:, 3]

idxs = np.where((x1 - x2 >= 0) | (y1 - y2 >= 0))

all_boxes = np.delete(all_boxes, idxs, 0)
all_probs = np.delete(all_probs, idxs, 0)

当右下角的坐标点超过了左上角的坐标点,说明产生了错误,将这些点删除。

numpy.where( )找到array中值为1的位置。

all_boxes = np.delete(all_boxes, idxs, 0)表示在第0个维度删除这些点(一次删除一组点)。

1
2
3
result = non_max_suppression_fast(all_boxes, all_probs, 
overlap_thresh=overlap_thresh,
max_boxes=max_boxes)[0]

页内跳转:non_max_suppression_fast( )函数说明

non_max_suppression_fast( )返回的是一个列表:

  • non_max_suppression_fast( )[0]是筛选出按概率大小排序的box的坐标信息(feature map上的),shape:(selected_boxes_number, 4)
  • non_max_suppression_fast( )[1]是筛选出的box的按大小排序的概率,shape:(selected_boxes_number, )

这里的result只取了[0],返回排序后的box信息。

apply_regr_np( )

1. 参数

apply_regr_np函数的定义参数和调用时传入的参数:

1
2
3
4
def apply_regr_np(X, T):
...

A[:, :, :, curr_layer] = apply_regr_np(A[:, :, :, curr_layer], regr)

2. 内部分析

传入的两个参数是AregrA(X)是我们根据anchor scale直接生成的nd_arrayregr(T)是RPN网络reg层输出的张量,代表anchor box需要修正的偏移量

X、T的维度都是(4, 38, 50).

函数的主要起一个修正作用:

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
x = X[0, :, :]
y = X[1, :, :]
w = X[2, :, :]
h = X[3, :, :]

tx = T[0, :, :]
ty = T[1, :, :]
tw = T[2, :, :]
th = T[3, :, :]

cx = x + w/2.
cy = y + h/2.
cx1 = tx * w + cx
cy1 = ty * h + cy

w1 = np.exp(tw.astype(np.float64)) * w
h1 = np.exp(th.astype(np.float64)) * h
x1 = cx1 - w1/2.
y1 = cy1 - h1/2.

x1 = np.round(x1)
y1 = np.round(y1)
w1 = np.round(w1)
h1 = np.round(h1)

return np.stack([x1, y1, w1, h1])

在训练过程中,对于box中心点偏移量的定义是:偏差÷高度/宽度;对于box边长偏移量的定义是:(log(偏差))÷高度/宽度。所以,在测试过程中反向操作了一波:cx1 = tx * w + cxw1 = np.exp(tw.astype(np.float64)) * w.

np.round( )函数将小数化为整数。

关于np.stack([x1, y1, w1, h1]),见官方文档: numpy.stack( ). 默认沿axis=0方向堆叠。所以输出的维度还是:(4, 38, 50).

non_max_suppression_fast( )

1. 参数

non_max_suppression_fast函数的定义参数和调用时传入的参数:

1
2
3
4
def non_max_suppression_fast(boxes, probs, overlap_thresh=0.9, max_boxes=300):
...

result = non_max_suppression_fast(all_boxes, all_probs, overlap_thresh=overlap_thresh, max_boxes=max_boxes)[0]

假设经过前面的筛选,合格的box数量为:16986,那么到这一步:

  • boxes的shape为(16986, 4).
  • probs的shape为(16986, ),表示objectness probability.

2. 内部分析

1
2
3
4
5
6
7
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]

np.testing.assert_array_less(x1, x2)
np.testing.assert_array_less(y1, y2)

官方文档:numpy.testing.assert_array_less

当不满足条件“x1的对应元素均比x2小”时报错。

其他的一些用于array的assert参考文档numpy.testing.

1
2
if boxes.dtype.kind == "i":
boxes = boxes.astype("float")

类型转换,整型转浮点型。参考numpy.dtype.kind. 由于是切片操作,修改boxes中的数据,x1、y1、x2、y2也会随之改变。

1
2
3
4
5
pick = []

area = (x2 - x1) * (y2 - y1)

idxs = np.argsort(probs)

pick在后面用到;计算每个box的面积放入area中;对objectness probability由小到大排序,参考numpy.argsort( ),得到的是indexes,放入idxs中。

1
while len(idxs) > 0:

objectness probability从大到小依次选择box进行判断,下面是详细过程。

  1. 求交集 (Intersection)

    1
    2
    3
    4
    5
    6
    7
    8
    last = len(idxs) - 1
    i = idxs[last]
    pick.append(i)

    xx1_int = np.maximum(x1[i], x1[idxs[:last]])
    yy1_int = np.maximum(y1[i], y1[idxs[:last]])
    xx2_int = np.minimum(x2[i], x2[idxs[:last]])
    yy2_int = np.minimum(y2[i], y2[idxs[:last]])

    i表示最大的 probability 所在的 index
    xx1_int,yy1_int,xx2_int,yy2_int 为交集区域的坐标,后缀_intintersection的缩写,不代表整型。他们表示其他boxes与当前最大概率box的交集(如果存在的话)的左上角与右下角(box和交集区域都是矩形),如果交集不存在,这样计算就不对了(可以画图了解一下),对于没有交集的box,算出来的(xx1_int,yy1_int)大于(xx2_int,yy2_int),可以用这个性质将交集设为0,方法如下:

    1
    2
    3
    4
    ww_int = np.maximum(0, xx2_int - xx1_int)
    hh_int = np.maximum(0, yy2_int - yy1_int)

    area_int = ww_int * hh_int

    至此,就算出了当前最大objectness概率的box与其他boxes交集的宽度ww_int, 高度hh_int和交集区域面积area_int. 他们的维度都是(boxes_num, ).

  2. 求并集 (Union)

    1
    area_union = area[i] + area[idxs[:last]] - area_int

    很好理解,两个box的并集面积就是两个box的面积减去交集面积。

  3. 求IoU (Intersection over Union)

    1
    overlap = area_int/(area_union + 1e-6)

    为了防止分母为零,所以加上1e-6.

  4. 判断是否满足阈值要求

    1
    2
    3
    4
    5
    idxs = np.delete(idxs, np.concatenate(([last],
    np.where(overlap > overlap_thresh)[0])))

    if len(pick) >= max_boxes:
    break

    numpy.concatenate( )numpy.where( ).

    将IoU值大于阈值的box和当前判断过的box删除(当前判断过的box已经存放在了pick列表中)。np.where(overlap > overlap_thresh)[0]中的[0]很关键,如果不加[0],返回的就是(array([3, 4, 5, ...]),),取第0维度之后,才是想要的[3, 4, 5, ...].

    如果达到了想要的box数量,就终止循环。

最后,用以下的三行代码:

1
2
3
boxes = boxes[pick].astype("int")
probs = probs[pick]
return boxes, probs

返回筛选的box信息。

apply_regr( )

1. 参数

1
apply_regr(x, y, w, h, tx, ty, tw, th)

输入的是一组原始信息(已经被RPN网络的reg层输出调整过),一组调整的参数(classifier模型输出的回归参数)。

2. 内部分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
try:
cx = x + w/2.
cy = y + h/2.
cx1 = tx * w + cx
cy1 = ty * h + cy
w1 = math.exp(tw) * w
h1 = math.exp(th) * h
x1 = cx1 - w1/2.
y1 = cy1 - h1/2.
x1 = int(round(x1))
y1 = int(round(y1))
w1 = int(round(w1))
h1 = int(round(h1))

return x1, y1, w1, h1

except ValueError:
return x, y, w, h
except OverflowError:
return x, y, w, h
except Exception as e:
print(e)
return x, y, w, h

也是根据训练过程中回归参数的计算方式,逆向计算。通过classifier模型输出的回归参数再次调整。

calc_iou( )

1. 参数

1
2
3
4
5
def calc_iou(R, img_data, C, class_mapping): 
...

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)
X2, Y1, Y2, IouS = roi_helpers.calc_iou(R, img_data, C, class_mapping)

R是筛选出的按概率大小排序的box的坐标信息(feature map上的),shape:(selected_boxes_number, 4).

img_data是包含图片RoIs信息的字典,形如:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
'filepath': '/home/ubuntu/Downloads/dataset/VOCdevkit/VOC2012/JPEGImages/2012_003845.jpg', 'width': 333,
'height': 500,
'bboxes': [{
'class': 'person',
'x1': 29,
'x2': 163,
'y1': 108,
'y2': 406,
'difficult': False
}],
'imageset': 'trainval'
}

C是一些参数,参考config.py.

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

2. 内部分析

1
2
3
4
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)

get_new_img_size( )函数将图片宽度和高度中较短的一边变为600(config.py文件中默认设置im_size = 600),另外一边按比例变化。在测试过程中有一个相同作用的函数:format_img_size,插值方法选择的都是INTER_CUBIC.

1
2
3
4
5
6
7
8
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))

求得的gta是ground truth box坐标在feature map上的映射坐标。

1
for ix in range(R.shape[0]):

接下来是对300个按概率筛选出的predict box(rpn输出,在feature map上修正过的)进行逐一判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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)):
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

对每一个ground truth再做判断。

iou函数计算anchor box与ground truth box的交幷比,在Keras版Faster RCNN——test过程 (2) roi_helpers——non_max_suppression_fast( )中已经介绍过相关的求交集、并集、IoU的方法。

两个for循环就是对筛选后的每一个predict box和每一个ground truth box都求交幷比,并记录差生最大IoU时的IoU数值和对应的ground truth 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
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

这里将每个predict box与所有ground truth box的最佳IoU (best_iou)和两个阈值比较,分为3种情况:

  • best_iou < C.classifier_min_overlap

    说明这个predict box不好,直接跳过,丢弃。

  • C.classifier_min_overlap <= best_iou < C.classifier_max_overlap

    将当前的predict box标记为“bg”类。问题:这和论文的3.1.2节对anchor赋值正负,中立有没有关系?

  • C.classifier_max_overlap <= best_iou

    将当前的predict box标记为20类中对应的类。由论文中3.1.2节公式(2)计算回归参数。注意calc_rpn( )中也用到了这个公式计算回归参数。问题,两处分别计算的是?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class_num = class_mapping[cls_name]
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[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))

class_label是一个长度为21的列表,当前predict box对应的类位置上标记为1,其余为0. y_class_num也就是将每个predict box对应的class_label放在一个列表里(输入的是300个predict box,但经过IoU筛选之后变少)。

coords、labels都是长度为80的列表。

如果当前predict box对应的类不是背景,就将回归参数×C.classifier_regr_std后存放到coords的对应位置上,将对应的labels四个位置全置1。

如果当前predict box对应的是背景类,相关信息全为0。

所有best_iou ≥ C.classifier_min_overlap 的 predict boxes的coords全存到y_class_regr_coords中,所有的labels全存到y_class_regr_label中。

至此,对一个predict box的处理结束。

1
2
3
4
5
6
7
8
if len(x_roi) == 0:
return None, None, None, None

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)

return np.expand_dims(X, axis=0), np.expand_dims(Y1, axis=0), np.expand_dims(Y2, axis=0), IoUs

X (x_roi)是存放 best_iou ≥ C.classifier_min_overlap 的 predict boxes 坐标信息[x1, y1, w, h]。

IoUs是存放的对应x_roi位置的predict box的最大IoU值。

Y1是对应selected box的物体标签,shape:(selected_boxes_num, 21). 问题,实际上很多selected box都被判断为背景(最后一个位置上是1,其他都为0。这样判断为背景导致后面Y2也全为0).

np.concatenate之后,Y2的shape: (selected_boxes_num, 160)。 (selected_boxes_num, :80)表示每个物体类的回归参数×C.classifier_regr_std后的值, (selected_boxes_num, 80:)表示label,即这四个值的对应位置是1。如果判断是背景,相关信息都是0.

np.expand_dims(x, 0)在最前面加一个维度。


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


文章标题:Keras版Faster RCNN——roi_helpers

文章作者:Ge垚

发布时间:2018年05月29日 - 17:05

最后更新:2018年06月11日 - 20:06

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

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