参考: 13.4.1生成多个锚框.
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
# 计算交并比函数
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
# 测试
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)
# 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
# 真实边界框
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')
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])
# 该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标。
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()