《动手学深度学习》13.4锚框

13.4.1生成多个锚框

参考: 13.4.1生成多个锚框.

Alt

代码实现

  • 导入功能包
import torch
import matplotlib.pyplot as plt
# 精简打印精度,保留两位小数
torch.set_printoptions(precision=2)  
  • 定义生成以每个像素为中心具有不同形状的锚框函数
# 指定输入、一组大小和一组宽高比,该函数将返回输入的所有锚框
def multibox_prior(feature_map, sizes, ratios):
    """生成以每个像素为中心具有不同形状的锚框。"""
    # 输入图像(特征图)的高和宽
    in_height, in_width = feature_map.shape[-2:]
    # 指定设备
    device = feature_map.device
    # 将比例s和高宽比r转换为tensor,用于后面的tensor计算
    size_tensor = torch.tensor(sizes, device=device)
    ratio_tensor = torch.tensor(ratios, device=device)
    # 为了将锚点移动到像素的中心,需要设置偏移量。
    # 因为一个像素的的高为1且宽为1,我们选择偏移我们的中心0.5
    offset_h, offset_w = 0.5, 0.5
    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
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    # 生成网格,用于生成坐标
    shift_y, shift_x = torch.meshgrid(center_h, center_w)
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
    # 通过在matplotlib中进行可视化,来查看函数运行后得到的网格化数据的结果
    # plt.plot(shift_x, shift_y, marker='.', color='red', linestyle="none",markersize="0.1")
    # plt.show()

    # 生成“n+m-1”个高和宽,之后用于创建锚框的四角坐标 (xmin, xmax, ymin, ymax)
    anc_w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
                   sizes[0] * torch.sqrt(ratio_tensor[1:])))
    anc_h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))

    # 除以2来获得半宽和半高,应该有hw个[5,4]
    anchor_manipulations = torch.stack([-anc_w, -anc_h, anc_w, anc_h], axis=1) / 2 # torch.Size([5, 4])
    # 每个中心点都将有“n+m-1”个锚框,所以生成含所有锚框中心的网格为[5hw,4]
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],dim=1) # torch.Size([hw, 4])

    # 广播机制out_grid [hw, 4]->[hw,1,4] anchor_manipulations:[5,4]->[1,5,4]
    anchors = out_grid.reshape((-1, 1, 4)) + anchor_manipulations.reshape((1, -1, 4)) #[hw,5,4]
    # 返回锚框的变量的形状是[批量大小,锚框的数量,4]
    return anchors.reshape(1,-1,4) #[1,5hw,4]
  • 测试
img = plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
print("Y.shape",Y.shape)
# 将锚框变量 Y 的形状更改为(图像高度、图像宽度、以同一像素为中心的锚框的数量,4)
boxes = Y.reshape(h, w, 5, 4)
print(boxes.shape)
# 获得以指定像素的位置为中心的所有锚框
# 访问以(250,250)为中心的第一个锚框的四个坐标点
res = boxes[250, 250, 0, :]
# tensor([-0.03,  0.07,  0.72,  0.82])
print("第一个锚框坐标",res)
# 可以验证以上输出对不对:size和ratio分别为0.75和1,则(归一化后的)宽和高均为0.75,
# 所以输出是正确的 0.75 = 0.82-0.07 = 0.71 + 0.03
res_w = res[3] - res[1]
res_h = res[2] - res[0]
print(res_w == res_h) # True

13.4.2交并比

Alt

  • 代码实现
# 计算交并比函数
def box_iou(boxes1, boxes2):
    """计算两个锚框或边界框列表中成对的交并比。"""
    # 计算一个框的面积(长X宽)
    box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
                              (boxes[:, 3] - boxes[:, 1]))
    # 分别计算给定锚框的面积
    areas1 = box_area(boxes1)
    areas2 = box_area(boxes2)
    # 计算两个锚框重叠部分的坐标
    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_areas = inters[:, :, 0] * inters[:, :, 1]
    # 相并面积
    union_areas = areas1[:, None] + areas2 - inter_areas
    # 交并比=相交面积/相并面积
    return inter_areas / union_areas
  • 测试
    Alt
# 测试
box1 = torch.tensor([100,100,200,200]).unsqueeze(0)
box2 = torch.tensor([120,120,220,220]).unsqueeze(0)
iou = box_iou(box1,box2)
print("交并比为:",iou)
  • 计算结果Alt

13.4.3标注训练数据的锚框

一 、将真实边界框分配给锚框

  • 算法原理
    Alt
  • 代码实现
# ground_truth真实边界框[nb,4]
# anchors待分配的锚框[na,4]
# iou_threshold预先设定的阈值
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 = box_iou(anchors, ground_truth) # 计算交并比 [na,nb]
    """
    tensor([[0.0536, 0.0000],
        [0.1417, 0.0000],
        [0.0000, 0.5657],
        [0.0000, 0.2059],
        [0.0000, 0.7459]])
    """
    # 对于每个锚框,分配的真实边界框的张量
    # 存放标签初始全为-1
    anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,device=device)
    # 先为每个bb分配一个anchor(不要求满足iou_threshold)
    jaccard_cp = jaccard.clone()
    # 将最大元素的行和列用-1代替,相当于丢弃这行这列的所有元素
    col_discard = torch.full((num_anchors,), -1)
    row_discard = torch.full((num_gt_boxes,), -1)
    # 先遍历每一个真实边界框,为它们找到交并比最大的那个锚框
    for _ in range(num_gt_boxes):
        # 获取数值最大的那个元素的索引
        max_idx = torch.argmax(jaccard_cp)
        # 列索引
        box_idx = (max_idx % num_gt_boxes).long()
        # 行索引
        anc_idx = (max_idx / num_gt_boxes).long()
        # 将真实边界框分配给锚框
        anchors_bbox_map[anc_idx] = box_idx
        # 把anc_idx行box_idx列元素变为-1
        jaccard_cp[:, box_idx] = col_discard
        jaccard_cp[anc_idx, :] = row_discard

    # 遍历剩余的na−nb个锚框
    # 处理还未被分配的anchor, 要求满足iou_threshold
    for i in range(num_anchors):
        # 索引等于初始值-1 的就是剩下的锚框
        if anchors_bbox_map[i] == -1:
            j = torch.argmax(jaccard[i, :])
            # 根据阈值,决定是否分配真实边界框
            if jaccard[i, j] >= iou_threshold:
                anchors_bbox_map[i] = j

    # 每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1
    return anchors_bbox_map
  • 算法理解
    Alt
  • 代码测试
# 真实边界框
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]])

device = torch.device("cuda")
res = assign_anchor_to_bbox(ground_truth[:,1:], anchors, device=device)
print("res:",res)
  • 输出结果
res: tensor([-1,  0,  1, -1,  1], device='cuda:0')

二 、标记类和偏移

Alt

  • 代码实现
  • 坐标转换函数
def box_corner_to_center(boxes):
    """从(左上,右下)转换到(中间,宽度,高度)"""
    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 = torch.stack((cx, cy, w, h), axis=-1)
    return boxes
  • 计算偏移量
def offset_boxes(anchors, assigned_bb, eps=1e-6):
    """对锚框偏移量的转换。"""
    # 坐标转换 从(左上,右下)转换到(中间,宽度,高度)
    c_anc = box_corner_to_center(anchors) # 锚框坐标
    c_assigned_bb = box_corner_to_center(assigned_bb) # 真实边界框坐标
    # 偏移量计算公式
    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:])
    # 拼接
    offset = torch.cat([offset_xy, offset_wh], axis=1)
    return offset
  • 标记锚框的类和偏移量
# 标记类和偏移(anchors参数)
# anchors输入的锚框[1,锚框总数,4] labels真实标签[bn,真实锚框数,5]
def multibox_target(anchors, labels):
    """使用真实边界框标记锚框。"""
    batch_size, anchors = labels.shape[0], anchors.squeeze(0)
    batch_offset, batch_mask, batch_class_labels = [], [], []
    device, num_anchors = anchors.device, anchors.shape[0]
    # 处理每个batch
    for i in range(batch_size):
        # 真实边界框
        label = labels[i, :, :]
        # 为每个锚框分配真实的边界框
        # assign_anchor_to_bbox函数返回,每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1
        # tensor([-1,  0,  1, -1,  1])
        anchors_bbox_map = assign_anchor_to_bbox(label[:, 1:], anchors, device)
        #  bbox_mask: (锚框总数, 4), 0代表背景, 1代表非背景
        bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(1, 4)
        # 将类标签和分配的边界框坐标初始化为零
        class_labels = torch.zeros(num_anchors, dtype=torch.long,device=device)
        # 所有anchor对应的bb坐标
        assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,device=device)
        # 使用真实边界框来标记锚框的类别。
        # 如果一个锚框没有被分配,我们标记其为背景(值为零)
        indices_true = torch.nonzero(anchors_bbox_map >= 0) # 非背景的索引 [-1,  0,  1, -1,  1]-> 1,2,4
        # 非背景对应的类别标签索引 0,1,1
        bb_idx = anchors_bbox_map[indices_true]
        # 背景为0,新类的整数索引递增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)
    """
    Returns:
    列表, [bbox_offset, bbox_mask, class_labels]
    bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4)
    bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1
    cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)
    """
    return (bbox_offset, bbox_mask, class_labels)
  • 测试
labels = multibox_target(anchors.unsqueeze(dim=0), ground_truth.unsqueeze(dim=0))
# 第三个元素包含标记的输入锚框的类
print(labels[2])
# 第二个元素是掩码(mask)变量,形状为(批量大小,锚框数的四倍)
#  通过元素乘法,掩码变量中的零将在计算目标函数之前过滤掉负类偏移量
print(labels[1])
# 第一个元素包含了为每个锚框标记的四个偏移值。
# 负类锚框的偏移量被标记为零
print(labels[0])
  • 输出
    Alt

13.4.4. 使用非极大值抑制预测边界框

  • 算法原理
    Alt
  • 应用逆偏移变换来返回预测的边界框坐标
# 该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标。
def offset_inverse(anchors, offset_preds):
    """根据带有预测偏移量的锚框来预测边界框。"""
    # 从(左上,右下)转换到(中间,宽度,高度)
    anc = 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 = box_center_to_corner(pred_bbox)
    return predicted_bbox

*非极大值抑制函数

# 按降序对置信度进行排序并返回其索引
def nms(bboxs, scores, threshold):
    # 取出分数从大到小排列的索引 order为排序后的得分对应的原数组索引值
    order = torch.argsort(scores, dim=-1, descending=True)
    # 这边的keep用于存放,NMS后剩余的方框(保存所有结果框的索引值)
    keep = []
    while order.numel() > 0:
        if order.numel() == 1:
            keep.append(order.item())
            break
        else:
            # 置信度最高的索引
            i = order[0].item()
            # keep保留的是索引值,不是具体的分数。
            keep.append(i) # 添加本次置信度最高的boundingbox的index
        # 计算最大得分的bboxs[i]与其余各框的IOU
        iou = box_iou(bboxs[i, :].reshape(-1, 4),
                      bboxs[order[1:], :].reshape(-1, 4)).reshape(-1)
        # 保留iou小于阈值的剩余bboxs,iou小表示两个box交集少,可能是另一个物体的框,故需要保留
        idx = torch.nonzero((iou <= threshold)).reshape(-1)  # 返回非零元素的索引
        # 待处理boundingbox的个数为0时,结束循环
        if idx.numel() == 0:
            break
        # 把留下来框在进行NMS操作
        # 这边留下的框是去除当前操作的框,和当前操作的框重叠度大于thresh的框
        # 每一次都会先去除当前操作框(n个框计算n-1个IOU值),所以索引的列表就会向前移动移位,要还原就+1,向后移动一位
        order = order[idx + 1] #iou小于阈值的框
    return torch.tensor(keep,device=bboxs.device)
  • 将非极大值抑制应用于预测边界框
# 将非极大值抑制应用于预测边界框
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
                       pos_threshold=0.0099):
    """使用非极大值抑制来预测边界框。"""
    device, batch_size = cls_probs.device, cls_probs.shape[0]
    anchors = anchors.squeeze(0)
    # 保存最终的输出
    out = []
    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)
        # 预测的边界框坐标
        predicted_bb = offset_inverse(anchors, offset_pred)
        # 对置信度进行排序并返回其索引[0,3,1,2]
        all_id_sorted = torch.argsort(conf, dim=-1, descending=True)
        # 非极大值抑制结果 [0,3]
        keep = nms(predicted_bb, conf, nms_threshold)
        # 找到所有的 non_keep 索引,并将类设置为背景
        non_keep = []
        for i in range(all_id_sorted.numel()):
            res = all_id_sorted[i] in keep
            if not res:
                non_keep.append(all_id_sorted[i].item())
        # [1,2]
        non_keep = torch.tensor(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 = (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)
  • 测试
# 构造4个锚框
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]])  # 猫的预测概率
# 为输入增加样本维度
output = multibox_detection(cls_probs.unsqueeze(dim=0),
                            offset_preds.unsqueeze(dim=0),
                            anchors.unsqueeze(dim=0), nms_threshold=0.5)
"""
我们可以看到返回结果的形状是(批量大小,锚框的数量,6)。 
最内层维度中的六个元素提供了同一预测边界框的输出信息。 
第一个元素是预测的类索引,从 0 开始(0代表狗,1代表猫),
值 -1 表示背景或在非极大值抑制中被移除了。 
第二个元素是预测的边界框的置信度。 
其余四个元素分别是预测边界框左上角和右下角的  (x,y)  轴坐标(范围介于 0 和 1 之间)。
"""                           
print(output)
  • 输出结果
tensor([[[ 0.0000,  0.9000,  0.1000,  0.0800,  0.5200,  0.9200],
         [ 1.0000,  0.9000,  0.5500,  0.2000,  0.9000,  0.8800],
         [-1.0000,  0.8000,  0.0800,  0.2000,  0.5600,  0.9500],
         [-1.0000,  0.7000,  0.1500,  0.3000,  0.6200,  0.9100]]])
  • 读取图片
# 读取图片
img = plt.imread("C:\\Users\\52xj\\Desktop\\pytorch\\img\\catdog.jpg")
h,w = img.shape[:2]
bbox_scale = torch.tensor((w,h,w,h))
fig = plt.imshow(img)
# 删除 -1 (背景)类的预测边界框,输出由非极大值抑制保存的最终预测边界框。
for i in output[0].detach().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)
plt.show()
  • 输出结果
    Alt
  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值