> ## Content Index
> Fetch the complete content index at: https://yinguobing.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# RetinaNet的Anchor生成策略
- URL: https://yinguobing.com/blog/anchor-boxes-of-retinanet/
- Published: 2020-11-22T10:52:51.000Z
- Updated: 2020-11-22T10:52:51.000Z
- Description: 全卷积网络的动态anchor boxes.
- Author: 尹国冰
- Tags: 深度学习, 物体检测, TensorFlow

封面图片：[Jed Dela Cruz](https://unsplash.com/@delajed?utm%5Fsource=unsplash&utm%5Fmedium=referral&utm%5Fcontent=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/boxes?utm%5Fsource=unsplash&utm%5Fmedium=referral&utm%5Fcontent=creditCopyText)

Keras官方提供了一份RetianNet示例代码，地址如下：

[keras-team/keras-ioKeras documentation, hosted live at keras.io. Contribute to keras-team/keras-io development by creating an account on GitHub.![](https://github.githubassets.com/favicons/favicon.svg)GitHubkeras-team![](https://avatars0.githubusercontent.com/u/34455048?s=400&v=4)](https://github.com/keras-team/keras-io/blob/master/examples/vision/retinanet.py?ref=yinguobing.com)

这份代码与文章配合使用可以获得最佳的理解效果。本篇着重根据代码分析其Anchor生成策略。

### 与生成有关的参数

Anchor类的初始化函数如下：

```python
def __init__(self):
    self.aspect_ratios = [0.5, 1.0, 2.0]
    self.scales = [2 ** x for x in [0, 1 / 3, 2 / 3]]

    self._num_anchors = len(self.aspect_ratios) * len(self.scales)
    self._strides = [2 ** i for i in range(3, 8)]
    self._areas = [x ** 2 for x in [32.0, 64.0, 128.0, 256.0, 512.0]]
    self._anchor_dims = self._compute_dims()
```

其中一共涉及到了6个参数：

1. `aspect_ratio`: anchor的宽高比  
三个比例：0.5、1.0、2，对应高、方、扁。
2. `scales`: anchor的缩放比例  
同样是三个值，换算成浮点数为1,、1.26、1.59，都是大于1的数。
3. `_num_anchors`: anchor的数量  
这里是指特征图上每个像素所对应的anchor数量。很明显是长宽比与缩放比数量的乘积，本例中为3×3=9。
4. `_strides`: 采样间隔  
隔多远采样一次。当前值为\[8, 16, 32, 64, 128\]。其实对应了特征图与原始图像的尺寸比例。
5. `_areas`: anchor基础面积  
生成不同形状anchor的依据。目前是边长为32、64、128、256与512像素的正方形面积。
6. `_anchor_dims`: anchor的尺寸  
 是由类方法 `_compute_dims` 来计算得到的。

### 类方法

除了 `__init__` 外，整个Anchor类实现了3个方法。

#### 计算anchor尺寸

方法 `_compute_dims` 计算所有特征金字塔层对应的anchor维度。代码如下：

```python
def _compute_dims(self):
    """Computes anchor box dimensions for all ratios and scales at all levels
    of the feature pyramid.
    """
    anchor_dims_all = []
    for area in self._areas:
        anchor_dims = []
        for ratio in self.aspect_ratios:
            anchor_height = tf.math.sqrt(area / ratio)
            anchor_width = area / anchor_height
            dims = tf.reshape(
                tf.stack([anchor_width, anchor_height], axis=-1), [1, 1, 2]
            )
            for scale in self.scales:
                anchor_dims.append(scale * dims)
        anchor_dims_all.append(tf.stack(anchor_dims, axis=-2))
    return anchor_dims_all
```

其逻辑大致为：

1. 对于每个预定义anchor面积，根据宽高比获得与当前面积对应的anchor的宽度与高度。由于宽高比有三个数值，因此获得3个尺寸的anchor boxes。
2. 将上一步获得的anchor boxes与三个不同的anchor缩放比例相乘，获得9个不同尺度的anchor boxes。
3. 预定义的anchor面积数量为5，因此最终得到一个长度为5的list。每个list成员为与该面积对应的9个anchor boxe，形状为（1, 1, 9, 2)。

该函数在初始化时被调用，结果赋值给类属性 `_anchor_dims` 。

### 生成单张特征图的anchor

对应方法 `_get_anchors` 。该方法需要三个参数：特征图高度、宽度以及层级。代码如下：

```python3
def _get_anchors(self, feature_height, feature_width, level):
    """Generates anchor boxes for a given feature map size and level

    Arguments:
      feature_height: An integer representing the height of the feature map.
      feature_width: An integer representing the width of the feature map.
      level: An integer representing the level of the feature map in the
        feature pyramid.

    Returns:
      anchor boxes with the shape
      `(feature_height * feature_width * num_anchors, 4)`
    """
    rx = tf.range(feature_width, dtype=tf.float32) + 0.5
    ry = tf.range(feature_height, dtype=tf.float32) + 0.5
    centers = tf.stack(tf.meshgrid(rx, ry), axis=-1) * \
        self._strides[level - 3]
    centers = tf.expand_dims(centers, axis=-2)
    centers = tf.tile(centers, [1, 1, self._num_anchors, 1])
    dims = tf.tile(
        self._anchor_dims[level - 3], [feature_height, feature_width, 1, 1]
    )
    anchors = tf.concat([centers, dims], axis=-1)
    return tf.reshape(
        anchors, [feature_height * feature_width * self._num_anchors, 4]
    )
```

该函数首先使用 `tf.range` 生成与特征图尺度对应的网格点x与y坐标。之后使用 `tf.meshgrid` 将x、y组装成网格点。注意这时候 `_strides` 参数发挥作用了。组装后的网格点需要与当前层级对应的stride值相乘，以便与真实的图像尺寸相匹配。需要注意层级的总数为5，但是起始值为3，这是`leve-3`的原因。到这一步获得的只是一类anchor的中心点，而每个特征图对应9类anchor，所以需要使用 `tf.tile` 函数将其扩增，为每一类anchor生成中心网格点。同理，也要为特征图上的每个像素生成anchor的宽度与高度。之后，将中心点`[cx, cy]` 与 尺寸 `[anchor_width, anchor_height]` 拼接为anchor的标准形式并保证形状，便得到了与该层特征图对应的anchor boxes。

#### 生成全部anchor

方法 `get_anchors` 需要参数为图像高度与宽度。代码如下：

```python
def get_anchors(self, image_height, image_width):
    """Generates anchor boxes for all the feature maps of the feature pyramid.

    Arguments:
      image_height: Height of the input image.
      image_width: Width of the input image.

    Returns:
      anchor boxes for all the feature maps, stacked as a single tensor
        with shape `(total_anchors, 4)`
    """
    anchors = [
        self._get_anchors(
            tf.math.ceil(image_height / 2 ** i),
            tf.math.ceil(image_width / 2 ** i),
            i,
        )
        for i in range(3, 8)
    ]
    return tf.concat(anchors, axis=0)
```

这段代码就简单多了。在已知图像宽度与高度的前提下，计算出每一张特征图的尺寸，并与该特征图的层级一起传入 `_get_anchors` 方法得到对应anchor。最后堆叠所有anchor即可。

### Anchor的规律

从anchor生成的过程可以归纳出一些规律。

#### Anchor的数量取决于输入图像的分辨率

在生成每一层的anchor时，其数量取决于特征图的大小。而5张特征图的大小依次是输入图像的 1/8、1/16、1/32、1/64、1/128。所以输入图像越大，anchor的数量越多。另外在 `anchor` 模块中如果图像尺寸无法被整除，则向上取整。实际训练中生成训练数据的代码对原始图像做了padding。

#### Anchor大小与特征层级数正相关

特征层层级越高，anchor的尺寸越大。每一层中anchor最小面积为 `areas` 中定义的基础面积，最大面积为基础面积的 `max(scales)` 倍，代码中为1.59倍。

### 总结

RetinaNet是一只全卷积神经网络，可以接受可变大小的输入。其anchor数量取决于特征图的尺寸，继而取决于输入图像。Anchor生成的逻辑与特征图的生成逻辑关联，也就是说FPN的设计会影响到anchor。在下一篇文章中，我会继续解读FPN的原理。敬请期待！