欢迎访问我的博客首页。
1. 网络结构
下面都是以输入尺寸 300 为例。
1. 网络结构图
图
1
S
S
D
网
络
结
构
图\ 1\quad SSD 网络结构
图 1SSD网络结构
红色代表网络对目标的定位结果,绿色代表网络对目标的分类结果。蓝色代表默认框,默认框上红绿相间的长条代表该默认框与某个标注框重叠程度较好。
2. 网络输入输出
下采样次数 | 下采样的输出 | 检测网络的输入 分类网络的输入 | 检测网络的输出 | 分类网络的输出 | 原图上被检测区域的边长 | x的值 |
---|---|---|---|---|---|---|
第0次 | (300, 300, 64) | |||||
第1次 | (150, 150, 128) | |||||
第2次 | (76, 76, 256) | |||||
第3次 | (38, 38, 512) | (38, 38, 512) | (38, 38, 4x) | (38, 38, 21x) | 8 | 4 |
第4次 | (19, 19, 1024) | (19, 19, 1024) | (19, 19, 4x) | (19, 19, 21x) | 16 | 6 |
第5次 | (10, 10, 512) | (10, 10, 512) | (10, 10, 4x) | (10, 10, 21x) | 30 | 6 |
第6次 | (5, 5, 256) | (5, 5, 256) | (5, 5, 4x) | (5, 5, 21x) | 60 | 6 |
第7次 | (3, 3, 256) | (3, 3, 256) | (3, 3, 4x) | (3, 3, 21x) | 100 | 4 |
第8次 | (1, 1, 256) | (1, 1, 256) | (1, 1, 4x) | (1, 1, 21x) | 300 | 4 |
表 1 网 络 主 要 层 的 输 出 表\ 1 \quad 网络主要层的输出 表 1网络主要层的输出
x 代表在该尺度上,每个锚点产生的候选区数量。
2. 默认框
1. 默认框的产生
下面的代码根据输入图像的尺寸,生成8732个默认框的坐标,返回值output.shape=[8732, 4],output[i]=[cx, cy, w, h]。
import torch
import numpy as np
from math import sqrt as sqrt
class DefaultBox(object):
def __init__(self, cfg):
super(DefaultBox, self).__init__()
self.image_size = [300, 300]
self.feature_maps = [38, 19, 10, 5, 3, 1]
self.min_sizes = [30, 60, 111, 162, 213, 264]
self.max_sizes = [60, 111, 162, 213, 264, 315]
self.steps = [8, 16, 32, 64, 100, 300]
self.aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]],
self.clip = True
def forward(self):
mean = []
# 把原图划分成边长为38、19、10、5、3、1的栅格,然后以每个栅格为中心产生4或6个默认框。
for k, f in enumerate(self.feature_maps):
x, y = np.meshgrid(np.arange(f), np.arange(f)) # x.shape = y.shape = (38,38)。
x = x.reshape(-1)
y = y.reshape(-1)
for i, j in zip(y, x):
# 下面计算的坐标都是除以图像边长后的值,所以它们的值都在[0,1]之间。
# 1.栅格中心坐标。
cx = (j + 0.5) * self.steps[k] / self.image_size[1]
cy = (i + 0.5) * self.steps[k] / self.image_size[0]
# 2.一个小正方形的边长。
s_k = self.min_sizes[k] / self.image_size[0]
mean += [cx, cy, s_k, s_k]
# 3.一个大正方形的边长。
s_k_prime = sqrt(s_k * (self.max_sizes[k] / self.image_size[0]))
mean += [cx, cy, s_k_prime, s_k_prime]
# 4.二或四个长方形的边长。
for ar in self.aspect_ratios[k]:
mean += [cx, cy, s_k * sqrt(ar), s_k / sqrt(ar)]
mean += [cx, cy, s_k / sqrt(ar), s_k * sqrt(ar)]
# 把8732个默认框按顺序存放。
output = torch.Tensor(mean).view(-1, 4)
# 确保坐标值在[0,1]内。
if self.clip:
output.clamp_(max=1, min=0)
return output
输入的图像最好是正方形的,因为第 29、32 行产生正方形默认框的边长时,仅根据图像的一个边长计算。由第 26、27、29、32 行可以看出,默认框的边长都除以图像边长,所以默认框的坐标值在 0 到 1 之间。
2. 默认框的作用
首先,默认框和网络输出是一一对应的,如图 1。默认框和网络输出都有 6 个尺度。在每个尺度上,网络的输出是从上到下、从左到右卷积产生预测结果,在每个感受野产生 4 或 6 个预测;默认框的产生也是上到下、从左到右,在每个栅格产生 4 或 6 个默认框。它们最终都被 reshape 为 [8732, 4] 。
然后,默认框充当一个桥梁作用:使用 match 函数找到每个默认框代表标注的是目标还是背景,也就知道网络的每个输出代表的是目标还是背景。
3. 计算交并比
计算标注框与每个默认框的交并比。下面以计算 3 个标注框与 4 个默认框的交并比为例。
蓝色区域代表默认框,红色框代表标注框。这里的默认框和 SSD 的默认框不一样,SSD 的默认框排列紧密且有重合,这里做了简化。
import torch
def intersect(box_a, box_b):
# 求交集(重叠区域)的面积。
num_annotate_box = box_a.size(0)
num_default_box = box_b.size(0)
min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(num_annotate_box, num_default_box, 2),
box_b[:, :2].unsqueeze(0).expand(num_annotate_box, num_default_box, 2))
max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(num_annotate_box, num_default_box, 2),
box_b[:, 2:].unsqueeze(0).expand(num_annotate_box, num_default_box, 2))
# 把相减得到的tensor的小于0的元素换成0
intersection = torch.clamp((max_xy - min_xy), min=0)
# 计算n个默认框和8732个标注框的重合面积
return intersection[:, :, 0] * intersection[:, :, 1]
def unite(box_a, box_b):
# 求并集的面积。
intersection = intersect(box_a, box_b)
# 计算默认框和标注框各自的面积。
area_a = ((box_a[:, 2] - box_a[:, 0]) * (box_a[:, 3] - box_a[:, 1])).unsqueeze(1).expand_as(intersection)
area_b = ((box_b[:, 2] - box_b[:, 0]) * (box_b[:, 3] - box_b[:, 1])).unsqueeze(0).expand_as(intersection)
union_set = area_a + area_b - intersection
return union_set
def iou(annotate_box, default_box):
intersection = intersect(annotate_box, default_box)
union_set = unite(annotate_box, default_box)
return intersection / union_set
函数 intersect 计算两个矩形交集的面积,也就是重合区域的面积。如果没有重合,交集的面积为 0。函数 unite 计算两个矩形并集的面积,并集的面积等于各自的面积相加减去交集的面积。如果没有重合,并集面积等于面积和。
测试程序如下,输出结果如上图。
if __name__ == '__main__':
# (xmin, ymin, xmax, ymax)
annotate_box = torch.tensor([[10, 10, 100, 100], [20, 190, 120, 290], [190, 90, 290, 290]], dtype=torch.float32)
default_box = torch.tensor([[0, 0, 100, 100], [200, 0, 300, 100], [0, 200, 100, 300], [200, 200, 300, 300]],
dtype=torch.float32)
print('------交集------')
print(intersect(annotate_box, default_box))
print('------并集------')
print(unite(annotate_box, default_box))
print('-----交并比-----')
print(iou(annotate_box, default_box))
计算交并比的步骤:
- 计算交集面积。如函数 intersect。
- 计算标注框与默认框的面积和,减去交集面积。第 21 行。
- 用第 2 步的结果减去第 1 步的结果,除以第 2 步的结果。第 23 行。
还可以直接使用 torchvision.ops 中定义的 box_iou 函数,效果和上面的计算结果是一样的:
from torchvision.ops import box_iou
iou = box_iou(annotate_box, default_box)
4. 默认框的匹配
为每个默认框找出重合程度最好标注框的坐标和类别。根据第 8 行,假如一个默认框与任何标注框都没有交集,则它的最好标注框是第一个标注框 annotate_box[0]。
def match(idx, annotate_box, annotate_category, defaults, best_annotate_box, best_annotate_category, threshold=0.5):
# 1.计算重合度(交并比)。
overlaps = iou(annotate_box, defaults)
# 2.标注框的最好默认框下标。
_, best_default_box_idx_of_annotate_box = overlaps.max(1, keepdim=True)
best_default_box_idx_of_annotate_box.squeeze_(1) # [0,2,3]
# 3.默认框的最好重合度和最好标注框下标。
best_overlap_of_default_box, best_annotate_box_idx_of_default_box = overlaps.max(0, keepdim=True)
best_overlap_of_default_box.squeeze_(0) # [0.8100, 0.0309, 0.5625, 0.3699]
best_annotate_box_idx_of_default_box.squeeze_(0) # [0, 2, 1, 2]
# 4.重点默认框(该默认框是某个标注框的最好默认框)。
# 4.1把重点默认框的重合度设置为2。
best_overlap_of_default_box.index_fill_(0, best_default_box_idx_of_annotate_box, 2) # [2.0, 0.0309, 2.0, 2.0]
# 4.2为重点默认框分配合适的标注框
for i in range(best_default_box_idx_of_annotate_box.size(0)):
best_annotate_box_idx_of_default_box[best_default_box_idx_of_annotate_box[i]] = i # [0, 2, 1, 2]
# 5.默认框的最好标注框坐标和类别。
best_annotate_box_of_default_box = annotate_box[best_annotate_box_idx_of_default_box] # [[an0],[an2],[an1],[an2]]
best_annotate_category_of_default = annotate_category[best_annotate_box_idx_of_default_box] + 1 # [1, 3, 2, 3]
best_annotate_category_of_default[best_overlap_of_default_box < threshold] = 0 # [1, 0, 2, 3]
# 6.放入best_annotate_box和best_annotate_category。
best_annotate_box[idx] = best_annotate_box_of_default_box
best_annotate_category[idx] = best_annotate_category_of_default
其中 [[an0],[an2],[an1],[an2]] = [[10., 10., 100., 100.], [190., 90., 290., 290.], [20., 190., 120., 290.], [190., 90., 290., 290.]],即每个默认框的标注框坐标。需要注意的是,我们这里返回的 best_annotate_box[idx] 的坐标形式是 (xmin, ymin, xmax, ymax),实际中可以要返回它相对于默认框的偏移:
def encode(matched, defaults, variances):
g_cxcy = (matched[:, :2] + matched[:, 2:]) / 2 - defaults[:, :2]
g_cxcy /= (variances[0] * defaults[:, 2:])
g_wh = (matched[:, 2:] - matched[:, :2]) / defaults[:, 2:]
g_wh = torch.log(g_wh) / variances[1]
return torch.cat([g_cxcy, g_wh], 1)
测试程序如下。如第 4 行,我们假设标注框 i 的类型为 i,根据 match 函数的第 19 行,标注框 i 的类别被视为 i+1。
if __name__ == '__main__':
# (xmin, ymin, xmax, ymax)
annotate_box = torch.tensor([[10, 10, 100, 100], [20, 190, 120, 290], [190, 90, 290, 290]], dtype=torch.float32)
annotate_category = torch.tensor([0, 1, 2])
default_box = torch.tensor([[0, 0, 100, 100], [200, 0, 300, 100], [0, 200, 100, 300], [200, 200, 300, 300]],
dtype=torch.float32)
batch_size = 1
num_default = default_box.size(0)
bestGtLocationBS = torch.Tensor(batch_size, num_default, 4)
bestGtCategoryBS = torch.LongTensor(batch_size, num_default)
for index in range(batch_size):
match(index, annotate_box, annotate_category, default_box, bestGtLocationBS, bestGtCategoryBS)
print(bestGtLocationBS)
print(bestGtCategoryBS)
测试程序的输出结果:
tensor([[[ 10., 10., 100., 100.],
[190., 90., 290., 290.],
[ 20., 190., 120., 290.],
[190., 90., 290., 290.]]])
tensor([[1, 0, 2, 3]])
分析:从输出来看,虽然计算得到的第二个默认框的最好标注框坐标是 [190., 90., 290., 290.],但它的类别为 0,这就说明第二个默认框是背景区域而不是标注区域。