对于生成的锚框,计算得到任意一个锚框与所有的真实边界框的iou后,如何将真实边界框分配给锚框呢(也就是每一个锚框对应哪一个真实的边界框呢,或者对应背景呢),以下来自《动手学》
#输入真实gt边界框[n1,4],生成的锚框[n2,4],device设备,分配给锚框是非背景的阈值,输出值为每个anchor对应的真实gt边界框的索引[n2]
def assign_anchor_to_bbox(ground_truth,anchors,device,iou_threshold=0.5):
"""给每一个锚框分配最接近的真实边界框(或者背景)"""
# 锚框的数量anchor[n1],真实边界框的数量[n2]
num_anchors,num_gt_boxes = anchors.shape[0],ground_truth[0]
# 计算每个锚框与每个真实边界框的iou值,[n1,n2]
jaccard = box_iou(anchors,ground_truth)
# 建一个tensor当作map,用来放对每个锚框所对应的真实边界框,默认为-1,[n1]
anchors_bbox_map = torch.full((num_anchors,),-1,dtype=torch.long,device=device)
# 得到每行的最大值,即对于每个锚框来说,iou最大的那个真实边界框,返回iou值和对应真实边界框索引值[n1],[n1]
max_ious,indices = torch.max(jaccard,dim=1)
# 根据阈值得到锚框不为背景的相应的索引值[<=n1]
anc_i = torch.nonzero(max_ious>=iou_threshold).reshape(-1)
# 根据阈值得到锚框不为背景的真实边界框索引值[<=n1],与anc_i一一对应的
box_j = indices[max_ious>=iou_threshold]
# 挑出>=iou_threshold的值,重新赋值,也就是对每个锚框,得到大于给定阈值的匹配的真实gt边界框的对应索引
anchors_bbox_map[anc_i] = box_j
#行,列的默认值[n1],[n2]
col_discard = torch.full((num_anchors,),-1)
row_discard = torch.full((num_gt_boxes,),-1)
# 以下对每个真实边界框重新分配给的锚框,所以,循环次数是gt box的个数。防止在最大值赋值时,某几个锚框对应同一个真实边界框
for _ in range(num_gt_boxes):
#取得该矩形中最大值的索引,是按reshape(-1)得到的索引 0-(n1*n2-1)
max_idx = torch.argmax(jaccard)
# 得到矩阵最大值所在的列,就是对应的真实gt边界框的索引
box_idx = (max_idx%num_gt_boxes).long()
# 得到矩阵最大值所在的行,是对应的锚框的索引
anc_idx = (max_idx/num_gt_boxes).long()
# 重新赋值,就是矩阵最大iou值中锚框与其对应的真实gt边界框
anchors_bbox_map[anc_idx] = box_idx
# 将最大值所在该行置为-1
jaccard[:,box_idx] = col_discard
# 将最大值所在该列置为-1
jaccard[anc_idx,:] = row_discard
# 返回值是每个anchor对应的真实gt边界框的索引(其实是list)
return anchors_bbox_map
根据生成的锚框和给锚框分配的边界框,进行偏移量的计算。这里提到了常量(为0,0.1和0.2的),但在其他代码实现中并未发现。
def offset_boxes(anchors,assigned_bb,eps=1e-6):
# anchors是生成的锚框,assigned_bb是给每个锚框分配的边界框的值[n,4],[n,4]
# 将左上右下坐标转为中心点宽高的格式
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
# 根据公式得到中心点xy偏移量,宽高wh的偏移量[n,2]
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:])
#维度拼接,将维度拼接为[n,4]
offset = torch.cat([offset_xy,offset_wh],axis=1)
return offset
如果一个锚框没有被分配给真实边界框,我们只需将锚框的类标记为 “背景”类。 背景类的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。 我们使用真实边界框( labels 参数)实现以下 multibox_target 函数,来标记锚框的类和偏移量( anchors 参数)。 此函数将背景类设置为零,然后将新类的整数索引递增一。
# 输入为左上右下坐标的矩形框,生成的锚框和分配给锚框的真实gt边界框
# 输出的第一个维度为batch.offset为偏移量,mask表每个锚框的对应类别是背景还是非背景的mask,类别class_labels表示每个锚框对应类别,0是背景,>=1是其他类别。
def multibox_target(anchors, labels): #[1,5,4],[1,2,5]
"""使用真实边界框标记锚框。"""
batch_size, anchors = labels.shape[0], anchors.squeeze(0) # 1,[5,4]
batch_offset, batch_mask, batch_class_labels = [], [], []
device, num_anchors = anchors.device, anchors.shape[0]
# 循环次数为batch_size
for i in range(batch_size):
label = labels[i, :, :]
# 看似map,实际是list[5],5个锚框对应[-1,0,1,-1,1].给每个anchor分配真实边界框(-1表背景),表现为真实边界框类别
anchors_bbox_map = assign_anchor_to_bbox(label[:, 1:], anchors,device)
# 将锚框所属类别为背景和非背景区分开来,1非背景
bbox_mask_2 = (anchors_bbox_map >= 0).float() #[5][0,1,1,0,1]
# 维度扩充,从[5]->[5,1],[[0.],[1.],...,[1.]]
bbox_mask_1 = bbox_mask_2.unsqueeze(-1) #[5,1]
# 复制为矩阵,用于之后的矩阵运算,为了与之后的偏移量进行计算方便,因此为4维
bbox_mask = bbox_mask_1.repeat(1, 4) # [5,4][[0,0,0,0],[1,1,1,1],...,[1,1,1,1]]
# 将分配给锚框的对应的类别标签初始化为零[5]
class_labels = torch.zeros(num_anchors, dtype=torch.long, device=device)
# 将分配给锚框的对应的真实边界框初始化为零[5,4]
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, device=device)
# [z,n]此时n是输入维度,z是非零元素的个数。[[1],[2],[4]],
# 得到锚框anchor对应类别是非背景的锚框的索引
indices_true = torch.nonzero(anchors_bbox_map >= 0) # [3,1]
# [[0],[1],[1]]得到锚框anchor对应类别是非背景的锚框的对应真实gt边界框的索引
bb_idx = anchors_bbox_map[indices_true] #[3,1]
# 对于对应类别是非背景的锚框,更新其对应的真实gt边界框的类别。非背景的类别+1,背景类别为0
class_labels[indices_true] = label[bb_idx, 0].long() + 1
# 非背景的anchor对应给分配的gt坐标,背景为[0,0,0,0]
assigned_bb[indices_true] = label[bb_idx, 1:]
# 锚框与分配给该锚框的真实gt边界框框进行偏移量计算,bbox_mask为将背景的偏移量给归零(*是元素相乘)(类别为背景的assigned_bb值全为0)
offset = offset_boxes(anchors, assigned_bb) * bbox_mask
# 存储每个batch
batch_offset.append(offset.reshape(-1))
batch_mask.append(bbox_mask.reshape(-1))
batch_class_labels.append(class_labels)
# 将batch维提取出来
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)
此函数返回三个变量,第一个维度都是batch维度,bbox_offset返回的是锚框(anchor)与分配给该锚框的真实gt边界框的偏移量(如果锚框对应的类别是背景的话,偏移量为0),bbox_mask返回锚框对应类别是背景(0)还是非背景(1),mask第二个维度是对应偏移量的四个坐标值。class_labels是锚框对应的真实类别(0是背景,>0是其他类别(原始类别+1了的))
# ground_truth 是有两个真实gt框,此时0,1都表示类别0和类别1(不表示背景),anchors表示5个生成的锚框
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']);
# 多出一个batch维度
labels = multibox_target(anchors.unsqueeze(dim=0),
ground_truth.unsqueeze(dim=0))
labels[0]
labels[1]
labels[2]
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]])
===================
tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1.,
1., 1.]])
===================
tensor([[0, 1, 2, 0, 2]])
定义偏移量的逆变换,正变换是根据锚框和真实gt边界框生成偏移量。逆变换是由锚框和偏移量生成预测边界框
# 输入是生成的锚框,预测的偏移量,
# 输出是预测的边界框
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
nms的解释:众多的预测边界框,从中挑选出合适的框。
置信度:对于一个预测边界框B,计算它是每个类别的概率,最大的那个概率值p就是边界框B的置信度,同时,该概率值对应类别就是该预测边界框的预测类别。
# 输入为boxes是预测边界框,shape是(预测边界框的个数,4维坐标),scores是每张图片中预测边界框的置信度(也就是预测类别概率最大的值)shape是(预测边界框的个数),输出的值是进行了nms后处理得到的预测边界框的索引数组。
def nms(boxes,scores,iou_threshold):
# tensor([0.9000, 0.8000, 0.7000, 0.9000])->[0,3,1,2]
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 = d2l.box_iou(boxes[i,:].reshape(-1,4),
boxes[B[1:],:].reshape(-1,4)).reshape(-1) #计算当前置信度最大的预测边界框与其他预测边界框的iou值,维度是boxes.shape-1
inds = torch.nonzero(iou<=iou_threshold).reshape(-1) #inds保留当前置信度最大的预测边界框与其他预测边界框的iou小于阈值的预测边界框的索引
B = B[inds+1] #因为从第二个预测边界框开始的,所以边界框索引inds要加1,keep已经保存了这一轮的预测边界框,更新B
return torch.tensor(keep,device=boxes.device)
在这个函数中,将非最大值抑制应用到预测边界框。
#@save
# 背景,猫,狗三种类别的预测概率[1,3,4],偏移量[1,16],设为0,生成的四个锚框[1,4,4],nms的阈值nms_threshold,正类的阈值pos_threshold
# 返回值为类别索引,置信度,预测边界框坐标
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,pos_threshold=0.009999999):
"""使用非极大值抑制来预测边界框。"""
device, batch_size = cls_probs.device, cls_probs.shape[0]
anchors = anchors.squeeze(0)
num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] #预测的类别数(包含背景类),生成的anchor数
out = []
# 对每个batch进行循环
for i in range(batch_size):
# 得到类别概率,预测偏移量
cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
conf, class_id = torch.max(cls_prob[1:], 0) #除去背景类别,得到图片的置信度。max(x,0)去除0维度,
predicted_bb = offset_inverse(anchors, offset_pred) #根据生成的anchor和预测得到的偏移量,通过逆变换得到预测的边界框
keep = nms(predicted_bb, conf, nms_threshold) #进行nms处理,得到最终的预测边界框
# 找到所有的 non_keep 索引,并将类设置为背景
all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
combined = torch.cat((keep, all_idx))
uniques, counts = combined.unique(return_counts=True)
non_keep = uniques[counts == 1]
# 以上几行是得到非预测输出的预测边界框
all_id_sorted = torch.cat((keep, non_keep)) # 将nms得到的边界框和丢弃的边界框进行排序合并,[0,3,1,2]
class_id[non_keep] = -1 # 非nms结果更新为-1
class_id = class_id[all_id_sorted] # 按nms重新得到类别索引,[0,1,-1,-1]
conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] # 按nms结果重排
# `pos_threshold` 是一个用于非背景预测的阈值
below_min_idx = (conf < pos_threshold)
class_id[below_min_idx] = -1
conf[below_min_idx] = 1 - conf[below_min_idx]
pred_info = torch.cat(
(class_id.unsqueeze(1), conf.unsqueeze(1), predicted_bb), dim=1)
out.append(pred_info)
return torch.stack(out)
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()) #偏移量简化为0
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'])
output = multibox_detection(cls_probs.unsqueeze(dim=0),
offset_preds.unsqueeze(dim=0),
anchors.unsqueeze(dim=0), nms_threshold=0.5)
output
关于torch.max(input,dim)的demo,是干掉dim那一个维度。
关于torch.nonzero的用法
class_id.unsqueeze(1)
a = torch.tensor([1,1,1,1])
a.unsqueeze(1).shape # tensor.Size([4,1])
a.unsqueeze(1) # [[1],[1],[1],[1]]
# torch.nonzero
multibox_target函数中的函数demo
```python
# When as_tuple is ``False`` (default):
# If input has n dimensions, then the resulting indices tensor out is of size (z×n), where z is the total number of non-zero elements in the input tensor.
# n是输入维度,z是非零元素的个数
import torch
a = torch.tensor([[[3,0],[1,2]],[[2,2],[0,0]]])
print(torch.nonzero(a).shape)
torch.nonzero(a)
torch.Size([5, 3])
tensor([[0, 0, 0],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1]])
# 一些demo
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718115359782.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MTM2MTk2,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718115431696.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MTM2MTk2,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718115510118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MTM2MTk2,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718115526761.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MTM2MTk2,size_16,color_FFFFFF,t_70)