RetinaFace

Table of Contents

1. RetinaFace

1.1. Overview

RetinaFace 是一个 face detection 模型, 它的要点是 multi-task learning.

一般 face detection 的目标是:

  1. 检测出 class (face or background)
  2. 检测出 bbox

RetinaFace 的改进是通过把 landmark 做为辅助的目标能提高性能.

1.2. Network

retinaface.png

retinaface 实际上就是一个标准的 object detection 网络:

  1. 前面是一个 Feature Pyramid Networks
    1. 使用 resnet 或 mobilenet 做 backbone, 并取出其中某些层 c2~c6
    2. 用 1x1 conv 变成统一的 channel, 例如图中的 p2~p6, 大小分别为 160x160x256, 80x80x256, …, 10x10x256, 用来对应原图上不同尺度的 anchor
    3. \(p_n = p_n+ upsample(p_{n-1})\)
  2. 每个 feature 都通过一个 Context Module (SSH, https://arxiv.org/pdf/1708.03979.pdf) 做为一个变换, 这个变换会保持输入输出尺寸不变
  3. 把所有 feature 拼起来做为 neck

    例如 10x10x256, …, 160x160x256 拼起来变为 34100x256, 表示对于 34100 个点的数据

  4. 接上不同的 detection head
    1. loc head

      通过一个 conv2d(2x4, 1x1) 的卷积把 256 维的 feature 变为 2x4, 表示一个点对应的两个 anchor 的坐标

    2. cls head

      conv2d(2x1, 1x1), 表示一个点对应两个 anchor 的 cls

    3. landmark head

      conv2d(2x5x4, 1x1), 表示一个点对应两个 anchor 的 5 个 landmark 的坐标

1.2.1. Context Module

https://arxiv.org/pdf/1708.03979.pdf 2017/11

这里参考了 SSH (Single Stage Headless) 的 context module, 所谓 context module, 是加入更大的卷积 (例如 5x5, 7x7) 扩大感受野.

retinaface 的 context module 是:

  1. mxmx256 通过 conv2d(128, 3x3) 变为 mxmx128
  2. mxmx128 再通过 conv2d(64, 3x3) 变为 mxmx64
  3. mxmx64 再通过 conv2d(64, 3x3) 变为 mxmx64
  4. 三次输出 concat 在一起又变回 mxmx256

1.3. Sample

假设:

  1. 输入为 160x160
  2. 使用 resnet50 做 backbone
  3. fpn 使用三个不同的尺度: 8, 16, 32
  4. 每个点有两个不同大小的正方形 anchor

1.3.1. model

class RetinaFace(nn.Module):
    def __init__(self, cfg = None, phase = 'train'):
        backbone = models.resnet50(pretrained=cfg['pretrain'])
        # layer2 输出为 (512, 20, 20)
        # layer3 输出为 (1024, 10, 10)
        # layer4 输出为 (2048, 5, 5)
        # 所以这里只用了三种尺度的 feature map
        self.body = _utils.IntermediateLayerGetter(backbone, {'layer2': 1, 'layer3': 2, 'layer4': 3})

        self.fpn = FPN(in_channels_list, out_channels)
        self.ssh1 = SSH(out_channels, out_channels)
        self.ssh2 = SSH(out_channels, out_channels)
        self.ssh3 = SSH(out_channels, out_channels)


        self.ClassHead = self._make_class_head(fpn_num=3, inchannels=256)
        self.BboxHead = self._make_bbox_head(fpn_num=3, inchannels=256)
        self.LandmarkHead = self._make_landmark_head(fpn_num=3, inchannels=256)


    def forward(self,inputs):
        # backbone
        # out[1].shape=(512, 20, 20)
        # out[2].shape=(1024, 10, 10)
        # out[3].shape=(2048, 5, 5)
        out = self.body(inputs)

        # FPN
        # fpn[0].shape=(256, 20, 20)
        # fpn[1].shape=(256, 10, 10)
        # fpn[2].shape=(256, 5, 5)
        fpn = self.fpn(out)
        # SSH
        # SSH 的输出尺寸不变
        feature1 = self.ssh1(fpn[0])
        feature2 = self.ssh2(fpn[1])
        feature3 = self.ssh3(fpn[2])
        features = [feature1, feature2, feature3]

        # 这里并没有把 feature concat 在一起再用 detecion head, 而是先分别用
        # detecion head 后再 concat 起来
        # bbox_regressions.shape=(1050, 4), 因为每个点有两个 anchor,  (400+100+25) * 2 = 1050
        bbox_regressions = torch.cat([self.BboxHead[i](feature) for i, feature in enumerate(features)], dim=1)
        classifications = torch.cat([self.ClassHead[i](feature) for i, feature in enumerate(features)],dim=1)
        ldm_regressions = torch.cat([self.LandmarkHead[i](feature) for i, feature in enumerate(features)], dim=1)

        output = (bbox_regressions, classifications, ldm_regressions)
        return output

1.3.2. anchor

class PriorBox(object):
    def __init__(self, cfg, image_size=None, phase='train'):
        super(PriorBox, self).__init__()
        # min_size 代表了三种尺度下两个 anchor 的大小, level 0 最小, level 2 最大
        self.min_sizes = [[4, 8], [16, 32], [64, 128]]
        self.steps = [8, 16, 32]
        self.image_size = image_size
        # 这里的 feature_maps 与网络输出的 feature map 尺寸一致   
        self.feature_maps = [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]

    def forward(self):
        anchors = []
        for k, f in enumerate(self.feature_maps):
            # 针对每个 level 的 feature map
            min_sizes = self.min_sizes[k]
            for i, j in product(range(f[0]), range(f[1])):
                # 针对 feature map 的每个点
                for min_size in min_sizes:
                    # 针对每个点的两个 anchor 计算 center 和 长度, 与标准 ssd 不
                    # 同的是, 所有 anchor 都是正方形
                    s_kx = min_size / self.image_size[1]
                    s_ky = min_size / self.image_size[0]
                    dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]]
                    dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]]
                    for cy, cx in product(dense_cy, dense_cx):
                        anchors += [cx, cy, s_kx, s_ky]

        # 输出为 (1050, 4)
        output = torch.Tensor(anchors).view(-1, 4)
        return output

1.3.3. label

SSD 相同, 也会通过 encoder/decode 转换为相对于 anchor 中心的坐标

1.3.4. loss

与 ssd 相同, 但加上了一个针对 landmark 的 regression loss

Backlinks

Object Detection (Object Detection > RetinaFace): RetinaFace

Author: [email protected]
Date: 2022-02-18 Fri 17:17
Last updated: 2022-02-18 Fri 19:32

知识共享许可协议