目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边界从而更准确地预测目标的真实边界框(ground-truth bounding box)。 不同的模型使用的区域采样方法可能不同。 这里我们介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)。
使用锚框进行目标检测的大致思路为:先对图像的每个像素点生成sizes+ratios-1个锚框,再计算这些锚框与真实边界框的交并比,从而将真实的边界框分配给最接近的锚框,分配完成后,利用真实边界框对这些锚框进行标注,标签为偏移量,是否分配的布尔掩码以及类别,再根据标签信息生成预测的边界框,此时运用非极大值抑制预测,筛选出不重叠且置信度尽可能大的预测边界框,从而进行目标的锚定。
一.生成多个锚框
假设输入图像的高度为ℎ,宽度为w。 我们以图像的每个像素为中心生成不同形状的锚框:缩放比为s∈(0,1],宽高比为r>0。 那么锚框的宽度和高度分别是和
。 请注意,当中心位置给定时,已知宽和高的锚框是确定的。
要生成多个不同形状的锚框,让我们设置许多缩放比(scale)取值,…,
和许多宽高比(aspect ratio)取值
,…,
。 当使用这些比例和长宽比的所有组合以每个像素为中心时,输入图像将总共有
个锚框。 尽管这些锚框可能会覆盖所有真实边界框,但计算复杂性很容易过高。 在实践中,我们只考虑包含
或
的组合:
(),(
),…,(
),(
),(
),…,(
).
也就是说,以同一像素为中心的锚框的数量是。 对于整个输入图像,将共生成
个锚框。
上述生成锚框的方法在下面的multibox_prior
函数中实现。 我们指定输入图像、尺寸列表和宽高比列表,然后此函数将返回所有的锚框。
主要思路为,先计算出所有像素中心点的坐标,再计算出锚框的宽和高,根据这两个元素可以计算出锚框左上角和右下角顶点的坐标。
需要注意的是,该生成函数中的锚框默认为宽1高1,且进行了缩放,所以在显示锚框的时候,还需乘上图像真实的宽和高。
%matplotlib inline
import torch
import d2l
torch.set_printoptions(2) #精简输出精度
#上述生成锚框的方法在下面的multibox_prior函数中实现。 我们指定输入图像、尺寸列表和宽高比列表,然后此函数将返回所有的锚框
#这里生成锚框的所有数据是缩放过的,缩放因子为1.0/height以及1.0/width
def multibox_prior(data, sizes, ratios):
"""以每个像素为中心生成具有不同形状的锚框"""
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
boxes_per_pixel = (num_sizes + num_ratios - 1) #计算每个像素点生成的锚框数量
size_tensor = torch.tensor(sizes, device=device)
ratio_tensor = torch.tensor(ratios, device=device)
#为了将锚框的中心点移到像素的中心,需要设置偏移量
#因为1像素的高为1且宽为1,我们选择偏移量中心为0.5
offset_h, offset_w = 0.5, 0.5
#每个像素的宽和高都是1,这里是计算缩放比
steps_h = 1.0 / in_height #在y轴上缩放步长
steps_w = 1.0 / in_width #在x轴上缩放步长
#生成锚框的所有中心点
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h #生成所有中心点的y坐标
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w #生成所有中心点的x坐标
#meshgrid函数将center_h,center_w生成一个二维网格并赋值给他们,形状均为[in_height,in_width]
#因为共有in_height*in_width个中心点,而shift_y和shift_x也需要表示这么多个点的y坐标和x坐标
#然而center_h和center_w中并没有这么多个点,所以需要先将其变成二维网格,然后再重塑为一维张量
shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
#特别地,这里要注意,我们现在默认是对宽为1高为1的正方形锚框在进行缩放和调整宽高比的
#因此,最后我们需要恢复它对原始的坐标值,需要乘上图片真实的宽和高
#生成boxes_per_pixel个高和宽
#之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
#size_tensor和ratio_tensor是相对于正方形锚框的比例,所以将宽度进行相对于输入宽高比做对应的调整,这样才能处理矩形输入
#将宽度乘以in_height/in_width之后,就能够将矩形输入转换为正方形锚框,此时便可利用size_tensor和ratio_tensor了
w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), #仅考虑包含s1和r1的组合,即s*根号r,共boxes_per_pixel个高和宽
sizes[0] * torch.sqrt(ratio_tensor[1:]))) * in_height / in_width #处理矩形输入
h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
sizes[0] / torch.sqrt(ratio_tensor[1:])))
#除以2来获得半高和半宽
#用来获得相对于像素中心的偏移量,所以要除以2
#首先,通过使用 torch.stack 函数将 -w、-h、w、h 这四个张量按列(即按维度0)堆叠起来,形成一个[4,boxes_per_pixel]的张量
#然后进行转置,使得每行为每个像素点的xmin,ymin,xmax,ymax,(即对中心点的半宽和半高)他们用于计算每个像素点的左上角和右下角坐标
#然后将其重复到in_height*in_width个像素点,最终形状为[in_height*in_width*boxes_per_pixel,4],即每个锚框的对角两个顶点相对于中心点的参数
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
#每个中心点都将有boxes_per_pixel个锚框
#所以生成含所有锚框中心点的网格重复了boxes_per_pixel次
#torch.stack将每一列连接,使得每行为每个像素点的中心坐标,重复两次,因为要计算左上角和右下角坐标此时形状为[in_height*in_width,4]
#每个点对应的锚框个数为boxees_per_pixel,所以重复这么多次,最终形状为[in_height*in_width*boxes_per_pixel,4]
#out_grid即每个像素点对应锚框的中心点坐标
#由上面分析知anchor_manipulations为像素点的锚框对角两个顶点对应于中心点的参数
#相加即得到锚框对角两个顶点的坐标
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1).repeat_interleave(boxes_per_pixel, dim=0)
output = out_grid + anchor_manipulations
#添加维度0,以适应不同批次大小的输入
#此时的输出形状为[批次大小,锚框数量,4],4是因为要存储左上角和右下角的坐标
return output.unsqueeze(0)
img = d2l.plt.imread('./img/catdog.jpg')
h, w = img.shape[:2]
print(h, w)
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
#将锚框变量Y的形状更改为(图像高度,图像宽度,以同一像素为中心的锚框的数量,4)后,我们可以获得以指定像素的位置为中心的所有锚框
#在接下来的内容中,我们访问以(250,250)为中心的第一个锚框。 它有四个元素:锚框左上角的(x,y)轴坐标和右下角的(x,y)轴坐标
#输出中两个轴的坐标各分别除以了图像的宽度和高度
boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]
为了显示图像中以某个像素为中心的所有锚框,定义下面的show_bboxes函数,以在图像中绘制多个边界框。绘制锚框时,需要乘上真实的宽高以恢复它们原始的坐标值。
def show_bboxes(axes, bboxes, labels=None, colors=None):
"""显示所有边界框"""
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj
labels = _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().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=9, color=text_color,
bbox=dict(facecolor=color, lw=0))
#调用该函数部分
d2l.set_figsize()
#函数中boxes中x轴和y轴坐标已分别除以图像的宽度和高度,所以绘制锚框时,我们需要恢复他们原始的坐标值,因此定义bbox_scale
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
#显示第250*250个像素点的所有锚框(这里是五个)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2', #锚框的标签
's=0.75, r=0.5'])
二.交并比
我们刚刚提到某个锚框“较好地”覆盖了图像中的狗。 如果已知目标的真实边界框,那么这里的“好”该如何如何量化呢? 直观地说,可以衡量锚框和真实边界框之间的相似性。 杰卡德系数(Jaccard)可以衡量两组之间的相似性。 给定集合和
,他们的杰卡德系数是他们交集的大小除以他们并集的大小
下面box_iou函数将对于锚框和真实边界框计算交并比
#接下来部分将使用交并比来衡量锚框和真实边界框之间、以及不同锚框之间的相似度
#给定两个锚框或边界框的列表,以下box_iou函数将在这两个列表中计算它们成对的交并比
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
#已知左上角和右下角的坐标,可以计算锚框的面积
box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
(boxes[:, 3] - boxes[:, 1]))
# boxes1、boxes2、areas1和areas2的形状分别为
#(boxes1的数量,4)
#(boxes2的数量,4)
#(boxes1的数量,)
#(boxes2的数量,)
areas1 = box_area(boxes1)
areas2 = box_area(boxes2)
# inter_upperlefts、inter_lowerrights、inters的形状为
# (boxes1的数量,boxes2的数量,2)
#存储两组矩形框交集的左上角顶点,通过None在维度上进行扩展,从而利用广播机制,以便于boxes2进行比较
#因为两个box轴1的长度可能是不同的
inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
#存储两组矩形框交集的右下角顶点
inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
#计算交集区域的宽和高
inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
# inter_areasandunion_areas的形状为(boxes1的数量,boxes2的数量)
#计算交集区域面积
inter_areas = inters[:, :, 0] * inters[:, :, 1]
#计算并集区域面积
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas
三.将真实边界框分配给锚框
在训练集中,我们将每个锚框视为一个训练样本。 为了训练目标检测模型,我们需要每个锚框的类别(class)和偏移量(offset)标签,其中前者是与锚框相关的对象的类别,后者是真实边界框相对于锚框的偏移量。 在预测时,我们为每个图像生成多个锚框,预测所有锚框的类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。
算法思想:
给定图像,假设锚框是,真实边界框是
,其中
。 让我们定义一个矩阵
∈
,其中第
行、第
列的元素
是锚框
和真实边界框
的IoU。 该算法包含以下步骤。
-
在矩阵
中找到最大的元素,并将它的行索引和列索引分别表示为
和
。然后将真实边界框
分配给锚框
。这很直观,因为
和
是所有锚框和真实边界框配对中最相近的。在第一个分配完成后,丢弃矩阵中
行和
列中的所有元素。
-
在矩阵
中找到剩余元素中最大的元素,并将它的行索引和列索引分别表示为
和
。我们将真实边界框
分配给锚框
,并丢弃矩阵中
行和
列中的所有元素。
-
此时,矩阵
中两行和两列中的元素已被丢弃。我们继续,直到丢弃掉矩阵
中
列中的所有元素。此时已经为这
个锚框各自分配了一个真实边界框。
-
只遍历剩下的
−
个锚框。例如,给定任何锚框
,在矩阵的
第
行中找到与
的IoU最大的真实边界框
,只有当此IoU大于预定义的阈值时,才将
分配给
。
算法解释:行索引代表锚框索引,列索引代表真实边界框索引,找到交并比最大的元素,将该真实边界框分配给该锚框,删去行列,即代表该锚框已分配真实边界框,该锚框和对应的真实边界框不再参与分配,直到分配完个锚框后,对于剩下的锚框,根据阈值确定是否为其分配真实的边界框
其中anchors_bbox_map为锚框与对应分配的真实边界框的映射,值为-1表示该锚框并没有分配真实的边界框,他是一个形状为(锚框数量,)的一维张量
这里需要注意的是,torch.argmax函数会将二维张量扁平化为一维张量之后返回索引,所以根据返回的索引(记为max_idx),max_idx/列即为行索引,即锚框索引,max_idx%列即为列索引,即对应真实边界框的索引,anc_i表示满足条件的锚框索引,box_j为满足条件的真实边界框索引
#@save
#将真实边界框分配给锚框的算法实现
#选取最优的那一批锚框,将真实的边界框分配给这些锚框,然后进行标注,根据标注后的锚框来生成预测的边界框
#选取一行中最大的交并比,意为选取与该锚框最匹配的真实边界框分配给该锚框,随后删除行代表该锚框已被分配,删除列代表该真实边界框已分配,均不参与后续分配了
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
"""将最接近的真实边界框分配给锚框"""
num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
# 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU
#jaccard是一个大小为[num_anchors, num_gt_boxes]的矩阵
jaccard = box_iou(anchors, ground_truth)
# 对于每个锚框,分配的真实边界框的张量
#初始值均分配为-1,表示未分配真实边界框,长度为num_anchors,即锚框数量
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
device=device)
# 根据阈值,决定是否分配真实边界框
#找出每行中最大的IoU值存储到max_ious中,并将对应的列索引(真实边界框索引)存入indices中
#此时max_ious,indices具有相同的形状[num_anchors,]
max_ious, indices = torch.max(jaccard, dim=1)
#nonzero方法查找所有非零元素的索引
#这里是找出所有大于等于阈值的max_ious,然后通过nonzero找到其索引,并存入anc_j中
anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
#max_ious >= iou_threshold 创建了一个布尔张量,表示相应锚框的最大IoU是否大于或等于指定的IoU阈值
#从indices中筛选出满足阈值条件的值,比如[1,2,3,4,5],box[box>=3]的值是[3,4,5]
#所以这里box_j是找出indices中对应max_ious>=iou_threshold的值,也就是满足条件锚框对应的列索引(即真实边界框索引)
box_j = indices[max_ious >= iou_threshold]
#anc_i是筛选出满足条件的锚框对应的行索引(即锚框索引)
#所以anchors_bbox_map是一个行列映射,即找出该锚框对应的行列索引
anchors_bbox_map[anc_i] = box_j
#丢弃列赋值
col_discard = torch.full((num_anchors,), -1)
#丢弃行赋值
row_discard = torch.full((num_gt_boxes,), -1)
for _ in range(num_gt_boxes):
#注意了,虽然jaccard是一个二维数组,但是argmax方法会将jaccard扁平化为一维数组然后进行索引值返回,所以max_idx就是一个数,而不是二维坐标
max_idx = torch.argmax(jaccard)
#得到列,即Bj
box_idx = (max_idx % num_gt_boxes).long()
#得到行,即Ai
anc_idx = (max_idx / num_gt_boxes).long()
#该行中最大锚框对应的真实边界框的列索引
anchors_bbox_map[anc_idx] = box_idx
#列被丢弃
jaccard[:, box_idx] = col_discard
#行被丢弃
jaccard[anc_idx, :] = row_discard
#返回最终的锚框-真实边界框分配,此时没被分配的不符合的锚框对应的列索引为-1
return anchors_bbox_map
四.用真实边界框标注锚框
用真实边界框标注锚框,标签信息为偏移量,是否分配的布尔掩码以及标注类别。
假设一个锚框被分配了一个真实边界框
。 一方面,锚框
的类别将被标记为与
相同。 另一方面,锚框
的偏移量将根据
和
中心坐标的相对位置以及这两个框的相对大小进行标记。 鉴于数据集内不同的框的位置和大小不同,我们可以对那些相对位置和大小应用变换,使其获得分布更均匀且易于拟合的偏移量。 这里介绍一种常见的变换。 给定框
和
,中心坐标分别为
和
,宽度分别为
和
,高度分别为
和
,可以将
的偏移量标记为:
其中,常量的默认值为。这种转换在下面的offset_boxes函数中实现。
#对锚框偏移量的转换
def offset_boxes(anchors, assigned_bb, eps=1e-6):
"""对锚框偏移量的转换"""
#锚框表示法转化成中心点模式
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
#这里的10,5以及直接除没有减是因为那些常量使用了默认值
offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
#通过轴1进行拼接
offset = torch.cat([offset_xy, offset_wh], axis=1)
return offset
如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为背景(background)。 背景类别的锚框通常被称为负类锚框,其余的被称为正类锚框。 我们使用真实边界框(labels
参数)实现以下multibox_target
函数,来标记锚框的类别和偏移量(anchors
参数)。 此函数将背景类别的索引设置为零,然后将新类别的整数索引递增一。
这里利用了布尔掩码,其主要目的是过滤掉负类偏移量,即被标注为背景的偏移量为0。且考虑背景的情况下,0表示背景,1表示狗,2表示猫
其中,label为真实边界框的信息,是一个(批次,5)的二维张量,最后得到的标签均为锚框-真实边界框的映射
def multibox_target(anchors, labels):
"""使用真实边界框标注锚框"""
#标注完成后作为标签,标签包含了三项,即偏移量,是否分配的布尔掩码,类别
#batch_size为批次数量
#squeeze为挤压函数,挤压指定的维度,如果该维度数为1,则删除该维度,否则保持不变
batch_size, anchors = labels.shape[0], anchors.squeeze(0)
batch_offset, batch_mask, batch_class_labels = [], [], []
#挤压后,anchors.shape[0]即代表锚框数量
device, num_anchors = anchors.device, anchors.shape[0]
#多批次的
for i in range(batch_size):
label = labels[i, :, :]
#先将真实边界框分配给锚框,再进行标注(即计算偏移量,类别以及是否分配)
#索引0代表真实边界框的类别,因此不需要输入到分配锚框参数中
anchors_bbox_map = assign_anchor_to_bbox(
label[:, 1:], anchors, device)
#锚框是否被分配的布尔掩码,这里repeat是为了后面做乘积形状的需要,因为背景是负类偏移量,所以需要过滤掉,掩码中的0可以做到这点
bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(1, 4)
#将类别标签和分配的边界框坐标初始化为0
class_labels = torch.zeros(num_anchors, dtype=torch.long,
device=device)
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
device=device)
#使用真实边界框来标注锚框的类别
#如果一个锚框没有被分配,标注其为背景类别(值为0)
#找到分配的锚框的行索引
indices_true = torch.nonzero(anchors_bbox_map >= 0)
#找到锚框对应的真实边界框的索引
bb_idx = anchors_bbox_map[indices_true]
#标签,最终0代表背景,1代表狗,2代表猫,所以这里需要加1
class_labels[indices_true] = label[bb_idx, 0].long() + 1
#锚框对应被分配的真实边界框
assigned_bb[indices_true] = label[bb_idx, 1:]
#偏移量转换,利用bbox_mask过滤负类偏移量
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
batch_offset.append(offset.reshape(-1))
batch_mask.append(bbox_mask.reshape(-1))
batch_class_labels.append(class_labels)
#偏移量表
bbox_offset = torch.stack(batch_offset)
#布尔掩码表
bbox_mask = torch.stack(batch_mask)
#标签表
class_labels = torch.stack(batch_class_labels)
return (bbox_offset, bbox_mask, class_labels)
下面通过一个具体的例子来说明锚框标签。 我们已经为加载图像中的狗和猫定义了真实边界框,其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的轴坐标(范围介于0和1之间)。 我们还构建了五个锚框,用左上角和右下角的坐标进行标记:
(索引从0开始)。 然后我们在图像中绘制这些真实边界框和锚框。
#下面通过一个具体的例子来说明锚框标签。 我们已经为加载图像中的狗和猫定义了真实边界框
#其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的(x,y)轴坐标(范围介于0和1之间)
#我们还构建了五个锚框,用左上角和右下角的坐标进行标记:(索引从0开始)。 然后我们在图像中绘制这些真实边界框和锚框
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']);
五.使用非极大值抑制预测边界框
在预测时,我们先为图像生成多个锚框,再为这些锚框一一预测类别和偏移量。 一个预测好的边界框则根据其中某个带有预测偏移量的锚框而生成。 下面我们实现了offset_inverse
函数,该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标。
#在预测时,我们先为图像生成多个锚框,再为这些锚框一一预测类别和偏移量。 一个预测好的边界框则根据其中某个带有预测偏移量的锚框而生成
#下面我们实现了offset_inverse函数,该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标
def offset_inverse(anchors, offset_preds):
"""根据带有预测偏移量的锚框来预测边界框"""
#利用标注好的锚框,重新生成预测的边界框
anc = d2l.box_corner_to_center(anchors)
#预测偏移量的逆变换(逆偏移变换)
pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
predicted_bbox = d2l.box_center_to_corner(pred_bbox)
#最终得到预测的边界框
return predicted_bbox
当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框,都围绕着同一目标。 为了简化输出,我们可以使用非极大值抑制(non-maximum suppression,NMS)合并属于同一目标的类似的预测边界框。
以下是非极大值抑制的工作原理。 对于一个预测边界框,目标检测模型会计算每个类别的预测概率。 假设最大的预测概率为
,则该概率所对应的类别
即为预测的类别。 具体来说,我们将
称为预测边界框
的置信度(confidence)。 在同一张图像中,所有预测的非背景边界框都按置信度降序排序,以生成列表
。然后我们通过以下步骤操作排序列表
。
-
从
中选取置信度最高的预测边界框
作为基准,然后将所有与
的IoU超过预定阈值
的非基准预测边界框从
中移除。这时,
保留了置信度最高的预测边界框,去除了与其太过相似的其他预测边界框。简而言之,那些具有非极大值置信度的边界框被抑制了。
-
从
中选取置信度第二高的预测边界框
作为又一个基准,然后将所有与�2的IoU大于
的非基准预测边界框从
中移除。
-
重复上述过程,直到
中的所有预测边界框都曾被用作基准。此时,
中任意一对预测边界框的IoU都小于阈值
;因此,没有一对边界框过于相似。
-
输出列表
中的所有预测边界框。
以下nms
函数按降序对置信度进行排序并返回其索引。
#得到最终的预测边界框后,当锚框数目很多时,可能会得到多个具有明显重叠的预测边界框,此时利用非极大值抑制预测边界框
#主要思路为,找出置信度最大的那一批,将与之相似的删除即可
#以下nms函数按降序对置信度进行排序并返回其索引
def nms(boxes, scores, iou_threshold):
"""对预测边界框的置信度进行排序"""
#输入张量scores是每个边界框对于不同类别的最大置信度,长度为(边界框数,)的张量
#对输入张量scores在最后一个维度上进行降序排序
#这里得到的B,就是最大置信度的排序,元素为对应的边界框索引
B = torch.argsort(scores, dim=-1, descending=True)
keep = [] #保留预测边界框的指标
while B.numel() > 0:
i = B[0]
keep.append(i)
if B.numel() == 1: break
#计算当前边界框与其他边界框的iou值
iou = box_iou(boxes[i, :].reshape(-1,4),
boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
#找出iou值较小的边界框的索引
#索引中的元素是一个列表,列表中的元素代表着符合要求元素的坐标
#如scores = torch.tensor([[0] * 4,
#[0.9, 0.8, 0.7, 0.1],
#[0.1, 0.2, 0.3, 0.9]])
#inds = torch.nonzero(scores<=0.5)
#此时打印inds为:
#tensor([[0, 0],
#[0, 1],
#[0, 2],
#[0, 3],
#[1, 3],
#[2, 0],
#[2, 1],
#[2, 2]])
#找出满足条件的边界框的索引,而这里由于box_iou是从索引为1的边界框的数据(跳过了它本身)
#所以box_iou索引0其实对应真实边界框的索引为1,要加1才能对应到真实的边界框
#所以inds+1才为真实需要选取的边界框,这也是为什么后面B=B[inds+1]的原因
inds = torch.nonzero(iou <= iou_threshold).reshape(-1)
#让B中保留所有满足条件的边界框
B = B[inds + 1]
return torch.tensor(keep, device=boxes.device)
这里比较难理解的是B=B[inds + 1]这一段,首先输入张量scores是每个边界框对于不同类别的最大置信度,是一个形状为(边界框数,)的一维张量,随后对输入张量scores进行排序,得到B,就是最大置信度的排序,B中的元素为边界框的索引,此时取出B[0],也就是置信度最大的边界框,计算该边界框与其他边界框的iou,此时需要注意的是,iou的计算过程中跳过了B[0]本身,也就是说,iou中的边界框索引与真实边界框索引之间是加1的关系,也就是iou得到的索引需要加1才能得到真实边界框的索引,因此,在找出符合条件的索引inds后,需要B=B[inds + 1],才能让B中保留所有满足条件的边界框。
我们定义以下multibox_detection
函数来将非极大值抑制应用于预测边界框。主要目的是找出保留的边界框和非保留的边界框 。
#将非极大值抑制应用于预测边界框
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
pos_threshold=0.009999999):
#cls_probs的形状为(批次,类别数,锚框数)
device, batch_size = cls_probs.device, cls_probs.shape[0] #批次
#对anchors进行挤压操作
anchors = anchors.squeeze(0)
#cls_probs.shape[1]对应类别数,shape[2]对应锚框数
num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
out = []
for i in range(batch_size):
cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
#找出最大预测概率conf并返回该概率的行索引class_id(即狗还是猫)
conf, class_id = torch.max(cls_prob[1:], 0)
#生成预测的边界框
predicted_bb = offset_inverse(anchors, offset_pred)
#使用非极大值抑制,keep是保留的边界框的索引
keep = nms(predicted_bb, conf, nms_threshold)
#找到所有的non_keep索引,并将类别设置为背景
#arange生成0到num_anchors-1的数,此时all_idx就是所有边界框的索引
all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
#连接,得到一个保留的边界框和所有边界框连接的集合
combined = torch.cat((keep, all_idx))
#使用unique找出combined中的唯一元素以及他们出现的次数
#uniques中存储的是出现的边界框的索引,counts为边界框出现的次数
uniques, counts = combined.unique(return_counts=True)
#其中出现次数为1的索引被认为是非保留的边界框
#non_keep为非保留的边界框的索引
non_keep = uniques[counts == 1]
#重新排序
#此时序列就为(保留的边界框,非保留的边界框)
all_id_sorted = torch.cat((keep, non_keep))
#将非保留锚框设置为背景类别-1
#此时class_id还未排序成(保留,非保留),所以可以直接使用non_keep将非保留类别设置为-1
class_id[non_keep] = -1
#类别设置好后重新排列类别标签
class_id = class_id[all_id_sorted]
#同样地,概率和预测锚框也要重新排列
conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
# pos_threshold是一个用于非背景预测的阈值
#below_min_idx,生成了一个布尔掩码,值为True表示概率低于阈值
below_min_idx = (conf < pos_threshold)
#将置信度过小的也设置为背景类别
#找出小于阈值的量将其赋值为背景类别-1
#事实上,经过我的实践布尔掩码确实可以做到这一点,将值为True的地方赋值为-1
class_id[below_min_idx] = -1
#对于低置信度的预测,将其置信度 conf 更新为 1 减去原来的置信度
#这一步的目的是增加这些低置信度预测的置信度值,以便后续处理时它们不容易被筛除或忽略
conf[below_min_idx] = 1 - conf[below_min_idx]
#按照维度1进行连接,连接后维度1的个数为6,即类别,置信度,以及bbox的四个
pred_info = torch.cat((class_id.unsqueeze(1),
conf.unsqueeze(1),
predicted_bb), dim=1)
out.append(pred_info)
#最终out的形状为(批次大小,预测目标数(即预测锚框的数量),6)
return torch.stack(out)
主要操作思路为将分配的边界框与所有边界框(利用arange方法生成一维张量)进行连接,统计边界框出现的次数,出现次数为1的边界框是非保留的,运用unique方法找出非保留边界框的索引记为non_keep,再将所有序列排列成(保留,非保留)的形式,最后将输出进行拼接,维度1上有六个信息,即类别,置信度,bbox的左上角顶点坐标和右下角顶点坐标,最终输出的形状为(批次大小,预测边界框数量,6)
其中,nms_threshold为非极大值抑制的阈值,pos_threshold为置信度的阈值,这里,我们将重叠度较高的边界框以及置信度较低的边界框设置为背景类别,表示丢弃这些边界框,同时我们增加这些低置信度预测的置信度值,以便后续处理时它们不容易被筛除或忽略。
需要注意的是,置信度通常是由目标检测模型计算得到的,用于表示模型对该边界框内是否存在目标的置信程度通过比较置信度,NMS可以选择置信度较高、较具代表性的边界框,并抑制置信度较低、重叠度较高的边界框。
下面是一个例子。
#现在让我们将上述算法应用到一个带有四个锚框的具体示例中。 为简单起见,我们假设预测的偏移量都是零,这意味着预测的边界框即是锚框
#对于背景、狗和猫其中的每个类,我们还定义了它的预测概率
#置信度通常是由目标检测模型计算得到的,用于表示模型对该边界框内是否存在目标的置信程度
#通过比较置信度,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] * anchors.numel())
cls_probs = torch.tensor([[0] * 4, # 背景的预测概率
[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'])
#我们可以看到返回结果的形状是(批量大小,锚框的数量,6)
#最内层维度中的六个元素提供了同一预测边界框的输出信息
#第一个元素是预测的类索引,从0开始(0代表狗,1代表猫),值-1表示背景或在非极大值抑制中被移除了。 第二个元素是预测的边界框的置信度
#其余四个元素分别是预测边界框左上角和右下角的(x,y)轴坐标(范围介于0和1之间)
#置信度通常是由目标检测模型计算得到的,用于表示模型对该边界框内是否存在目标的置信程度
#通过比较置信度,NMS可以选择置信度较高、较具代表性的边界框,并抑制置信度较低、重叠度较高的边界框
output = multibox_detection(cls_probs.unsqueeze(dim=0),
offset_preds.unsqueeze(dim=0),
anchors.unsqueeze(dim=0),
nms_threshold=0.5)
output
#删除-1类别(背景)的预测边界框后,我们可以输出由非极大值抑制保存的最终预测边界框
fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
if i[0] == -1:
continue
#根据i[0]的值进行选择,如果是0,就选择dog,否则选择cat
label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)
下面附上几个需要用到的d2l中的函数。
set_figsize.
def set_figsize(figsize=(3.5, 2.5)):
"""设置matplotlib的图表大小
Defined in :numref:`sec_calculus`"""
use_svg_display()
d2l.plt.rcParams['figure.figsize'] = figsize
边界框表示法转换函数
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)
Defined in :numref:`sec_bbox`"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = d2l.stack((cx, cy, w, h), axis=-1)
return boxes
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)
Defined in :numref:`sec_bbox`"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = d2l.stack((x1, y1, x2, y2), axis=-1)
return boxes
def bbox_to_rect(bbox, color):
"""Defined in :numref:`sec_bbox`"""
# 将边界框(左上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)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/102993.html