1 锚框

锚框是目标检测中的一个重要概念,简单来说,就像是预先在图像上摆放好各种形状的“模具”,用来告诉模型:“在这里可能会有一个物体”。

1.1 为什么需要锚框?

在深度学习之前,传统的检测方法(如滑动窗口)计算量巨大。如果我们让模型直接预测物体的坐标 (x,y,w,h),由于物体的尺寸和宽高比千变万化,神经网络很难从头学习这种极大的回归范围

锚框的作用是将搜索空间缩小:

  • 降低难度:模型不再“凭空”预测位置,而是预测锚框相对于真实边缘框的偏移(Offset);
  • 处理多尺度:通过在同一位置设置不同大小和比例的锚框,可以同时检测出小目标(如远处的行人)和大目标(如近处的卡车)。

(由此笔者联想到了自己的毕设***三维重建中动态高斯建设技术的思想 —— 将动态时空表征解耦为静态几何属性和动态位移偏差,对于任意时刻 t ,并不凭空预测高斯点的属性,而是预测其相对于静态属性的偏移,由此实现动态场景的稳定连续表示! )

1.2 锚框的核心参数

        通常情况下,我们会为图像中的每个像素点(或特征图上的每个点)生成多个锚框,这些模锚框由两个参数决定:①尺寸(锚框的大小)  ②宽高比(锚框的形状)

1.3 锚框的工作流程

在学习锚框的算法流程之前,我们需要了解一个概念,交并比(IoU)!

IoU-交并比

IoU用来计算两个框之间的相似度:0表示无重叠,1表示重合;它是Jacquard 指数的一个特殊情况:给定两个集合,J(A,B)=\frac{|A\cap B|}{|A\cup B|}

在训练阶段,主要涉及以下三个步骤:

A. 生成与分配

        算法会在特征图的每个位置生成若干锚框,每个锚框是一个训练样本。

        接着我们需要判断哪些锚框是有用的:计算每个锚框与所有GT 框的IoU

        下面的示例:有4个真实边缘框,生成了9个锚框:

赋予标号的算法有很多种。一些办法是选取与各真实边界框的IoU最大值的锚框作为正例样本,但是这会导致只保留若干个正样本,有时会生成上百万的锚框,那上百万个未选中的锚框都将作为负例样本,严重失衡。因此可以采用阈值筛选的方式:

  • 如果锚框与某个真实边界框的 IoU 超过阈值 \theta(通常为0.7),或者与某个GT框IoU最大,则标记为正样本(将其赋予对应边界框的标号);
  • 如果 IoU 小于阈值(通常为0.3),则标记为负样本(背景);
  • 忽略样本:IoU在0.3~0.7之间的锚框,忽略掉,不参与训练

(实际上的做法不尽相同,比如生成锚框:一些方法是固定生成,一些是根据图片生成...)

B. 边界框回归

        模型会学习4个偏移量来微调锚框:中心点的位移(\Delta x,\Delta y)、宽高的缩放(\Delta w, \Delta h)。通过这种方式,原始锚框逐渐被“推”向边界框;

C. 非极大值抑制(NMS)

        由于一个物体往往会被多个锚框同时捕捉到,NMS会根据置信度排序,保留最高分的框,删除与其重叠度(IoU)大于阈值的其他锚框。—— 重复此过程直到所有预测要么被选中,要么被去掉—— 每个物体只剩下一个最准确的边界框。

另外,在训练时,总损失由两部分组成:

  • 分类损失:这是一个二分类或多分类问题,判断锚框内是背景还是物体(及具体类别)
  • 回归损失:仅针对正样本计算,衡量模型预测的偏移量与真实偏移量之间的差异(常用 Smoooth L1 或 IoU Loss)

小结

        基于锚框做预测的算法是一类目标检测算法,它首先生成大量锚框,再对每个锚框赋予标号,随后对于正样本预测其与真实边界框的偏移实现优化,逐渐逼近与真实边界框的位置,在此过程中,通过某方法来去除冗余的预测,NMS是一种常见办法 。

2 代码实现

这部分的代码确实比较难理解,耐心学习吧~

首先,修改输出精度以获得更简洁的输出。

%matplotlib inline
import torch
from d2l import torch as d2l

torch.set_printoptions(2)  # 精简输出精度

2.1 初始化

2.1.1 生成多个锚框

        目标检测算法通常在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边界从而更准确地预测目标的GT框。不同的模型使用的区域采样的方法可能不同,课程中介绍了其中一种方法:以每个像素为中心,生成多个缩放比和宽高比不同的边界框,这些边界框被称为锚框

        具体实现:假设输入图像的尺寸:w\times h,以图像的每个像素为中心生成不同形状的锚框,其中缩放比为s\in (0,1](代表锚框相对于原图的大小比例),宽高比为r > 0s\in (0,1](锚框自身的宽高之比)。(这里推导卡了很久,按照下面的理解方式和书上一致):

(注意:r是锚框的高宽比和图像宽高比的比)

为生成多个不同形状的锚框,我们将设置多种缩放比取值(s_{1},...,s_{n})和宽高比取值(r_{1},...,r_{m})。若使用这些比例和宽高比的所有组合以每个像素为中心时,输入图像将有whnm个锚框,容易导致过高的复杂度。因此实践中,只考虑包含 s_{1} 或 r_{1} 的组合:

(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 + m - 1。对于整个输入图像,将共生成wh(n + m - 1)个锚框。

(1) 生成锚框

输入:图像数据、尺寸、宽高比之比

输出:对于该图像以每个像素为中心不同形状/尺寸的锚框

def multibox_prior(data, sizes, ratios):
    """生成以每个像素为中心具有不同形状的锚框"""
    # (1)初始化参数
    in_height, in_width = data.shape[-2:]              # shape:(N,C,W,H)-批次、通道、宽、高
    device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
    boxes_per_pixel = (num_sizes + num_ratios - 1)     # 锚框数量
    size_tensor = torch.tensor(sizes, device=device)   # 缩放比
    ratio_tensor = torch.tensor(ratios, device=device) # 宽高比
    # (2)计算像素中心坐标
    # 为了将锚点移动到像素的中心,需要设置偏移量。
    # 因为一个像素的高为1且宽为1,我们选择偏移我们的中心0.5
    offset_h, offset_w = 0.5, 0.5
    steps_h = 1.0 / in_height  # 在y轴上缩放步长
    steps_w = 1.0 / in_width  # 在x轴上缩放步长

    # 生成锚框的所有中心点
    center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    # 生成二维网格,shift_y、shift_x都是形状为(h,w)的矩阵,分别存每个格点的y坐标、x坐标
    shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
    # 展平为1维,长度=(h×w)
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)

    #(3)生成“boxes_per_pixel”个高和宽,
    # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax)
    w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
                   sizes[0] * torch.sqrt(ratio_tensor[1:])))\
                   * in_height / in_width  # 处理矩形输入(对宽度做等比例矫正)
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))
    # (4)从宽高生成锚框四角偏移量
    # 除以2来获得半高和半宽
    anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
                                        in_height * in_width, 1) / 2

    # 每个中心点都将有“boxes_per_pixel”个锚框,
    # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
                dim=1).repeat_interleave(boxes_per_pixel, dim=0)
    output = out_grid + anchor_manipulations
    return output.unsqueeze(0)

整体思路可以归结如下:

输入: data(N,C,H,W), sizes=[s1,s2...], ratios=[r1,r2...]

1. 计算每个像素的归一化中心坐标 (cx, cy)  → 共 H×W 个中心
2. 根据 sizes 和 ratios 组合出 boxes_per_pixel 种 (w, h)
3. 对每个中心:
      对每种 (w, h):
          xmin = cx - w/2
          ymin = cy - h/2
          xmax = cx + w/2
          ymax = cy + h/2
4. 输出形状: (1, H×W×boxes_per_pixel, 4)
img = d2l.plt.imread('data/img/catdog.jpg')
h, w = img.shape[:2]

print(h, w)
# 举个例子:
X = torch.rand(size=(1, 3, h, w))
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape

(2) 绘制锚框

show_bboxes函数:以图像中某个像素为中心,绘制对应的边界框

def show_bboxes(axes, bboxes, labels=None, colors=None):
    """显示所有边界框"""
    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = d2l.bbox_to_rect(bbox.detach().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=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))
d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 500, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
             's=0.75, r=0.5'])

2.1.2 计算IoU值

可以看到蓝色框“较好地”把狗狗覆盖住。若已知目标的真实边界框,那此处的“好”该如和量化呢?——计算该边界框面积与gt边界框面积的IoU值。

下面函数用来衡量不同边界框直接的相似度。给定锚框或边界框的列表,以下box_iou函数将在两个列表中计算它们成对的交并比。

def box_iou(boxes1, boxes2):
    """计算两个锚框或边界框列表中成对的交并比
    输入:boxes1:(N,4),boxes(M,4)
    输出:IoU值矩阵:(N,M)
    每个框用(xmin, ymin, xmax, ymax)
    """
    # (1)计算每个框的面积
    box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
                              (boxes[:, 3] - boxes[:, 1]))
    areas1 = box_area(boxes1)
    areas2 = box_area(boxes2)
    # (2)计算交集的左上角和左下角
    # 整个函数最精妙的部分,利用了 PyTorch 的广播(broadcasting)机制来一次性计算所有框对的交集。
    inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
    inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
    # .clamp(min=0):把所有负数截断为 0,表示"不相交时交集为 0"
    inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
    # inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量)
    inter_areas = inters[:, :, 0] * inters[:, :, 1]
    # (3)计算并集
    union_areas = areas1[:, None] + areas2 - inter_areas
    return inter_areas / union_areas

整体思路归结如下:

输入: boxes1(N,4), boxes2(M,4)
         │
         ├─→ 计算面积 → areas1(N,), areas2(M,)
         │
         ├─→ 广播计算交集左上角 → inter_upperlefts(N,M,2)
         │
         └─→ 广播计算交集右下角 → inter_lowerrights(N,M,2)
                     │
                     ↓
              宽高相减 → clamp(0) → 交集面积(N,M)
                     │
                     ↓
         areas1(N,1) + areas2(M,) - inter_areas(N,M) = 并集面积(N,M)
                     │
                     ↓
              inter_areas / union_areas = IoU(N,M)

2.1.3 在训练数据中标注锚框

在训练集中,我们将每个锚框视为一个训练样本,为了训练目标检测模型,我们需要每个锚框的类别(class)偏移量(offset)标签(其中前者是与锚框相关的对象的类别,后者是真实边界框相对于锚框的偏移量。)

在预测时,为每个图像生成多个锚框,预测所有锚框的类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。

  • 目标检测训练集带有真实边界框的位置及其包围物体类别的标签。要标记任何生成的锚框,我们可以参考分配到的最接近此锚框的真是边界框的位置和类别标签。

下面的算法能够把最接近的真实边界框分配给锚框。

(1) 将真实边界框分配给锚框

这里直接把书里的内容截过来:

def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
    """将最接近的真实边界框分配给锚框"""
    num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
    # (1)计算IoU矩阵
    jaccard = box_iou(anchors, ground_truth)
    # (2)对于每个锚框,初始化分配结果
    anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
                                  device=device)
    # (3)根据阈值,决定是否分配真实边界框(锚框视角:够阈值就分配)
    max_ious, indices = torch.max(jaccard, dim=1)
    anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
    box_j = indices[max_ious >= iou_threshold]
    anchors_bbox_map[anc_i] = box_j
    # (4)贪心策略-确保每个真实框至少有一个锚框
    col_discard = torch.full((num_anchors,), -1)
    row_discard = torch.full((num_gt_boxes,), -1)
    for _ in range(num_gt_boxes):
        max_idx = torch.argmax(jaccard)
        box_idx = (max_idx % num_gt_boxes).long() # 列号(真实框索引)
        anc_idx = (max_idx / num_gt_boxes).long() # 行号(锚框索引)
        anchors_bbox_map[anc_idx] = box_idx
        jaccard[:, box_idx] = col_discard  # 该真实框对应的整列设为 -1
        jaccard[anc_idx, :] = row_discard  # 该锚框对应的整行设为 -1
    return anchors_bbox_map

上述流程可以总结为:

输入: anchors(N,4), ground_truth(M,4)

1. 计算 IoU 矩阵 jaccard(N, M)

2. 【阈值分配】
   对每个锚框 i:
       找到 max_j IoU(i, j)
       如果 max_iou >= 0.5:
           anchors_bbox_map[i] = argmax_j

3. 【贪心分配 - 确保全覆盖】
   重复 M 次:
       找到全局最大 IoU 的位置 (anc_idx, box_idx)
       anchors_bbox_map[anc_idx] = box_idx
       将第 anc_idx 行和第 box_idx 列设为 -1(屏蔽)

4. 返回 anchors_bbox_map(N,)

第四段可能会覆盖第三段的结果(同一个锚框被重新分配),但最终效果是:所有真实框都有锚框负责,同时尽量多的锚框参与了分配

(2) 标记类别和偏移量

分配完成后,需要为每个锚框生成训练标签:

  • 类别标签:-1或某个真实框的类比
  • 偏移量标签:模型预测锚框到真实框的偏移量。偏移量的计算公式:

其中常量的默认值为  \mu_x = \mu_y = \mu_w = \mu_h = 0,\; \sigma_x = \sigma_y = 0.1,\; \sigma_w = \sigma_h = 0.2 。 

前两项代表位置偏移,后两项表示尺寸缩放偏移(使用对数函数,确保尺度缩放更稳定)。在代码实现中,可以乘以一些常数来进行特征缩放,使得损失函数更容易优化。

#@save
def offset_boxes(anchors, assigned_bb, eps=1e-6):
    """对锚框偏移量的转换"""
    c_anc = d2l.box_corner_to_center(anchors)
    c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
    offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
    offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
    offset = torch.cat([offset_xy, offset_wh], axis=1)
    return offset

其中未分配的锚框(背景),偏移量标注为 0。另外新类别的整数索引递增一。

def multibox_target(anchors, labels):
    """使用真实边界框标记锚框"""
    batch_size, anchors = labels.shape[0], anchors.squeeze(0)
    batch_offset, batch_mask, batch_class_labels = [], [], []
    device, num_anchors = anchors.device, anchors.shape[0]
    for i in range(batch_size):
        label = labels[i, :, :]
        anchors_bbox_map = assign_anchor_to_bbox(
            label[:, 1:], anchors, device)
        bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
            1, 4)
        # 将类标签和分配的边界框坐标初始化为零
        class_labels = torch.zeros(num_anchors, dtype=torch.long,
                                   device=device)
        assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
                                  device=device)
        # 使用真实边界框来标记锚框的类别。
        # 如果一个锚框没有被分配,标记其为背景(值为零)
        indices_true = torch.nonzero(anchors_bbox_map >= 0)
        bb_idx = anchors_bbox_map[indices_true]
        class_labels[indices_true] = label[bb_idx, 0].long() + 1
        assigned_bb[indices_true] = label[bb_idx, 1:]
        # 偏移量转换
        offset = offset_boxes(anchors, assigned_bb) * bbox_mask
        batch_offset.append(offset.reshape(-1))
        batch_mask.append(bbox_mask.reshape(-1))
        batch_class_labels.append(class_labels)
    bbox_offset = torch.stack(batch_offset)
    bbox_mask = torch.stack(batch_mask)
    class_labels = torch.stack(batch_class_labels)
    return (bbox_offset, bbox_mask, class_labels)

//返回值分析:

  • bbox_ooffset(边界框偏移量):每个锚框相对于匹配到的真实框的4维偏移回归目标——对于没有匹配任何真实框的锚框(背景),offset值会后续被bbox_mask屏蔽,实际不参与位置损失。
  • bbox_mask(边界框掩码):取值为0.0/1.0。0.0表示背景锚框(负样本);1.0表示匹配到真实物体框(正样本锚框),需要计算位置回归损失。
  • class_labels(类别标签):分类分支的训练标签,用来计算交叉熵分类损失。
(3) 一个例子

下面为猫、狗分配了标注了边界框,另外构建了5个锚框。

ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
                         [1, 0.48, 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 = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['cat', 'dog'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);

下面根据猫和狗的真实边界框,标注这些锚框的分类和偏移量。

labels = multibox_target(anchors.unsqueeze(dim=0),
                         ground_truth.unsqueeze(dim=0))
labels

//分析:

张量①:每个锚框标记的4个偏移值。注意负类锚框的偏移量标记为0。

张量②:掩码变量,形状为(批量大小,锚框数的4倍)。掩码变量中的元素与每个锚框的4个偏移量一一对应。用于过滤掉负类偏移量。

张量③:最初,锚框A_{4}与狗的真实边界框的IoU最大,因此被标记为狗。去除包含A_4或狗的真实边界框的配对;在剩下的配对中,锚框A_{1}和猫的真实边界框有最大IoU,因此A_{1}的类别被标记为猫。接下来遍历剩余的3个未标记的锚框:A_{0}A_{2}A_{3}。对于A_{0},与其拥有最大IoU的真实边界框的类别是猫,但由于IoU值没有超过预定义的阈值(0.5)因此标记背景0;对于A_{2}A_{3}同理。

2.2 训练

在预测时,先生成多个锚框,再为这些锚框一一预测类别和偏移量。一个预测好的边界框则根据其中某个带有预测偏移量的锚框而生成。

下面的函数将锚框和偏移量预测作为输入,应用逆偏移变换来返回预测的边界框坐标。

def offset_inverse(anchors, offset_preds):
    """根据带有预测偏移量的锚框来预测边界框"""
    anc = d2l.box_corner_to_center(anchors)
    pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
    pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
    pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
    predicted_bbox = d2l.box_center_to_corner(pred_bbox)
    return predicted_bbox

当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框,都围绕同一目标。为了简化输出,可使用非极大值抑制(non-maxing supression,NMS)合并属于同一目标的类似的预测边界框。

(1) 非极大值抑制的工作原理

对于一个预测边界框B,目标检测模型会计算每个类别的预测概率。假设最大的预测概率为 p ,则该概率所对应的类别 B 即为预测的类别。此处的  p 即称为预测边界框 B 的置信度。在同一张图像上,所有预测的非背景边界框都按置信度降序排序,以生成列表  L 。接着:

  • Step1.从 L中选取p最高的预测边界框 B_{1}作为基准,然后将所有与B_{1}的IoU超过预定义阈值\epsilon的非基准预测边界框从L中移除。这时,L保留了置信度最高的预测边界框,去除了与其太过相似的其他预测边界框——那些具有非极大值置信度的边界框被抑制了。
  • Step2.从L中选取p第二高的预测边界框 B_{2}作为又一个基准,然后将所有与B_{2}的IoU......
  • ......
  • 重复上述过程,直到L中的所有预测边界框都曾被用作基准。此时,L中任意一对预测边界框的IoU都小于阈值\epsilon;输出列表L中所有预测边界框。

下面函数按降序对置信度进行排序并返回其索引。

def nms(boxes, scores, iou_threshold):
    """对预测边界框的置信度进行排序"""
    B = torch.argsort(scores, dim=-1, descending=True)
    keep = []  # 保留预测边界框的指标
    while B.numel() > 0:
        i = B[0]
        keep.append(i)
        if B.numel() == 1: break
        iou = box_iou(boxes[i, :].reshape(-1, 4),
                      boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
        inds = torch.nonzero(iou <= iou_threshold).reshape(-1)
        B = B[inds + 1]
    return torch.tensor(keep, device=boxes.device)

下面函数来将非极大值抑制应用于预测边界框。

#@save
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
                       pos_threshold=0.009999999):
    """使用非极大值抑制来预测边界框"""
    device, batch_size = cls_probs.device, cls_probs.shape[0]
    anchors = anchors.squeeze(0)
    num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
    out = []
    for i in range(batch_size):
        cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
        conf, class_id = torch.max(cls_prob[1:], 0)
        predicted_bb = offset_inverse(anchors, offset_pred)
        keep = nms(predicted_bb, conf, nms_threshold)

        # 找到所有的non_keep索引,并将类设置为背景
        all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
        combined = torch.cat((keep, all_idx))
        uniques, counts = combined.unique(return_counts=True)
        non_keep = uniques[counts == 1]
        all_id_sorted = torch.cat((keep, non_keep))
        class_id[non_keep] = -1
        class_id = class_id[all_id_sorted]
        conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
        # pos_threshold是一个用于非背景预测的阈值
        below_min_idx = (conf < pos_threshold)
        class_id[below_min_idx] = -1
        conf[below_min_idx] = 1 - conf[below_min_idx]
        pred_info = torch.cat((class_id.unsqueeze(1),
                               conf.unsqueeze(1),
                               predicted_bb), dim=1)
        out.append(pred_info)
    return torch.stack(out)

(2) 应用示例

为简单起见,假设预测的偏移量都是零,即预测的边界框都是锚框。对于背景、狗和猫其中的每个类,定义它的预测概率。

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] * anchors.numel())
cls_probs = torch.tensor([[0] * 4,  # 背景的预测概率
                      [0.9, 0.8, 0.7, 0.1],  # cat的预测概率
                      [0.1, 0.2, 0.3, 0.9]])  # dog的预测概率

可以在图像上绘制这些预测边界框和置信度:

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
            ['cat=0.9', 'cat=0.8', 'cat=0.7', 'dog=0.9'])

现在开始执行非极大值抑制,其中,阈值设置为0.5。注意,源代码在示例的张量中添加了维度。

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

//分析:返回的形状是(批量大小,锚框的数量,6)。最内层维度中的6个元素提供了同一预测边界框的输出信息。第一个元素是预测的类索引,从0开始(0代表cat,1代表dog),-1表示背景或在nms过程中被移除了;第2个元素是预测的边界框的置信度;其余4个元素分别表示预测边界框左上角和右下角的(x,,y)轴坐标。(范围介于0和1之间)。

删除-1类别的预测边界框后,可以输出由nms保存的最终预测边界框。

fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
    if i[0] == -1:
        continue
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

实践中,在执行非极大值抑制前,可以将置信度较低的预测边界框移除,从而减少此算法中的计算量。我们也可以对非极大值抑制的输出结果进行后处理。例如,只保留置信度更高的结果作为最终输出。

小结

下面整体梳理一下过程。

  1. 为图像生成多个锚框(训练样本)。
  2. 计算锚框与真实边界框所有组合的IoU值。
  3. 根据IoU值对训练数据分配真实边界框。(确保每个边界框都至少有一个锚框对应,一些锚框可能此时)
  4. 为锚框标记标签(具体正样本类别)和边界框相对其的偏移量(位置和形状偏移)。
  5. 将锚框、偏移量作为输入,目标检测模型会预测其对应每个正样本类别的预测概率。(有许多锚框时,可能会输出许多相似/重叠的预测边界框,期间采用非极大值抑制策略减少锚框,最终保留的锚框各自差异大,从而减免不必要的计算量)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐