天天看點

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台

  • 更新說明
  • 學習前言
  • 什麼是SSD目标檢測算法
  • 源碼下載下傳
  • SSD實作思路
    • 一、預測部分
      • 1、主幹網絡介紹
      • 2、從特征擷取預測結果
      • 3、預測結果的解碼
      • 4、在原圖上進行繪制
    • 二、訓練部分
      • 1、真實框的處理
        • a、找到真實框對應的先驗框
        • b、真實框的編碼
      • 2、利用處理完的真實框與對應圖檔的預測結果計算loss
  • 訓練自己的ssd模型

更新說明

有小夥伴聯系我說,我實作的mobilenet-ssd并不是原版的mobilenet-ssd,于是我去查了資料,發現還真不是,又重新制作了原版結構的mobilenet-ssd,主要是修改了特征層的shape,分别利用19x19,10x10,5x5,3x3,2x2,1x1的有效特征層進行分類預測與回歸預測。github位址如下:

https://github.com/bubbliiiing/Mobilenet-SSD-Essay

學習前言

一起來看看Mobilenet-SSD的keras實作吧,順便訓練一下自己的資料。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

什麼是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/mobilenet-ssd-keras

喜歡的可以點個star噢。

另外實作的原版結構的SSD的github位址如下:

https://github.com/bubbliiiing/Mobilenet-SSD-Essay

SSD實作思路

一、預測部分

1、主幹網絡介紹

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

上圖的SSD采用的主幹網絡是VGG網絡,我們需要将其替換成Mobilenet網絡。Mobilenet網絡的結構可以在這裡了解,https://blog.csdn.net/weixin_44791964/article/details/102819915

需要注意兩個部分:

1、Conv4-3是長寬壓縮三次的結果,是以我們取mobilenet長寬壓縮三次的特征層替代Conv4-3。

2、fc7是長寬壓縮四次的結果,是以我們取mobilenet長寬壓縮四次的特征層替代fc7。(在SSD中,其将VGG的Conv5的池化層的步長修改為1,是以本文也将mobilenet的Block5修改成了步長為1。)

後面的Conv6,Conv7,Conv8,Conv9不變。

import keras.backend as K
from keras.layers import Activation
from keras.layers import Conv2D
from keras.layers import Dense
from keras.layers import Flatten,Add,ZeroPadding2D
from keras.layers import GlobalAveragePooling2D,DepthwiseConv2D,BatchNormalization
from keras.layers import Input
from keras.layers import MaxPooling2D
from keras.layers import merge, concatenate
from keras.layers import Reshape
from keras.layers import ZeroPadding2D
from keras.models import Model

def _depthwise_conv_block(inputs, pointwise_conv_filters,
                          depth_multiplier=1, strides=(1, 1), block_id=1):

    x = DepthwiseConv2D((3, 3),
                        padding='same',
                        depth_multiplier=1,
                        strides=strides,
                        use_bias=False,
                        name='conv_dw_%d' % block_id)(inputs)

    x = BatchNormalization(name='conv_dw_%d_bn' % block_id)(x)
    x = Activation(relu6, name='conv_dw_%d_relu' % block_id)(x)

    x = Conv2D(pointwise_conv_filters, (1, 1),
               padding='same',
               use_bias=False,
               strides=(1, 1),
               name='conv_pw_%d' % block_id)(x)
    x = BatchNormalization(name='conv_pw_%d_bn' % block_id)(x)
    return Activation(relu6, name='conv_pw_%d_relu' % block_id)(x)

def relu6(x):
    return K.relu(x, max_value=6)


def mobilenet(input_tensor):
    #----------------------------主幹特征提取網絡開始---------------------------#
    # SSD結構,net字典
    net = {} 
    # Block 1
    x = input_tensor
    # 300,300,3 -> 150,150,64
    x = Conv2D(32, (3,3),
            padding='same',
            use_bias=False,
            strides=(2, 2),
            name='conv1')(input_tensor)
    x = BatchNormalization(name='conv1_bn')(x)
    x = Activation(relu6, name='conv1_relu')(x)
    x = _depthwise_conv_block(x, 64, 1, block_id=1)
    
    # 150,150,64 -> 75,75,128
    x = _depthwise_conv_block(x, 128, 1,
                              strides=(2, 2), block_id=2)
    x = _depthwise_conv_block(x, 128, 1, block_id=3)

    
    # Block 3
    # 75,75,128 -> 38,38,256
    x = _depthwise_conv_block(x, 256, 1,
                              strides=(2, 2), block_id=4)
    
    x = _depthwise_conv_block(x, 256, 1, block_id=5)
    net['conv4_3'] = x

    # Block 4
    # 38,38,256 -> 19,19,512
    x = _depthwise_conv_block(x, 512, 1,
                              strides=(2, 2), block_id=6)
    x = _depthwise_conv_block(x, 512, 1, block_id=7)
    x = _depthwise_conv_block(x, 512, 1, block_id=8)
    x = _depthwise_conv_block(x, 512, 1, block_id=9)
    x = _depthwise_conv_block(x, 512, 1, block_id=10)
    x = _depthwise_conv_block(x, 512, 1, block_id=11)

    # Block 5
    # 19,19,512 -> 19,19,1024
    x = _depthwise_conv_block(x, 1024, 1,
                              strides=(2, 2), block_id=12)
    x = _depthwise_conv_block(x, 1024, 1, block_id=13)
    net['fc7'] = x

    # 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、從特征擷取預測結果

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

由上圖可知,我們分别取:

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

共六個特征層進行下一步的處理。為了和普通特征層區分,我們稱之為有效特征層,來擷取預測結果。

對擷取到的每一個有效特征層,我們都需要對其做三個操作,分别是:

  • 一次num_priors x 4的卷積
  • 一次num_priors x num_classes的卷積
  • 計算對應的先驗框。

而num_priors指的是該特征層每一個特征點所擁有的先驗框數量。上述提到的六個特征層,每個特征層的每個特征點對應的先驗框數量分别為4、6、6、6、4、4。

上述操作分别對應的對象為:

  • num_priors x 4的卷積 用于預測 該特征層上 每一個網格點上 每一個先驗框的變化情況。**
  • num_priors x num_classes的卷積 用于預測 該特征層上 每一個網格點上 每一個預測對應的種類。
  • 每一個特征層的每一個特征點上對應的若幹個先驗框。

所有的特征層對應的預測結果的shape如下:

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

實作代碼為:

def SSD300(input_shape, num_classes=21):
    # 300,300,3
    input_tensor = Input(shape=input_shape)
    img_size = (input_shape[1], input_shape[0])

    # SSD結構,net字典
    net = VGG16(input_tensor)
    #-----------------------将提取到的主幹特征進行處理---------------------------#
    # 對conv4_3進行處理 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'])
    
    # 對fc7層進行處理 
    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'])

    # 對conv6_2進行處理
    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'])

    # 對conv7_2進行處理
    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'])

    # 對conv8_2進行處理
    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'])

    # 對conv9_2進行處理
    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'])

    # 将所有結果進行堆疊
    net['mbox_loc'] = concatenate([net['conv4_3_norm_mbox_loc_flat'],
                             net['fc7_mbox_loc_flat'],
                             net['conv6_2_mbox_loc_flat'],
                             net['conv7_2_mbox_loc_flat'],
                             net['conv8_2_mbox_loc_flat'],
                             net['conv9_2_mbox_loc_flat']],
                            axis=1, name='mbox_loc')
    net['mbox_conf'] = concatenate([net['conv4_3_norm_mbox_conf_flat'],
                              net['fc7_mbox_conf_flat'],
                              net['conv6_2_mbox_conf_flat'],
                              net['conv7_2_mbox_conf_flat'],
                              net['conv8_2_mbox_conf_flat'],
                              net['conv9_2_mbox_conf_flat']],
                             axis=1, name='mbox_conf')
    net['mbox_priorbox'] = concatenate([net['conv4_3_norm_mbox_priorbox'],
                                  net['fc7_mbox_priorbox'],
                                  net['conv6_2_mbox_priorbox'],
                                  net['conv7_2_mbox_priorbox'],
                                  net['conv8_2_mbox_priorbox'],
                                  net['conv9_2_mbox_priorbox']],
                                  axis=1, name='mbox_priorbox')

    if hasattr(net['mbox_loc'], '_keras_shape'):
        num_boxes = net['mbox_loc']._keras_shape[-1] // 4
    elif hasattr(net['mbox_loc'], 'int_shape'):
        num_boxes = K.int_shape(net['mbox_loc'])[-1] // 4
    # 8732,4
    net['mbox_loc'] = Reshape((num_boxes, 4),name='mbox_loc_final')(net['mbox_loc'])
    # 8732,21
    net['mbox_conf'] = Reshape((num_boxes, num_classes),name='mbox_conf_logits')(net['mbox_conf'])
    net['mbox_conf'] = Activation('softmax',name='mbox_conf_final')(net['mbox_conf'])

    net['predictions'] = concatenate([net['mbox_loc'],
                               net['mbox_conf'],
                               net['mbox_priorbox']],
                               axis=2, name='predictions')
    print(net['predictions'])
    model = Model(net['input'], net['predictions'])
    return model
           

3、預測結果的解碼

利用SSD的主幹網絡我們可以獲得六個有效特征層,分别是:

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

通過對每一個特征層的處理,我們可以獲得每個特征層對應的三個内容,分别是:

  • num_priors x 4的卷積 用于預測 該特征層上 每一個網格點上 每一個先驗框的變化情況。**
  • num_priors x num_classes的卷積 用于預測 該特征層上 每一個網格點上 每一個預測對應的種類。
  • 每一個特征層的每一個特征點上對應的若幹個先驗框。

我們利用 num_priors x 4的卷積 對 每一個有效特征層對應的先驗框 進行調整獲得 預測框。

在這裡我們簡單了解一下每個特征層到底做了什麼:

每一個有效特征層将整個圖檔分成與其長寬對應的網格,如conv4-3的特征層就是将整個圖像分成38x38個網格;然後從每個網格中心建立多個先驗框,對于conv4-3的特征層來說,它的每個特征點分别建立了4個先驗框;

是以,對于conv4-3整個特征層來講,整個圖檔被分成38x38個網格,每個網格中心對應4個先驗框,一共建立了38x38x4個,5776個先驗框。這些框密密麻麻的遍布在整個圖檔上。網絡的預測結果會對這些框進行調整獲得預測框。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

先驗框雖然可以代表一定的框的位置資訊與框的大小資訊,但是其是有限的,無法表示任意情況,是以還需要調整,ssd利用num_priors x 4的卷積的結果對先驗框進行調整。

num_priors x 4中的num_priors表示了這個網格點所包含的先驗框數量,其中的4表示了x_offset、y_offset、h和w的調整情況。

x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。

h和w代表了真實框的寬與高相對于先驗框的變化情況。

SSD解碼過程可以分為兩部分:

将每個網格的中心點加上它對應的x_offset和y_offset,加完後的結果就是預測框的中心;

利用h和w調整先驗框獲得預測框的寬和高。

此時我們獲得了預測框的中心和預測框的寬高,已經可以在圖檔上繪制預測框了。

想要獲得最終的預測結果,還要對每一個預測框進行得分排序與非極大抑制篩選。

這一部分基本上是所有目标檢測通用的部分。

1、取出每一類得分大于self.obj_threshold的框和得分。

2、利用框的位置和得分進行非極大抑制。

實作代碼如下:

def decode_boxes(self, mbox_loc, mbox_priorbox, variances):
    # 獲得先驗框的寬與高
    prior_width = mbox_priorbox[:, 2] - mbox_priorbox[:, 0]
    prior_height = mbox_priorbox[:, 3] - mbox_priorbox[:, 1]
    # 獲得先驗框的中心點
    prior_center_x = 0.5 * (mbox_priorbox[:, 2] + mbox_priorbox[:, 0])
    prior_center_y = 0.5 * (mbox_priorbox[:, 3] + mbox_priorbox[:, 1])

    # 真實框距離先驗框中心的xy軸偏移情況
    decode_bbox_center_x = mbox_loc[:, 0] * prior_width * variances[:, 0]
    decode_bbox_center_x += prior_center_x
    decode_bbox_center_y = mbox_loc[:, 1] * prior_height * variances[:, 1]
    decode_bbox_center_y += prior_center_y
    
    # 真實框的寬與高的求取
    decode_bbox_width = np.exp(mbox_loc[:, 2] * variances[:, 2])
    decode_bbox_width *= prior_width
    decode_bbox_height = np.exp(mbox_loc[:, 3] * variances[:, 3])
    decode_bbox_height *= prior_height

    # 擷取真實框的左上角與右下角
    decode_bbox_xmin = decode_bbox_center_x - 0.5 * decode_bbox_width
    decode_bbox_ymin = decode_bbox_center_y - 0.5 * decode_bbox_height
    decode_bbox_xmax = decode_bbox_center_x + 0.5 * decode_bbox_width
    decode_bbox_ymax = decode_bbox_center_y + 0.5 * decode_bbox_height

    # 真實框的左上角與右下角進行堆疊
    decode_bbox = np.concatenate((decode_bbox_xmin[:, None],
                                    decode_bbox_ymin[:, None],
                                    decode_bbox_xmax[:, None],
                                    decode_bbox_ymax[:, None]), axis=-1)
    # 防止超出0與1
    decode_bbox = np.minimum(np.maximum(decode_bbox, 0.0), 1.0)
    return decode_bbox

def detection_out(self, predictions, background_label_id=0, keep_top_k=200, confidence_threshold=0.5):
    #---------------------------------------------------#
    #   :4是回歸預測結果
    #---------------------------------------------------#
    mbox_loc = predictions[:, :, :4]
    #---------------------------------------------------#
    #   獲得種類的置信度
    #---------------------------------------------------#
    mbox_conf = predictions[:, :, 4:-8]
    #---------------------------------------------------#
    #   獲得網絡的先驗框
    #---------------------------------------------------#
    mbox_priorbox = predictions[:, :, -8:-4]
    #---------------------------------------------------#
    #   variances是一個改變數量級的參數。
    #   所有variances全去了也可以訓練,有了效果更好。
    #---------------------------------------------------#
    variances = predictions[:, :, -4:]

    results = []

    # 對每一張圖檔進行處理,由于在predict.py的時候,我們隻輸入一張圖檔,是以for i in range(len(mbox_loc))隻進行一次
    for i in range(len(mbox_loc)):
        results.append([])
        #--------------------------------#
        #   利用回歸結果對先驗框進行解碼
        #--------------------------------#
        decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox[i],  variances[i])

        for c in range(self.num_classes):
            if c == background_label_id:
                continue
            #--------------------------------#
            #   取出屬于該類的所有框的置信度
            #   判斷是否大于門限
            #--------------------------------#
            c_confs = mbox_conf[i, :, c]
            c_confs_m = c_confs > confidence_threshold
            if len(c_confs[c_confs_m]) > 0:
                # 取出得分高于confidence_threshold的框
                boxes_to_process = decode_bbox[c_confs_m]
                confs_to_process = c_confs[c_confs_m]
                # 進行iou的非極大抑制
                feed_dict = {self.boxes: boxes_to_process,
                                self.scores: confs_to_process}
                idx = self.sess.run(self.nms, feed_dict=feed_dict)
                # 取出在非極大抑制中效果較好的内容
                good_boxes = boxes_to_process[idx]
                confs = confs_to_process[idx][:, None]
                # 将label、置信度、框的位置進行堆疊。
                labels = c * np.ones((len(idx), 1))
                c_pred = np.concatenate((labels, confs, good_boxes),
                                        axis=1)
                # 添加進result裡
                results[-1].extend(c_pred)

        if len(results[-1]) > 0:
            # 按照置信度進行排序
            results[-1] = np.array(results[-1])
            argsort = np.argsort(results[-1][:, 1])[::-1]
            results[-1] = results[-1][argsort]
            # 選出置信度最大的keep_top_k個
            results[-1] = results[-1][:keep_top_k]
    return results
           

4、在原圖上進行繪制

通過第三步,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪制在圖檔上,就可以獲得結果了。

二、訓練部分

1、真實框的處理

真實框的處理可以分為兩個部分,分别是:

  • 找到真實框對應的先驗框;
  • 對真實框進行編碼。

a、找到真實框對應的先驗框

在這一步中,我們需要找到真實框所對應的先驗框,代表這個真實框由某個先驗框進行預測。

我們首先需要将每一個的真實框和所有的先驗框進行一個iou計算,這一步做的工作是計算每一個真實框和所有的先驗框的重合程度。

在獲得每一個真實框和所有的先驗框的重合程度之後,選出和每一個真實框重合程度大于一定門限的先驗框。代表這個真實框由這些先驗框負責預測。

由于一個先驗框隻能負責一個真實框的預測,是以如果某個先驗框和多個真實框的重合度較大,那麼這個先驗框隻負責與其iou最大的真實框的預測。

在這一步後,我們可以找到每一個先驗框所負責預測的真實框,在下一步中,我們需要根據這些真實框和先驗框獲得網絡的預測結果。

實作代碼如下:

def assign_boxes(self, boxes):
    #---------------------------------------------------#
    #   assignment分為3個部分
    #   :4      的内容為網絡應該有的回歸預測結果
    #   4:-8    的内容為先驗框所對應的種類,預設為背景
    #   -8      的内容為目前先驗框是否包含目标
    #   -7:     無意義
    #---------------------------------------------------#
    assignment = np.zeros((self.num_priors, 4 + self.num_classes + 8))
    assignment[:, 4] = 1.0
    if len(boxes) == 0:
        return assignment

    # 對每一個真實框都進行iou計算
    encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
    #---------------------------------------------------#
    #   在reshape後,獲得的encoded_boxes的shape為:
    #   [num_true_box, num_priors, 4+1]
    #   4是編碼後的結果,1為iou
    #---------------------------------------------------#
    encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
    
    #---------------------------------------------------#
    #   [num_priors]求取每一個先驗框重合度最大的真實框
    #---------------------------------------------------#
    best_iou = encoded_boxes[:, :, -1].max(axis=0)
    best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
    best_iou_mask = best_iou > 0
    best_iou_idx = best_iou_idx[best_iou_mask]
    
    #---------------------------------------------------#
    #   計算一共有多少先驗框滿足需求
    #---------------------------------------------------#
    assign_num = len(best_iou_idx)

    # 将編碼後的真實框取出
    encoded_boxes = encoded_boxes[:, best_iou_mask, :]
    assignment[:, :4][best_iou_mask] = encoded_boxes[best_iou_idx,np.arange(assign_num),:4]
    #----------------------------------------------------------#
    #   4代表為背景的機率,設定為0,因為這些先驗框有對應的物體
    #----------------------------------------------------------#
    assignment[:, 4][best_iou_mask] = 0
    assignment[:, 5:-8][best_iou_mask] = boxes[best_iou_idx, 4:]
    #----------------------------------------------------------#
    #   -8表示先驗框是否有對應的物體
    #----------------------------------------------------------#
    assignment[:, -8][best_iou_mask] = 1
    # 通過assign_boxes我們就獲得了,輸入進來的這張圖檔,應該有的預測結果是什麼樣子的
    return assignment
           

b、真實框的編碼

利用SSD的主幹網絡我們可以獲得六個有效特征層,分别是:

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

通過對每一個特征層的處理,我們可以獲得每個特征層對應的三個内容,分别是:

  • num_priors x 4的卷積 用于預測 該特征層上 每一個網格點上 每一個先驗框的變化情況。**
  • num_priors x num_classes的卷積 用于預測 該特征層上 每一個網格點上 每一個預測對應的種類。
  • 每一個特征層的每一個特征點上對應的若幹個先驗框。

是以,我們直接利用ssd網絡預測到的結果,并不是預測框在圖檔上的真實位置,需要解碼才能得到真實位置。

是以在訓練的時候,如果我們需要計算loss函數,這個loss函數是相對于ssd網絡的預測結果的。是以我們需要對真實框的資訊進行處理,使得它的結構和預測結果的格式是一樣的,這樣的一個過程我們稱之為編碼(encode)。用一句話概括編碼的過程就是将真實框的位置資訊格式轉化為ssd預測結果的格式資訊。

也就是,我們需要找到 每一張用于訓練的圖檔的每一個真實框對應的先驗框,并求出如果想要得到這樣一個真實框,我們的預測結果應該是怎麼樣的。

從預測結果獲得真實框的過程被稱作解碼,而從真實框獲得預測結果的過程就是編碼的過程。是以我們隻需要将解碼過程逆過來就是編碼過程了。

是以我們可以利用真實框和先驗框進行編碼,獲得該特征點對應的該先驗框應該有的預測結果。預測結果分為兩個,分别是:

  • num_priors x 4的卷積
  • num_priors x num_classes的卷積

前者對應先驗框的調整參數,後者對應先驗框的種類,這個都可以通過先驗框對應的真實框獲得。

實作代碼如下:

def encode_box(self, box, return_iou=True):
    # 計算目前真實框和先驗框的重合情況
    iou = self.iou(box)
    encoded_box = np.zeros((self.num_priors, 4 + return_iou))
    
    # 找到每一個真實框,重合程度較高的先驗框
    assign_mask = iou > self.overlap_threshold

    # 如果沒有一個先驗框重合度大于self.overlap_threshold
    # 則選擇重合度最大的為正樣本
    if not assign_mask.any():
        assign_mask[iou.argmax()] = True
    
    # 利用iou進行指派 
    if return_iou:
        encoded_box[:, -1][assign_mask] = iou[assign_mask]
    
    # 找到對應的先驗框
    assigned_priors = self.priors[assign_mask]

    #---------------------------------------------#
    #   逆向編碼,将真實框轉化為ssd預測結果的格式
    #   先計算真實框的中心與長寬
    #---------------------------------------------#
    box_center  = 0.5 * (box[:2] + box[2:])
    box_wh      = box[2:] - box[:2]
    #---------------------------------------------#
    #   再計算重合度較高的先驗框的中心與長寬
    #---------------------------------------------#
    assigned_priors_center = 0.5 * (assigned_priors[:, :2] +
                                    assigned_priors[:, 2:4])
    assigned_priors_wh = (assigned_priors[:, 2:4] -
                            assigned_priors[:, :2])
    
    #------------------------------------------------#
    #   逆向求取ssd應該有的預測結果
    #   先求取中心的預測結果,再求取寬高的預測結果
    #   存在改變數量級的參數,預設為[0.1,0.1,0.2,0.2]
    #------------------------------------------------#
    encoded_box[:, :2][assign_mask] = box_center - assigned_priors_center
    encoded_box[:, :2][assign_mask] /= assigned_priors_wh
    encoded_box[:, :2][assign_mask] /= assigned_priors[:, -4:-2]

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

2、利用處理完的真實框與對應圖檔的預測結果計算loss

loss的計算分為三個部分:

1、擷取所有正标簽的框的預測結果的回歸loss。

2、擷取所有正标簽的種類的預測結果的交叉熵loss。

3、擷取一定負标簽的種類的預測結果的交叉熵loss。

由于在ssd的訓練過程中,正負樣本極其不平衡,即 存在對應真實框的先驗框可能隻有若幹個,但是不存在對應真實框的負樣本卻有幾千個,這就會導緻負樣本的loss值極大,是以我們可以考慮減少負樣本的選取,對于ssd的訓練來講,常見的情況是取三倍正樣本數量的負樣本用于訓練。這個三倍呢,也可以修改,調整成自己喜歡的數字。

實作代碼如下:

class MultiboxLoss(object):
    def __init__(self, num_classes, alpha=1.0, neg_pos_ratio=3.0,
                 background_label_id=0, negatives_for_hard=100.0):
        self.num_classes = num_classes
        self.alpha = alpha
        self.neg_pos_ratio = neg_pos_ratio
        if background_label_id != 0:
            raise Exception('Only 0 as background label id is supported')
        self.background_label_id = background_label_id
        self.negatives_for_hard = negatives_for_hard

    def _l1_smooth_loss(self, y_true, y_pred):
        abs_loss = tf.abs(y_true - y_pred)
        sq_loss = 0.5 * (y_true - y_pred)**2
        l1_loss = tf.where(tf.less(abs_loss, 1.0), sq_loss, abs_loss - 0.5)
        return tf.reduce_sum(l1_loss, -1)

    def _softmax_loss(self, y_true, y_pred):
        y_pred = tf.maximum(y_pred, 1e-7)
        softmax_loss = -tf.reduce_sum(y_true * tf.log(y_pred),
                                      axis=-1)
        return softmax_loss

    def compute_loss(self, y_true, y_pred):
        batch_size = tf.shape(y_true)[0]
        num_boxes = tf.to_float(tf.shape(y_true)[1])

        # --------------------------------------------- #
        #   分類的loss
        #   batch_size,8732,21 -> batch_size,8732
        # --------------------------------------------- #
        conf_loss = self._softmax_loss(y_true[:, :, 4:-8],
                                       y_pred[:, :, 4:-8])
        # --------------------------------------------- #
        #   框的位置的loss
        #   batch_size,8732,4 -> batch_size,8732
        # --------------------------------------------- #
        loc_loss = self._l1_smooth_loss(y_true[:, :, :4],
                                        y_pred[:, :, :4])

        # --------------------------------------------- #
        #   擷取所有的正标簽的loss
        # --------------------------------------------- #
        pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -8],
                                     axis=1)
        pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -8],
                                      axis=1)

        # --------------------------------------------- #
        #   每一張圖的正樣本的個數
        #   batch_size,
        # --------------------------------------------- #
        num_pos = tf.reduce_sum(y_true[:, :, -8], axis=-1)

        # --------------------------------------------- #
        #   每一張圖的負樣本的個數
        #   batch_size,
        # --------------------------------------------- #
        num_neg = tf.minimum(self.neg_pos_ratio * num_pos, num_boxes - num_pos)
        # 找到了哪些值是大于0的
        pos_num_neg_mask = tf.greater(num_neg, 0)
        # --------------------------------------------- #
        #   如果所有的圖,正樣本的數量均為0
        #   那麼則預設選取100個先驗框作為負樣本
        # --------------------------------------------- #
        has_min = tf.to_float(tf.reduce_any(pos_num_neg_mask))
        num_neg = tf.concat(axis=0, values=[num_neg, [(1 - has_min) * self.negatives_for_hard]])
        
        # --------------------------------------------- #
        #   從這裡往後,與視訊中看到的代碼有些許不同。
        #   由于以前的負樣本選取方式存在一些問題,
        #   我對該部分代碼進行重構。
        #   求整個batch應該的負樣本數量總和
        # --------------------------------------------- #
        num_neg_batch = tf.reduce_sum(tf.boolean_mask(num_neg, tf.greater(num_neg, 0)))
        num_neg_batch = tf.to_int32(num_neg_batch)

        # --------------------------------------------- #
        #   對預測結果進行判斷,如果該先驗框沒有包含物體
        #   那麼它的不屬于背景的預測機率過大的話
        #   就是難分類樣本
        # --------------------------------------------- #
        confs_start = 4 + self.background_label_id + 1
        confs_end = confs_start + self.num_classes - 1

        # --------------------------------------------- #
        #   batch_size,8732
        #   把不是背景的機率求和,求和後的機率越大
        #   代表越難分類。
        # --------------------------------------------- #
        max_confs = tf.reduce_sum(y_pred[:, :, confs_start:confs_end], axis=2)

        # --------------------------------------------------- #
        #   隻有沒有包含物體的先驗框才得到保留
        #   我們在整個batch裡面選取最難分類的num_neg_batch個
        #   先驗框作為負樣本。
        # --------------------------------------------------- #
        max_confs = tf.reshape(max_confs * (1 - y_true[:, :, -8]), [-1])
        _, indices = tf.nn.top_k(max_confs, k=num_neg_batch)

        neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]), indices)

        # 進行歸一化
        num_pos     = tf.where(tf.not_equal(num_pos, 0), num_pos, tf.ones_like(num_pos))
        total_loss  = tf.reduce_sum(pos_conf_loss) + tf.reduce_sum(neg_conf_loss) + tf.reduce_sum(self.alpha * pos_loc_loss)
        total_loss /= tf.reduce_sum(num_pos)
        return total_loss
           

訓練自己的ssd模型

ssd整體的檔案夾構架如下:

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

本文使用VOC格式進行訓練。

訓練前将标簽檔案放在VOCdevkit檔案夾下的VOC2007檔案夾下的Annotation中。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

訓練前将圖檔檔案放在VOCdevkit檔案夾下的VOC2007檔案夾下的JPEGImages中。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

在訓練前利用voc2ssd.py檔案生成對應的txt。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

再運作根目錄下的voc_annotation.py,運作前需要将classes改成你自己的classes。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

就會生成對應的2007_train.txt,每一行對應其圖檔位置及其真實框的位置。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

在訓練前需要修改model_data裡面的voc_classes.txt檔案,需要将classes改成你自己的classes。這裡的classes的順序需要和voc_annotation.py中對應。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

在train.py裡面修改對應的NUM_CLASSES,修改成所需要區分的類的個數+1。運作train.py即可開始訓練。

睿智的目标檢測24——Keras搭建Mobilenet-SSD目标檢測平台更新說明學習前言什麼是SSD目标檢測算法源碼下載下傳SSD實作思路訓練自己的ssd模型

繼續閱讀