前言
沐神的代码看了很久、B站上的视频也刷了很多遍,感叹下自己的基础确实不怎么扎实,锚框部分的底层代码几乎是一行行撸过来的,因此对整个学习的部分做一个总结先放上沐神的b站和电子教程,以示敬意!
《动手学深度学习》第二版预览版
跟李沐学AI
上一篇主要总结了锚框,以及如何以一副图像的每个像素点为中心,进行锚框的生成,具体可以移步:
SSD目标检测的个人总结(1)—— 锚框的生成
交并比
在目标检测任务中,如果在已知真实框的情况下,如何判断生成锚框的质量,如何选择生成的最优锚框?
通常我们会通过交并比,既杰卡德系数(Jaccard)来衡量,需要注意的是,杰卡德系数的计算结果在0-1之间,越接近0则代表锚框和真实框重合较少,越接近1则代表锚框和真实框重合度较高;等于0完全不重合,等于1完全重合
我们以上一章节里使用的猫狗图片为例,假设我们事先定义好猫和狗的真实框,同时假定拥有5个anchors的坐标相对值,和两个真实框(ground_truth)的标签数据,每个真实框的标签数据的第一个元素为所属类别,在该例中,0代表dog,1代表cat;
我们可以逐一计算出每个锚框(anchors)与真实框(ground_truth)的IOU,具体计算如下:
# 构建的anchors和真实框
# 锚框和真实框x坐标相减
# 锚框和真实框y坐标相减
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]])
# 计算交并比的部分
box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *(boxes[:, 3] - boxes[:, 1]))
areas1 = box_area(anchors.unsqueeze(dim=0)[0,:,:])
areas2 = box_area(ground_truth.unsqueeze(dim=0)[0,:,:][:,1:])
# 取出左上角锚框和真实框交集部分较大的值,对anchors取前两个坐标同时保持当前维度
inter_upperlefts = torch.max(anchors.unsqueeze(dim=0)[0,:,:][:, None, :2], ground_truth.unsqueeze(dim=0)[0,:,:][:,1:][:, :2])
# 取出右下角锚框和真实框交集部分较小的值,对anchors取后两个坐标同时保持当前维度
inter_lowerrights = torch.min(anchors.unsqueeze(dim=0)[0,:,:][:, None, 2:], ground_truth.unsqueeze(dim=0)[0,:,:][:,1:][:, 2:])
inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
# 计算交集面积____矩形____长*宽
inter_areas = inters[:, :, 0] * inters[:, :, 1]
# 锚框面积+真实框面积-交集面积
union_areas = areas1[:, None] + areas2 - inter_areas
# 交并比 = 交集面积占并集面积的比例
# 数据格式为M个锚框与N个类别的真实框的IOU值的缩放
IOUS = inter_areas/union_areas
通过计算后返回IOU的值,进一步封装以后可以得到计算交并比的函数,提供锚框和真实框的具体坐标信息作为参数
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
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
计算真实框和锚框的交并比结果如下图所示,shape为(5,2),每一个元素内分别对应两个分类,如[0.00,0.75]则表示当前锚框与分类2的交并比为0.75
为真实框分配锚框
经过计算交并比IOU后,我们可以得到不同锚框anchors对应同一个真实框ground_truth的交并比,真实框的标签数据构成为类别+偏移量,对于IOU值最大的锚框,我们可以认为该锚框与真实框最为接近,因此可以将一个真实框的类别和偏移量分配给该锚框.具体实现代码如下
- 获得交并比相关信息,主要获取交并比大小,以及该大小所属的分类是什么
- 构建真实框与锚框的索引矩阵
- 创建行列掩码,全填充-1
- 根据真实框数量创建循环,IOU值最大的分类下找到索引,则将该数值所在的行\列填充为-1,知道所有真实框都被分配到锚框
# 分配真实框的部分
num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long)
# 返回最大交并比及其下标,IOUS的格式为,M个锚框,N个类别真实框;在N个类别上获得最大IOU的值;在N个类别上获得最大IOU值的下标
max_ious, indices = torch.max(IOUS, dim=1)
print('max_ious:',max_ious)
print('indices:',indices)
# 在maxiou上获得超过阈值的数组下标
anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1)
# 在indices上获得超过阈值的值
box_j = indices[max_ious >= 0.5]
# # 建立对应坐标
# anc_i对应数组下标,box_j对应下标索引的值
anchors_bbox_map[anc_i] = box_j
# 有五列——对应五个锚框,有两行,对应两个真实框,创建空白的形状,统统填充-1
col_discard = torch.full((num_anchors,), -1)
row_discard = torch.full((num_gt_boxes,), -1)
# 根据真实边框数量,将真实边框分配给锚框
for _ in range(num_gt_boxes):
# 获取最大IOU的索引,在使用argmax时,获取索引以顺序方式返回,类似于展平成一维后返回索引,如shape为(5,2)的tensor,若最大值在tensor[4][1]的位置,则直接返回9
max_idx = torch.argmax(IOUS)
print('max_idx is',max_idx)
# 与真实边框取余数,作为列索引
box_idx = (max_idx % num_gt_boxes).long()
# 与真实边框取商,作为行索引
anc_idx = (max_idx / num_gt_boxes).long()
# 决定将哪个锚框分配给真实框,即将类别给对应的索引
anchors_bbox_map[anc_idx] = box_idx
# 对最大IOU所在的行列赋值为-1,因为一个类别的真实框只分配IOU最大的锚框,因此所在列的类别可以删除
IOUS[:, box_idx] = col_discard
IOUS[anc_idx, :] = row_discard
运行该代码后,我们可以得到几个关键变量:
- max_ious: 交并比计算后,在不同分类下,取交并比的最大值;
- indices: 不同交并比取最大之后所属的分类;
- anc_i: 大于thresholds阈值的交并比数值;
- box_j: anc_i所对应的类别;
- max_idx: 根据真实框所构建循环内的最大iou值的索引
通过上述代码的运行,我们可以为两个真实框分配到当前类别下iou值最大的锚框,我们通过进一步封装,提供,真实框信息,锚框信息,iou阈值等,以便后续代码调用,
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]
jaccard = box_iou(anchors, ground_truth)
# 通过full,利用num_anchors构建坐标,有多少个锚框就有多少行
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
device=device)
max_ious, indices = torch.max(jaccard, dim=1)
anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
box_j = indices[max_ious >= iou_threshold]
# 建立对应坐标
anchors_bbox_map[anc_i] = box_j
col_discard = torch.full((num_anchors,), -1)
row_discard = torch.full((num_gt_boxes,), -1)
for _ in range(num_gt_boxes):
# 获取最大IOU的索引
max_idx = torch.argmax(jaccard)
box_idx = (max_idx % num_gt_boxes).long()
anc_idx = (max_idx / num_gt_boxes).long()
anchors_bbox_map[anc_idx] = box_idx
jaccard[:, box_idx] = col_discard
jaccard[anc_idx, :] = row_discard
return anchors_bbox_map
以我们提供的真实框和锚框的信息为输入,最后返回anchors_bbox_map,该信息存储了当前锚框和真实框iou值所对应的类别,-1为未小于iou阈值的类别,统一标记为-1.
anchors_bbox_map is : tensor([-1, 0, 1, -1, 1])