欢迎访问我的博客首页。
Faster R-CNN
1. 网络框架
下面以这个PyTorch 实现的 Faster R-CNN 为例。
1.1 框架图
图
1
网
络
数
据
流
图\ 1\quad 网络数据流
图 1网络数据流
1.2 框架描述
1. 主干网络
- 主干网络把输入图像的高宽缩小 16 倍,从 3x800x800 变成 1024x50x50。得到的公共特征图对应 2500 个锚点。
2. RPN
- 产生建议框。假设每个锚点产生 9 个建议框。RPN 网络使用 36 个 1x1 的卷积核从公共特征图提取 2500 个锚点处目标的坐标信息。因为每个锚点对应 9 个先验框,每个先验框对应的目标包括 4 个坐标,所以需要 36 个卷积核。RPN 网络使用 18 个 1x1 的卷积核从公共特征图提取 2500 个锚点处目标的类别信息。因为每个锚点对应 9 个先验框,每个先验框是不是背景包含 2 种可能,所以需要 18 个卷积核。
- 筛选建议框。当检测到的坐标宽高足够大且不是背景的可能性大于一定阈值时被认为是建议框。再通过非极大值抑制算法筛选出 600 个建议框。
- 损失函数。根据 IOU 大小,从 RPN 在每个先验框处检测到的结果(坐标和类别)中筛选出 256 个正负样本,与标注框构造损失函数。
- 注意:RPN 的损失函数利用标注坐标和标注类别。对于类别,RPN 只关心目标是不是背景,不关心目标具体属于哪一类。
3. ROIPooling
- 根据标注信息和 IOU 大小,从 600 个建议框中筛选 128 个坐标样本,得到它们的标注坐标和标注的具体类别。
- 使用 ROIPool 获取公共特征图上,128 个样本坐标对应区域的特征,从这些特征只再次提取位置和类别信息。
- 使用 1 得到的 128 个结果和 2 得到的 128 个结果构造损失函数。
1.3 框架代码
class FasterRCNNTrainer(nn.Module):
# ...
def forward(self, imgs, bboxes, labels, scale):
""" 参数:
图像:numpy.ndarray,shape = [1, 3, 800, 800]。
标注框:list,见下面第一个for循环的开头。
类别标签:list,见下面第一个for循环的开头。
scale:1。
"""
# 1.batch_size和图像高宽。
n = imgs.shape[0]
img_size = imgs.shape[2:]
# 2.获取公用特征层,base_feature.shape = [1, 1024, 50, 50]。
base_feature = self.faster_rcnn.extractor(imgs)
# 3.利用rpn网络获得建议框的偏移坐标、建议框的类别得分、建议框的像素坐标、batch中的建议框的下标、先验框。具体意义见下面。
rpn_locs, rpn_scores, rois, roi_indices, anchor = self.faster_rcnn.rpn(base_feature, img_size, scale)
# 4.定义rpn损失。
rpn_loc_loss_all, rpn_cls_loss_all, roi_loc_loss_all, roi_cls_loss_all = 0, 0, 0, 0
for i in range(n):
# 5.标注值。
bbox = bboxes[i] # shape = (n,4),n是标注框(标签)的数量。
label = labels[i] # shape = (n,),n是标签(标注框)的数量。
# 6.rpn输出。
rpn_loc = rpn_locs[i] # shape = [22500, 4]。rpn输出的检测结果,即相对先验框的偏移量(dx,dy,dw,dh)。
rpn_score = rpn_scores[i] # shape = torch.Size([22500, 2])。rpn输出的分类结果,只区分背景与非背景。
# shape = torch.Size([600, 4])。建议框,(xmin,ymin,xmax,ymax)。
# 网络输出的偏移量转换为像素坐标再经过nms获取得分(非背景分类得分)最高的前600个建议框。
roi = rois[roi_indices == i]
# 7.rpn输出的内容对应的公共特征。
feature = base_feature[i] # shape = [1024, 50, 50]。主干网络输出的公共特征,batch之一。
# 8.为每个先验框匹配一个标注框,shape=(22500, 4),shape=(22500,)。
# 因为标注框很少,大部分先验框与标注框不重合。这样的先验框匹配第一个标注框,但类别标签是背景。
gt_rpn_loc, gt_rpn_label = self.anchor_target_creator(bbox, anchor, img_size)
# 9.转换数据类型。
gt_rpn_loc = torch.Tensor(gt_rpn_loc)
gt_rpn_label = torch.Tensor(gt_rpn_label).long()
if rpn_loc.is_cuda:
gt_rpn_loc = gt_rpn_loc.cuda()
gt_rpn_label = gt_rpn_label.cuda()
# 10.计算rpn网络的回归损失和分类损失。
rpn_loc_loss = _fast_rcnn_loc_loss(rpn_loc, gt_rpn_loc, gt_rpn_label, self.rpn_sigma)
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label, ignore_index=-1)
# 11.从建议框中选择128个正负样本。
# sample_roi:正负样本的像素坐标,shape = (128, 4)。
# gt_roi_loc:正负样本的偏移坐标。shape = (128, 4)。
# gt_roi_label:正负样本的标签。shape = (128,)。这个标签区分目标类别。
sample_roi, gt_roi_loc, gt_roi_label = \
self.proposal_target_creator(roi, bbox, label, self.loc_normalize_mean, self.loc_normalize_std)
# 12.转换数据类型。
sample_roi = torch.Tensor(sample_roi)
gt_roi_loc = torch.Tensor(gt_roi_loc)
gt_roi_label = torch.Tensor(gt_roi_label).long()
sample_roi_index = torch.zeros(len(sample_roi))
if feature.is_cuda:
sample_roi = sample_roi.cuda()
sample_roi_index = sample_roi_index.cuda()
gt_roi_loc = gt_roi_loc.cuda()
gt_roi_label = gt_roi_label.cuda()
# 13.ROIPooling。把正负样本的像素坐标sample_roi映射到公共特征,用最大池化获取公共特征映射区内的特征并统一尺寸。然后第2次检测、分类。
# roi_cls_locs:shape = [1,128,84]。对统一尺寸的特征进行第2次检测的结果。
# roi_score:shape=[1,128,21]。对统一尺寸的特征进行第2次分类的结果。
roi_cls_loc, roi_score = \
self.faster_rcnn.head(torch.unsqueeze(feature, 0), sample_roi, sample_roi_index, img_size)
# 14.第2次检测与分类的结果。
n_sample = roi_cls_loc.size()[1] # 128。
roi_cls_loc = roi_cls_loc.view(n_sample, -1, 4) # shape = [128,21,4]。
roi_loc = roi_cls_loc[torch.arange(0, n_sample), gt_roi_label] # shape = [128,4]。
# 15.第2次检测与分类的损失。
roi_loc_loss = _fast_rcnn_loc_loss(roi_loc, gt_roi_loc, gt_roi_label.data, self.roi_sigma)
roi_cls_loss = nn.CrossEntropyLoss()(roi_score[0], gt_roi_label)
# 16.两次检测与分类的损失。
rpn_loc_loss_all += rpn_loc_loss
rpn_cls_loss_all += rpn_cls_loss
roi_loc_loss_all += roi_loc_loss
roi_cls_loss_all += roi_cls_loss
# 17.统计损失。
losses = [rpn_loc_loss_all / n, rpn_cls_loss_all / n, roi_loc_loss_all / n, roi_cls_loss_all / n]
losses = losses + [sum(losses)]
return LossTuple(*losses)
2. 重点概念
- 先验框:在图像平面均匀产生的框,每组 9 个。称为 anchor,坐标形式是 (xmin, ymin, xmax, ymax)。
- 锚点:一组先验框的中心坐标称为锚点。事实上,先验框就给根据锚点产生的。
- 建议框:RPN 检测到的目标位置,经过非极大值抑制算法筛选。称为 roi,坐标形式是 (xmin, ymin, xmax, ymax)。
3. 先验框
1. 基础先验框
以某一个锚点为中心,产生一组先验框。把这组先验框平移,就可以为图像上的不同区域生成先验框。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpathes
def generate_anchor_base(base_size=16, ratios=(0.5, 1, 2), anchor_scales=(8, 16, 32)):
anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4), dtype=np.float32)
for i in range(len(ratios)):
for j in 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] = - h / 2.
anchor_base[index, 1] = - w / 2.
anchor_base[index, 2] = h / 2.
anchor_base[index, 3] = w / 2.
return anchor_base
def draw(rects):
fig, ax = plt.subplots()
ax.xaxis.set_ticks_position('top')
ax.invert_yaxis()
colors = ['r', 'g', 'b']
for i, rect in enumerate(rects):
# (xmin, ymin, w, h)
temp = mpathes.Rectangle(rect[:2], rect[2] - rect[0], rect[3] - rect[1],
color=colors[i % 3], fill=False, linewidth=1)
ax.add_patch(temp)
plt.axis('equal')
plt.grid()
plt.show()
if __name__ == '__main__':
anchors = generate_anchor_base()
draw(anchors)
base_size 设置为 16 是因为从原图到公共特征图,边长缩小了 16 倍,也就是说公共特征图上每个像素点对应原图上一个 16x16 的正方形区域。
2. 一幅图像上的所有先验框
利用第一步产生的一组先验框,在图像上均匀产生多组先验框。
def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):
# 计算网格中心点
shift_x = np.arange(0, width * feat_stride, feat_stride)
shift_y = np.arange(0, height * feat_stride, feat_stride)
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
shift = np.stack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel(),), axis=1)
# 每个网格点上的9个先验框
A = anchor_base.shape[0]
K = shift.shape[0]
anchor = anchor_base.reshape((1, A, 4)) + shift.reshape((K, 1, 4))
# 所有的先验框
anchor = anchor.reshape((K * A, 4)).astype(np.float32)
return anchor
if __name__ == '__main__':
anchors = generate_anchor_base()
anchors_all = _enumerate_shifted_anchor(anchors, 16, 30, 30)
anchors_all = np.concatenate((anchors_all[:9], anchors_all[-9:]), axis=0)
draw(anchors_all)
feat_stride= 16,height = width = 50。由第 3、4 行知,该函数在边长为 16x50 的平面上,每隔 16 个像素产生一个锚点,以每个锚点产生 9 个先验框。运行结果:
图
2
先
验
框
图\ 2\quad 先验框
图 2先验框
左图是第一步产生的一组先验框。中图是注释掉第 18 行的运行结果,它是在宽高都为 30 的图像上产生的先验框。右图是没有注释掉第 18 行的运行结果,它只画出了在宽高都为 30 的图像上产生的第一组先验框(左上方)和最后一组先验框(右下方)。
4. 计算 iou
利用 numpy 的广播特性:
def bbox_iou(bbox_a, bbox_b):
# type(bbox_a) = type(bbox_b) = type(res) = np.array.
# bbox_a.shape = (m, 4), bbox_b.shape = (n, 4), res.shape = (m, n).
if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
raise IndexError
tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
return area_i / (area_a[:, None] + area_b - area_i)
5. 区域生成网络 RPN
区域生成网络如图 1 淡黄色部分。其中生成建议框的部分为灰色。
5.1 从公共特征图提取目标位置与类别
class RegionProposalNetwork(nn.Module):
def __init__(
self, in_channels=512, mid_channels=512, ratios=[0.5, 1, 2],
anchor_scales=[8, 16, 32], feat_stride=16,
mode="training",
):
super(RegionProposalNetwork, self).__init__()
self.feat_stride = feat_stride # 值为16意味着公共特征图每个像素对应原图边长为16的正方形区域。
self.proposal_layer = ProposalCreator(mode) # 从为22500个先验框预测的结果中筛选出建议框。
# 1.生成基础先验框,shape为[9, 4]。
self.anchor_base = generate_anchor_base(anchor_scales=anchor_scales, ratios=ratios)
n_anchor = self.anchor_base.shape[0] # self.anchor_base.shape(9, 4).
# 2.改变公共特征图的通道数。
self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
# 3.分类预测先验框内部是否包含物体:从公共特征图中预测22500个先验框对应的目标类别,只区分背景与非背景。
self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
# 4.回归预测对先验框进行调整:从公共特征图中预测22500个先验框对应的目标相对于各先验框的位置偏移量。
self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
# 5.对FPN的网络部分进行权值初始化
normal_init(self.conv1, 0, 0.01)
normal_init(self.score, 0, 0.01)
normal_init(self.loc, 0, 0.01)
def forward(self, x, img_size, scale=1.):
n, _, h, w = x.shape # n = batch_size = 1.
# 1.先进行一个3x3的卷积,可理解为特征整合:把公共特征图处理一下作为RPN的输入。
x = F.relu(self.conv1(x)) # shape = [1, 1024, 50, 50] -> shape = [1, 512, 50, 50]。
# 2.回归预测对先验框进行调整。预测位置。
rpn_locs = self.loc(x) # shape=[1, 36, 50, 50].
rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4) # shape=[1, 22500, 4].
# 3.分类预测先验框内部是否包含物体。预测是否包含目标。
rpn_scores = self.score(x) # shape=[1, 18, 50, 50].
rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous().view(n, -1, 2) # shape=[1, 22500, 2].
# 4.进行softmax概率计算,每个先验框只有两个判别结果。rpn_softmax_scores[:, :, 1]的内容为包含物体的概率。
rpn_softmax_scores = F.softmax(rpn_scores, dim=-1) # shape=[1, 22500, 2].
rpn_fg_scores = rpn_softmax_scores[:, :, 1].contiguous() # shape=[1, 22500].
rpn_fg_scores = rpn_fg_scores.view(n, -1) # shape=[1, 22500].
# 5.生成先验框,此时获得的anchor是布满网格点的,当输入图片为600,600,3的时候,shape为(12996, 4)
anchor = _enumerate_shifted_anchor(np.array(self.anchor_base), self.feat_stride, h, w) # shape=(22500, 4).
# 6.用检测和分类结果生成建议框。
rois = list()
roi_indices = list() # 这个用于ROIPool函数。
for i in range(n):
roi = self.proposal_layer(
rpn_locs[i], rpn_fg_scores[i], anchor, img_size, scale=scale) # shape=[600, 4].
batch_index = i * torch.ones((len(roi),))
rois.append(roi)
roi_indices.append(batch_index)
rois = torch.cat(rois, dim=0)
roi_indices = torch.cat(roi_indices, dim=0)
return rpn_locs, rpn_scores, rois, roi_indices, anchor
函数 ProposalCreator 的具体实现如下。
5.2 生成建议框
输入从公共特征图提取的目标位置与类别,输出建议框坐标。
class ProposalCreator:
def __init__(self, mode, nms_thresh=0.7, # nms函数的阈值参数。
n_train_pre_nms=12000, # 训练时,先选择IOU最大的前12000个建议框。
n_train_post_nms=600, # 训练时,再使用nms从12000个建议框中选择600个建议框。
n_test_pre_nms=3000,
n_test_post_nms=300,
min_size=16):
self.mode = mode
self.nms_thresh = nms_thresh
self.n_train_pre_nms = n_train_pre_nms
self.n_train_post_nms = n_train_post_nms
self.n_test_pre_nms = n_test_pre_nms
self.n_test_post_nms = n_test_post_nms
self.min_size = min_size
def __call__(self, loc, score, anchor, img_size, scale=1.): # loc.shape=[22500, 4], score.shape=[22500].
if self.mode == "training":
n_pre_nms = self.n_train_pre_nms
n_post_nms = self.n_train_post_nms
else:
n_pre_nms = self.n_test_pre_nms
n_post_nms = self.n_test_post_nms
# 1.先验框。
anchor = torch.from_numpy(anchor)
if loc.is_cuda:
anchor = anchor.cuda()
# 2.将RPN网络预测结果(偏移量)转化成建议框(xmin, ymin, xmax, ymax)。然后防止建议框超出图像边缘。
roi = loc2bbox(anchor, loc) # shape=[22500, 4].
roi[:, [0, 2]] = torch.clamp(roi[:, [0, 2]], min=0, max=img_size[1])
roi[:, [1, 3]] = torch.clamp(roi[:, [1, 3]], min=0, max=img_size[0])
# 3.筛选建议框:建议框的宽高的最小值不可以小于16。
min_size = self.min_size * scale # 16 = 16 * 1.
keep = torch.where(((roi[:, 2] - roi[:, 0]) >= min_size) & ((roi[:, 3] - roi[:, 1]) >= min_size))[0]
roi = roi[keep, :] # shape=[22500,4] -> shape=[22430,4]. 22430是不固定的。
score = score[keep] # shape=[22500] -> shape=[22430]. 22430是不固定的。
# 4.筛选建议框:IOU最大的前12000个。
order = torch.argsort(score, descending=True)
if n_pre_nms > 0: # 12000
order = order[:n_pre_nms]
roi = roi[order, :]
score = score[order] # 12000.
# 5.筛选建议框:使用非极大值抑制算法筛选出600个。
keep = nms(roi, score, self.nms_thresh)
keep = keep[:n_post_nms] # 获取得分最高的前600个建议框。
roi = roi[keep]
return roi
网络在每个先验框的位置检测目标,输出的位置检测结果是相对先验框的偏移量 loc。有时候需要把 loc 转换成像素坐标,loc2bbox 函数就是用来把偏移量转换为像素坐标。
def loc2bbox(src_bbox, loc):
# 输入:先验框src_Hbox.shape = [n, 4],坐标形式是(xmin, ymin, xmax, ymax)。
# 输入:网络输出的位置检测结果loc.shape = [n, 4],坐标形式是(dx, dy, dw, dh)。
# 输出:网络输出的位置检测结果的像素坐标dst_bbox.shape = [n, 4],坐标形式是(xmin, ymin, xmax, year)。
if src_bbox.size()[0] == 0:
return torch.zeros((0, 4), dtype=loc.dtype)
src_width = torch.unsqueeze(src_bbox[:, 2] - src_bbox[:, 0], -1)
src_height = torch.unsqueeze(src_bbox[:, 3] - src_bbox[:, 1], -1)
src_ctr_x = torch.unsqueeze(src_bbox[:, 0], -1) + 0.5 * src_width
src_ctr_y = torch.unsqueeze(src_bbox[:, 1], -1) + 0.5 * src_height
dx = loc[:, 0::4]
dy = loc[:, 1::4]
dw = loc[:, 2::4]
dh = loc[:, 3::4]
ctr_x = dx * src_width + src_ctr_x
ctr_y = dy * src_height + src_ctr_y
w = torch.exp(dw) * src_width
h = torch.exp(dh) * src_height
dst_bbox = torch.zeros_like(loc)
dst_bbox[:, 0::4] = ctr_x - 0.5 * w # xmin
dst_bbox[:, 1::4] = ctr_y - 0.5 * h # ymin
dst_bbox[:, 2::4] = ctr_x + 0.5 * w # xmax
dst_bbox[:, 3::4] = ctr_y + 0.5 * h # ymax
return dst_bbox
该函数第 7 至 10 行把 (xmin, ymin, xmax, ymax) 形式的先验框转换成 (cx, cy, w, h) 形式。再根据下面的公式把预测的偏移量转换为像素坐标。
{ C X = d x ⋅ w + c x C Y = d y ⋅ h + c y W = w 2 ⋅ e x p ( d w ) H = h 2 ⋅ e x p ( d h ) \begin{cases} CX = dx \cdot w + cx \\ CY = dy \cdot h + cy \\ W = \frac{w}{2} \cdot exp(dw) \\ H = \frac{h}{2} \cdot exp(dh) \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧CX=dx⋅w+cxCY=dy⋅h+cyW=2w⋅exp(dw)H=2h⋅exp(dh)
5.3 RPN 的损失函数
1. 利用标注信息获取每个先验框附近目标的位置和类别
class AnchorTargetCreator(object):
def __init__(self, n_sample=256, pos_iou_thresh=0.7, neg_iou_thresh=0.3, pos_ratio=0.5):
self.n_sample = n_sample # 样本总数。
self.pos_iou_thresh = pos_iou_thresh # 正样本的IOU阈值。
self.neg_iou_thresh = neg_iou_thresh # 负样本的IOU阈值。
self.pos_ratio = pos_ratio # 正样本占样本总数的比例。
def __call__(self, bbox, anchor, img_size):
# 1.为每个先验框匹配一个标注框。绝大多数先验框与任何标注框都没交集,这些先验框匹配第一个标注框但标签是背景。即argmax_ious大部分元素为0.
# argmax_ious[i]=x:下标为i的先验框与标注框的最大iou是x。
argmax_ious, label = self._create_label(anchor, bbox) # argmax_ious.shape = label.shape=(22500,).
# 2.(xmin,ymin,xmax,ymax)转偏移坐标。
if (label > 0).any():
loc = bbox2loc(anchor, bbox[argmax_ious])
return loc, label
else:
return np.zeros_like(anchor), label
def _calc_ious(self, anchor, bbox):
# 1.获取先验框与标注框的交并比。
ious = bbox_iou(anchor, bbox) # ious.shape=(22500, n), n是标注框的个数。
if len(bbox) == 0:
return np.zeros(len(anchor), np.int32), np.zeros(len(anchor)), np.zeros(len(bbox))
# 2.获得每一个先验框最对应的标注框及交并比。
argmax_ious = ious.argmax(axis=1) # argmax_ious.shape=(22500,)。
max_ious = np.max(ious, axis=1)
# 3.获得每个标注框最对应的先验框 [num_gt, ]
gt_argmax_ious = ious.argmax(axis=0) # gt_argmax_ious.shape=(n,), n是标注框的个数。
# 4.保证每一个真实框都存在对应的先验框:假如标注框a的最好先验框是b,则把先验框b的最好标注框设置为a。
for i in range(len(gt_argmax_ious)):
argmax_ious[gt_argmax_ious[i]] = i
return argmax_ious, max_ious, gt_argmax_ious
def _create_label(self, anchor, bbox):
# 1.先验框的标签。1是正样本,0是负样本,-1忽略。初始化的时候全部设置为-1。
label = np.empty((len(anchor),), dtype=np.int32)
label.fill(-1)
# ------------------------------------------------------------------------ #
# argmax_ious为每个先验框对应的最大的真实框的序号 [num_anchors, ]
# max_ious为每个真实框对应的最大的真实框的iou [num_anchors, ]
# gt_argmax_ious为每一个真实框对应的最大的先验框的序号 [num_gt, ]
# ------------------------------------------------------------------------ #
argmax_ious, max_ious, gt_argmax_ious = self._calc_ious(anchor, bbox)
# ----------------------------------------------------- #
# 如果小于门限值则设置为负样本
# 如果大于门限值则设置为正样本
# 每个真实框至少对应一个先验框
# ----------------------------------------------------- #
label[max_ious < self.neg_iou_thresh] = 0
label[max_ious >= self.pos_iou_thresh] = 1
if len(gt_argmax_ious) > 0:
label[gt_argmax_ious] = 1
# ----------------------------------------------------- #
# 判断正样本数量是否大于128,如果大于则限制在128
# ----------------------------------------------------- #
n_pos = int(self.pos_ratio * self.n_sample)
pos_index = np.where(label == 1)[0]
if len(pos_index) > n_pos:
disable_index = np.random.choice(pos_index, size=(len(pos_index) - n_pos), replace=False)
label[disable_index] = -1
# ----------------------------------------------------- #
# 平衡正负样本,保持总数量为256
# ----------------------------------------------------- #
n_neg = self.n_sample - np.sum(label == 1)
neg_index = np.where(label == 0)[0]
if len(neg_index) > n_neg:
disable_index = np.random.choice(neg_index, size=(len(neg_index) - n_neg), replace=False)
label[disable_index] = -1
return argmax_ious, label
6. 重要的 pytorch 函数
1. softmax
out = torch.tensor([[[-4, 4], [-3, 4], [3, 5]]], dtype=torch.float32) # shape=[1, 3, 2].
score = F.softmax(out, dim=-1)
# tensor([[[3.3535e-04, 9.9966e-01],
# [9.1105e-04, 9.9909e-01],
# [1.1920e-01, 8.8080e-01]]])
fg_score = score[:, :, 1].contiguous()
# tensor([[0.9997, 0.9991, 0.8808]])
2. nms
from torchvision.ops import nms
boxes = torch.Tensor([[0, 0, 1, 1], [0, 0, 0.4, 1], [0, 0, 0.5, 1], [0, 0, 0.6, 1]]) # (xmin, ymin, xmax, ymax).
scores = torch.Tensor([0.4, 0.3, 0.2, 0.1])
iou_threshold = 0.5
selected_boxes_index = nms(boxes, scores, iou_threshold)
selected_boxes = boxes[selected_boxes_index]
print(selected_boxes)
# tensor([[0, 0, 1, 1], [0, 0, 0.4, 1]])
把 boxes 中重叠程度较大的框去掉。如果 boxes[i] 与 boxes[j] 的交并比大于 iou_threshold,根据它们的分数 scores[i] 与 scores[j],去掉分数较小的那个。
3. argsort
arr = torch.tensor([3, 1, 4, 0, 2])
argsort = torch.argsort(arr, descending=True)
print(argsort) # [2, 0, 4, 1, 3].
函数 argsort 用于获取递减排序后元素在原数组中的下标。
7. ROIPool 与 ROIAlign
1. ROIPool
这里介绍 roi_pool 函数的两种用法和实现。roi_pool 与 roi_align 的区别在下面介绍 ROIAlign 的部分。
import numpy as np
import torch
from torchvision.ops import roi_pool
def f1(features):
boxes = torch.Tensor([[1, 0, 0, 2, 2]])
pooled_features = roi_pool(features, boxes, [3, 3]) # Tensor[K, 5].
print(pooled_features)
def f2(features):
box1 = torch.Tensor([[0, 0, 2, 2], [0, 0, 1, 1]])
box2 = torch.Tensor([[1, 1, 3, 3]])
boxes = [box1, box2]
pooled_features = roi_pool(features, boxes, [3, 3]) # List[Tensor[L, 4]].
print(pooled_features)
if __name__ == '__main__':
data = torch.Tensor(np.random.randint(0, 10, size=(3, 1, 5, 5)))
print(data)
f1(data)
f2(data)
函数 roi_pool 需要 4 个参数:input、boxes、output_size、spatial_scale:
- input,维度为 [b, c, h, w]。
- output_size,维度为 [2]。输出特种的高和宽。
- boxes,维度为 [K, 5] 或 List[Tensor[L, 4]]。当维度为 [K, 5] 时,boxes 的元素 box = [batch_idx, xmin, ymin, xmax, ymax]:对 input[batch_idx, :, ymin:ymax, xmin:xmax] 做最大池化,最终得到 res.shape = [K, c, output_size[0], output_size[1]];当维度为 List[Tensor[L, 4]] 时,boxes 的元素 box = [[xmin, ymin, xmax, ymax], …]:按 box 在 boxes 中的下标 idx,对 input[idx, :, ymin:ymax, xmin:xmax] 做最大池化,最终得到 res.shape = [len(boxes.reshape(-1, 4)), c, output_size[0], output_size[1]]。
下面实现参数 boxes 为 Tensor[K, 5] 类型的 roi_pool。
def roi_pool(input, boxes, output_size, spatial_scale=1):
assert boxes.dim() == 2 and boxes.size(1) == 5
boxes[:, 1:].mul_(spatial_scale)
boxes = boxes.int()
output = list()
adapt_max_pool = torch.nn.AdaptiveMaxPool2d(output_size)
for box in boxes:
batch_idx = box[0]
patch = input.narrow(0, batch_idx, 1)[..., box[2]:box[4] + 1, box[1]:box[3] + 1]
output.append(adapt_max_pool(patch))
output = torch.cat(output, 0)
return output
可以看出,roi_pool 就是对原特征上 box 映射的矩形区域做自适应最大池化。
2. ROIAlign
ROIAlign 是在 Mask R-CNN 中提出的方法。在 PyTorch 中,roi_align 的用法与 roi_pool 的用法相似。它们的一个重要区别是 roi_align 的坐标可以是浮点数,这是因为它使用双线性插值算法;而 roi_pool 的坐标即使是浮点数也会被丢掉小数部分,当作整数使用。
def ROIPool(features):
boxes = torch.tensor([[0, 1, 3, 7, 7]], dtype=torch.float32)
pooled_features = roi_pool(features, boxes, [2, 2])
print(pooled_features)
def ROIAlign(features):
boxes = torch.tensor([[0, 1, 3, 7, 7]], dtype=torch.float32)
pooled_features = roi_align(features, boxes, [2, 2])
print(pooled_features)
if __name__ == '__main__':
data = torch.tensor([[[[0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08],
[0.09, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16],
[0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24],
[0.25, 0.26, 0.27, 0.28, 0.29, 0.30, 0.31, 0.32],
[0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.40],
[0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48],
[0.49, 0.50, 0.51, 0.52, 0.53, 0.54, 0.55, 0.56],
[0.57, 0.58, 0.59, 0.60, 0.61, 0.62, 0.63, 0.64]]]], dtype=torch.float32)
ROIPool(data)
print()
ROIAlign(data)
# 输出
tensor([[[[0.4500, 0.4800],
[0.6100, 0.6400]]]])
tensor([[[[0.3550, 0.3850],
[0.5150, 0.5450]]]])
上面的代码中,ROI 部分在第 2 行和第 7 行指定:x 从 1 到 7,y 从 3 到 7,如图 7 中的绿色部分。如前所述,在第 7 行指定 x、y 时可以使用浮点数,这使得 roi_align 得到的结果更精确。
图
7
R
O
I
P
o
o
l
与
R
O
I
A
l
i
g
n
图\ 7 \quad ROIPool 与 ROIAlign
图 7ROIPool与ROIAlign
图 7 中绿色部分是宽为 7 高为 5 的 ROI 区域。因为输出特征指定为 2x2,roi_pool 函数分别用 7 和 5 除以 2,向上取整得到 4 和 3,然后把 ROI 划分成宽为 4 高为 3 的小块。因为不能整除,最下侧和最右侧小块的宽小于 5,高小于 3。然后在各小块内取最大值。
roi_align 根据浮点数坐标使用双线性插值算法,有点复杂,原理不再展开。
3. MultiScaleROIAlign
8. 参考
- Faster R-CNN,arxiv,2015
- Bubbliiiing 基于 PyTorch 的 Faster R-CNN github B站 CSDN
- 最流行的 Faster R-CNN 的 PyTorch 实现,Github
- Faster R-CNN 网络框架,CSDN
- 区域生成网络 RPN,知乎
- Faster R-CNN 组件详解,CSDN
- Faster R-CNN 的损失函数,CSDN
- 计算 iou 的方法,CSDN
- roi_pool 的实现,CSDN
- roipool 与 roialign,CSDN