锚框分配算法详解,偏移量代码详解,nms详解

目录

  • 锚框分配
  • 锚框分配算法代码
  • 偏移量计算
  • 标记锚框的类和偏移量
  • NMS
  • torch.max和torch.nonzero用法
  • unsqueeze

  1. 某种锚框生成方法:以输入图像的每个像素为中心生成多个大小和比例不同的边界框。这些边界框称之为锚框(anchor box)
  2. 训练时,将每一个锚框视为一个训练样本。
  3. 训练阶段,需要每个锚框的类别(class)和偏移量(offset),偏移量是真实边界框相对于锚框的偏移量。
  4. 预测阶段,为图像生成多个锚框,预测出锚框的类别和偏移量,根据预测的偏移量来调整它的位置以获得预测的边界框,最后输出符合特定条件的预测边界框。

锚框分配

对于生成的锚框,计算得到任意一个锚框与所有的真实边界框的iou后,如何将真实边界框分配给锚框呢(也就是每一个锚框对应哪一个真实的边界框呢,或者对应背景呢),以下来自《动手学》
锚框分配算法详解,偏移量代码详解,nms详解_第1张图片
锚框分配算法详解,偏移量代码详解,nms详解_第2张图片

锚框分配算法代码

#输入真实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

关于torch.nonzero的demo
锚框分配算法详解,偏移量代码详解,nms详解_第3张图片

偏移量计算

锚框分配算法详解,偏移量代码详解,nms详解_第4张图片
根据生成的锚框和给锚框分配的边界框,进行偏移量的计算。这里提到了常量(为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]])

NMS

定义偏移量的逆变换,正变换是根据锚框和真实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的解释:众多的预测边界框,从中挑选出合适的框。
锚框分配算法详解,偏移量代码详解,nms详解_第5张图片
置信度:对于一个预测边界框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和torch.nonzero用法

关于torch.max(input,dim)的demo,是干掉dim那一个维度。
锚框分配算法详解,偏移量代码详解,nms详解_第6张图片
关于torch.nonzero的用法

锚框分配算法详解,偏移量代码详解,nms详解_第7张图片

unsqueeze

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)

你可能感兴趣的:(目标检测,深度学习)