Faster-RCNN开创了基于锚框(anchors)的目标检测框架,并且提出了RPN(Region proposal network),来生成RoI,用来取代之前的selective search方法。Faster-RCNN无论是训练/测试速度,还是物体检测的精度都超过了Fast-RCNN,并且实现了end-to-end训练。

从RCNN到Fast-RCNN再到Faster-RCNN,后者无疑达到了这一系列算法的巅峰,并且后来的YOLO、SSD、Mask-RCNN、RFCN等物体检测框架都是借鉴了Faster-RCNN

Faster-RCNN作为一种two-stage的物体检测框架,流程无疑比SSD这种one-stage物体检测框架要复杂,在阅读论文,以及代码复现的过程中也理解了很多细节,在这里记录一下自己的学习过程和自己的一点体会。

背景介绍

Fast-RCNN通过共享卷积层,极大地提升了整体的运算速度。Selective Search 反倒成为了限制计算效率的瓶颈。Faster-RCNN中使用卷积神经网络取代了Selective Search,这个网络就是Region Proposal Networks(RPN),Faster-RCNN将所有的步骤都包含到一个完整的框架中,真正实现了端对端(end-to-end)的训练。

论文主要贡献

  • 提出RPN,实现了端对端的训练
  • 提出了基于anchors的物体检测方法

1、网络框架

Faster-RCNN总体流程框图如下(点击原图查看大图),通过这个框图我们比较一下Faster-RCNN和SSD的不同: SSD中每一阶段生成的特征图,每个cell都会生成锚框,并且进行类别+边界框回归。 Faster-RCNN只对basenet提取出的特征图上生成锚框,并且对该锚框进行二分类(背景 or 有物体)+边界框回归,然后会进行NMS移除相似的结果,这样RPN最后会输出一系列region proposal,将这些region proposal区域从feature map中提取出来即为RoI,之后将会通过RoI pooling,进行真正的类别预测(判断属于哪一类)+边界框回归

可以看出Faster-RCNN之所以被称为two-stage,是由于需要有RPN生成region proposal这一步骤。相比来看SSD可以看做是稠密采样,它对所有生成的锚框进行了预测,而没有进行筛选。

RPN中还有一些细节操作,比如说采样比例的设置,如何进行预测,这个在后面的部分会详细说明。

2、RPN(Region Proposal Network)

处理流程

RPN在Faster-RCNN中作用为生成RoI,RPN的处理流程具体如下,一些细节将在之后介绍: 1. 输入为base_net提取出来的feature map,首先在feature map上生成锚框(anchor),其中每个cell有多个锚框。 2. 通过一个conv_3x3,stride=1,padding=1的卷积层,进一步提取特征,输出特征图的大小不变,这里称为rpn_feature。 3. 在rpn_feature上用两个1x1卷积层进行预测输出,分别为每个锚框的二分类分数、每个锚框的坐标偏移量。 4. 利用上面预测的分数以及偏移量,对锚框(anchor)进行非极大值抑制(NMS)操作,最终输出RoI候选区域

详细步骤及代码

在feature_map上生成锚框

这一步中,会在feature_map每个cell上生成一系列不同大小和宽高比例的锚框。生成锚框的方式如下: 1. 选定一个锚框的基准大小,记为base,比如为16 2. 选定一组宽高比例(aspect ratios),比如为【0.5、1、2】 3. 选定一组大小比例(scales),比如为【16、32、64】 4. 那么每个cell将会生成ratios*scales个锚框,而每个锚框的形状大小的计算公式如下: $$ width_{anchor} = size_{base} times scale times sqrt{ 1 / ratio}$$ $$ height_{anchor} = size_{base} times scale times sqrt{ratio}$$ 举个例子,我们按照论文中取3种大小比例以及3种长宽比例,那么每个cell生成的锚框个数为$k=9$,而假设我们的特征图大小为$Wtimes H=2400$,那么我们一共生成了$WHk$个锚框。可以看到,生成的锚框数量非常多,有大量的重复区域。RPN输出时不应该使用所有锚框,所以采用NMS 来去除大量重复的锚框,而只选择一些得分较高的锚框作为RoI输出。其实,RPN在训练时也进行了采样,这个后面具体介绍。RPN生成的锚框如下图所示:

MXNet中,生成锚框的类源码如下所示:

class RPNAnchorGenerator(gluon.Block):"""@输入参数stride:int              特征图的每个像素感受野大小,通常为原图和特征图尺寸比例base_size:int           默认大小ratios:int              宽高比scales:int              大小比例每个锚框为   width = base_size*size/sqrt(ratio)  height = base_size*size*sqrt(ratio)alloc_size:(int,int)          默认的特征图大小(H,W),以后每次生成直接索引切片"""def __init__(self, stride, base_size, ratios, scales, alloc_size, **kwargs):super(RPNAnchorGenerator, self).__init__(**kwargs)if not base_size:raise ValueError("Invalid base_size: {}".format(base_size))# 防止非法输入if not isinstance(ratios, (tuple, list)):ratios = [ratios]if not isinstance(scales, (tuple, list)):scales = [scales]# 每个像素的锚框数self._num_depth = len(ratios) * len(scales)# 预生成锚框anchors = self._generate_anchors(stride, base_size, ratios, scales, alloc_size)self.anchors = self.params.get_constant('anchor_', anchors)@propertydef num_depth(self):return self._num_depthdef _generate_anchors(self, stride, base_size, ratios, scales, alloc_size):# 计算中心点坐标px, py = (base_size - 1) * 0.5, (base_size - 1) * 0.5base_sizes = []for r in ratios:for s in scales:size = base_size * base_size / rws = np.round(np.sqrt(size))w = (ws * s - 1) * 0.5h = (np.round(ws * r) * s - 1) * 0.5base_sizes.append([px - w, py - h, px + w, py + h])# 每个像素的锚框base_sizes = np.array(base_sizes)# 下面进行偏移量的生成width, height = alloc_sizeoffset_x = np.arange(0, width * stride, stride)offset_y = np.arange(0, height * stride, stride)offset_x, offset_y = np.meshgrid(offset_x, offset_x)# 生成(H*W,4)offset = np.stack((offset_x.ravel(), offset_y.ravel(),offset_x.ravel(), offset_y.ravel()), axis=1)# 下面广播到每一个anchor中    (1,N,4) + (M,1,4)anchors = base_sizes.reshape((1, -1, 4)) + offset.reshape((-1, 1, 4))anchors = anchors.reshape((1, 1, width, height, -1)).astype(np.float32)return anchors# 对原始生成的锚框进行切片操作def forward(self, x):# 切片索引anchors = self.anchors.valuea = nd.slice_like(anchors, x * 0, axes=(2, 3))return a.reshape((1, -1, 4))

用conv3x3卷积进一步提取特征图

这一步中就是RPN进一步抽取特征,生成的RPN-feature map提供给之后的类别预测和回归预测。该步骤中使用的是kernel_size=3x3,strides=1,padding=1,Activation='relu'的卷积层,不改变特征图的尺寸,这也是为了之后的1x1卷积层预测时,空间位置能够一一对应,而用通道数来表示预测的类别分数和偏移量。这一步的代码很简单,就是单独的构建了一个3x3 Conv2D的卷积层。

# 第一个提取特征的3x3卷积
self.conv1 = nn.Sequential()
self.conv1.add(nn.Conv2D(channels, kernel_size=3, strides=1, padding=1, weight_initializer=weight_initializer), nn.Activation('relu'))

用1x1卷积层进行二分类预测以及边界框回归预测

我们在第一步中生成了固定的默认锚框,这一步我们需要用两个1x1卷积层对每个锚框分别预测(1)类别分数(背景or物体)$score$(2)锚框偏移量$offset$。而这些预测值$score、offset$将用于后面的NMS操作,可以去除一些得分低,或者有大量重复区域的锚框,从而最终输出良好的Region Proposal给后面网络进行处理。 类别分数$score$,RPN中只关心是否有物体,所以是个二分类问题(背景、物体)。 锚框的坐标偏移量$offset$,一般为4个值,$boldsymbolDelta xcenter、boldsymbolDelta ycenter、boldsymbolDelta width、boldsymbolDelta height$。

上面介绍了,两个1x1卷积层的输入为RPN-feature map,1x1卷积并不改变特征图尺寸,我们采用通道数来表示对应cell锚框的预测值。假设输入RPN-feature map 形状为$(C,H,W)$,每个cell生成了$k$个锚框。输出的锚框分数和偏移量在空间位置上一一对应(也就是尺寸不变)。 类别分数,输出通道应为$(ktimes2,H,W)$,不同通道表示每个类别的分数 偏移量预测,输出通道应为$(ktimes4,H,W)$,不同通道表示锚框的坐标偏移量

代码很简单,就是添加两个卷积层并前向运算:

# 预测偏移量和预测类别的卷积层# 使用sigmoid预测,减少通道数self.score = nn.Conv2D(anchor_depth, kernel_size=1, strides=1, padding=0,weight_initializer=weight_initializer)self.loc = nn.Conv2D(anchor_depth * 4, kernel_size=1, strides=1, padding=0,weight_initializer=weight_initializer)

使用预测的score和offset对锚框处理,输出Region Proposal

上面的步骤中,我们会对feature map的每个cell都生成多个锚框,并且预测$score、offset$,我们生成了$WHk$个锚框(大约有2W个),不难想象,大量的锚框其实都是背景,而且有着大量的重叠锚框,我们不可能将所有的锚框都当做Region Proposal输出给RoI Pooling层,提供给Fast-RCNN进行后面的进一步运算。第一个原因是会造成计算量过大,第二个原因是大量的背景框,重复的锚框是没有意义的,我们应该输出得分最高的topk个锚框。最后一步的Region Proposal具体处理过程如下: 将上一步预测的偏移量加到生成的默认锚框中,我们把这些区域称作RoI 对超出图像边界的RoI进行剪切,保证所有RoI都在原始图像内部 丢弃小于我们设定最小尺寸的锚框 根据我们预测的$score$,对RoI进行非极大值抑制操作(NMS),去除得分较低以及重复区域的RoI * 最后我们选择得分为topk的RoI输出,作为最终输出的Region Proposal(比如说前2000个)

通过这一步,我们筛选出了置信度最高的Region Proposal,也就是我们认为最有可能有物体的区域,输入到后面的Fast-RCNN网络中,进行最终的分类以及再一次的边界框回归预测。MXNet GluonCV 中生成Region Proposal的类源码如下:

class RPNProposal(gluon.Block):"""@:parameter------------------clip : float如果提供,将bbox剪切到这个值num_thresh : floatnms的阈值,用于去除重复的框train_pre_nms : int训练时对前 train_pre_nms 进行 NMS操作train_post_nms : int训练时进行NMS后,返回前 train_post_nms 个region proposaltest_pre_nms : int测试时对前 test_pre_nms 进行 NMS操作test_post_nms : int测试时进行NMS后,返回前 test_post_nms 个region proposalmin_size : int小于 min_size 的 proposal将会被舍弃stds : tuple of int 计算偏移量用的标准差"""def __init__(self, clip, nms_thresh, train_pre_nms, train_post_nms,test_pre_nms, test_post_nms, min_size, stds, **kwargs):super(RPNProposal, self).__init__(**kwargs)self._clip = clipself._nms_thresh = nms_threshself._train_pre_nms = train_pre_nmsself._train_post_nms = train_post_nmsself._test_pre_nms = test_pre_nmsself._test_post_nms = test_post_nmsself._min_size = min_sizeself._bbox_decoder = NormalizedBoxCenterDecoder(stds=stds, clip=clip)self._cliper = BBoxClipToImage()self._bbox_tocenter = BBoxCornerToCenter(axis=-1, split=False)"""@:parameterscores : (B,N,1) 通过RPN预测的得分输出(sigmoid之后) (0,1)offsets : ndarray (B,N,4)通过RPN预测的锚框偏移量anchors : ndarray (B,N,4)生成的默认锚框,坐标编码方式为 Cornerimg : ndarray (B,C,H,W)图像的张量,用来剪切锚框@:returns"""def forward(self, scores, offsets, anchors, img):# 训练和预测的处理流程不同if autograd.is_training():pre_nms = self._train_pre_nmspost_nms = self._train_post_nmselse:pre_nms = self._test_pre_nmspost_nms = self._test_post_nmswith autograd.pause():# 将预测的偏移量加到anchors中rois = self._bbox_decoder(offsets, self._bbox_tocenter(anchors))rois = self._cliper(rois, img)# 下面将所有尺寸小于设定最小值的ROI去除x_min, y_min, x_max, y_max = nd.split(rois, num_outputs=4, axis=-1)width = x_max - x_minheight = y_max - y_mininvalid_mask = (width < self._min_size) + (height < self._min_size)# 将对应位置的score 设为-1scores = nd.where(invalid_mask, nd.ones_like(scores) * -1, scores)invalid_mask = nd.repeat(invalid_mask, repeats=4, axis=-1)rois = nd.where(invalid_mask, nd.ones_like(rois) * -1, rois)# 下面进行NMS操作pre = nd.concat(scores, rois, dim=-1)pre = nd.contrib.box_nms(pre, overlap_thresh=self._nms_thresh, topk=pre_nms,coord_start=1, score_index=0, id_index=-1, force_suppress=True)# 下面进行采样result = nd.slice_axis(pre,axis=1, begin=0, end=post_nms)rpn_score = nd.slice_axis(result, axis=-1, begin=0, end=1)rpn_bbox = nd.slice_axis(result, axis=-1, begin=1, end=None)return rpn_score, rpn_bbox

RPN最终输出的Region Proposal 如图所示,去除了大量的重复锚框,和得分低的背景区域:

RPN整体代码

RPN的处理流程如上所述,下面是RPN层的整体代码:

# 定义RPN网络
# RPN网络输出应为一系列 region proposal  默认为 2000个
class RPN(nn.Block):"""@输入参数channels : int卷积层的输出通道stride:int              特征图的每个像素感受野大小,通常为原图和特征图尺寸比例base_size:int           默认大小ratios:int              宽高比scales:int              大小比例每个锚框为   width = base_size*size/sqrt(ratio)  height = base_size*size*sqrt(ratio)alloc_size:(int,int)          默认的特征图大小(H,W),以后每次生成直接索引切片clip : float如果设置则将边界框剪切到该值nms_thresh : float非极大值抑制的阈值train_pre_nms : int训练时对前 train_pre_nms 进行 NMS操作train_post_nms : int训练时进行NMS后,返回前 train_post_nms 个region proposaltest_pre_nms : int测试时对前 test_pre_nms 进行 NMS操作test_post_nms : int测试时进行NMS后,返回前 test_post_nms 个region proposalmin_size : int小于 min_size 的 proposal将会被舍弃"""def __init__(self, channels, stride, base_size, ratios,scales, alloc_size, clip, nms_thresh,train_pre_nms, train_post_nms, test_pre_nms, test_post_nms, min_size, **kwargs):super(RPN, self).__init__(**kwargs)weight_initializer = mx.init.Normal(sigma=0.01)# 锚框生成器self._anchor_generator = RPNAnchorGenerator(stride, base_size, ratios, scales, alloc_size)anchor_depth = self._anchor_generator.num_depthself._rpn_proposal = RPNProposal(clip, nms_thresh, train_pre_nms,train_post_nms, test_pre_nms, test_post_nms, min_size, stds=(1., 1., 1., 1.))# 第一个提取特征的3x3卷积self.conv1 = nn.Sequential()self.conv1.add(nn.Conv2D(channels, kernel_size=3, strides=1, padding=1, weight_initializer=weight_initializer),nn.Activation('relu'))# 预测偏移量和预测类别的卷积层# 使用sigmoid预测,减少通道数self.score = nn.Conv2D(anchor_depth, kernel_size=1, strides=1, padding=0,weight_initializer=weight_initializer)self.loc = nn.Conv2D(anchor_depth * 4, kernel_size=1, strides=1, padding=0,weight_initializer=weight_initializer)# 前向运算函数def forward(self, x, img):"""产生锚框,并且对每个锚框进行二分类,以及回归预测************************注意,这一阶段只是进行了粗采样,在RCNN中还要进行一次采样@:parameter-------------x : (B,C,H,W)由basenet提取出的特征图img : (B,C,H,W)图像tensor,用来剪切超出边框的锚框@:returns-----------------(1)训练阶段rpn_score : ndarray (B,train_post_nms,1)输出的region proposal 分数 (用来给RCNN采样)rpn_box : ndarray (B,train_post_nms,4)输出的region proposal坐标 Cornerraw_score : ndarray (B,N,1)卷积层的原始输出,用来训练RPNrpn_bbox_pred : ndarray (B,N,4)卷积层的原始输出,用来训练RPNanchors : ndarray (1,N,4)生成的锚框(2)预测阶段rpn_score : ndarray (B,train_post_nms,1)输出的region proposal 分数 (用来给RCNN采样)rpn_box : ndarray (B,train_post_nms,4)输出的region proposal坐标 Corner"""anchors = self._anchor_generator(x)# 提取特征feat = self.conv1(x)# 预测raw_score = self.score(feat)raw_score = raw_score.transpose((0, 2, 3, 1)).reshape(0, -1, 1)rpn_scores = mx.nd.sigmoid(mx.nd.stop_gradient(raw_score))rpn_bbox_pred = self.loc(feat)rpn_bbox_pred = rpn_bbox_pred.transpose((0, 2, 3, 1)).reshape(0, -1, 4)# 下面生成region proposalrpn_score, rpn_box = self._rpn_proposal(rpn_scores, mx.nd.stop_gradient(rpn_bbox_pred), anchors,img)# 处于训练阶段if autograd.is_training():# raw_score, rpn_bbox_pred 用于 RPN 的训练return rpn_score, rpn_box, raw_score, rpn_bbox_pred, anchors# 处于预测阶段return rpn_score, rpn_box


3、对RPN输出的Region Proposal采样处理

上面说道通过RPN层后,我们进行了粗采样,输出了大约2000个Region Proposal,然而我们并不会将这个2000个Region Proposal全部送入RoI Pooling中进行计算,这样效率很低、计算很慢。论文作者对这些Region Proposal进行了采样处理,只采样了一小部分的Region Proposal送入之后的网络运算,而且训练过程的采样和预测过程的采样是不一样的。下面详细介绍一下处理流程。

训练过程中的Region Proposal采样

训练过程的采样在Fast-RCNN论文中有提到,由于要考虑训练过程中正负样本均衡的问题,最终输出了128个Region Proposal,其中正样本的比例为0.25。正负样本的定义如下: * 如果一个Region Proposal与任意一个ground truth的 IoU 大于设定阈值(默认为0.5),那么标记其为正样本,否则为负样本。

将所有Region Proposal打上标记后,进行随机采样,其中采样正样本的比例为0.25,其余的为负样本。最终采样输出128个Region Proposal,送入之后的网络进行处理计算。

测试过程中的Region Proposal采样

测试过程中的采样很简单,直接采样Region Proposal中,$scores$为前topk个(比如300)的样本,目的就是提取最有可能为物体的区域输入到后面的网络了。

Region Proposal采样代码

class RCNNTargetSampler(gluon.Block):"""@:parameter------------num_images : int每个batch的图片数,目前仅支持1num_inputs : int输入的RoI 数量num_samples : int输出的采样 RoI 数量pos_thresh : float正类样本阈值pos_ratio : float采样正样本的比例max_gt_box : int"""def __init__(self, num_images, num_inputs, num_samples, pos_thresh, pos_ratio, max_gt_box, **kwargs):super(RCNNTargetSampler, self).__init__(**kwargs)self._num_images = num_imagesself._num_inputs = num_inputsself._num_samples = num_samplesself._pos_thresh = pos_threshself._pos_ratios = pos_ratioself._max_pos = int(np.round(num_samples * pos_ratio))self._max_gt_box = max_gt_boxdef forward(self, rois, scores, gt_bboxes):"""@:parameter-----------rois : ndarray (B,self._num_inputs,4)RPN输出的roi区域坐标,Cornerscores : ndarray (B,self._num_inputs,1)RPN输出的roi区域分数,(0,1) -1表示忽略gt_bboxes:ndarray (B,M,4)ground truth box 坐标@:returns-----------new_rois : ndarray (B,self._num_samples,4)采样后的RoI区域new_samples : ndarray (B,self._num_samples,1)采样后RoI区域的标签 1:pos -1:neg 0:ignorenew_matches : ndarray (B,self._num_samples,1)采样后的RoI匹配的锚框编号 [0,M)"""new_rois, new_samples, new_matches = [], [], []# 对每个batch分别进行处理for i in range(self._num_images):roi = nd.squeeze(nd.slice_axis(rois, axis=0, begin=i, end=i + 1), axis=0)score = nd.squeeze(nd.slice_axis(scores, axis=0, begin=i, end=i + 1), axis=0)gt_bbox = nd.squeeze(nd.slice_axis(gt_bboxes, axis=0, begin=i, end=i + 1), axis=0)# 将ground truth的分数设置为1 形状为(M,1)gt_score = nd.ones_like(nd.sum(gt_bbox, axis=-1, keepdims=True))# 将ground truth 和 roi 拼接 (N+M,4) (N+m,1)roi = nd.concat(roi, gt_bbox, dim=0)score = nd.concat(score, gt_score, dim=0).squeeze(axis=-1)# 计算iou   (N+M,M)iou = nd.contrib.box_iou(roi, gt_bbox, format='corner')# (N+M,)iou_max = nd.max(iou, axis=-1)# (N+M,)  与哪个ground truth 匹配iou_argmax = nd.argmax(iou, axis=-1)# 将所有的标记为 2 negmask = nd.ones_like(iou_argmax) * 2# 标记ignore 为 0mask = nd.where(score < 0, nd.zeros_like(mask), mask)# 将正类标记为 3 pospos_idx = (iou_max >= self._pos_thresh)mask = nd.where(pos_idx, nd.ones_like(mask) * 3, mask)# 下面进行shuffle操作rand = nd.random.uniform(0, 1, shape=(self._num_inputs + self._max_gt_box,))# 取前面 N+M 个 对mask 做shuffle操作rand = nd.slice_like(rand, mask)# shuffle 操作后的 indexindex = nd.argsort(rand)# 将三个结果进行shufflemask = nd.take(mask, index)iou_argmax = nd.take(iou_argmax, index)# 下面进行采样# 排序 3:pos 2:neg 0:ignoreorder = nd.argsort(mask, is_ascend=False)# 取topk个作为正例topk = nd.slice_axis(order, axis=0, begin=0, end=self._max_pos)# 下面取出相对应的值pos_indices = nd.take(index, topk)pos_samples = nd.take(mask, topk)pos_matches = nd.take(iou_argmax, topk)# 下面将原来的标签改了pos_samples = nd.where(pos_samples == 3, nd.ones_like(pos_samples), pos_samples)pos_samples = nd.where(pos_samples == 2, nd.ones_like(pos_samples) * -1, pos_samples)index = nd.slice_axis(index, axis=0, begin=self._max_pos, end=None)mask = nd.slice_axis(mask, axis=0, begin=self._max_pos, end=None)iou_argmax = nd.slice_axis(iou_argmax, axis=0, begin=self._max_pos, end=None)# 对负样本进行采样# neg 2---->4mask = nd.where(mask == 2, nd.ones_like(mask) * 4, mask)order = nd.argsort(mask, is_ascend=False)num_neg = self._num_samples - self._max_posbottomk = nd.slice_axis(order, axis=0, begin=0, end=num_neg)neg_indices = nd.take(index, bottomk)neg_samples = nd.take(mask, bottomk)neg_matches = nd.take(iou_argmax, topk)neg_samples = nd.where(neg_samples == 3, nd.ones_like(neg_samples), neg_samples)neg_samples = nd.where(neg_samples == 4, nd.ones_like(neg_samples) * -1, neg_samples)# 输出new_idx = nd.concat(pos_indices, neg_indices, dim=0)new_sample = nd.concat(pos_samples, neg_samples, dim=0)new_match = nd.concat(pos_matches, neg_matches, dim=0)new_rois.append(roi.take(new_idx))new_samples.append(new_sample)new_matches.append(new_match)new_rois = nd.stack(*new_rois, axis=0)new_samples = nd.stack(*new_samples, axis=0)new_matches = nd.stack(*new_matches, axis=0)return new_rois, new_samples, new_matches


4、RoI Pooling层

通过上一步的采样后,我们得到了一堆没有class score的Region Proposal,这些Region Proposal是对应于我们第一步base net 提取出来 feature map上的区域。可以从网络图中看到,我们最终将Region Proposal又输出回我们feature map,我们可以将RPN看做是一个额外的中间过程,这也是Faster-RCNN被称为two-stage的原因。由于输出的Region Proposal大小并不一致,而Fast-RCNN最后为全连接层,需要输出固定尺寸的特征,所以RoI Pooling层的作用就是将这些大小不同的Region Proposal,映射输出为统一大小的特征图。比如我设置RoI Pooling层的输出大小为(14,14),那么无论输入的特征图尺寸是什么,输出的特征图均为(14,14)。

代码的话直接使用nd.ROIPooling()就能实现了。

5、后续Fast-RCNN处理

处理流程

到了这一步我们的处理已经到了尾声了,我们通过RoI Pooling已经得到了固定尺寸的feature map,最后一步就是用Fast-RCNN,进行预测类别分数以及边界框的回归。具体的处理流程如下: 1. 使用卷积层再提取一次特征 2. 进行全局池化,将特征图尺寸变为(channel,1,1) 3. 通过两个不同的全连接层,分别预测类别分数和进行坐标回归 * 类别预测全连接层有num_classes+1个神经元,其中包括所有类别和背景 * 坐标回归全连接层有4*num_classes个神经元,它会为每一个类别预测4个坐标回归值$boldsymbolDelta xcenter、boldsymbolDelta ycenter、boldsymbolDelta width、boldsymbolDelta height$

最后如果是测试的话,那么将输入的Region Proposal加上我们预测的偏移量,然后根据预测得分再进行一次NMS操作,那么就可以得到我们最终输出的物体框。并且我们可以设定一个阈值(如0.5),得分大于阈值的物体框我们才进行输出。

代码

class FasterRCNN(RCNN):"""@:parameter-------------"""def __init__(self, features, top_features, classes,short=600, max_size=1000, train_patterns=None,nms_thresh=0.3, nms_topk=400, post_nms=100,roi_mode='align', roi_size=(14, 14), stride=16, clip=None,rpn_channel=1024, base_size=16, scales=(8, 16, 32),ratios=(0.5, 1, 2), alloc_size=(128, 128), rpn_nms_thresh=0.7,rpn_train_pre_nms=12000, rpn_train_post_nms=2000,rpn_test_pre_nms=6000, rpn_test_post_nms=300, rpn_min_size=16,num_sample=128, pos_iou_thresh=0.5, pos_ratio=0.25, max_num_gt=300,**kwargs):super(FasterRCNN, self).__init__(features=features, top_features=top_features, classes=classes,short=short, max_size=max_size, train_patterns=train_patterns,nms_thresh=nms_thresh, nms_topk=nms_topk, post_nms=post_nms,roi_mode=roi_mode, roi_size=roi_size, stride=stride, clip=clip, **kwargs)self._max_batch = 1  # 最大支持batch=1self._num_sample = num_sampleself._rpn_test_post_nms = rpn_test_post_nmsself._target_generator = {RCNNTargetGenerator(self.num_class)}with self.name_scope():# Faster-RCNN的RPNself.rpn = RPN(channels=rpn_channel, stride=stride, base_size=base_size,scales=scales, ratios=ratios, alloc_size=alloc_size,clip=clip, nms_thresh=rpn_nms_thresh, train_pre_nms=rpn_train_pre_nms,train_post_nms=rpn_train_post_nms, test_pre_nms=rpn_test_pre_nms,test_post_nms=rpn_test_post_nms, min_size=rpn_min_size)# 用来给训练时Region Proposal采样,正负样本比例为0.25self.sampler = RCNNTargetSampler(num_images=self._max_batch, num_inputs=rpn_train_post_nms,num_samples=self._num_sample, pos_thresh=pos_iou_thresh,pos_ratio=pos_ratio, max_gt_box=max_num_gt)@propertydef target_generator(self):return list(self._target_generator)[0]def forward(self, x, gt_boxes=None):""":param x: ndarray (B,C,H,W):return: """def _split_box(x, num_outputs, axis, squeeze_axis=False):a = nd.split(x, axis=axis, num_outputs=num_outputs, squeeze_axis=squeeze_axis)if not isinstance(a, (list, tuple)):return [a]return a# 首先用basenet抽取特征feat = self.features(x)# 输入RPN网络if autograd.is_training():# 训练过程rpn_score, rpn_box, raw_rpn_score, raw_rpn_box, anchors = self.rpn(feat, nd.zeros_like(x))# 采样输出rpn_box, samples, matches = self.sampler(rpn_box, rpn_score, gt_boxes)else:# 预测过程# output shape (B,N,4)_, rpn_box = self.rpn(feat, x)# 对输出的Region Proposal 进行采样# 输出送到后面运算的RoI# rois shape = (B,self._num_sampler,4),num_roi = self._num_sample if autograd.is_training() else self._rpn_test_post_nms# 将rois变为2D,加上batch_indexwith autograd.pause():roi_batchid = nd.arange(0, self._max_batch, repeat=num_roi)rpn_roi = nd.concat(*[roi_batchid.reshape((-1, 1)), rpn_box.reshape((-1, 4))], dim=-1)rpn_roi = nd.stop_gradient(rpn_roi)# RoI Pooling 层if self._roi_mode == 'pool':# (Batch*num_roi,channel,H,W)pool_feat = nd.ROIPooling(feat, rpn_roi, self._roi_size, 1 / self._stride)elif self._roi_mode == 'align':pool_feat = nd.contrib.ROIAlign(feat, rpn_roi, self._roi_size,1 / self._stride, sample_ratio=2)else:raise ValueError("Invalid roi mode: {}".format(self._roi_mode))top_feat = self.top_features(pool_feat)avg_feat = self.global_avg_pool(top_feat)# 类别预测,回归预测# output shape (B*num_roi,(num_cls+1)) -> (B,N,C)cls_pred = self.class_predictor(avg_feat)# output shape (B*num_roi,(num_cls)*4) -> (B,N,C,4)box_pred = self.bbox_predictor(avg_feat)cls_pred = cls_pred.reshape((self._max_batch, num_roi, self.num_class + 1))box_pred = box_pred.reshape((self._max_batch, num_roi, self.num_class, 4))# 训练过程if autograd.is_training():return (cls_pred, box_pred, rpn_box, samples, matches,raw_rpn_score, raw_rpn_box, anchors)# 预测过程# 还要进行的步骤,将预测的类别和预测的偏移量加到输入的RoI中else:# 直接输出所有类别的信息# cls_id (B,N,C) scores(B,N,C)cls_ids, scores = self.cls_decoder(nd.softmax(cls_pred, axis=-1))# 将所有的C调换到第一维# (B,N,C)  -----> (B,N,C,1) -------> (B,C,N,1)cls_ids = cls_ids.transpose((0, 2, 1)).reshape((0, 0, 0, 1))# (B,N,C)  -----> (B,N,C,1) -------> (B,C,N,1)scores = scores.transpose((0, 2, 1)).reshape((0, 0, 0, 1))# (B,N,C,4) -----> (B,C,N,4),box_pred = box_pred.transpose((0, 2, 1, 3))rpn_boxes = _split_box(rpn_box, num_outputs=self._max_batch, axis=0, squeeze_axis=False)cls_ids = _split_box(cls_ids, num_outputs=self._max_batch, axis=0, squeeze_axis=True)scores = _split_box(scores, num_outputs=self._max_batch, axis=0, squeeze_axis=True)box_preds = _split_box(box_pred, num_outputs=self._max_batch, axis=0, squeeze_axis=True)results = []# 对每个batch分别进行decoder nmsfor cls_id, score, box_pred, rpn_box in zip(cls_ids, scores, box_preds, rpn_boxes):# box_pred(C,N,4)   rpn_box(1,N,4)   box (C,N,4)box = self.box_decoder(box_pred, self.box_to_center(rpn_box))# cls_id (C,N,1) score (C,N,1) box (C,N,4)# result (C,N,6)res = nd.concat(*[cls_id, score, box], dim=-1)# nms操作 (C,self.nms_topk,6)res = nd.contrib.box_nms(res, overlap_thresh=self.nms_thresh, valid_thresh=0.0001,topk=self.nms_topk, coord_start=2, score_index=1, id_index=0,force_suppress=True)res = res.reshape((-3, 0))results.append(res)results = nd.stack(*results, axis=0)ids = nd.slice_axis(results, axis=-1, begin=0, end=1)scores = nd.slice_axis(results, axis=-1, begin=1, end=2)bboxes = nd.slice_axis(results, axis=-1, begin=2, end=6)# 输出为score,bboxreturn ids, scores, bboxes

6、总结

总的来说Faster-RCNN主要的改进地方在于用RPN来生成候选区域,使整个预测,训练过程都能用深度学习的方法完成。Faster-RCNN达到了这一系列算法的巅峰,并且在论文中提出的基于anchor的物体检测方法,更是被之后的state-of-the-art的框架广泛采用。Faster-RCNN 在 COCO和PASCAL数据集上都取得了当时最好的成绩,感兴趣的话,具体数据在论文中都有详细提到。Faster-RCNN比SSD处理流程要复杂许多,其中还涉及到非常多的细节,例如如何对anchor进行标记,如何对整个网络进行训练等等,这些我会另外写一篇博客来记录Faster-RCNN的训练过程。

7、题外话

Faster-RCNN我也是学习了很久了,从读论文到看源码,最深的一个感受就是“纸上得来终觉浅,绝知此事要躬行”。论文上始终都是宏观的东西,看完之后觉得自己似乎是懂了,但是当写代码时,才会发现有许多许多问题。我想只有当把代码和论文同时完全理解,才能算真正的看懂了吧。现在我的水平还完全不够,还停留在能看懂,稍微改改能用的阶段,如果是一篇新论文,要自己从零开始复现,目前的我还做不到,不过坚持下去多看多想多学多写,每天进步一点点,我想在毕业之前应该能达到我想要的目标吧~

学习过程中还有一个很深的体会就是多看底层源码,我就是通过看GluonCV中Faster-RCNN源码才理解了论文中的许多细节,总之多向这些优秀的代码学习吧,特别是深度学习框架的一些高级API使用,只有看了源码才会想到,原来代码还可以这样编~

以上Faster-RCNN都是我的个人浅薄理解,欢迎大家指出我存在的问题~

rcnn代码实现_Faster-RCNN论文细节原理解读+代码实现gluoncv(MXNet)相关推荐

  1. Faster-RCNN论文细节原理解读+代码实现gluoncv(MXNet)

      Faster-RCNN开创了基于锚框(anchors)的目标检测框架,并且提出了RPN(Region proposal network),来生成RoI,用来取代之前的selective searc ...

  2. Yolov1-pytorch版 论文、原理及代码实现

    Yolov1-pytorch版 论文.原理及代码实现 Yolov1 论文.原理.代码实现 1.论文 2.原理 2.1 目标检测方法 2.2 相关名词解释 2.3 网络结构设计分析 2.4 损失函数 3 ...

  3. python粘贴代码到word_写论文必备,在线代码高亮工具,无缝粘贴到 Word

    在校的朋友最近应该就要进入论文期了,分享一个在线代码高亮的站点,可以一键高亮美化代码,粘贴到 Word 中也可以保持非常好的格式,为您的论文加分. 1. 功能支持 161 种语言格式的高亮: 默认为自 ...

  4. ORB-SLAM / ORB-SLAM2原理解读+代码解析(汇总了资料,方便大家学习)

    注释:本文非原创,初学搜集了很多资料附上链接,方便初学者学习,避免盲目搜索浪费时间. 目录 官方代码链接 代码框架思维导图 参考解读 参考链接- -一步步带你看懂orbslam2源码 ORB-SLAM ...

  5. 拼图java代码_Java制作智能拼图游戏原理及代码

    今天突发奇想,想做一个智能拼图游戏来给哄女友. 需要实现这些功能 第一图片自定义 第二宫格自定义,当然我一开始就想的是3*3 4*4 5*5,没有使用3*5这样的宫格. 第三要实现自动拼图的功能,相信 ...

  6. K-Means(K均值聚类)原理及代码实现

    机器学习 没有免费午餐定理和三大机器学习任务 如何对模型进行评估 K-Means(K均值聚类)原理及代码实现 KNN(K最近邻算法)原理及代码实现 KMeans和KNN的联合演习 文章目录 机器学习 ...

  7. 域名拦截检测机制原理和代码分享

    因为团队项目需要在微信中推广,由于微信限制太严格了,域名总能被误判为诱导分享,作为一名程序猿写了一串微信域名检测判断代码,分享给大家机制原理和代码! 域名被拦截判断如下: 判断一:域名能正常访问,未被 ...

  8. Lucene原理与代码分析(高手博客备忘)

    2019独角兽企业重金招聘Python工程师标准>>> 随笔 - 69  文章 - 77  评论 - 687 随笔分类 - Lucene原理与代码分析 Lucene 4.X 倒排索引 ...

  9. 【资源】Faster R-CNN原理及代码讲解电子书

    <Faster R-CNN原理及代码讲解>是首发于GiantPandaCV公众号的教程,针对陈云大佬实现的Faster R-CNN代码讲解,Github链接如下: https://gith ...

最新文章

  1. 贝尔实验室发布6G通信白皮书
  2. 北京科技大学转专业到计算机,北科大学生全可转专业
  3. unity3d 数学基础与数学辅助类
  4. centos 安装 图像识别工具 tesseract-ocr 流程
  5. 从java读取Excel继续说大道至简 .
  6. 面试 | 程序猿面试,Elasticsearch被坑被虐的体无完肤...
  7. 一个CSS3滤镜Drop-shadow阴影效果
  8. 人月神话阅读笔记06
  9. 二叉树的创建及遍历--java实现
  10. http协议中的keeplive是做什么的?它的适应场景是什么?
  11. 软件测试项目实战学习路线
  12. 理解GAN网络基本原理
  13. PRIMARY KEY与identity(1,1)的比较
  14. 谈谈对springioc的理解
  15. python编写计算二项式值_python二项式期权定价方法
  16. org.quartz.JobPersistenceException: Couldn‘t store job:
  17. H.266代码学习:decompressCtu和xDecompressCU函数
  18. word 文档结构二级标题和一级不一致解决办法
  19. 【评测】常规PCR(终点法,跑胶)的多重PCR的试剂
  20. 价值 25k 的面试题及其答案分享

热门文章

  1. mysql注解批量添加mybatis_Mybatis注解方式 实现批量插入数据库
  2. SpringBoot使用@Scheduled创建定时任务
  3. avue-crud属性说明
  4. es创建索引库报错 :Types cannot be provided in put mapping requests, unless the include_type_na
  5. Oracle提示“ORA-04098:触发器‘XXX_TRIGGER’无效且未通过重新验证”
  6. matlab mxarray赋值,C++中数组与MATLAB mxArray相互赋值
  7. python基础知识7——迭代器,生成器,装饰器
  8. 删除 java代码中所有的注释
  9. 深入理解Auto Layout 第一弹
  10. 怎样使用SSH连接OpenStack上的云主机