计算机视觉(一)—— 目标检测和边界框

1. 什么是目标检测

在图像分类任务里,假设图像里只有一个主体目标,并关注如何识别该目标的类别。然而,很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。在计算机视觉里,我们将这类任务称为目标检测(object detection)或物体检测

2. 目标检测大致思路

基本思路大致可以分为以下两个步骤:

  1. 先把目标图片中的物体用框标出来
  2. 输入到分类器中,对目标的类别进行分类

在这里插入图片描述

2.1 边界框

边界框是一个矩形框,可以由矩形左上角的 x 和 y 轴坐标与右下角的 x 和 y 轴坐标确定。我们根据上面的图的坐标信息来定义图中狗和猫的边界框。图中的坐标原点在图像的左上角,原点往右和往下分别为 x 轴和 y 轴的正方向。像这样:
在这里插入图片描述

2.2 合理的边界框?

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。不同的模型使用的区域采样方法可能不同。

2.2.1 某一种锚框的生成方法

2.2.1.1 方法描述

每个像素为中心生成多个大小和宽高比(aspect ratio)不同的边界框。
假设我们输入的图像 h h h w w w。那么,我们分别以图像的每个像素为中心生成不同形状的锚框。假设锚框占总大小的 s ∈ ( 0 , 1 ] s \in (0, 1] s(0,1],假设锚框的宽高比为: r r r,那么,可以计算出锚框的宽和高分别为: w s r ws\sqrt{r} wsr h s / r hs/\sqrt{r} hs/r 而且,当中心位置固定时,生成的锚框是唯一的。

假设一组锚框的大小 s 1 , … , s n s_1,\ldots,s_n s1,,sn和一组宽高比 r 1 , … , r m r_1,\ldots,r_m r1,,rm

如果以每个像素为中心时使用所有的大小与宽高比的组合,输入图像将一共可以得到很多个锚框。虽然这些锚框可能覆盖了所有的真实边界框,但计算复杂度容易过高因此,我们通常只对包含 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), \ldots, (s_1, r_m), (s_2, r_1), (s_3, r_1), \ldots, (s_n, r_1). (s1,r1),(s1,r2),,(s1,rm),(s2,r1),(s3,r1),,(sn,r1).

也就是说,以相同像素为中心的锚框的数量为 n + m − 1 n+m-1 n+m1。对于整个输入图像,我们将一共生成 w h ( n + m − 1 ) wh(n+m-1) wh(n+m1)个锚框。
在这里插入图片描述


为什么是 m + n − 1 m+n-1 m+n1个?
在这里插入图片描述

2.2.1.2 代码

功能:指定输入、一组大小和一组宽高比,该函数将返回输入的所有锚框。


# 本函数已保存在d2lzh_pytorch包中方便以后使用
def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):
    """
    # 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成(xmin, ymin, xmax, ymax).
    https://zh.d2l.ai/chapter_computer-vision/anchor.html
    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
    
    h, w = feature_map.shape[-2:]
    shifts_x = np.arange(0, w) / w
    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)
    
    anchors = shifts.reshape((-1, 1, 4)) + base_anchors.reshape((1, -1, 4))
    
    return torch.tensor(anchors, dtype=torch.float32).view(1, -1, 4)

X = torch.Tensor(1, 3, h=40, w=40)  # 构造输入数据
Y = MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
'''
Y.shape:torch.Size([1, 2042040, 4])
'''
boxes = Y.reshape((h, w, 5, 4))
'''
boxes.shape:(561, 728, 5, 4)
'''

示例:

X:(1, 3, 561, 728):给定一张大小为3个channel, 561 × 728 561 \times 728 561×728像素的图片。
sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]:给定一组大小(0.75, 0.5, 0.25),一组长宽比分别为:(1, 2, 0.5)。

调用了MultiBoxPrior函数之后,生成了一组 40 × 40 40 \times 40 40×40像素为中心的,每个像素拥有 3 + 3 − 1 3+3-1 3+31个锚框,每个锚框用 4 4 4个坐标来描述。

返回锚框变量y的形状为(批量大小,锚框个数,4)。将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)后,我们就可以通过指定像素位置来获取所有以该像素为中心的锚框了。

上述代码,来自于《动手学深度学习》—— pytorch版。


2.2.2 锚框优劣的评定

2.2.2.1 基本概念

如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。我们知道,Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合 A \mathcal{A} A B \mathcal{B} B ,它们的Jaccard系数即二者交集大小除以二者并集大小:
J ( A , B ) = ∣ A ∩ B ∣ ∣ A ∪ B ∣ . J(\mathcal{A},\mathcal{B}) = \frac{\left|\mathcal{A} \cap \mathcal{B}\right|}{\left| \mathcal{A} \cup \mathcal{B}\right|}. J(A,B)=ABAB.
实际上,我们可以把边界框内的像素区域看成是像素的集合。可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度
通常将Jaccard系数称为交并比(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如图所示。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。
在这里插入图片描述

2.2.2.2 代码

代码来自:代码地址

# 以下函数已保存在d2lzh_pytorch包中方便以后使用
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
    lower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0))  # (n1, n2, 2)
    upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0))  # (n1, n2, 2)
    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)

2.2.3 怎么样训练锚框

2.2.3.1 基本概念

在训练集中,我们将每个锚框视为一个训练样本。需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。

大致思路:在目标检测时,

  1. 我们首先生成多个锚框
  2. 为每个锚框预测类别以及偏移量
  3. 根据预测的偏移量调整锚框位置从而得到预测边界框
  4. 筛选需要输出的预测边界框。

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

解决办法是用一个矩阵存储“我们猜测的锚框位置”和“真实的锚框位置”的交并比。每次从这个矩阵中选出最大的交并比,然后剔除该最大值所在的行和列,重复这个步骤 n n n次就可以得到对应的最佳锚框。

在这里插入图片描述
在这里插入图片描述
确定了标注框之后,就可以进行锚框的类别和偏移量的标注了。

例如,如果一个锚框 𝐴 被分配了真实边界框 𝐵 ,将锚框 𝐴 的类别设为 𝐵 的类别,并根据 𝐵 和 𝐴 的中心坐标的相对位置以及两个框的相对大小为锚框 𝐴 标注偏移量。

由于数据集中各个框的位置和大小各异,因此这些相对位置和相对大小通常需要一些特殊变换,才能使偏移量的分布更均匀从而更容易拟合。设锚框 𝐴 及其被分配的真实边界框 𝐵 的中心坐标分别为 ( x a , y a ) (x_a,y_a) (xa,ya) ( x b , y b ) (x_b,y_b) (xb,yb) , 𝐴 和 𝐵 的宽分别为 w a w_a wa w b w_b wb,高分别为 h a h_a ha h a , h b h_a,h_b ha,hb ,一个常用的技巧是将 𝐴 的偏移量标注为
( x b − x a w a − μ x σ x , y b − y a h a − μ y σ y , log ⁡ w b w a − μ w σ w , log ⁡ h b h a − μ 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) (σxwaxbxaμx,σyhaybyaμy,σwlogwawbμw,σhloghahbμh)
其中常数的默认值为 μ 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 μx=μy=μw=μh=0,σx=σy=0.1,σw=σh=0.2。如果一个锚框没有被分配真实边界框,我们只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框,其余则被称为正类锚框。

2.2.3.2 代码实现
# 以下函数已保存在d2lzh_pytorch包中方便以后使用
def assign_anchor(bb, anchor, jaccard_threshold=0.5):
    """
    # 按照「9.4.1. 生成多个锚框」图9.3所讲为每个anchor分配真实的bb, anchor表示成归一化(xmin, ymin, xmax, ymax).
    https://zh.d2l.ai/chapter_computer-vision/anchor.html
    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)


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

def MultiBoxTarget(anchor, label):
    """
    # 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax).
    https://zh.d2l.ai/chapter_computer-vision/anchor.html
    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: (锚框总数, 4), 0代表背景
        """
        an = anc.shape[0]
        assigned_idx = assign_anchor(lab[:, 1:], anc) # (锚框总数, )
        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坐标
        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]
# ground_truth 真实的边界框
ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
                            [1, 0.55, 0.2, 0.9, 0.88]])
# anchors 生成的边界框
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]])
labels = MultiBoxTarget(anchors.unsqueeze(dim=0), ground_truth.unsqueeze(dim=0))

MultiBoxTarget函数来为锚框标注类别和偏移量。该函数将背景类别设为0,并令从零开始的目标类别的整数索引自加1(1为狗,2为猫)。我们通过unsqueeze函数为锚框和真实边界框添加样本维,并构造形状为(批量大小, 包括背景的类别个数, 锚框数)的任意预测结果。
MultiBoxTarget函数的输入为:

返回的结果有:

  1. 每个锚框标注的四个偏移量
    其中负类锚框的偏移量标注为0。
    在这里插入图片描述

  2. 掩码变量。
    下面演示一个具体的例子。我们为读取的图像中的猫和狗定义真实边界框,其中第一个元素为类别(0为狗,1为猫),剩余4个元素分别为左上角的 x x x y y y轴坐标以及右下角的 x x x y y y轴坐标(值域在0到1之间)。这里通过左上角和右下角的坐标构造了5个需要标注的锚框,分别记为 A 0 , … , A 4 A_0, \ldots, A_4 A0,,A4(程序中索引从0开始)。先画出这些锚框与真实边界框在图像中的位置。
    在这里插入图片描述
    根据锚框与真实边界框在图像中的位置来分析这些标注的类别。

  • 在所有的“锚框—真实边界框”的配对中,锚框 A 4 A_4 A4 与猫的真实边界框的交并比最大,因此锚框 A 4 A_4 A4 的类别标注为猫。
  • 不考虑锚框 A 4 A_4 A4 或猫的真实边界框,在剩余的“锚框—真实边界框”的配对中,最大交并比的配对为锚框 A 1 A_1 A1 和狗的真实边界框,因此锚框 A 1 A_1 A1 的类别标注为狗。
  • 接下来遍历未标注的剩余3个锚框:与锚框 A 0 A_0 A0 交并比最大的真实边界框的类别为狗,但交并比小于阈值(默认为0.5),因此类别标注为背景
  • 与锚框 A 2 A_2 A2 交并比最大的真实边界框的类别为猫,且交并比大于阈值,因此类别标注为猫;
  • 与锚框 A 3 A_3 A3 交并比最大的真实边界框的类别为猫,但交并比小于阈值,因此类别标注为背景。
    在这里插入图片描述
  1. 每个锚框标注的类别
    在这里插入图片描述

2.2.4 输出预测边界框

2.2.4.1 基本概念

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

非极大值抑制的工作原理

通俗的说:每个框都有一个所属类别的概率值,根据概率值先确定一个框的类别,然后把与这个框相交、且概率值大于阈值的框全都扔掉。最后剩下的框就是没有重复的预测边界框了

对于一个预测边界框 B B B,模型会计算各个类别的预测概率。设其中最大的预测概率为 p p p,该概率所对应的类别即 B B B 的预测类别。我们也将 p p p 称为预测边界框 B B B 的置信度。

在同一图像上,我们将预测类别非背景的预测边界框按置信度从高到低排序,得到列表 L L L。从 L L L 中选取置信度最高的预测边界框 B 1 B_1 B1 作为基准,将所有与 B 1 B_1 B1 的交并比大于某阈值的非基准预测边界框从 L L L 中移除。这里的阈值是预先设定的超参数。此时, L L L保留了置信度最高的预测边界框并移除了与其相似的其他预测边界框。

接下来,从 L L L 中选取置信度第二高的预测边界框 B 2 B_2 B2作为基准,将所有与 B 2 B_2 B2 的交并比大于某阈值的非基准预测边界框从 L L L中移除。重复这一过程,直到 L L L 中所有的预测边界框都曾作为基准。此时 L L L 中任意一对预测边界框的交并比都小于阈值。最终,输出列表 L L L 中的所有预测边界框。

2.2.4.2 代码实现
# 以下函数已保存在d2lzh_pytorch包中方便以后使用
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), )
        
        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):
    """
    # 按照「9.4.1. 生成多个锚框」所讲的实现, anchor表示成归一化(xmin, ymin, xmax, ymax).
    https://zh.d2l.ai/chapter_computer-vision/anchor.html
    Args:
        cls_prob: 经过softmax后得到的各个锚框的预测概率, shape:(bn, 预测总类别数+1, 锚框个数)
        loc_pred: 预测的各个锚框的偏移量, shape:(bn, 锚框个数*4)
        anchor: MultiBoxPrior输出的默认锚框, 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]
    
    def MultiBoxDetection_one(c_p, l_p, anc, nms_threshold = 0.5):
        """
        MultiBoxDetection的辅助函数, 处理batch中的一个
        Args:
            c_p: (预测总类别数+1, 锚框个数)
            l_p: (锚框个数*4, )
            anc: (锚框个数, 4)
            nms_threshold: 非极大抑制中的阈值
        Return:
            output: (锚框个数, 6)
        """
        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 = nd.array([[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 = nd.array([0] * anchors.size)
cls_probs = nd.array([[0] * 4,  # 背景的预测概率
                      [0.9, 0.8, 0.7, 0.1],  # 狗的预测概率
                      [0.1, 0.2, 0.3, 0.9]])  # 猫的预测概率
output = MultiBoxDetection(
    cls_probs.unsqueeze(dim=0), offset_preds.unsqueeze(dim=0),
    anchors.unsqueeze(dim=0), nms_threshold=0.5)

先构造4个锚框。假设

  • 预测偏移量offset_preds全是0。
  • 预测边界框即锚框anchors
  • 每个类别的预测概率cls_probs

MultiBoxDetection函数来执行非极大值抑制并设阈值为0.5。这里为输入都增加了样本维。我们看到,返回的结果的形状为(批量大小, 锚框个数, 6)。其中每一行的6个元素代表同一个预测边界框的输出信息。

  • 第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),其中-1表示背景或在非极大值抑制中被移除。
  • 第二个元素是预测边界框的置信度。
  • 剩余的4个元素分别是预测边界框左上角的 𝑥 和 𝑦 轴坐标以及右下角的 𝑥 和 𝑦 轴坐标(值域在0到1之间)。

在这里插入图片描述


3. 多尺度目标检测

3.1 小目标检测

为了在显示时更容易分辨,这里令不同中心的锚框不重合:设锚框大小为0.15,特征图的高和宽分别为4。可以看出,图像上4行4列的锚框中心分布均匀。
在这里插入图片描述

3.2 中等尺度目标检测

我们将特征图的高和宽分别减半,并用更大的锚框检测更大的目标。当锚框大小设0.4时,有些锚框的区域有重合。
在这里插入图片描述

3.2 大尺度目标检测

在这里插入图片描述

4. 参考链接

《动手学深度学习》—— 目标检测和边界框

  • 12
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是获取目标检测结果边界信息的Python代码示例: ```python # 导入必要的包 import cv2 import numpy as np # 加载图像和模型 image = cv2.imread('image.jpg') net = cv2.dnn.readNetFromDarknet('yolov3.cfg', 'yolov3.weights') # 获取输出层信息 layer_names = net.getLayerNames() output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()] # 执行前向传递并获取输出 blob = cv2.dnn.blobFromImage(image, 1/255.0, (416, 416), swapRB=True, crop=False) net.setInput(blob) outputs = net.forward(output_layers) # 解析输出并提取边界信息 boxes = [] confidences = [] class_ids = [] for output in outputs: for detection in output: scores = detection[5:] class_id = np.argmax(scores) confidence = scores[class_id] if confidence > 0.5: center_x = int(detection[0] * image.shape[1]) center_y = int(detection[1] * image.shape[0]) width = int(detection[2] * image.shape[1]) height = int(detection[3] * image.shape[0]) left = int(center_x - width / 2) top = int(center_y - height / 2) boxes.append([left, top, width, height]) confidences.append(float(confidence)) class_ids.append(class_id) # 应用非最大值抑制(NMS)以去除重叠的边界 indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) # 打印检测到的物体的边界信息 for i in indices: i = i[0] box = boxes[i] left = box[0] top = box[1] width = box[2] height = box[3] label = str(classes[class_ids[i]]) confidence = confidences[i] print(f'{label}: ({left}, {top}) - ({left + width}, {top + height}), confidence: {confidence}') ``` 该代码首先加载图像和模型,然后执行前向传递并获取输出。接下来,它解析输出并提取每个检测到的物体的边界信息,包括左上角坐标、宽度和高度。最后,它应用非最大值抑制(NMS)以去除重叠的边界,并打印每个检测到的物体的边界信息和置信度。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值