锚框02-初步实现

大家好,我是阿林。上次我们讲了怎么去画出一个锚框。其实是比较简单的,就是在选中的中心点上n+m-1的锚框加上预定的高宽。就可以画出来了。这次我们学习更加难的筛选锚框,使得锚框的数量减少,使得锚框计算成本减少。

我们要计算交并函数,来挑选哪个锚框与真实框最接近。代码如下:

# 计算交并比函数
def box_iou(boxes1, boxes2):
    """计算两个锚框或边界框列表中成对的交并比。"""
    #boxes1(左上角x,左上角y,右下角x,右下角y)
    # 计算一个框的面积(长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

进行下测试

box1 = torch.tensor([100,100,220,220]).unsqueeze(0)
box2 = torch.tensor([120,120,200,220]).unsqueeze(0)
iou = box_iou(box1,box2)

print("交并比为:",iou)

学习了交并函数我们就有了将真实框分配给哪个锚框的基础,现在让我们来写一下分配函数。

分配函数的大致的意思就是先计算出交并函数,通过求最大值而求出anchors_bbox_map[-1, 0, -1, -1, 1]这个值。这里有5个锚框就有五个值。-1代表是没分配到真实框,0代表分到第一个真实框,

1代表分到第二个真实框。通过比交并函数的大小取出给配真实框,然后在没有分配到真实框的锚框在看阈值。

# 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

    print("anchors_bbox_map1")
    print(anchors_bbox_map)
    # 遍历剩余的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

看完分配函数,我们还要看锚框对真实框的偏移量。代码如下:

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

如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为“背景”(background)。 背景类别的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。 我们使用真实边界框(labels参数)实现以下multibox_target函数,来标记锚框的类别和偏移量(anchors参数)。 

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] #5
    # 处理每个batch
    for i in range(batch_size):
        # 真实边界框
        label = labels[i, :, :]

        # 为每个锚框分配真实的边界框
        # assign_anchor_to_bbox函数返回,每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1
        # tensor([-1,  0,  1, -1,  1])
        #这边label[:, 1:] 从1开始是因为,求IOU的时候不需要用到类别
        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)

        # 将类标签和分配的边界框坐标初始化为零,tensor([0, 0, 0, 0, 0])
        class_labels = torch.zeros(num_anchors, dtype=torch.long,device=device)
        print("class_labels")
        print(class_labels)
        # 所有anchor对应的真实边框坐标
        assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,device=device)
        print("assigned_bb1")
        print(assigned_bb)
        #tensor([[0., 0., 0., 0.],
        #[0., 0., 0., 0.],
        #[0., 0., 0., 0.],
        #[0., 0., 0., 0.],
        #[0., 0., 0., 0.]])
        # 如果一个锚框没有被分配,我们标记其为背景(值为零)
        indices_true = torch.nonzero(anchors_bbox_map >= 0) # 非背景的索引 [-1,  0,  1, -1,  1]-> 1,2,4
        print("indices_true")
        print(indices_true)
        # 非背景对应的类别标签索引 0,1,1
        bb_idx = anchors_bbox_map[indices_true]
        # 背景为0,新类的整数索引递增1
        #class_lable为[0,  1,  2, 0,  2]
        print("label[bb_idx, 0].long()")
        print(label[bb_idx, 0].long())
        class_labels[indices_true] = label[bb_idx, 0].long() + 1
        #把真实标注好的边界框的坐标值赋给与其对应的某一锚框,
        assigned_bb[indices_true] = label[bb_idx, 1:]
        print("assigned_bb2")
        print(assigned_bb)
        # 偏移量转换,bbox_mask过滤掉背景
        offset = offset_boxes(anchors, assigned_bb) * bbox_mask
        print("offset")
        print(offset)
        batch_offset.append(offset.reshape(-1))
        print("batch_offset")
        print(batch_offset)
        batch_mask.append(bbox_mask.reshape(-1))
        print("bbox_mask.reshape(-1)")
        print(bbox_mask.reshape(-1))
        print("batch_mask")
        print(batch_mask)
        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: 每个锚框的标注偏移量
    bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1
    cls_labels: 每个锚框的标注类别, 其中0表示为背景
    """
    return (bbox_offset, bbox_mask, class_labels)

测试一下

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']);


labels = multibox_target(anchors.unsqueeze(dim=0), ground_truth.unsqueeze(dim=0))
print("分隔开--------------------------------------------------------------------")
# 第三个元素包含标记的输入锚框的类
print(labels[2])
# 第二个元素是掩码(mask)变量,形状为(批量大小,锚框数的四倍)
#  通过元素乘法,掩码变量中的零将在计算目标函数之前过滤掉负类偏移量
print(labels[1])
# 第一个元素包含了为每个锚框标记的四个偏移值。
# 负类锚框的偏移量被标记为零
print(labels[0])

这一期就到这里了,我们下一期计划实现使用非极大值抑制预测边界框这一个实现。这一期的代码量有点大。若是看不太懂代码,可以向阿林一样打印各个变量观看就容易理解了。阿林已经打印了部分变量。可以运行一下啊会好懂一点。阿林的下一期可能要延后一下了。阿林的研究方向的进度有点慢,这几天可能要学一下遥感数据处理方面的知识。还有c++要学,哭哭哭。

 阿林,我要吐了。那我们下一期见。

你可能感兴趣的:(深度学习,人工智能,python)