2013年的RCNN,ICCV2015年的fast rcnn,以及NIPS2015的faster rcnn逐步确定了two stage目标检测的框架。
本文将会主要讲述三个方面的内容:
从上面的rcnn的结构图可以RCNN启发式的Selective Search选取候选框,用CNN提取到的特征训练分类器,以及进行bbox回归
但是需要事先提取多个候选框,这就会占用很多的内存,提取之后的region proposals需要归一化到统一尺寸,导致形变使得检测效果不佳,每个region proposals都需要经过一次CNN,导致特征重复提取,而且浪费时间
RCNN参考阅读:
https://zhuanlan.zhihu.com/p/23006190
https://blog.csdn.net/WoPawn/article/details/52133338
由于RCNN从原图中选出的所有的ROI都要经过一次convnet,这必然会导致速度非常慢,fast rcnn便是基于此进行改进。
从fast rcnn的结构图中可以看出,不同于rcnn的是,fast rcnn是先将整个图片作为convnet的输入,然后在输出的feature map上面选取roi,然后再将得到的roi区域的特征进行后续的全连接层进行分类,回归。这样确实极大的提高了rcnn的速度,但是存在一个问题,就是在feature map上面选取roi的时候,由于roi的尺寸都是不同的,必须将roi的特征统一到相同的尺寸才可以统一送到后续的全连接层中,这里就需要用到ROI pooling,或者ROI Align
讲解ROI pooling与ROI Align
ROI pooling与ROI Align参考阅读:
http://blog.leanote.com/post/[email protected]/b5f4f526490b
https://www.jianshu.com/p/8b7d7036d715
fast rcnn参考阅读:
https://blog.csdn.net/gentelyang/article/details/80469553
https://blog.csdn.net/Wonder233/article/details/53671018
https://blog.csdn.net/WoPawn/article/details/52463853
rcnn,fast rcnn在选取roi的时候一般都会采用selective search等方式,faster rcnn则是直接提出了一种RPN来选取若干region proposal进一步提高了检测的精度。
以下为mmdetection中faster rcnn的实现思路。
base_anchor的生成,给base_anchors分配gt的过程跟retinanet的过程一样,具体参见:Focal Loss for Dense Object Detection。不同的是在mmdetection的faster rcnn实现中,anchor_strides为[4,8,16,32,64], anchor_ratios=[0.5, 1.0, 2.0],但是scale只有一个,值为8。三种ration,一种scale,所以相当于在每个位置会产生三个base_anchor
网络结构图中的rpn_conv,rpn_cls,rpn_reg是所有pyramid feature共享的,可以看出网路主要由两部分组成,一部分是RPN网络,用于产生region proposal,这里会计算一个RNP loss来使得RNP产生的region proposal越来越好。另外一部分就是RPN产生的region proposal经过ROI pooling之后在上图中的feature map中在提取特征,最后经过全连接层之后得到的bbox_pred,cls_prob与选出的region propos之间计算一个loss。下面的章节将依次计算这两个loss
这里以三分类的任务为例,首先考虑stride=4的时候,经过rpn_cls后输出rpn_cls_score[1,3,a,a], 这里的a其实就是图像原尺寸除以4之后的结果。经过rpn_reg后输出rnp_bbox_pred[1,12,a,a]。这里的3表示每个位置是前景即目标的概率,12表示该位置有三个框,每个框由四个坐标表示。经过RPN网络得到的rpn_cls_score[1,3,a,a],rnp_bbox_pred[1,12,a,a]会跟开始产生的base_anchor计算一个RPN损失,主要目的是为了区别背景与目标,进而选择出region proposal。计算RNP loss的方法如下:
def sample(self,
assign_result,
bboxes,
gt_bboxes,
gt_labels=None,
**kwargs):
"""Sample positive and negative bboxes.
This is a simple implementation of bbox sampling given candidates,
assigning results and ground truth bboxes.
Args:
assign_result (:obj:`AssignResult`): Bbox assigning results.
bboxes (Tensor): Boxes to be sampled from.
gt_bboxes (Tensor): Ground truth bboxes.
gt_labels (Tensor, optional): Class labels of ground truth bboxes.
Returns:
:obj:`SamplingResult`: Sampling result.
"""
bboxes = bboxes[:, :4]
gt_flags = bboxes.new_zeros((bboxes.shape[0], ), dtype=torch.uint8)
if self.add_gt_as_proposals:
bboxes = torch.cat([gt_bboxes, bboxes], dim=0)
assign_result.add_gt_(gt_labels)
gt_ones = bboxes.new_ones(gt_bboxes.shape[0], dtype=torch.uint8)
gt_flags = torch.cat([gt_ones, gt_flags])
num_expected_pos = int(self.num * self.pos_fraction) #计算出选择的正样本的个数
pos_inds = self.pos_sampler._sample_pos(
assign_result, num_expected_pos, bboxes=bboxes, **kwargs) #从所有正样本中随机选择出num_expected_pos 个正样本
# We found that sampled indices have duplicated items occasionally.
# (may be a bug of PyTorch)
pos_inds = pos_inds.unique()
num_sampled_pos = pos_inds.numel()
num_expected_neg = self.num - num_sampled_pos #负样本等于总共需要的样本数减去已经选择正样本数目
if self.neg_pos_ub >= 0:
_pos = max(1, num_sampled_pos)
neg_upper_bound = int(self.neg_pos_ub * _pos)
if num_expected_neg > neg_upper_bound:
num_expected_neg = neg_upper_bound
neg_inds = self.neg_sampler._sample_neg(
assign_result, num_expected_neg, bboxes=bboxes, **kwargs) #从所有负样本中随机选择num_expected_neg个负样本
neg_inds = neg_inds.unique()
return SamplingResult(pos_inds, neg_inds, bboxes, gt_bboxes,
assign_result, gt_flags)
经过了上述了RNP loss之后,我们可以认为RNP最后输出的rpn_cls_score[1,3,a,a],rnp_bbox_pred[1,12,a,a]可以较好的作为选取region proposal的依据,具体做法如下:
1.在每pyramid feature的每一个level上,首先rpn_cls_score resize为rpn_cls_score[a*a,3],np_bbox_pred[1,12,a,a]resize为[a*a*3,4]之后经过sigmoi在进行排序选出得到最高的num_pre=2000个rpn_bbox_pred(这里代表着与gt的差值), rpn_bbox_pred与base_anchor反变换后变成proposals,经过nms=0.7选出不多于nms_post=2000个
2.经过上面的所有level之后选出的proposals,在经过proposal的得分score选出最多max_num=2000个proposals生成proposal_list。
def get_bboxes_single(self,
cls_scores,
bbox_preds,
mlvl_anchors,
img_shape,
scale_factor,
cfg,
rescale=False):
mlvl_proposals = []
for idx in range(len(cls_scores)): #对 def get_bboxes_single(self,
cls_scores,
bbox_preds,
mlvl_anchors,
img_shape,
scale_factor,
cfg,
rescale=False):
mlvl_proposals = []
for idx in range(len(cls_scores)): #对pyramid feature的每一个level进行选择
rpn_cls_score = cls_scores[idx]
rpn_bbox_pred = bbox_preds[idx]
assert rpn_cls_score.size()[-2:] == rpn_bbox_pred.size()[-2:]
anchors = mlvl_anchors[idx]
rpn_cls_score = rpn_cls_score.permute(1, 2, 0)
if self.use_sigmoid_cls:
rpn_cls_score = rpn_cls_score.reshape(-1)
scores = rpn_cls_score.sigmoid()
else:
rpn_cls_score = rpn_cls_score.reshape(-1, 2)
scores = rpn_cls_score.softmax(dim=1)[:, 1]
rpn_bbox_pred = rpn_bbox_pred.permute(1, 2, 0).reshape(-1, 4)
if cfg.nms_pre > 0 and scores.shape[0] > cfg.nms_pre:
_, topk_inds = scores.topk(cfg.nms_pre) #选出最好的nms_pre个
rpn_bbox_pred = rpn_bbox_pred[topk_inds, :]
anchors = anchors[topk_inds, :]
scores = scores[topk_inds]
proposals = delta2bbox(anchors, rpn_bbox_pred, self.target_means,
self.target_stds, img_shape)
if cfg.min_bbox_size > 0:
w = proposals[:, 2] - proposals[:, 0] + 1
h = proposals[:, 3] - proposals[:, 1] + 1
valid_inds = torch.nonzero((w >= cfg.min_bbox_size) &
(h >= cfg.min_bbox_size)).squeeze()
proposals = proposals[valid_inds, :]
scores = scores[valid_inds]
proposals = torch.cat([proposals, scores.unsqueeze(-1)], dim=-1)
proposals, _ = nms(proposals, cfg.nms_thr) #nms去除一些proposals
proposals = proposals[:cfg.nms_post, :]
mlvl_proposals.append(proposals)
proposals = torch.cat(mlvl_proposals, 0)
if cfg.nms_across_levels:
proposals, _ = nms(proposals, cfg.nms_thr)
proposals = proposals[:cfg.max_num, :]
else:
scores = proposals[:, 4]
num = min(cfg.max_num, proposals.shape[0])
_, topk_inds = scores.topk(num) #所有level中选出的proposals在根据得分选出num个
proposals = proposals[topk_inds, :]
return proposals
用RNP选出2000个proposals组成的proposal_list之后,会将gt也加入到这个proposal_list中(???,可能会为了确保至少有一个正样本吧),接着按照原来的一样的步骤为proposal_list分配gt,不同的是这里的neg_iou_thr=0.5,pos_iou_thr=0.5,最后按照正样本与负样本1:1选取512个proposals(选择方法与RPN阶段选取256个anchor一样)。选取的这512个proposals通过ROI Align的方法在特征图上提取特征得到roi_features[512,256,7,7],经过最后的全连接层之后得到clc_score[512,4], bbox_pred[512,16].这里的4表示有四个类(三个目标类加上背景类),[512,16]表示一共有512个位置,每个位置有四个类,每个类有一个bbox表示。
最后一步就是用得最后得到的512个proposals[512,4]与clc_score[512,4], bbox_pred[512,16].分别计算分类损失与回归损失。
分类损失:lables[:num_pos]=pos_gt_lables(512个proposals被分配gt之后得到的lable,num_pos表示正样本的数目,后面的元素当成背景类) , lable_weigths[num_pos, num_reg]=1, loss_clc = crossEntropyloss(clc_socre,lables)*lable_weights
回归损失:bbox_target[num_pos]=pos_bbox_target, bbox_weights[:num_pos]=1, 在bbox_pred[512,16]每行中选出指定类别对于的bbox变为[512,4], loss_reg=L1loss(proposals,bbox_pred]*bbox_weights
1.首先跟在train中一样的方式选出得分前1000的proposals,只不过参数有所区别
test_cfg = dict(
rpn=dict(
nms_across_levels=False,
nms_pre=1000,
nms_post=1000,
max_num=1000,
nms_thr=0.7,
min_bbox_size=0),
rcnn=dict(
score_thr=0.05, nms=dict(type='nms', iou_thr=0.5), max_per_img=100)
2.用选出的1000个proposals变为roi[1000,5]在FPN不同的level(取得前四个level???)中提取roi_features[1000,256,7,7],经过最后的全连接层之后得到cls_score[1000,4],bbox_pred[1000,16] ,cls_score= F.softmax(cls_score, dim=1),用roi[:,1:]与bbox_pred[1000,16]变换生成最后的bboxs[1000,16], bboxes /= scale_factor(消除训练时候的尺寸变换).
3.cls_score[1000,4]中选取每一类,即每一列(背景类除外),选择每一列中大于score_thr=0.05的行,用这个行以及对应的类别label在bboxes[1000,16]中选出对应类的bboxs。最后在用NMS得到最后的bboxs以及为每个bboxs分配对应类的label。
1.计算loss的时候,代码中写的是reg_loss与clc_loss的比重是1:1,即认为两个损失同等重要,难道不会一个loss比较大导致最后loss被某个loss所主导吗
算出来的最后损失除以正样本数目问题
多个二分类与多分类损失的区别
ROI Align参考:
http://blog.leanote.com/post/[email protected]/b5f4f526490b
https://www.jianshu.com/p/8b7d7036d715