欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

目标检测(一):边界框、锚框、多尺度目标检测

程序员文章站 2022-03-30 09:49:35
本文参考–PyTorch官方教程中文版链接:http://pytorch123.com/FirstSection/PyTorchIntro/Pytorch中文文档:https://pytorch-cn.readthedocs.io/zh/latest/package_references/Tensor/PyTorch英文文档:https://pytorch.org/docs/stable/tensors.html《深度学习之PyTorch物体检测实战》《动手学深度学习》代码参考:Dive-into-...

本文参考–PyTorch官方教程中文版链接:http://pytorch123.com/FirstSection/PyTorchIntro/
Pytorch中文文档:https://pytorch-cn.readthedocs.io/zh/latest/package_references/Tensor/
PyTorch英文文档:https://pytorch.org/docs/stable/tensors.html
《深度学习之PyTorch物体检测实战》《动手学深度学习》

代码参考:Dive-into-DL-PyTorch

本篇博客的jupyter notebook文件:
链接:https://pan.baidu.com/s/1Wdomkf2TX2YpLh82v1b17g
提取码:liau

默认加载以下模块:

%matplotlib inline
import os
import json
from PIL import Image
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
import math
from IPython import display

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
import torchvision
from torchvision import models
from torch.utils.data import Dataset
from torchvision import transforms
from torch.utils.data import DataLoader
import visdom
# from tensorboardX import SummaryWriter
from torch.utils.tensorboard import SummaryWriter

为了更清晰的显示图像,还要运行以下程序:

def use_svg_display():
    """Use svg format to display plot in jupyter"""
    display.set_matplotlib_formats('svg')

def set_figsize(figsize=(3.5, 2.5)):
    use_svg_display()
    # 设置图的尺寸
    plt.rcParams['figure.figsize'] = figsize
    
set_figsize()

实验图片:

img = Image.open('catdog.jpg')
plt.imshow(img);

目标检测(一):边界框、锚框、多尺度目标检测

边界框 bounding box

在目标检测里,我们通常使用边界框(bounding box)来描述目标位置。边界框是一个矩形框,可以由矩形左上角的 x 和 y 轴坐标与右下角的 x 和 y 轴坐标确定。

# bbox是bounding box的缩写
dog_bbox, cat_bbox = [60, 45, 378, 516], [400, 112, 655, 493]
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'));

目标检测(一):边界框、锚框、多尺度目标检测

def xy_to_cxcy(xy):
    """
    将(x_min, y_min, x_max, y_max)形式的anchor转换成(center_x, center_y, w, h)形式的.
    https://github.com/sgrvinod/a-PyTorch-Tutorial-to-Object-Detection/blob/master/utils.py
    Args:
        xy: bounding boxes in boundary coordinates, a tensor of size (n_boxes, 4)
    Returns: 
        bounding boxes in center-size coordinates, a tensor of size (n_boxes, 4)
    """
    return torch.cat([(xy[:, 2:] + xy[:, :2]) / 2,  # c_x, c_y
                      xy[:, 2:] - xy[:, :2]], 1)  # w, h

锚框 Anchor box

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。不同的模型使用的区域采样方法可能不同。这里我们介绍其中的一种方法:它以每个像素为中心生成多个大小和宽高比(aspect ratio)不同的边界框。这些边界框被称为锚框(anchor box)

生成多个锚框

假设输入图像高为hh,宽为ww。我们分别以图像的每个像素为中心生成不同形状的锚框。设大小为ss∈(0,1]且宽高比为rr>0,那么锚框的宽和高将分别为wsrws\sqrt rhs/rhs/\sqrt r。当中心位置给定时,已知宽和高的锚框是确定的。

下面我们分别设定好一组大小s1,,sns_1,…,s_n和一组宽高比r1,,rmr_1,…,r_m。如果以每个像素为中心时使用所有的大小与宽高比的组合,输入图像将一共得到whnmwhnm个锚框。虽然这些锚框可能覆盖了所有的真实边界框,但计算复杂度容易过高。因此,我们通常只对包含s1s_1r1r_1的大小与宽高比的组合感兴趣,即
(s1,r1),(s1,r2),,(s1,rm),(s2,r1),(s3,r1),,(sn,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)也就是说,以相同像素为中心的锚框的数量为n+m1n+m−1。对于整个输入图像,我们将一共生成wh(n+m1)wh(n+m−1)个锚框。

  • 生成图像上所有像素点的锚框:
def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):
    """
    # anchor表示成(xmin, ymin, xmax, ymax).
    
    Args:
        feature_map: torch tensor, Shape: [N, C, H, W].
        sizes: List of sizes (0~1) of generated MultiBoxPriores. 
        ratios: List of aspect ratios (non-negative) of generated MultiBoxPriores. 
    Returns:
        anchors of shape (1, num_anchors, 4). 由于batch里每个都一样, 所以第一维为1
    """
    pairs = [] # pair of (size, sqrt(ration))
    for r in ratios:
        pairs.append([sizes[0], math.sqrt(r)])
    for s in sizes[1:]:
        pairs.append([s, math.sqrt(ratios[0])])
    pairs = np.array(pairs)
    
    ss1 = pairs[:, 0] * pairs[:, 1] # size * sqrt(ration)
    ss2 = pairs[:, 0] / pairs[:, 1] # size / sqrt(ration)
    
    # 左边界、上边界、右边界、下边界
    base_anchors = np.stack([-ss1, -ss2, ss1, ss2], axis=1) / 2 # 形状为(n+m-1,4)
    
    # 对图像中的所有像素点生成锚框
    h, w = feature_map.shape[-2:]
    shifts_x = np.arange(0, w) / w # 像素点的坐标范围为0~1
    shifts_y = np.arange(0, h) / h
    shift_x, shift_y = np.meshgrid(shifts_x, shifts_y)
    shift_x = shift_x.reshape(-1)
    shift_y = shift_y.reshape(-1)
    shifts = np.stack((shift_x, shift_y, shift_x, shift_y), axis=1) # 形状为(h*w,4)
    
    # 利用广播将每个像素点的坐标与基础锚框的宽高偏移相加,得到每个像素点的锚框
    # 最后得到的锚框坐标值范围均为0~1,再分别乘上w和h即可得到实际的像素位置
    # 形状为(h*w, n+m-1, 4)
    anchors = shifts.reshape((-1, 1, 4)) + base_anchors.reshape((1, -1, 4))
    
    return torch.tensor(anchors, dtype=torch.float32).view(1, -1, 4)
w, h = img.size
print("w = %d, h = %d" % (w, h))
X = torch.Tensor(1, 3, h, w)  # 构造输入数据
boxes = MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
boxes = boxes.view(h, w, -1, 4)
boxes.shape

output:

torch.Size([561, 728, 5, 4])
  • 描绘图像中以某个像素为中心的所有锚框:
def show_bboxes(axes, bboxes, labels=None, colors=['b', 'g', 'r', 'm', 'c']):
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = bbox_to_rect(bbox.detach().cpu().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=6, color=text_color,
                      bbox=dict(facecolor=color, lw=0))
fig = plt.imshow(img)
bbox_scale = torch.Tensor([[w, h, w, h]]) # 因为MultiBoxPrior生成的锚框坐标值是0`1,因此这里乘上宽高来恢复其原来的坐标位置
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.75, r=2', 's=0.75, r=0.5', 's=0.5, r=1', 's=0.25, r=1'])

目标检测(一):边界框、锚框、多尺度目标检测

交并比 Intersection over Union, IoU

交并比:两个边界框相交面积与相并面积之比,用来衡量两个边界框的相似度
目标检测(一):边界框、锚框、多尺度目标检测
下面的函数计算所有锚框与所有真实边界框的IoU,返回一个IoU矩阵:

def compute_intersection(set_1, set_2):
    """
    计算anchor之间的交集
    Args:
        set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
        set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
    Returns:
        intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
    """
    # PyTorch auto-broadcasts singleton dimensions
    # 形状:(n1, n2, 2) 计算left_max和top_max
    # set_1[:, :2].unsqueeze(1):(n1, 1, 2)
    # set_2[:, :2].unsqueeze(0):(1, n2, 2)
    # 经过广播之后它们的形状就都变成了(n1, n2, 2),进而进行element-wise maximum
    lower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0))  
    # 形状:(n1, n2, 2) 计算right_min和bottom_min
    upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0))  
    
    intersection_dims = torch.clamp(upper_bounds - lower_bounds, min=0)  # (n1, n2, 2)
    return intersection_dims[:, :, 0] * intersection_dims[:, :, 1]  # (n1, n2)


def compute_jaccard(set_1, set_2):
    """
    计算anchor之间的Jaccard系数(IoU)
    Args:
        set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
        set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
    Returns:
        Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
    """
    # Find intersections
    intersection = compute_intersection(set_1, set_2)  # (n1, n2)

    # Find areas of each box in both sets
    areas_set_1 = (set_1[:, 2] - set_1[:, 0]) * (set_1[:, 3] - set_1[:, 1])  # (n1)
    areas_set_2 = (set_2[:, 2] - set_2[:, 0]) * (set_2[:, 3] - set_2[:, 1])  # (n2)

    # Find the union
    # PyTorch auto-broadcasts singleton dimensions
    union = areas_set_1.unsqueeze(1) + areas_set_2.unsqueeze(0) - intersection  # (n1, n2)

    return intersection / union  # (n1, n2)

利用锚框进行目标检测的思路

在训练集中,我们将每个锚框视为一个训练样本。为了训练目标检测模型,我们需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。在目标检测时,我们首先生成多个锚框,然后为每个锚框预测类别以及偏移量,接着根据预测的偏移量调整锚框位置从而得到预测边界框,最后筛选需要输出的预测边界框

标注训练集的锚框

为锚框分配真实边界框

我们知道,在目标检测的训练集中,每个图像已标注了真实边界框的位置以及所含目标的类别。在生成锚框之后,我们主要依据与锚框相似的真实边界框的位置和类别信息为锚框标注。那么,该如何为锚框分配与其相似的真实边界框呢?

假设图像中锚框分别为A1,A2,,AnaA_1,A_2,…,A_{n_a},真实边界框分别为B1,B2,,BnbB_1,B_2,…,B_{n_b},且nanbn_a≥n_b。定义矩阵XRna×nb\boldsymbol{X} \in \mathbb{R}^{n_a \times n_b},其中第ii行第jj列的元素xijx_{ij}为锚框AiA_i与真实边界框BjB_j的交并比。 首先,我们找出矩阵X\boldsymbol{X}中最大元素,并将该元素的行索引与列索引分别记为i1i_1,j1j_1。我们为锚框Ai1A_{i_1}分配真实边界框Bj1B_{j_1}。显然,锚框Ai1A_{i_1}和真实边界框Bj1B_{j_1}在所有的“锚框—真实边界框”的配对中相似度最高。接下来,将矩阵X\boldsymbol{X}中第i1i_1行和第j1j_1列上的所有元素丢弃。找出矩阵X\boldsymbol{X}中剩余的最大元素,并将该元素的行索引与列索引分别记为i2i_2,j2,j_2。我们为锚框Ai2A_{i_2}分配真实边界框Bj2B_{j_2},再将矩阵X\boldsymbol{X}中第i2i_2行和第j2j_2列上的所有元素丢弃。此时矩阵X\boldsymbol{X}中已有2行2列的元素被丢弃。 依此类推,直到矩阵X\boldsymbol{X}中所有nbn_b列元素全部被丢弃。这个时候,我们已为nbn_b个锚框各分配了一个真实边界框。 接下来,我们只遍历剩余的nanbn_a - n_b个锚框:给定其中的锚框AiA_i,根据矩阵X\boldsymbol{X}的第ii行找到与AiA_i交并比最大的真实边界框BjB_j,且只有当该交并比大于预先设定的阈值(默认为0.5)时,才为锚框AiA_i分配真实边界框BjB_j

def assign_anchor(bb, anchor, jaccard_threshold=0.5):
    """
    # 为每个anchor分配真实的bb, anchor表示成归一化(xmin, ymin, xmax, ymax).
    
    Args:
        bb: 真实边界框(bounding box), shape:(nb, 4)
        anchor: 待分配的anchor, shape:(na, 4)
        jaccard_threshold: 预先设定的阈值
    Returns:
        assigned_idx: shape: (na, ), 每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1
    """
    na = anchor.shape[0]
    nb = bb.shape[0]
    jaccard = compute_jaccard(anchor, bb).detach().cpu().numpy() # shape: (na, nb)
    assigned_idx = np.ones(na) * -1  # 初始全为-1
    
    # 先为每个bb分配一个anchor(不要求满足jaccard_threshold)
    jaccard_cp = jaccard.copy()
    for j in range(nb):
        i = np.argmax(jaccard_cp[:, j])
        assigned_idx[i] = j
        jaccard_cp[i, :] = float("-inf") # 赋值为负无穷, 相当于去掉这一行
     
    # 处理还未被分配的anchor, 要求满足jaccard_threshold
    for i in range(na):
        if assigned_idx[i] == -1:
            j = np.argmax(jaccard[i, :])
            if jaccard[i, j] >= jaccard_threshold:
                assigned_idx[i] = j
    
    return torch.tensor(assigned_idx, dtype=torch.long)

标注锚框的类别和偏移量

现在我们可以标注锚框的类别和偏移量了。如果一个锚框AA被分配了真实边界框BB,将锚框AA的类别设为BB的类别,并根据BBAA的中心坐标的相对位置以及两个框的相对大小为锚框AA标注偏移量。由于数据集中各个框的位置和大小各异,因此这些相对位置和相对大小通常需要一些特殊变换,才能使偏移量的分布更均匀从而更容易拟合。设锚框AA及其被分配的真实边界框BB的中心坐标分别为(xa,ya)(x_a, y_a)(xb,yb)(x_b, y_b)AABB的宽分别为waw_awbw_b,高分别为hah_ahbh_b,一个常用的技巧是将AA的偏移量标注为

(xbxawaμxσx,ybyahaμyσy,logwbwaμwσw,loghbhaμhσh)\left( \frac{ \frac{x_b - x_a}{w_a} - \mu_x }{\sigma_x}, \frac{ \frac{y_b - y_a}{h_a} - \mu_y }{\sigma_y}, \frac{ \log \frac{w_b}{w_a} - \mu_w }{\sigma_w}, \frac{ \log \frac{h_b}{h_a} - \mu_h }{\sigma_h}\right)其中常数的默认值为μx=μy=μw=μh=0,σx=σy=0.1,σw=σh=0.2\mu_x = \mu_y = \mu_w = \mu_h = 0, \sigma_x=\sigma_y=0.1, \sigma_w=\sigma_h=0.2。如果一个锚框没有被分配真实边界框,我们只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框(negative anchor boxes),其余则被称为正类锚框(positive anchor boxes)。

def MultiBoxTarget(anchor, label):
    """
    # 为所有生成的锚框标注类别和偏移量,anchor表示成归一化(xmin, ymin, xmax, ymax).

    Args:
        anchor: torch tensor, 输入的锚框, 一般是通过MultiBoxPrior生成, shape:(1,锚框总数,4)
        label: 真实标签, shape为(bn, 每张图片最多的真实锚框数, 5)
               第二维中,如果给定图片没有这么多锚框, 可以先用-1填充空白, 最后一维中的元素为[类别标签, 四个坐标值]
    Returns:
        列表, [bbox_offset, bbox_mask, cls_labels]
        bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4)
        bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1
        cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)
    """
    assert len(anchor.shape) == 3 and len(label.shape) == 3
    bn = label.shape[0] # 图片数量
    
    def MultiBoxTarget_one(anc, lab, eps=1e-6):
        """
        MultiBoxTarget函数的辅助函数, 处理batch中的一个
        Args:
            anc: shape of (锚框总数, 4)
            lab: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值]
            eps: 一个极小值, 防止log0
        Returns:
            offset: (锚框总数*4, ) 每个锚框的偏移量
            bbox_mask: (锚框总数*4, ), 0代表未分配真实边界框,1表示分配了真实边界框
            cls_labels: (锚框总数), 0代表背景
        """
        an = anc.shape[0] # 锚框总数
        assigned_idx = assign_anchor(lab[:, 1:], anc) # (锚框总数, ) 为锚框分配真实边界框,-1表示未被分配
        bbox_mask = ((assigned_idx >= 0).float().unsqueeze(-1)).repeat(1, 4) # (锚框总数, 4)

        cls_labels = torch.zeros(an, dtype=torch.long) # 0表示背景
        assigned_bb = torch.zeros((an, 4), dtype=torch.float32) # 所有anchor对应的bb坐标,未分配即为0
        for i in range(an):
            bb_idx = assigned_idx[i] # 锚框所属的真实边界框的序号
            if bb_idx >= 0: # 即非背景
                cls_labels[i] = lab[bb_idx, 0].long().item() + 1 # 注意要加一
                assigned_bb[i, :] = lab[bb_idx, 1:]

        center_anc = xy_to_cxcy(anc) # (center_x, center_y, w, h)
        center_assigned_bb = xy_to_cxcy(assigned_bb)

        offset_xy = 10.0 * (center_assigned_bb[:, :2] - center_anc[:, :2]) / center_anc[:, 2:]
        offset_wh = 5.0 * torch.log(eps + center_assigned_bb[:, 2:] / center_anc[:, 2:])
        offset = torch.cat([offset_xy, offset_wh], dim = 1) * bbox_mask # (锚框总数, 4)

        return offset.view(-1), bbox_mask.view(-1), cls_labels
    
    batch_offset = []
    batch_mask = []
    batch_cls_labels = []
    for b in range(bn):
        offset, bbox_mask, cls_labels = MultiBoxTarget_one(anchor[0, :, :], label[b, :, :])
        
        batch_offset.append(offset)
        batch_mask.append(bbox_mask)
        batch_cls_labels.append(cls_labels)
    
    bbox_offset = torch.stack(batch_offset)
    bbox_mask = torch.stack(batch_mask)
    cls_labels = torch.stack(batch_cls_labels)
    
    return [bbox_offset, bbox_mask, cls_labels]

假设真实边界框与生成的锚框如下所示:

bbox_scale = torch.Tensor([[w, h, w, h]]) # 因为MultiBoxPrior生成的锚框坐标值是0`1,因此这里乘上宽高来恢复其原来的坐标位置
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 = MultiBoxTarget(anchors.unsqueeze(dim=0),
                        ground_truth.unsqueeze(dim=0))

bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4)

print(labels[0]) 
print(labels[0].shape)

output:

tensor([[-0.0000e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00,  1.4000e+00,
          1.0000e+01,  2.5940e+00,  7.1754e+00, -1.2000e+00,  2.6882e-01,
          1.6824e+00, -1.5655e+00, -0.0000e+00, -0.0000e+00, -0.0000e+00,
         -0.0000e+00, -5.7143e-01, -1.0000e+00,  4.1723e-06,  6.2582e-01]])
torch.Size([1, 20])

bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1。由于我们不关心对背景的检测,有关负类的偏移量不应影响目标函数。通过按元素乘法,掩码变量中的0可以在计算目标函数之前过滤掉负类的偏移量。

print(labels[1])  
print(labels[1].shape)

output:

tensor([[0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 1.,
         1., 1.]])
torch.Size([1, 20])

cls_labels:每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)

print(labels[2]) 
print(labels[2].shape)

output:

tensor([[0, 1, 2, 0, 2]])
torch.Size([1, 5])

输出预测边界框

在模型预测阶段,我们先为图像生成多个锚框,并为这些锚框一一预测类别和偏移量。随后,我们根据锚框及其预测偏移量得到预测边界框。当锚框数量较多时,同一个目标上可能会输出较多相似的预测边界框。为了使结果更加简洁,我们可以移除相似的预测边界框。常用的方法叫作非极大值抑制(non-maximum suppression,NMS

NMS:对于一个预测边界框 BB ,模型会计算各个类别的预测概率。设其中最大的预测概率为 pp ,该概率所对应的类别即 BB的预测类别。我们也将 pp 称为预测边界框 B 的置信度。在同一图像上,我们将预测类别非背景的预测边界框按置信度从高到低排序,得到列表 LL。从 LL 中选取置信度最高的预测边界框 B1B_1 作为基准,将所有与 B1B_1 的交并比大于某阈值(默认为0.5)的非基准预测边界框从 LL 中移除。这里的阈值是预先设定的超参数。此时, LL 保留了置信度最高的预测边界框并移除了与其相似的其他预测边界框。 接下来,从 L 中选取置信度第二高的预测边界框 B2B_2 作为基准,将所有与 B2B_2的交并比大于某阈值的非基准预测边界框从 LL 中移除。重复这一过程,直到 LL 中所有的预测边界框都曾作为基准。此时LL 中任意一对预测边界框的交并比都小于阈值。最终,输出列表 LL 中的所有预测边界框

实践中,我们可以在执行非极大值抑制前将置信度较低的预测边界框移除,从而减小非极大值抑制的计算量。我们还可以筛选非极大值抑制的输出,例如,只保留其中置信度较高的结果作为最终输出

from collections import namedtuple
Pred_BB_Info = namedtuple("Pred_BB_Info", ["index", "class_id", "confidence", "xyxy"])

def non_max_suppression(bb_info_list, nms_threshold = 0.5):
    """
    非极大抑制处理预测的边界框
    Args:
        bb_info_list: Pred_BB_Info的列表, 包含预测类别、置信度等信息
        nms_threshold: 阈值
    Returns:
        output: Pred_BB_Info的列表, 只保留过滤后的边界框信息
    """
    output = []
    # 先根据置信度从高到低排序
    sorted_bb_info_list = sorted(bb_info_list, key = lambda x: x.confidence, reverse=True)

    while len(sorted_bb_info_list) != 0:
        best = sorted_bb_info_list.pop(0)
        output.append(best) # 将置信度最高的锚框加入输出
        
        if len(sorted_bb_info_list) == 0:
            break

        bb_xyxy = []
        for bb in sorted_bb_info_list:
            bb_xyxy.append(bb.xyxy)
        
        # 计算置信度最高的锚框与其他锚框的交并比
        iou = compute_jaccard(torch.tensor([best.xyxy]), 
                              torch.tensor(bb_xyxy))[0] # shape: (len(sorted_bb_info_list), )
        
        # 删去IoU大于阈值的锚框
        n = len(sorted_bb_info_list)
        sorted_bb_info_list = [sorted_bb_info_list[i] for i in range(n) if iou[i] <= nms_threshold]
    return output
def MultiBoxDetection(cls_prob, loc_pred, anchor, nms_threshold = 0.5):
    """
    利用非极大值抑制来输出预测边界框
    # anchor表示成归一化(xmin, ymin, xmax, ymax).

    Args:
        cls_prob: 经过softmax后得到的各个锚框的预测概率, shape:(bn, 预测总类别数+1, 锚框个数),类别数加1是因为加上了背景的概率
        loc_pred: 预测的各个锚框的偏移量, shape:(bn, 锚框个数*4)
        anchor: MultiBoxTarget输出的分配了真实边界框的默认锚框, shape: (1, 锚框个数, 4)
        nms_threshold: 非极大抑制中的阈值
    Returns:
        所有锚框的信息, shape: (bn, 锚框个数, 6)
        每个锚框信息由[class_id, confidence, xmin, ymin, xmax, ymax]表示
        class_id=-1 表示背景或在非极大值抑制中被移除了
    """
    assert len(cls_prob.shape) == 3 and len(loc_pred.shape) == 2 and len(anchor.shape) == 3
    bn = cls_prob.shape[0] # batch size
    
    def MultiBoxDetection_one(c_p, l_p, anc, nms_threshold = 0.5):
        """
        MultiBoxDetection的辅助函数, 处理batch中的一个
        Args:
            c_p: (预测总类别数+1, 锚框个数) 经过softmax后得到的各个锚框的预测概率
            l_p: (锚框个数*4, ) 预测的各个锚框的偏移量
            anc: (锚框个数, 4) MultiBoxTarget输出的分配了真实边界框的默认锚框
            nms_threshold: 非极大抑制中的阈值
        Return:
            output: (锚框个数, 6)
            每个锚框信息由[class_id, confidence, xmin, ymin, xmax, ymax]表示
            class_id=-1 表示背景或在非极大值抑制中被移除了
        """
        pred_bb_num = c_p.shape[1] # 筛选前的预测边界框个数
        # 将默认锚框加上预测偏移量,得到未筛选的预测边界框
        anc = (anc + l_p.view(pred_bb_num, 4)).detach().cpu().numpy() 
        
        # (锚框个数, ) 得到每个锚框的置信度以及所属类别
        confidence, class_id = torch.max(c_p, 0)
        confidence = confidence.detach().cpu().numpy()
        class_id = class_id.detach().cpu().numpy()
        
        # 将所有锚框的信息打包成有名元组
        pred_bb_info = [Pred_BB_Info(
                            index = i,
                            class_id = class_id[i] - 1, # 正类label从0开始
                            confidence = confidence[i],
                            xyxy=[*anc[i]]) # xyxy是个列表
                        for i in range(pred_bb_num)]
        
        # 保存筛选后的预测边界框的index
        obj_bb_idx = [bb.index for bb in non_max_suppression(pred_bb_info, nms_threshold)]
        
        output = []
        for bb in pred_bb_info:
            output.append([
                (bb.class_id if bb.index in obj_bb_idx else -1.0),
                bb.confidence,
                *bb.xyxy
            ])
            
        return torch.tensor(output) # shape: (锚框个数, 6)
    
    batch_output = []
    for b in range(bn):
        batch_output.append(MultiBoxDetection_one(cls_prob[b], loc_pred[b], anchor[0], nms_threshold))
    
    return torch.stack(batch_output)

下面来看一个具体的例子。先构造4个锚框。简单起见,我们假设预测偏移量全是0:预测边界框即锚框。最后,我们构造每个类别的预测概率。

anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
                        [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = torch.tensor([0.0] * (4 * len(anchors)))
cls_probs = torch.tensor([[0., 0., 0., 0.,],  # 背景的预测概率
                          [0.9, 0.8, 0.7, 0.1],  # 狗的预测概率
                          [0.1, 0.2, 0.3, 0.9]])  # 猫的预测概率
fig = plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
            ['dog=0.9', 'dog=0.8', 'dog=0.7', 'cat=0.9'])

目标检测(一):边界框、锚框、多尺度目标检测

output = MultiBoxDetection(
    cls_probs.unsqueeze(dim=0), offset_preds.unsqueeze(dim=0),
    anchors.unsqueeze(dim=0), nms_threshold=0.5)
output

output:

tensor([[[ 0.0000,  0.9000,  0.1000,  0.0800,  0.5200,  0.9200],
         [-1.0000,  0.8000,  0.0800,  0.2000,  0.5600,  0.9500],
         [-1.0000,  0.7000,  0.1500,  0.3000,  0.6200,  0.9100],
         [ 1.0000,  0.9000,  0.5500,  0.2000,  0.9000,  0.8800]]])

可以看到剔除了两个锚框

fig = plt.imshow(img)
for i in output[0].detach().cpu().numpy():
    if i[0] == -1: # 略去筛选掉的锚框
        continue
    # i[0]为类别,i[1]为置信度
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    show_bboxes(fig.axes, list(torch.tensor(i[2:]) * bbox_scale), [label])

目标检测(一):边界框、锚框、多尺度目标检测

多尺度目标检测 Multiscale Object Detection

以输入图像的每个像素为中心生成多个锚框实际上是对输入图像不同区域的采样。然而,如果以图像每个像素为中心都生成锚框,很容易生成过多锚框而造成计算量过大。

减少锚框个数并不难。一种简单的方法是在输入图像中均匀采样一小部分像素,并以采样的像素为中心生成锚框。此外,在不同尺度下,我们可以生成不同数量和不同大小的锚框。值得注意的是,较小目标比较大目标在图像上出现位置的可能性更多。举个简单的例子:形状为 1×1 、 1×2 和 2×2 的目标在形状为 2×2 的图像上可能出现的位置分别有4、2和1种。因此,当使用较小锚框来检测较小目标时,我们可以采样较多的区域;而当使用较大锚框来检测较大目标时,我们可以采样较少的区域

可以通过定义特征图的形状来确定任一图像上均匀采样的锚框中心

下面定义display_anchors函数。我们在特征图fmap上以每个像素为中心生成锚框anchors。由于锚框anchorsxxyy 轴的坐标值分别已除以特征图fmap的宽和高,这些值域在0和1之间的值表达了锚框在特征图中的相对位置。由于锚框anchors的中心遍布特征图fmap上的所有单元,anchors的中心在任一图像的空间相对位置一定是均匀分布的。具体来说,当特征图的宽和高分别设为fmap_wfmap_h时,该函数将在任一图像上均匀采样fmap_hfmap_w列个像素,并分别以它们为中心生成大小为s(假设列表s长度为1)的不同宽高比(ratios)的锚框。

特征图的形状能确定任一图像上均匀采样的锚框中心。

def display_anchors(fmap_h, fmap_w, s):
    # 前两维的取值不影响输出结果
    fmap = torch.zeros((1, 10, fmap_h, fmap_w), dtype=torch.float32)
    
    # 平移所有锚框使均匀分布在图片上
    offset_x, offset_y = 1.0/fmap_w, 1.0/fmap_h
    anchors = MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5]) + \
        torch.tensor([offset_x/2, offset_y/2, offset_x/2, offset_y/2])
    
    bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32)
    show_bboxes(plt.imshow(img).axes, anchors[0] * bbox_scale)
  • 小目标检测:
display_anchors(fmap_h=4, fmap_w=4, s=[0.15])

目标检测(一):边界框、锚框、多尺度目标检测

  • 大目标检测:
display_anchors(fmap_w=2, fmap_h=2, s=[0.4])

目标检测(一):边界框、锚框、多尺度目标检测
既然我们已在多个尺度上生成了不同大小的锚框,相应地,我们需要在不同尺度下检测不同大小的目标

在某个尺度下,假设我们依据 cic_i 张形状为 h×wh×w 的特征图生成 h×wh×w 组不同中心的锚框,且每组的锚框个数为 aa 。例如,在刚才实验的第一个尺度下,我们依据1010(通道数)张形状为 4×44×4 的特征图生成了1616组不同中心的锚框,且每组含33个锚框。 接下来,依据真实边界框的类别和位置,每个锚框将被标注类别和偏移量。在当前的尺度下,目标检测模型需要根据输入图像预测 h×wh×w 组不同中心的锚框的类别和偏移量。

假设这里的 cic_i张特征图为卷积神经网络根据输入图像做前向计算所得的中间输出。既然每张特征图上都有 h×wh×w 个不同的空间位置,那么相同空间位置可以看作含有 cic_i个单元。 特征图在相同空间位置的 cic_i个单元在输入图像上的感受野相同,并表征了同一感受野内的输入图像信息。 因此,我们可以将特征图在相同空间位置的 cic_i个单元变换为以该位置为中心生成的 aa 个锚框的类别和偏移量。 不难发现,本质上,我们用输入图像在某个感受野区域内的信息来预测输入图像上与该区域位置相近的锚框的类别和偏移量。

当不同层的特征图在输入图像上分别拥有不同大小的感受野时,它们将分别用来检测不同大小的目标。例如,我们可以通过设计网络,令较接近输出层的特征图中每个单元拥有更广阔的感受野,从而检测输入图像中更大尺寸的目标

本文地址:https://blog.csdn.net/weixin_42437114/article/details/107250816