精华内容
下载资源
问答
  • 之前搜集的基于SSD目标检测结构的改进论文英文原文,看过的大佬不必在意,如果需要进行相关文档而不愿意费力去找的可以直接下载。
  • 睿智的目标检测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算法的人脸目标检测的研究.pdf
  • 上一章我们了解到,物体识别检测算法是在传统CNN算法基础上加上目标区域建议策略和边框回归算法得到的。前辈们的工作主要体现在目标区域建议的改进策略上,从最开始的穷举建议框,到划分图像区域,再到从特征图上...

    4.2 改进的SSD

    上一章我们了解到,物体识别检测算法是在传统CNN算法基础上加上目标区域建议策略和边框回归算法得到的。前辈们的工作主要体现在目标区域建议的改进策略上,从最开始的穷举建议框,到划分图像区域,再到从特征图上生成建议框,算法的改进带来的收益越来越大。本节的主要内容是结合上一章讲的DenseNet和前面讲的SSD目标检测算法,将DenseNet作为SSD算法的基础网络,用于提取深度特征,再结合SSD算法中的目标边框建议策略和边框回归算法,重新构建一个端到端的物体识别检测网络。

    4.2.1 基础网络改进

    在寻找进一步提高检测准确性的方法时,尤其是对于小对象来说,除了提高预测边界框的空间分辨率外,更明显的目标是使用更好的特征网络和增加更多的上下文信息。以前的SSD版本是基于VGG网络,但很多研究人员使用更深的网络取得了更好的精确度。

    大多数目标检测方法,包括Fast R-CNN,YOLO等使用最顶层的卷积层来学习检测不同尺度的物体。尽管这些方法很实用,但是单层模型对于所有可能存在的目标尺寸和形状来说,拟合的能力有些捉襟见肘。有很多种方法可以利用ConvNet中的多个层来提高检测精度。一种方法就是合并不同层的特征图,由于组合特征映射具有来自不同抽象级别输入图像的特征,所以合并特征更具有描述性。但是,组合特征映射不仅显著增加模型的内存占有量,而且也降低了模型的速度。另一种方法使用ConvNet中的不同层来预测不同比例的对象。由于不同层的节点具有不同的接受域,因此从具有较大接受域的层预测较大对象并使用具有较小接受域的层来预测小对象是很自然的。然而,为了很好地检测小物体,这些方法需要使用具有小接受域和密集特征图的浅层信息,这可能导致小物体检测性能低下,因为浅层具有较少的关于物体的语义信息。通过使用跳跃连接,我们可以在特征映射中注入更多的语义信息,从而帮助预测小物体。

    我们的改进是使用DenseNet代替原始SSD结构中的VGG,以提高目标检测准确性。图4-9显示了以DenseNet为基础网络的SSD。在这种结构下,我们在conv5_x之后添加不同尺寸的特征图层,并预测conv3_x,conv5_x和其他特征图上的目标置信度和边框偏移量。
    在这里插入图片描述
    图4-9 SSD基础网络改进图

    4.2.2 预测网络改进

    在原始SSD中,目标函数直接作用于选定的特征映射,并且由于梯度的大小,L2归一化层被用于conv4_3层。研究表明,改进每个任务的子网络可以提高检测准确性。遵循这个原则,我们为每个预测层添加一个密集连接块,如图4-10中模块c所示。我们还尝试原始SSD方法(a)和带有跳跃连接(b)以及两个连续密集块(d)的版本。从实验结果来看,用DenseNet作为SSD的基础网络以及用DenseBlock作为预测网络相比原始SSD有更好的准确度。
    在这里插入图片描述
    图4-10 预测块及变种

    4.3 训练网络

    我们遵循与SSD一样的训练策略。首先,我们必须匹配一组Default Box来定位目标真实边框。对于每个目标真实边框,我们将其与最佳重叠的Default Box以及IoU大于阈值(例如0.5)的任何Box匹配,Box尺寸如表4-1所示。在未匹配的Box中,我们根据置信度损失来选择某些Box作为负样本,并使正负样本的比例为1:3。然后我们联合定位损失函数(如Smooth L1)和置信度损失(如Softmax),并最小化损失函数。最后采用随机光度测量失真和随机翻转裁剪区域来完成大量数据增强。

    在这里插入图片描述
    实验都基于DenseNet,它是在ILSVRC CLS-LOC数据集上预先训练的。遵循R-FCN的建议,我们将conv5阶段的有效步幅从32像素改为16个像素,以增加特征图的分辨率。在conv5阶段中第一个带有步幅2的卷积层被修改为1。然后遵循trous算法,对于卷积核大小大于1的conv5阶段中的所有卷积层,我们将它们的扩张幅度从1增加到2。跟SSD一样,我们使用DenseNet块生成一些额外的层,以弥补逐渐减少的特征图。

    我们在VOC 2007和VOC 2012的训练集上联合训练了模型。对于原始的SSD模型,我们对321×321输入的模型使用32的批处理大小,对于513×513输入的模型使用20的批处理大小,并且对于前40k的迭代开始0.001的学习率。然后,我们在60K迭代时将其降至0.0001,70K降至0.00001。我们将这个训练好的SSD模型作为网络的预训练模型。对于第一阶段,我们将前10k次迭代的学习率设为0.001,然后以0.0001的学习率继续训练10k次迭代。对于第二阶段,我们对整个网络进行微调,对于第一个20k迭代,学习率为0.001,并且在接下来的20k迭代中将其减小到0.0001。

    表4-2显示了被选中的VGG结构和DenseNet结构中特征层,深度depth就是网络中被选中层的位置。网络只考虑卷积层和池化层。注意这个网络的第一个预测层的深度很重要,虽然DenseNet包含101层,但我们需要使用密集层来预测较小的对象,只能选择conv3_x块中的最后一层。表4-2表示VGG与DenseNet中被选中用于预测的层所在的位置。
    在这里插入图片描述

    4.4 实验结果对比与分析

    如下图4-7所示,左侧是原始SSD检测出的图像,右侧是改进后的SSD检测出的图像。很显然,改进后的SSD网络对小物体比较敏感,能够对小物体产生很好的拟合。类似的对比见图4-8和图4-9。
    在这里插入图片描述
    图4-7 SSD(左)和改进SSD(右)的检测效果
    在这里插入图片描述
    图4-8 检测小目标效果图(1)
    在这里插入图片描述
    图4-9 检测小目标效果图(2)

    表4-3显示了在PASCAL VOC2007数据集上测试的结果。SSD300和SSD512采用最新的SSD扩展数据增强技术,它已经比许多其他最先进的检测器更好。通过将VGGNet替换为DenseNet,如果输入图像很小,则性能相似。例如,SSD-Res类似于SSD-VGG,尽管DenseNet似乎能够更快地收敛(例如,我们只使用VGGNet一半的迭代时间来训练我们的SSD版本)。有趣的是,当我们增加输入图像尺寸时,DenseNet比VGGNet好大约1%。我们推测,DenseNet具有较大的输入图像尺寸是非常重要的,因为它比VGGNet更深,因此对象在某些非常深的层(例如conv5_x)中仍然具有强大的空间信息。更重要的是,我们看到通过添加跳过连接,我们的DSSD321和DSSD513比没有这些额外层的准确率要好1到1.5%。这证明了我们提出的方法的有效性。值得注意的是,即使模型不需要任何手工的上下文区域信息,DSSD513也比尝试包含上下文信息的其他方法好得多。此外,我们的单模型精度比当前最先进的探测器R-FCN提高1%。
    在这里插入图片描述
    总之,SSD在测试任务中都显示出对具有特定背景和小对象的类的巨大改进。例如,飞机,船,牛和羊有非常特殊的背景,比如飞机后的天空,牛后的草地等。这表明SSD中小物体检测的弱点可以通过所提出的模型来解决,并且对于具有独特上下文的类来说,性能更好。

    为了了解我们添加到SSD中结构的有效性,我们在VOC2007上运行具有不同设置的模型,并在表4-4中记录它们的评估。PM即为Prediction模块。使用321像素作为输入的纯SSD是76.4%mAP。这个数字实际上比VGG模型差。通过添加预测模块,我们可以看到结果正在改进,最好的情况是在预测之前使用一个残差块作为中间层。这个想法是为了避免允许目标函数的梯度直接流入剩余网络的主干。如果在预测之前叠加两个PM,我们看不到太大的差异。

    在这里插入图片描述

    4.5 本章小结

    本章首先阐述了物体检测算法的原理,并重点介绍了SSD检测算法的原理,即在基础网络上加上检测区域建议和边框回归策略。原始SSD的缺点在于预测层太浅,不能很好的拟合小目标。我们将第三章介绍的DenseNet作为SSD的基础网络,并使用“跳跃连接”的方式在预测层上进行深度特征提取。利用DenseNet拥有良好的上下文信息的优势,成功改造了SSD,提高了速度和精度。使其对小目标也有很好的拟合。

    展开全文
  • 为提升原始SSD算法的小目标检测精度及鲁棒性,提出一种基于通道注意力机制的SSD目标检测算法。在原始SSD算法的基础上对高层特征图进行全局池化操作,结合通道注意力机制增强高层特征图的语义信息,并利用膨胀卷积...
  • 基于tensorflow版SSD目标检测(linux系统) 致谢声明 本文在学习《目标检测实践_tensorflow版SSD数据准备》的基础上优化并总结,此博客链接:https://www.jianshu.com/p/3d9436b4cb66,感谢此博客作者。 1、前言 在...

    基于tensorflow版SSD目标检测(linux系统)

    致谢声明

    本文在学习《目标检测实践_tensorflow版SSD数据准备》的基础上优化并总结,此博客链接:https://www.jianshu.com/p/3d9436b4cb66,感谢此博客作者。
    1、前言
    在进行本文操作之前,需要先安装好tensorflow,anaconda。
    本文作者的环境:python3.7.5、linux、tensorflow1.13.1。
    2、下载图片
    本文作者将进行猫(cat)和狗(dog)的识别,同学们可从网上下载,本文作者将提供python爬虫下载图片。链接: https://pan.baidu.com/s/1PHsnL9OH8ABgutP8Ze_GaA 提取码: rdpr
    在桌面新建文件夹 目标识别 ,在该文件下放下载好的图片的文件夹 photos
    3、给图片打标签
    本文作者打标签将使用 labelImg-master 工具。在 labelImg-master文件下,打开终端,输入 python labelImg.py,打开后的界面如下图所示:在这里插入图片描述
    点击Open Dir,首先打开桌面的目标检测文件夹,在选中文件夹photos的情况下,点击选择文件夹按钮。
    注意:只需要鼠标选中文件夹photos,不需要进入文件夹photos中。
    在输入法为英文输入的情况下,按键盘上的w键则可以开始绘制方框,方框会框住图片中的物体。完成绘制方框后,还需要为方框标上类别,如下图所示。在这里插入图片描述
    注意:每完成一张图的打标签,一定要记得保存。
    标签打完之后,在photos文件夹下会出现 .xml 文件。
    4、数据准备
    在 目标检测 文件夹下打开终端,输入 jupyter notebook ,会打开一个新的标签页,如下图:在这里插入图片描述
    点击 新建——python3,将出现一个新的未命名标签页,如下图:在这里插入图片描述
    点击 未命名 进行重命名。为了同学们方便对照学习,建议同学们与本文作者的重命名一样。
    <1>、xml文件转csv文件。新建文件xml_to_csv.ipynb。
    注意:xml转csv的意思是,将xml文件中的信息整合到csv文件中。
    代码链接: https://pan.baidu.com/s/1mJQmBTd0RsVHiZm4L4lnPQ 提取码: e7pi
    如下图 :在这里插入图片描述
    运行代码后会显示 成功产生文件train.csv,训练集共有多少张图片
    成功产生文件test.csv,测试集共有多少张图片。
    <2>、csv 文件转 tfrecord 文件。新建文件csv_to_tfrecord.ipynb。
    注意:csv转tfrecord的意思是,将csv文件中的信息和图片数据整合到tfrecord文件中。
    代码链接: https://pan.baidu.com/s/1jtE2fEFu2BVL4oFNuM3Sog 提取码: s4z2
    如下图:在这里插入图片描述
    运行代码后会显示 成功产生tfrecord文件,保存在路径:training/train.tfrecord
    成功产生tfrecord文件,保存在路径:training/test.tfrecord。
    5、配置环境
    下载文件object_detection
    链接: https://pan.baidu.com/s/1wIwipa-HVyf9amQrSFJuzw 提取码: gu8w
    下载文件nets
    链接: https://pan.baidu.com/s/1Z-Fy5oKQwOfki-n4sbidsA 提取码: ngdf
    6、编写pbtxt文件
    在桌面文件夹目标检测的文件夹training中,创建文本文件my_label_map.pbtxt。
    复制下面一段内容到文本文件my_label_map.pbtxt中。

    item {
        name : "cat"
        id : 1
    }
    item {
        name : "dog"
        id : 2
    }
    

    注意:此处的"cat" "dog"应该与打的标签名称一致。如果同学们以后自己练习时,可根据自己的标签名称和数量修改my_label_map.pbtxt文件。
    7、编写tensorflow_model.pth文件
    在桌面文件夹目标检测的文件夹中,创建文本文件tensorflow_model.pth。内容为:
    目标检测文件夹的路径
    目标检测文件夹的路径/silm

    例如本文作者:/home/lmj/桌面/目标检测2
    /home/lmj/桌面/目标检测2/slim

    注意:将文本文件tensorflow_model.pth编辑好之后,复制到/anaconda3/lib/python3.7/site-packages路径下
    如果没有编写配置tensorflow_model.pth,训练模型时会报错:

    Traceback (most recent call last):
      File "object_detection/model_main.py", line 25, in <module>
        from object_detection import model_hparams
    ModuleNotFoundError: No module named 'object_detection'
    

    在这里插入图片描述
    8、编写配置文件
    <1>、下载配置文件
    在桌面文件夹目标检测的文件夹training中,创建配置文件ssdlite_mobilenet_v2_coco.config。
    链接: https://pan.baidu.com/s/1ERp0zJ4cpI5VHHeHcyI_Rg 提取码: ud28
    <2>、修改配置文件
    原生配置文件中的需要修改的部分:
    a、第9行的num_classes,对于本文来说,此数设置为2。因为只有两类标签“cat” “dog”
    b、第143行的batch_size,对于本文来说,此数设置为5,读者根据自己的电脑配置,可以调高或者调低。
    c、第177行input_path设置成"training/train.tfrecord"。
    d、第179行label_map_path设置成"training/my_label_map.pbtxt"。
    e、第191行input_path设置成"training/test.tfrecord"。
    f、第193行label_map_path设置成"training/my_label_map.pbtxt"。
    g、第158、159这2行需要删除。

    9、训练数据
    在桌面目标检测文件夹下打开终端,输入代码:
    python object_detection/model_main.py --pipeline_config_path=training/ssdlite_mobilenet_v2_coco.config --model_dir=training --alsologtostderr按下回车开始训练。训练时不可关闭这个终端,否则训练终止。训练次数可在目标检测文件夹下的training文件中查看。当终端显示 Use tf.data.Dataset.batch(..., drop_remainder=True).时说明没有出现错误。
    10、查看模型训练情况
    模型训练稳定地进行后,在桌面的目标检测文件夹中新打开一个终端,终端中运行命令:tensorboard --logdir=training。运行结束后,终端中会有一个链接(例如本文作者http://lmj-Inspiron-3442:6006 ),将此链接复制到浏览器中,可查看模型训练情况(建议使用谷歌浏览器)。如果训练次数较少,模型训练会不太理想,识别度较低或者识别错误等等,建议训练20000次。本文作者当训练8000次左右时可查看到模型,训练次数较少时输入的链接打不开。
    11、模型测试
    <1>、在目标检测文件夹下打开终端,输入代码:

    python object_detection/export_inference_graph.py --input_type=image_tensor --pipeline_config_path=training/ssdlite_mobilenet_v2_coco.config --trained_checkpoint_prefix=training/model.ckpt-35549 --output_directory=cat_35549_graph
    

    注意:这里的 cat_35549_graph名称是导出的模型的名称,可自行修改,但数字 35549 是自己训练模型的次数,训练了N次,这里的数字就是N。
    <2>、将导出的模型文件放到object_detection文件夹下。
    <3>、复制my_label_map.pbtxt文件到object_detection文件夹下的data文件夹中。
    <4>、在object_detection文件下的test_images文件夹中放入我们要测试的图片。
    <5>在object_detection文件下打开终端,输入jupyter notebook,回车,此时将打开jupyter notebook。
    <6>打开object_detection_tutorial.ipynb。需要修改一下地方:在这里插入图片描述
    MODEL_NAME = 后面的名称根据导出的模型名称修改。在这里插入图片描述
    {}.jpg根据自己的测试图片的名称修改,(1, 2)为测试图片的数量。
    <7>、每个模块挨个运行,否则会出现/home/lmj/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:22: UserWarning: Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.这样的错误。若没有错误将会显示图片

    在这里插入图片描述
    此时,猫和狗的目标检测已经基本结束。时间仓促,文章肯定有不足的地方和错误的地方,希望大家多多指教。如果同学们在练习时有啥问题可以留言评论,大家多多交流。

    展开全文
  • SSD目标检测】3:训练自己的数据集

    万次阅读 多人点赞 2018-07-20 14:22:34
    前言:上一章已经详细介绍了SSD——多目标定位检测识别算法,该框架是Google开源的代码,所以自然有很多前辈做改进了。我也不过是站在前辈的肩膀上,这一章就是讲解如何训练自己的数据集,让SSD框架识别。源码也无偿...
  • 针对SSD算法在计算机视觉中检测多目标时出现的漏检现象,在SSD算法的基础上,将VGG-16网络替换为密集型网络,并对目标检测后的所有预测框进行非极大值抑制算法改进,使得每层网络都直接连接输入和损失,保证所提取到...
  • 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...
  • SSD目标检测原理

    千次阅读 2017-10-04 11:51:42
    SSD目标检测原理
  • 目标检测是机器人技术领域中重要的技术环节,而作为...本文就将针对以上问题,提出一种基于SSD_MobileNet框架,结合独立制作的图像数据集训练定制的目标检测模型,并将模型集成到ROS平台实现快速准确的目标检测功能.
  • 提出了一种基于深度学习的多视窗 SSD(Single Shot multibox Detector)目标检测方法。首先 阐述了经典 SSD 方法的模型与工作原理,并根据卷积感受野的概念和模型特征层与原始图像的映射 关系,分析了各层级卷积感受野...
  • 【深度学习】基于SSD的实时视频目标检测

    千次阅读 热门讨论 2019-12-30 13:13:38
    这一期我们基于SSD图像目标检测,做一个实时的视频目标检测
  • 使用目标检测中流行的SSD算法,智能识别中国马路上的交通标志牌。文档末端附开源代码连接
  • 睿智的目标检测37——TF2搭建SSD目标检测平台(tensorflow2)学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练...
  • SSD目标检测

    2021-04-20 12:08:05
    SSD,全称Single Shot MultiBox Detector,是Wei Liu在ECCV 2016上提出的一种目标检测算法,是主要的检测框架之一,相比Faster RCNN有明显的速度优势,相比YOLO又有明显的mAP优势。 SSD主要用来解决目标检测的问题...
  • 基于caffe的SSD目标检测——训练集生成和lmdb文件的制作 https://github.com/weiliu89/caffe/tree/ssd 一级目录: build cmake CONTRIBUTING.md data docker examples INSTALL.md LICENSE Makefile.config ...
  • 该代码实现了基于一张照片上的20种目标检测,检测率高达80%以上。
  • opencv运行SSD目标检测

    2018-06-20 15:46:06
    VS2015+OPENCV3.4.0+SSD进行目标完整检测的工程,包括图片目标检测和相应的加载视频进行目标检测。直接下载编译即可以运行。
  • 睿智的目标检测24—Keras搭建Mobilenet-SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、...
  • SSD目标检测算法简介: 论文地址:https://arxiv.org/abs/1512.02325 全称:Singel Shot MultiBox Detector(那么为什么不叫SSMD呢,可能因为不太好听吧) SSD属于one-stage目标检测算法,就是目标的分类和检测任务是...
  • 针对目前目标检测技术中小目标检测困难问题, 提出了一种基于SSD (Single Shot multibox Detector) 改进的小目标检测算法Bi-SSD (Bi-directional Single Shot multibox Detector). 该算法为SSD的浅层特征设计了小目标...
  • 返回主目录 返回 目标检测史 目录 上一章:深度篇——目标检测史(四)细说从 Fast R-CNN到 Faster R-CNN目标检测 ...本小节,细说 SSD目标检测,下一小节细说 YOLO-V3目标检测 六.SSD目标检测 ...
  • SSD是目标检测的主要框架之一,本博客对基于SSD结构设计的各种目标检测网络做一个总结。 目前主要有以下这些: original SSD : https://github.com/weiliu89/caffe/tree/ssd ...
  • 用opencv框架实现的基于SSD目标检测,c++语言版,里面有下载好的SSD模型,有测试图片,运行环境是VS2017+opencv4.0
  • Pytorch搭建SSD目标检测网络

    千次阅读 2020-07-20 21:02:37
    什么是SSD目标检测算法2. 源码下载3. SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、真实框的处理2、利用处理完的真实框与对应图片的预测结果...
  • 各位好,今天主要讲解基于目标检测的图像分类,之前在做图像分类的模型时,分类准确度始终是个问题。特别是在公司做项目时,要对图像内容进行识别,图像样式进200多种,在对其进行识别时,困难十分大,故要对其分类...
  • 小论文打算写的是基于ssd的算法,ssd是之前看的论文,无奈记性太差理解力太差,忙一段时间别的事,之前看过的就差不多忘光了。。。故做一个记录贴,以便加深记忆和理解。 目前基于深度学习的目标检测算法主要分为两...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,928
精华内容 4,771
关键字:

基于ssd的目标检测