keras版faster-rcnn算法详解(2.roi计算及其他)

keras版faster-rcnn算法详解(2.roi计算及其他)

上篇:keras版faster-rcnn算法详解(1.RPN计算)


上次提到计算出rpn,就可以用于训练第一阶段的网络了。今天谈一谈如何进一步完成整个end2end网络的训练。可能目前伴随着大家有几个疑问:

  • 通过ground true计算出来的class和regr和我们rpn网络输出的结果shape为什么是不匹配的?
  • 找出这么多region_proposal,在预测是不是乱七八糟的,还有很多重合的,如何取到合适的region呢?
  • bla bla。。。

在本文中会找到你想要的答案。先简单的梳理一下整体流程,关于rpn部分就简要一点,详细的可看上篇文章。

  1. 通过预训练网络输出feature_map.
  2. 根据feature_map尺寸和bbox,计算region_proposal。与作为rpn_model的label。
  3. 通过rpn计算roi,这一步优先选择概率大的region_proposal,并且过滤掉重合度高的region和不合理的region(比如x1>x2),只保留一个。
  4. 计算第三步筛选出的region与每个bbox的重合度,为每个region打标。
  5. 最后就是训练分类网络了。输入img和region,输出class和regr。

由于剩下的部分代码量也不少,也没必要一步步分析,挑几处重点的讲一下。

  • 为啥rpn model的output和label形状不匹配?
y_rpn_cls = np.concatenate([y_is_box_valid, y_rpn_overlap], axis=1)
y_rpn_regr = np.concatenate([np.repeat(y_rpn_overlap, 4, axis=1), y_rpn_regr], axis=1)

这里是因为region proposal过程针对每一个锚点的每一个anchor都是有输出的,其实有很多anchor是不可用的,在y_is_box_valid那个array里面有记录。那么我们在计算loss时,也是不计算这些anchor的。因此我们在输出时,将与输出等形状的y_is_box_valid array拼接起来,计算loss时做一个对应元素的乘法,就可以舍去这些anchor产生的loss了。所以regr那里要将y_is_box_valid repeat 4倍,再与输出concatenate起来。

  • 通过non_max_suppression_fast()过滤掉重合度高的region并保留最优的。
def non_max_suppression_fast(boxes, probs, overlap_thresh=0.9, max_boxes=300):
	pick = []
	# calculate the areas
	area = (x2 - x1) * (y2 - y1)
	# sort the bounding boxes 
	idxs = np.argsort(probs)
	# keep looping while some indexes still remain in the indexes
	# list
	while len(idxs) > 0:
		# grab the last index in the indexes list and add the
		# index value to the list of picked indexes
		last = len(idxs) - 1
		i = idxs[last]
		pick.append(i)
		# find the intersection
		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]])

		ww_int = np.maximum(0, xx2_int - xx1_int)
		hh_int = np.maximum(0, yy2_int - yy1_int)

		area_int = ww_int * hh_int

		# find the union
		area_union = area[i] + area[idxs[:last]] - area_int

		# compute the ratio of overlap
		overlap = area_int/(area_union + 1e-6)

		# delete all indexes from the index list that have
		idxs = np.delete(idxs, np.concatenate(([last],
			np.where(overlap > overlap_thresh)[0])))

		if len(pick) >= max_boxes:
			break

	# return only the bounding boxes that were picked using the integer data type
	boxes = boxes[pick].astype("int")
	probs = probs[pick]
	return boxes, probs

对region的index按照概率值进行排序,然后从后往前取,那么每次取到的都是剩下的region中概率最大的添加到pick[]里,然后把剩下的region中,与之重合度很高的删掉。循环下来,找出所有合适的region。作为model_classifier的输入之一。

  • 通过calc_iou()找出剩下的不多的region对应ground truth里重合度最高的bbox,从而获得model_classifier的和标签。
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)):
    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

      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']

对每个region遍历所有的bbox,找出重合度最高的。如果best_iou小于min_overlap,则作为副样本,大于max_overlap,则作为正样本。注意,这里的overlap阈值是针对classifier的,可以不同与之前的region_proposal,具体如何设置,有什么影响,可以自己思考一下。

  • 最后还是看一下模型的classifier部分的关键:ROIPOOLINGCONV
def call(self, x, mask=None):
        assert(len(x) == 2)
        img = x[0]
        rois = x[1]
        input_shape = K.shape(img)
        outputs = []
        for roi_idx in range(self.num_rois):
            x = rois[0, roi_idx, 0]
            y = rois[0, roi_idx, 1]
            w = rois[0, roi_idx, 2]
            h = rois[0, roi_idx, 3]
            
            row_length = w / float(self.pool_size)
            col_length = h / float(self.pool_size)
            num_pool_regions = self.pool_size
            #-----------省略theano部分--------------------
            elif self.dim_ordering == 'tf':
                x = K.cast(x, 'int32')
                y = K.cast(y, 'int32')
                w = K.cast(w, 'int32')
                h = K.cast(h, 'int32')

                rs = tf.image.resize_images(img[:, y:y+h, x:x+w, :], (self.pool_size, self.pool_size))
                outputs.append(rs)

        final_output = K.concatenate(outputs, axis=0)
        final_output = K.reshape(final_output, (1, self.num_rois, self.pool_size, self.pool_size, self.nb_channels))

这是一个自定义的keras layer,主要看call部分。之前我们通过各种计算,从model_rpn的输出中得到了比较靠谱的rois和对应的bbox了。通过设置num_rois,从这些rois中,提取num_rois数量的样本用于模型训练,把这些rois输入roipooling层,进行训练。


至此,这个keras版本的frcnn就介绍完了。其中还有很多细节没有提到,比如loss的计算,config的设置等。不过大致的思路应该讲清楚了。马上上手试试吧!对了,根据一些朋友私信提到的问题,再提供几个快速上手小tips:

  • 先用小数据集训练,便于快速看到效果,比如上篇中提到的猫狗数据集。
  • 用vgg做baselayer,便于理解。
  • 调整num_rois数量,可加快训练速度。


封面图来源:medium.com/alex-attia-b

一个基于该项目做的辛普森一家的人物检测,挺有意思的。


ps:真心求一份杭州的deep_learning+cv方向的工作机会。

编辑于 2017-09-16

文章被以下专栏收录