compute_jaccard(anchors, ground_truth[:, 1:]) # 验证一下写的compute_jaccard函数
输出:tensor([[0.0536, 0.0000],
[0.1417, 0.0000],
[0.0000, 0.5657],
[0.0000, 0.2059],
[0.0000, 0.7459]])
下面实现MultiBoxTarget
函数来为锚框标注类别和偏移量。该函数将背景类别设为0,并令从零开始的目标类别的整数索引自加1(1为狗,2为猫)。
# 以下函数已保存在d2lzh_pytorch包中方便以后使用 def assign_anchor(bb, anchor, jaccard_threshold=0.5): """ # 按照「9.4.1. 生成多个锚框」图9.3所讲为每个anchor分配真实的bb, anchor表示成归一化(xmin, ymin, xmax, ymax). https://zh.d2l.ai/chapter_computer-vision/anchor.html Args: bb: 真实边界框(bounding box), shape:(nb, 4) anchor: 待分配的anchor, shape:(na, 4) jaccard_threshold: 预先设定的阈值 Returns: assigned_idx: shape: (na, ), 每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1 """ na = anchor.shape[0] nb = bb.shape[0] jaccard = compute_jaccard(anchor, bb).detach().cpu().numpy() # shape: (na, nb) assigned_idx = np.ones(na) * -1 # 存放标签初始全为-1 # 先为每个bb分配一个anchor(不要求满足jaccard_threshold) jaccard_cp = jaccard.copy() for j in range(nb): i = np.argmax(jaccard_cp[:, j]) assigned_idx[i] = j jaccard_cp[i, :] = float("-inf") # 赋值为负无穷, 相当于去掉这一行 # 处理还未被分配的anchor, 要求满足jaccard_threshold for i in range(na): if assigned_idx[i] == -1: j = np.argmax(jaccard[i, :]) if jaccard[i, j] >= jaccard_threshold: assigned_idx[i] = j return torch.tensor(assigned_idx, dtype=torch.long) def xy_to_cxcy(xy): """ 将(x_min, y_min, x_max, y_max)形式的anchor转换成(center_x, center_y, w, h)形式的. https://github.com/sgrvinod/a-PyTorch-Tutorial-to-Object-Detection/blob/master/utils.py Args: xy: bounding boxes in boundary coordinates, a tensor of size (n_boxes, 4) Returns: bounding boxes in center-size coordinates, a tensor of size (n_boxes, 4) """ return torch.cat([(xy[:, 2:] + xy[:, :2]) / 2, # c_x, c_y xy[:, 2:] - xy[:, :2]], 1) # w, h def MultiBoxTarget(anchor, label): """ # 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax). https://zh.d2l.ai/chapter_computer-vision/anchor.html Args: anchor: torch tensor, 输入的锚框, 一般是通过MultiBoxPrior生成, shape:(1,锚框总数,4) label: 真实标签, shape为(bn, 每张图片最多的真实锚框数, 5) 第二维中,如果给定图片没有这么多锚框, 可以先用-1填充空白, 最后一维中的元素为[类别标签, 四个坐标值] Returns: 列表, [bbox_offset, bbox_mask, cls_labels] bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4) bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1 cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数) """ assert len(anchor.shape) == 3 and len(label.shape) == 3 bn = label.shape[0] def MultiBoxTarget_one(anc, lab, eps=1e-6): """ MultiBoxTarget函数的辅助函数, 处理batch中的一个 Args: anc: shape of (锚框总数, 4) lab: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值] eps: 一个极小值, 防止log0 Returns: offset: (锚框总数*4, ) bbox_mask: (锚框总数*4, ), 0代表背景, 1代表非背景 cls_labels: (锚框总数, 4), 0代表背景 """ an = anc.shape[0] # 变量的意义 assigned_idx = assign_anchor(lab[:, 1:], anc) # (锚框总数, ) print("a: ", assigned_idx.shape) print(assigned_idx) bbox_mask = ((assigned_idx >= 0).float().unsqueeze(-1)).repeat(1, 4) # (锚框总数, 4) print("b: " , bbox_mask.shape) print(bbox_mask) cls_labels = torch.zeros(an, dtype=torch.long) # 0表示背景 assigned_bb = torch.zeros((an, 4), dtype=torch.float32) # 所有anchor对应的bb坐标 for i in range(an): bb_idx = assigned_idx[i] if bb_idx >= 0: # 即非背景 cls_labels[i] = lab[bb_idx, 0].long().item() + 1 # 注意要加一 assigned_bb[i, :] = lab[bb_idx, 1:] # 如何计算偏移量 center_anc = xy_to_cxcy(anc) # (center_x, center_y, w, h) center_assigned_bb = xy_to_cxcy(assigned_bb) offset_xy = 10.0 * (center_assigned_bb[:, :2] - center_anc[:, :2]) / center_anc[:, 2:] offset_wh = 5.0 * torch.log(eps + center_assigned_bb[:, 2:] / center_anc[:, 2:]) offset = torch.cat([offset_xy, offset_wh], dim = 1) * bbox_mask # (锚框总数, 4) return offset.view(-1), bbox_mask.view(-1), cls_labels # 组合输出 batch_offset = [] batch_mask = [] batch_cls_labels = [] for b in range(bn): offset, bbox_mask, cls_labels = MultiBoxTarget_one(anchor[0, :, :], label[b, :, :]) batch_offset.append(offset) batch_mask.append(bbox_mask) batch_cls_labels.append(cls_labels) bbox_offset = torch.stack(batch_offset) bbox_mask = torch.stack(batch_mask) cls_labels = torch.stack(batch_cls_labels) return [bbox_offset, bbox_mask, cls_labels]
我们通过unsqueeze
函数为锚框和真实边界框添加样本维。
labels = MultiBoxTarget(anchors.unsqueeze(dim=0), ground_truth.unsqueeze(dim=0))
输出:a: torch.Size([5])
tensor([-1, 0, 1, -1, 1])
b: torch.Size([5, 4])
tensor([[0., 0., 0., 0.],
[1., 1., 1., 1.],
[1., 1., 1., 1.],
[0., 0., 0., 0.],
[1., 1., 1., 1.]])
返回的结果里有3项,均为Tensor
。第三项表示为锚框标注的类别。
labels[2]
输出:tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1., 1., 1.]])
返回的第一项是为每个锚框标注的四个偏移量,其中负类锚框的偏移量标注为0。
labels[0]
输出:tensor([[-0.0000e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00, 1.4000e+00,
1.0000e+01, 2.5940e+00, 7.1754e+00, -1.2000e+00, 2.6882e-01,
1.6824e+00, -1.5655e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00,
-0.0000e+00, -5.7143e-01, -1.0000e+00, 4.1723e-06, 6.2582e-01]])
输出预测边界框
在模型预测阶段,我们先为图像生成多个锚框,并为这些锚框一一预测类别和偏移量。随后,我们根据锚框及其预测偏移量得到预测边界框。当锚框数量较多时,同一个目标上可能会输出较多相似的预测边界框。为了使结果更加简洁,我们可以移除相似的预测边界框。常用的方法叫作非极大值抑制(non-maximum suppression,NMS)。
anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95], [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]]) offset_preds = torch.tensor([0.0] * (4 * len(anchors))) cls_probs = torch.tensor([[0., 0., 0., 0.,], # 背景的预测概率 [0.9, 0.8, 0.7, 0.1], # 狗的预测概率 [0.1, 0.2, 0.3, 0.9]]) # 猫的预测概率
在图像上打印预测边界框和它们的置信度。
fig = d2l.plt.imshow(img) show_bboxes(fig.axes, anchors * bbox_scale, ['dog=0.9', 'dog=0.8', 'dog=0.7', 'cat=0.9'])
下面我们实现MultiBoxDetection
函数来执行非极大值抑制。
# 以下函数已保存在d2lzh_pytorch包中方便以后使用 from collections import namedtuple Pred_BB_Info = namedtuple("Pred_BB_Info", ["index", "class_id", "confidence", "xyxy"]) def non_max_suppression(bb_info_list, nms_threshold = 0.5): """ 非极大抑制处理预测的边界框 Args: bb_info_list: Pred_BB_Info的列表, 包含预测类别、置信度等信息 nms_threshold: 阈值 Returns: output: Pred_BB_Info的列表, 只保留过滤后的边界框信息 """ output = [] # 先根据置信度从高到低排序 sorted_bb_info_list = sorted(bb_info_list, key = lambda x: x.confidence, reverse=True) # 循环遍历删除冗余输出 while len(sorted_bb_info_list) != 0: best = sorted_bb_info_list.pop(0) output.append(best) if len(sorted_bb_info_list) == 0: break bb_xyxy = [] for bb in sorted_bb_info_list: bb_xyxy.append(bb.xyxy) iou = compute_jaccard(torch.tensor([best.xyxy]), torch.tensor(bb_xyxy))[0] # shape: (len(sorted_bb_info_list), ) n = len(sorted_bb_info_list) sorted_bb_info_list = [sorted_bb_info_list[i] for i in range(n) if iou[i] <= nms_threshold] return output def MultiBoxDetection(cls_prob, loc_pred, anchor, nms_threshold = 0.5): """ # 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax). https://zh.d2l.ai/chapter_computer-vision/anchor.html Args: cls_prob: 经过softmax后得到的各个锚框的预测概率, shape:(bn, 预测总类别数+1, 锚框个数) loc_pred: 预测的各个锚框的偏移量, shape:(bn, 锚框个数*4) anchor: MultiBoxPrior输出的默认锚框, shape: (1, 锚框个数, 4) nms_threshold: 非极大抑制中的阈值 Returns: 所有锚框的信息, shape: (bn, 锚框个数, 6) 每个锚框信息由[class_id, confidence, xmin, ymin, xmax, ymax]表示 class_id=-1 表示背景或在非极大值抑制中被移除了 """ assert len(cls_prob.shape) == 3 and len(loc_pred.shape) == 2 and len(anchor.shape) == 3 bn = cls_prob.shape[0] def MultiBoxDetection_one(c_p, l_p, anc, nms_threshold = 0.5): """ MultiBoxDetection的辅助函数, 处理batch中的一个 Args: c_p: (预测总类别数+1, 锚框个数) l_p: (锚框个数*4, ) anc: (锚框个数, 4) nms_threshold: 非极大抑制中的阈值 Return: output: (锚框个数, 6) """ pred_bb_num = c_p.shape[1] anc = (anc + l_p.view(pred_bb_num, 4)).detach().cpu().numpy() # 加上偏移量 confidence, class_id = torch.max(c_p, 0) confidence = confidence.detach().cpu().numpy() class_id = class_id.detach().cpu().numpy() pred_bb_info = [Pred_BB_Info( index = i, class_id = class_id[i] - 1, # 正类label从0开始 confidence = confidence[i], xyxy=[*anc[i]]) # xyxy是个列表 for i in range(pred_bb_num)] # 正类的index obj_bb_idx = [bb.index for bb in non_max_suppression(pred_bb_info, nms_threshold)] output = [] for bb in pred_bb_info: output.append([ (bb.class_id if bb.index in obj_bb_idx else -1.0), bb.confidence, *bb.xyxy ]) return torch.tensor(output) # shape: (锚框个数, 6) batch_output = [] for b in range(bn): batch_output.append(MultiBoxDetection_one(cls_prob[b], loc_pred[b], anchor[0], nms_threshold)) return torch.stack(batch_output)
然后我们运行MultiBoxDetection
函数并设阈值为0.5。这里为输入都增加了样本维。我们看到,返回的结果的形状为(批量大小, 锚框个数, 6)。其中每一行的6个元素代表同一个预测边界框的输出信息。第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),其中-1表示背景或在非极大值抑制中被移除。第二个元素是预测边界框的置信度。剩余的4个元素分别是预测边界框左上角的和轴坐标以及右下角的和轴坐标(值域在0到1之间)。
output = MultiBoxDetection( cls_probs.unsqueeze(dim=0), offset_preds.unsqueeze(dim=0), anchors.unsqueeze(dim=0), nms_threshold=0.5) output
fig = d2l.plt.imshow(img) for i in output[0].detach().cpu().numpy(): if i[0] == -1: continue label = ('dog=', 'cat=')[int(i[0])] + str(i[1]) show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)
实践中,我们可以在执行非极大值抑制前将置信度较低的预测边界框移除,从而减小非极大值抑制的计算量。我们还可以筛选非极大值抑制的输出,例如,只保留其中置信度较高的结果作为最终输出。
多尺度目标检测
在9.4节(锚框)中,我们在实验中以输入图像的每个像素为中心生成多个锚框。这些锚框是对输入图像不同区域的采样。然而,如果以图像每个像素为中心都生成锚框,很容易生成过多锚框而造成计算量过大。举个例子,假设输入图像的高和宽分别为561像素和728像素,如果以每个像素为中心生成5个不同形状的锚框,那么一张图像上则需要标注并预测200多万个锚框
d2l.set_figsize() def display_anchors(fmap_w, fmap_h, s): # 前两维的取值不影响输出结果(原书这里是(1, 10, fmap_w, fmap_h), 我认为错了) fmap = torch.zeros((1, 10, fmap_h, fmap_w), dtype=torch.float32) # 平移所有锚框使均匀分布在图片上 offset_x, offset_y = 1.0/fmap_w, 1.0/fmap_h anchors = d2l.MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5]) + \ torch.tensor([offset_x/2, offset_y/2, offset_x/2, offset_y/2]) bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32) d2l.show_bboxes(d2l.plt.imshow(img).axes, anchors[0] * bbox_scale) display_anchors(fmap_w=4, fmap_h=2, s=[0.15])
display_anchors(fmap_w=2, fmap_h=1, s=[0.4])
display_anchors(fmap_w=1, fmap_h=1, s=[0.8])
参考文献
[1]《动手深度学习》李沐
[2]伯禹教育课程jupyternotebook