当前位置: 移动技术网 > IT编程>脚本编程>Python > SSD目标检测网络学习笔记--Tensorflow框架(附代码链接)

SSD目标检测网络学习笔记--Tensorflow框架(附代码链接)

2020年07月30日  | 移动技术网IT编程  | 我要评论
SSD目标检测网络SSD网络概述SSD网络将输入的图片resize成300×300的大小。用深度神经网络进行特征提取。得到不同大小的特征层(38×38×512,19×19×1024,10×10×512, 5×5×6, 3×3×256, 1×1×256)。每个特征层可以看做对图片划分成不同的网格,每个网格对应若干先验框。训练的过程是对先验框进行调整的过程。这6中不同大小网格,每一个网格对应的先验框的个数为:(4, 6, 6, 6, 4, 4)。计算得到一共需要计算先验框的个数为:38×38×4+

SSD目标检测网络

1、SSD网络概述

SSD网络将输入的图片resize成300×300的大小。用深度神经网络进行特征提取。得到不同大小的特征层(38×38×512,19×19×1024,10×10×512, 5×5×6, 3×3×256, 1×1×256)
每个特征层可以看做对图片划分成不同的网格,每个网格对应若干先验框。训练的过程是对先验框进行调整的过程。
这6中不同大小网格,每一个网格对应的先验框的个数为:(4, 6, 6, 6, 4, 4)
计算得到一共需要计算先验框的个数为:
38×38×4+19×19×6+10×10×6+5×5×6+3×3×4+1×1×4=8732
通过非极大值抑制得到最终的预测结果。

2、SSD主干特征提取网络

SSD网络的输入特征层大小规定为300×300×3。经过9个block特征提取,得到6个有效特征层。
block1代码如下:

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'])

对于输入的特征层,经过两次3×3卷积+1次步长为2的最大池化层得到特征层大小为150×150×64


block2代码如下:

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'])

经过两次3×3卷积+一次步长为2的最大池化层,输出特征层大小为75×75×128


block3代码如下:

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'])

经过3次3×3卷积+一次步长为2的最大池化层,输出特征层大小为38×38×256


block4代码如下:

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'])

经过3次3×3卷积+1次步长为2的最大池化层,经过三次卷积输出特征层大小为38×38×512 该特征层作为有效特征层进行下一步操作。 block4输出特征层大小为19×19×512



block5代码如下:

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'])

经过3次3×3卷积+1次步长为1的最大池化层,输出特征层大小为19×19×512


FC6、FC7代码如下:

net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
                                     activation='relu', padding='same',
                                     name='fc6')(net['pool5'])
net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
                               padding='same', name='fc7')(net['fc6'])

空洞卷积+1×1卷积,输出特征层大小为19×19×1024该特征层作为有效特征层进行下一步操作。


block6代码如下:

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'])

经过1次1×1卷积+zeropadding+步长为2的3×3卷积,输出通道数为10×10×512该特征层作为有效特征层进行下一步操作。


block7代码如下:

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'])

经过1次1×1卷积+zeropadding+一次步长为2的3×3卷积,输出特征层大小为5×5×256该特征层作为有效特征层进行下一步操作。


block8代码如下:

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'])

经过一次1×1卷积+一次3×3卷积,输出特征层大小为3×3×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'])

一次1×1卷积+一次步长为2的3×3卷积,输出特征层大小为1×1×256该特征层作为有效特征层进行下一步操作。

  • 至此SSD网络的特征提取过程完成

3、进一步处理提取的6个有效特征层

对共享特征层尽心进一步处理,得到的结果包括对先验框的调整参数调整以后的先验框中物体所属的类别
这里先验框的代码调用了ssd_layers.py中的PriorBox类。

class PriorBox(Layer):
    def __init__(self, img_size, min_size, max_size=None, aspect_ratios=None,
                 flip=True, variances=[0.1], clip=True, **kwargs):

        self.waxis = 2
        self.haxis = 1

        self.img_size = img_size
        if min_size <= 0:
            raise Exception('min_size must be positive.')

        self.min_size = min_size
        self.max_size = max_size
        self.aspect_ratios = [1.0]
        if max_size:
            if max_size < min_size:
                raise Exception('max_size must be greater than min_size.')
            self.aspect_ratios.append(1.0)
        if aspect_ratios:
            for ar in aspect_ratios:
                if ar in self.aspect_ratios:
                    continue
                self.aspect_ratios.append(ar)
                if flip:
                    self.aspect_ratios.append(1.0 / ar)
        self.variances = np.array(variances)
        self.clip = True
        super(PriorBox, self).__init__(**kwargs)

函数输入参数包括:img_size图片大小min_size确定先验框大小的最小值max_size确定先验框大小的最大值aspect_ratios计算比率
确保0 < min_size < max_size

函数call代码如下:

    def call(self, x, mask=None):
        if hasattr(x, '_keras_shape'):
            input_shape = x._keras_shape
        elif hasattr(K, 'int_shape'):
            input_shape = K.int_shape(x)

        layer_width = input_shape[self.waxis]
        layer_height = input_shape[self.haxis]

        img_width = self.img_size[0]
        img_height = self.img_size[1]
        box_widths = []
        box_heights = []
        for ar in self.aspect_ratios:
            if ar == 1 and len(box_widths) == 0:
                box_widths.append(self.min_size)
                box_heights.append(self.min_size)
            elif ar == 1 and len(box_widths) > 0:
                box_widths.append(np.sqrt(self.min_size * self.max_size))
                box_heights.append(np.sqrt(self.min_size * self.max_size))
            elif ar != 1:
                box_widths.append(self.min_size * np.sqrt(ar))
                box_heights.append(self.min_size / np.sqrt(ar))
        box_widths = 0.5 * np.array(box_widths)
        box_heights = 0.5 * np.array(box_heights)
        step_x = img_width / layer_width
        step_y = img_height / layer_height
        linx = np.linspace(0.5 * step_x, img_width - 0.5 * step_x,
                           layer_width)
        liny = np.linspace(0.5 * step_y, img_height - 0.5 * step_y,
                           layer_height)
        centers_x, centers_y = np.meshgrid(linx, liny)
        centers_x = centers_x.reshape(-1, 1)
        centers_y = centers_y.reshape(-1, 1)

        num_priors_ = len(self.aspect_ratios)
        # 每一个先验框需要两个(centers_x, centers_y),前一个用来计算左上角,后一个计算右下角
        prior_boxes = np.concatenate((centers_x, centers_y), axis=1)
        prior_boxes = np.tile(prior_boxes, (1, 2 * num_priors_))
        
        # 获得先验框的左上角和右下角
        prior_boxes[:, ::4] -= box_widths
        prior_boxes[:, 1::4] -= box_heights
        prior_boxes[:, 2::4] += box_widths
        prior_boxes[:, 3::4] += box_heights

        # 变成小数的形式
        prior_boxes[:, ::2] /= img_width
        prior_boxes[:, 1::2] /= img_height
        prior_boxes = prior_boxes.reshape(-1, 4)

        prior_boxes = np.minimum(np.maximum(prior_boxes, 0.0), 1.0)

        num_boxes = len(prior_boxes)
        
        if len(self.variances) == 1:
            variances = np.ones((num_boxes, 4)) * self.variances[0]
        elif len(self.variances) == 4:
            variances = np.tile(self.variances, (num_boxes, 1))
        else:
            raise Exception('Must provide one or four variances.')

        prior_boxes = np.concatenate((prior_boxes, variances), axis=1)
        prior_boxes_tensor = K.expand_dims(tf.cast(prior_boxes, dtype=tf.float32), 0)
    
        pattern = [tf.shape(x)[0], 1, 1]
        prior_boxes_tensor = tf.tile(prior_boxes_tensor, pattern)

        return prior_boxes_tensor

提取输入图片的宽、高,遍历所有计算比率aspect_ratios
如果aspect_ratios=1,则存在一个先验框的宽、高等于min_size,和一个先验框的宽、高等于sqrt(self.min_size * self.max_size);对于计算比率不等于1的情况,先验框的宽、高分别等于min_size * np.sqrt(ar)min_size / np.sqrt(ar)
然后计算每一个先验框的中心点坐标,然后转化成先验框左上角坐标和宽高的,并专户成小数的形式。

  1. 对于38×38×512的特征层,处理的代码如下:
net['conv4_3_norm'] = Normalize(20, name='conv4_3_norm')(net['conv4_3'])
num_priors = 4
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
net['conv4_3_norm_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same', name='conv4_3_norm_mbox_loc')(net['conv4_3_norm'])
net['conv4_3_norm_mbox_loc_flat'] = Flatten(name='conv4_3_norm_mbox_loc_flat')(net['conv4_3_norm_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
net['conv4_3_norm_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv4_3_norm_mbox_conf')(net['conv4_3_norm'])
net['conv4_3_norm_mbox_conf_flat'] = Flatten(name='conv4_3_norm_mbox_conf_flat')(net['conv4_3_norm_mbox_conf'])
priorbox = PriorBox(img_size, 30.0,max_size = 60.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv4_3_norm_mbox_priorbox')
net['conv4_3_norm_mbox_priorbox'] = priorbox(net['conv4_3_norm'])

conv4_3_norm代表38×38×512的特征层。(以下同理)
首先对特征层进行归一化处理,输出更有利于获得更好的预测框。
定义该特征层的每一个网格上有4个先验框。
3×3卷积,输出通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到38×38×512特征层的所有先验框。


  1. 对于大小为19×19×1024的特征层,处理的代码如下:
num_priors = 6
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
net['fc7_mbox_loc'] = Conv2D(num_priors * 4, kernel_size=(3,3),padding='same',name='fc7_mbox_loc')(net['fc7'])
net['fc7_mbox_loc_flat'] = Flatten(name='fc7_mbox_loc_flat')(net['fc7_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
net['fc7_mbox_conf'] = Conv2D(num_priors * num_classes, kernel_size=(3,3),padding='same',name='fc7_mbox_conf')(net['fc7'])
net['fc7_mbox_conf_flat'] = Flatten(name='fc7_mbox_conf_flat')(net['fc7_mbox_conf'])
priorbox = PriorBox(img_size, 60.0, max_size=111.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='fc7_mbox_priorbox')
net['fc7_mbox_priorbox'] = priorbox(net['fc7'])

首先定义特征层中每一个网格对应的先验框个数为6,
3×3的卷积,输出的通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到19×19×1024特征层的所有先验框。



  1. 对于大小为10×10×512的特征层,处理的代码如下:
num_priors = 6
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv6_2_mbox_loc')(net['conv6_2'])
net['conv6_2_mbox_loc'] = x
net['conv6_2_mbox_loc_flat'] = Flatten(name='conv6_2_mbox_loc_flat')(net['conv6_2_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv6_2_mbox_conf')(net['conv6_2'])
net['conv6_2_mbox_conf'] = x
net['conv6_2_mbox_conf_flat'] = Flatten(name='conv6_2_mbox_conf_flat')(net['conv6_2_mbox_conf'])
priorbox = PriorBox(img_size, 111.0, max_size=162.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv6_2_mbox_priorbox')
net['conv6_2_mbox_priorbox'] = priorbox(net['conv6_2'])

首先定义每一个网格对应的先验框个数为6;
3×3的卷积,输出的通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到10×10×512特征层的所有先验框。


  1. 对于5×5×256的特征层,处理的代码如下:
num_priors = 6
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv7_2_mbox_loc')(net['conv7_2'])
net['conv7_2_mbox_loc'] = x
net['conv7_2_mbox_loc_flat'] = Flatten(name='conv7_2_mbox_loc_flat')(net['conv7_2_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv7_2_mbox_conf')(net['conv7_2'])
net['conv7_2_mbox_conf'] = x
net['conv7_2_mbox_conf_flat'] = Flatten(name='conv7_2_mbox_conf_flat')(net['conv7_2_mbox_conf'])
priorbox = PriorBox(img_size, 162.0, max_size=213.0, aspect_ratios=[2, 3],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv7_2_mbox_priorbox')
net['conv7_2_mbox_priorbox'] = priorbox(net['conv7_2'])

首先定义每一个网格对应的先验框个数为6;
3×3的卷积,输出的通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到5×5×256特征层的所有先验框。


  1. 对于3×3×256的特征层,处理的代码如下:
num_priors = 4
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv8_2_mbox_loc')(net['conv8_2'])
net['conv8_2_mbox_loc'] = x
net['conv8_2_mbox_loc_flat'] = Flatten(name='conv8_2_mbox_loc_flat')(net['conv8_2_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv8_2_mbox_conf')(net['conv8_2'])
net['conv8_2_mbox_conf'] = x
net['conv8_2_mbox_conf_flat'] = Flatten(name='conv8_2_mbox_conf_flat')(net['conv8_2_mbox_conf'])
priorbox = PriorBox(img_size, 213.0, max_size=264.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv8_2_mbox_priorbox')
net['conv8_2_mbox_priorbox'] = priorbox(net['conv8_2'])

首先定义每一个网格对应的先验框个数为4;
3×3的卷积,输出的通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到3×3×256特征层的所有先验框。


  1. 对于1×1×256的特征层,处理的代码如下:
num_priors = 4 
# 预测框的处理
# num_priors表示每个网格点先验框的数量,4是x,y,h,w的调整
x = Conv2D(num_priors * 4, kernel_size=(3,3), padding='same',name='conv9_2_mbox_loc')(net['conv9_2'])
net['conv9_2_mbox_loc'] = x
net['conv9_2_mbox_loc_flat'] = Flatten(name='conv9_2_mbox_loc_flat')(net['conv9_2_mbox_loc'])
# num_priors表示每个网格点先验框的数量,num_classes是所分的类
x = Conv2D(num_priors * num_classes, kernel_size=(3,3), padding='same',name='conv9_2_mbox_conf')(net['conv9_2'])
net['conv9_2_mbox_conf'] = x
net['conv9_2_mbox_conf_flat'] = Flatten(name='conv9_2_mbox_conf_flat')(net['conv9_2_mbox_conf'])
priorbox = PriorBox(img_size, 264.0, max_size=315.0, aspect_ratios=[2],
                        variances=[0.1, 0.1, 0.2, 0.2],
                        name='conv9_2_mbox_priorbox')
net['conv9_2_mbox_priorbox'] = priorbox(net['conv9_2'])

首先定义每一个网格对应的先验框个数为4;
3×3的卷积,输出的通道数为num_priors * 4表示先验框的调整参数
将输出的神经元平铺,
3×3卷积,输出通道数是num_priors * num_classes,表示调整以后先验框中物体所属类别
将输出神经元平铺。
调用PriorBox,得到1×1×256特征层的所有先验框。


对结果进行堆叠。

4、SSD网络的解码过程

解码过程是指,对于SSD网络的输出结果为先验框的调整参数,将这个调整参数反应在先验框上,得到调整后的先验框。
ssd.py

_defaults = {
        "model_path": 'model_data/ssd_weights.h5',
        "classes_path": 'model_data/voc_classes.txt',
        # 这里输入到SSD网络中图片的大小规定为(300, 300, 3)
        "model_image_size" : (300, 300, 3),
        # 置信度设置的阈值
        "confidence": 0.5,
    }

首先初始化权重存放的位置、物体所属类别对应.txt文件的存放位置、还有SSD网络规定输入的图片大小、还有设置的置信度的阈值即置信度超过某一值就可以将该物体认定为属于该类别。


    def generate(self):
        model_path = os.path.expanduser(self.model_path)
        assert model_path.endswith('.h5'), 'Keras model or weights must be a .h5 file.'
        
        # 计算总的种类
        self.num_classes = len(self.class_names) + 1

        # 载入模型,如果原来的模型里已经包括了模型结构则直接载入。
        # 否则先构建模型再载入

        self.ssd_model = ssd.SSD300(self.model_image_size,self.num_classes)
        self.ssd_model.load_weights(self.model_path,by_name=True)

        self.ssd_model.summary()
        print('{} model, anchors, and classes loaded.'.format(model_path))

        # 画框设置不同的颜色
        hsv_tuples = [(x / len(self.class_names), 1., 1.)
                      for x in range(len(self.class_names))]
        self.colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
        self.colors = list(
            map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)),
                self.colors))

generate函数中,载入模型文件,
载入总的类别数+1(背景),
然后为最终的框设置不同的颜色。


在最终的预测代码predict.py中,调用函数detect_image
函数detect_image代码如下:

    def detect_image(self, image):
        image_shape = np.array(np.shape(image)[0:2])
        crop_img,x_offset,y_offset = letterbox_image(image, (self.model_image_size[0],self.model_image_size[1]))
        photo = np.array(crop_img,dtype = np.float64)

        # 图片预处理,归一化
        photo = preprocess_input(np.reshape(photo,[1,self.model_image_size[0],self.model_image_size[1],3]))
        preds = self.get_pred(photo).numpy()

        # 将预测结果进行解码
        results = self.bbox_util.detection_out(preds, confidence_threshold=self.confidence)
        
        if len(results[0])<=0:
            return image

        # 筛选出其中得分高于confidence的框
        det_label = results[0][:, 0]
        det_conf = results[0][:, 1]
        det_xmin, det_ymin, det_xmax, det_ymax = results[0][:, 2], results[0][:, 3], results[0][:, 4], results[0][:, 5]
        top_indices = [i for i, conf in enumerate(det_conf) if conf >= self.confidence]
        top_conf = det_conf[top_indices]
        top_label_indices = det_label[top_indices].tolist()
        top_xmin, top_ymin, top_xmax, top_ymax = np.expand_dims(det_xmin[top_indices],-1),np.expand_dims(det_ymin[top_indices],-1),np.expand_dims(det_xmax[top_indices],-1),np.expand_dims(det_ymax[top_indices],-1)
        
        # 去掉灰条
        boxes = ssd_correct_boxes(top_ymin,top_xmin,top_ymax,top_xmax,np.array([self.model_image_size[0],self.model_image_size[1]]),image_shape)

        font = ImageFont.truetype(font='model_data/simhei.ttf',size=np.floor(3e-2 * np.shape(image)[1] + 0.5).astype('int32'))

        thickness = (np.shape(image)[0] + np.shape(image)[1]) // self.model_image_size[0]

        for i, c in enumerate(top_label_indices):
            predicted_class = self.class_names[int(c)-1]
            score = top_conf[i]

            top, left, bottom, right = boxes[i]
            top = top - 5
            left = left - 5
            bottom = bottom + 5
            right = right + 5

            top = max(0, np.floor(top + 0.5).astype('int32'))
            left = max(0, np.floor(left + 0.5).astype('int32'))
            bottom = min(np.shape(image)[0], np.floor(bottom + 0.5).astype('int32'))
            right = min(np.shape(image)[1], np.floor(right + 0.5).astype('int32'))

            # 画框框
            label = '{} {:.2f}'.format(predicted_class, score)
            draw = ImageDraw.Draw(image)
            label_size = draw.textsize(label, font)
            label = label.encode('utf-8')
            print(label)
            
            if top - label_size[1] >= 0:
                text_origin = np.array([left, top - label_size[1]])
            else:
                text_origin = np.array([left, top + 1])

            for i in range(thickness):
                draw.rectangle(
                    [left + i, top + i, right - i, bottom - i],
                    outline=self.colors[int(c)-1])
            draw.rectangle(
                [tuple(text_origin), tuple(text_origin + label_size)],
                fill=self.colors[int(c)-1])
            draw.text(text_origin, str(label,'UTF-8'), fill=(0, 0, 0), font=font)
            del draw
        return image

首先获取图片大小,对于不同宽高比的图片,在图片上加上灰条防止图片失真。
对训练的结果进行解码,(网络训练的结果是对先验框的调整参数和框中物体所属的类别),这里的解码是指将输出结果转化成能够作用在先验框上的形式,从而得到最终的预测框。
调用函数detection_out,函数代码如下:

    def detection_out(self, predictions, background_label_id=0, keep_top_k=200,
                      confidence_threshold=0.5):
        # 网络预测的结果
        mbox_loc = predictions[:, :, :4]
        # 0.1,0.1,0.2,0.2
        variances = predictions[:, :, -4:]
        # 先验框
        mbox_priorbox = predictions[:, :, -8:-4]
        # 置信度
        mbox_conf = predictions[:, :, 4:-8]
        results = []
        # 对每一个特征层进行处理
        for i in range(len(mbox_loc)):
            results.append([])
            decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox[i], variances[i])

这里提取SSD网络的预测结果,还有常数variances、先验框和框中物体所属类别的置信度。
调用函数decode_boxes,该函数的作用就是利用预测结果和常数对先验框进行调整,返回调整以后的框decode_bbox



对每一个特征层进行decode,用物体所属类别的置信度进行筛选,利用非极大值抑制进行筛选,得到得分较高和重合程度较小的框,再利用置信度从高到底进行排序。

            for c in range(self.num_classes):
                if c == background_label_id:
                    continue
                c_confs = mbox_conf[i, :, c]
                c_confs_m = c_confs > confidence_threshold
                if len(c_confs[c_confs_m]) > 0:
                    boxes_to_process = decode_bbox[c_confs_m]
                    confs_to_process = c_confs[c_confs_m]
                    idx = tf.image.non_max_suppression(tf.cast(boxes_to_process,tf.float32), tf.cast(confs_to_process,tf.float32),
                                                self._top_k,
                                                iou_threshold=self._nms_thresh).numpy()
                    good_boxes = boxes_to_process[idx]
                    confs = confs_to_process[idx][:, None]
                    labels = c * np.ones((len(idx), 1))
                    c_pred = np.concatenate((labels, confs, good_boxes),
                                            axis=1)
                    results[-1].extend(c_pred)
            if len(results[-1]) > 0:
                results[-1] = np.array(results[-1])
                argsort = np.argsort(results[-1][:, 1])[::-1]
                results[-1] = results[-1][argsort]
                results[-1] = results[-1][:keep_top_k]
        return results

ssd.py中,利用correct_boxes去掉灰条,然后在图片上将最终的预测框画出来。


5、SSD网络的训练过程(编码过程)

train.py中是训练的代码:

if __name__ == "__main__":
    log_dir = "logs/"
    annotation_path = '2007_train.txt'
    
    NUM_CLASSES = 21# 待识别目标种类数+1
    input_shape = (300, 300, 3)
    priors = get_anchors()
    bbox_util = BBoxUtility(NUM_CLASSES, priors)

    # 0.1用于验证,0.9用于训练
    val_split = 0.1
    with open(annotation_path) as f:
        lines = f.readlines()
    np.random.seed(10101)
    np.random.shuffle(lines)
    np.random.seed(None)
    num_val = int(len(lines)*val_split)
    num_train = len(lines) - num_val

    model = SSD300(input_shape, num_classes=NUM_CLASSES)
    # 载入预训练好的权重
    model.load_weights('model_data/ssd_weights.h5', by_name=True, skip_mismatch=True)

    # 训练参数设置
    logging = TensorBoard(log_dir=log_dir)
    # 保存训练过程中的日志文件
    checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
        monitor='val_loss', save_weights_only=True, save_best_only=True, period=1)
    reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, verbose=1)
    early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=6, verbose=1)

    freeze_layer = 21
    for i in range(freeze_layer):
        model.layers[i].trainable = False

    if True:

        BATCH_SIZE = 8
        Lr = 5e-4
        Init_Epoch = 0
        Freeze_Epoch = 25
        gen = Generator(bbox_util, BATCH_SIZE, lines[:num_train], lines[num_train:],
                    (input_shape[0], input_shape[1]),NUM_CLASSES)

        model.compile(optimizer=Adam(lr=Lr),loss=MultiboxLoss(NUM_CLASSES, neg_pos_ratio=3.0).compute_loss)
        model.fit(gen.generate(True), 
                steps_per_epoch=num_train//BATCH_SIZE,
                validation_data=gen.generate(False),
                validation_steps=num_val//BATCH_SIZE,
                epochs=Freeze_Epoch, 
                initial_epoch=Init_Epoch,
                callbacks=[logging, checkpoint, reduce_lr, early_stopping])

    for i in range(freeze_layer):
        model.layers[i].trainable = True
    if True:

        BATCH_SIZE = 8
        Lr = 1e-4
        Freeze_Epoch = 25
        Epoch = 50
        gen = Generator(bbox_util, BATCH_SIZE, lines[:num_train], lines[num_train:],
                    (input_shape[0], input_shape[1]),NUM_CLASSES)

        model.compile(optimizer=Adam(lr=Lr),loss=MultiboxLoss(NUM_CLASSES, neg_pos_ratio=3.0).compute_loss)
        model.fit(gen.generate(True), 
                steps_per_epoch=num_train//BATCH_SIZE,
                validation_data=gen.generate(False),
                validation_steps=num_val//BATCH_SIZE,
                epochs=Epoch, 
                initial_epoch=Freeze_Epoch,
                callbacks=[logging, checkpoint, reduce_lr, early_stopping])

首先定义模型存放的位置,载入训练数据的.txt文件。
整体的训练过程是将10%数据用于验证,90%数据用于训练。
提取训练数据的过程之前将数据及打乱顺序。
载入训练好的权重、定义好保存训练日志文件的位置,设置学习率和早停。
利用迁移学习,使用已经训练好的权重,就需要进行两次训练,第一次是粗略的训练,学习率较大,然后进行比较精细的训练,学习率较小。


具体的训练过程在Generate类中,函数generate代码如下:

    def generate(self, train=True):
        while True:
            if train:
                # 打乱
                shuffle(self.train_lines)
                lines = self.train_lines
            else:
                shuffle(self.val_lines)
                lines = self.val_lines
            inputs = []
            targets = []
            for annotation_line in lines:  
                img,y=self.get_random_data(annotation_line,self.image_size[0:2])
                if len(y)!=0:
                    boxes = np.array(y[:,:4],dtype=np.float32)
                    boxes[:,0] = boxes[:,0]/self.image_size[1]
                    boxes[:,1] = boxes[:,1]/self.image_size[0]
                    boxes[:,2] = boxes[:,2]/self.image_size[1]
                    boxes[:,3] = boxes[:,3]/self.image_size[0]
                    one_hot_label = np.eye(self.num_classes)[np.array(y[:,4],np.int32)]
                    if ((boxes[:,3]-boxes[:,1])<=0).any() and ((boxes[:,2]-boxes[:,0])<=0).any():
                        continue
                    
                    y = np.concatenate([boxes,one_hot_label],axis=-1)

                y = self.bbox_util.assign_boxes(y)
                inputs.append(img)               
                targets.append(y)
                if len(targets) == self.batch_size:
                    tmp_inp = np.array(inputs)
                    tmp_targets = np.array(targets)
                    inputs = []
                    targets = []
                    yield preprocess_input(tmp_inp), tmp_targets

打乱图片顺序,读取图片信息,图片中物体的位置还有图片类别标签。
图片增加噪声,扩充数据集,提高模型的鲁棒性。
在训练的过程中,我们使用到loss,利用loss值对模型进行训练。我们需要将真实框进行编码得到的结果和预测结果进行比较得到loss值。
encode_boxes代码内容是对真实框进行编码。

    def encode_box(self, box, return_iou=True):
        iou = self.iou(box)
        encoded_box = np.zeros((self.num_priors, 4 + return_iou))

        # 找到每一个真实框,重合程度较高的先验框
        assign_mask = iou > self.overlap_threshold
        if not assign_mask.any():
            assign_mask[iou.argmax()] = True
        if return_iou:
            encoded_box[:, -1][assign_mask] = iou[assign_mask]
        
        # 找到对应的先验框
        assigned_priors = self.priors[assign_mask]
        # 逆向编码,将真实框转化为ssd预测结果的格式

        # 先计算真实框的中心与长宽
        box_center = 0.5 * (box[:2] + box[2:])
        box_wh = box[2:] - box[:2]
        # 再计算重合度较高的先验框的中心与长宽
        assigned_priors_center = 0.5 * (assigned_priors[:, :2] +
                                        assigned_priors[:, 2:4])
        assigned_priors_wh = (assigned_priors[:, 2:4] -
                              assigned_priors[:, :2])
        
        # 逆向求取ssd应该有的预测结果
        encoded_box[:, :2][assign_mask] = box_center - assigned_priors_center
        encoded_box[:, :2][assign_mask] /= assigned_priors_wh
        # 除以0.1
        encoded_box[:, :2][assign_mask] /= assigned_priors[:, -4:-2]

        encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_priors_wh)
        # 除以0.2
        encoded_box[:, 2:4][assign_mask] /= assigned_priors[:, -2:]
        return encoded_box.ravel()

计算真实框和所有先验框比较重合程度,可以通过调整这些筛选出的先验框得到真实框。
编码过程得到的结果是网络输出的预期的结果。

保留重合程度最大的先验框,也就是网络应有的预测结果。
将loss值存放在class MultiboxLoss(object):中,调用compute_loss函数网络预测结果的loss.


对正样本和负样本进行筛选,正样本和服样本不平衡的问题
求出正负样本加权平均的loss和框位置的loss值。


至此,SSD目标检测网络tensorflow框架的代码完成
感谢
https://www.bilibili.com/video/BV16K411H7Zb?p=10
https://github.com/bubbliiiing/ssd-tf2
https://blog.csdn.net/weixin_44791964/article/details/107289289

本文地址:https://blog.csdn.net/weixin_43227526/article/details/107602713

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网