大家好,我是CuddleSabe,目前大四在读,深圳准入职算法工程师,研究主要方向为多模态(VQA、ImageCaptioning等),欢迎各位佬来讨论!
我最近在有序地计划整理CV入门实战系列及NLP入门实战系列。在这两个专栏中,我将会带领大家一步步进行经典网络算法的实现,
欢迎各位读者(da lao)订阅
本教程皆是对李沐老师的动手学深度学习-锚框视频教程及zh.d2l.ai中代码的详细解释(李沐老师跳过了一部分的代码解释)
⚠️代码警告!
def multibox_prior(data, sizes, ratios):
"""生成以每个像素为中心具有不同形状的锚框"""
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
boxes_per_pixel = (num_sizes + num_ratios - 1)
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)
# 生成“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来获得半高和半宽
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
in_height * in_width, 1) / 2
# 每个中心点都将有“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)
output = out_grid + anchor_manipulations
return output.unsqueeze(0)
第一眼看上去各位同学可能会有点迷糊,没关系,我们一行一行来解析!
首先,我们需要确定的是,作为生成锚框的函数,它的输入和输出都是什么?尺寸多少呢?
输入参数 | 含义 | 形状 | 类型 |
---|---|---|---|
data | 那当然是图片的数据啦 | [b, c, h, w] | tensor |
sizes | 锚框的尺寸!也称 S S S | 任意长度,本文为3 | list |
ratios | 锚框的高宽比!也称 R R R | 任意长度,本文为3 | list |
输出参数 | 含义 | 形状 | 类型 |
---|---|---|---|
output | 各个锚框的左上右下点坐标 | [b, 该图片的锚框数, 4] | tensor |
可以看到,该函数的任务是输入图片和想要的锚框的尺寸信息,则会返回所有该图片所有锚框的左上顶点和右下顶点顶坐标!那么它是如何实现的?我们接下来逐行看代码:
in_height, in_width = data.shape[-2:]
device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
这两行的作用是获取图片的尺寸(高和宽)以及其余参数的数量
boxes_per_pixel = (num_sizes + num_ratios - 1)
这里是获得一个像素有多少个锚框。
我们本教程中使用3个size和3个ratios,如果是标准官方实现的话是3*3=9,
但是我们这里为了加快运算,每个像素只会获得3+3-1=5个锚框
那么我们怎么通过 sizes 和 ratios 来判断锚框的尺寸呢? 例如,我们输入的
sizes=[0.75, 0.5, 0.25] 和 ratios=[1, 2, 0.5],这里我们可以听下李沐老师的说法:
# 为了将锚点移动到像素的中心,需要设置偏移量。
# 因为一个像素的高为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 = (np.arange(in_height, ctx=device) + offset_h) * steps_h
center_w = (np.arange(in_width, ctx=device) + offset_w) * steps_w
假定图片大小为100*100,这里是先生成0,1,2,3...100的序列,
加上0.5后即是在图片中的像素坐标,再乘以steps_w(h)等同于除图片的宽(高)
等同于归一化到0-1,即在图片内到百分比坐标
shift_y, shift_x = torch.meshgrid(center_h, center_w)
这里我们使用meshgrid来对xy坐标两两组合得到网格坐标,
两个输出的尺寸信息为[输入图片高,输入图片宽],即[100, 100]
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
摊平,因为函数输出是宽和高在同一维度
w = np.concatenate((size_tensor * np.sqrt(ratio_tensor[0]),
sizes[0] * np.sqrt(ratio_tensor[1:]))) \
* in_height / in_width # 处理矩形输入
h = np.concatenate((size_tensor / np.sqrt(ratio_tensor[0]),
sizes[0] / np.sqrt(ratio_tensor[1:])))
怎么样,是不是有点眼熟?!
接下来重头戏来了!
anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(in_height * in_width, 1) / 2
相信很多同学第一次看到的时候头都晕了,没关系,我们把它拆解开
torch.stack((-w, -h, w, h))
这里输出的shape是[4, 5],那么我们再加一个转置,相当于两个维度交换
torch.stack((-w, -h, w, h)).T
shpe为[5, 4],即一个像素的锚框坐标,这里的5是因为每个像素有(num_sizes + num_ratios - 1)个锚框,也就是5。
而这里的4就是一个锚框的左上顶点和右下顶点的xy坐标。
那么这个只是一个像素的,我们在第一个维度(dim=0)重复h*w(总像素个数)次
torch.stack((-w, -h, w, h)).T.repeat(in_height * in_width, 1)
这时候的shape为[h*w*5, 4],第一维度也就是一张图片所有的锚框数,第二维度为两个顶点的xy坐标。
但我们所需的是半宽半高(因为我们使用半宽半高加上中心点才是顶点坐标)
即
torch.stack((-w, -h, w, h)).T.repeat(in_height * in_width, 1)/2
接下来倒数第二行代码: 我们依旧一点点进行分解
torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1)
输出为[h*w, 4],我们这里为什么要重复添加两个x和y呢?
因为我们上边的代码中获得了(-w/2, -h/2, w/2, h/2)即一个锚框的半宽半高。
但是我们现在一个像素只有一个框,因此我们将每一行重复5次,即(num_sizes + num_ratios - 1)次
torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1).repeat_interleave(boxes_per_pixel, dim=0)
最后,我们将每个锚框的中心点坐标加上每个锚框的[负半宽,负半高,半宽,半高],就是每个锚框的左上顶点和右下顶点坐标(图片的纵坐标向下增长)
output = out_grid + anchor_manipulations
返回尺寸为 [b, h * w * 每个像素的锚框数, 4]
#@save
def show_bboxes(axes, bboxes, labels=None, colors=None):
"""显示所有边界框"""
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj
"""将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式((左上x, 左上y), 宽, 高)"""
def bbox_to_rect(bbox, color):
return plt.Rectangle(xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1], fill=False, edgecolor=color, linewidth=2)
labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = bbox_to_rect(bbox.detach().numpy(), color)
axes.add_patch(rect)
if labels and len(labels) > i:
text_color = 'k' if color == 'w' else 'w'
axes.text(rect.xy[0], rect.xy[1], labels[i],
va='center', ha='center', fontsize=9, color=text_color,
bbox=dict(facecolor=color, lw=0))
使用
我们使用上述的锚框生成函数,获得如下shape的输出 torch.Size([1, 2042040, 4])
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
为了方便,我们将其reshape一下,如下
boxes = Y.reshape(h, w, 5, 4)
则这个变量代表了在250行250列像素的第一个锚框的俩顶点坐标值
boxes[250, 250, 0, :]
如下
tensor([0.06, 0.07, 0.63, 0.82])
同时,因为我们的顶点坐标都缩放到0-1之间,因此我们需要将其还原到原本的图像坐标上去,即乘上原图像的w和h
bbox_scale = torch.tensor((w, h, w, h))
boxes[250, 250, :, :] * bbox_scale 就是图片[250, 250]处的锚框在图片中的真实坐标
bbox_scale = torch.tensor((w, h, w, h))
fig = plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
's=0.75, r=0.5'])