精华内容
下载资源
问答
  • 睿智目标检测20——利用mAP计算目标检测精确度

    万次阅读 多人点赞 2020-03-06 17:12:48
    睿智目标检测20——利用mAP计算目标检测精确度学习前言GITHUB代码下载知识储备1、IOU的概念2、TP TN FP FN的概念3、precision(精确度)和recall(召回率)4、概念举例5、单个指标的局限性什么是AP绘制mAP ...

    学习前言

    好多人都想算一下目标检测的精确度,mAP的概念虽然不好理解,但是理解了就很懂。
    在这里插入图片描述

    GITHUB代码下载

    这个是用来绘制mAP曲线的。
    https://github.com/Cartucho/mAP
    这个是用来获取绘制mAP曲线所需的txt的
    https://github.com/bubbliiiing/count-mAP-txt

    知识储备

    1、IOU的概念

    IOU的概念应该比较简单,就是衡量预测框和真实框的重合程度。
    下图是一个示例:图中绿色框为实际框(好像不是很绿……),红色框为预测框,当我们需要判断两个框之间的关系时,主要就是判断两个框的重合程度。
    在这里插入图片描述
    计算IOU的公式为:
    在这里插入图片描述
    可以看到IOU是一个比值,即交并比。
    在分子部分,值为预测框和实际框之间的重叠区域;
    在分母部分,值为预测框和实际框所占有的总区域。
    在这里插入图片描述
    在这里插入图片描述
    交区域和并区域的比值,就是IOU。

    2、TP TN FP FN的概念

    TP TN FP FN里面一共出现了4个字母,分别是T F P N。
    T是True;
    F是False;
    P是Positive;
    N是Negative。

    T或者F代表的是该样本 是否被正确分类。
    P或者N代表的是该样本 被预测成了正样本还是负样本。

    TP(True Positives)意思就是被分为了正样本,而且分对了。
    TN(True Negatives)意思就是被分为了负样本,而且分对了,
    FP(False Positives)意思就是被分为了正样本,但是分错了(事实上这个样本是负样本)。
    FN(False Negatives)意思就是被分为了负样本,但是分错了(事实上这个样本是这样本)。

    在mAP计算的过程中主要用到了,TP、FP、FN这三个概念。

    3、precision(精确度)和recall(召回率)

    P r e c i s i o n = T P T P + F P Precision= \frac{TP}{TP+FP} Precision=TP+FPTP
    TP是分类器认为是正样本而且确实是正样本的例子,FP是分类器认为是正样本但实际上不是正样本的例子,Precision翻译成中文就是“分类器认为是正类并且确实是正类的部分占所有分类器认为是正类的比例”。

    R e c a l l = T P T P + F N Recall= \frac{TP}{TP+FN} Recall=TP+FNTP
    TP是分类器认为是正样本而且确实是正样本的例子,FN是分类器认为是负样本但实际上不是负样本的例子,Recall翻译成中文就是“分类器认为是正类并且确实是正类的部分占所有确实是正类的比例”。

    4、概念举例

    在这里插入图片描述
    如图所示,蓝色的框是 真实框绿色和红色的框是 预测框,绿色的框是正样本,红色的框是负样本。一般来讲,当预测框和真实框IOU>=0.5时,被认为是正样本。
    因此对于这幅图来讲。
    真实框一共有3个,正样本一共有2个,负样本一共有2个。
    此时
    P r e c i s i o n = 2 2 + 2 = 1 / 2 Precision= \frac{2}{2+2} = 1/2 Precision=2+22=1/2
    R e c a l l = 2 2 + 1 = 2 / 3 Recall= \frac{2}{2+1} = 2/3 Recall=2+12=2/3

    5、单个指标的局限性

    在目标检测算法里面有一个非常重要的概念是置信度,如果置信度设置的高的话,预测的结果和实际情况就很符合,如果置信度低的话,就会有很多误检测。

    假设一幅图里面总共有3个正样本,目标检测对这幅图的预测结果有10个,其中3个实际上是正样本,7个实际上是负样本。对应置信度如下。
    在这里插入图片描述
    如果我们将可以接受的置信度设置为0.95的话,那么目标检测算法就会将序号为1的样本作为正样本,其它的都是负样本。此时TP = 1,FP = 0,FN = 2。
    P r e c i s i o n = 1 1 + 0 = 1 Precision= \frac{1}{1+0} = 1 Precision=1+01=1
    R e c a l l = 1 1 + 2 = 1 / 3 Recall= \frac{1}{1+2} = 1/3 Recall=1+21=1/3
    此时Precision非常高,但是事实上我们只检测出一个正样本,还有两个没有检测出来,因此只用Precision就不合适。

    这个时候如果我们将可以接受的置信度设置为0.35的话,那么目标检测算法就会将序号为1的样本作为正样本,其它的都是负样本。此时TP = 3,FP = 3,FN = 0。
    P r e c i s i o n = 3 3 + 3 = 1 / 2 Precision= \frac{3}{3+3} = 1/2 Precision=3+33=1/2
    R e c a l l = 3 3 + 0 = 1 Recall= \frac{3}{3+0} = 1 Recall=3+03=1
    此时Recall非常高,但是事实上目标检测算法认为是正样本的样本里面,有3个样本确实是正样本,但有三个是负样本,存在非常严重的误检测,因此只用Recall就不合适。

    二者进行结合才是评价的正确方法。

    什么是AP

    AP事实上指的是,利用不同的Precision和Recall的点的组合,画出来的曲线下面的面积。
    如下面这幅图所示。
    在这里插入图片描述
    当我们取不同的置信度,可以获得不同的Precision和不同的Recall,当我们取得置信度够密集的时候,就可以获得非常多的Precision和Recall。

    此时Precision和Recall可以在图片上画出一条线,这条线下部分的面积就是某个类的AP值。

    mAP就是所有的类的AP值求平均。

    绘制mAP

    我们首先在这个github上下载绘制mAP所需的代码。
    https://github.com/Cartucho/mAP
    在这个代码中,如果想要绘制mAP则需要三个内容。分别是:
    detection-results:指的是预测结果的txt。
    在这里插入图片描述
    ground-truth:指的是真实框的txt。
    在这里插入图片描述
    image-optional:指的是图片,有这个可以可视化,但是这个可以没有。
    在这里插入图片描述
    我们需要生成这三个内容,此时下载第二个库,这个是我拿我制作的ssd代码写的一个可以生成对应txt的例子。
    https://github.com/bubbliiiing/count-mAP-txt
    我们首先将整个VOC的数据集放到VOCdevikit中
    在这里插入图片描述
    然后修改voc2ssd.py里面的trainval_percent,一般用数据集的10%或者更少用于测试。如果大家放进VOCdevikit的数据集不是全部数据,而是已经筛选好的测试数据集的话,那么就把trainval_percent设置成0,表示全部的数据都用于测试。
    在这里插入图片描述
    然后运行voc2ssd.py。
    此时会生成test.txt,存放用于测试的图片的名字。
    在这里插入图片描述
    然后依次运行主目录下的get_dr_txt.py和get_gt_txt.py获得预测框对应的txt和真实框对应的txt。
    get_dr_txt.py是用来检测测试集里面的图片的,然后会生成每一个图片的检测结果,我重写了detect_image代码,用于生成预测框的txt。
    利用for循环检测所有的图片。
    在这里插入图片描述
    get_dr_txt.py是用来获取测试集中的xml,然后根据每个xml的结果生成真实框的txt。
    利用for循环检测所有的xml。
    在这里插入图片描述
    完成后我们会在input获得三个文件夹。
    在这里插入图片描述
    此时把input内部的文件夹复制到mAP的代码中的input文件夹内部就可以了,然后我们运行mAP的代码中的main.py,运行结束后,会生成mAP相关的文件。
    在这里插入图片描述
    结果生成在Result里面。
    在这里插入图片描述

    展开全文
  • 睿智目标检测27——Pytorch搭建Faster R-CNN目标检测平台学习前言什么是FasterRCNN目标检测算法源码下载Faster-RCNN实现思路一、预测部分1、主干网络介绍2、获得Proposal建议框3、Proposal建议框的解码4、对...

    学习前言

    好的pytorch版本也应该有个faster rcnn。
    在这里插入图片描述

    什么是FasterRCNN目标检测算法

    在这里插入图片描述
    Faster-RCNN是一个非常有效的目标检测算法,虽然是一个比较早的论文, 但它至今仍是许多目标检测算法的基础。

    Faster-RCNN作为一种two-stage的算法,与one-stage的算法相比,two-stage的算法更加复杂且速度较慢,但是检测精度会更高。

    事实上也确实是这样,Faster-RCNN的检测效果非常不错,但是检测速度与训练速度有待提高。

    源码下载

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

    Faster-RCNN实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    Faster-RCNN可以采用多种的主干特征提取网络,常用的有VGG,Resnet,Xception等等,本文以Resnet网络为例子来给大家演示一下。

    Faster-Rcnn对输入进来的图片尺寸没有固定,但是一般会把输入进来的图片短边固定成600,如输入一张1200x1800的图片,会把图片不失真的resize到600x900上。

    ResNet50有两个基本的块,分别名为Conv Block和Identity Block,其中Conv Block输入和输出的维度是不一样的,所以不能连续串联,它的作用是改变网络的维度;Identity Block输入维度和输出维度相同,可以串联,用于加深网络的。
    Conv Block的结构如下:
    在这里插入图片描述
    Identity Block的结构如下:
    在这里插入图片描述
    这两个都是残差网络结构。

    Faster-RCNN的主干特征提取网络部分只包含了长宽压缩了四次的内容,第五次压缩后的内容在ROI中使用。即Faster-RCNN在主干特征提取网络所用的网络层如图所示。

    以输入的图片为600x600为例,shape变化如下:
    在这里插入图片描述
    最后一层的输出就是公用特征层。

    在代码里里面,我们使用resnet50()函数来获得resnet50的公用特征层。

    其中features部分为公用特征层,classifier部分为第二阶段用到的分类器。

    import math
    
    import torch.nn as nn
    from torchvision.models.utils import load_state_dict_from_url
    
    class Bottleneck(nn.Module):
        expansion = 4
        def __init__(self, inplanes, planes, stride=1, downsample=None):
            super(Bottleneck, self).__init__()
            self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
            self.bn1 = nn.BatchNorm2d(planes)
    
            self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
            self.bn2 = nn.BatchNorm2d(planes)
    
            self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
            self.bn3 = nn.BatchNorm2d(planes * 4)
    
            self.relu = nn.ReLU(inplace=True)
            self.downsample = downsample
            self.stride = stride
    
        def forward(self, x):
            residual = x
    
            out = self.conv1(x)
            out = self.bn1(out)
            out = self.relu(out)
    
            out = self.conv2(out)
            out = self.bn2(out)
            out = self.relu(out)
    
            out = self.conv3(out)
            out = self.bn3(out)
            if self.downsample is not None:
                residual = self.downsample(x)
    
            out += residual
            out = self.relu(out)
    
            return out
    
    class ResNet(nn.Module):
        def __init__(self, block, layers, num_classes=1000):
            #-----------------------------------#
            #   假设输入进来的图片是600,600,3
            #-----------------------------------#
            self.inplanes = 64
            super(ResNet, self).__init__()
    
            # 600,600,3 -> 300,300,64
            self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
            self.bn1 = nn.BatchNorm2d(64)
            self.relu = nn.ReLU(inplace=True)
    
            # 300,300,64 -> 150,150,64
            self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=True)
    
            # 150,150,64 -> 150,150,256
            self.layer1 = self._make_layer(block, 64, layers[0])
            # 150,150,256 -> 75,75,512
            self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
            # 75,75,512 -> 38,38,1024 到这里可以获得一个38,38,1024的共享特征层
            self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
            # self.layer4被用在classifier模型中
            self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
            
            self.avgpool = nn.AvgPool2d(7)
            self.fc = nn.Linear(512 * block.expansion, num_classes)
    
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                    m.weight.data.normal_(0, math.sqrt(2. / n))
                elif isinstance(m, nn.BatchNorm2d):
                    m.weight.data.fill_(1)
                    m.bias.data.zero_()
    
        def _make_layer(self, block, planes, blocks, stride=1):
            downsample = None
            #-------------------------------------------------------------------#
            #   当模型需要进行高和宽的压缩的时候,就需要用到残差边的downsample
            #-------------------------------------------------------------------#
            if stride != 1 or self.inplanes != planes * block.expansion:
                downsample = nn.Sequential(
                    nn.Conv2d(self.inplanes, planes * block.expansion,kernel_size=1, stride=stride, bias=False),
                    nn.BatchNorm2d(planes * block.expansion),
                )
            layers = []
            layers.append(block(self.inplanes, planes, stride, downsample))
            self.inplanes = planes * block.expansion
            for i in range(1, blocks):
                layers.append(block(self.inplanes, planes))
            return nn.Sequential(*layers)
    
        def forward(self, x):
            x = self.conv1(x)
            x = self.bn1(x)
            x = self.relu(x)
            x = self.maxpool(x)
    
            x = self.layer1(x)
            x = self.layer2(x)
            x = self.layer3(x)
            x = self.layer4(x)
    
            x = self.avgpool(x)
            x = x.view(x.size(0), -1)
            x = self.fc(x)
            return x
    
    def resnet50(pretrained = False):
        model = ResNet(Bottleneck, [3, 4, 6, 3])
        if pretrained:
            state_dict = load_state_dict_from_url("https://download.pytorch.org/models/resnet50-19c8e357.pth", model_dir="./model_data")
            model.load_state_dict(state_dict)
        #----------------------------------------------------------------------------#
        #   获取特征提取部分,从conv1到model.layer3,最终获得一个38,38,1024的特征层
        #----------------------------------------------------------------------------#
        features    = list([model.conv1, model.bn1, model.relu, model.maxpool, model.layer1, model.layer2, model.layer3])
        #----------------------------------------------------------------------------#
        #   获取分类部分,从model.layer4到model.avgpool
        #----------------------------------------------------------------------------#
        classifier  = list([model.layer4, model.avgpool])
        
        features    = nn.Sequential(*features)
        classifier  = nn.Sequential(*classifier)
        return features, classifier
    

    2、获得Proposal建议框

    在这里插入图片描述
    获得的公用特征层在图像中就是Feature Map,其有两个应用,一个是和ROIPooling结合使用、另一个是进行一次3x3的卷积后,进行一个18通道的1x1卷积,还有一个36通道的1x1卷积。

    在Faster-RCNN中,num_priors也就是先验框的数量就是9,所以两个1x1卷积的结果实际上也就是:

    9 x 4的卷积 用于预测 公用特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为Faster-RCNN的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

    9 x 2的卷积 用于预测 公用特征层上 每一个网格点上 每一个预测框内部是否包含了物体,序号为1的内容为包含物体的概率

    当我们输入的图片的shape是600x600x3的时候,公用特征层的shape就是38x38x1024,相当于把输入进来的图像分割成38x38的网格,然后每个网格存在9个先验框,这些先验框有不同的大小,在图像上密密麻麻。

    9 x 4的卷积的结果会对这些先验框进行调整,获得一个新的框。
    9 x 2的卷积会判断上述获得的新框是否包含物体。

    到这里我们可以获得了一些有用的框,这些框会利用9 x 2的卷积判断是否存在物体。

    到此位置还只是粗略的一个框的获取,也就是一个建议框。然后我们会在建议框里面继续找东西。

    实现代码为:

    class RegionProposalNetwork(nn.Module):
        def __init__(
            self, 
            in_channels     = 512, 
            mid_channels    = 512, 
            ratios          = [0.5, 1, 2],
            anchor_scales   = [8, 16, 32], 
            feat_stride     = 16,
            mode            = "training",
        ):
            super(RegionProposalNetwork, self).__init__()
            #-----------------------------------------#
            #   生成基础先验框,shape为[9, 4]
            #-----------------------------------------#
            self.anchor_base    = generate_anchor_base(anchor_scales = anchor_scales, ratios = ratios)
            n_anchor            = self.anchor_base.shape[0]
    
            #-----------------------------------------#
            #   先进行一个3x3的卷积,可理解为特征整合
            #-----------------------------------------#
            self.conv1  = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
            #-----------------------------------------#
            #   分类预测先验框内部是否包含物体
            #-----------------------------------------#
            self.score  = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
            #-----------------------------------------#
            #   回归预测对先验框进行调整
            #-----------------------------------------#
            self.loc    = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
    
            #-----------------------------------------#
            #   特征点间距步长
            #-----------------------------------------#
            self.feat_stride    = feat_stride
            #-----------------------------------------#
            #   用于对建议框解码并进行非极大抑制
            #-----------------------------------------#
            self.proposal_layer = ProposalCreator(mode)
            #--------------------------------------#
            #   对FPN的网络部分进行权值初始化
            #--------------------------------------#
            normal_init(self.conv1, 0, 0.01)
            normal_init(self.score, 0, 0.01)
            normal_init(self.loc, 0, 0.01)
    
        def forward(self, x, img_size, scale=1.):
            n, _, h, w = x.shape
            #-----------------------------------------#
            #   先进行一个3x3的卷积,可理解为特征整合
            #-----------------------------------------#
            x = F.relu(self.conv1(x))
            #-----------------------------------------#
            #   回归预测对先验框进行调整
            #-----------------------------------------#
            rpn_locs = self.loc(x)
            rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4)
            #-----------------------------------------#
            #   分类预测先验框内部是否包含物体
            #-----------------------------------------#
            rpn_scores = self.score(x)
            rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous().view(n, -1, 2)
            
            #--------------------------------------------------------------------------------------#
            #   进行softmax概率计算,每个先验框只有两个判别结果
            #   内部包含物体或者内部不包含物体,rpn_softmax_scores[:, :, 1]的内容为包含物体的概率
            #--------------------------------------------------------------------------------------#
            rpn_softmax_scores  = F.softmax(rpn_scores, dim=-1)
            rpn_fg_scores       = rpn_softmax_scores[:, :, 1].contiguous()
            rpn_fg_scores       = rpn_fg_scores.view(n, -1)
    

    3、Proposal建议框的解码

    通过第二步我们获得了38x38x9个先验框的预测结果。预测结果包含两部分。

    9 x 4的卷积 用于预测 公用特征层上 每一个网格点上 每一个先验框的变化情况。**

    9 x 2的卷积 用于预测 公用特征层上 每一个网格点上 每一个预测框内部是否包含了物体。

    相当于就是将整个图像分成38x38个网格;然后从每个网格中心建立9个先验框,一共38x38x9个,12996个先验框。

    当输入图像shape不同时,先验框的数量也会发生改变。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整。

    9 x 4中的9表示了这个网格点所包含的先验框数量,其中的4表示了框的中心与长宽的调整情况。

    实现代码如下:

    class ProposalCreator():
        def __init__(
            self, 
            mode, 
            nms_iou             = 0.7,
            n_train_pre_nms     = 12000,
            n_train_post_nms    = 600,
            n_test_pre_nms      = 3000,
            n_test_post_nms     = 300,
            min_size            = 16
        
        ):
            #-----------------------------------#
            #   设置预测还是训练
            #-----------------------------------#
            self.mode               = mode
            #-----------------------------------#
            #   建议框非极大抑制的iou大小
            #-----------------------------------#
            self.nms_iou            = nms_iou
            #-----------------------------------#
            #   训练用到的建议框数量
            #-----------------------------------#
            self.n_train_pre_nms    = n_train_pre_nms
            self.n_train_post_nms   = n_train_post_nms
            #-----------------------------------#
            #   预测用到的建议框数量
            #-----------------------------------#
            self.n_test_pre_nms     = n_test_pre_nms
            self.n_test_post_nms    = n_test_post_nms
            self.min_size           = min_size
    
        def __call__(self, loc, score, anchor, img_size, scale=1.):
            if self.mode == "training":
                n_pre_nms   = self.n_train_pre_nms
                n_post_nms  = self.n_train_post_nms
            else:
                n_pre_nms   = self.n_test_pre_nms
                n_post_nms  = self.n_test_post_nms
    
            #-----------------------------------#
            #   将先验框转换成tensor
            #-----------------------------------#
            anchor = torch.from_numpy(anchor)
            if loc.is_cuda:
                anchor = anchor.cuda()
            #-----------------------------------#
            #   将RPN网络预测结果转化成建议框
            #-----------------------------------#
            roi = loc2bbox(anchor, loc)
            #-----------------------------------#
            #   防止建议框超出图像边缘
            #-----------------------------------#
            roi[:, [0, 2]] = torch.clamp(roi[:, [0, 2]], min = 0, max = img_size[1])
            roi[:, [1, 3]] = torch.clamp(roi[:, [1, 3]], min = 0, max = img_size[0])
            
            #-----------------------------------#
            #   建议框的宽高的最小值不可以小于16
            #-----------------------------------#
            min_size    = self.min_size * scale
            keep        = torch.where(((roi[:, 2] - roi[:, 0]) >= min_size) & ((roi[:, 3] - roi[:, 1]) >= min_size))[0]
            #-----------------------------------#
            #   将对应的建议框保留下来
            #-----------------------------------#
            roi         = roi[keep, :]
            score       = score[keep]
    
            #-----------------------------------#
            #   根据得分进行排序,取出建议框
            #-----------------------------------#
            order       = torch.argsort(score, descending=True)
            if n_pre_nms > 0:
                order   = order[:n_pre_nms]
            roi     = roi[order, :]
            score   = score[order]
    
            #-----------------------------------#
            #   对建议框进行非极大抑制
            #   使用官方的非极大抑制会快非常多
            #-----------------------------------#
            keep    = nms(roi, score, self.nms_iou)
            keep    = keep[:n_post_nms]
            roi     = roi[keep]
            return roi
    

    4、对Proposal建议框加以利用(RoiPoolingConv)

    在这里插入图片描述
    让我们对建议框有一个整体的理解:
    事实上建议框就是对图片哪一个区域有物体存在进行初步筛选。

    通过主干特征提取网络,我们可以获得一个公用特征层,当输入图片为600x600x3的时候,它的shape是38x38x1024,然后建议框会对这个公用特征层进行截取。

    其实公用特征层里面的38x38对应着图片里的38x38个区域,38x38中的每一个点相当于这个区域内部所有特征的浓缩。

    建议框会对这38x38个区域进行截取,也就是认为这些区域里存在目标,然后将截取的结果进行resize,resize到14x14x1024的大小。

    然后再对每个建议框再进行Resnet原有的第五次压缩。压缩完后进行一个平均池化,再进行一个Flatten,最后分别进行一个num_classes的全连接和(num_classes)x4全连接。

    num_classes的全连接用于对最后获得的框进行分类,(num_classes)x4全连接用于对相应的建议框进行调整。

    通过这些操作,我们可以获得所有建议框的调整情况,和这个建议框调整后框内物体的类别。

    事实上,在上一步获得的建议框就是ROI的先验框

    对Proposal建议框加以利用的过程与shape变化如图所示:
    在这里插入图片描述

    建议框调整后的结果就是最终的预测结果了,可以在图上进行绘画了。

    class Resnet50RoIHead(nn.Module):
        def __init__(self, n_class, roi_size, spatial_scale, classifier):
            super(Resnet50RoIHead, self).__init__()
            self.classifier = classifier
            #--------------------------------------#
            #   对ROIPooling后的的结果进行回归预测
            #--------------------------------------#
            self.cls_loc = nn.Linear(2048, n_class * 4)
            #-----------------------------------#
            #   对ROIPooling后的的结果进行分类
            #-----------------------------------#
            self.score = nn.Linear(2048, n_class)
            #-----------------------------------#
            #   权值初始化
            #-----------------------------------#
            normal_init(self.cls_loc, 0, 0.001)
            normal_init(self.score, 0, 0.01)
    
            self.roi = RoIPool((roi_size, roi_size), spatial_scale)
    
        def forward(self, x, rois, roi_indices, img_size):
            n, _, _, _ = x.shape
            if x.is_cuda:
                roi_indices = roi_indices.cuda()
                rois = rois.cuda()
            
            rois_feature_map = torch.zeros_like(rois)
            rois_feature_map[:, [0,2]] = rois[:, [0,2]] / img_size[1] * x.size()[3]
            rois_feature_map[:, [1,3]] = rois[:, [1,3]] / img_size[0] * x.size()[2]
    
            indices_and_rois = torch.cat([roi_indices[:, None], rois_feature_map], dim=1)
            #-----------------------------------#
            #   利用建议框对公用特征层进行截取
            #-----------------------------------#
            pool = self.roi(x, indices_and_rois)
            #-----------------------------------#
            #   利用classifier网络进行特征提取
            #-----------------------------------#
            fc7 = self.classifier(pool)
            #--------------------------------------------------------------#
            #   当输入为一张图片的时候,这里获得的f7的shape为[300, 2048]
            #--------------------------------------------------------------#
            fc7 = fc7.view(fc7.size(0), -1)
    
            roi_cls_locs    = self.cls_loc(fc7)
            roi_scores      = self.score(fc7)
            roi_cls_locs    = roi_cls_locs.view(n, -1, roi_cls_locs.size(1))
            roi_scores      = roi_scores.view(n, -1, roi_scores.size(1))
            return roi_cls_locs, roi_scores
    

    5、在原图上进行绘制

    在第四步的结尾,我们对建议框进行再一次进行解码后,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    6、整体的执行流程

    在这里插入图片描述
    几个小tip:
    1、共包含了两次解码过程。
    2、先进行粗略的筛选再细调。
    3、第一次获得的建议框解码后的结果是对共享特征层featuremap进行截取。

    二、训练部分

    Faster-RCNN的训练过程和它的预测过程一样,分为两部分,首先要训练获得建议框网络,然后再训练后面利用ROI获得预测结果的网络。

    1、建议框网络的训练

    公用特征层如果要获得建议框的预测结果,需要再进行一次3x3的卷积后,进行一个2通道的1x1卷积,还有一个36通道的1x1卷积。

    在Faster-RCNN中,num_priors也就是先验框的数量就是9,所以两个1x1卷积的结果实际上也就是:

    9 x 4的卷积 用于预测 公用特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为Faster-RCNN的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

    9 x 2的卷积 用于预测 公用特征层上 每一个网格点上 每一个预测框内部是否包含了物体。

    也就是说,我们直接利用Faster-RCNN建议框网络预测到的结果,并不是建议框在图片上的真实位置,需要解码才能得到真实位置。

    而在训练的时候,我们需要计算loss函数,这个loss函数是相对于Faster-RCNN建议框网络的预测结果的。我们需要把图片输入到当前的Faster-RCNN建议框的网络中,得到建议框的结果;同时还需要进行编码,这个编码是把真实框的位置信息格式转化为Faster-RCNN建议框预测结果的格式信息

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

    从建议框预测结果获得真实框的过程被称作解码,而从真实框获得建议框预测结果的过程就是编码的过程。

    因此我们只需要将解码过程逆过来就是编码过程了。

    实现代码如下:

    def bbox_iou(bbox_a, bbox_b):
        if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
            print(bbox_a, bbox_b)
            raise IndexError
        tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
        br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
        area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
        area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
        area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
        return area_i / (area_a[:, None] + area_b - area_i)
    
    def bbox2loc(src_bbox, dst_bbox):
        width = src_bbox[:, 2] - src_bbox[:, 0]
        height = src_bbox[:, 3] - src_bbox[:, 1]
        ctr_x = src_bbox[:, 0] + 0.5 * width
        ctr_y = src_bbox[:, 1] + 0.5 * height
    
        base_width = dst_bbox[:, 2] - dst_bbox[:, 0]
        base_height = dst_bbox[:, 3] - dst_bbox[:, 1]
        base_ctr_x = dst_bbox[:, 0] + 0.5 * base_width
        base_ctr_y = dst_bbox[:, 1] + 0.5 * base_height
    
        eps = np.finfo(height.dtype).eps
        width = np.maximum(width, eps)
        height = np.maximum(height, eps)
    
        dx = (base_ctr_x - ctr_x) / width
        dy = (base_ctr_y - ctr_y) / height
        dw = np.log(base_width / width)
        dh = np.log(base_height / height)
    
        loc = np.vstack((dx, dy, dw, dh)).transpose()
        return loc
    
    class AnchorTargetCreator(object):
        def __init__(self, n_sample=256, pos_iou_thresh=0.7, neg_iou_thresh=0.3, pos_ratio=0.5):
            self.n_sample       = n_sample
            self.pos_iou_thresh = pos_iou_thresh
            self.neg_iou_thresh = neg_iou_thresh
            self.pos_ratio      = pos_ratio
    
        def __call__(self, bbox, anchor):
            argmax_ious, label = self._create_label(anchor, bbox)
            if (label > 0).any():
                loc = bbox2loc(anchor, bbox[argmax_ious])
                return loc, label
            else:
                return np.zeros_like(anchor), label
    
        def _calc_ious(self, anchor, bbox):
            #----------------------------------------------#
            #   anchor和bbox的iou
            #   获得的ious的shape为[num_anchors, num_gt]
            #----------------------------------------------#
            ious = bbox_iou(anchor, bbox)
    
            if len(bbox)==0:
                return np.zeros(len(anchor), np.int32), np.zeros(len(anchor)), np.zeros(len(bbox))
            #---------------------------------------------------------#
            #   获得每一个先验框最对应的真实框  [num_anchors, ]
            #---------------------------------------------------------#
            argmax_ious = ious.argmax(axis=1)
            #---------------------------------------------------------#
            #   找出每一个先验框最对应的真实框的iou  [num_anchors, ]
            #---------------------------------------------------------#
            max_ious = np.max(ious, axis=1)
            #---------------------------------------------------------#
            #   获得每一个真实框最对应的先验框  [num_gt, ]
            #---------------------------------------------------------#
            gt_argmax_ious = ious.argmax(axis=0)
            #---------------------------------------------------------#
            #   保证每一个真实框都存在对应的先验框
            #---------------------------------------------------------#
            for i in range(len(gt_argmax_ious)):
                argmax_ious[gt_argmax_ious[i]] = i
    
            return argmax_ious, max_ious, gt_argmax_ious
            
        def _create_label(self, anchor, bbox):
            # ------------------------------------------ #
            #   1是正样本,0是负样本,-1忽略
            #   初始化的时候全部设置为-1
            # ------------------------------------------ #
            label = np.empty((len(anchor),), dtype=np.int32)
            label.fill(-1)
    
            # ------------------------------------------------------------------------ #
            #   argmax_ious为每个先验框对应的最大的真实框的序号         [num_anchors, ]
            #   max_ious为每个真实框对应的最大的真实框的iou             [num_anchors, ]
            #   gt_argmax_ious为每一个真实框对应的最大的先验框的序号    [num_gt, ]
            # ------------------------------------------------------------------------ #
            argmax_ious, max_ious, gt_argmax_ious = self._calc_ious(anchor, bbox)
            
            # ----------------------------------------------------- #
            #   如果小于门限值则设置为负样本
            #   如果大于门限值则设置为正样本
            #   每个真实框至少对应一个先验框
            # ----------------------------------------------------- #
            label[max_ious < self.neg_iou_thresh] = 0
            label[max_ious >= self.pos_iou_thresh] = 1
            if len(gt_argmax_ious)>0:
                label[gt_argmax_ious] = 1
    
            # ----------------------------------------------------- #
            #   判断正样本数量是否大于128,如果大于则限制在128
            # ----------------------------------------------------- #
            n_pos = int(self.pos_ratio * self.n_sample)
            pos_index = np.where(label == 1)[0]
            if len(pos_index) > n_pos:
                disable_index = np.random.choice(pos_index, size=(len(pos_index) - n_pos), replace=False)
                label[disable_index] = -1
    
            # ----------------------------------------------------- #
            #   平衡正负样本,保持总数量为256
            # ----------------------------------------------------- #
            n_neg = self.n_sample - np.sum(label == 1)
            neg_index = np.where(label == 0)[0]
            if len(neg_index) > n_neg:
                disable_index = np.random.choice(neg_index, size=(len(neg_index) - n_neg), replace=False)
                label[disable_index] = -1
    
            return argmax_ious, label
    

    focal会忽略一些重合度相对较高但是不是非常高的先验框,一般将重合度在0.3-0.7之间的先验框进行忽略。

    2、Roi网络的训练

    通过上一步已经可以对建议框网络进行训练了,建议框网络会提供一些位置的建议,在ROI网络部分,其会将建议框根据进行一定的截取,并获得对应的预测结果,事实上就是将上一步建议框当作了ROI网络的先验框。

    因此,我们需要计算所有建议框和真实框的重合程度,并进行筛选,如果某个真实框和建议框的重合程度大于0.5则认为该建议框为正样本,如果重合程度小于0.5则认为该建议框为负样本

    因此我们可以对真实框进行编码,这个编码是相对于建议框的,也就是,当我们存在这些建议框的时候,我们的ROI预测网络需要有什么样的预测结果才能将这些建议框调整成真实框。

    每次训练我们都放入128个建议框进行训练,同时要注意正负样本的平衡。
    实现代码如下:

    class ProposalTargetCreator(object):
        def __init__(self, n_sample=128, pos_ratio=0.5, pos_iou_thresh=0.5, neg_iou_thresh_high=0.5, neg_iou_thresh_low=0):
            self.n_sample = n_sample
            self.pos_ratio = pos_ratio
            self.pos_roi_per_image = np.round(self.n_sample * self.pos_ratio)
            self.pos_iou_thresh = pos_iou_thresh
            self.neg_iou_thresh_high = neg_iou_thresh_high
            self.neg_iou_thresh_low = neg_iou_thresh_low
    
        def __call__(self, roi, bbox, label, loc_normalize_std=(0.1, 0.1, 0.2, 0.2)):
            roi = np.concatenate((roi.detach().cpu().numpy(), bbox), axis=0)
            # ----------------------------------------------------- #
            #   计算建议框和真实框的重合程度
            # ----------------------------------------------------- #
            iou = bbox_iou(roi, bbox)
            
            if len(bbox)==0:
                gt_assignment = np.zeros(len(roi), np.int32)
                max_iou = np.zeros(len(roi))
                gt_roi_label = np.zeros(len(roi))
            else:
                #---------------------------------------------------------#
                #   获得每一个建议框最对应的真实框  [num_roi, ]
                #---------------------------------------------------------#
                gt_assignment = iou.argmax(axis=1)
                #---------------------------------------------------------#
                #   获得每一个建议框最对应的真实框的iou  [num_roi, ]
                #---------------------------------------------------------#
                max_iou = iou.max(axis=1)
                #---------------------------------------------------------#
                #   真实框的标签要+1因为有背景的存在
                #---------------------------------------------------------#
                gt_roi_label = label[gt_assignment] + 1
    
            #----------------------------------------------------------------#
            #   满足建议框和真实框重合程度大于neg_iou_thresh_high的作为负样本
            #   将正样本的数量限制在self.pos_roi_per_image以内
            #----------------------------------------------------------------#
            pos_index = np.where(max_iou >= self.pos_iou_thresh)[0]
            pos_roi_per_this_image = int(min(self.pos_roi_per_image, pos_index.size))
            if pos_index.size > 0:
                pos_index = np.random.choice(pos_index, size=pos_roi_per_this_image, replace=False)
    
            #-----------------------------------------------------------------------------------------------------#
            #   满足建议框和真实框重合程度小于neg_iou_thresh_high大于neg_iou_thresh_low作为负样本
            #   将正样本的数量和负样本的数量的总和固定成self.n_sample
            #-----------------------------------------------------------------------------------------------------#
            neg_index = np.where((max_iou < self.neg_iou_thresh_high) & (max_iou >= self.neg_iou_thresh_low))[0]
            neg_roi_per_this_image = self.n_sample - pos_roi_per_this_image
            neg_roi_per_this_image = int(min(neg_roi_per_this_image, neg_index.size))
            if neg_index.size > 0:
                neg_index = np.random.choice(neg_index, size=neg_roi_per_this_image, replace=False)
                
            #---------------------------------------------------------#
            #   sample_roi      [n_sample, ]
            #   gt_roi_loc      [n_sample, 4]
            #   gt_roi_label    [n_sample, ]
            #---------------------------------------------------------#
            keep_index = np.append(pos_index, neg_index)
    
            sample_roi = roi[keep_index]
            if len(bbox)==0:
                return sample_roi, np.zeros_like(sample_roi), gt_roi_label[keep_index]
    
            gt_roi_loc = bbox2loc(sample_roi, bbox[gt_assignment[keep_index]])
            gt_roi_loc = (gt_roi_loc / np.array(loc_normalize_std, np.float32))
    
            gt_roi_label = gt_roi_label[keep_index]
            gt_roi_label[pos_roi_per_this_image:] = 0
            return sample_roi, gt_roi_loc, gt_roi_label
    

    训练自己的Faster-RCNN模型

    首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
    注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

    一定要注意打开后的根目录是文件存放的目录。
    在这里插入图片描述

    一、数据集的准备

    本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    此时数据集的摆放已经结束。

    二、数据集的处理

    在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

    voc_annotation.py里面有一些参数需要设置。
    分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

    '''
    annotation_mode用于指定该文件运行时计算的内容
    annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
    annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
    annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
    '''
    annotation_mode     = 0
    '''
    必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
    与训练和预测所用的classes_path一致即可
    如果生成的2007_train.txt里面没有目标信息
    那么就是因为classes没有设定正确
    仅在annotation_mode为0和2的时候有效
    '''
    classes_path        = 'model_data/voc_classes.txt'
    '''
    trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
    train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
    仅在annotation_mode为0和1的时候有效
    '''
    trainval_percent    = 0.9
    train_percent       = 0.9
    '''
    指向VOC数据集所在的文件夹
    默认指向根目录下的VOC数据集
    '''
    VOCdevkit_path  = 'VOCdevkit'
    

    classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:
    在这里插入图片描述
    训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

    三、开始网络训练

    通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
    训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

    classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!
    在这里插入图片描述
    修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。
    其它参数的作用如下:

    #-------------------------------#
    #   是否使用Cuda
    #   没有GPU可以设置成False
    #-------------------------------#
    Cuda = True
    #--------------------------------------------------------#
    #   训练前一定要修改classes_path,使其对应自己的数据集
    #--------------------------------------------------------#
    classes_path    = 'model_data/voc_classes.txt'
    #----------------------------------------------------------------------------------------------------------------------------#
    #   权值文件请看README,百度网盘下载。数据的预训练权重对不同数据集是通用的,因为特征是通用的。
    #   预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。
    #
    #   如果想要断点续练就将model_path设置成logs文件夹下已经训练的权值文件。 
    #   当model_path = ''的时候不加载整个模型的权值。
    #
    #   此处使用的是整个模型的权重,因此是在train.py进行加载的,下面的pretrain不影响此处的权值加载。
    #   如果想要让模型从主干的预训练权值开始训练,则设置model_path = '',下面的pretrain = True,此时仅加载主干。
    #   如果想要让模型从0开始训练,则设置model_path = '',下面的pretrain = Fasle,Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。
    #   一般来讲,从0开始训练效果会很差,因为权值太过随机,特征提取效果不明显。
    #----------------------------------------------------------------------------------------------------------------------------#
    model_path      = 'model_data/voc_weights_resnet.pth'
    #------------------------------------------------------#
    #   输入的shape大小
    #------------------------------------------------------#
    input_shape     = [600, 600]
    #---------------------------------------------#
    #   vgg或者resnet50
    #---------------------------------------------#
    backbone        = "resnet50"
    #----------------------------------------------------------------------------------------------------------------------------#
    #   是否使用主干网络的预训练权重,此处使用的是主干的权重,因此是在模型构建的时候进行加载的。
    #   如果设置了model_path,则主干的权值无需加载,pretrained的值无意义。
    #   如果不设置model_path,pretrained = True,此时仅加载主干开始训练。
    #   如果不设置model_path,pretrained = False,Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。
    #----------------------------------------------------------------------------------------------------------------------------#
    pretrained      = False
    #------------------------------------------------------------------------#
    #   anchors_size用于设定先验框的大小,每个特征点均存在9个先验框。
    #   anchors_size每个数对应3个先验框。
    #   当anchors_size = [8, 16, 32]的时候,生成的先验框宽高约为:
    #   [90, 180] ; [180, 360]; [360, 720]; [128, 128]; 
    #   [256, 256]; [512, 512]; [180, 90] ; [360, 180]; 
    #   [720, 360]; 详情查看anchors.py
    #   如果想要检测小物体,可以减小anchors_size靠前的数。
    #   比如设置anchors_size = [4, 16, 32]
    #------------------------------------------------------------------------#
    anchors_size    = [8, 16, 32]
    
    #----------------------------------------------------#
    #   训练分为两个阶段,分别是冻结阶段和解冻阶段。
    #   显存不足与数据集大小无关,提示显存不足请调小batch_size。
    #----------------------------------------------------#
    #----------------------------------------------------#
    #   冻结阶段训练参数
    #   此时模型的主干被冻结了,特征提取网络不发生改变
    #   占用的显存较小,仅对网络进行微调
    #----------------------------------------------------#
    Init_Epoch          = 0
    Freeze_Epoch        = 50
    Freeze_batch_size   = 4
    Freeze_lr           = 1e-4
    #----------------------------------------------------#
    #   解冻阶段训练参数
    #   此时模型的主干不被冻结了,特征提取网络会发生改变
    #   占用的显存较大,网络所有的参数都会发生改变
    #----------------------------------------------------#
    UnFreeze_Epoch      = 100
    Unfreeze_batch_size = 2
    Unfreeze_lr         = 1e-5
    #------------------------------------------------------#
    #   是否进行冻结训练,默认先冻结主干训练后解冻训练。
    #------------------------------------------------------#
    Freeze_Train        = True
    #------------------------------------------------------#
    #   用于设置是否使用多线程读取数据
    #   开启后会加快数据读取速度,但是会占用更多内存
    #   内存较小的电脑可以设置为2或者0  
    #------------------------------------------------------#
    num_workers         = 4
    #----------------------------------------------------#
    #   获得图片路径和标签
    #----------------------------------------------------#
    train_annotation_path   = '2007_train.txt'
    val_annotation_path     = '2007_val.txt'
    

    四、训练结果预测

    训练结果预测需要用到两个文件,分别是yolo.py和predict.py。
    我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

    model_path指向训练好的权值文件,在logs文件夹里。
    classes_path指向检测类别所对应的txt。

    在这里插入图片描述
    完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

    展开全文
  • 睿智目标检测18——Keras搭建FasterRCNN目标检测平台学习前言什么是FasterRCNN目标检测算法源码下载Faster-RCNN实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二...

    学习前言

    最近对实例分割感兴趣了,不过实例分割MaskRCNN是基于FasterRCNN的,之前学了非常多的One-Stage的目标检测算法,对FasterRCNN并不感兴趣,这次我们来学学FasterRCNN。
    在这里插入图片描述

    什么是FasterRCNN目标检测算法

    在这里插入图片描述
    Faster-RCNN是一个非常有效的目标检测算法,虽然是一个比较早的论文, 但它至今仍是许多目标检测算法的基础。

    Faster-RCNN作为一种two-stage的算法,与one-stage的算法相比,two-stage的算法更加复杂且速度较慢,但是检测精度会更高。

    事实上也确实是这样,Faster-RCNN的检测效果非常不错,但是检测速度与训练速度有待提高。

    源码下载

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

    Faster-RCNN实现思路

    一、预测部分

    1、主干网络介绍

    在这里插入图片描述
    Faster-RCNN可以采用多种的主干特征提取网络,常用的有VGG,Resnet,Xception等等,本文采用的是Resnet网络,关于Resnet的介绍大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102790260

    Faster-RCNN对输入进来的图片尺寸没有固定,但是一般会把输入进来的图片短边固定成600,如输入一张1200x1800的图片,会把图片不失真的resize到600x900上。

    ResNet50有两个基本的块,分别名为Conv BlockIdentity Block。其中:

    • Conv Block输入和输出的维度是不一样的,所以不能连续串联,它的作用是改变网络的维度;
    • Identity Block输入维度和输出维度相同,可以串联,用于加深网络的。

    上述提到的Conv Block的结构如下,由于残差边上存在卷积,Conv Block输入和输出的维度是不一样的
    在这里插入图片描述
    Identity Block的结构如下,由于残差边上不存在卷积,Identity Block输入维度和输出维度相同
    在这里插入图片描述

    Faster-RCNN的主干特征提取网络部分只用到了Resnet50当中高宽压缩四次的特征层,此时我们可以获得一个共享特征层,假设我们输入进来的图片是600x600x3,我们最后会获得一个38x38x3的特征层。

    原本Resnet50第五次高宽压缩的部分ROI中使用

    对Faster-RCNN的主干特征提取网络部分而言,以输入的图片为600x600为例shape变化如下:
    在这里插入图片描述
    最后一层的输出就是公用特征层。

    实现代码:

    class BatchNormalization(Layer):
        def __init__(self, epsilon=1e-3, axis=-1,
                     weights=None, beta_init='zero', gamma_init='one',
                     gamma_regularizer=None, beta_regularizer=None, **kwargs):
    
            self.supports_masking = True
            self.beta_init = initializers.get(beta_init)
            self.gamma_init = initializers.get(gamma_init)
            self.epsilon = epsilon
            self.axis = axis
            self.gamma_regularizer = regularizers.get(gamma_regularizer)
            self.beta_regularizer = regularizers.get(beta_regularizer)
            self.initial_weights = weights
            super(BatchNormalization, self).__init__(**kwargs)
    
        def build(self, input_shape):
            self.input_spec = [InputSpec(shape=input_shape)]
            shape = (input_shape[self.axis],)
    
            self.gamma = self.add_weight(shape,
                                         initializer=self.gamma_init,
                                         regularizer=self.gamma_regularizer,
                                         name='{}_gamma'.format(self.name),
                                         trainable=False)
            self.beta = self.add_weight(shape,
                                        initializer=self.beta_init,
                                        regularizer=self.beta_regularizer,
                                        name='{}_beta'.format(self.name),
                                        trainable=False)
            self.running_mean = self.add_weight(shape, initializer='zero',
                                                name='{}_running_mean'.format(self.name),
                                                trainable=False)
            self.running_std = self.add_weight(shape, initializer='one',
                                               name='{}_running_std'.format(self.name),
                                               trainable=False)
    
            if self.initial_weights is not None:
                self.set_weights(self.initial_weights)
                del self.initial_weights
    
            self.built = True
    
        def call(self, x, mask=None):
    
            assert self.built, 'Layer must be built before being called'
            input_shape = K.int_shape(x)
    
            reduction_axes = list(range(len(input_shape)))
            del reduction_axes[self.axis]
            broadcast_shape = [1] * len(input_shape)
            broadcast_shape[self.axis] = input_shape[self.axis]
    
            if sorted(reduction_axes) == range(K.ndim(x))[:-1]:
                x_normed = K.batch_normalization(
                    x, self.running_mean, self.running_std,
                    self.beta, self.gamma,
                    epsilon=self.epsilon)
            else:
                broadcast_running_mean = K.reshape(self.running_mean, broadcast_shape)
                broadcast_running_std = K.reshape(self.running_std, broadcast_shape)
                broadcast_beta = K.reshape(self.beta, broadcast_shape)
                broadcast_gamma = K.reshape(self.gamma, broadcast_shape)
                x_normed = K.batch_normalization(
                    x, broadcast_running_mean, broadcast_running_std,
                    broadcast_beta, broadcast_gamma,
                    epsilon=self.epsilon)
    
            return x_normed
    
        def get_config(self):
            config = {'epsilon': self.epsilon,
                      'axis': self.axis,
                      'gamma_regularizer': self.gamma_regularizer.get_config() if self.gamma_regularizer else None,
                      'beta_regularizer': self.beta_regularizer.get_config() if self.beta_regularizer else None}
            base_config = super(BatchNormalization, self).get_config()
            return dict(list(base_config.items()) + list(config.items()))
    
    
    def identity_block(input_tensor, kernel_size, filters, stage, block):
    
        filters1, filters2, filters3 = filters
    
        conv_name_base = 'res' + str(stage) + block + '_branch'
        bn_name_base = 'bn' + str(stage) + block + '_branch'
    
        x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
        x = BatchNormalization(name=bn_name_base + '2a')(x)
        x = Activation('relu')(x)
    
        x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b')(x)
        x = BatchNormalization(name=bn_name_base + '2b')(x)
        x = Activation('relu')(x)
    
        x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
        x = BatchNormalization(name=bn_name_base + '2c')(x)
    
        x = layers.add([x, input_tensor])
        x = Activation('relu')(x)
        return x
    
    
    def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):
    
        filters1, filters2, filters3 = filters
    
        conv_name_base = 'res' + str(stage) + block + '_branch'
        bn_name_base = 'bn' + str(stage) + block + '_branch'
    
        x = Conv2D(filters1, (1, 1), strides=strides,
                   name=conv_name_base + '2a')(input_tensor)
        x = BatchNormalization(name=bn_name_base + '2a')(x)
        x = Activation('relu')(x)
    
        x = Conv2D(filters2, kernel_size, padding='same',
                   name=conv_name_base + '2b')(x)
        x = BatchNormalization(name=bn_name_base + '2b')(x)
        x = Activation('relu')(x)
    
        x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
        x = BatchNormalization(name=bn_name_base + '2c')(x)
    
        shortcut = Conv2D(filters3, (1, 1), strides=strides,
                          name=conv_name_base + '1')(input_tensor)
        shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
    
        x = layers.add([x, shortcut])
        x = Activation('relu')(x)
        return x
    
    def ResNet50(inputs):
        #-----------------------------------#
        #   假设输入进来的图片是600,600,3
        #-----------------------------------#
        img_input = inputs
    
        # 600,600,3 -> 300,300,64
        x = ZeroPadding2D((3, 3))(img_input)
        x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(x)
        x = BatchNormalization(name='bn_conv1')(x)
        x = Activation('relu')(x)
    
        # 300,300,64 -> 150,150,64
        x = MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)
    
        # 150,150,64 -> 150,150,256
        x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
        x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
        x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')
    
        # 150,150,256 -> 75,75,512
        x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
        x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
        x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
        x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')
    
        # 75,75,512 -> 38,38,1024
        x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
        x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
        x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
        x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
        x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
        x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')
    
        # 最终获得一个38,38,1024的共享特征层
        return x
    

    2、获得Proposal建议框

    在这里插入图片描述
    上一步获得的公用特征层在图像中就是Feature Map

    假设我们输入进来的图片是600x600x3,我们最后会获得一个38x38x3的特征层。

    其有两个应用:

    • 一个是利用一次3x3的卷积进行特征整合,再进行一个9(分解为9 x 1)通道的1x1卷积,还有一个36(分解为9 x 4)通道的1x1卷积。这两个卷积的结果分别用于判断先验框内部是否包含物体以及这个先验框的调整参数
    • 一个是和ROIPooling结合使用。

    这一部分主要是解析第一个应用。

    在Faster-RCNN中,共享特征层上的每一个像素点的先验框的数量是9,上述提到的两个1x1卷积的结果对应了下述两个内容:

    • 9 x 4的卷积 用于预测 共享特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为Faster-RCNN需要结合先验框获得建议框,9 x 4卷积的预测结果就是先验框的变化情况。)
    • 9 x 1的卷积 用于预测 共享特征层上 每一个网格点上 每一个先验框内部是否包含了物体。

    当我们输入的图片的shape是600x600x3的时候,共享特征层的shape就是38x38x1024,相当于把输入进来的图像分割成38x38的网格,然后每个网格点存在9个先验框,这些先验框有不同的大小,在图像上密密麻麻。

    上述提到的两个卷积:

    • 9 x 4的卷积的结果会对这些先验框进行调整,获得建议框。
    • 9 x 1的卷积的结果会判断先验框内是否包含物体。

    到此位置还只是粗略的一个框的获取,也就是一个建议框。然后我们会在建议框里面继续找东西。

    实现代码为:

    #----------------------------------------------------#
    #   创建建议框网络
    #   该网络结果会对先验框进行调整获得建议框
    #----------------------------------------------------#
    def get_rpn(base_layers, num_anchors):
        #----------------------------------------------------#
        #   利用一个512通道的3x3卷积进行特征整合
        #----------------------------------------------------#
        x = Conv2D(512, (3, 3), padding='same', activation='relu', kernel_initializer=random_normal(stddev=0.02), name='rpn_conv1')(base_layers)
    
        #----------------------------------------------------#
        #   利用一个1x1卷积调整通道数,获得预测结果
        #----------------------------------------------------#
        x_class = Conv2D(num_anchors, (1, 1), activation = 'sigmoid', kernel_initializer=random_normal(stddev=0.02), name='rpn_out_class')(x)
        x_regr  = Conv2D(num_anchors * 4, (1, 1), activation = 'linear', kernel_initializer=random_normal(stddev=0.02), name='rpn_out_regress')(x)
        
        x_class = Reshape((-1, 1),name="classification")(x_class)
        x_regr  = Reshape((-1, 4),name="regression")(x_regr)
        return [x_class, x_regr]
    

    3、Proposal建议框的解码

    通过第二步我们获得了38x38x9个先验框对应的预测结果。预测结果包含两部分。
    9 x 4的卷积 用于预测 共享特征层上 每一个网格点上 每一个先验框的变化情况。
    9 x 1的卷积 用于预测 共享特征层上 每一个网格点上 每一个先验框内部是否包含了物体。

    该共享特征层相当于将整个图像分成38x38个网格;然后从每个网格中心建立9个先验框一共38x38x9个,12996个先验框。

    当输入图像shape不同时,先验框的数量也会发生改变。这和共享特征层的shape相关。
    在这里插入图片描述
    先验框虽然可以代表一定的框的位置信息与框的大小信息,但是其是有限的,无法表示任意情况,因此还需要调整。

    9 x 4中的9表示了这个网格点所包含的先验框数量,其中的4表示了框的中心与长宽的调整情况。

    实现代码如下:

    def decode_boxes(self, mbox_loc, anchors, variances):
        # 获得先验框的宽与高
        anchor_width     = anchors[:, 2] - anchors[:, 0]
        anchor_height    = anchors[:, 3] - anchors[:, 1]
        # 获得先验框的中心点
        anchor_center_x  = 0.5 * (anchors[:, 2] + anchors[:, 0])
        anchor_center_y  = 0.5 * (anchors[:, 3] + anchors[:, 1])
    
        # 建议框距离先验框中心的xy轴偏移情况
        detections_center_x = mbox_loc[:, 0] * anchor_width * variances[0]
        detections_center_x += anchor_center_x
        detections_center_y = mbox_loc[:, 1] * anchor_height * variances[1]
        detections_center_y += anchor_center_y
        
        # 建议框的宽与高的求取
        detections_width   = np.exp(mbox_loc[:, 2] * variances[2])
        detections_width   *= anchor_width
        detections_height  = np.exp(mbox_loc[:, 3] * variances[3])
        detections_height  *= anchor_height
    
        # 获取建议框的左上角与右下角
        detections_xmin = detections_center_x - 0.5 * detections_width
        detections_ymin = detections_center_y - 0.5 * detections_height
        detections_xmax = detections_center_x + 0.5 * detections_width
        detections_ymax = detections_center_y + 0.5 * detections_height
    
        # 建议框的左上角与右下角进行堆叠
        detections = np.concatenate((detections_xmin[:, None],
                                        detections_ymin[:, None],
                                        detections_xmax[:, None],
                                        detections_ymax[:, None]), axis=-1)
        # 防止超出0与1
        detections = np.minimum(np.maximum(detections, 0.0), 1.0)
        return detections
    
    def detection_out_rpn(self, predictions, anchors, variances = [0.25, 0.25, 0.25, 0.25]):
        #---------------------------------------------------#
        #   获得种类的置信度
        #---------------------------------------------------#
        mbox_conf   = predictions[0]
        #---------------------------------------------------#
        #   mbox_loc是回归预测结果
        #---------------------------------------------------#
        mbox_loc    = predictions[1]
    
        results = []
        #----------------------------------------------------------------------------------------------------------------------#
        #   对每一张图片进行处理,由于在predict.py的时候,我们只输入一张图片,所以for i in range(len(mbox_loc))只进行一次
        #----------------------------------------------------------------------------------------------------------------------#
        for i in range(len(mbox_loc)):
            #--------------------------------#
            #   利用回归结果对先验框进行解码
            #--------------------------------#
            detections     = self.decode_boxes(mbox_loc[i], anchors, variances)
            #--------------------------------#
            #   取出先验框内包含物体的概率
            #--------------------------------#
            c_confs         = mbox_conf[i, :, 0]
            c_confs_argsort = np.argsort(c_confs)[::-1][:self.rpn_pre_boxes]
    
            #------------------------------------#
            #   原始的预测框较多,先选一些高分框
            #------------------------------------#
            confs_to_process = c_confs[c_confs_argsort]
            boxes_to_process = detections[c_confs_argsort, :]
            #--------------------------------#
            #   进行iou的非极大抑制
            #--------------------------------#
            idx = self.sess.run(self.nms_out_rpn, feed_dict={self.boxes: boxes_to_process, self.scores: confs_to_process})
    
            #--------------------------------#
            #   取出在非极大抑制中效果较好的内容
            #--------------------------------#
            good_boxes  = boxes_to_process[idx]
            results.append(good_boxes)
        return np.array(results)
        return np.array(results)
    

    4、对Proposal建议框加以利用(RoiPoolingConv)

    在这里插入图片描述
    让我们对建议框有一个整体的理解:
    事实上建议框就是对图片哪一个区域有物体存在进行初步筛选。

    通过主干特征提取网络,我们可以获得一个公用特征层,当输入图片为600x600x3的时候,它的shape是38x38x1024,然后建议框会对这个公用特征层进行截取。

    其实公用特征层里面的38x38对应着图片里的38x38个区域,38x38中的每一个点相当于这个区域内部所有特征的浓缩。

    建议框会对这38x38个区域进行截取,也就是认为这些区域里存在目标,然后将截取的结果进行resize,resize到14x14x1024的大小。这个过程称为ROIPooling,本质上是对截取到的各区域分割为14x14的大小后分区域池化在tensorflow中的实现形式为resize。在这里我们称上述截取到的特征层为局部特征层。

    然后我们再对每个局部特征层再进行Resnet原有的第五次压缩。压缩完后进行一个平均池化,再进行一个Flatten,此时,对于每一个局部特征层,我们可以获得一个长度为2048的特征向量。我们最后对这个特征向量分别进行一个num_classes的全连接和(num_classes-1)x4全连接。其中:

    • num_classes的全连接用于对建议框进行分类;
    • (num_classes-1)x4的全连接用于对建议框进行调整(之所以-1是不调整被认定为背景的框。)

    通过这些操作,我们可以获得所有建议框的调整情况,和这个建议框调整后框内物体的类别。

    事实上,我们可以将建议框,看作是ROIPooling层的先验框

    对Proposal建议框加以利用的过程与shape变化如图所示:
    在这里插入图片描述
    建议框调整后的结果就是最终的预测结果了,可以在图上进行绘画了。

    class RoiPoolingConv(Layer):
        def __init__(self, pool_size, **kwargs):
            self.pool_size = pool_size
            super(RoiPoolingConv, self).__init__(**kwargs)
    
        def build(self, input_shape):
            self.nb_channels = input_shape[0][3]
    
        def compute_output_shape(self, input_shape):
            input_shape2 = input_shape[1]
            return None, input_shape2[1], self.pool_size, self.pool_size, self.nb_channels
    
        def call(self, x, mask=None):
            assert(len(x) == 2)
            #--------------------------------#
            #   共享特征层
            #   batch_size, 38, 38, 1024
            #--------------------------------#
            feature_map = x[0]
            #--------------------------------#
            #   建议框
            #   batch_size, num_rois, 4
            #--------------------------------#
            rois        = x[1]
            #---------------------------------#
            #   建议框数量,batch_size大小
            #---------------------------------#
            num_rois    = tf.shape(rois)[1]
            batch_size  = tf.shape(rois)[0]
            #---------------------------------#
            #   生成建议框序号信息
            #   用于在进行crop_and_resize时
            #   帮助建议框找到对应的共享特征层
            #---------------------------------#
            box_index   = tf.expand_dims(tf.range(0, batch_size), 1)
            box_index   = tf.tile(box_index, (1, num_rois))
            box_index   = tf.reshape(box_index, [-1])
    
            rs          = tf.image.crop_and_resize(feature_map, tf.reshape(rois, [-1, 4]), box_index, (self.pool_size, self.pool_size))
                
            #---------------------------------------------------------------------------------#
            #   最终的输出为
            #   (batch_size, num_rois, 14, 14, 1024)
            #---------------------------------------------------------------------------------#
            final_output = K.reshape(rs, (batch_size, num_rois, self.pool_size, self.pool_size, self.nb_channels))
            return final_output
    
    def identity_block_td(input_tensor, kernel_size, filters, stage, block):
        nb_filter1, nb_filter2, nb_filter3 = filters
        conv_name_base  = 'res' + str(stage) + block + '_branch'
        bn_name_base    = 'bn' + str(stage) + block + '_branch'
    
        x = TimeDistributed(Conv2D(nb_filter1, (1, 1), kernel_initializer='normal'), name=conv_name_base + '2a')(input_tensor)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2a')(x)
        x = Activation('relu')(x)
    
        x = TimeDistributed(Conv2D(nb_filter2, (kernel_size, kernel_size), kernel_initializer='normal',padding='same'), name=conv_name_base + '2b')(x)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2b')(x)
        x = Activation('relu')(x)
    
        x = TimeDistributed(Conv2D(nb_filter3, (1, 1), kernel_initializer='normal'), name=conv_name_base + '2c')(x)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2c')(x)
    
        x = Add()([x, input_tensor])
        x = Activation('relu')(x)
    
        return x
    
    def conv_block_td(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):
        nb_filter1, nb_filter2, nb_filter3 = filters
        conv_name_base  = 'res' + str(stage) + block + '_branch'
        bn_name_base    = 'bn' + str(stage) + block + '_branch'
    
        x = TimeDistributed(Conv2D(nb_filter1, (1, 1), strides=strides, kernel_initializer='normal'), name=conv_name_base + '2a')(input_tensor)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2a')(x)
        x = Activation('relu')(x)
    
        x = TimeDistributed(Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same', kernel_initializer='normal'), name=conv_name_base + '2b')(x)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2b')(x)
        x = Activation('relu')(x)
    
        x = TimeDistributed(Conv2D(nb_filter3, (1, 1), kernel_initializer='normal'), name=conv_name_base + '2c')(x)
        x = TimeDistributed(BatchNormalization(), name=bn_name_base + '2c')(x)
    
        shortcut = TimeDistributed(Conv2D(nb_filter3, (1, 1), strides=strides, kernel_initializer='normal'), name=conv_name_base + '1')(input_tensor)
        shortcut = TimeDistributed(BatchNormalization(), name=bn_name_base + '1')(shortcut)
    
        x = Add()([x, shortcut])
        x = Activation('relu')(x)
        return x
    
    def resnet50_classifier_layers(x):
        # batch_size, num_rois, 14, 14, 1024 -> batch_size, num_rois, 7, 7, 2048
        x = conv_block_td(x, 3, [512, 512, 2048], stage=5, block='a', strides=(2, 2))
        # batch_size, num_rois, 7, 7, 2048 -> batch_size, num_rois, 7, 7, 2048
        x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='b')
        # batch_size, num_rois, 7, 7, 2048 -> batch_size, num_rois, 7, 7, 2048
        x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='c')
        # batch_size, num_rois, 7, 7, 2048 -> batch_size, num_rois, 1, 1, 2048
        x = TimeDistributed(AveragePooling2D((7, 7)), name='avg_pool')(x)
    
        return x
        
    #----------------------------------------------------#
    #   将共享特征层和建议框传入classifier网络
    #   该网络结果会对建议框进行调整获得预测框
    #----------------------------------------------------#
    def get_resnet50_classifier(base_layers, input_rois, roi_size, num_classes=21):
        # batch_size, 38, 38, 1024 -> batch_size, num_rois, 14, 14, 1024
        out_roi_pool = RoiPoolingConv(roi_size)([base_layers, input_rois])
    
        # batch_size, num_rois, 14, 14, 1024 -> num_rois, 1, 1, 2048
        out = resnet50_classifier_layers(out_roi_pool)
    
        # batch_size, num_rois, 1, 1, 2048 -> batch_size, num_rois, 2048
        out = TimeDistributed(Flatten())(out)
    
        # batch_size, num_rois, 2048 -> batch_size, num_rois, num_classes
        out_class   = TimeDistributed(Dense(num_classes, activation='softmax', kernel_initializer=random_normal(stddev=0.02)), name='dense_class_{}'.format(num_classes))(out)
        # batch_size, num_rois, 2048 -> batch_size, num_rois, 4 * (num_classes-1)
        out_regr    = TimeDistributed(Dense(4 * (num_classes - 1), activation='linear', kernel_initializer=random_normal(stddev=0.02)), name='dense_regress_{}'.format(num_classes))(out)
        return [out_class, out_regr]
    

    5、在原图上进行绘制

    在第四步的结尾,我们对建议框进行再一次进行解码后,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    6、整体的执行流程

    在这里插入图片描述
    几个小tip:
    1、共包含了两次解码过程。
    2、先进行粗略的筛选再细调。
    3、第一次获得的建议框解码后的结果是对共享特征层featuremap进行截取。

    二、训练部分

    Faster-RCNN的训练过程和它的预测过程一样,分为两部分,首先要训练获得建议框网络,然后再训练后面利用ROI获得预测结果的网络。

    1、建议框网络的训练

    在预测过程中我们知道,如果想要对先验框调整获得建议框,我们需要对共享特征层利用一次3x3的卷积进行特征整合,然后进行两个卷积操作。这两个卷积分别为:

    • 一个36(分解为9 x 4)通道的1x1卷积
    • 一个9(分解为9 x 1)通道的1x1卷积

    两个1x1卷积的结果实际上也就是:

    9 x 4的卷积 用于预测 公用特征层上 每一个网格点上 每一个先验框的变化情况。(为什么说是变化情况呢,这是因为Faster-RCNN的预测结果需要结合先验框获得预测框,预测结果就是先验框的变化情况。)

    9 x 1的卷积 用于预测 公用特征层上 每一个网格点上 每一个预测框内部是否包含了物体。

    也就是说,我们直接利用Faster-RCNN建议框网络预测到的结果,并不是建议框在图片上的真实位置,需要解码才能得到真实位置。

    而在训练的时候,我们需要计算loss函数,这个loss函数是相对于Faster-RCNN建议框网络的预测结果的。计算loss函数我们需要进行两个操作,分别是:

    • 我们需要把图片输入到当前的Faster-RCNN建议框的网络中,得到建议框网络的预测结果;
    • 同时还需要对真实框进行编码,这个编码是把真实框的位置信息格式转化为Faster-RCNN建议框预测结果的格式信息

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

    从建议框预测结果获得真实框的过程被称作解码,而从真实框获得建议框预测结果的过程就是编码的过程。

    因此我们只需要将解码过程逆过来就是编码过程了。

    在进行编码前,我们还需要找到每一个真实框对应的先验框,代表这个真实框由某个先验框进行预测。

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

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

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

    在这一步后,我们可以找到每一个先验框所负责预测的真实框,然后再进行上述提到的编码行为。

    实现代码如下:

    def iou(self, box):
        #---------------------------------------------#
        #   计算出每个真实框与所有的先验框的iou
        #   判断真实框与先验框的重合情况
        #---------------------------------------------#
        inter_upleft    = np.maximum(self.anchors[:, :2], box[:2])
        inter_botright  = np.minimum(self.anchors[:, 2:4], box[2:])
    
        inter_wh    = inter_botright - inter_upleft
        inter_wh    = np.maximum(inter_wh, 0)
        inter       = inter_wh[:, 0] * inter_wh[:, 1]
        #---------------------------------------------# 
        #   真实框的面积
        #---------------------------------------------#
        area_true = (box[2] - box[0]) * (box[3] - box[1])
        #---------------------------------------------#
        #   先验框的面积
        #---------------------------------------------#
        area_gt = (self.anchors[:, 2] - self.anchors[:, 0])*(self.anchors[:, 3] - self.anchors[:, 1])
        #---------------------------------------------#
        #   计算iou
        #---------------------------------------------#
        union = area_true + area_gt - inter
    
        iou = inter / union
        return iou
    
    def encode_ignore_box(self, box, return_iou=True, variances = [0.25, 0.25, 0.25, 0.25]):
        #---------------------------------------------#
        #   计算当前真实框和先验框的重合情况
        #---------------------------------------------#
        iou         = self.iou(box)
        ignored_box = np.zeros((self.num_anchors, 1))
        #---------------------------------------------------#
        #   找到处于忽略门限值范围内的先验框
        #---------------------------------------------------#
        assign_mask_ignore = (iou > self.ignore_threshold) & (iou < self.overlap_threshold)
        ignored_box[:, 0][assign_mask_ignore] = iou[assign_mask_ignore]
    
        encoded_box = np.zeros((self.num_anchors, 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_anchors = self.anchors[assign_mask]
        #---------------------------------------------#
        #   逆向编码,将真实框转化为FRCNN预测结果的格式
        #   先计算真实框的中心与长宽
        #---------------------------------------------#
        box_center  = 0.5 * (box[:2] + box[2:])
        box_wh      = box[2:] - box[:2]
        #---------------------------------------------#
        #   再计算重合度较高的先验框的中心与长宽
        #---------------------------------------------#
        assigned_anchors_center = 0.5 * (assigned_anchors[:, :2] + assigned_anchors[:, 2:4])
        assigned_anchors_wh     = assigned_anchors[:, 2:4] - assigned_anchors[:, :2]
    
        # 逆向求取FasterRCNN应该有的预测结果
        encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
        encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
        encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]
    
        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
        encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
    
        return encoded_box.ravel(), ignored_box.ravel()
    
    def assign_boxes(self, boxes):
        #---------------------------------------------------#
        #   assignment分为2个部分
        #   :4      的内容为网络应该有的回归预测结果
        #   4       的内容为先验框是否包含物体,默认为背景
        #---------------------------------------------------#
        assignment          = np.zeros((self.num_anchors, 4 + 1))
        assignment[:, 4]    = 0.0
        if len(boxes) == 0:
            return assignment
    
        #---------------------------------------------------#
        #   对每一个真实框都进行iou计算
        #---------------------------------------------------#
        apply_along_axis_boxes = np.apply_along_axis(self.encode_ignore_box, 1, boxes[:, :4])
        encoded_boxes = np.array([apply_along_axis_boxes[i, 0] for i in range(len(apply_along_axis_boxes))])
        ingored_boxes = np.array([apply_along_axis_boxes[i, 1] for i in range(len(apply_along_axis_boxes))])
    
        #---------------------------------------------------#
        #   在reshape后,获得的ingored_boxes的shape为:
        #   [num_true_box, num_anchors, 1] 其中1为iou
        #---------------------------------------------------#
        ingored_boxes   = ingored_boxes.reshape(-1, self.num_anchors, 1)
        ignore_iou      = ingored_boxes[:, :, 0].max(axis=0)
        ignore_iou_mask = ignore_iou > 0
    
        assignment[:, 4][ignore_iou_mask] = -1
    
        #---------------------------------------------------#
        #   在reshape后,获得的encoded_boxes的shape为:
        #   [num_true_box, num_anchors, 4+1]
        #   4是编码后的结果,1为iou
        #---------------------------------------------------#
        encoded_boxes   = encoded_boxes.reshape(-1, self.num_anchors, 5)
        
        #---------------------------------------------------#
        #   [num_anchors]求取每一个先验框重合度最大的真实框
        #---------------------------------------------------#
        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] = 1
        # 通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的
        return assignment
    

    focal会忽略一些重合度相对较高但是不是非常高的先验框,一般将重合度在0.3-0.7之间的先验框进行忽略。

    2、Roi网络的训练

    通过上一步已经可以对建议框网络进行训练了,建议框网络会提供一些位置的建议,在ROI网络部分,其会将建议框根据进行一定的截取,并获得对应的预测结果,事实上就是将上一步建议框当作了ROI网络的先验框。

    因此,我们需要计算所有建议框和真实框的重合程度,并进行筛选,如果某个真实框和建议框的重合程度大于0.5则认为该建议框为正样本,如果重合程度小于0.5则认为该建议框为负样本

    因此我们可以对真实框进行编码,这个编码是相对于建议框的,也就是,当我们存在这些建议框的时候,我们的ROI预测网络需要有什么样的预测结果才能将这些建议框调整成真实框。

    每次训练我们都放入128个建议框进行训练,同时要注意正负样本的平衡。
    实现代码如下:

    class ProposalTargetCreator(object):
        def __init__(self, num_classes, n_sample=128, pos_ratio=0.5, pos_iou_thresh=0.5, 
            neg_iou_thresh_high=0.5, neg_iou_thresh_low=0, variance=[0.125, 0.125, 0.25, 0.25]):
    
            self.n_sample               = n_sample
            self.pos_ratio              = pos_ratio
            self.pos_roi_per_image      = np.round(self.n_sample * self.pos_ratio)
            self.pos_iou_thresh         = pos_iou_thresh
            self.neg_iou_thresh_high    = neg_iou_thresh_high
            self.neg_iou_thresh_low     = neg_iou_thresh_low
            self.num_classes            = num_classes
            self.variance               = variance
    
        def bbox_iou(self, bbox_a, bbox_b):
            if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
                print(bbox_a, bbox_b)
                raise IndexError
            tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
            br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
            area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
            area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
            area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
            return area_i / (area_a[:, None] + area_b - area_i)
    
        def bbox2loc(self, src_bbox, dst_bbox):
            width = src_bbox[:, 2] - src_bbox[:, 0]
            height = src_bbox[:, 3] - src_bbox[:, 1]
            ctr_x = src_bbox[:, 0] + 0.5 * width
            ctr_y = src_bbox[:, 1] + 0.5 * height
    
            base_width = dst_bbox[:, 2] - dst_bbox[:, 0]
            base_height = dst_bbox[:, 3] - dst_bbox[:, 1]
            base_ctr_x = dst_bbox[:, 0] + 0.5 * base_width
            base_ctr_y = dst_bbox[:, 1] + 0.5 * base_height
    
            eps = np.finfo(height.dtype).eps
            width = np.maximum(width, eps)
            height = np.maximum(height, eps)
    
            dx = (base_ctr_x - ctr_x) / width
            dy = (base_ctr_y - ctr_y) / height
            dw = np.log(base_width / width)
            dh = np.log(base_height / height)
    
            loc = np.vstack((dx, dy, dw, dh)).transpose()
            return loc
    
        def calc_iou(self, R, all_boxes):
            bboxes  = all_boxes[:, :4]
            label   = all_boxes[:, 4]
            R       = np.concatenate([R, bboxes], axis=0)
    
            # ----------------------------------------------------- #
            #   计算建议框和真实框的重合程度
            # ----------------------------------------------------- #
            if len(bboxes)==0:
                max_iou         = np.zeros(len(R))
                gt_assignment   = np.zeros(len(R), np.int32)
                gt_roi_label    = np.zeros(len(R))
            else:
                iou             = self.bbox_iou(R, bboxes)
                #---------------------------------------------------------#
                #   获得每一个建议框最对应的真实框的iou  [num_roi, ]
                #---------------------------------------------------------#
                max_iou         = iou.max(axis=1)
                #---------------------------------------------------------#
                #   获得每一个建议框最对应的真实框  [num_roi, ]
                #---------------------------------------------------------#
                gt_assignment   = iou.argmax(axis=1)
                #---------------------------------------------------------#
                #   真实框的标签
                #---------------------------------------------------------#
                gt_roi_label    = label[gt_assignment] 
    
            #----------------------------------------------------------------#
            #   满足建议框和真实框重合程度大于neg_iou_thresh_high的作为负样本
            #   将正样本的数量限制在self.pos_roi_per_image以内
            #----------------------------------------------------------------#
            pos_index = np.where(max_iou >= self.pos_iou_thresh)[0]
            pos_roi_per_this_image = int(min(self.n_sample//2, pos_index.size))
            if pos_index.size > 0:
                pos_index = np.random.choice(pos_index, size=pos_roi_per_this_image, replace=False)
    
            #-----------------------------------------------------------------------------------------------------#
            #   满足建议框和真实框重合程度小于neg_iou_thresh_high大于neg_iou_thresh_low作为负样本
            #   将正样本的数量和负样本的数量的总和固定成self.n_sample
            #-----------------------------------------------------------------------------------------------------#
            neg_index = np.where((max_iou < self.neg_iou_thresh_high) & (max_iou >= self.neg_iou_thresh_low))[0]
            neg_roi_per_this_image = self.n_sample - pos_roi_per_this_image
            if neg_roi_per_this_image > neg_index.size:
                neg_index = np.random.choice(neg_index, size=neg_roi_per_this_image, replace=True)
            else:
                neg_index = np.random.choice(neg_index, size=neg_roi_per_this_image, replace=False)
            
            #---------------------------------------------------------#
            #   sample_roi      [n_sample, ]
            #   gt_roi_loc      [n_sample, 4]
            #   gt_roi_label    [n_sample, ]
            #---------------------------------------------------------#
            keep_index = np.append(pos_index, neg_index)
            sample_roi = R[keep_index]
    
            if len(bboxes) != 0:
                gt_roi_loc = self.bbox2loc(sample_roi, bboxes[gt_assignment[keep_index]])
                gt_roi_loc = gt_roi_loc / np.array(self.variance)
            else:
                gt_roi_loc = np.zeros_like(sample_roi)
    
            gt_roi_label                            = gt_roi_label[keep_index]
            gt_roi_label[pos_roi_per_this_image:]   = self.num_classes - 1
            
            #---------------------------------------------------------#
            #   X       [n_sample, 4]
            #   Y1      [n_sample, num_classes]
            #   Y2      [n_sample, (num_clssees-1) * 8]
            #---------------------------------------------------------#
            X                   = np.zeros_like(sample_roi)
            X[:, [0, 1, 2, 3]]  = sample_roi[:, [1, 0, 3, 2]]
    
            Y1                  = np.eye(self.num_classes)[np.array(gt_roi_label, np.int32)]
    
            y_class_regr_label  = np.zeros([np.shape(gt_roi_loc)[0], self.num_classes-1, 4])
            y_class_regr_coords = np.zeros([np.shape(gt_roi_loc)[0], self.num_classes-1, 4])
            y_class_regr_label[np.arange(np.shape(gt_roi_loc)[0])[:pos_roi_per_this_image], np.array(gt_roi_label[:pos_roi_per_this_image], np.int32)] = 1
            y_class_regr_coords[np.arange(np.shape(gt_roi_loc)[0])[:pos_roi_per_this_image], np.array(gt_roi_label[:pos_roi_per_this_image], np.int32)] = \
                gt_roi_loc[:pos_roi_per_this_image]
            y_class_regr_label  = np.reshape(y_class_regr_label, [np.shape(gt_roi_loc)[0], -1])
            y_class_regr_coords = np.reshape(y_class_regr_coords, [np.shape(gt_roi_loc)[0], -1])
    
            Y2 = np.concatenate([np.array(y_class_regr_label), np.array(y_class_regr_coords)], axis = 1)
            return X, Y1, Y2
    

    训练自己的Faster-RCNN模型

    首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
    注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

    一定要注意打开后的根目录是文件存放的目录。
    在这里插入图片描述

    一、数据集的准备

    本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    此时数据集的摆放已经结束。

    二、数据集的处理

    在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

    voc_annotation.py里面有一些参数需要设置。
    分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

    '''
    annotation_mode用于指定该文件运行时计算的内容
    annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
    annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
    annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
    '''
    annotation_mode     = 0
    '''
    必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
    与训练和预测所用的classes_path一致即可
    如果生成的2007_train.txt里面没有目标信息
    那么就是因为classes没有设定正确
    仅在annotation_mode为0和2的时候有效
    '''
    classes_path        = 'model_data/voc_classes.txt'
    '''
    trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
    train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
    仅在annotation_mode为0和1的时候有效
    '''
    trainval_percent    = 0.9
    train_percent       = 0.9
    '''
    指向VOC数据集所在的文件夹
    默认指向根目录下的VOC数据集
    '''
    VOCdevkit_path  = 'VOCdevkit'
    

    classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:
    在这里插入图片描述
    训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

    三、开始网络训练

    通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
    训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

    classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!
    在这里插入图片描述
    修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。
    其它参数的作用如下:

    #--------------------------------------------------------#
    #   训练前一定要修改classes_path,使其对应自己的数据集
    #--------------------------------------------------------#
    classes_path    = 'model_data/voc_classes.txt'
    #----------------------------------------------------------------------------------------------------------------------------#
    #   权值文件请看README,百度网盘下载。数据的预训练权重对不同数据集是通用的,因为特征是通用的。
    #   预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。
    #   训练自己的数据集时提示维度不匹配正常,预测的东西都不一样了自然维度不匹配
    #
    #   如果想要断点续练就将model_path设置成logs文件夹下已经训练的权值文件。 
    #   当model_path = ''的时候不加载整个模型的权值。
    #
    #   此处使用的是整个模型的权重,因此是在train.py进行加载的。
    #   如果想要让模型从主干的预训练权值开始训练,则设置model_path为主干网络的权值,此时仅加载主干。
    #   如果想要让模型从0开始训练,则设置model_path = '',Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。
    #   一般来讲,从0开始训练效果会很差,因为权值太过随机,特征提取效果不明显。
    #----------------------------------------------------------------------------------------------------------------------------#
    model_path      = 'model_data/voc_weights_resnet.h5'
    #------------------------------------------------------#
    #   输入的shape大小 
    #------------------------------------------------------#
    input_shape     = [600, 600]
    #---------------------------------------------#
    #   vgg或者resnet50
    #---------------------------------------------#
    backbone        = "resnet50"
    #------------------------------------------------------------------------#
    #   anchors_size用于设定先验框的大小,每个特征点均存在9个先验框。
    #   anchors_size每个数对应3个先验框。
    #   当anchors_size = [8, 16, 32]的时候,生成的先验框宽高约为:
    #   [128, 128] ; [256, 256]; [512, 512]; [128, 256]; 
    #   [256, 512]; [512, 1024]; [256, 128] ; [512, 256]; 
    #   [1024, 512]; 详情查看anchors.py
    #   如果想要检测小物体,可以减小anchors_size靠前的数。
    #   比如设置anchors_size = [64, 256, 512]
    #------------------------------------------------------------------------#
    anchors_size    = [128, 256, 512]
    
    #----------------------------------------------------#
    #   训练分为两个阶段,分别是冻结阶段和解冻阶段。
    #   显存不足与数据集大小无关,提示显存不足请调小batch_size。
    #----------------------------------------------------#
    #----------------------------------------------------#
    #   冻结阶段训练参数
    #   此时模型的主干被冻结了,特征提取网络不发生改变
    #   占用的显存较小,仅对网络进行微调
    #----------------------------------------------------#
    Init_Epoch          = 0
    Freeze_Epoch        = 50
    Freeze_batch_size   = 4
    Freeze_lr           = 1e-4
    #----------------------------------------------------#
    #   解冻阶段训练参数
    #   此时模型的主干不被冻结了,特征提取网络会发生改变
    #   占用的显存较大,网络所有的参数都会发生改变
    #----------------------------------------------------#
    UnFreeze_Epoch      = 100
    Unfreeze_batch_size = 2
    Unfreeze_lr         = 1e-5
    #------------------------------------------------------#
    #   是否进行冻结训练,默认先冻结主干训练后解冻训练。
    #------------------------------------------------------#
    Freeze_Train        = True
    #----------------------------------------------------#
    #   获得图片路径和标签
    #----------------------------------------------------#
    train_annotation_path   = '2007_train.txt'
    val_annotation_path     = '2007_val.txt'
    

    四、训练结果预测

    训练结果预测需要用到两个文件,分别是yolo.py和predict.py。
    我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

    model_path指向训练好的权值文件,在logs文件夹里。
    classes_path指向检测类别所对应的txt。

    在这里插入图片描述
    完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

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

    学习前言

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

    什么是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-tf2
    喜欢的可以点个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)。

    实现代码:

    from tensorflow.keras.layers import Conv2D, MaxPooling2D, ZeroPadding2D
    
    
    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_anchors x 4的卷积
    • 一次num_anchors x num_classes的卷积

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

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

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

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

    import tensorflow.keras.backend as K
    import numpy as np
    from tensorflow.keras.engine.topology import InputSpec, Layer
    from tensorflow.keras.layers import (Activation, Concatenate, Conv2D, Flatten, Input,
                              Reshape)
    from tensorflow.keras.models import Model
    
    from nets.vgg import VGG16
    
    class Normalize(Layer):
        def __init__(self, scale, **kwargs):
            self.axis = 3
            self.scale = scale
            super(Normalize, self).__init__(**kwargs)
    
        def build(self, input_shape):
            self.input_spec = [InputSpec(shape=input_shape)]
            shape = (input_shape[self.axis],)
            init_gamma = self.scale * np.ones(shape)
            self.gamma = K.variable(init_gamma, name='{}_gamma'.format(self.name))
            self.trainable_weights = [self.gamma]
    
        def call(self, x, mask=None):
            output = K.l2_normalize(x, self.axis)
            output *= self.gamma
            return output
    
    def SSD300(input_shape, num_classes=21):
        #---------------------------------#
        #   典型的输入大小为[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_anchors = 4
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv4_3_norm_mbox_loc']        = Conv2D(num_anchors * 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_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv4_3_norm_mbox_conf']       = Conv2D(num_anchors * 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'])
    
        # 对fc7层进行处理 
        # 19,19,1024
        num_anchors = 6
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['fc7_mbox_loc']         = Conv2D(num_anchors * 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_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['fc7_mbox_conf']        = Conv2D(num_anchors * 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'])
    
        # 对conv6_2进行处理
        # 10,10,512
        num_anchors = 6
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv6_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
        net['conv6_2_mbox_loc_flat']    = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
        # num_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv6_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
        net['conv6_2_mbox_conf_flat']   = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])
    
        # 对conv7_2进行处理
        # 5,5,256
        num_anchors = 6
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv7_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
        net['conv7_2_mbox_loc_flat']    = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
        # num_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv7_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
        net['conv7_2_mbox_conf_flat']   = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])
    
        # 对conv8_2进行处理
        # 3,3,256
        num_anchors = 4
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv8_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
        net['conv8_2_mbox_loc_flat']    = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
        # num_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv8_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
        net['conv8_2_mbox_conf_flat']   = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])
    
        # 对conv9_2进行处理
        # 1,1,256
        num_anchors = 4
        # 预测框的处理
        # num_anchors表示每个网格点先验框的数量,4是x,y,h,w的调整
        net['conv9_2_mbox_loc']         = Conv2D(num_anchors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
        net['conv9_2_mbox_loc_flat']    = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
        # num_anchors表示每个网格点先验框的数量,num_classes是所分的类
        net['conv9_2_mbox_conf']        = Conv2D(num_anchors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
        net['conv9_2_mbox_conf_flat']   = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
        
        # 将所有结果进行堆叠
        net['mbox_loc'] = Concatenate(axis=1, name='mbox_loc')([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']])
                                        
        net['mbox_conf'] = Concatenate(axis=1, name='mbox_conf')([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']])
        # 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'])
        net['mbox_conf']    = Activation('softmax', name='mbox_conf_final')(net['mbox_conf'])
        # 8732,25
        net['predictions']  = Concatenate(axis =-1, name='predictions')([net['mbox_loc'], net['mbox_conf']])
    
        model = Model(net['input'], net['predictions'])
        return model
    

    3、预测结果的解码

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

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

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

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

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

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

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

    num_anchors x 4中的num_anchors表示了这个网格点所包含的先验框数量,其中的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、利用框的位置和得分进行非极大抑制。
    实现代码如下:

    import numpy as np
    import tensorflow as tf
    import tensorflow.keras.backend as K
    
    class BBoxUtility(object):
        def __init__(self, num_classes, nms_thresh=0.45, top_k=300):
            self.num_classes    = num_classes
            self._nms_thresh    = nms_thresh
            self._top_k         = top_k
            self.boxes          = K.placeholder(dtype='float32', shape=(None, 4))
            self.scores         = K.placeholder(dtype='float32', shape=(None,))
            self.nms            = tf.image.non_max_suppression(self.boxes, self.scores, self._top_k, iou_threshold=self._nms_thresh)
            self.sess           = K.get_session()
    
        def ssd_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image):
            #-----------------------------------------------------------------#
            #   把y轴放前面是因为方便预测框和图像的宽高进行相乘
            #-----------------------------------------------------------------#
            box_yx = box_xy[..., ::-1]
            box_hw = box_wh[..., ::-1]
            input_shape = np.array(input_shape)
            image_shape = np.array(image_shape)
    
            if letterbox_image:
                #-----------------------------------------------------------------#
                #   这里求出来的offset是图像有效区域相对于图像左上角的偏移情况
                #   new_shape指的是宽高缩放情况
                #-----------------------------------------------------------------#
                new_shape = np.round(image_shape * np.min(input_shape/image_shape))
                offset  = (input_shape - new_shape)/2./input_shape
                scale   = input_shape/new_shape
    
                box_yx  = (box_yx - offset) * scale
                box_hw *= scale
    
            box_mins    = box_yx - (box_hw / 2.)
            box_maxes   = box_yx + (box_hw / 2.)
            boxes  = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1)
            boxes *= np.concatenate([image_shape, image_shape], axis=-1)
            return boxes
    
        def decode_boxes(self, mbox_loc, anchors, variances):
            # 获得先验框的宽与高
            anchor_width     = anchors[:, 2] - anchors[:, 0]
            anchor_height    = anchors[:, 3] - anchors[:, 1]
            # 获得先验框的中心点
            anchor_center_x  = 0.5 * (anchors[:, 2] + anchors[:, 0])
            anchor_center_y  = 0.5 * (anchors[:, 3] + anchors[:, 1])
    
            # 真实框距离先验框中心的xy轴偏移情况
            decode_bbox_center_x = mbox_loc[:, 0] * anchor_width * variances[0]
            decode_bbox_center_x += anchor_center_x
            decode_bbox_center_y = mbox_loc[:, 1] * anchor_height * variances[1]
            decode_bbox_center_y += anchor_center_y
            
            # 真实框的宽与高的求取
            decode_bbox_width   = np.exp(mbox_loc[:, 2] * variances[2])
            decode_bbox_width   *= anchor_width
            decode_bbox_height  = np.exp(mbox_loc[:, 3] * variances[3])
            decode_bbox_height  *= anchor_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 decode_box(self, predictions, anchors, image_shape, input_shape, letterbox_image, variances = [0.1, 0.1, 0.2, 0.2], confidence=0.5):
            #---------------------------------------------------#
            #   :4是回归预测结果
            #---------------------------------------------------#
            mbox_loc        = predictions[:, :, :4]
            #---------------------------------------------------#
            #   获得种类的置信度
            #---------------------------------------------------#
            mbox_conf       = 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], anchors, variances)
    
                for c in range(1, self.num_classes):
                    #--------------------------------#
                    #   取出属于该类的所有框的置信度
                    #   判断是否大于门限
                    #--------------------------------#
                    c_confs     = mbox_conf[i, :, c]
                    c_confs_m   = c_confs > confidence
                    if len(c_confs[c_confs_m]) > 0:
                        #-----------------------------------------#
                        #   取出得分高于confidence的框
                        #-----------------------------------------#
                        boxes_to_process = decode_bbox[c_confs_m]
                        confs_to_process = c_confs[c_confs_m]
                        #-----------------------------------------#
                        #   进行iou的非极大抑制
                        #-----------------------------------------#
                        idx         = self.sess.run(self.nms, feed_dict={self.boxes: boxes_to_process, self.scores: confs_to_process})
                        #-----------------------------------------#
                        #   取出在非极大抑制中效果较好的内容
                        #-----------------------------------------#
                        good_boxes  = boxes_to_process[idx]
                        confs       = confs_to_process[idx][:, None]
                        labels      = (c - 1) * np.ones((len(idx), 1))
                        #-----------------------------------------#
                        #   将label、置信度、框的位置进行堆叠。
                        #-----------------------------------------#
                        c_pred      = np.concatenate((good_boxes, labels, confs), axis=1)
                        # 添加进result里
                        results[-1].extend(c_pred)
    
                if len(results[-1]) > 0:
                    results[-1] = np.array(results[-1])
                    box_xy, box_wh = (results[-1][:, 0:2] + results[-1][:, 2:4])/2, results[-1][:, 2:4] - results[-1][:, 0:2]
                    results[-1][:, :4] = self.ssd_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)
    
            return results
    

    4、在原图上进行绘制

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

    二、训练部分

    1、真实框的处理

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

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

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

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

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

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

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

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

    实现代码如下:

    def assign_boxes(self, boxes):
        #---------------------------------------------------#
        #   assignment分为3个部分
        #   :4      的内容为网络应该有的回归预测结果
        #   4:-1    的内容为先验框所对应的种类,默认为背景
        #   -1      的内容为当前先验框是否包含目标
        #---------------------------------------------------#
        assignment          = np.zeros((self.num_anchors, 4 + self.num_classes + 1))
        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_anchors, 4 + 1]
        #   4是编码后的结果,1为iou
        #---------------------------------------------------#
        encoded_boxes   = encoded_boxes.reshape(-1, self.num_anchors, 5)
        
        #---------------------------------------------------#
        #   [num_anchors]求取每一个先验框重合度最大的真实框
        #---------------------------------------------------#
        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:-1][best_iou_mask]  = boxes[best_iou_idx, 4:]
        #----------------------------------------------------------#
        #   -1表示先验框是否有对应的物体
        #----------------------------------------------------------#
        assignment[:, -1][best_iou_mask]    = 1
        # 通过assign_boxes我们就获得了,输入进来的这张图片,应该有的预测结果是什么样子的
        return assignment
    

    b、真实框的编码

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

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

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

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

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

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

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

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

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

    • num_anchors x 4的卷积
    • num_anchors x num_classes的卷积

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

    实现代码如下:

    def encode_box(self, box, return_iou=True, variances = [0.1, 0.1, 0.2, 0.2]):
        #---------------------------------------------#
        #   计算当前真实框和先验框的重合情况
        #   iou [self.num_anchors]
        #   encoded_box [self.num_anchors, 5]
        #---------------------------------------------#
        iou = self.iou(box)
        encoded_box = np.zeros((self.num_anchors, 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_anchors = self.anchors[assign_mask]
    
        #---------------------------------------------#
        #   逆向编码,将真实框转化为ssd预测结果的格式
        #   先计算真实框的中心与长宽
        #---------------------------------------------#
        box_center  = 0.5 * (box[:2] + box[2:])
        box_wh      = box[2:] - box[:2]
        #---------------------------------------------#
        #   再计算重合度较高的先验框的中心与长宽
        #---------------------------------------------#
        assigned_anchors_center = (assigned_anchors[:, 0:2] + assigned_anchors[:, 2:4]) * 0.5
        assigned_anchors_wh     = (assigned_anchors[:, 2:4] - assigned_anchors[:, 0:2])
        
        #------------------------------------------------#
        #   逆向求取ssd应该有的预测结果
        #   先求取中心的预测结果,再求取宽高的预测结果
        #   存在改变数量级的参数,默认为[0.1,0.1,0.2,0.2]
        #------------------------------------------------#
        encoded_box[:, :2][assign_mask] = box_center - assigned_anchors_center
        encoded_box[:, :2][assign_mask] /= assigned_anchors_wh
        encoded_box[:, :2][assign_mask] /= np.array(variances)[:2]
    
        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_anchors_wh)
        encoded_box[:, 2:4][assign_mask] /= np.array(variances)[2:4]
        return encoded_box.ravel()
    

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

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

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

    实现代码如下:

    import tensorflow as tf
    
    
    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):
            # --------------------------------------------- #
            #   y_true batch_size, 8732, 4 + self.num_classes + 1
            #   y_pred batch_size, 8732, 4 + self.num_classes
            # --------------------------------------------- #
            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:-1],
                                           y_pred[:, :, 4:])
            # --------------------------------------------- #
            #   框的位置的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[:, :, -1],
                                         axis=1)
            pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -1],
                                          axis=1)
    
            # --------------------------------------------- #
            #   每一张图的正样本的个数
            #   num_pos     [batch_size,]
            # --------------------------------------------- #
            num_pos = tf.reduce_sum(y_true[:, :, -1], axis=-1)
    
            # --------------------------------------------- #
            #   每一张图的负样本的个数
            #   num_neg     [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[:, :, -1]), [-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模型

    首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
    注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

    一定要注意打开后的根目录是文件存放的目录。
    在这里插入图片描述

    一、数据集的准备

    本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    此时数据集的摆放已经结束。

    二、数据集的处理

    在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

    voc_annotation.py里面有一些参数需要设置。
    分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

    '''
    annotation_mode用于指定该文件运行时计算的内容
    annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
    annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
    annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
    '''
    annotation_mode     = 0
    '''
    必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
    与训练和预测所用的classes_path一致即可
    如果生成的2007_train.txt里面没有目标信息
    那么就是因为classes没有设定正确
    仅在annotation_mode为0和2的时候有效
    '''
    classes_path        = 'model_data/voc_classes.txt'
    '''
    trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
    train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
    仅在annotation_mode为0和1的时候有效
    '''
    trainval_percent    = 0.9
    train_percent       = 0.9
    '''
    指向VOC数据集所在的文件夹
    默认指向根目录下的VOC数据集
    '''
    VOCdevkit_path  = 'VOCdevkit'
    

    classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:
    在这里插入图片描述

    训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

    三、开始网络训练

    通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
    训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

    classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!
    在这里插入图片描述

    修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。
    其它参数的作用如下:

    #--------------------------------------------------------#
    #   训练前一定要修改classes_path,使其对应自己的数据集
    #--------------------------------------------------------#
    classes_path    = 'model_data/voc_classes.txt'
    #----------------------------------------------------------------------------------------------------------------------------#
    #   权值文件请看README,百度网盘下载。数据的预训练权重对不同数据集是通用的,因为特征是通用的。
    #   预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显,网络训练的结果也不会好。
    #   训练自己的数据集时提示维度不匹配正常,预测的东西都不一样了自然维度不匹配
    #
    #   如果想要断点续练就将model_path设置成logs文件夹下已经训练的权值文件。 
    #   当model_path = ''的时候不加载整个模型的权值。
    #
    #   此处使用的是整个模型的权重,因此是在train.py进行加载的。
    #   如果想要让模型从主干的预训练权值开始训练,则设置model_path为主干网络的权值,此时仅加载主干。
    #   如果想要让模型从0开始训练,则设置model_path = '',Freeze_Train = Fasle,此时从0开始训练,且没有冻结主干的过程。
    #   一般来讲,从0开始训练效果会很差,因为权值太过随机,特征提取效果不明显。
    #----------------------------------------------------------------------------------------------------------------------------#
    model_path      = 'model_data/ssd_weights.h5'
    #------------------------------------------------------#
    #   输入的shape大小
    #------------------------------------------------------#
    input_shape     = [300, 300]
    #----------------------------------------------------#
    #   可用于设定先验框的大小,默认的anchors_size
    #   是根据voc数据集设定的,大多数情况下都是通用的!
    #   如果想要检测小物体,可以修改anchors_size
    #   一般调小浅层先验框的大小就行了!因为浅层负责小物体检测!
    #   比如anchors_size = [21, 45, 99, 153, 207, 261, 315]
    #----------------------------------------------------#
    anchors_size    = [30, 60, 111, 162, 213, 264, 315]
    
    #----------------------------------------------------#
    #   训练分为两个阶段,分别是冻结阶段和解冻阶段。
    #   显存不足与数据集大小无关,提示显存不足请调小batch_size。
    #   受到BatchNorm层影响,batch_size最小为2,不能为1。
    #----------------------------------------------------#
    #----------------------------------------------------#
    #   冻结阶段训练参数
    #   此时模型的主干被冻结了,特征提取网络不发生改变
    #   占用的显存较小,仅对网络进行微调
    #----------------------------------------------------#
    Init_Epoch          = 0
    Freeze_Epoch        = 50
    Freeze_batch_size   = 16
    Freeze_lr           = 5e-4
    #----------------------------------------------------#
    #   解冻阶段训练参数
    #   此时模型的主干不被冻结了,特征提取网络会发生改变
    #   占用的显存较大,网络所有的参数都会发生改变
    #----------------------------------------------------#
    UnFreeze_Epoch      = 100
    Unfreeze_batch_size = 8
    Unfreeze_lr         = 5e-5
    #------------------------------------------------------#
    #   是否进行冻结训练,默认先冻结主干训练后解冻训练。
    #------------------------------------------------------#
    Freeze_Train        = True
    #------------------------------------------------------#
    #   用于设置是否使用多线程读取数据,0代表关闭多线程
    #   开启后会加快数据读取速度,但是会占用更多内存
    #   keras里开启多线程有些时候速度反而慢了许多
    #   在IO为瓶颈的时候再开启多线程,即GPU运算速度远大于读取图片的速度。
    #------------------------------------------------------#
    num_workers         = 0
    #----------------------------------------------------#
    #   获得图片路径和标签
    #----------------------------------------------------#
    train_annotation_path   = '2007_train.txt'
    val_annotation_path     = '2007_val.txt'
    

    四、训练结果预测

    训练结果预测需要用到两个文件,分别是yolo.py和predict.py。
    我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

    model_path指向训练好的权值文件,在logs文件夹里。
    classes_path指向检测类别所对应的txt。

    在这里插入图片描述
    完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

    展开全文
  • 睿智目标检测28——YoloV4当中的Mosaic数据增强方法学习前言什么是Mosaic数据增强方法实现思路全部代码 学习前言 哈哈哈!我又来数据增强了! 什么是Mosaic数据增强方法 Yolov4的mosaic数据增强参考了CutMix数据...
  • 睿智目标检测26——Pytorch搭建yolo3目标检测平台

    万次阅读 多人点赞 2020-04-13 15:15:43
    睿智目标检测26——Pytorch搭建yolo3目标检测平台学习前言源码下载yolo3实现思路一、预测部分1、主题网络darknet53介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、计算loss所需...
  • 睿智目标检测23——Pytorch搭建SSD目标检测平台

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

    万次阅读 多人点赞 2020-05-24 14:14:13
    睿智目标检测30——Pytorch搭建YoloV4目标检测平台学习前言什么是YOLOV4代码下载YOLOV4改进的部分(不完全)YOLOV4结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行预测4、预测...
  • 睿智目标检测19——Keras搭建Mask R-CNN实例分割平台学习前言什么是Mask R-CNN源码下载Mask R-CNN实现思路一、预测部分1、主干网络介绍2、特征金字塔FPN的构建3、获得Proposal建议框4、Proposal建议框的解码5、对...
  • 睿智目标检测11——Keras搭建yolo3目标检测平台

    万次阅读 多人点赞 2019-11-27 17:33:51
    睿智目标检测11——Keras搭建yolo3目标检测平台学习前言yolo3实现思路一、预测部分1、主题网络darknet53介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分计算loss所需参数1、y_pre2、y...
  • 睿智目标检测53——Pytorch搭建YoloX目标检测平台

    万次阅读 多人点赞 2021-09-25 20:20:25
    睿智目标检测53——Pytorch搭建YoloX目标检测平台学习前言源码下载YoloX改进的部分(不完全)YoloX实现思路一、整体结构解析二、网络结构解析1、主干网络CSPDarknet介绍2、构建FPN特征金字塔进行加强特征提取3、...
  • 睿智目标检测4——SSD算法训练部分详解(亲测可用)学习前言讲解构架模型训练的流程1、设置参数2、读取数据集3、建立ssd网络。4、预处理数据集5、框的编码6、计算loss值7、训练模型并保存开始训练 学习前言 ……又...
  • 睿智目标检测29——Pytorch搭建Centernet目标检测平台学习前言什么是Centernet目标检测算法源码下载Centernet实现思路一、预测部分1、主干网络介绍2、利用初步特征获得高分辨率特征图3、Center Head从特征获取预测...
  • 相比于YOLOv2,YOLOv3主要做了如下改进:1、提出了新的特征提取器模型Darknet53,该模型相比于Darknet19采用了残差单元(类似...检测模型网络结构如下所示: 2、多尺度预测 YOLOv3使用了3个尺度的feature map(当
  • 睿智目标检测48——Tensorflow2 搭建自己的Centernet目标检测平台学习前言什么是Centernet目标检测算法源码下载Centernet实现思路一、预测部分1、主干网络介绍2、利用初步特征获得高分辨率特征图3、Center Head从...
  • 睿智目标检测32——TF2搭建YoloV4目标检测平台(tensorflow2)学习前言什么是YOLOV4代码下载YOLOV4改进的部分(不完全)YOLOV4结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行...
  • 睿智目标检测12——使用labelimg进行目标检测数据集标注学习前言labelimg的安装进行批量标注 学习前言 好久没写博文了,先水一水。 labelimg的安装 其实安装labelimg很简单,网上给了各种各样的方法,其实还不如...
  • 睿智目标检测1——IOU的概念与python实例

    千次阅读 多人点赞 2019-10-08 20:47:27
    睿智目标检测1——IOU的概念与python实例学习前言什么是IOUIOU的特点全部代码 学习前言 神经网络的应用还有许多,目标检测就是其中之一,目标检测中有一个很重要的概念便是IOU。 什么是IOU IOU是一种评价目标检测...
  • 睿智目标检测21——如何调用摄像头进行目标检测学习前言GITHUB代码下载使用到的库实现思路实现代码 学习前言 好多人都想了解一下如何对摄像头进行调用,然后进行目标检测,于是我做了这个小BLOG。 GITHUB代码下载...
  • 睿智目标检测24—Keras搭建Mobilenet-SSD目标检测平台学习前言什么是SSD目标检测算法源码下载SSD实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、训练部分1、...
  • 睿智目标检测28——Keras搭建Faster R-CNN目标检测平台学习前言什么是Retinanet目标检测算法源码下载Retinanet实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二...
  • 原文链接:https://blog.csdn.net/weixin_44791964/article/details/103276106 睿智目标检测11——Keras搭建yolo3目标检测平台
  • 睿智目标检测35——Pytorch 搭建YoloV4-Tiny目标检测平台学习前言什么是YOLOV4-Tiny代码下载YoloV4-Tiny结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行预测4、预测结果的解码5...
  • 睿智目标检测41——Keras搭建Retinanet目标检测平台学习前言什么是Retinanet目标检测算法源码下载Retinanet实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二、...
  • 睿智目标检测7——yolo3详解及其预测代码复现

    万次阅读 多人点赞 2019-10-23 15:30:35
    睿智目标检测7——yolo3详解及其预测代码复现学习前言代码下载实现思路1、yolo3的预测思路(网络构建思路)2、利用先验框对网络的输出进行解码3、进行得分排序与非极大抑制筛选实现结果 学习前言 对yolo2解析完了...
  • 睿智目标检测33—Pytorch搭建Efficientdet目标检测平台学习前言什么是Efficientdet目标检测算法源码下载Efficientdet实现思路一、预测部分1、主干网络介绍2、BiFPN加强特征提取3、从特征获取预测结果4、预测结果的...
  • 睿智目标检测-番外篇——数据增强在目标检测中的应用学习前言数据增强做了什么 学习前言 数据增强是非常重要的提高目标检测算法鲁棒性的手段,学习一下对身体有好处! 数据增强做了什么 ...
  • 睿智目标检测52——Keras搭建YoloX目标检测平台

    千次阅读 多人点赞 2021-09-02 00:26:19
    睿智目标检测52——Keras搭建YoloX目标检测平台学习前言源码下载YoloX实现思路一、整体结构解析二、网络结构解析1、主干网络CSPDarknet介绍2、构建FPN特征金字塔进行加强特征提取3、利用Yolo Head获得预测结果三、...
  • 睿智目标检测3——SSD算法预测部分详解学习前言什么是SSD算法讲解构架ssd_vgg_300主体的源码1、 大体框架2、net网络构建3、anchor先验框生成4、bboxes_decode框的解码利用ssd_vgg_300进行预测预测步骤具体预测过程...
  • 睿智目标检测39——Pytorch 利用mobilenet系列(v1,v2,v3)搭建yolov4目标检测平台学习前言源码下载网络替换实现思路1、mobilenet系列网络介绍a、mobilenetV1介绍b、mobilenetV2介绍c、mobilenetV3介绍2、将预测...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 826
精华内容 330
关键字:

睿智的目标检测