精华内容
下载资源
问答
  • SSD目标检测

    2019-09-17 12:02:59
    上两章已经详细介绍了SSD目标检测(1):图片+视频版物体定位(附源码),SSD目标检测(2):如何制作自己的数据集(详细说明附源码)。由于SSD框架是开源的代码,自然有很多前辈研究后做了改进。我也不过是站在前辈的...

    前言:

    上两章已经详细介绍了SSD目标检测(1):图片+视频版物体定位(附源码)SSD目标检测(2):如何制作自己的数据集(详细说明附源码)。由于SSD框架是开源的代码,自然有很多前辈研究后做了改进。我也不过是站在前辈的肩膀上才能完成这篇博客,在这里表示感谢。
    这一章就是讲解如何使用自己的数据集,让SSD框架识别。源码也无偿奉上了哦!

    -------------------------------------------------------------------------------------------------------------------------
    本篇博客我用的数据集可以在这里找到,有需要的同学自己下载。并不包含最后训练得到的模型。
    -------------------------------------------------------------------------------------------------------------------------

    行文说明:

    要用SSD训练自己的数据集,首先要知道怎样制作自己的数据集,上一章已经有详细的介绍。有了准备好了的数据集后,再怎样调整代码可以使SSD框架运行得到结果,这一章都有详细介绍。所以行文大致分如下三个部分:

    1. 第一部分:SSD目标检测(2):如何制作自己的数据集(详细说明附源码)
    2. 第二部分:SSD框架细节调整
    3. 第三部分:模型展示

    在本文中,笔者的操作系统是win10,用的IDE是PyCharm,python3.6 + GTX1060 + cuda9 + cudnn7,在此情况下以下的步骤都是运行完好。Linux系统下笔者也成功运行,只是小部分实现的方式不相同,这里不做详细讲解,还望包涵。

    -------------------------------------------------------------------------------------------------------------------------
    -------------------------------------------------------------------------------------------------------------------------

    一、SSD框架细节调整说明

    1、前期准备工作

    第一步:先将SSD框架下载到本地,解压出来;SSD源码下载

    第二步:在解压出来的主目录下依次创建tfrecords_train_modelVOC2007文件夹,再将之前在SSD目标检测(2):如何制作自己的数据集(详细说明附源码)中制作的三个文件夹AnnotationsImageSetsJPEGImages全都拖入VOC2007文件夹内;

    第2.5步:为方便操作不易混淆,请在PyCharm里建立工程;得到的截图如下,截图说明如下:

    1. 请注意红色框VOCxxx使用的是具体的名字,不过一般都是VOC2007
    2. 目录对应的从属关系不要出错
    3. tfrecords_文件夹是用来存储.tfrecords文件(后面有程序可以直接生成)
    4. train_model文件夹是用来存储模型的记录与参数的
      这里写图片描述

    2、生成.tfrecords文件的代码微调说明

    第三步:修改标签项——打开datasets文件夹中pascalvoc_common.py文件,将自己的标签项填入。我之前做的图片标签.xml文件中,就只有一个标签项“watch”,所以要根据你自己数据集实际情况进行修改;
    在这里插入图片描述

    第四步:修改读取个数、读取方式——打开datasets文件夹中的pascalvoc_to_tfrecords.py文件,

    • 修改67行SAMPLES_PER_FILES的个数;
    • 修改83行读取方式为'rb'
    • 如果你的文件不是.jpg格式,也可以修改图片的类型;这里写图片描述

    3、生成.tfrecords文件

    第五步:生成.tfrecords文件——打开tf_convert_data.py文件,依次点击:runEdit Configuration,在Parameters中填入以下内容,再运行tf_convert_data.py文件,在面板中得到成功信息,可以在tfrecords_文件夹下看到生成的.tfrecords文件;

    --dataset_name=pascalvoc
    --dataset_dir=./VOC2007/
    --output_name=voc_2007_train
    --output_dir=./tfrecords_
    

    这里写图片描述

    4、重新训练模型的代码微调说明

    第六步:修改训练数据shape——打开datasets文件夹中的pascalvoc_2007.py文件,

    • 根据自己训练数据修改:NUM_CLASSES = 类别数

    说明:TRAIN_STATISTICS的数值我并没有深入了解,大于新数据集该标签的总数一般都不会报错。我的数据集是由20张、每张包含一只手表的图片组成,所以下图的值我设定为20,大于20也没有报错,如果你有更精确的想法,请留言告诉大家!
    在这里插入图片描述
    第七步:修改类别个数——打开nets文件夹中的ssd_vgg_300.py文件,

    • 根据自己训练类别数修改96 和97行:等于类别数+1这里写图片描述

    第八步:修改类别个数——打开eval_ssd_network.py文件,

    • 修改66行的类别个数:等于类别数+1
      这里写图片描述

    第九步:修改训练步数epoch——打开train_ssd_network.py文件,

    • 修改27行的数据格式,改为'NHWC'
    • 修改135行的类别个数:等于类别数+1
    • 修改154行训练总步数,None会无限训练下去;
    • 说明:60行、63行是关于模型保存的参数;
      这里写图片描述

    5、加载vgg_16,重新训练模型

    第十步:下载vgg_16模型——下载地址请点击,密码:ge3x;下载完成解压后存入checkpoint文件中;
    最后一步:重新训练模型——打开train_ssd_network.py文件,依次点击:runEdit Configuration,在Parameters中填入以下内容,再运行train_ssd_network.py文件

    --train_dir=./train_model/
    --dataset_dir=./tfrecords_/
    --dataset_name=pascalvoc_2007
    --dataset_split_name=train
    --model_name=ssd_300_vgg
    --checkpoint_path=./checkpoints/vgg_16.ckpt
    --checkpoint_model_scope=vgg_16
    --checkpoint_exclude_scopes=ssd_300_vgg/conv6,ssd_300_vgg/conv7,ssd_300_vgg/block8,ssd_300_vgg/block9,ssd_300_vgg/block10,ssd_300_vgg/block11,ssd_300_vgg/block4_box,ssd_300_vgg/block7_box,ssd_300_vgg/block8_box,ssd_300_vgg/block9_box,ssd_300_vgg/block10_box,ssd_300_vgg/block11_box
    --trainable_scopes=ssd_300_vgg/conv6,ssd_300_vgg/conv7,ssd_300_vgg/block8,ssd_300_vgg/block9,ssd_300_vgg/block10,ssd_300_vgg/block11,ssd_300_vgg/block4_box,ssd_300_vgg/block7_box,ssd_300_vgg/block8_box,ssd_300_vgg/block9_box,ssd_300_vgg/block10_box,ssd_300_vgg/block11_box
    --save_summaries_secs=60
    --save_interval_secs=100
    --weight_decay=0.0005
    --optimizer=adam
    --learning_rate=0.001
    --learning_rate_decay_factor=0.94
    --batch_size=4
    --gpu_memory_fraction=0.7
    

    注意:上面是输入参数:

    • --save_interval_secs是训练多少次保存参数的步长;
    • --optimizer是优化器;
    • --learning_rate是学习率;
    • --learning_rate_decay_factor是学习率衰减因子;
    • 如果你的机器比较强大,可以适当增大--batch_size的数值,以及调高GPU的占比--gpu_memory_fraction
    • --model_name:我并没有尝试使用其他的模型做增量训练,如果你有需要,也请留言联系我,我很乐意研究;

    若得到下图日志,即说明模型开始训练:
    在这里插入图片描述
    训练结束可以在train_model文件夹下看到生成的参数文件;
    在这里插入图片描述

    到这里,训练终于结束了!!!

    -------------------------------------------------------------------------------------------------------------------------
    -------------------------------------------------------------------------------------------------------------------------

    二、结果展示

    这是我训练的loss,我的数据集总共就20张图片,进行4.8W次训练用了将近一个小时,我的配置是GTX1060的单显卡;在这里插入图片描述
    1、在日志中,选取最后一次生成模型作为测试模型进行测试;
    2、在demo文件夹下放入测试图片;
    3、最后在notebooks文件夹下建立demo_test.py测试文件,代码如下:
    4、注意第48行,导入的新模型的名称是否正确

    # -*- coding:utf-8 -*-
    # -*- author:zzZ_CMing  CSDN address:https://blog.csdn.net/zzZ_CMing
    # -*- 2018/07/20; 15:19
    # -*- python3.6
    import os
    import math
    import random
    import numpy as np
    import tensorflow as tf
    import cv2
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    from nets import ssd_vgg_300, ssd_common, np_methods
    from preprocessing import ssd_vgg_preprocessing
    from notebooks import visualization
    import sys
    
    sys.path.append('../')
    slim = tf.contrib.slim
    # TensorFlow session: grow memory when needed. TF, DO NOT USE ALL MY GPU MEMORY!!!
    gpu_options = tf.GPUOptions(allow_growth=True)
    config = tf.ConfigProto(log_device_placement=False, gpu_options=gpu_options)
    isess = tf.InteractiveSession(config=config)
    
    
    # 定义数据格式,设置占位符
    net_shape = (300, 300)
    # 输入图像的通道排列形式,'NHWC'表示 [batch_size,height,width,channel]
    data_format = 'NHWC'
    # 预处理,以Tensorflow backend, 将输入图片大小改成 300x300,作为下一步输入
    img_input = tf.placeholder(tf.uint8, shape=(None, None, 3))
    # 数据预处理,将img_input输入的图像resize为300大小,labels_pre,bboxes_pre,bbox_img待解析
    image_pre, labels_pre, bboxes_pre, bbox_img = ssd_vgg_preprocessing.preprocess_for_eval(
        img_input, None, None, net_shape, data_format, resize=ssd_vgg_preprocessing.Resize.WARP_RESIZE)
    # 拓展为4维变量用于输入
    image_4d = tf.expand_dims(image_pre, 0)
    
    # 定义SSD模型
    # 是否复用,目前我们没有在训练所以为None
    reuse = True if 'ssd_net' in locals() else None
    # 调出基于VGG神经网络的SSD模型对象,注意这是一个自定义类对象
    ssd_net = ssd_vgg_300.SSDNet()
    # 得到预测类和预测坐标的Tensor对象,这两个就是神经网络模型的计算流程
    with slim.arg_scope(ssd_net.arg_scope(data_format=data_format)):
        predictions, localisations, _, _ = ssd_net.net(image_4d, is_training=False, reuse=reuse)
    
    # 导入新训练的模型参数
    ckpt_filename = '../train_model/model.ckpt-xxx'   # 注意xxx代表的数字是否和文件夹下的一致
    # ckpt_filename = '../checkpoints/VGG_VOC0712_SSD_300x300_ft_iter_120000.ckpt'
    isess.run(tf.global_variables_initializer())
    saver = tf.train.Saver()
    saver.restore(isess, ckpt_filename)
    
    # 在网络模型结构中,提取搜索网格的位置
    # 根据模型超参数,得到每个特征层(这里用了6个特征层,分别是4,7,8,9,10,11)的anchors_boxes
    ssd_anchors = ssd_net.anchors(net_shape)
    """
    每层的anchors_boxes包含4个arrayList,前两个List分别是该特征层下x,y坐标轴对于原图(300x300)大小的映射
    第三,四个List为anchor_box的长度和宽度,同样是经过归一化映射的,根据每个特征层box数量的不同,这两个List元素
    个数会变化。其中,长宽的值根据超参数anchor_sizes和anchor_ratios制定。
    """
    
    
    # 主流程函数
    def process_image(img, select_threshold=0.6, nms_threshold=.01, net_shape=(300, 300)):
        # select_threshold:box阈值——每个像素的box分类预测数据的得分会与box阈值比较,高于一个box阈值则认为这个box成功框到了一个对象
        # nms_threshold:重合度阈值——同一对象的两个框的重合度高于该阈值,则运行下面去重函数
    
        # 执行SSD模型,得到4维输入变量,分类预测,坐标预测,rbbox_img参数为最大检测范围,本文固定为[0,0,1,1]即全图
        rimg, rpredictions, rlocalisations, rbbox_img = isess.run([image_4d, predictions, localisations, bbox_img],
                                                                  feed_dict={img_input: img})
    
        # ssd_bboxes_select()函数根据每个特征层的分类预测分数,归一化后的映射坐标,
        # ancohor_box的大小,通过设定一个阈值计算得到每个特征层检测到的对象以及其分类和坐标
        rclasses, rscores, rbboxes = np_methods.ssd_bboxes_select(
            rpredictions, rlocalisations, ssd_anchors,
            select_threshold=select_threshold, img_shape=net_shape, num_classes=21, decode=True)
        """
        这个函数做的事情比较多,这里说的细致一些:
        首先是输入,输入的数据为每个特征层(一共6个,见上文)的:
                                                    rpredictions: 分类预测数据,
                                                    rlocalisations: 坐标预测数据,
                                                    ssd_anchors: anchors_box数据
                                                其中:
                                                   分类预测数据为当前特征层中每个像素的每个box的分类预测
                                                   坐标预测数据为当前特征层中每个像素的每个box的坐标预测
                                                   anchors_box数据为当前特征层中每个像素的每个box的修正数据
    
            函数根据坐标预测数据和anchors_box数据,计算得到每个像素的每个box的中心和长宽,这个中心坐标和长宽会根据一个算法进行些许的修正,
        从而得到一个更加准确的box坐标;修正的算法会在后文中详细解释,如果只是为了理解算法流程也可以不必深究这个,因为这个修正算法属于经验算
        法,并没有太多逻辑可循。
            修正完box和中心后,函数会计算每个像素的每个box的分类预测数据的得分,当这个分数高于一个阈值(这里是0.5)则认为这个box成功
        框到了一个对象,然后将这个box的坐标数据,所属分类和分类得分导出,从而得到:
            rclasses:所属分类
            rscores:分类得分
            rbboxes:坐标
            
            最后要注意的是,同一个目标可能会在不同的特征层都被检测到,并且他们的box坐标会有些许不同,这里并没有去掉重复的目标,而是在下文
        中专门用了一个函数来去重
        """
    
        # 检测有没有超出检测边缘
        rbboxes = np_methods.bboxes_clip(rbbox_img, rbboxes)
        rclasses, rscores, rbboxes = np_methods.bboxes_sort(rclasses, rscores, rbboxes, top_k=400)
        # 去重,将重复检测到的目标去掉
        rclasses, rscores, rbboxes = np_methods.bboxes_nms(rclasses, rscores, rbboxes, nms_threshold=nms_threshold)
        # 将box的坐标重新映射到原图上(上文所有的坐标都进行了归一化,所以要逆操作一次)
        rbboxes = np_methods.bboxes_resize(rbbox_img, rbboxes)
        return rclasses, rscores, rbboxes
    
    
    # 测试的文件夹
    path = '../demo/'
    image_names = sorted(os.listdir(path))
    # 文件夹中的第几张图,-1代表最后一张
    img = mpimg.imread(path + image_names[-1])
    rclasses, rscores, rbboxes = process_image(img)
    
    # visualization.bboxes_draw_on_img(img, rclasses, rscores, rbboxes, visualization.colors_plasma)
    visualization.plt_bboxes(img, rclasses, rscores, rbboxes)
    

    结果展示:这是我自己拍的照片,得到的识别效果还算勉强吧(请自动忽略我那性感的手毛!)
    在这里插入图片描述

    如果你的测试结果是下面这样的:

    导致的原因:

    1. 训练次数太少,loss过高——解决方法除了优化数据集外,就是增大训练次数(要明白谷歌公布的模型都是在大型集群上训练好多天的结果,我们就在GTX1060单显卡上训练4.8W次就想出非常好的结果?偶然的成功比失败更可怕,而且想弯道超谷歌不太可能吧!)
    2. 另外上面程序65行的select_threshold、 nms_threshold参数你也可以做调整;观察下图可以发现误标框框的预测值都小于0.55,而唯一正确的框框预测值等于0.866。所以认真理解上面程序66、67行我写的注释,对你的问题会有帮助;
      在这里插入图片描述

    -------------------------------------------------------------------------------------------------------------------------
    本篇博客,我用的数据集可以在这里找到,但并不包含最后训练得到的模型,有需要的同学自己下载。

    展开全文
  • 睿智的目标检测23——Pytorch搭建SSD目标检测平台

    万次阅读 多人点赞 2020-03-20 14:48:05
    睿智的目标检测23——Pytorch搭建SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、真实框...

    学习前言

    一起来看看SSD的Pytorch实现吧,顺便训练一下自己的数据。
    在这里插入图片描述

    什么是SSD目标检测算法

    SSD是一种非常优秀的one-stage目标检测方法,one-stage算法就是目标检测分类是同时完成的,其主要思路是利用CNN提取特征后,均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,物体分类与预测框的回归同时进行,整个过程只需要一步,所以其优势是速度快
    但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡(参见Focal Loss),导致模型准确度稍低。
    SSD的英文全名是Single Shot MultiBox Detector,Single shot说明SSD算法属于one-stage方法,MultiBox说明SSD算法基于多框预测。

    源码下载

    https://github.com/bubbliiiing/ssd-pytorch
    喜欢的可以点个star噢。

    SSD实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    SSD采用的主干网络是VGG网络,关于VGG的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,这里的VGG网络相比普通的VGG网络有一定的修改,主要修改的地方就是:
    1、将VGG16的FC6和FC7层转化为卷积层。
    2、去掉所有的Dropout层和FC8层;
    3、新增了Conv6、Conv7、Conv8、Conv9。

    在这里插入图片描述
    如图所示,输入的图片经过了改进的VGG网络(Conv1->fc7)和几个另加的卷积层(Conv6->Conv9),进行特征提取:
    a、输入一张图片后,被resize到300x300的shape

    b、conv1,经过两次[3,3]卷积网络,输出的特征层为64,输出为(300,300,64),再2X2最大池化,该最大池化步长为2,输出net为(150,150,64)。

    c、conv2,经过两次[3,3]卷积网络,输出的特征层为128,输出net为(150,150,128),再2X2最大池化,该最大池化步长为2,输出net为(75,75,128)。

    d、conv3,经过三次[3,3]卷积网络,输出的特征层为256,输出net为(75,75,256),再2X2最大池化,该最大池化步长为2,输出net为(38,38,256)。

    e、conv4,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(38,38,512),再2X2最大池化,该最大池化步长为2,输出net为(19,19,512)。

    f、conv5,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(19,19,512),再3X3最大池化,该最大池化步长为1,输出net为(19,19,512)。

    g、利用卷积代替全连接层,进行了一次[3,3]卷积网络和一次[1,1]卷积网络,分别为fc6和fc7,输出的通道数为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)

    h、conv6,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的通道数为512,因此输出的net为(10,10,512)。

    i、conv7,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的通道数为256,因此输出的net为(5,5,256)。

    j、conv8,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的通道数为256,因此输出的net为(3,3,256)。

    k、conv9,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(1,1,256)。

    实现代码:

    base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
                512, 512, 512]
    
    def vgg(i):
        layers = []
        in_channels = i
        for v in base:
            if v == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            elif v == 'C':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
            else:
                conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
                layers += [conv2d, nn.ReLU(inplace=True)]
                in_channels = v
        pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
        conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
        layers += [pool5, conv6,
                   nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
        return layers
    
    def add_extras(i, batch_norm=False):
        # Extra layers added to VGG for feature scaling
        layers = []
        in_channels = i
    
        # Block 6
        # 19,19,1024 -> 10,10,512
        layers += [nn.Conv2d(in_channels, 256, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)]
    
        # Block 7
        # 10,10,512 -> 5,5,256
        layers += [nn.Conv2d(512, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)]
    
        # Block 8
        # 5,5,256 -> 3,3,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
        
        # Block 9
        # 3,3,256 -> 1,1,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
    
        return layers
    

    2、从特征获取预测结果

    在这里插入图片描述
    由上图可知,我们分别取:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    共六个特征层进行下一步的处理。为了和普通特征层区分,我们称之为有效特征层,来获取预测结果。

    对获取到的每一个有效特征层,我们都需要对其做三个操作,分别是:

    • 一次num_priors x 4的卷积
    • 一次num_priors x num_classes的卷积
    • 计算对应的先验框。

    num_priors指的是该特征层每一个特征点所拥有的先验框数量。上述提到的六个特征层,每个特征层的每个特征点对应的先验框数量分别为4、6、6、6、4、4。

    上述操作分别对应的对象为:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    所有的特征层对应的预测结果的shape如下:
    在这里插入图片描述
    实现代码为:

    class SSD(nn.Module):
        def __init__(self, phase, base, extras, head, num_classes):
            super(SSD, self).__init__()
            self.phase = phase
            self.num_classes = num_classes
            self.cfg = Config
            self.vgg = nn.ModuleList(base)
            self.L2Norm = L2Norm(512, 20)
            self.extras = nn.ModuleList(extras)
            self.priorbox = PriorBox(self.cfg)
            with torch.no_grad():
                self.priors = Variable(self.priorbox.forward())
            self.loc = nn.ModuleList(head[0])
            self.conf = nn.ModuleList(head[1])
            if phase == 'test':
                self.softmax = nn.Softmax(dim=-1)
                self.detect = Detect(num_classes, 0, 200, 0.01, 0.45)
        def forward(self, x):
            sources = list()
            loc = list()
            conf = list()
    
            # 获得conv4_3的内容
            for k in range(23):
                x = self.vgg[k](x)
    
            s = self.L2Norm(x)
            sources.append(s)
    
            # 获得fc7的内容
            for k in range(23, len(self.vgg)):
                x = self.vgg[k](x)
            sources.append(x)
    
            # 获得后面的内容
            for k, v in enumerate(self.extras):
                x = F.relu(v(x), inplace=True)
                if k % 2 == 1:
                    sources.append(x)
    
    
    
            # 添加回归层和分类层
            for (x, l, c) in zip(sources, self.loc, self.conf):
                loc.append(l(x).permute(0, 2, 3, 1).contiguous())
                conf.append(c(x).permute(0, 2, 3, 1).contiguous())
    
            # 进行resize
            loc = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
            conf = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
            if self.phase == "test":
                # loc会resize到batch_size,num_anchors,4
                # conf会resize到batch_size,num_anchors,
                output = self.detect(
                    loc.view(loc.size(0), -1, 4),                   # loc preds
                    self.softmax(conf.view(conf.size(0), -1,
                                 self.num_classes)),                # conf preds
                    self.priors              
                )
            else:
                output = (
                    loc.view(loc.size(0), -1, 4),
                    conf.view(conf.size(0), -1, self.num_classes),
                    self.priors
                )
            return output
    
    mbox = [4, 6, 6, 6, 4, 4]
    
    def get_ssd(phase,num_classes):
    
        vgg, extra_layers = add_vgg(3), add_extras(1024)
    
        loc_layers = []
        conf_layers = []
        vgg_source = [21, -2]
        for k, v in enumerate(vgg_source):
            loc_layers += [nn.Conv2d(vgg[v].out_channels,
                                     mbox[k] * 4, kernel_size=3, padding=1)]
            conf_layers += [nn.Conv2d(vgg[v].out_channels,
                            mbox[k] * num_classes, kernel_size=3, padding=1)]
                            
        for k, v in enumerate(extra_layers[1::2], 2):
            loc_layers += [nn.Conv2d(v.out_channels, mbox[k]
                                     * 4, kernel_size=3, padding=1)]
            conf_layers += [nn.Conv2d(v.out_channels, mbox[k]
                                      * num_classes, kernel_size=3, padding=1)]
    
        SSD_MODEL = SSD(phase, vgg, extra_layers, (loc_layers, conf_layers), num_classes)
        return SSD_MODEL
    

    3、预测结果的解码

    利用SSD的主干网络我们可以获得六个有效特征层,分别是:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    通过对每一个特征层的处理,我们可以获得每个特征层对应的三个内容,分别是:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    我们利用 num_priors x 4的卷积每一个有效特征层对应的先验框 进行调整获得 预测框

    在这里我们简单了解一下每个特征层到底做了什么:
    每一个有效特征层将整个图片分成与其长宽对应的网格如conv4-3的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,对于conv4-3的特征层来说,它的每个特征点分别建立了4个先验框;

    因此,对于conv4-3整个特征层来讲,整个图片被分成38x38个网格,每个网格中心对应4个先验框,一共建立了38x38x4个,5776个先验框。这些框密密麻麻的遍布在整个图片上网络的预测结果会对这些框进行调整获得预测框。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,ssd利用num_priors x 4的卷积的结果对先验框进行调整。

    num_priors x 4中的num_priors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

    x_offset与y_offset代表了真实框距离先验框中心的xy轴偏移情况。
    h和w代表了真实框的宽与高相对于先验框的变化情况。

    SSD解码过程可以分为两部分:
    每个网格的中心点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心;
    利用h和w调整先验框获得预测框的宽和高。
    此时我们获得了预测框的中心和预测框的宽高,已经可以在图片上绘制预测框了。

    想要获得最终的预测结果,还要对每一个预测框进行得分排序非极大抑制筛选
    这一部分基本上是所有目标检测通用的部分。
    1、取出每一类得分大于self.obj_threshold的框和得分
    2、利用框的位置和得分进行非极大抑制。
    实现代码如下:

    # Adapted from https://github.com/Hakuyume/chainer-ssd
    def decode(loc, priors, variances):
        boxes = torch.cat((
            priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
            priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)
        boxes[:, :2] -= boxes[:, 2:] / 2
        boxes[:, 2:] += boxes[:, :2]
        return boxes
    
    class Detect(Function):
        def __init__(self, num_classes, bkg_label, top_k, conf_thresh, nms_thresh):
            self.num_classes = num_classes
            self.background_label = bkg_label
            self.top_k = top_k
            self.nms_thresh = nms_thresh
            if nms_thresh <= 0:
                raise ValueError('nms_threshold must be non negative.')
            self.conf_thresh = conf_thresh
            self.variance = Config['variance']
    
        def forward(self, loc_data, conf_data, prior_data):
            loc_data = loc_data.cpu()
            conf_data = conf_data.cpu()
            num = loc_data.size(0)  # batch size
            num_priors = prior_data.size(0)
            output = torch.zeros(num, self.num_classes, self.top_k, 5)
            conf_preds = conf_data.view(num, num_priors,
                                        self.num_classes).transpose(2, 1)
            # 对每一张图片进行处理
            for i in range(num):
                # 对先验框解码获得预测框
                decoded_boxes = decode(loc_data[i], prior_data, self.variance)
                conf_scores = conf_preds[i].clone()
    
                for cl in range(1, self.num_classes):
                    # 对每一类进行非极大抑制
                    c_mask = conf_scores[cl].gt(self.conf_thresh)
                    scores = conf_scores[cl][c_mask]
                    if scores.size(0) == 0:
                        continue
                    l_mask = c_mask.unsqueeze(1).expand_as(decoded_boxes)
                    boxes = decoded_boxes[l_mask].view(-1, 4)
                    # 进行非极大抑制
                    ids, count = nms(boxes, scores, self.nms_thresh, self.top_k)
                    output[i, cl, :count] = \
                        torch.cat((scores[ids[:count]].unsqueeze(1),
                                   boxes[ids[:count]]), 1)
            flt = output.contiguous().view(num, -1, 5)
            _, idx = flt[:, :, 0].sort(1, descending=True)
            _, rank = idx.sort(1)
            flt[(rank < self.top_k).unsqueeze(-1).expand_as(flt)].fill_(0)
            return output
    

    4、在原图上进行绘制

    通过第三步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    二、训练部分

    1、真实框的处理

    真实框的处理可以分为两个部分,分别是:

    • 找到真实框对应的先验框
    • 对真实框进行编码。

    a、找到真实框对应的先验框

    在这一步中,我们需要找到真实框所对应的先验框,代表这个真实框由某个先验框进行预测。

    我们首先需要将每一个的真实框和所有的先验框进行一个iou计算,这一步做的工作是计算每一个真实框和所有的先验框的重合程度。

    在获得每一个真实框和所有的先验框的重合程度之后,选出和每一个真实框重合程度大于一定门限的先验框。代表这个真实框由这些先验框负责预测。

    由于一个先验框只能负责一个真实框的预测,所以如果某个先验框和多个真实框的重合度较大,那么这个先验框只负责与其iou最大的真实框的预测。

    在这一步后,我们可以找到每一个先验框所负责预测的真实框,在下一步中,我们需要根据这些真实框和先验框获得网络的预测结果。

    实现代码如下:

    def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
        # 计算所有的先验框和真实框的重合程度
        overlaps = jaccard(
            truths,
            point_form(priors)
        )
        # 所有真实框和先验框的最好重合程度
        # [truth_box,1]
        best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)
        best_prior_idx.squeeze_(1)
        best_prior_overlap.squeeze_(1)
        # 所有先验框和真实框的最好重合程度
        # [1,prior]
        best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)
        best_truth_idx.squeeze_(0)
        best_truth_overlap.squeeze_(0)
        # 找到与真实框重合程度最好的先验框,用于保证每个真实框都要有对应的一个先验框
        best_truth_overlap.index_fill_(0, best_prior_idx, 2)
        # 对best_truth_idx内容进行设置
        for j in range(best_prior_idx.size(0)):
            best_truth_idx[best_prior_idx[j]] = j
        
        # 找到每个先验框重合程度最好的真实框
        matches = truths[best_truth_idx]          # Shape: [num_priors,4]
        conf = labels[best_truth_idx] + 1         # Shape: [num_priors]
        # 如果重合程度小于threhold则认为是背景
        conf[best_truth_overlap < threshold] = 0  # label as background
        loc = encode(matches, priors, variances)
        loc_t[idx] = loc    # [num_priors,4] encoded offsets to learn
        conf_t[idx] = conf  # [num_priors] top class label for each prior
    

    b、真实框的编码

    利用SSD的主干网络我们可以获得六个有效特征层,分别是:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    通过对每一个特征层的处理,我们可以获得每个特征层对应的三个内容,分别是:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    因此,我们直接利用ssd网络预测到的结果,并不是预测框在图片上的真实位置,需要解码才能得到真实位置。

    所以在训练的时候,如果我们需要计算loss函数,这个loss函数是相对于ssd网络的预测结果的。因此我们需要对真实框的信息进行处理,使得它的结构和预测结果的格式是一样的,这样的一个过程我们称之为编码(encode)。用一句话概括编码的过程就是将真实框的位置信息格式转化为ssd预测结果的格式信息

    也就是,我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并求出如果想要得到这样一个真实框,我们的预测结果应该是怎么样的。

    从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。因此我们只需要将解码过程逆过来就是编码过程了。

    因此我们可以利用真实框和先验框进行编码,获得该特征点对应的该先验框应该有的预测结果。预测结果分为两个,分别是:

    • num_priors x 4的卷积
    • num_priors x num_classes的卷积

    前者对应先验框的调整参数,后者对应先验框的种类,这个都可以通过先验框对应的真实框获得。

    实现代码如下:

    def encode(matched, priors, variances):
        g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]
        g_cxcy /= (variances[0] * priors[:, 2:])
        g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
        g_wh = torch.log(g_wh) / variances[1]
        return torch.cat([g_cxcy, g_wh], 1) 
    

    2、利用处理完的真实框与对应图片的预测结果计算loss

    loss的计算分为三个部分:
    1、获取所有正标签的框的预测结果的回归loss。
    2、获取所有正标签的种类的预测结果的交叉熵loss。
    3、获取一定负标签的种类的预测结果的交叉熵loss。

    由于在ssd的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有若干个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于ssd的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

    实现代码如下:

    class MultiBoxLoss(nn.Module):
        def __init__(self, num_classes, overlap_thresh, prior_for_matching,
                     bkg_label, neg_mining, neg_pos, neg_overlap, encode_target,
                     use_gpu=True):
            super(MultiBoxLoss, self).__init__()
            self.use_gpu = use_gpu
            self.num_classes = num_classes
            self.threshold = overlap_thresh
            self.background_label = bkg_label
            self.encode_target = encode_target
            self.use_prior_for_matching = prior_for_matching
            self.do_neg_mining = neg_mining
            self.negpos_ratio = neg_pos
            self.neg_overlap = neg_overlap
            self.variance = Config['variance']
    
        def forward(self, predictions, targets):
            # 回归信息,置信度,先验框
            loc_data, conf_data, priors = predictions
            # 计算出batch_size
            num = loc_data.size(0)
            # 取出所有的先验框
            priors = priors[:loc_data.size(1), :]
            # 先验框的数量
            num_priors = (priors.size(0))
            num_classes = self.num_classes
            # 创建一个tensor进行处理
            loc_t = torch.Tensor(num, num_priors, 4)
            conf_t = torch.LongTensor(num, num_priors)
            for idx in range(num):
                # 获得框
                truths = targets[idx][:, :-1].data
                # 获得标签
                labels = targets[idx][:, -1].data
                # 获得先验框
                defaults = priors.data
                # 找到标签对应的先验框
                match(self.threshold, truths, defaults, self.variance, labels,
                      loc_t, conf_t, idx)
            if self.use_gpu:
                loc_t = loc_t.cuda()
                conf_t = conf_t.cuda()
                
            # 转化成Variable
            loc_t = Variable(loc_t, requires_grad=False)
            conf_t = Variable(conf_t, requires_grad=False)
    
            # 所有conf_t>0的地方,代表内部包含物体
            pos = conf_t > 0
            # 求和得到每一个图片内部有多少正样本
            num_pos = pos.sum(dim=1, keepdim=True)
            # 计算回归loss
            pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
            loc_p = loc_data[pos_idx].view(-1, 4)
            loc_t = loc_t[pos_idx].view(-1, 4)
            loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)
    
            # 转化形式
            batch_conf = conf_data.view(-1, self.num_classes)
            # 你可以把softmax函数看成一种接受任何数字并转换为概率分布的非线性方法
            # 获得每个框预测到真实框的类的概率
            loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
            loss_c = loss_c.view(num, -1)
    
            loss_c[pos] = 0 
            # 获得每一张图新的softmax的结果
            _, loss_idx = loss_c.sort(1, descending=True)
            _, idx_rank = loss_idx.sort(1)
            # 计算每一张图的正样本数量
            num_pos = pos.long().sum(1, keepdim=True)
            # 限制负样本数量
            num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
            neg = idx_rank < num_neg.expand_as(idx_rank)
    
            # 计算正样本的loss和负样本的loss
            pos_idx = pos.unsqueeze(2).expand_as(conf_data)
            neg_idx = neg.unsqueeze(2).expand_as(conf_data)
            conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
            targets_weighted = conf_t[(pos+neg).gt(0)]
            loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
    
            # Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N
    
            N = num_pos.data.sum()
            loss_l /= N
            loss_c /= N
            return loss_l, loss_c
    

    训练自己的ssd模型

    ssd整体的文件夹构架如下:
    在这里插入图片描述
    本文使用VOC格式进行训练。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    在训练前利用voc2ssd.py文件生成对应的txt。
    在这里插入图片描述
    再运行根目录下的voc_annotation.py,运行前需要将classes改成你自己的classes。

    classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
    

    在这里插入图片描述
    就会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。
    在这里插入图片描述
    在训练前需要修改model_data里面的voc_classes.txt文件,需要将classes改成你自己的classes。
    在这里插入图片描述
    还有config文件下面的Num_Classes,修改成分类的数量+1。
    在这里插入图片描述
    运行train.py即可开始训练。
    在这里插入图片描述

    展开全文
  • 睿智的目标检测16——Keras搭建SSD目标检测平台

    万次阅读 多人点赞 2020-01-30 16:23:28
    睿智的目标检测16——Keras搭建SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、计算loss...

    学习前言

    一起来看看SSD的keras实现吧,顺便训练一下自己的数据。
    在这里插入图片描述

    什么是SSD目标检测算法

    SSD是一种非常优秀的one-stage目标检测方法,one-stage算法就是目标检测分类是同时完成的,其主要思路是利用CNN提取特征后,均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,物体分类与预测框的回归同时进行,整个过程只需要一步,所以其优势是速度快
    但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡(参见Focal Loss),导致模型准确度稍低。
    SSD的英文全名是Single Shot MultiBox Detector,Single shot说明SSD算法属于one-stage方法,MultiBox说明SSD算法基于多框预测。

    源码下载

    https://github.com/bubbliiiing/ssd-keras
    喜欢的可以点个star噢。

    SSD实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    SSD采用的主干网络是VGG网络,关于VGG的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,这里的VGG网络相比普通的VGG网络有一定的修改,主要修改的地方就是:
    1、将VGG16的FC6和FC7层转化为卷积层。
    2、去掉所有的Dropout层和FC8层;
    3、新增了Conv6、Conv7、Conv8、Conv9。

    在这里插入图片描述
    如图所示,输入的图片经过了改进的VGG网络(Conv1->fc7)和几个另加的卷积层(Conv6->Conv9),进行特征提取:
    a、输入一张图片后,被resize到300x300的shape

    b、conv1,经过两次[3,3]卷积网络,输出的特征层为64,输出为(300,300,64),再2X2最大池化,该最大池化步长为2,输出net为(150,150,64)。

    c、conv2,经过两次[3,3]卷积网络,输出的特征层为128,输出net为(150,150,128),再2X2最大池化,该最大池化步长为2,输出net为(75,75,128)。

    d、conv3,经过三次[3,3]卷积网络,输出的特征层为256,输出net为(75,75,256),再2X2最大池化,该最大池化步长为2,输出net为(38,38,256)。

    e、conv4,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(38,38,512),再2X2最大池化,该最大池化步长为2,输出net为(19,19,512)。

    f、conv5,经过三次[3,3]卷积网络,输出的特征层为512,输出net为(19,19,512),再3X3最大池化,该最大池化步长为1,输出net为(19,19,512)。

    g、利用卷积代替全连接层,进行了一次[3,3]卷积网络和一次[1,1]卷积网络,分别为fc6和fc7,输出的通道数为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)

    h、conv6,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的通道数为512,因此输出的net为(10,10,512)。

    i、conv7,经过一次[1,1]卷积网络,调整通道数,一次步长为2的[3,3]卷积网络,输出的通道数为256,因此输出的net为(5,5,256)。

    j、conv8,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的通道数为256,因此输出的net为(3,3,256)。

    k、conv9,经过一次[1,1]卷积网络,调整通道数,一次padding为valid的[3,3]卷积网络,输出的特征层为256,因此输出的net为(1,1,256)。

    实现代码:

    import keras.backend as K
    from keras.layers import (Activation, Conv2D, Dense, Flatten,
                              GlobalAveragePooling2D, Input, MaxPooling2D, Reshape,
                              ZeroPadding2D, concatenate, merge)
    from keras.models import Model
    
    def VGG16(input_tensor):
        #----------------------------主干特征提取网络开始---------------------------#
        # SSD结构,net字典
        net = {} 
        # Block 1
        net['input'] = input_tensor
        # 300,300,3 -> 150,150,64
        net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv1_1')(net['input'])
        net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv1_2')(net['conv1_1'])
        net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool1')(net['conv1_2'])
        # Block 2
        # 150,150,64 -> 75,75,128
        net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv2_1')(net['pool1'])
        net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv2_2')(net['conv2_1'])
        net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool2')(net['conv2_2'])
        # Block 3
        # 75,75,128 -> 38,38,256
        net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_1')(net['pool2'])
        net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_2')(net['conv3_1'])
        net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv3_3')(net['conv3_2'])
        net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool3')(net['conv3_3'])
        # Block 4
        # 38,38,256 -> 19,19,512
        net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_1')(net['pool3'])
        net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_2')(net['conv4_1'])
        net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv4_3')(net['conv4_2'])
        net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                    name='pool4')(net['conv4_3'])
        # Block 5
        # 19,19,512 -> 19,19,512
        net['conv5_1'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_1')(net['pool4'])
        net['conv5_2'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_2')(net['conv5_1'])
        net['conv5_3'] = Conv2D(512, kernel_size=(3,3),
                                       activation='relu',
                                       padding='same',
                                       name='conv5_3')(net['conv5_2'])
        net['pool5'] = MaxPooling2D((3, 3), strides=(1, 1), padding='same',
                                    name='pool5')(net['conv5_3'])
        # FC6
        # 19,19,512 -> 19,19,1024
        net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
                                         activation='relu', padding='same',
                                         name='fc6')(net['pool5'])
    
        # x = Dropout(0.5, name='drop6')(x)
        # FC7
        # 19,19,1024 -> 19,19,1024
        net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
                                   padding='same', name='fc7')(net['fc6'])
    
        # x = Dropout(0.5, name='drop7')(x)
        # Block 6
        # 19,19,512 -> 10,10,512
        net['conv6_1'] = Conv2D(256, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv6_1')(net['fc7'])
        net['conv6_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv6_padding')(net['conv6_1'])
        net['conv6_2'] = Conv2D(512, kernel_size=(3,3), strides=(2, 2),
                                       activation='relu',
                                       name='conv6_2')(net['conv6_2'])
    
        # Block 7
        # 10,10,512 -> 5,5,256
        net['conv7_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same', 
                                       name='conv7_1')(net['conv6_2'])
        net['conv7_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv7_padding')(net['conv7_1'])
        net['conv7_2'] = Conv2D(256, kernel_size=(3,3), strides=(2, 2),
                                       activation='relu', padding='valid',
                                       name='conv7_2')(net['conv7_2'])
        # Block 8
        # 5,5,256 -> 3,3,256
        net['conv8_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv8_1')(net['conv7_2'])
        net['conv8_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                       activation='relu', padding='valid',
                                       name='conv8_2')(net['conv8_1'])
        # Block 9
        # 3,3,256 -> 1,1,256
        net['conv9_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                       padding='same',
                                       name='conv9_1')(net['conv8_2'])
        net['conv9_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                       activation='relu', padding='valid',
                                       name='conv9_2')(net['conv9_1'])
        #----------------------------主干特征提取网络结束---------------------------#
        return net
    

    2、从特征获取预测结果

    在这里插入图片描述
    由上图可知,我们分别取:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    共六个特征层进行下一步的处理。为了和普通特征层区分,我们称之为有效特征层,来获取预测结果。

    对获取到的每一个有效特征层,我们都需要对其做三个操作,分别是:

    • 一次num_priors x 4的卷积
    • 一次num_priors x num_classes的卷积
    • 计算对应的先验框。

    num_priors指的是该特征层每一个特征点所拥有的先验框数量。上述提到的六个特征层,每个特征层的每个特征点对应的先验框数量分别为4、6、6、6、4、4。

    上述操作分别对应的对象为:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    所有的特征层对应的预测结果的shape如下:
    在这里插入图片描述
    实现代码为:

    
    def SSD300(input_shape, num_classes=21, anchors_size=[30,60,111,162,213,264,315]):
        #---------------------------------#
        #   典型的输入大小为[300,300,3]
        #---------------------------------#
        input_tensor = Input(shape=input_shape)
        
        # net变量里面包含了整个SSD的结构,通过层名可以找到对应的特征层
        net = VGG16(input_tensor)
    
        #-----------------------将提取到的主干特征进行处理---------------------------#
        # 对conv4_3的通道进行l2标准化处理 
        # 38,38,512
        net['conv4_3_norm'] = Normalize(20, name='conv4_3_norm')(net['conv4_3'])
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv4_3_norm_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same', name='conv4_3_norm_mbox_loc')(net['conv4_3_norm'])
        net['conv4_3_norm_mbox_loc_flat'] = Flatten(name='conv4_3_norm_mbox_loc_flat')(net['conv4_3_norm_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv4_3_norm_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv4_3_norm_mbox_conf')(net['conv4_3_norm'])
        net['conv4_3_norm_mbox_conf_flat'] = Flatten(name='conv4_3_norm_mbox_conf_flat')(net['conv4_3_norm_mbox_conf'])
    
        priorbox = PriorBox(input_shape, anchors_size[0], max_size=anchors_size[1], aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv4_3_norm_mbox_priorbox')
        net['conv4_3_norm_mbox_priorbox'] = priorbox(net['conv4_3_norm'])
        
        # 对fc7层进行处理 
        # 19,19,1024
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['fc7_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3),padding='same',name='fc7_mbox_loc')(net['fc7'])
        net['fc7_mbox_loc_flat'] = Flatten(name='fc7_mbox_loc_flat')(net['fc7_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        net['fc7_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3),padding='same',name='fc7_mbox_conf')(net['fc7'])
        net['fc7_mbox_conf_flat'] = Flatten(name='fc7_mbox_conf_flat')(net['fc7_mbox_conf'])
    
        priorbox = PriorBox(input_shape, anchors_size[1], max_size=anchors_size[2], aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='fc7_mbox_priorbox')
        net['fc7_mbox_priorbox'] = priorbox(net['fc7'])
    
        # 对conv6_2进行处理
        # 10,10,512
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
        net['conv6_2_mbox_loc'] = x
        net['conv6_2_mbox_loc_flat'] = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
        net['conv6_2_mbox_conf'] = x
        net['conv6_2_mbox_conf_flat'] = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])
    
        priorbox = PriorBox(input_shape, anchors_size[2], max_size=anchors_size[3], aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv6_2_mbox_priorbox')
        net['conv6_2_mbox_priorbox'] = priorbox(net['conv6_2'])
    
        # 对conv7_2进行处理
        # 5,5,256
        num_priors = 6
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
        net['conv7_2_mbox_loc'] = x
        net['conv7_2_mbox_loc_flat'] = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
        net['conv7_2_mbox_conf'] = x
        net['conv7_2_mbox_conf_flat'] = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])
    
        priorbox = PriorBox(input_shape, anchors_size[3], max_size=anchors_size[4], aspect_ratios=[2, 3],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv7_2_mbox_priorbox')
        net['conv7_2_mbox_priorbox'] = priorbox(net['conv7_2'])
    
        # 对conv8_2进行处理
        # 3,3,256
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
        net['conv8_2_mbox_loc'] = x
        net['conv8_2_mbox_loc_flat'] = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
        net['conv8_2_mbox_conf'] = x
        net['conv8_2_mbox_conf_flat'] = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])
    
        priorbox = PriorBox(input_shape, anchors_size[4], max_size=anchors_size[5], aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv8_2_mbox_priorbox')
        net['conv8_2_mbox_priorbox'] = priorbox(net['conv8_2'])
    
        # 对conv9_2进行处理
        # 1,1,256
        num_priors = 4
        # 预测框的处理
        # num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
        x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
        net['conv9_2_mbox_loc'] = x
        net['conv9_2_mbox_loc_flat'] = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
        # num_priors表示每个网格点先验框的数量,num_classes是所分的类
        x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
        net['conv9_2_mbox_conf'] = x
        net['conv9_2_mbox_conf_flat'] = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
        
        priorbox = PriorBox(input_shape, anchors_size[5], max_size=anchors_size[6], aspect_ratios=[2],
                            variances=[0.1, 0.1, 0.2, 0.2],
                            name='conv9_2_mbox_priorbox')
    
        net['conv9_2_mbox_priorbox'] = priorbox(net['conv9_2'])
    
        # 将所有结果进行堆叠
        net['mbox_loc'] = concatenate([net['conv4_3_norm_mbox_loc_flat'],
                                 net['fc7_mbox_loc_flat'],
                                 net['conv6_2_mbox_loc_flat'],
                                 net['conv7_2_mbox_loc_flat'],
                                 net['conv8_2_mbox_loc_flat'],
                                 net['conv9_2_mbox_loc_flat']],
                                axis=1, name='mbox_loc')
        net['mbox_conf'] = concatenate([net['conv4_3_norm_mbox_conf_flat'],
                                  net['fc7_mbox_conf_flat'],
                                  net['conv6_2_mbox_conf_flat'],
                                  net['conv7_2_mbox_conf_flat'],
                                  net['conv8_2_mbox_conf_flat'],
                                  net['conv9_2_mbox_conf_flat']],
                                 axis=1, name='mbox_conf')
        net['mbox_priorbox'] = concatenate([net['conv4_3_norm_mbox_priorbox'],
                                      net['fc7_mbox_priorbox'],
                                      net['conv6_2_mbox_priorbox'],
                                      net['conv7_2_mbox_priorbox'],
                                      net['conv8_2_mbox_priorbox'],
                                      net['conv9_2_mbox_priorbox']],
                                      axis=1, name='mbox_priorbox')
    
        # 8732,4
        net['mbox_loc'] = Reshape((-1, 4),name='mbox_loc_final')(net['mbox_loc'])
        # 8732,21
        net['mbox_conf'] = Reshape((-1, num_classes),name='mbox_conf_logits')(net['mbox_conf'])
        # 8732,8
        net['mbox_conf'] = Activation('softmax',name='mbox_conf_final')(net['mbox_conf'])
        # 8732,33
        net['predictions'] = concatenate([net['mbox_loc'],
                                        net['mbox_conf'],
                                        net['mbox_priorbox']],
                                        axis=2, name='predictions')
    
        model = Model(net['input'], net['predictions'])
        return model
    

    3、预测结果的解码

    利用SSD的主干网络我们可以获得六个有效特征层,分别是:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    通过对每一个特征层的处理,我们可以获得每个特征层对应的三个内容,分别是:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    我们利用 num_priors x 4的卷积每一个有效特征层对应的先验框 进行调整获得 预测框

    在这里我们简单了解一下每个特征层到底做了什么:
    每一个有效特征层将整个图片分成与其长宽对应的网格如conv4-3的特征层就是将整个图像分成38x38个网格;然后从每个网格中心建立多个先验框,对于conv4-3的特征层来说,它的每个特征点分别建立了4个先验框;

    因此,对于conv4-3整个特征层来讲,整个图片被分成38x38个网格,每个网格中心对应4个先验框,一共建立了38x38x4个,5776个先验框。这些框密密麻麻的遍布在整个图片上网络的预测结果会对这些框进行调整获得预测框。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整,ssd利用num_priors x 4的卷积的结果对先验框进行调整。

    num_priors x 4中的num_priors表示了这个网格点所包含的先验框数量,其中的4表示了x_offset、y_offset、h和w的调整情况。

    x_offset与y_offset代表了真实框距离先验框中心的xy轴偏移情况。
    h和w代表了真实框的宽与高相对于先验框的变化情况。

    SSD解码过程可以分为两部分:
    每个网格的中心点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心;
    利用h和w调整先验框获得预测框的宽和高。
    此时我们获得了预测框的中心和预测框的宽高,已经可以在图片上绘制预测框了。

    想要获得最终的预测结果,还要对每一个预测框进行得分排序非极大抑制筛选
    这一部分基本上是所有目标检测通用的部分。
    1、取出每一类得分大于self.obj_threshold的框和得分
    2、利用框的位置和得分进行非极大抑制。
    实现代码如下:

    def decode_boxes(self, mbox_loc, mbox_priorbox, variances):
        # 获得先验框的宽与高
        prior_width = mbox_priorbox[:, 2] - mbox_priorbox[:, 0]
        prior_height = mbox_priorbox[:, 3] - mbox_priorbox[:, 1]
        # 获得先验框的中心点
        prior_center_x = 0.5 * (mbox_priorbox[:, 2] + mbox_priorbox[:, 0])
        prior_center_y = 0.5 * (mbox_priorbox[:, 3] + mbox_priorbox[:, 1])
    
        # 真实框距离先验框中心的xy轴偏移情况
        decode_bbox_center_x = mbox_loc[:, 0] * prior_width * variances[:, 0]
        decode_bbox_center_x += prior_center_x
        decode_bbox_center_y = mbox_loc[:, 1] * prior_height * variances[:, 1]
        decode_bbox_center_y += prior_center_y
        
        # 真实框的宽与高的求取
        decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[:, 2])
        decode_bbox_width *= prior_width
        decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[:, 3])
        decode_bbox_height *= prior_height
    
        # 获取真实框的左上角与右下角
        decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
        decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
        decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
        decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height
    
        # 真实框的左上角与右下角进行堆叠
        decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
                                        decode_bbox_ymin[:, None],
                                        decode_bbox_xmax[:, None],
                                        decode_bbox_ymax[:, None]), axis=-1)
        # 防止超出0与1
        decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
        return decode_bbox
    
    def detection_out(self, predictions, background_label_id=0, keep_top_k=200, confidence_threshold=0.5):
        #---------------------------------------------------#
        #   :4是回归预测结果
        #---------------------------------------------------#
        mbox_loc = predictions[:, :, :4]
        #---------------------------------------------------#
        #   获得种类的置信度
        #---------------------------------------------------#
        mbox_conf = predictions[:, :, 4:-8]
        #---------------------------------------------------#
        #   获得网络的先验框
        #---------------------------------------------------#
        mbox_priorbox = predictions[:, :, -8:-4]
        #---------------------------------------------------#
        #   variances是一个改变数量级的参数。
        #   所有variances全去了也可以训练,有了效果更好。
        #---------------------------------------------------#
        variances = predictions[:, :, -4:]
    
        results = []
    
        # 对每一张图片进行处理,由于在predict.py的时候,我们只输入一张图片,所以for i in range(len(mbox_loc))只进行一次
        for i in range(len(mbox_loc)):
            results.append([])
            #--------------------------------#
            #   利用回归结果对先验框进行解码
            #--------------------------------#
            decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox[i],  variances[i])
    
            for c in range(self.num_classes):
                if c == background_label_id:
                    continue
                #--------------------------------#
                #   取出属于该类的所有框的置信度
                #   判断是否大于门限
                #--------------------------------#
                c_confs = mbox_conf[i, :, c]
                c_confs_m = c_confs > confidence_threshold
                if len(c_confs[c_confs_m]) > 0:
                    # 取出得分高于confidence_threshold的框
                    boxes_to_process = decode_bbox[c_confs_m]
                    confs_to_process = c_confs[c_confs_m]
                    # 进行iou的非极大抑制
                    feed_dict = {self.boxes: boxes_to_process,
                                    self.scores: confs_to_process}
                    idx = self.sess.run(self.nms, feed_dict=feed_dict)
                    # 取出在非极大抑制中效果较好的内容
                    good_boxes = boxes_to_process[idx]
                    confs = confs_to_process[idx][:, None]
                    # 将label、置信度、框的位置进行堆叠。
                    labels = c * np.ones((len(idx), 1))
                    c_pred = np.concatenate((labels, confs, good_boxes),
                                            axis=1)
                    # 添加进result里
                    results[-1].extend(c_pred)
    
            if len(results[-1]) > 0:
                # 按照置信度进行排序
                results[-1] = np.array(results[-1])
                argsort = np.argsort(results[-1][:, 1])[::-1]
                results[-1] = results[-1][argsort]
                # 选出置信度最大的keep_top_k个
                results[-1] = results[-1][:keep_top_k]
        return results
    

    4、在原图上进行绘制

    通过第三步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    二、训练部分

    1、真实框的处理

    真实框的处理可以分为两个部分,分别是:

    • 找到真实框对应的先验框
    • 对真实框进行编码。

    a、找到真实框对应的先验框

    在这一步中,我们需要找到真实框所对应的先验框,代表这个真实框由某个先验框进行预测。

    我们首先需要将每一个的真实框和所有的先验框进行一个iou计算,这一步做的工作是计算每一个真实框和所有的先验框的重合程度。

    在获得每一个真实框和所有的先验框的重合程度之后,选出和每一个真实框重合程度大于一定门限的先验框。代表这个真实框由这些先验框负责预测。

    由于一个先验框只能负责一个真实框的预测,所以如果某个先验框和多个真实框的重合度较大,那么这个先验框只负责与其iou最大的真实框的预测。

    在这一步后,我们可以找到每一个先验框所负责预测的真实框,在下一步中,我们需要根据这些真实框和先验框获得网络的预测结果。

    实现代码如下:

    def assign_boxes(self, boxes):
        #---------------------------------------------------#
        #   assignment分为3个部分
        #   :4      的内容为网络应该有的回归预测结果
        #   4:-8    的内容为先验框所对应的种类,默认为背景
        #   -8      的内容为当前先验框是否包含目标
        #   -7:     无意义
        #---------------------------------------------------#
        assignment = np.zeros((self.num_priors, 4 + self.num_classes + 8))
        assignment[:, 4] = 1.0
        if len(boxes) == 0:
            return assignment
    
        # 对每一个真实框都进行iou计算
        encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
        #---------------------------------------------------#
        #   在reshape后,获得的encoded_boxes的shape为:
        #   [num_true_box, num_priors, 4+1]
        #   4是编码后的结果,1为iou
        #---------------------------------------------------#
        encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
        
        #---------------------------------------------------#
        #   [num_priors]求取每一个先验框重合度最大的真实框
        #---------------------------------------------------#
        best_iou = encoded_boxes[:, :, -1].max(axis=0)
        best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
        best_iou_mask = best_iou > 0
        best_iou_idx = best_iou_idx[best_iou_mask]
        
        #---------------------------------------------------#
        #   计算一共有多少先验框满足需求
        #---------------------------------------------------#
        assign_num = len(best_iou_idx)
    
        # 将编码后的真实框取出
        encoded_boxes = encoded_boxes[:, best_iou_mask, :]
        assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
        #----------------------------------------------------------#
        #   4代表为背景的概率,设定为0,因为这些先验框有对应的物体
        #----------------------------------------------------------#
        assignment[:, 4][best_iou_mask] = 0
        assignment[:, 5:-8][best_iou_mask] = boxes[best_iou_idx, 4:]
        #----------------------------------------------------------#
        #   -8表示先验框是否有对应的物体
        #----------------------------------------------------------#
        assignment[:, -8][best_iou_mask] = 1
        # 通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的
        return assignment
    

    b、真实框的编码

    利用SSD的主干网络我们可以获得六个有效特征层,分别是:

    • conv4的第三次卷积的特征;
    • fc7卷积的特征;
    • conv6的第二次卷积的特征;
    • conv7的第二次卷积的特征;
    • conv8的第二次卷积的特征;
    • conv9的第二次卷积的特征。

    通过对每一个特征层的处理,我们可以获得每个特征层对应的三个内容,分别是:

    • num_priors x 4的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框的变化情况。**
    • num_priors x num_classes的卷积 用于预测 该特征层上 每一个网格点上 每一个预测对应的种类。
    • 每一个特征层每一个特征点上对应的若干个先验框

    因此,我们直接利用ssd网络预测到的结果,并不是预测框在图片上的真实位置,需要解码才能得到真实位置。

    所以在训练的时候,如果我们需要计算loss函数,这个loss函数是相对于ssd网络的预测结果的。因此我们需要对真实框的信息进行处理,使得它的结构和预测结果的格式是一样的,这样的一个过程我们称之为编码(encode)。用一句话概括编码的过程就是将真实框的位置信息格式转化为ssd预测结果的格式信息

    也就是,我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并求出如果想要得到这样一个真实框,我们的预测结果应该是怎么样的。

    从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。因此我们只需要将解码过程逆过来就是编码过程了。

    因此我们可以利用真实框和先验框进行编码,获得该特征点对应的该先验框应该有的预测结果。预测结果分为两个,分别是:

    • num_priors x 4的卷积
    • num_priors x num_classes的卷积

    前者对应先验框的调整参数,后者对应先验框的种类,这个都可以通过先验框对应的真实框获得。

    实现代码如下:

    def encode_box(self, box, return_iou=True):
        # 计算当前真实框和先验框的重合情况
        iou = self.iou(box)
        encoded_box = np.zeros((self.num_priors, 4 + return_iou))
        
        # 找到每一个真实框,重合程度较高的先验框
        assign_mask = iou > self.overlap_threshold
    
        # 如果没有一个先验框重合度大于self.overlap_threshold
        # 则选择重合度最大的为正样本
        if not assign_mask.any():
            assign_mask[iou.argmax()] = True
        
        # 利用iou进行赋值 
        if return_iou:
            encoded_box[:, -1][assign_mask] = iou[assign_mask]
        
        # 找到对应的先验框
        assigned_priors = self.priors[assign_mask]
    
        #---------------------------------------------#
        #   逆向编码,将真实框转化为ssd预测结果的格式
        #   先计算真实框的中心与长宽
        #---------------------------------------------#
        box_center  = 0.5 * (box[:2] + box[2:])
        box_wh      = box[2:] - box[:2]
        #---------------------------------------------#
        #   再计算重合度较高的先验框的中心与长宽
        #---------------------------------------------#
        assigned_priors_center = 0.5 * (assigned_priors[:, :2] +
                                        assigned_priors[:, 2:4])
        assigned_priors_wh = (assigned_priors[:, 2:4] -
                                assigned_priors[:, :2])
        
        #------------------------------------------------#
        #   逆向求取ssd应该有的预测结果
        #   先求取中心的预测结果,再求取宽高的预测结果
        #   存在改变数量级的参数,默认为[0.1,0.1,0.2,0.2]
        #------------------------------------------------#
        encoded_box[:, :2][assign_mask] = box_center - assigned_priors_center
        encoded_box[:, :2][assign_mask] /= assigned_priors_wh
        encoded_box[:, :2][assign_mask] /= assigned_priors[:, -4:-2]
    
        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_priors_wh)
        encoded_box[:, 2:4][assign_mask] /= assigned_priors[:, -2:]
        return encoded_box.ravel()
    

    2、利用处理完的真实框与对应图片的预测结果计算loss

    loss的计算分为三个部分:
    1、获取所有正标签的框的预测结果的回归loss。
    2、获取所有正标签的种类的预测结果的交叉熵loss。
    3、获取一定负标签的种类的预测结果的交叉熵loss。

    由于在ssd的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有若干个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于ssd的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

    实现代码如下:

    class MultiboxLoss(object):
        def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
                     background_label_id=0, negatives_for_hard=100.0):
            self.num_classes = num_classes
            self.alpha = alpha
            self.neg_pos_ratio = neg_pos_ratio
            if background_label_id != 0:
                raise Exception('Only 0 as background label id is supported')
            self.background_label_id = background_label_id
            self.negatives_for_hard = negatives_for_hard
    
        def _l1_smooth_loss(self, y_true, y_pred):
            abs_loss = tf.abs(y_true - y_pred)
            sq_loss = 0.5 * (y_true - y_pred)**2
            l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
            return tf.reduce_sum(l1_loss, -1)
    
        def _softmax_loss(self, y_true, y_pred):
            y_pred = tf.maximum(y_pred, 1e-7)
            softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                                          axis=-1)
            return softmax_loss
    
        def compute_loss(self, y_true, y_pred):
            batch_size = tf.shape(y_true)[0]
            num_boxes = tf.to_float(tf.shape(y_true)[1])
    
            # --------------------------------------------- #
            #   分类的loss
            #   batch_size,8732,21 -> batch_size,8732
            # --------------------------------------------- #
            conf_loss = self._softmax_loss(y_true[:, :, 4:-8],
                                           y_pred[:, :, 4:-8])
            # --------------------------------------------- #
            #   框的位置的loss
            #   batch_size,8732,4 -> batch_size,8732
            # --------------------------------------------- #
            loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
                                            y_pred[:, :, :4])
    
            # --------------------------------------------- #
            #   获取所有的正标签的loss
            # --------------------------------------------- #
            pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -8],
                                         axis=1)
            pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -8],
                                          axis=1)
    
            # --------------------------------------------- #
            #   每一张图的正样本的个数
            #   batch_size,
            # --------------------------------------------- #
            num_pos = tf.reduce_sum(y_true[:, :, -8], axis=-1)
    
            # --------------------------------------------- #
            #   每一张图的负样本的个数
            #   batch_size,
            # --------------------------------------------- #
            num_neg = tf.minimum(self.neg_pos_ratio * num_pos, num_boxes - num_pos)
            # 找到了哪些值是大于0的
            pos_num_neg_mask = tf.greater(num_neg, 0)
            # --------------------------------------------- #
            #   如果所有的图,正样本的数量均为0
            #   那么则默认选取100个先验框作为负样本
            # --------------------------------------------- #
            has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
            num_neg = tf.concat(axis=0, values=[num_neg, [(1 - has_min) * self.negatives_for_hard]])
            
            # --------------------------------------------- #
            #   从这里往后,与视频中看到的代码有些许不同。
            #   由于以前的负样本选取方式存在一些问题,
            #   我对该部分代码进行重构。
            #   求整个batch应该的负样本数量总和
            # --------------------------------------------- #
            num_neg_batch = tf.reduce_sum(tf.boolean_mask(num_neg, tf.greater(num_neg, 0)))
            num_neg_batch = tf.to_int32(num_neg_batch)
    
            # --------------------------------------------- #
            #   对预测结果进行判断,如果该先验框没有包含物体
            #   那么它的不属于背景的预测概率过大的话
            #   就是难分类样本
            # --------------------------------------------- #
            confs_start = 4 + self.background_label_id + 1
            confs_end = confs_start + self.num_classes - 1
    
            # --------------------------------------------- #
            #   batch_size,8732
            #   把不是背景的概率求和,求和后的概率越大
            #   代表越难分类。
            # --------------------------------------------- #
            max_confs = tf.reduce_sum(y_pred[:, :, confs_start:confs_end], axis=2)
    
            # --------------------------------------------------- #
            #   只有没有包含物体的先验框才得到保留
            #   我们在整个batch里面选取最难分类的num_neg_batch个
            #   先验框作为负样本。
            # --------------------------------------------------- #
            max_confs = tf.reshape(max_confs * (1 - y_true[:, :, -8]), [-1])
            _, indices = tf.nn.top_k(max_confs, k=num_neg_batch)
    
            neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]), indices)
    
            # 进行归一化
            num_pos     = tf.where(tf.not_equal(num_pos, 0), num_pos, tf.ones_like(num_pos))
            total_loss  = tf.reduce_sum(pos_conf_loss) + tf.reduce_sum(neg_conf_loss) + tf.reduce_sum(self.alpha * pos_loc_loss)
            total_loss /= tf.reduce_sum(num_pos)
            return total_loss
    

    训练自己的ssd模型

    ssd整体的文件夹构架如下:
    在这里插入图片描述
    本文使用VOC格式进行训练。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    在训练前利用voc2ssd.py文件生成对应的txt。
    在这里插入图片描述
    再运行根目录下的voc_annotation.py,运行前需要将classes改成你自己的classes。

    classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
    

    在这里插入图片描述
    就会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。
    在这里插入图片描述
    在训练前需要修改model_data里面的voc_classes.txt文件,需要将classes改成你自己的classes。这里的classes的顺序需要和voc_annotation.py中对应。
    在这里插入图片描述
    在train.py里面修改对应的NUM_CLASSES,修改成所需要区分的类的个数+1。运行train.py即可开始训练。
    在这里插入图片描述

    展开全文
  • SSD 目标检测

    千次阅读 2018-09-30 17:23:57
    转载自:...SSD github : https://github.com/weiliu89/caffe/tree/ssd SSD paper :&nbsp;https://arxiv.org/abs/1512.02325 SSD eccv2016 slide pdf : http://do...

    转载自:https://blog.csdn.net/a8039974/article/details/77592395

    SSD github : https://github.com/weiliu89/caffe/tree/ssd

    SSD paper : https://arxiv.org/abs/1512.02325

    SSD eccv2016 slide pdf : http://download.csdn.NET/download/zy1034092330/9940054

    SSD pose estimation paper : http://download.csdn.net/download/zy1034092330/9940059



    图1

    缩进SSD,全称Single Shot MultiBox Detector,是Wei Liu在ECCV 2016上提出的一种目标检测算法,截至目前是主要的检测框架之一,相比Faster RCNN有明显的速度优势,相比YOLO又有明显的mAP优势(不过已经被CVPR 2017的YOLO9000超越)。SSD具有如下主要特点:

    1. 从YOLO中继承了将detection转化为regression的思路,同时一次即可完成网络训练
    2. 基于Faster RCNN中的anchor,提出了相似的prior box;
    3. 加入基于特征金字塔(Pyramidal Feature Hierarchy)的检测方式,相当于半个FPN思路

    本文接下来都以SSD 300为例进行分析。


    1 SSD网络结构

    图2 SSD网络结构(和代码貌似有点差别)

    缩进上图2是原论文中的SSD 300网络结构图。可以看到YOLO在卷积层后接全连接层,即检测时只利用了最高层feature maps(包括Faster RCNN也是如此);而SSD采用了特征金字塔结构进行检测,即检测时利用了conv4-3,conv-7(FC7),conv6-2,conv7-2,conv8_2,conv9_2这些大小不同的feature maps,在多个feature maps上同时进行softmax分类和位置回归,如图3。

    图3 单层feature map预测和特征金字塔预测对比


    2 Prior Box

    缩进在SSD中引入了Prior Box,实际上与anchor非常类似,就是一些目标的预选框,后续通过softmax分类+bounding box regression获得真实目标的位置。SSD按照如下规则生成prior box:

    • 以feature map上每个点的中点为中心(offset=0.5),生成一些列同心的prior box(然后中心点的坐标会乘以step,相当于从feature map位置映射回原图位置)
    • 正方形prior box最小边长为,最大边长为:
    • 每在prototxt设置一个aspect ratio,会生成2个长方形,长宽为: 和 


    图4 prior box

    • 而每个feature map对应prior box的min_size和max_size由以下公式决定,公式中m是使用feature map的数量(SSD 300中m=6):

    第一层feature map对应的min_size=S1,max_size=S2;第二层min_size=S2,max_size=S3;其他类推。在原文中,Smin=0.2,Smax=0.9,但是在SSD 300中prior box设置并不能和paper中上述公式对应:

      min_size max_size
    conv4_3
    30
    60
    fc7
    60
    111
    conv6_2
    111
    162
    conv7_2
    162
    213
    conv8_2
    213
    264
    conv9_2 
    264
    315

    不过依然可以看出,SSD使用低层feature map检测小目标,使用高层feature map检测大目标,这也应该是SSD的突出贡献了。其中SSD 300在conv4_3生成prior box的conv4_3_norm_priorbox层prototxt定义如下:

    1. layer {  
    2.   name: "conv4_3_norm_mbox_priorbox"  
    3.   type: "PriorBox"  
    4.   bottom: "conv4_3_norm"  
    5.   bottom: "data"  
    6.   top: "conv4_3_norm_mbox_priorbox"  
    7.   prior_box_param {  
    8.     min_size: 30.0  
    9.     max_size: 60.0  
    10.     aspect_ratio: 2  
    11.     flip: true  
    12.     clip: false  
    13.     variance: 0.1  
    14.     variance: 0.1  
    15.     variance: 0.2  
    16.     variance: 0.2  
    17.     step: 8  
    18.     offset: 0.5  
    19.   }  
    20. }  

    知道了priorbox如何产生,接下来分析prior box如何使用。这里以conv4_3为例进行分析。

    图5

    从图5可以看到,在conv4_3 feature map网络pipeline分为了3条线路:

    • 经过一次batch norm+一次卷积后,生成了[1, num_class*num_priorbox, layer_height, layer_width]大小的feature用于softmax分类目标和非目标(其中num_class是目标类别,SSD 300中num_class = 21)
    • 经过一次batch norm+一次卷积后,生成了[1, 4*num_priorbox, layer_height, layer_width]大小的feature用于bounding box regression(即每个点一组[dxmin,dymin,dxmax,dymax],参考Faster RCNN 2.5节)
    • 生成了[1, 2, 4*num_priorbox]大小的prior box blob,其中2个channel分别存储prior box的4个点坐标和对应的4个variance

    缩进后续通过softmax分类+bounding box regression即可从priox box中预测到目标,熟悉Faster RCNN的读者应该对上述过程应该并不陌生。其实pribox box的与Faster RCNN中的anchor非常类似,都是目标的预设框,没有本质的差异。区别是每个位置的prior box一般是4~6个,少于Faster RCNN默认的9个anchor;同时prior box是设置在不同尺度的feature maps上的,而且大小不同。

    缩进还有一个细节就是上面prototxt中的4个variance,这实际上是一种bounding regression中的权重。在图4线路(2)中,网络输出[dxmin,dymin,dxmax,dymax],即对应下面代码中bbox;然后利用如下方法进行针对prior box的位置回归:

    1. decode_bbox->set_xmin(  
    2.     prior_bbox.xmin() + prior_variance[0] * bbox.xmin() * prior_width);  
    3. decode_bbox->set_ymin(  
    4.     prior_bbox.ymin() + prior_variance[1] * bbox.ymin() * prior_height);  
    5. decode_bbox->set_xmax(  
    6.     prior_bbox.xmax() + prior_variance[2] * bbox.xmax() * prior_width);  
    7. decode_bbox->set_ymax(  
    8.     prior_bbox.ymax() + prior_variance[3] * bbox.ymax() * prior_height);  

    上述代码可以在SSD box_utils.cpp的void DecodeBBox()函数见到。


    3 Permute,Flatten And Concat Layers

    图6

    缩进上一节以conv4_3 feature map分析了如何检测到目标的真实位置,但是SSD 300是使用包括conv4_3在内的共计6个feature maps一同检测出最终目标的。在网络运行的时候显然不能像图6一样:一个feature map单独计算一次softmax socre+box regression(虽然原理如此,但是不能如此实现)。那么多个feature maps如何协同工作?这时候就要用到Permute,Flatten和Concat这3种层了。其中conv4_3_norm_conf_perm的prototxt定义如下:

    Permute是SSD中自带的层,上面conv4_3_norm_mbox_conf_perm的的定义。Permute相当于交换caffe blob中的数据维度。在正常情况下caffe blob的顺序为:

    bottom blob = [batch_num, channel, height, width]

    经过conv4_3_norm_mbox_conf_perm后的caffe blob为:

    top blob = [batch_num, height, width, channel]

    而Flattlen和Concat层都是caffe自带层,请参照caffe official documentation理解。



    图7 SSD中部分层caffe blob shape变化


    缩进那么接下来以conv4_3和fc7为例分析SSD是如何将不同size的feature map组合在一起进行prediction。图7展示了conv4_3和fc7合并在一起的过程中caffe blob shape变化(其他层类似,考虑到图片大小没有画出来,请脑补)。

    • 对于conv4_3 feature map,conv4_3_norm_priorbox(priorbox层)设置了每个点共有4个prior box。由于SSD 300共有21个分类,所以conv4_3_norm_mbox_conf的channel值为num_priorbox * num_class = 4 * 21 = 84;而每个prior box都要回归出4个位置变换量,所以conv4_3_norm_mbox_loc的caffe blob channel值为4 * 4 = 16。
    • fc7每个点有6个prior box,其他feature map同理。
    • 经过一系列图7展示的caffe blob shape变化后,最后拼接成mbox_conf和mbox_loc。而mbox_conf后接reshape,再进行softmax(为何在softmax前进行reshape,Faster RCNN有提及)。
    • 最后这些值输出detection_out_layer,获得检测结果

    4 SSD网络结构优劣分析

    缩进SSD算法的优点应该很明显:运行速度可以和YOLO媲美,检测精度可以和Faster RCNN媲美。除此之外,还有一些鸡毛蒜皮的优点,不解释了。这里谈谈缺点:

    1. 需要人工设置prior box的min_size,max_size和aspect_ratio值。网络中prior box的基础大小和形状不能直接通过学习获得,而是需要手工设置。而网络中每一层feature使用的prior box大小和形状恰好都不一样,导致调试过程非常依赖经验。
    2. 虽然采用了pyramdial feature hierarchy的思路,但是对小目标的recall依然一般,并没有达到碾压Faster RCNN的级别。作者认为,这是由于SSD使用conv4_3低级feature去检测小目标,而低级特征卷积层数少,存在特征提取不充分的问题。


    5 SSD训练过程


    缩进对于SSD,虽然paper中指出采用了所谓的“multibox loss”,但是依然可以清晰看到SSD loss分为了confidence loss和location loss两部分,其中N是match到GT(Ground Truth)的prior box数量;而α参数用于调整confidence loss和location loss之间的比例,默认α=1。SSD中的confidence loss是典型的softmax loss:


    其中代表第i个prior box匹配到了第j个class为p类别的GT box;而location loss是典型的smooth L1 loss:

    Matching strategy:

    缩进在训练时,groundtruth boxes 与 default boxes(就是prior boxes) 按照如下方式进行配对:

    • 首先,寻找与每一个ground truth box有最大的jaccard overlap的default box,这样就能保证每一个groundtruth box与唯一的一个default box对应起来(所谓的jaccard overlap就是IoU,如图8)。
    • SSD之后又将剩余还没有配对的default box与任意一个groundtruth box尝试配对,只要两者之间的jaccard overlap大于阈值,就认为match(SSD 300 阈值为0.5)。
    • 显然配对到GT的default box就是positive,没有配对到GT的default box就是negative。

    图8 jaccard overlap

    Hard negative mining:
    缩进值得注意的是,一般情况下negative default boxes数量>>positive default boxes数量,直接训练会导致网络过于重视负样本,从而loss不稳定。所以需要采取:
    • 所以SSD在训练时会依据confidience score排序default box,挑选其中confidience高的box进行训练,控制positive:negative=1:3

    Data augmentation:

    缩进数据增广,即每一张训练图像,随机的进行如下几种选择:

    • 使用原始的图像
    • 采样一个 patch,与物体之间最小的 jaccard overlap 为:0.1,0.3,0.5,0.7 或 0.9
    • 随机的采样一个 patch
    采样的 patch 是原始图像大小比例是[0.1,1],aspect ratio在1/2与2之间。当 groundtruth box 的 中心(center)在采样的patch中时,保留重叠部分。在这些采样步骤之后,每一个采样的patch被resize到固定的大小,并且以0.5的概率随机的 水平翻转(horizontally flipped)。


    其实Matching strategy,Hard negative mining,Data augmentation,都是为了加快网络收敛而设计的。尤其是Data augmentation,翻来覆去的randomly crop,保证每一个prior box都获得充分训练而已。不过当数据达到一定量的时候,不建议再进行Data augmentation,毕竟“真”的数据比“假”数据还是要好很多。

    展开全文
  • 睿智的目标检测37——TF2搭建SSD目标检测平台(tensorflow2)学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练...
  • SSD目标检测】3:训练自己的数据集

    万次阅读 多人点赞 2018-07-20 14:22:34
    前言:上一章已经详细介绍了SSD——多目标定位检测识别算法,该框架是Google开源的代码,所以自然有很多前辈做改进了。我也不过是站在前辈的肩膀上,这一章就是讲解如何训练自己的数据集,让SSD框架识别。源码也无偿...
  • SSD目标检测理解

    2017-10-05 14:00:15
    SSD目标检测
  • 睿智的目标检测24—Keras搭建Mobilenet-SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、...
  • SSD目标检测网络预训练模型
  • SSD目标检测原理

    千次阅读 2017-10-04 11:51:42
    SSD目标检测原理
  • ssd目标检测整理

    千次阅读 2017-08-18 22:51:23
    ssd目标检测整理
  • keras版本SSD目标检测模型H5文件,用于模型训练和测试。包含SSD300的权重文件。
  • SSD目标检测算法论文-英文原版,SSD: Single Shot MultiBox Detector
  • 返回主目录 返回 目标检测史 目录 上一章:深度篇——目标检测史(四)细说从 Fast R-CNN到 Faster R-CNN目标检测 ...本小节,细说 SSD目标检测,下一小节细说 YOLO-V3目标检测 六.SSD目标检测 ...
  • 什么是SSD目标检测算法 源码下载 SSD实现思路 一、预测部分 1、主干网络介绍 2、从特征获取预测结果 3、预测结果的解码 4、在原图上进行绘制 二、训练部分 1、真实框的处理 2、...
  • Pytorch搭建SSD目标检测网络

    千次阅读 2020-07-20 21:02:37
    什么是SSD目标检测算法2. 源码下载3. SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、真实框的处理2、利用处理完的真实框与对应图片的预测结果...
  • opencv运行SSD目标检测

    2018-06-20 15:46:06
    VS2015+OPENCV3.4.0+SSD进行目标完整检测的工程,包括图片目标检测和相应的加载视频进行目标检测。直接下载编译即可以运行。
  • 之前搜集的基于SSD目标检测结构的改进论文英文原文,看过的大佬不必在意,如果需要进行相关文档而不愿意费力去找的可以直接下载。
  • SSD目标检测】2:如何制作自己的数据集

    万次阅读 多人点赞 2018-07-20 14:04:07
    想制作自己的数据集当然要先了解SSD使用的数据集VOC2007长啥样。VOC2007下载链接 ,密码是:m5io(VOC2007完整下载有3个压缩包+1个PDF,上面链接里只包含其中一个压缩包VOCtrainval_06-Nov-2007)。打开压缩包就如下...
  • SSD目标检测算法出现在Faster-RCNN和YOLOv1算法之后,融合了二者的优点的one-stage算法。速度比faster-RCNN更快,比YOLOv1算法准确率更高。 如果你对YOLOv1算法不了解的话,可以先了解一下。 关于YOLOv1目标检测算法...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,266
精华内容 1,306
关键字:

ssd目标检测