本文是在李沐老师的d2l课程的基础上,对提供的多锚框实现和IoU计算代码进行逐步解释(注释),当作备忘。
课程和代码地址:13.4. 锚框 — 动手学深度学习 2.0.0 documentation (d2l.ai)
一、锚框实现
锚框的定义不再赘述,本质上也是bbox的一种从采样角度的表达方式。这里需要注意的是,以同一像素为中心的锚框的数量是n+m-1。对于整个输入图像,将共生成wh(n+m-1)个锚框。其中w h m n分别为宽、高、宽高比数量、缩放比数量。
首先,定义多锚框函数如下:
def multibox_prior(data, sizes, ratios):
"""生成以每个像素为中心具有不同形状的锚框"""
其中data大小为(batch_size, chanel, height, width),sizes和ratios分别为人为设定的缩放比和宽高比,大小为(1, n)和(1, m)。
接着是数据提取和转换:
# 读取data后两位作为图片输入的高、宽:
in_height, in_width = data.shape[-2:]
# 指定设备、获取m、n:
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
#计算每一个像素的锚框数量(n+m-1):
boxes_per_pixel = (num_sizes + num_ratios - 1)
#将输入的数组转化为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的相对位置(相对于图像左上角的位置)都除以了总长度,因此后面我们的输出要乘以长度来恢复
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
# mesgrid生成大小为(h, w)的输出
shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
# 拉平偏移量
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
然后根据锚框高、宽计算公式,计算每个像素对应的锚框大小:
# 生成“boxes_per_pixel”个高和宽,
# 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
sizes[0] * torch.sqrt(ratio_tensor[1:])))\
* in_height / in_width # 处理矩形输入
h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
sizes[0] / torch.sqrt(ratio_tensor[1:])))
# 除以2来获得半高和半宽
# 这里repeat相当于作用在每一个像素上,一共是h*w个像素
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
最后,把锚框中心和manipulation相加得到四角坐标:
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次(n+m-1)
# 其中堆叠的[x, y, x, y]就对应加上[-w, -h, w, h]/2得到四角坐标
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
dim=1).repeat_interleave(boxes_per_pixel, dim=0)
output = out_grid + anchor_manipulations
return output.unsqueeze(0)
输出output结果大小是(batch_size, 全部anchor的数量, 4),其中最后一维就是(xmin,xmax,ymin,ymax)。
二、IoU的计算
交并比示意图如下(图源d2l.ai)
显然,根据几何推理,对于两个不同像素产生的锚框,其2个锚框之间的IoU只需要求出两个框各自的面积和相交面积即可。我们假设某情况下box1落在box2的左上:
则IoU的计算可以表达为:,其中
。这里需要注意,图像定义的坐标原点为左上角。
回到代码,先定义IoU函数的输入boxes为指定像素位置的锚框组成的tensor,并求出各自的面积:
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
# boxes第1维是编号,第2维是四角坐标,提取四角坐标用于计算每一个box的面积:
box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
(boxes[:, 3] - boxes[:, 1]))
# boxes1,boxes2,areas1,areas2的形状:
# boxes1:(boxes1的数量,4),
# boxes2:(boxes2的数量,4),
# areas1:(boxes1的数量,),
# areas2:(boxes2的数量,)
# 计算面积:
areas1 = box_area(boxes1)
areas2 = box_area(boxes2)
然后计算分子和分母,进而得到IoU,需要注意维度的变换和切片:
# inter_upperlefts,inter_lowerrights,inters的形状:
# (boxes1的数量,boxes2的数量,2)
# 这里使用了None来占位,tensor从后往前对齐,目的是为了组合两个box的编号形成inter的前2维:
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) # clamp()规定了inter最小值是0,防止没有交集时分子是负数的情况
# inter_areas and union_areas的形状:(boxes1的数量,boxes2的数量)
inter_areas = inters[:, :, 0] * inters[:, :, 1]
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas
这样就可以实现pixel_wise的IoU计算。
(不足之处,还请指出~)