提示:本文是参考李沐老师目标检测这一章,对该章边界框和锚框代码的整体梳理,具体资料连接会在文章中给出。且全部实验代码是在kaggle平台上验证过滴。
前言
李沐老师参考资料地址:link(代码参考地址). link(视屏参考地址).
这部分会使用到一张猫狗图片,现在我将其下载下来并放到我的网盘之中,提取链接以及提取码如下:
链接:https://pan.baidu.com/s/12vsj-HYPs1GKFFgJ3f8MsA
提取码:6666
一、边界框
这里我将图片上传到了kaggle平台上,如下图所示:
接下来,我们读取图片,并将对图片进行显示。
'''
注意
%matplotlib inline在pycharm中会报错,这只适用于在Juter和其类似的软件中(如kaggle上)。
若想要在pycharm中显示图片,在调用plt.imshow()函数后,调用plt.show()函数即可。
'''
%matplotlib inline
import torch
import matplotlib.pyplot as plt
from IPython import display
def set_figsize(figsize=(3.5, 2.5)):
"""Set the figure size for matplotlib.
Defined in :numref:`sec_calculus`"""
display.set_matplotlib_formats('svg')
plt.rcParams['figure.figsize'] = figsize
# 调用
set_figsize()
'''这是我的路径,大家注意一下填写自己的路径'''
img = plt.imread('../input/catdog/catdog.jpg')
plt.imshow(img);
结果如下图所示:
注意
我们看到显示图片的坐标的y轴与我们平常所学平面坐标系是不一样的,这是理解后续代码的基础之一。
接下来,查看图片的shape
img.shape
结果如下图所示:
边界框定义
在目标检测中,我们通常使用边界框来描述对象空间的位置,且边界框是矩形的。
边界框表示方法
方法1:
由左上角坐标(
x
左
上
x_{左上}
x左上,
y
左
上
y_{左上}
y左上)和右下角坐标(
x
右
下
x_{右下}
x右下,
y
右
下
y_{右下}
y右下),在显示图片的坐标系中永远有
x
右
下
x_{右下}
x右下>
x
左
上
x_{左上}
x左上,
y
右
下
y_{右下}
y右下>
y
左
上
y_{左上}
y左上。
方法2:
知道边界框的中心坐标(
x
中
心
x_{中心}
x中心,
y
中
心
y_{中心}
y中心),以及边界框的高H和宽W。
接下来定义在两种表示方法之间进行转换的函数。
'''
方法1转换到方法2
输入参数boxes可以是长度为4的张量,也可以是形状为(n,4)的二维张量,其中n是边界框的数量。
输入参数格式为 (x_左上, y_左上, x_右下, y_右下)———>(boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3),下同
返回值格式为 (x_中心, y_中心, w, h)
'''
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
'''
方法2转换到方法1
输入参数格式为 (x_中心, y_中心, w, h)
返回值格式为 (x_左上, y_左上, x_右下, y_右下)
'''
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
进行验证:
# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]
boxes = torch.tensor((dog_bbox, cat_bbox))
box_center_to_corner(box_corner_to_center(boxes)) == boxes
输出结果如下:
在图片中画出边界框
def bbox_to_rect(bbox, color):
# 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
# ((左上x,左上y),宽,高)
return plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)
fig = plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));
关于fig.axes.add_patch这部分可参考该博客link.
输出结果如下:
二、锚框
2.1生成多个锚框
生成方法
假设输入图像的高度为
h
h
h,宽度为
w
w
w。我们以图像的每个像素为中心生成不同形状的锚框。
缩放比为(缩放比由我们自己指定的)
s
∈
(
0
,
1
]
s∈(0, 1]
s∈(0,1]
宽高比为(注意这里的宽高比是指锚框的高宽比,也是由我们自己指定)
r
>
0
r>0
r>0那么锚框的宽度和高度分别是
w
s
r
和
h
s
r
ws\sqrt{r}和{hs\over\sqrt{r}}
wsr和rhs(注意,在QA环节时,沐神说要把
s
s
s换成
s
\sqrt{s}
s,但是这样写也可以,无伤大雅吧应该。)。。。。但是我们应该要发现一个问题这里根据上述公式计算的锚框的宽度和高度之比为
w
r
h
{wr}\over{h}
hwr而不是
r
r
r这里表述相互矛盾了,但是在代码中还是按照锚框的宽度和高度之比为
r
r
r来进行计算的,我会在代码解释中指出来。
要生成多个不同形状的锚框,让我们设置许多缩放比(scale)取值
s
1
,
.
.
.
,
s
n
s_1,...,s_n
s1,...,sn和许多宽高比(aspect ratio)取值
r
1
,
.
.
.
,
r
m
r_1, ... , r_m
r1,...,rm当使用这些缩放比和长宽比的所有组合以每个像素为中心时,输入图像将总共拥有
w
h
n
m
whnm
whnm个锚框。这种情况下计算复杂性很容易过高。 在实践中,我们只考虑包含
s
1
s_1
s1或
r
1
r_1
r1的组合:
(
s
1
,
r
1
)
,
(
s
1
,
r
2
)
,
.
.
.
,
(
s
1
,
r
m
)
,
(
s
2
,
r
1
)
,
(
s
3
,
r
1
)
,
.
.
.
.
,
(
s
n
,
r
1
)
(s_1, r_1), (s_1, r_2), ... ,(s_1, r_m), (s_2, r_1), (s_3, r_1), .... , (s_n, r_1)
(s1,r1),(s1,r2),...,(s1,rm),(s2,r1),(s3,r1),....,(sn,r1)也就是说,以同一像素为中心的锚框的数量是n+m-1。对于整个输入图像,我们将共生成
w
h
(
n
+
m
−
1
)
wh(n+m-1)
wh(n+m−1)个锚框。
上述描述方法我们将在下面的multibox_prior函数中实现。实现之后首先我会先运行该函数看下效果,然后我会对程序内一些细节进行解释。
'''
输入参数:
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)
'''每一个像素对应的锚框数量'''
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轴上缩放步长
# 生成锚框的所有中心点(将所有中心点进行缩放处理,限制在(0,1)之间)
center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
'''
meshgrid函数只接受标量和一维向量输入
'''
shift_y, shift_x = torch.meshgrid(center_h, center_w)
'''
shift_y为所有锚框的y_中心,shift_x为所有锚框的x_中心
这里reshape(-1)操作相当于flatten操作
'''
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)
验证一下,看一下什么效果:
'''注意图片路径要换'''
img = plt.imread('../input/catdog/catdog.jpg')
h, w = img.shape[:2]
print(h, w)
'''产生[0,1)的均匀分布'''
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape
结果如下:
从结果中看出我们的图片宽度为561,图片高度为728,因此我们的像素像素点总共有561*728个,而我们的缩放比
s
s
s有3个,宽高比
r
r
r有3个,因此针对每一个 像素我们有
(
3
+
3
−
1
)
=
5
(3+3-1)=5
(3+3−1)=5个锚框,所以最后我们一共生成了
561
∗
728
∗
5
=
2042040
561*728*5=2042040
561∗728∗5=2042040个锚框。接下来讲一下代码大家普遍都觉得有点困扰的地方。首先看下这段代码,如下图所示:
首先我们计算锚框宽度和高度的公式为
w
s
r
和
h
s
r
ws\sqrt{r}和{hs\over\sqrt{r}}
wsr和rhs
按照该公式计算宽高比为
w
r
h
{wr}\over{h}
hwr而不是
r
r
r因此程序中(*in_height / in_width)操作就是将锚框宽高比限制为
r
r
r同时在该程序中是将猫狗图片高宽均缩放为1,因此在画图时还需要还原回原来的大小(这个我感觉没说清楚,看下面show_bboxes函数讲解)。
接下来这部分代码如下图所示:
因为在前面我们已经求出了中心坐标和各锚框高和宽,因此上述代码就是求方法1中的表示方式即由左上角坐标(
x
左
上
x_{左上}
x左上,
y
左
上
y_{左上}
y左上)和右下角坐标(
x
右
下
x_{右下}
x右下,
y
右
下
y_{右下}
y右下)进行表示。
对上述代码中一些函数使用方法参考博客链接附下:
repeat()方法:link.
repeat_interleave()方法:link.
该函数是对锚框进行展示:
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
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))
boxes = Y.reshape(h, w, 5, 4)
print("第(250,250)像素坐标第0个锚框坐标:", boxes[250, 250, 0, :])
set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = plt.imshow(img)
# 注意这部分代码乘bbox_scale就是前面对求锚框时对其进行了缩放处理,现在要放大为原来的样子
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'])
输出结果如下:
2.2 交并比(IOU)
计算方法
这是沐神参考资料里面的一张图,很直白,就是我们将一个像素的面积看做1,两个边界框的交并比就是两个边界框的相交面积除以两个边界框的面积的并集。
计算交并比的函数如下:
'''
算交并比
按照沐神该函数的写法,各参数含义如下:
boxes1:一张图片中的所有锚框
boxes2:一张图片中的所有真实边界框
返回值:
假设锚框有m个,真实边界框(其实就代表图片中我们要识别的物体个数)有n个,则返回值(交并比)的形状(shape)为(m,n),
第i行第j列的元素含义代表,第i个锚框与第j个真实边界框的交并比
'''
def box_iou(boxes1, boxes2):
"""计算两个锚框或边界框列表中成对的交并比"""
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) # 真实边界框面积
# inter_upperlefts,inter_lowerrights,inters的形状:
# (boxes1的数量,boxes2的数量,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)
# inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量)
inter_areas = inters[:, :, 0] * inters[:, :, 1]
union_areas = areas1[:, None] + areas2 - inter_areas
return inter_areas / union_areas
首先我们来调用一下该函数,看看效果。首先定义真实边界框和锚框,如下代码所示
'''
ground_truth代表真实边界框,其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的(x,y)轴坐标。
anchors表示我们自己构建的5个锚框,四个元素分别代表左上角和右下角的(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]])
fig = plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);
运行上述代码,在图片中显示我们真实的边界框和锚框,如下图所示:
其中标签’0’,‘1’,‘2’,‘3’,‘4’代表我们定义的锚框,标签‘dog’,‘cat’表示真实边界框。
接下来我们运行函数计算交并比的函数:
''' 按照我上面解释的参数含义,传入相关参数计算交并比 '''
box_iou(anchors, ground_truth[:, 1:])
最后输出结果如下图所示:
我们可以看出交并比的形状为(5,2),第一行第一列元素0.05代表,第一个锚框(即标签为‘0’)与第一个真实边界框(标签为‘dog’)的交并比为0.05,第一行第二列元素0.00代表第一个锚框(即标签为‘0’)与第二个真实边界框(标签为‘cat’)的交并比为0.00,即不相交,大家可以看看图片。其余元素类似含义。
接下来我们讲讲代码比较难理解的地方,如下图所示,这里的广播机制用的非常巧妙,广播机制介绍见该链接link.
这里我们首先来分析一下形状,在这里我们采用的例子中锚框的数量是5,因此有如下解释:
针对 inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])的分析:
boxes1[:, :2].shape ----> (5,2)
boxes1[:, None, :2].shape ----> (5,1,2),可以看出None的作用就是增加一个维度
boxes[:, :2].shape ----> (2,2)
torch.max(boxes1[:, None, :2], boxes[:, :2])中参数维度不一致,因此此时就会用到广播机制
根据广播机制规则,首先将boxes[:, :2]向左扩充一个维度使其形状变成(1,2,2),现在参数都变成了三维,一个为(5,1,2),一个为(1,2,2),
所以又根据广播规则最后两个形状都会变成(5,2,2)。
boxes1[:, None, :2]从形状(5,1,2)变成(5,2,2)后各元素的含义如下:
[[[第一个锚框x_左上,第一个锚框y_左上]
[第一个锚框x_左上,第一个锚框y_左上]]
[[第二个锚框x_左上,第二个锚框y_左上]
[第二个锚框x_左上,第二个锚框y_左上]]
........
[[第五个锚框x_左上,第五个锚框y_左上]
[第五个锚框x_左上,第五个锚框y_左上]]]
同理boxes[:, :2]从形状(2,2)变成(5,2,2)后各参数含义如下:
[[[第一个真实边界框x_左上,第一个真实边界框y_左上]
[第二个真实边界框x_左上,第二个真实边界框y_左上]]
[[第一个真实边界框x_左上,第一个真实边界框y_左上]
[第二个真实边界框x_左上,第二个真实边界框y_左上]]
........
[[第一个真实边界框x_左上,第一个真实边界框y_左上]
[第二个真实边界框x_左上,第二个真实边界框y_左上]]]
最后算出来的结果(inter_upperlefts)含义如下所示:
[[[第一个锚框与第一个真实边界框相交部分x_左上,第一个锚框与第一个真实边界框相交部分y_左上]
[第一个锚框与第二个真实边界框相交部分x_左上,第一个锚框与第二个真实边界框相交部分y_左上]]
[[第二个锚框与第一个真实边界框相交部分x_左上,第二个锚框与第一个真实边界框相交部分y_左上]
[第二个锚框与第二个真实边界框相交部分x_左上,第二个锚框与第二个真实边界框相交部分y_左上]]
........
[[第五个锚框与第一个真实边界框相交部分x_左上,第五个锚框与第一个真实边界框相交部分y_左上]
[第五个锚框与第二个真实边界框相交部分x_左上,第五个锚框与第二个真实边界框相交部分y_左上]]]
其实以上广播机制所做的操作与我下述代码进行的操作是一样的,大家可以看下:
# 重要。。。。。。。。。。。。。。。。。
a = torch.randint(0,10, (4,4))
#print(a)
a1 = a[:, None, :2]
a2 = a[:, None, :2].repeat_interleave(4, dim=1)
#print(a)
b = torch.randint(0,10, (4,4))
#print(b)
b1 = b[:, :2]
b2 = b[:, :2].unsqueeze(0).repeat(4,1,1)
#print(b)
torch.max(a1,b1) == torch.max(a2,b2)
其实大家将我讲的那部分代码弄懂了之后,后续代码一样的道理,可以自己体会一下。
2.3将真实边界框分给锚框
这部分解释可看我提供的资料链接,沐神解释的很清晰。这里就直接贴出源码。
'''
参数:
ground_truth:代表真实边界框,其中第一个元素是类别(0代表狗,1代表猫),其余四个元素是左上角和右下角的(x,y)轴坐标。
anchors:表示我们自己构建的5个锚框,四个元素分别代表左上角和右下角的(x, y)轴坐标
device:数据所在的设备
返回值:假设锚框有n,则返回值的shape为(n,),旗下表idx代表第(idx+1)个锚框,+1是因为数组下标起始为0,索引idx对应的值为value,则是将第(value+1)个真实边界框赋给第(idx+1)个锚框。
'''
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)
# 对于每个锚框,分配的真实边界框的张量
# 设索引为idx,代表第idx+1个锚框,索引idx对应的value(0或1)为value+1个真实边界框
anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
device=device)
# 根据阈值,决定是否分配真实边界框
# 每一行的最大值,和其对应的列索引
max_ious, indices = torch.max(jaccard, dim=1)
# 返回max_ious中大于0.5的元素索引的下标
anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1)
# 返回在jaccard中大于0.5的元素的列索引下标
box_j = indices[max_ious >= 0.5]
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):
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
运行上述代码。
assign_anchor_to_bbox(ground_truth[:, 1:], anchors, "cpu")
输出结果如下:
大家这张图可以对比我们在输出交并比结果那张图,两者是对应的。输出中0代表标签为“dog”的真实边界框,1代表标签为“cat”的真实边界框,-1代表没有分配真实边界框。
2.4标记类别和偏移量
这个我就直接截李沐老师的图了。代码附下。
# 计算偏移量
'''
输入参数:
anchors:锚框
assigned_bb:被分配的真实边界框
输出:偏移量
'''
def offset_boxes(anchors, assigned_bb, eps=1e-6):
"""对锚框偏移量的转换"""
c_anc = d2l.box_corner_to_center(anchors)
c_assigned_bb = d2l.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
如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为“背景”(background)。 背景类别的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。 我们使用真实边界框(labels参数)实现以下multibox_target函数,来标记锚框的类别和偏移量(anchors参数)。 此函数将背景类别的索引设置为零,然后将新类别的整数索引递增一。
这部分代码其实是前面代码的一个综合,代码如下:
'''
参数:
anchors:其shape为(1,anchor_num,4),第0维度的1是沐神在锚框函数 multibox_prior函数返回的时候加的
第1维度anchor_num表示一张图片中所有锚框数量,第2维度4采用的是(左上,右下)方法表示
labels:其shape为(batch_size, class_num, 5),第0维度为batch_size;class_num,表示该图片中标记物体数量;
第2维度第一个元素代表物体类别,这里用0,1,2...代表,后面是表示用(左上,右下)表示的真实边界款范围
返回值:
bbox_offset:其shape为(batch_size, ele_num),第0维度表示batch_size;第1维度表示每张图片的偏移量,4个元素为一组
每组为一个锚框对应的偏移量。ele_num = anchor_num*4
bbox_mask:其shape为(batch_size, ele_num),第0维度表示batch_size;第1维度每四个元素为一组表示该锚框是否为背景
若为背景,则四个元素均为0,否则均为1
class_labels:其shape为(anchor_num,),值为0表示背景,否则表示某类别的物体(例如等于1代表一种物体,等于2又代表一种物体)
在原labels中所有标签加了1。
'''
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]
for i in range(batch_size):
label = labels[i, :, :]
anchors_bbox_map = assign_anchor_to_bbox(
label[:, 1:], anchors, device)
bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
1, 4)
# 将类标签和分配的边界框坐标初始化为零
class_labels = torch.zeros(num_anchors, dtype=torch.long,
device=device)
assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
device=device)
# 使用真实边界框来标记锚框的类别。
# 如果一个锚框没有被分配,我们标记其为背景(值为零)
indices_true = torch.nonzero(anchors_bbox_map >= 0)
bb_idx = anchors_bbox_map[indices_true]
class_labels[indices_true] = label[bb_idx, 0].long() + 1
assigned_bb[indices_true] = label[bb_idx, 1:]
# 偏移量转换
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)
return (bbox_offset, bbox_mask, class_labels)
举例:
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]])
fig = plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);
输出结果:
labels = multibox_target(anchors.unsqueeze(dim=0),
ground_truth.unsqueeze(dim=0))
print("bbox_offset\n", labels[0])
print("bbox_mask\n", labels[1])
print("class_labels\n", labels[2])
输出结果(这部分具体结果解释可直接查看沐神的,我觉得挺清楚的):
上述就是对本小节难点代码的分析。到此结束。。