DataWhale-CV目标检测-Task2
本文最后更新于 385 天前,其中的信息可能已经有所发展或是发生改变。

三、锚框(先验框)

3.1 先验框

在很多目标检测模型中都有先验框的说法,在一些论文(e.g. Faster RCNN)中亦被称为锚点(anchor)。

那么先验框的作用是什么呢?我们先回到前面2.1节中提到的目标检测最初的解决方案:我们需要遍历图片上的每一个目标框,再对这些框进行分类与微调,以完成目标检测的任务。而我们先验框的作用即解决哪些位置是候选目标框的问题。

接下来需要介绍3个概念:

  • 设置不同尺度的先验框
  • 先验框与特征图的对应
  • 先验框类别信息的确定

设置不同尺度的先验框
通常,为了覆盖尽可能多的情况,我们会在同一个位置设置多个不同尺度的先验框,如下图所示:

注意:此处的尺度不仅指大小,亦包含图片的长宽比)

可以看到,通过设置不同尺度的先验框,我们就可以更容易得到与目标物体有良好匹配度的先验框(由高IoU体现)

先验框与特征图的对应
除了不同的尺度以外,我们也需要将先验框铺撒在图片的不同位置上,但倘若我们去遍历原图中每一个像素,就会需要远超于常规数量的先验框,这是完全没有必要的。如下图所示,如果我们要在一张224×224的图片中每个位置设置三个不同尺寸的先验框,则共有2242243=150528个先验框,那如果我们不去遍历原图而是遍历原图采样得到的feature map呢?以vgg16的backbone为例,下采样了5次,得到7×7的feature map,那就只需要得到7x7x3=147个先验,这样的设置大大减少了先验框的数量,同时也能覆盖大多数情况。

因此,我们就将先验框的设置位置与特征图建立一一对应的关系。而且,通过建立这种映射关系,我们可以通过特征图一次性的输出所有先验框的类别信息以及坐标信息,而不是使每个候选框都去独立的进行一次分类的预测,大大提高了处理速度。

先验框类别信息的确定
我们铺设了很多的先验框,但是我们要先给出这些先验框的类别信息,才能让模型学着去预测每个先验框是否对应着一个目标物体。可在这些先验框中有很多是和图片中我们要检测的目标完全没有交集或者有很小的交集的,此时我们采用的做法是,设定一个IoU阈值,例如IoU=0.5,与图片中目标的iou<0.5的先验框,这些框我们将其划分为背景,IoU>=0.5的被归到目标先验框,通过这样划分,得到供模型学习的ground truth信息,如下图所示:

3.2 先验框的生成

结合代码我们来看一下先验框是如何生成的。

model.py 脚本下有一个 tiny_detector 类,是本章节介绍的目标检测网络的定义函数,其内部实现了一个 create_prior_boxes 函数,该函数便是用来生成先验框的。

"""
设置细节介绍:
1. 离散程度 fmap_dims = 7: VGG16最后的特征图尺寸为 7*7
2. 在上面的举例中我们是假设了三种尺寸的先验框,然后遍历坐标。在先验框生成过程中,先验框的尺寸是提前设置好的,
   本教程为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9)

生成过程:
0. cx, cy表示中心点坐标
1. 遍历特征图上每一个cell,i+0.5是为了从坐标点移动至cell中心,/fmap_dims目的是将坐标在特征图上归一化
2. 这个时候我们已经可以在每个cell上各生成一个框了,但是这个不是我们需要的,我们称之为base_prior_bbox基准框。
3. 根据我们在每个cell上得到的长宽比1:1的基准框,结合我们设置的3种尺度obj_scales和3种长宽比aspect_ratios就得到了每个cell的9个先验框。
4. 最终结果保存在prior_boxes中并返回。

需要注意的是,这个时候我们的到的先验框是针对特征图的尺寸并归一化的,因此要映射到原图计算IOU或者展示,需要:
img_prior_boxes = prior_boxes * 图像尺寸
"""

def create_prior_boxes():
        """
        Create the 441 prior (default) boxes for the network, as described in the tutorial.
        VGG16最后的特征图尺寸为 7*7
        我们为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9)
        因此总的候选框个数 = 7 * 7 * 9 = 441
        :return: prior boxes in center-size coordinates, a tensor of dimensions (441, 4)
        """
        fmap_dims = 7 
        obj_scales = [0.2, 0.4, 0.6]
        aspect_ratios = [1., 2., 0.5]

        prior_boxes = []
        for i in range(fmap_dims):
            for j in range(fmap_dims):
                cx = (j + 0.5) / fmap_dims
                cy = (i + 0.5) / fmap_dims

                for obj_scale in obj_scales:
                    for ratio in aspect_ratios:
                        prior_boxes.append([cx, cy, obj_scale * sqrt(ratio), obj_scale / sqrt(ratio)])

        prior_boxes = torch.FloatTensor(prior_boxes).to(device)  # (441, 4)
        prior_boxes.clamp_(0, 1)  # (441, 4)

        return prior_boxes

根据上述代码,我们得到了先验框,接下来我们进行进一步的可视化操作,为了便于观看,仅展示特征图间那个cell对应的先验框。

为了更好的对比,此处我们设置了两组obj_scales尺度参数
1、 obj_scales = [0.1,0.2,0.3]

这里的参数是归一化的,0.1代表anchor的基准大小为原图的长宽的0.1倍,如图所示:

2、 obj_scales = [0.2,0.4,0.6]

这里对比两组不同的尺度设置,是想展示一个需要注意的小问题,那就是越界.

我们可以看到第二组可视化部分蓝色和绿色的先验框都超出图片界限了,这种情况其实是非常容易出现的,越靠近四周的位置的先验框越容易越界,那么这个问题怎么处理呢?

这里我们一般用图片尺寸将越界的先验框进行截断,比如某个先验框左上角坐标是(-5, -9),那么就截断为(0,0),某个先验框右下角坐标是(324,134),当我们的图片大小为(224,224)时,就将其截断为(224,134)。

对应于代码中是这行,prior_boxes.clamp_(0, 1),由于进行了归一化,所以使用0-1进行截断。

四、模型结构

本次学习中使用的网络我们称其为Tiny_Detector,是为了组队学习特意设计的网络,为SSD模型的简化版本,并非某个经典的目标检测网络。

4.1 VGG16作为backbone

为了使结构简单易懂,我们使用VGG16作为backbone,即完全采用vgg16的结构作为特征提取模块,只是去掉fc6和fc7两个全连接层。如下所示:

对于网格输入尺寸的确定,由于VGG16的ImageNet预训练模型采用的是224224,因此为了使模型发挥更好的作用,我们的网络输入也固定为224224。

(通常这个网络输入大小对于检测网络来说还是偏小,可自行对尺度进行扩大,看看是否会有更好的效果)

特征提取模块对应的代码在moel.py中的VGGBase类进行了定义:

class VGGBase(nn.Module):                                                                                                                                         

    def __init__(self):
        super(VGGBase, self).__init__()

        # Standard convolutional layers in VGG16
        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)  # stride = 1, by default
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)    # 224->112

        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)    # 112->56

        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)    # 56->28

        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)    # 28->14

        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2)    # 14->7

        # Load pretrained weights on ImageNet
        self.load_pretrained_layers()


    def forward(self, image):
        """
        Forward propagation.

        :param image: images, a tensor of dimensions (N, 3, 224, 224)
        :return: feature maps pool5
        """
        out = F.relu(self.conv1_1(image))  # (N, 64, 224, 224)
        out = F.relu(self.conv1_2(out))  # (N, 64, 224, 224)
        out = self.pool1(out)  # (N, 64, 112, 112)

        out = F.relu(self.conv2_1(out))  # (N, 128, 112, 112)
        out = F.relu(self.conv2_2(out))  # (N, 128, 112, 112)
        out = self.pool2(out)  # (N, 128, 56, 56)

        out = F.relu(self.conv3_1(out))  # (N, 256, 56, 56)
        out = F.relu(self.conv3_2(out))  # (N, 256, 56, 56)
        out = F.relu(self.conv3_3(out))  # (N, 256, 56, 56)
        out = self.pool3(out)  # (N, 256, 28, 28)

        out = F.relu(self.conv4_1(out))  # (N, 512, 28, 28)
        out = F.relu(self.conv4_2(out))  # (N, 512, 28, 28)
        out = F.relu(self.conv4_3(out))  # (N, 512, 28, 28)
        out = self.pool4(out)  # (N, 512, 14, 14)

        out = F.relu(self.conv5_1(out))  # (N, 512, 14, 14)
        out = F.relu(self.conv5_2(out))  # (N, 512, 14, 14)
        out = F.relu(self.conv5_3(out))  # (N, 512, 14, 14)
        out = self.pool5(out)  # (N, 512, 7, 7)

        # return 7*7 feature map                                                                                                                                  
        return out


    def load_pretrained_layers(self):
        """
        we use a VGG-16 pretrained on the ImageNet task as the base network.
        There's one available in PyTorch, see https://pytorch.org/docs/stable/torchvision/models.html#torchvision.models.vgg16
        We copy these parameters into our network. It's straightforward for conv1 to conv5.
        """
        # Current state of base
        state_dict = self.state_dict()
        param_names = list(state_dict.keys())

        # Pretrained VGG base
        pretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()
        pretrained_param_names = list(pretrained_state_dict.keys())

        # Transfer conv. parameters from pretrained model to current model
        for i, param in enumerate(param_names):  
            state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]

        self.load_state_dict(state_dict)
        print("\nLoaded base model.\n")

我们的Tiny_Detector特征提取层输出的是7*7的feature map,接下来我们需要在feature——map上设置对应的先验框。

本次实验中,anchor的配置如下:

  • 将原图均匀分成7×7个cell
  • 设置3种不同的尺度:0.2, 0.4, 0.6
  • 设置3种不同的长宽比:1:1, 1:2, 2:1

因此我们为77的feature_map设置了对应的77*9个anchor框,其中每一个cell有9个anchor框,如图所示:

对于每一个anchor,我们需要预测两类信息:一是这个anchor的类别信息,另一个则是物体的边界框信息。

本次实验中,类别信息有21个类别(VOC数据集的20个类别 + 1个背景类)的得分组成,而模型最终会选择预测得分最高的类作为边界框对象的类别。

而我们的边界框信息是指:我们大致知道了当前anchor中包含一个物体的情况下,如何对anchor进行微调以提高预测物体bbx的精度。

这两种预测我们分别称为分类头和回归头,那么分类头预测和回归头预测是怎么得到的?

其实我们只需在7×7的feature map后,接上两个3×3的卷积层,即可分别完成分类和回归的预测。

4.2 分类头和回归头

4.2.1 边界框的编解码

由于Tiny_Detector并不是直接预测目标框,而是回归对于anchor要进行多大的调整,我们的目标就是找到一种办法来量化如何计算偏差值。

对于一只狗的目标边界框和先验框的示例如图所示:

我们的模型要预测anchor与目标框的偏移,并且这个偏移会进行某种形式的归一化,这个过程我们称为边界框的编码。

这里我们使用的是与SSD完全一致的编码方法,具体公示表达如下:

$$g_{cx}=\frac{c_x-\hat{c}_x}{\hat{w}}$$

$$g_{cy}=\frac{c_y-\hat{c}_y}{\hat{h}}$$

$$g_w=log(\frac{w}{\hat{w}})$$

$$g_h=log(\frac{h}{\hat{h}})$$

模型预测并输出的是这个编码后的偏移量($g_{cx},g_{cy},g_x,g_h$),最终只要再依照公式反向进行解码,就可以得到预测的目标框的信息。

具体实现代码实现位于utils.py中,如下:

def cxcy_to_gcxgcy(cxcy, priors_cxcy):
    """ 
    Encode bounding boxes (that are in center-size form) w.r.t. the corresponding prior boxes (that are in center-size form).

    For the center coordinates, find the offset with respect to the prior box, and scale by the size of the prior box.
    For the size coordinates, scale by the size of the prior box, and convert to the log-space.

    In the model, we are predicting bounding box coordinates in this encoded form.

    :param cxcy: bounding boxes in center-size coordinates, a tensor of size (n_priors, 4)
    :param priors_cxcy: prior boxes with respect to which the encoding must be performed, a tensor of size (n_priors, 4)
    :return: encoded bounding boxes, a tensor of size (n_priors, 4)
    """

    # The 10 and 5 below are referred to as 'variances' in the original SSD Caffe repo, completely empirical
    # They are for some sort of numerical conditioning, for 'scaling the localization gradient'
    # See https://github.com/weiliu89/caffe/issues/155
    return torch.cat([(cxcy[:, :2] - priors_cxcy[:, :2]) / (priors_cxcy[:, 2:] / 10),  # g_c_x, g_c_y
                      torch.log(cxcy[:, 2:] / priors_cxcy[:, 2:]) * 5], 1)  # g_w, g_h


def gcxgcy_to_cxcy(gcxgcy, priors_cxcy):
    """ 
    Decode bounding box coordinates predicted by the model, since they are encoded in the form mentioned above.

    They are decoded into center-size coordinates.

    This is the inverse of the function above.

    :param gcxgcy: encoded bounding boxes, i.e. output of the model, a tensor of size (n_priors, 4)
    :param priors_cxcy: prior boxes with respect to which the encoding is defined, a tensor of size (n_priors, 4)
    :return: decoded bounding boxes in center-size form, a tensor of size (n_priors, 4)
    """

    return torch.cat([gcxgcy[:, :2] * priors_cxcy[:, 2:] / 10 + priors_cxcy[:, :2],  # c_x, c_y
                      torch.exp(gcxgcy[:, 2:] / 5) * priors_cxcy[:, 2:]], 1)  # w, h

4.2.2 分类头与回归头预测

根据前面的介绍,对于输出7×7的feature map上的每个先验框我们想预测:

1)边界框的一组21类分数,其中包括VOC的2个0类和一个背景类。

2)边界框编码后的偏移量($g_{cx},g_{cy},g_w,g_h$)。

为了得到我们想预测的类别和偏移量,我们需要在feature map后分别接上两个卷积层:

1)一个分类预测的卷积层采用3×3卷积核padding和stride都为1,每个anchor需要分配21个卷积核,每个位置有9个anchor,因此需要21×9个卷积核。

2)一个定位预测卷积层,每个位置使用3×3卷积核padding和stride都为1,每个anchor需要分配4个卷积核,因此需要4×9个卷积核。

我们直观的看看这些卷积上的输出,见下图:

这个回归头和分类头的输出分别用蓝色和黄色表示,其feature_map的大小7*7保持不变,我们真正关心的是第三维度通道数,其具体的展开如下所示:

也就是说,最终回归头的输出有36个通道,其中每4个值就对应了一个anchor的编码后偏移量的预测,这样的4个值的预测共有9组,因此通道数是36。

分类头可以用同样的方式理解,如下图所示:

分类头和回归头结构的定义,由 model.py 中的 PredictionConvolutions 类实现,代码如下:

class PredictionConvolutions(nn.Module):
    """ 
    Convolutions to predict class scores and bounding boxes using feature maps.

    The bounding boxes (locations) are predicted as encoded offsets w.r.t each of the 441 prior (default) boxes.
    See 'cxcy_to_gcxgcy' in utils.py for the encoding definition.
    这里预测坐标的编码方式完全遵循的SSD的定义

    The class scores represent the scores of each object class in each of the 441 bounding boxes located.
    A high score for 'background' = no object.
    """

    def __init__(self, n_classes):
        """ 
        :param n_classes: number of different types of objects
        """
        super(PredictionConvolutions, self).__init__()

        self.n_classes = n_classes

        # Number of prior-boxes we are considering per position in the feature map
        # 9 prior-boxes implies we use 9 different aspect ratios, etc.
        n_boxes = 9 

        # Localization prediction convolutions (predict offsets w.r.t prior-boxes)
        self.loc_conv = nn.Conv2d(512, n_boxes * 4, kernel_size=3, padding=1)

        # Class prediction convolutions (predict classes in localization boxes)
        self.cl_conv = nn.Conv2d(512, n_boxes * n_classes, kernel_size=3, padding=1)

        # Initialize convolutions' parameters
        self.init_conv2d()


    def init_conv2d(self):
        """
        Initialize convolution parameters.
        """
        for c in self.children():
            if isinstance(c, nn.Conv2d):
                nn.init.xavier_uniform_(c.weight)
                nn.init.constant_(c.bias, 0.)


    def forward(self, pool5_feats):
        """
        Forward propagation.

        :param pool5_feats: conv4_3 feature map, a tensor of dimensions (N, 512, 7, 7)
        :return: 441 locations and class scores (i.e. w.r.t each prior box) for each image
        """
        batch_size = pool5_feats.size(0)

        # Predict localization boxes' bounds (as offsets w.r.t prior-boxes)
        l_conv = self.loc_conv(pool5_feats)  # (N, n_boxes * 4, 7, 7)
        l_conv = l_conv.permute(0, 2, 3, 1).contiguous()  
        # (N, 7, 7, n_boxes * 4), to match prior-box order (after .view())
        # (.contiguous() ensures it is stored in a contiguous chunk of memory, needed for .view() below)
        locs = l_conv.view(batch_size, -1, 4)  # (N, 441, 4), there are a total 441 boxes on this feature map

        # Predict classes in localization boxes
        c_conv = self.cl_conv(pool5_feats)  # (N, n_boxes * n_classes, 7, 7)
        c_conv = c_conv.permute(0, 2, 3, 1).contiguous()  # (N, 7, 7, n_boxes * n_classes), to match prior-box order (after .view())
        classes_scores = c_conv.view(batch_size, -1, self.n_classes)  # (N, 441, n_classes), there are a total 441 boxes on this feature map

        return locs, classes_scores

按照上面的介绍,我们的模型输出的shape应该为:

  • 分类头 batch_size x 7 x 7 x 189
  • 回归头 batch_size x 7 x 7 x 36

但是为了方便后面的处理,我们肯定更希望每个anchor的预测独自成一维,也就是:

  • 分类头 batch_size x 441 x 21
  • 回归头 batch_size x 441 x 4

441是因为我们的模型定义了总共441=7x7x9个先验框,这个转换对应了这两行代码:

locs = l_conv.view(batch_size, -1, 4)

classes_scores = c_conv.view(batch_size, -1, self.n_classes)

这个过程的可视化如图所示。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇