赞
踩
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)的训练。
Faster-RCNN总体流程框图如下(点击原图查看大图),通过这个框图我们比较一下Faster-RCNN和SSD的不同:
可以看出Faster-RCNN之所以被称为two-stage,是由于需要有RPN生成region proposal这一步骤。相比来看SSD可以看做是稠密采样,它对所有生成的锚框进行了预测,而没有进行筛选。
RPN中还有一些细节操作,比如说采样比例的设置,如何进行预测,这个在后面的部分会详细说明。
RPN在Faster-RCNN中作用为生成RoI,RPN的处理流程具体如下,一些细节将在之后介绍:
这一步中,会在feature_map每个cell上生成一系列不同大小和宽高比例的锚框。生成锚框的方式如下:
1. 选定一个锚框的基准大小,记为base,比如为16
2. 选定一组宽高比例(aspect ratios),比如为【0.5、1、2】
3. 选定一组大小比例(scales),比如为【16、32、64】
4. 那么每个cell将会生成ratios*scales个锚框,而每个锚框的形状大小的计算公式如下:
w
i
d
t
h
a
n
c
h
o
r
=
s
i
z
e
b
a
s
e
×
s
c
a
l
e
×
1
/
r
a
t
i
o
width_{anchor} = size_{base} \times scale \times \sqrt{ 1 / ratio}
widthanchor=sizebase×scale×1/ratio
h
e
i
g
h
t
a
n
c
h
o
r
=
s
i
z
e
b
a
s
e
×
s
c
a
l
e
×
r
a
t
i
o
height_{anchor} = size_{base} \times scale \times \sqrt{ratio}
heightanchor=sizebase×scale×ratio
举个例子,我们按照论文中取3种大小比例以及3种长宽比例,那么每个cell生成的锚框个数为
k
=
9
k=9
k=9,而假设我们的特征图大小为
W
×
H
=
2400
W\times H=2400
W×H=2400,那么我们一共生成了
W
H
k
WHk
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) @property def num_depth(self): return self._num_depth def _generate_anchors(self, stride, base_size, ratios, scales, alloc_size): # 计算中心点坐标 px, py = (base_size - 1) * 0.5, (base_size - 1) * 0.5 base_sizes = [] for r in ratios: for s in scales: size = base_size * base_size / r ws = np.round(np.sqrt(size)) w = (ws * s - 1) * 0.5 h = (np.round(ws * r) * s - 1) * 0.5 base_sizes.append([px - w, py - h, px + w, py + h]) # 每个像素的锚框 base_sizes = np.array(base_sizes) # 下面进行偏移量的生成 width, height = alloc_size offset_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.value a = nd.slice_like(anchors, x * 0, axes=(2, 3)) return a.reshape((1, -1, 4))
这一步中就是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卷积层对每个锚框分别预测(1)类别分数(背景or物体) s c o r e score score(2)锚框偏移量 o f f s e t offset offset。而这些预测值 s c o r e 、 o f f s e t score、offset score、offset将用于后面的NMS操作,可以去除一些得分低,或者有大量重复区域的锚框,从而最终输出良好的Region Proposal给后面网络进行处理。
上面介绍了,两个1x1卷积层的输入为RPN-feature map,1x1卷积并不改变特征图尺寸,我们采用通道数来表示对应cell锚框的预测值。假设输入RPN-feature map 形状为 ( C , H , W ) (C,H,W) (C,H,W),每个cell生成了 k k k个锚框。输出的锚框分数和偏移量在空间位置上一一对应(也就是尺寸不变)。
代码很简单,就是添加两个卷积层并前向运算:
# 预测偏移量和预测类别的卷积层
# 使用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)
上面的步骤中,我们会对feature map的每个cell都生成多个锚框,并且预测 s c o r e 、 o f f s e t score、offset score、offset,我们生成了 W H k WHk WHk个锚框(大约有2W个),不难想象,大量的锚框其实都是背景,而且有着大量的重叠锚框,我们不可能将所有的锚框都当做Region Proposal输出给RoI Pooling层,提供给Fast-RCNN进行后面的进一步运算。第一个原因是会造成计算量过大,第二个原因是大量的背景框,重复的锚框是没有意义的,我们应该输出得分最高的topk个锚框。最后一步的Region Proposal具体处理过程如下:
通过这一步,我们筛选出了置信度最高的Region Proposal,也就是我们认为最有可能有物体的区域,输入到后面的Fast-RCNN网络中,进行最终的分类以及再一次的边界框回归预测。MXNet GluonCV 中生成Region Proposal的类源码如下:
class RPNProposal(gluon.Block): """ @:parameter ------------------ clip : float 如果提供,将bbox剪切到这个值 num_thresh : float nms的阈值,用于去除重复的框 train_pre_nms : int 训练时对前 train_pre_nms 进行 NMS操作 train_post_nms : int 训练时进行NMS后,返回前 train_post_nms 个region proposal test_pre_nms : int 测试时对前 test_pre_nms 进行 NMS操作 test_post_nms : int 测试时进行NMS后,返回前 test_post_nms 个region proposal min_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 = clip self._nms_thresh = nms_thresh self._train_pre_nms = train_pre_nms self._train_post_nms = train_post_nms self._test_pre_nms = test_pre_nms self._test_post_nms = test_post_nms self._min_size = min_size self._bbox_decoder = NormalizedBoxCenterDecoder(stds=stds, clip=clip) self._cliper = BBoxClipToImage() self._bbox_tocenter = BBoxCornerToCenter(axis=-1, split=False) """ @:parameter scores : (B,N,1) 通过RPN预测的得分输出(sigmoid之后) (0,1) offsets : ndarray (B,N,4) 通过RPN预测的锚框偏移量 anchors : ndarray (B,N,4) 生成的默认锚框,坐标编码方式为 Corner img : ndarray (B,C,H,W) 图像的张量,用来剪切锚框 @:returns """ def forward(self, scores, offsets, anchors, img): # 训练和预测的处理流程不同 if autograd.is_training(): pre_nms = self._train_pre_nms post_nms = self._train_post_nms else: pre_nms = self._test_pre_nms post_nms = self._test_post_nms with 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_min height = y_max - y_min invalid_mask = (width < self._min_size) + (height < self._min_size) # 将对应位置的score 设为-1 scores = 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网络输出应为一系列 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 proposal test_pre_nms : int 测试时对前 test_pre_nms 进行 NMS操作 test_post_nms : int 测试时进行NMS后,返回前 test_post_nms 个region proposal min_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_depth self._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坐标 Corner raw_score : ndarray (B,N,1) 卷积层的原始输出,用来训练RPN rpn_bbox_pred : ndarray (B,N,4) 卷积层的原始输出,用来训练RPN anchors : 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 proposal rpn_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
上面说道通过RPN层后,我们进行了粗采样,输出了大约2000个Region Proposal,然而我们并不会将这个2000个Region Proposal全部送入RoI Pooling中进行计算,这样效率很低、计算很慢。论文作者对这些Region Proposal进行了采样处理,只采样了一小部分的Region Proposal送入之后的网络运算,而且训练过程的采样和预测过程的采样是不一样的。下面详细介绍一下处理流程。
训练过程的采样在Fast-RCNN论文中有提到,由于要考虑训练过程中正负样本均衡的问题,最终输出了128个Region Proposal,其中正样本的比例为0.25。正负样本的定义如下:
将所有Region Proposal打上标记后,进行随机采样,其中采样正样本的比例为0.25,其余的为负样本。最终采样输出128个Region Proposal,送入之后的网络进行处理计算。
测试过程中的采样很简单,直接采样Region Proposal中, s c o r e s scores scores为前topk个(比如300)的样本,目的就是提取最有可能为物体的区域输入到后面的网络了。
class RCNNTargetSampler(gluon.Block): """ @:parameter ------------ num_images : int 每个batch的图片数,目前仅支持1 num_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_images self._num_inputs = num_inputs self._num_samples = num_samples self._pos_thresh = pos_thresh self._pos_ratios = pos_ratio self._max_pos = int(np.round(num_samples * pos_ratio)) self._max_gt_box = max_gt_box def forward(self, rois, scores, gt_bboxes): """ @:parameter ----------- rois : ndarray (B,self._num_inputs,4) RPN输出的roi区域坐标,Corner scores : 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:ignore new_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 neg mask = nd.ones_like(iou_argmax) * 2 # 标记ignore 为 0 mask = nd.where(score < 0, nd.zeros_like(mask), mask) # 将正类标记为 3 pos pos_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 操作后的 index index = nd.argsort(rand) # 将三个结果进行shuffle mask = nd.take(mask, index) iou_argmax = nd.take(iou_argmax, index) # 下面进行采样 # 排序 3:pos 2:neg 0:ignore order = 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---->4 mask = nd.where(mask == 2, nd.ones_like(mask) * 4, mask) order = nd.argsort(mask, is_ascend=False) num_neg = self._num_samples - self._max_pos bottomk = 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
通过上一步的采样后,我们得到了一堆没有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()
就能实现了。
到了这一步我们的处理已经到了尾声了,我们通过RoI Pooling已经得到了固定尺寸的feature map,最后一步就是用Fast-RCNN,进行预测类别分数以及边界框的回归。具体的处理流程如下:
最后如果是测试的话,那么将输入的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=1 self._num_sample = num_sample self._rpn_test_post_nms = rpn_test_post_nms self._target_generator = {RCNNTargetGenerator(self.num_class)} with self.name_scope(): # Faster-RCNN的RPN self.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.25 self.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) @property def 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_index with 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 nms for 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,bbox return ids, scores, bboxes
总的来说Faster-RCNN主要的改进地方在于用RPN来生成候选区域,使整个预测,训练过程都能用深度学习的方法完成。Faster-RCNN达到了这一系列算法的巅峰,并且在论文中提出的基于anchor的物体检测方法,更是被之后的state-of-the-art的框架广泛采用。Faster-RCNN 在 COCO和PASCAL数据集上都取得了当时最好的成绩,感兴趣的话,具体数据在论文中都有详细提到。Faster-RCNN比SSD处理流程要复杂许多,其中还涉及到非常多的细节,例如如何对anchor进行标记,如何对整个网络进行训练等等,这些我会另外写一篇博客来记录Faster-RCNN的训练过程。
Faster-RCNN我也是学习了很久了,从读论文到看源码,最深的一个感受就是“纸上得来终觉浅,绝知此事要躬行”。论文上始终都是宏观的东西,看完之后觉得自己似乎是懂了,但是当写代码时,才会发现有许多许多问题。我想只有当把代码和论文同时完全理解,才能算真正的看懂了吧。现在我的水平还完全不够,还停留在能看懂,稍微改改能用的阶段,如果是一篇新论文,要自己从零开始复现,目前的我还做不到,不过坚持下去多看多想多学多写,每天进步一点点,我想在毕业之前应该能达到我想要的目标吧~
学习过程中还有一个很深的体会就是多看底层源码,我就是通过看GluonCV中Faster-RCNN源码才理解了论文中的许多细节,总之多向这些优秀的代码学习吧,特别是深度学习框架的一些高级API使用,只有看了源码才会想到,原来代码还可以这样编~
以上Faster-RCNN都是我的个人浅薄理解,欢迎大家指出我存在的问题~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。