SSD目标检测算法

欢迎访问我的博客首页


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)84
第4次(19, 19, 1024)(19, 19, 1024)(19, 19, 4x)(19, 19, 21x)166
第5次(10, 10, 512)(10, 10, 512)(10, 10, 4x)(10, 10, 21x)306
第6次(5, 5, 256)(5, 5, 256)(5, 5, 4x)(5, 5, 21x)606
第7次(3, 3, 256)(3, 3, 256)(3, 3, 4x)(3, 3, 21x)1004
第8次(1, 1, 256)(1, 1, 256)(1, 1, 4x)(1, 1, 21x)3004

表   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 讲解
SSD目标检测算法(Single Shot MultiBox Detector)是一种单阶段的目标检测算法,它在2016年被提出,并在当时超越了当时最强的目标检测算法Faster RCNN的性能[^1]。SSD算法的主要思想是将多个不同尺度的特征图与预定义的一系列锚(anchor boxes)相结合,通过卷积操作同时进行目标类别的分类和边界的回归,从而实现目标的检测。 与Faster RCNN相比,SSD算法具有以下优势: 1. 小目标检测效果更好:SSD算法通过在不同尺度的特征图上进行检测,可以更好地适应不同大小的目标,提高小目标的检测效果。 2. 模型更小,检测速度更快:SSD算法是一个单阶段的目标检测算法,只需要进行一次前向传播,相比于Faster RCNN的两阶段检测,模型更小,检测速度更快。 SSD目标检测算法的基本流程如下: 1. 首先,SSD算法通过在输入图像上滑动不同尺度的滑动窗口,生成一系列锚。 2. 然后,将这些锚与预定义的一系列锚进行匹配,得到每个锚的类别和边界的预测。 3. 接下来,通过分类损失和边界回归损失来训练模型,使得模型能够准确地预测目标的类别和位置。 4. 最后,通过非极大值抑制算法来去除重叠的边界,得到最终的检测结果[^2]。 通过以上步骤,SSD目标检测算法能够在图像中准确地检测出目标的位置和类别,具有较好的性能和效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值