1. 前言

在前面的内容中,我们已经知道:

  • 物体检测不仅要识别“是什么”,还要定位“在哪里”

  • 边界框用于表示目标位置

  • 数据集中的标签需要同时包含类别和边界框信息

但新的问题马上就出现了:

一张图片中目标的位置、大小、形状都不固定,模型到底该从哪里开始找目标?

如果让模型直接从整张图中“凭空”预测边界框,难度会非常大。
因此,目标检测中引入了一种非常重要的机制:锚框(Anchor Box)

锚框的核心思想是:

先在图像上预设大量可能包含目标的候选框,再由模型判断哪些框中有目标,并进一步修正它们的位置。

这一节我们不仅要理解锚框的概念,还要结合《动手学深度学习》中的代码,真正看懂锚框是怎么生成的。


2. 什么是锚框

锚框,英文叫 Anchor Box,也常被称为先验框

它指的是:
在图像的每个像素位置(或者特征图的每个位置)上,预先放置若干个不同大小、不同长宽比的矩形框,用来作为候选检测框。

你可以把它理解成:

  • 我们先在图像上“撒”很多框

  • 这些框形状各不相同

  • 真实目标总会和某些框比较接近

  • 模型再从这些框中筛选并修正出最终结果

所以:

锚框不是最终结果,而是检测的起点。


3. 为什么需要锚框

3.1 目标位置不固定

图像中的目标可能出现在任意位置。
如果没有参考框,模型就必须直接回归所有可能坐标,学习难度很高。

有了锚框后,模型就不需要“从零开始”,而是在已有候选框基础上微调。


3.2 目标大小不同

有些目标很小,有些目标很大。
如果只用一种固定大小的框,就很难同时适应不同尺度的目标。

因此锚框通常会设置多个尺度


3.3 目标形状不同

例如:

  • 行人通常偏瘦高

  • 汽车通常偏宽

  • 某些物体接近正方形

所以锚框不仅要有不同大小,还要有不同的宽高比


3.4 将检测问题转化为更容易学习的问题

锚框机制建立后,模型只需要做两件事:

  • 分类:这个锚框里有没有目标,属于什么类别

  • 回归:这个锚框应该往哪里偏移,才能更贴近真实框

这样一来,复杂的检测问题就被转成了“候选框分类 + 候选框回归”。


4. 李沐代码中锚框的核心函数

在《动手学深度学习》中,生成锚框使用的核心函数是:

d2l.multibox_prior

这个函数的作用可以概括为一句话:

以输入特征图的每个像素位置为中心,生成一组不同尺度和宽高比的锚框。


5. 先看一段核心代码

先看最经典的一段代码:

import torch
from d2l import torch as d2l

img = d2l.plt.imread('catdog.jpg')
h, w = img.shape[:2]

X = torch.rand(size=(1, 3, h, w))
Y = d2l.multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape

输出结果通常是:

torch.Size([1, h*w*5, 4])

如果书里的图片大小是 561 × 728,那么结果就是:

torch.Size([1, 2042040, 4])

6. 这段代码到底在干什么

我们一行一行看。


6.1 读取图片尺寸

img = d2l.plt.imread('catdog.jpg')
h, w = img.shape[:2]

这里读取图片后,取出:

  • h:图片高度

  • w:图片宽度

假设:

  • h = 561

  • w = 728


6.2 构造一个输入张量

X = torch.rand(size=(1, 3, h, w))

这里并不是要真的拿随机数做检测,而是因为 multibox_prior 只关心输入张量的空间尺寸,也就是高和宽,不关心里面具体是什么数值。

这个张量形状是:

(批量大小, 通道数, 高, 宽)

也就是:

(1, 3, h, w)

这和卷积神经网络常见输入格式一致。


6.3 生成锚框

Y = d2l.multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])

这里设置了:

  • sizes=[0.75, 0.5, 0.25]

  • ratios=[1, 2, 0.5]

意思是:
在每个像素位置,都生成若干个不同尺度、不同宽高比的锚框。


7. 为什么每个像素位置会生成 5 个锚框

这是最容易考的点,也是最容易懵的地方。

先看参数:

  • sizes 有 3 个:[0.75, 0.5, 0.25]

  • ratios 有 3 个:[1, 2, 0.5]

很多人第一反应会觉得:

不是应该 3 × 3 = 9 个锚框吗?

其实不是。
在李沐这套实现中,每个像素位置生成的锚框个数是:

len(sizes) + len(ratios) - 1

所以这里就是:

3 + 3 - 1 = 5

8. 为什么是 m + n - 1

设:

  • m = len(sizes)

  • n = len(ratios)

生成方式不是所有尺度和所有宽高比全部两两组合,
而是采用下面这种策略:

  • 先固定 ratio = 1,配合所有 sizes

  • 再固定 size = sizes[0],配合其余所有 ratios

具体到这里:

第一组:固定 ratio = 1

配合所有尺度:

  • (0.75, 1)

  • (0.5, 1)

  • (0.25, 1)

共 3 个。

第二组:固定 size = 0.75

配合剩余宽高比:

  • (0.75, 2)

  • (0.75, 0.5)

共 2 个。

总共就是:

3 + 2 = 5

所以每个像素位置生成 5 个锚框。


9. 输出形状为什么是 [1, h*w*5, 4]

现在就能理解这个输出了:

Y.shape = (1, h*w*5, 4)

含义如下:

第一个维度 1

表示 batch size,也就是这里只有 1 张图。

第二个维度 h*w*5

表示总锚框数量:

  • 一共有 h*w 个像素位置

  • 每个位置生成 5 个锚框

所以总数为:

h * w * 5

第三个维度 4

每个锚框用 4 个数表示:

(xmin, ymin, xmax, ymax)

所以最后维度是 4。


10. 以书中图片为例计算

若图片大小为:

h = 561
w = 728

则总像素点数为:

561 * 728 = 408408

每个像素生成 5 个锚框,所以总锚框数为:

408408 * 5 = 2042040

所以最终输出形状就是:

(1, 2042040, 4)

这也正是书里的结果。


11. 如何查看某个像素位置的锚框

代码通常还会继续写成:

boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]

这里的作用是:

把原来扁平化保存的所有锚框,重新整理成更容易理解的形式:

(h, w, 每个位置的锚框数, 4)

也就是:

(h, w, 5, 4)

这样:

  • boxes[i, j, k, :]
    就表示第 i 行、第 j 列像素位置的第 k 个锚框。

例如:

boxes[250, 250, 0, :]

就是查看 (250, 250) 这个位置处的第 1 个锚框。


12. 锚框坐标为什么会出现负数或大于 1

查看某些锚框时,经常会看到类似这样的结果:

tensor([-0.08, -0.13, 0.52, 0.69])

甚至可能出现小于 0 或大于 1 的值。

这并不奇怪,因为这里的坐标通常是归一化坐标,相对于整张图宽高而言:

  • 左上角是 (0, 0)

  • 右下角是 (1, 1)

如果某个锚框中心靠近边缘,而锚框本身又比较大,那么它的一部分就可能“伸出图像边界”,于是:

  • 左上角可能小于 0

  • 右下角可能大于 1

这是正常现象,后续处理中通常会裁剪或者在匹配时处理。


13. 锚框的可视化代码

为了更直观地理解锚框,通常会把某个像素位置生成的多个锚框画出来。

常见代码如下:

bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
d2l.show_bboxes(fig.axes,
                boxes[250, 250, :, :] * 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'])
d2l.plt.show()

14. 这段可视化代码怎么理解

14.1 bbox_scale

bbox_scale = torch.tensor((w, h, w, h))

因为 boxes 中保存的是归一化坐标,范围大多在 0 到 1 附近。
而画图时需要真实像素坐标,所以这里乘上:

(w, h, w, h)

把归一化坐标还原到像素尺度。


14.2 boxes[250, 250, :, :]

表示:

  • (250, 250) 这个像素位置

  • 该位置所有 5 个锚框

  • 每个锚框有 4 个坐标


14.3 d2l.show_bboxes(...)

这个函数的作用是:

  • 把多个边界框画到图片上

  • 同时标注每个框对应的尺寸和宽高比

这样我们就能非常直观地看到:

  • 大尺度锚框更大

  • 小尺度锚框更小

  • 不同宽高比的锚框形状不同


15. 锚框中的“尺度”和“宽高比”到底表示什么

这是理解锚框的关键。

15.1 尺度 size

尺度控制的是锚框整体大小。
值越大,框越大;值越小,框越小。

例如:

  • 0.75:较大

  • 0.5:中等

  • 0.25:较小


15.2 宽高比 ratio

宽高比控制的是框的形状。

例如:

  • ratio = 1:正方形

  • ratio = 2:宽高比更宽

  • ratio = 0.5:更高更瘦

所以:

尺度决定“多大”,宽高比决定“长什么样”。


16. 锚框的本质理解

到这里可以把锚框机制总结成一句非常重要的话:

锚框是在每个位置预先放置的一组不同形状参考框,用来覆盖各种可能出现的目标。

真实目标出现时,总会有一些锚框和它比较接近。
模型再学习:

  • 哪些锚框应该保留

  • 它们属于什么类别

  • 它们还需要怎样微调

所以锚框本质上是一种“先验假设”。


17. 锚框机制的优点

17.1 让检测有了明确起点

模型不用从零开始瞎猜,而是基于候选框学习。

17.2 能覆盖不同大小目标

通过多尺度锚框,可以检测大目标和小目标。

17.3 能覆盖不同形状目标

通过不同宽高比,可以适配行人、汽车等不同形状物体。


18. 锚框机制的缺点

18.1 数量太多

每个位置都生成多个锚框,整张图下来会有海量候选框,计算成本很高。

18.2 需要人工设计参数

尺度设多少、比例设多少,往往需要经验。

18.3 不够灵活

如果真实目标和预设锚框差异太大,匹配效果就不好。

这也是后来很多 anchor-free 检测方法出现的原因。


19. 本节总结

这一节我们结合代码,系统学习了锚框的生成过程,重点包括:

19.1 锚框是什么

锚框是预先生成的候选框,不是最终检测框。

19.2 为什么需要锚框

因为目标位置、大小、形状都不固定,锚框为检测提供了参考模板。

19.3 multibox_prior 的作用

它会在每个像素位置生成多个不同尺度、不同宽高比的锚框。

19.4 每个像素位置生成多少个锚框

数量公式为:

len(sizes) + len(ratios) - 1

19.5 输出形状如何理解

(1, h*w*每位置锚框数, 4)

其中最后一个 4 表示一个锚框的四个坐标。


20. 学习感悟

锚框这一节,往往是目标检测真正开始“上强度”的地方。
因为从这里开始,我们不再只是看图片和类别,而是开始考虑:

  • 每个位置生成多少候选框

  • 候选框尺度怎么设计

  • 宽高比怎么选择

  • 框与真实目标如何匹配

这些问题都比图像分类更复杂,但也更能体现目标检测的核心思想。

如果说分类任务是在问:

“这张图是什么?”

那么锚框机制已经开始在问:

“目标可能在哪?应该从哪些候选位置开始找?”

这正是目标检测和普通分类任务本质不同的地方。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐