各ピクセルを中心に配置して、異なるスケールとアスペクト比を持つ複数の境界ボックスを生成します。これらの境界ボックスはアンカー ボックスと呼ばれます。----「ハンズオンディープラーニングv2」
なぜこれをしたいのですか?
スケーリングとアスペクト比のすべての組み合わせを使用して各ピクセルを中心にすると、入力画像には合計 whnm 個のアンカー ボックスが含まれます。この場合、計算の複雑さが容易に高くなりすぎます。
ただし、上記の方法では wh*(n+m-1) 個のアンカー ボックスのみが生成されるため、計算量が削減されます。
簡単に言うと、アンカーボックスが多すぎて不要なので、いくつか削除します。
なぜ正規化された w_0、h_0 を使用するのでしょうか?
絶対座標の表現は元の画像のピクセル値に基づいているため、画像の実際のスケールを知る必要があります。画像が拡大縮小されている場合、この表現は正確に配置できません。
座標は正規化されているため、画像のスケールがわかっていれば、現在のスケールで配置された長方形のフレームを簡単に使用できます。
import torch
'''
输入参数:
data:需要进行锚框的图片,形状为(batch_size, channel_num, h, w),其中 batch_size代表图片数量,channel_num代表图片通道数,
h代表图片的高,w代表图片的宽
sizes:缩放比,以标量、元组、列表形式均可(一般缩放比都具有多个)
ratios:宽高比,以标量、元组、列表形式均可(一般宽高比都具有多个)
返回值:形状为(batch_size, anchors_num, 4);其中anchors_num代表锚框总数量,对该维度下标[0,n+m-1),[n+m-1, 2n+2m-2)表示第二个像
素中心的锚框,... ,[anchors_num-n-m+1, anchors_num)表示最后一个像素中心对应的锚框;最后一个维度4表示用方法1来进行表示的锚框。
'''
def multibox_prior(data, sizes, ratios):
"""生成以每个像素为中心具有不同形状的锚框"""
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios) # 3, 3
boxes_per_pixel = (num_sizes + num_ratios - 1) # 每个像素的锚框数
size_tensor = torch.tensor(sizes, device=device) # list 转为 tensor
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) # 展平为一维
# 生成“boxes_per_pixel”个高和宽,
# 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
## 动手学深度学习V2 原始代码
# 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来获得半高和半宽
# anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
# in_height * in_width, 1) / 2
## 更新后的代码
w_0 = torch.cat((sizes[0] * torch.sqrt(in_height * ratio_tensor[:] / in_width),
size_tensor[1:] * torch.sqrt(in_height * ratio_tensor[0] / in_width))) # 归一化后的宽
h_0 = torch.cat((sizes[0] * torch.sqrt(in_width / ratio_tensor[:] / in_height),
size_tensor[1:] * torch.sqrt(in_width / ratio_tensor[0] / in_height))) # 归一化后的高
# 除以2来获得半高和半宽(因为后续想获得左上角和右下角的坐标)
'''
torch.stack((-w_0, -h_0, w_0, h_0)).T
如果不转置:
[[-w_0,-w_0,...,-w_0],
[-h_0,-h_0,...,-h_0],
[w_0,w_0,...,w_0],
[h_0,h_0,...,h_0]]
转置后:
[[-w_0, -h_0, w_0, h_0],
[-w_0, -h_0, w_0, h_0],
...
[-w_0, -h_0, w_0, h_0]]
repeat不同于repeat_interleave,repeat是整块整块重复的
'''
anchor_manipulations = torch.stack((-w_0, -h_0, w_0, h_0)).T.repeat(
in_height * in_width, 1) / 2
'''
torch.stack([shift_x, shift_y, shift_x, shift_y],dim=1)
由于shift_x、shift_y都是一维的,又由于dim=1,那么上面这句代码就相当于给接上后再转置
例如:
shift_x=[x1,x2,x3]
shift_y=[y1,y2,y3]
那么接上后为:[shift_x, shift_y, shift_x, shift_y]为:
[[x1,x2,x3],
[y1,y2,y3],
[x1,x2,x3],
[y1,y2,y3]]
转置后为:
[[x1,y1,x1,y1],
[x2,y2,x2,y2],
[x3,y3,x3,y3]]
再给转置后的张量进行重复:repeat_interleave(boxes_per_pixel, dim=0):
有趣的是它是这样重复的:
[[x1,y1,x1,y1],
[x1,y1,x1,y1],
[x1,y1,x1,y1],
[x2,y2,x2,y2],
[x2,y2,x2,y2],
[x2,y2,x2,y2],
[x3,y3,x3,y3],
[x3,y3,x3,y3],
[x3,y3,x3,y3]]
'''
# 每个中心点都将有“boxes_per_pixel”个锚框,
# 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
dim=1).repeat_interleave(boxes_per_pixel, dim=0)
'''
【注:】s为sizes中元素的个数,r为ratios中元素的个数
out_grid一行会重复s+r-1次,刚好对应anchor_manipulations中的s+r-1个框
out_grid大小为:wh*(s+r-1)
anchor_manipulations大小为:(s+r-1)*wh
'''
output = out_grid + anchor_manipulations # output结果包含了左上角的坐标(前两个元素),右下角的坐标(后两个元素)
return output.unsqueeze(0)