大家好,我是阿林。上次我们讲了怎么去画出一个锚框。其实是比较简单的,就是在选中的中心点上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++要学,哭哭哭。
阿林,我要吐了。那我们下一期见。