欢迎访问我的博客首页


1. 网络结构


  下面都是以输入尺寸 300 为例。

1. 网络结构图

SSD网络结构
图   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))

  计算交并比的步骤:

  1. 计算交集面积。如函数 intersect。
  2. 计算标注框与默认框的面积和,减去交集面积。第 21 行。
  3. 用第 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,这就说明第二个默认框是背景区域而不是标注区域。

5. 预测阶段的 nms


6. 参考


  1. SSD 论文
  2. SSD 讲解

更多推荐