目标检测和边界框

%matplotlib inline
from PIL import Imageimport sys
sys.path.append('/home/input/')  #数据集路径
import d2lzh1981 as d2l  #已封装好的包# 展示用于目标检测的图
d2l.set_figsize()   #已封装好的包
img = Image.open('/home/input/img2083/img/catdog.jpg')
d2l.plt.imshow(img); # 加分号只显示图

边界框

# bbox是bounding box的缩写
dog_bbox, cat_bbox = [60, 45, 378, 516], [400, 112, 655, 493]
def bbox_to_rect(bbox, color):  # 本函数已保存在d2lzh_pytorch中方便以后使用# 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:# ((左上x, 左上y), 宽, 高)return d2l.plt.Rectangle(xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],fill=False, edgecolor=color, linewidth=2)
fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));

锚框

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。不同的模型使用的区域采样方法可能不同。这里我们介绍其中的一种方法:它以每个像素为中心生成多个大小和宽高比(aspect ratio)不同的边界框。这些边界框被称为锚框(anchor box)。我们将在后面基于锚框实践目标检测。

注: 建议想学习用PyTorch做检测的童鞋阅读一下仓库a-PyTorch-Tutorial-to-Object-Detection

先导入一下相关包。

import numpy as np
import math
import torch
import os
IMAGE_DIR = '/home/input/img2083/img/'

生成多个锚框

假设输入图像高为

,宽为
。我们分别以图像的每个像素为中心生成不同形状的锚框。设大小为
且宽高比为
,那么锚框的宽和高将分别为
。当中心位置给定时,已知宽和高的锚框是确定的。

下面我们分别设定好一组大小

和一组宽高比
。如果以每个像素为中心时使用所有的大小与宽高比的组合,输入图像将一共得到
个锚框。虽然这些锚框可能覆盖了所有的真实边界框,但计算复杂度容易过高。因此,我们通常只对包含
的大小与宽高比的组合感兴趣,即

也就是说,以相同像素为中心的锚框的数量为

。对于整个输入图像,我们将一共生成
个锚框。

以上生成锚框的方法已实现在MultiBoxPrior函数中。指定输入、一组大小和一组宽高比,该函数将返回输入的所有锚框。

d2l.set_figsize()
img = Image.open(os.path.join(IMAGE_DIR, 'catdog.jpg'))
w, h = img.size
print("w = %d, h = %d" % (w, h))# d2l.plt.imshow(img);  # 加分号只显示图

输出:w = 728, h = 561

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):"""# 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs:feature_map: torch tensor, Shape: [N, C, H, W].sizes: List of sizes (0~1) of generated MultiBoxPriores. ratios: List of aspect ratios (non-negative) of generated MultiBoxPriores. Returns:anchors of shape (1, num_anchors, 4). 由于batch里每个都一样, 所以第一维为1"""pairs = [] # pair of (size, sqrt(ration))# 生成n + m -1个框for r in ratios:pairs.append([sizes[0], math.sqrt(r)])for s in sizes[1:]:pairs.append([s, math.sqrt(ratios[0])])pairs = np.array(pairs)# 生成相对于坐标中心点的框(x,y,x,y)ss1 = pairs[:, 0] * pairs[:, 1] # size * sqrt(ration)ss2 = pairs[:, 0] / pairs[:, 1] # size / sqrt(ration)base_anchors = np.stack([-ss1, -ss2, ss1, ss2], axis=1) / 2#将坐标点和anchor组合起来生成hw(n+m-1)个框输出h, w = feature_map.shape[-2:]shifts_x = np.arange(0, w) / wshifts_y = np.arange(0, h) / hshift_x, shift_y = np.meshgrid(shifts_x, shifts_y)shift_x = shift_x.reshape(-1)shift_y = shift_y.reshape(-1)shifts = np.stack((shift_x, shift_y, shift_x, shift_y), axis=1)anchors = shifts.reshape((-1, 1, 4)) + base_anchors.reshape((1, -1, 4))return torch.tensor(anchors, dtype=torch.float32).view(1, -1, 4)
X = torch.Tensor(1, 3, h, w)  # 构造输入数据
Y = MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
print(Y.shape)

输出:torch.Size([1, 2042040, 4])

我们看到,返回锚框变量y的形状为(1,锚框个数,4)。将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)后,我们就可以通过指定像素位置来获取所有以该像素为中心的锚框了。下面的例子里我们访问以(250,250)为中心的第一个锚框。它有4个元素,分别是锚框左上角的

轴坐标和右下角的
轴坐标,其中
轴的坐标值分别已除以图像的宽和高,因此值域均为0和1之间。
# 展示某个像素点的anchor
boxes = Y.reshape((h, w, 5, 4))
boxes[250, 250, 0, :]# * torch.tensor([w, h, w, h], dtype=torch.float32)
# 第一个size和ratio分别为0.75和1, 则宽高均为0.75 = 0.7184 + 0.0316 = 0.8206 - 0.0706

输出:tensor([-0.0316, 0.0706, 0.7184, 0.8206])

可以验证一下以上输出对不对:size和ratio分别为0.75和1, 则(归一化后的)宽高均为0.75, 所以输出是正确的(0.75 = 0.7184 + 0.0316 = 0.8206 - 0.0706)。

为了描绘图像中以某个像素为中心的所有锚框,我们先定义show_bboxes函数以便在图像上画出多个边界框。

# 本函数已保存在dd2lzh_pytorch包中方便以后使用
def show_bboxes(axes, bboxes, labels=None, colors=None):def _make_list(obj, default_values=None):if obj is None:obj = default_valueselif not isinstance(obj, (list, tuple)):obj = [obj]return objlabels = _make_list(labels)colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])for i, bbox in enumerate(bboxes):color = colors[i % len(colors)]rect = d2l.bbox_to_rect(bbox.detach().cpu().numpy(), color)axes.add_patch(rect)if labels and len(labels) > i:text_color = 'k' if color == 'w' else 'w'axes.text(rect.xy[0], rect.xy[1], labels[i],va='center', ha='center', fontsize=6, color=text_color,bbox=dict(facecolor=color, lw=0))

刚刚我们看到,变量boxes

轴的坐标值分别已除以图像的宽和高。在绘图时,我们需要恢复锚框的原始坐标值,并因此定义了变量

bbox_scale。现在,我们可以画出图像中以(250, 250)为中心的所有锚框了。可以看到,大小为0.75且宽高比为1的锚框较好地覆盖了图像中的狗。

# 展示 250 250像素点的anchor
d2l.set_figsize()
fig = d2l.plt.imshow(img)
bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,['s=0.75, r=1', 's=0.75, r=2', 's=0.75, r=0.5', 's=0.5, r=1', 's=0.25, r=1'])

交并比

我们刚刚提到某个锚框较好地覆盖了图像中的狗。如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。我们知道,Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合

,它们的Jaccard系数即二者交集大小除以二者并集大小:

实际上,我们可以把边界框内的像素区域看成是像素的集合。如此一来,我们可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度。当衡量两个边界框的相似度时,我们通常将Jaccard系数称为交并比(Intersection over Union,IoU),即两个边界框相交面积与相并面积之比,如图9.2所示。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。

# 以下函数已保存在d2lzh_pytorch包中方便以后使用
def compute_intersection(set_1, set_2):"""计算anchor之间的交集Args:set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)Returns:intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)"""# PyTorch auto-broadcasts singleton dimensionslower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0))  # (n1, n2, 2)upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0))  # (n1, n2, 2)intersection_dims = torch.clamp(upper_bounds - lower_bounds, min=0)  # (n1, n2, 2)return intersection_dims[:, :, 0] * intersection_dims[:, :, 1]  # (n1, n2)def compute_jaccard(set_1, set_2):"""计算anchor之间的Jaccard系数(IoU)Args:set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)Returns:Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)"""# Find intersectionsintersection = compute_intersection(set_1, set_2)  # (n1, n2)# Find areas of each box in both setsareas_set_1 = (set_1[:, 2] - set_1[:, 0]) * (set_1[:, 3] - set_1[:, 1])  # (n1)areas_set_2 = (set_2[:, 2] - set_2[:, 0]) * (set_2[:, 3] - set_2[:, 1])  # (n2)# Find the union# PyTorch auto-broadcasts singleton dimensionsunion = areas_set_1.unsqueeze(1) + areas_set_2.unsqueeze(0) - intersection  # (n1, n2)return intersection / union  # (n1, n2)

标注训练集的锚框

在训练集中,我们将每个锚框视为一个训练样本。为了训练目标检测模型,我们需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。在目标检测时,我们首先生成多个锚框,然后为每个锚框预测类别以及偏移量,接着根据预测的偏移量调整锚框位置从而得到预测边界框,最后筛选需要输出的预测边界框。

我们知道,在目标检测的训练集中,每个图像已标注了真实边界框的位置以及所含目标的类别。在生成锚框之后,我们主要依据与锚框相似的真实边界框的位置和类别信息为锚框标注。那么,该如何为锚框分配与其相似的真实边界框呢?

假设图像中锚框分别为

,真实边界框分别为
,且
。定义矩阵
,其中第
行第
列的元素
为锚框
与真实边界框
的交并比。 首先,我们找出矩阵
中最大元素,并将该元素的行索引与列索引分别记为
。我们为锚框
分配真实边界框
。显然,锚框
和真实边界框
在所有的“锚框—真实边界框”的配对中相似度最高。接下来,将矩阵
中第
行和第
列上的所有元素丢弃。找出矩阵
中剩余的最大元素,并将该元素的行索引与列索引分别记为
。我们为锚框
分配真实边界框
,再将矩阵
中第
行和第
列上的所有元素丢弃。此时矩阵
中已有两行两列的元素被丢弃。 依此类推,直到矩阵
中所有
列元素全部被丢弃。这个时候,我们已为
个锚框各分配了一个真实边界框。 接下来,我们只遍历剩余的
个锚框:给定其中的锚框
,根据矩阵
的第
行找到与
交并比最大的真实边界框
,且只有当该交并比大于预先设定的阈值时,才为锚框
分配真实边界框

如图9.3(左)所示,假设矩阵

中最大值为
,我们将为锚框
分配真实边界框
。然后,丢弃矩阵中第2行和第3列的所有元素,找出剩余阴影部分的最大元素
,为锚框
分配真实边界框
。接着如图9.3(中)所示,丢弃矩阵中第7行和第1列的所有元素,找出剩余阴影部分的最大元素
,为锚框
分配真实边界框
。最后如图9.3(右)所示,丢弃矩阵中第5行和第4列的所有元素,找出剩余阴影部分的最大元素
,为锚框
分配真实边界框
。之后,我们只需遍历除去
的剩余锚框,并根据阈值判断是否为剩余锚框分配真实边界框。

现在我们可以标注锚框的类别和偏移量了。如果一个锚框

被分配了真实边界框
,将锚框
的类别设为
的类别,并根据
的中心坐标的相对位置以及两个框的相对大小为锚框
标注偏移量。由于数据集中各个框的位置和大小各异,因此这些相对位置和相对大小通常需要一些特殊变换,才能使偏移量的分布更均匀从而更容易拟合。设锚框
及其被分配的真实边界框
的中心坐标分别为
的宽分别为
,高分别为
,一个常用的技巧是将
的偏移量标注为

其中常数的默认值为

。如果一个锚框没有被分配真实边界框,我们只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框,其余则被称为正类锚框。

下面演示一个具体的例子。我们为读取的图像中的猫和狗定义真实边界框,其中第一个元素为类别(0为狗,1为猫),剩余4个元素分别为左上角的

轴坐标以及右下角的
轴坐标(值域在0到1之间)。这里通过左上角和右下角的坐标构造了5个需要标注的锚框,分别记为
(程序中索引从0开始)。先画出这些锚框与真实边界框在图像中的位置。
bbox_scale = torch.tensor((w, h, w, h), dtype=torch.float32)
ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],[1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],[0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],[0.57, 0.3, 0.92, 0.9]])fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);

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.htmlArgs: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] = jjaccard_cp[i, :] = float("-inf") # 赋值为负无穷, 相当于去掉这一行# 处理还未被分配的anchor, 要求满足jaccard_thresholdfor i in range(na):if assigned_idx[i] == -1:j = np.argmax(jaccard[i, :])if jaccard[i, j] >= jaccard_threshold:assigned_idx[i] = jreturn 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.pyArgs: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_yxy[:, 2:] - xy[:, :2]], 1)  # w, hdef MultiBoxTarget(anchor, label):"""# 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs: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, 正类锚框的掩码均为1cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)"""assert len(anchor.shape) == 3 and len(label.shape) == 3bn = label.shape[0]def MultiBoxTarget_one(anc, lab, eps=1e-6):"""MultiBoxTarget函数的辅助函数, 处理batch中的一个Args:anc: shape of (锚框总数, 4)lab: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值]eps: 一个极小值, 防止log0Returns: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)。

我们来描述一下非极大值抑制的工作原理。对于一个预测边界框

,模型会计算各个类别的预测概率。设其中最大的预测概率为
,该概率所对应的类别即
的预测类别。我们也将
称为预测边界框
的置信度。在同一图像上,我们将预测类别非背景的预测边界框按置信度从高到低排序,得到列表
。从
中选取置信度最高的预测边界框
作为基准,将所有与
的交并比大于某阈值的非基准预测边界框从
中移除。这里的阈值是预先设定的超参数。此时,
保留了置信度最高的预测边界框并移除了与其相似的其他预测边界框。 接下来,从
中选取置信度第二高的预测边界框
作为基准,将所有与
的交并比大于某阈值的非基准预测边界框从
中移除。重复这一过程,直到
中所有的预测边界框都曾作为基准。此时
中任意一对预测边界框的交并比都小于阈值。最终,输出列表
中的所有预测边界框。

下面来看一个具体的例子。先构造4个锚框。简单起见,我们假设预测偏移量全是0:预测边界框即锚框。最后,我们构造每个类别的预测概率。

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:breakbb_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 outputdef 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.htmlArgs: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) == 3bn = 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)]# 正类的indexobj_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:continuelabel = ('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多万个锚框(

)。

减少锚框个数并不难。一种简单的方法是在输入图像中均匀采样一小部分像素,并以采样的像素为中心生成锚框。此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。值得注意的是,较小目标比较大目标在图像上出现位置的可能性更多。举个简单的例子:形状为

的目标在形状为
的图像上可能出现的位置分别有4、2和1种。因此,当使用较小锚框来检测较小目标时,我们可以采样较多的区域;而当使用较大锚框来检测较大目标时,我们可以采样较少的区域。

为了演示如何多尺度生成锚框,我们先读取一张图像。它的高和宽分别为561像素和728像素。

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_hanchors = 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])

mpandroidchart y轴从0开始_从零开始学Pytorch(十七)之目标检测基础相关推荐

  1. 梯度消失和梯度爆炸_从零开始学Pytorch之梯度消失、梯度爆炸

    本文首发于微信公众号"计算机视觉cv" 深度模型有关数值稳定性的典型问题是消失(vanishing)和爆炸(explosion). 当神经网络的层数较多时,模型的数值稳定性容易变差 ...

  2. ECharts双y轴设置0刻度对齐,左右刻度根据数据变化?

    一.ECharts双y轴设置0刻度对齐,左右刻度值根据数据变化? 双y轴因为左右的数值有正数有负数的原因,出现0刻度不在一条线上的情况,解决办法如下. 代码如下: //获取数据中的最大值 functi ...

  3. 从零开始学Pytorch(第5天)

    从零开始学Pytorch(第5天) 前言 一.模块类的构建 1. nn.Module 2.构建一个线性回归类 二.计算图和自动求导机制 1.计算图 2.自动求导 总结 前言 今天主要了解和学习Pyto ...

  4. 从零开始学Pytorch(零)之安装Pytorch

    本文首发于公众号"计算机视觉cv" Pytorch优势   聊聊为什么使用Pytorch,个人觉得Pytorch比Tensorflow对新手更为友善,而且现在Pytorch在学术界 ...

  5. MpAndroidChart Y轴显示整数

    最近写项目,用到了图表控件,也就是MpAndroidChart控件 遇到了一个神奇的问题,也就是MP控件要求Y轴数据必须是float的,那我们就需要自己去转换 转换完毕后,发现显示出来的都是带有.00 ...

  6. java 实现雷达图,如何使用y轴为0到100的chart.js创建雷达图?

    我目前正在使用Chart.js将我的数据显示为雷达图 . 我面临的问题是,每次我向页面添加数据时,它都会将最大值设置为表中最高值 . 我需要它从0到100的Y轴开始 . 我正在使用Chart.js v ...

  7. 跟我从零开始学python之一_从零开始学 Python 之运算符

    从零开始学 Python 之运算符 前言 大家好,这里是「痴海」从零开始学习 Python 系列教程.此文首发于「痴海」公众号,欢迎大家去关注.学习一门语言最好的办法,就是教懂别人.在这公众号,我会从 ...

  8. python箱线图_从零开始学Python-matplotlib系列(III):箱线图

    原标题:从零开始学Python-matplotlib系列(III):箱线图 前言 今天继续我们的python绘图系列,针对离散变量我们可以使用常见的条形图和饼图完成数据的可视化工作,那么,针对数值型变 ...

  9. r语言中矩阵QR分解_从零开始学R语言Day4|向量、矩阵和数组

    从零开始学R语言Day4|向量.矩阵和数组 1.1向量 1.1.1向量 在Day2中我们提及过用和c()函数来构建向量,具体实例如下. 我们还可以采用vector("类型",长度) ...

最新文章

  1. Unity3d载入外部图片文件
  2. Linux环境下安装nginx
  3. Oracle weblogic线程Thread status分类和总结
  4. 各个系统动态多路径软件识别LUN的方法
  5. 决策树 随机森林 xgboost_决策树算法之随机森林
  6. 20181009-9 每周例行报告
  7. Linux(Ubuntu)下嵌入式gdb调试环境
  8. boost::math::pow相关用法的测试程序
  9. CTF EasyStrcmp
  10. 容器的访问元素的成员函数(front,back,下标和at)返回的都是引用,如果顺序容器是const的对象,那么返回的是const的引用
  11. HDU - 6118 度度熊的交易计划(最大费用可行流)
  12. 列Readonly的问题
  13. Android系统下内存使用情况与监测
  14. Xml中SelectSingleNode方法中的xpath用法
  15. ASP.NET使用Coolite.Ext.Web.dll,显示ext未定义的解决方法
  16. 基于JAVA+SpringBoot+Mybatis+MYSQL的疫情信息分析管理系统
  17. 日本新年传统习俗介绍(二)
  18. 各式各样的极品程序员 你属于哪一种
  19. Ubuntu 携手初创企业用代码开拓物联网
  20. 阿里云峰会上海见,云原生场景实战即将开启

热门文章

  1. 南阳师范学院ACM集训队博客使用方法
  2. iOS学习系列 - 扩展机制category与associative
  3. MFC创建模态对话框和非模态对话框的方法
  4. dataTable的用法
  5. C++回调机制的几种实现方式
  6. Android—ActivityThread与Handler
  7. IOS UI开发基础之超级猜图完整版本-08
  8. python中return和and连用
  9. Notice: Undefined variable: id in D:\phpStudy18\PHPTutorial\WWW\sqli-labs-master\Less-32\index.php o
  10. 树莓派okdo_创客的AK47—树莓派4正式发布,性能大幅提升,售价仅为35美元。