RPN网络是Faster-RCNN的核心,因为我们的目标检测网络就是要确定目标的位置,那么怎么生成目标候选框,而又如何对候选框筛选训练,这部分就是RPN网络的内容。
相对于传统的滑动窗口以及RCNN的Selective Search方法,RPN网络思想是:
- 图像的每一个点都是潜在的目标中心(实际上意思和滑动窗口类似,只不过滑动窗口有点类似是串行,而RPN类似并行,多点开花,但是都是以每个像素点为中心,进行不同大小邻域的检测)。
- 另一方面,如果在原始图像对每个点或者每隔几个点作为候选Box的中心,则计算量很大,RPN网络则是将重点放在了经过特征提取后的feature map上了,将feature map上每个点作为候选点,同时,相对于原始图,RPN则在每一通道上都有Bounding Box的候选框,进一步的利用了feature map在特征提取上的优势。
代码来源simple-faster-rcnn-pytorch
参考
逐字理解目标检测simple-faster-rcnn-pytorch-master代码(二)
从编程实现角度学习Faster R-CNN(附极简实现)
下面看代码,这一部分主要看region_proposal_network.py部分的代码,前后穿插其他代码。
1.得到feature map
在trainer.py中,我们的训练器FasterRCNNTrainer首先利用预训练的vgg16模型,提取到了feature map.
#trainer.py
features = self.faster_rcnn.extractor(imgs)#97
进一步的,我们看一下得到的怎样的特征图呢?在faster_rcnn_vgg16.py中,我们找到了代码。
#model/faster_rcnn_vgg16.py
extractor, classifier = decom_vgg16()#63
#model/faster_rcnn_vgg16.py
def decom_vgg16():#12
# the 30th layer of features is relu of conv5_3
if opt.caffe_pretrain:
model = vgg16(pretrained=False)
if not opt.load_path:
model.load_state_dict(t.load(opt.caffe_pretrain_path))
else:
model = vgg16(not opt.load_path)
features = list(model.features)[:30]
...
return nn.Sequential(*features), classifier
通过注释,我们也看到其拿到的是conv5_3经过relu后的特征图,通过对VGG16网络的分析,我们大概知道应该是14×14×512这个大小的特征图,相对原图缩小了16倍。
但是训练时就不是VGG16这个大小,我们从图像的输入找起。从train.py一路找起,找到data/dataset.py代码。
#train.py
dataset = Dataset(opt)#53
#data/dataset.py
class Dataset:#100
...
def __init__(self, opt):
...
self.tsf = Transform(opt.min_size, opt.max_size)
def __getitem__(self, idx):
ori_img, bbox, label, difficult = self.db.get_example(idx)
img, bbox, label, scale = self.tsf((ori_img, bbox, label))
return img.copy(), bbox.copy(), label.copy(), scale
#data/dataset.py
class Transform(object):#77
...
def __call__(self, in_data):
...
img = preprocess(img, self.min_size, self.max_size)
...
return img, bbox, label, scale
#data/dataset.py
def preprocess(img, min_size=600, max_size=1000):#42
C, H, W = img.shape
scale1 = min_size / min(H, W)
scale2 = max_size / max(H, W)
scale = min(scale1, scale2)
img = img / 255.
img = sktsf.resize(img, (C, H * scale, W * scale), mode='reflect',anti_aliasing=False)
...
return normalize(img)
所以,可以看到图像大小是经过resize的,由于训练时候采用的VOC2007数据集是500×375大小,这里经过了数据预处理,送入网络的大小为800×600,那么经过VGG16部分特征提取后,得到的feature map大小为(800/16, 600/16, 512)=(50, 37, 512).
产生anchor
得到了feature map,那么我们RPN网络就拿它开刀,我们从最初trianer.py看起.
#trainer.py
rpn_locs, rpn_scores, rois, roi_indices, anchor = \
self.faster_rcnn.rpn(features, img_size, scale)#99
可以看到RPN网络确实只对feature map感兴趣。
#model/faster_rcnn_vgg16.py
rpn = RegionProposalNetwork(
512, 512,
ratios=ratios,
anchor_scales=anchor_scales,
feat_stride=self.feat_stride,
)#70
终于到了model/region_proposal_network.py部分
#model/region_proposal_network.py
class RegionProposalNetwork(nn.Module):#10
def __init__(...)
self.anchor_base = generate_anchor_base(..)
self.proposal_layer = ProposalCreator(self, **proposal_creator_params)
n_anchor = self.anchor_base.shape[0]
self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
...
首先,我们先熟悉下init这里面的几个很重要的成员,后面都会用到。然后,我们进行过程分析,先看forward部分。
#model/region_proposal_network.py
def forward(self, x, img_size, scale=1.):#62
n, _, hh, ww = x.shape
anchor = _enumerate_shifted_anchor(
np.array(self.anchor_base),
self.feat_stride, hh, ww)
....
这里是得到anchor,利用用到了两个函数一个是self.anchor_base,这个可以通过init部分找到其来源,另一个是_enumerate_shifted_anchor,我们一个一个看:
#model/utils/bbox_tools.py
def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2],
anchor_scales=[8, 16, 32]):#195
py = base_size / 2.
px = base_size / 2.
anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4),
dtype=np.float32)
for i in six.moves.range(len(ratios)):
for j in six.moves.range(len(anchor_scales)):
h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i])
index = i * len(anchor_scales) + j
anchor_base[index, 0] = py - h / 2.
anchor_base[index, 1] = px - w / 2.
anchor_base[index, 2] = py + h / 2.
anchor_base[index, 3] = px + w / 2.
return anchor_base
是不是很熟悉这个函数,没错,这个就是我们第一篇博客里面强调的anchor生成函数,就是生成9个不同比例大小的box,接下来:
这部分详细可参考逐字理解目标检测simple-faster-rcnn-pytorch-master代码(二)
#model/region_proposal_network.py
def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):#137
###利用anchor_base生成所有对应feature map的anchor
import numpy as xp
shift_y = xp.arange(0, height * feat_stride, feat_stride) #纵向偏移量(0,16,32,...)
shift_x = xp.arange(0, width * feat_stride, feat_stride)# 横向偏移量(0,16,32,...)
shift_x, shift_y = xp.meshgrid(shift_x, shift_y)
#shift_x = [[0,16,32,..],[0,16,32,..],[0,16,32,..]...]
#shift_y = [[0,0,0,..],[16,16,16,..],[32,32,32,..]...]
shift = xp.stack((shift_y.ravel(), shift_x.ravel(),
shift_y.ravel(), shift_x.ravel()), axis=1)
A = anchor_base.shape[0]#A=9
K = shift.shape[0]#读取特征图中元素的总个数
anchor = anchor_base.reshape((1, A, 4)) + \
shift.reshape((1, K, 4)).transpose((1, 0, 2))
#用基础的9个anchor的坐标分别和偏移量相加,最后得出了所有的anchor的坐标
anchor = anchor.reshape((K * A, 4)).astype(np.float32)
return anchor
这个分析起来就比较复杂,但是一步一步跟着代码走也并不麻烦,这里的height和width就是特征图的大小,按照我们前面分析,特征图的大小应该为50×37那样,代码的中间部分就是生成网格,每个网格交叉点都有大小不同的9个anchor,那么整体的所有的anchor就是返回量,最后的anchor的数量为50×37×9=16650个。于是就产生了所有的anchor,这么多的anchor不能都用于训练吧,下面就开始对anchor进行操作了。
anchor的处理
在对anchor筛选之前,我们首先需要找到了一个依据对anchors预判,包括重要的Bound Box回归,那么怎么做呢,就要对feature map进行操作啦,之前只用了其大小…首先看对feature map的处理代码
#model/region_proposal_network.py
anchor = _enumerate_shifted_anchor(
np.array(self.anchor_base),
self.feat_stride, hh, ww)#102
h = F.relu(self.conv1(x))
rpn_locs = self.loc(h)
rpn_scores = self.score(h)
RPN网络在通过特征图中的每个anchor的可能所属的box以及所属前景或者背景做了预测,其中self.loc做box的预测,为后面做bound box回归提供依据,self.score则为当前点为前景或者背景做了预测,为后面的anchor筛选提供依据。
#model/region_proposal_network.py
rois = list()#120
roi_indices = list()
for i in range(n):
roi = self.proposal_layer(
rpn_locs[i].cpu().data.numpy(),
rpn_fg_scores[i].cpu().data.numpy(),
anchor, img_size,
scale=scale)
batch_index = i * np.ones((len(roi),), dtype=np.int32)
rois.append(roi)
roi_indices.append(batch_index)
其中rpn_fg_scores为特征图某点的为前景(物体)的概率图。找到self.proposal_layer的实现
#model/utils/creator_tool.py
class ProposalCreator:#291
def __init__(self,
parent_model,
nms_thresh=0.7,
n_train_pre_nms=12000,
n_train_post_nms=2000,
...
):
...
def __call__(self, loc, score,
anchor, img_size, scale=1.):
roi = loc2bbox(anchor, loc)
...
keep = np.where((hs >= min_size) & (ws >= min_size))[0]
roi = roi[keep, :]
score = score[keep]
order = score.ravel().argsort()[::-1]
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
...
keep = non_maximum_suppression(...)
if n_post_nms > 0:
keep = keep[:n_post_nms]
roi = roi[keep]
return roi
这几段代码就很清晰了首先是loc2bbox做Bound box回归,然后,按照前景概率做过滤,最后对过滤后的框做非极大值抑制,得到n_post_nms=n_train_post_nms=2000个候选的框区域。
到此,我们通过RegionProposalNetwork的前向传播,就得到了,
- rpn_locs:所用的anchor的在特征图上的预测位置
- rpn_scores:所有的anchor的在特征图上的前景
- rois:筛选后的候选区域
- roi_indices:筛选后的候选区域的下标
- anchor:所有的anchor
其中,rpn_locs和rpn_scores可帮助我们训练RegionProposalNetwork中的self.conv1()、 self.loc()、self.score()卷积网络, rois可为我们下一步分类网络训练提供来源。
RPN网络的结构如下:
这里的
输入的feature map,
3×3 conv,512为我们的self.conv1(),
1×1 conv ,18为我们的self.score(),辅助进行候选框的筛选,
1×1 conv,36为我们的self.loc(),辅助anchor定位
RPN通过generate_anchor生成的anchors单独靠计算生成,不涉及网络结构,但是RPN中的网络会辅助对这些anchor的回归和筛选,回归和筛选的好坏就全靠这些网络,因此需要训练。
至此,RPN网络的分析就结束了,这里得到了RPN loss的来源,下一篇,我们开始分析分类网络以及损失函数。