总览
在Faster-RCNN的配置文件中,关于nms的设置放在model training and testing settings下,分别作用于rpn和rcnn部分网络的输出,在rpn部分设置的参数包括:
nms_across_levels=False,
nms_pre=2000,
nms_post=1000,
nms_thr=0.7,
在rcnn部分设置的参数包括:
score_thr=0.05,
nms=dict(type='nms', iou_threshold=0.5),
max_per_img=100
注意到在train_cfg的rcnn部分未设置相关参数。
RPN的nms实现
-
nms_across_levels
该参数只在
GARPNHead
下用到,由于Faster-rcnn中设置的普通的RPNHead
所以该参数并不会真实的使用。该参数会作用在RPN生成proposal的时候,若设为True,则会对rpn网络生成的所有level的proposal一起做nms,采用的nms方式为mmcv.ops下的nms函数。具体的,这里采用了最基础的nms,相应的nms_thr设置的就是此时iou的阈值,具体的nms实现方法见nms.cpp下的nms函数。对应的nms_thr设置的越小,proposal之间的重叠程度越低,proposal数量越小。 -
nms_pre
如果一个level生产的proposal数量大于nms_pre,则会按照score进行排序之后取前nms_pre个保留。
-
nms_thr
NMS的iou阈值。nms_thr设置的越小,proposal之间的重叠程度越低,proposal数量越小。
-
nms_post
最后留下的所有level的nms结果数量的最大值。真正nms结果的输出。
值得注意的是,rpn中nms的实现对于每一个level是相互独立的,代码将level编号设计成了类别的label,之后调用batch_nms
巧妙的实现了这一点。关于batch_nms
,会在本文之后进行分析。
RCNN的nms实现
找到RCNN的nms实现还是比较复杂的,这里需要一层一层的看。
首先Faster-rcnn使用的roi_head为StandardRoIHead,该类有三个父类,分别是BaseRoIHead, BBoxTestMixin, MaskTestMixin。直接看该类的forward_train
函数,在其中调用了_bbox_forward_train
来获得bbox结果,在_bbox_forward_train
中又通过_bbox_forward
来从输入的特征和roi区域得到bbox的预测结果。之后会使用self.bbox_head
的get_targets
获得bbox的结果。
Faster-rcnn的bbox_head使用了Shared2FCBBoxHead,因为该类继承了ConvFCBBoxHead
,并且只是修改了初始化的参数值,所以直接看ConvFCBBoxHead
的下的get_targets
实现,发现ConvFCBBoxHead
类也没有该函数的实现,所以再去他的父类去看,在BBoxHead类中,get_targets
的实现实际是多次调用了_get_target_single
,在_get_target_single
中我们其实可以看到实际没有使用任何nms的操作。所以可以认为训练时RCNN部分并没有做nms的操作。
之后我们来考察test时的实现,这里回到StandardRoIHead,我们观察simple_test
函数(aug_test
函数应该只是增加了一个针对图像增强的图像映射转换)。这里调用了simple_test_bboxes
找到这个函数,需要去他的父类中找,我们可以从BBoxTestMixin中找到这个函数。这个函数实际调用的是Shared2FCBBoxHead
的get_bboxes
函数来获取bbox的,该函数的实现还是位于BBoxHead。最终可以定位Faster-rcnn的的nms只在测试时使用,并且通过multiclass_nms函数实现,通过该函数的实现,可以看到config中各项参数设置的含义分别为:
-
score_thr
获得的bbox的最低得分值,分数低于该阈值的bbox会在nms之前被剔除。
-
max_per_img
网络输出的最终结果中的bbox的数量
-
nms
调用
batch_nms
时的设置。
batch_nms
batch_nms的实现实际上是在基本的单类别的nms的基础上,增加了按照不同类别进行nms的功能。代码如下:
def batched_nms(boxes, scores, idxs, nms_cfg, class_agnostic=False):
nms_cfg_ = nms_cfg.copy()
class_agnostic = nms_cfg_.pop('class_agnostic', class_agnostic)
# 判断是否要分类别做nms
if class_agnostic:
# 不分类别,则所有box一起算
boxes_for_nms = boxes
else:
# 分类别
# 找到box的最大坐标值
max_coordinate = boxes.max()
# 按照label对box做一个偏置,偏置的值等于label*最大坐标。
offsets = idxs.to(boxes) * (max_coordinate + torch.tensor(1).to(boxes))
# bbox坐标加上偏置,相当于将不同类别的bbox平移到了不同的平面区域
boxes_for_nms = boxes + offsets[:, None]
# 获取nms的类型
nms_type = nms_cfg_.pop('type', 'nms')
nms_op = eval(nms_type)
# 对bbox进行拆分,之后分组做nms
split_thr = nms_cfg_.pop('split_thr', 10000)
if boxes_for_nms.shape[0] < split_thr:
dets, keep = nms_op(boxes_for_nms, scores, **nms_cfg_)
boxes = boxes[keep]
scores = dets[:, -1]
else:
total_mask = scores.new_zeros(scores.size(), dtype=torch.bool)
for id in torch.unique(idxs):
mask = (idxs == id).nonzero(as_tuple=False).view(-1)
dets, keep = nms_op(boxes_for_nms[mask], scores[mask], **nms_cfg_)
total_mask[mask[keep]] = True
keep = total_mask.nonzero(as_tuple=False).view(-1)
keep = keep[scores[keep].argsort(descending=True)]
boxes = boxes[keep]
scores = scores[keep]
return torch.cat([boxes, scores[:, None]], -1), keep