DataWhale计算机视觉实践(目标检测)Task02

DataWhale计算机视觉实践(目标检测)Task02



文中图片和部分内容、代码转自:动手学CV-Pytorch

目标检测

一. 锚框/先验框

1. 先验框

锚点(anchor)==先验框(prior bounding box)

先验框主要就是解决:如何定义哪些位置是候选目标框的问题

设置不同尺度的先验框
  • 为了覆盖更多可能的情况,在进行目标检测时,会在图中的同一个位置,设置几个不同尺度的先验框(不同大小和长宽)。

  • 通过不同尺度的先验框,就可以有更高的概率出现对于目标物体有良好匹配度的先验框。(体现为高IoU

先验框与特征图的对应

将先验框的设置位置与特征图建立一一对应的关系,只需要遍历特征图即可,能够减少先验框的数量,同时也能覆盖大多数的情况。

  • 通过特征图能够直接一次性的输出所有先验框的类别信息以及坐标信息。
先验框类别信息的确定
  • 通过设定IoU阈值,计算先验框与图片中目标的IoU,将先验框划分为背景(例如IoU<0.5)和目标先验框(IoU>=0.5),将他们作为模型学习的ground truth信息。
2. 先验框的生成

先验框生成代码如下:

"""
设置细节介绍:
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)
        """
        # 离散程度,即特征图的维度,VGG16一般定义为7*7
        fmap_dims = 7 
        # 定义三种不同的尺度
        obj_scales = [0.2, 0.4, 0.6]
        # 定义三中不同长宽比
        aspect_ratios = [1., 2., 0.5]
        # 初始化先验框数组
        prior_boxes = []
        # 迭代生成7*7*9个先验框
        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)])
        # 将先验框数组转换为FloatTensor
        prior_boxes = torch.FloatTensor(prior_boxes).to(device)  # (441, 4)
        # clamp有下划线的表示修改并付给自身,无下划线的表示需要返回处理后的值
        prior_boxes.clamp_(0, 1)  # (441, 4)

        return prior_boxes


二. 模型结构

1. VGG16作为backbone

使用VGG16作为backbone,即完全采用vgg16的结构作为特征提取模块,但是去掉了fc6fc7两个全连接层。
网络的输入也固定224*224,保持和预训练模型尺度保持一致

VGG16模型结构
特征提取代码在下面的VGGBase中定义,输出一个7*7feature map

class VGGBase(nn.Module):                                                                                                                                         
    """
    VGG base convolutions to produce feature maps.
    完全采用vgg16的结构作为特征提取模块,丢掉fc6和fc7两个全连接层。
    因为vgg16的ImageNet预训练模型是使用224×224尺寸训练的,因此我们的网络输入也固定为224×224
    """

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

        # Standard convolutional layers in VGG16
        # 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.
        网络前向传播,输入为图片训练集图片tensor。N张3颜色通道的224*224图片。
        :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")

2. 先验框设置
  • 基于上文生成的7*7feature map,需要设置对应的先验框(anchor),即本文第一部分生成anchor代码中所定义的,其配置如下:

    • 将原图均匀分成7x7个cell;
    • 设置3中不同的尺度:0.2, 0.4 ,0.6
    • 设置3中不同的长宽比:1:1, 1:2, 2:1
  • 一张图片就有7*7=49个cell;一个cell中有3*3个anchor框,如下图所示:

anchor框示意图

  • 对于每一个anchor,模型主要预测两类信息:

    • 这个anchor中的类别信息;
    • 物体的边界框信息:指的是大致知道了当前anchor中包含一个物体的情况下,如何对anchor进行微调使得最终能够预测出物体的bbox。
  • 并将这两种预测分别称为:分类头回归头。主要通过在7*7feature map后,接上两个3*3的卷积层,即可分别完成分类和回归的预测。

  • 最终模型会选择预测得分最高的类作为边界框对象的类别。下图为模型的输出示例:

模型输出示例

3. 分类头和回归头
边界框的解编码

模型并不是直接去预测目标框,而是回归对于anchor要进行多大程度的调整,其实也就是要预测到anchor与目标框之间的偏移,才能够更准确的预测出边界框的位置。因此需要找到一种方法来量化这个偏差,即anchor的调整程度。

目标框和预测矿示例

  • 边界框的编码:模型预测anchor与目标框的偏移,并且这个偏移会进行某种形式的归一化。这里使用的是与SSD完全一致的编码方法,**模型预测并输出这个编码后的偏移量 ( g c x , g c y , g w , g h ) (g_{cx}, g_{cy}, g_w, g_h) (gcx,gcy,gw,gh),最终再根据公式反向进行解码,就可以得到预测的目标框的信息,**具体公式如下:

g c x = c x − c ^ x w ^ g_{cx} = \frac{c_x-\hat{c}_x}{\hat{w}} gcx=w^cxc^x

g c y = c y − c ^ y h ^ g_{cy} = \frac{c_y-\hat{c}_y}{\hat{h}} gcy=h^cyc^y

g w = l o g ( w w ^ ) g_{w} = log(\frac{w}{\hat{w}}) gw=log(w^w)

g h = l o g ( h h ^ ) g_{h} = log(\frac{h}{\hat{h}}) gh=log(h^h)

  • 目标框编码与解码的实现代码如下所示:
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
    
    # 分别计算g_c_x,g_c_y和g_w,g_h,并将他们拼接到一起
    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


分类头与回归头预测
  • 在我们的模型中,对于输出的7*7的feature map上的每一个先验框,想要预测:

    • 分类头:边界框的一组21类分数,其中包括VOC的20类和一个背景类;
    • 回归头:边界框编码后的偏移量 ( g c x , g c y , g w , g h ) (g_{cx}, g_{cy}, g_w, g_h) (gcx,gcy,gw,gh)
  • 要得到想预测的类别和偏移量,需要在feature map后分别接上两个卷积层:

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

在这里插入图片描述

如上图所示,回归头和分类头的输出分别用蓝色和黄色表示,其feature map的大小7*7保持不变,主要关注的是第三纬度的通道数,也就是卷积核的个数,回归头具体展开如下图所示:

在这里插入图片描述

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

分类头也可以用同样的方式理解,如下图所示:
在这里插入图片描述

分类头和回归头结构的定义,主要是由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)
        # 将输出shape转换为每个anchor的预测独自成一维的形式
        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


  • 基于上面的介绍,每个anchor的预测独自成一维的形式,模型输出的shape为:
    • 分类头:batch*441*21
    • 回归头:batch*441*4

其中441是因为模型一共定义了7*7*9个先验框。

  • pool5层输出分类和回归结果的过程可视化如下:

在这里插入图片描述

小结:
  • 这部分主要是介绍了跟模型前向推理相关的内容,主要是知道了模型的基本结构,以及通过模型最终会输出的结果是什么样的。
  • 学习到了先验框的作用以及如何生成等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值