睿智的目標檢測16——Keras搭建SSD目標檢測平臺

學習前言

一起來看看SSD的keras實現吧,順便訓練一下自己的數據。
在這裏插入圖片描述

什麼是SSD目標檢測算法

SSD是一種非常優秀的one-stage目標檢測方法,one-stage算法就是目標檢測和分類是同時完成的,其主要思路是利用CNN提取特徵後,均勻地在圖片的不同位置進行密集抽樣,抽樣時可以採用不同尺度和長寬比,物體分類與預測框的迴歸同時進行,整個過程只需要一步,所以其優勢是速度快
但是均勻的密集採樣的一個重要缺點是訓練比較困難,這主要是因爲正樣本與負樣本(背景)極其不均衡(參見Focal Loss),導致模型準確度稍低。
SSD的英文全名是Single Shot MultiBox Detector,Single shot說明SSD算法屬於one-stage方法,MultiBox說明SSD算法基於多框預測。

源碼下載

https://github.com/bubbliiiing/ssd-keras
喜歡的可以點個star噢。

SSD實現思路

一、預測部分

1、主幹網絡介紹

在這裏插入圖片描述
SSD採用的主幹網絡是VGG網絡,關於VGG的介紹大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,這裏的VGG網絡相比普通的VGG網絡有一定的修改,主要修改的地方就是:
1、將VGG16的FC6和FC7層轉化爲卷積層。
2、去掉所有的Dropout層和FC8層;
3、新增了Conv6、Conv7、Conv8、Conv9。

在這裏插入圖片描述
如圖所示,輸入的圖片經過了改進的VGG網絡(Conv1->fc7)和幾個另加的卷積層(Conv6->Conv9),進行特徵提取:
a、輸入一張圖片後,被resize到300x300的shape

b、conv1,經過兩次[3,3]卷積網絡,輸出的特徵層爲64,輸出爲(300,300,64),再2X2最大池化,輸出net爲(150,150,64)。

c、conv2,經過兩次[3,3]卷積網絡,輸出的特徵層爲128,輸出net爲(150,150,128),再2X2最大池化,輸出net爲(75,75,128)。

d、conv3,經過三次[3,3]卷積網絡,輸出的特徵層爲256,輸出net爲(75,75,256),再2X2最大池化,輸出net爲(38,38,256)。

e、conv4,經過三次[3,3]卷積網絡,輸出的特徵層爲512,輸出net爲(38,38,512),再2X2最大池化,輸出net爲(19,19,512)。

f、conv5,經過三次[3,3]卷積網絡,輸出的特徵層爲512,輸出net爲(19,19,512),再2X2最大池化,輸出net爲(19,19,512)。

g、利用卷積代替全連接層,進行了兩次[3,3]卷積網絡,輸出的特徵層爲1024,因此輸出的net爲(19,19,1024)。(從這裏往前都是VGG的結構)

h、conv6,經過一次[1,1]卷積網絡,調整通道數,一次步長爲2的[3,3]卷積網絡,輸出的特徵層爲512,因此輸出的net爲(10,10,512)。

i、conv7,經過一次[1,1]卷積網絡,調整通道數,一次步長爲2的[3,3]卷積網絡,輸出的特徵層爲256,因此輸出的net爲(5,5,256)。

j、conv8,經過一次[1,1]卷積網絡,調整通道數,一次padding爲valid的[3,3]卷積網絡,輸出的特徵層爲256,因此輸出的net爲(3,3,256)。

k、conv9,經過一次[1,1]卷積網絡,調整通道數,一次padding爲valid的[3,3]卷積網絡,輸出的特徵層爲256,因此輸出的net爲(1,1,256)。

實現代碼:

import keras.backend as K
from keras.layers import Activation
from keras.layers import Conv2D
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import GlobalAveragePooling2D
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 VGG16(input_tensor):
    #----------------------------主幹特徵提取網絡開始---------------------------#
    # SSD結構,net字典
    net = {} 
    # Block 1
    net['input'] = input_tensor
    # 300,300,3 -> 150,150,64
    net['conv1_1'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_1')(net['input'])
    net['conv1_2'] = Conv2D(64, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv1_2')(net['conv1_1'])
    net['pool1'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool1')(net['conv1_2'])

    
    # Block 2
    # 150,150,64 -> 75,75,128
    net['conv2_1'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_1')(net['pool1'])
    net['conv2_2'] = Conv2D(128, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv2_2')(net['conv2_1'])
    net['pool2'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool2')(net['conv2_2'])
    # Block 3
    # 75,75,128 -> 38,38,256
    net['conv3_1'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_1')(net['pool2'])
    net['conv3_2'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_2')(net['conv3_1'])
    net['conv3_3'] = Conv2D(256, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv3_3')(net['conv3_2'])
    net['pool3'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool3')(net['conv3_3'])
    # Block 4
    # 38,38,256 -> 19,19,512
    net['conv4_1'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_1')(net['pool3'])
    net['conv4_2'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_2')(net['conv4_1'])
    net['conv4_3'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv4_3')(net['conv4_2'])
    net['pool4'] = MaxPooling2D((2, 2), strides=(2, 2), padding='same',
                                name='pool4')(net['conv4_3'])
    # Block 5
    # 19,19,512 -> 19,19,512
    net['conv5_1'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_1')(net['pool4'])
    net['conv5_2'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_2')(net['conv5_1'])
    net['conv5_3'] = Conv2D(512, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_3')(net['conv5_2'])
    net['pool5'] = MaxPooling2D((3, 3), strides=(1, 1), padding='same',
                                name='pool5')(net['conv5_3'])
    # FC6
    # 19,19,512 -> 19,19,1024
    net['fc6'] = Conv2D(1024, kernel_size=(3,3), dilation_rate=(6, 6),
                                     activation='relu', padding='same',
                                     name='fc6')(net['pool5'])

    # x = Dropout(0.5, name='drop6')(x)
    # FC7
    # 19,19,1024 -> 19,19,1024
    net['fc7'] = Conv2D(1024, kernel_size=(1,1), activation='relu',
                               padding='same', name='fc7')(net['fc6'])

    # x = Dropout(0.5, name='drop7')(x)
    # Block 6
    # 19,19,512 -> 10,10,512
    net['conv6_1'] = Conv2D(256, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv6_1')(net['fc7'])
    net['conv6_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv6_padding')(net['conv6_1'])
    net['conv6_2'] = Conv2D(512, kernel_size=(3,3), strides=(2, 2),
                                   activation='relu',
                                   name='conv6_2')(net['conv6_2'])

    # Block 7
    # 10,10,512 -> 5,5,256
    net['conv7_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same', 
                                   name='conv7_1')(net['conv6_2'])
    net['conv7_2'] = ZeroPadding2D(padding=((1, 1), (1, 1)), name='conv7_padding')(net['conv7_1'])
    net['conv7_2'] = Conv2D(256, kernel_size=(3,3), strides=(2, 2),
                                   activation='relu', padding='valid',
                                   name='conv7_2')(net['conv7_2'])
    # Block 8
    # 5,5,256 -> 3,3,256
    net['conv8_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv8_1')(net['conv7_2'])
    net['conv8_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                   activation='relu', padding='valid',
                                   name='conv8_2')(net['conv8_1'])

    # Block 9
    # 3,3,256 -> 1,1,256
    net['conv9_1'] = Conv2D(128, kernel_size=(1,1), activation='relu',
                                   padding='same',
                                   name='conv9_1')(net['conv8_2'])
    net['conv9_2'] = Conv2D(256, kernel_size=(3,3), strides=(1, 1),
                                   activation='relu', padding='valid',
                                   name='conv9_2')(net['conv9_1'])
    #----------------------------主幹特徵提取網絡結束---------------------------#
    return net

2、從特徵獲取預測結果

在這裏插入圖片描述
由上圖我們可以知道,我們分別取conv4的第三次卷積的特徵、fc7的特徵、conv6的第二次卷積的特徵、conv7的第二次卷積的特徵、conv8的第二次卷積的特徵、conv9的第二次卷積的特徵,爲了和普通特徵層區分,我們稱之爲有效特徵層,來獲取預測結果。

對獲取到的每一個有效特徵層,我們分別對其進行一次num_priors x 4的卷積、一次num_priors x num_classes的卷積、並需要計算每一個有效特徵層對應的先驗框。而num_priors指的是該特徵層所擁有的先驗框數量。

其中:
num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。(爲什麼說是變化情況呢,這是因爲ssd的預測結果需要結合先驗框獲得預測框,預測結果就是先驗框的變化情況。)

num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。

每一個有效特徵層對應的先驗框對應着該特徵層上 每一個網格點上 預先設定好的三個框。

所有的特徵層對應的預測結果的shape如下:
在這裏插入圖片描述
實現代碼爲:

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、預測結果的解碼

我們通過對每一個特徵層的處理,可以獲得三個內容,分別是:

num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。**

num_priors x num_classes的卷積 用於預測 該特徵層上 每一個網格點上 每一個預測框對應的種類。

每一個有效特徵層對應的先驗框對應着該特徵層上 每一個網格點上 預先設定好的三個框。

我們利用 num_priors x 4的卷積每一個有效特徵層對應的先驗框 獲得框的真實位置。

每一個有效特徵層對應的先驗框就是,如圖所示的作用:
每一個有效特徵層將整個圖片分成與其長寬對應的網格,如conv4-3的特徵層就是將整個圖像分成38x38個網格;然後從每個網格中心建立多個先驗框,如conv4-3的特徵層就是建立了4個先驗框;對於conv4-3的特徵層來講,整個圖片被分成38x38個網格,每個網格中心對應4個先驗框,一共包含了,38x38x4個,5776個先驗框。
在這裏插入圖片描述
先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調整,ssd利用num_priors x 4的卷積的結果對先驗框進行調整。

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

x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。
h和w代表了真實框的寬與高相對於先驗框的變化情況。

SSD解碼過程就是將每個網格的中心點加上它對應的x_offset和y_offset,加完後的結果就是預測框的中心,然後再利用 先驗框和h、w結合 計算出預測框的長和寬。這樣就能得到整個預測框的位置了。

當然得到最終的預測結構後還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
1、取出每一類得分大於self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。

實現代碼如下:

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

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

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

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

def detection_out(self, predictions, background_label_id=0, keep_top_k=200,
                    confidence_threshold=0.5):
    # 網絡預測的結果
    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])

        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、真實框的處理

從預測部分我們知道,每個特徵層的預測結果,num_priors x 4的卷積 用於預測 該特徵層上 每一個網格點上 每一個先驗框的變化情況。

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

而在訓練的時候,我們需要計算loss函數,這個loss函數是相對於ssd網絡的預測結果的。我們需要把圖片輸入到當前的ssd網絡中,得到預測結果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉化爲ssd預測結果的格式信息

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

從預測結果獲得真實框的過程被稱作解碼,而從真實框獲得預測結果的過程就是編碼的過程。

因此我們只需要將解碼過程逆過來就是編碼過程了。

實現代碼如下:

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()

利用上述代碼我們可以獲得,真實框對應的所有的iou較大先驗框,並計算了真實框對應的所有iou較大的先驗框應該有的預測結果。

在訓練的時候我們只需要選擇iou最大的先驗框就行了,這個iou最大的先驗框就是我們用來預測這個真實框所用的先驗框。

因此我們還要經過一次篩選,將上述代碼獲得的真實框對應的所有的iou較大先驗框的預測結果中,iou最大的那個篩選出來。

通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的。

實現代碼如下:

def assign_boxes(self, boxes):
    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])
    # 每一個真實框的編碼後的值,和iou
    encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)
    
    # 取重合程度最大的先驗框,並且獲取這個先驗框的index
    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:]
    assignment[:, -8][best_iou_mask] = 1
    # 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的
    return assignment

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

loss的計算分爲三個部分:
1、獲取所有正標籤的框的預測結果的迴歸loss。
2、獲取所有正標籤的種類的預測結果的交叉熵loss。
3、獲取一定負標籤的種類的預測結果的交叉熵loss。

由於在ssd的訓練過程中,正負樣本極其不平衡,即 存在對應真實框的先驗框可能只有2~3個,但是不存在對應真實框的負樣本卻有幾千個,這就會導致負樣本的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(tf.minimum(y_pred, 1 - 1e-15), 1e-15)
        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
        # 分類的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的個數
        num_pos = tf.reduce_sum(y_true[:, :, -8], axis=-1)
        # 每一張圖的pos_loc_loss
        pos_loc_loss = tf.reduce_sum(loc_loss * y_true[:, :, -8],
                                     axis=1)
        # 每一張圖的pos_conf_loss
        pos_conf_loss = tf.reduce_sum(conf_loss * y_true[:, :, -8],
                                      axis=1)

        # 獲取一定的負樣本
        num_neg = tf.minimum(self.neg_pos_ratio * num_pos,
                             num_boxes - num_pos)

        # 找到了哪些值是大於0的
        pos_num_neg_mask = tf.greater(num_neg, 0)
        # 獲得一個1.0
        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]])
        # 求平均每個圖片要取多少個負樣本
        num_neg_batch = tf.reduce_mean(tf.boolean_mask(num_neg,
                                                      tf.greater(num_neg, 0)))
        num_neg_batch = tf.to_int32(num_neg_batch)

        # conf的起始
        confs_start = 4 + self.background_label_id + 1
        # conf的結束
        confs_end = confs_start + self.num_classes - 1

        # 找到實際上在該位置不應該有預測結果的框,求他們最大的置信度。
        max_confs = tf.reduce_max(y_pred[:, :, confs_start:confs_end],
                                  axis=2)
        
        # 取top_k個置信度,作爲負樣本
        _, indices = tf.nn.top_k(max_confs * (1 - y_true[:, :, -8]),
                                 k=num_neg_batch)

        # 找到其在1維上的索引
        batch_idx = tf.expand_dims(tf.range(0, batch_size), 1)
        batch_idx = tf.tile(batch_idx, (1, num_neg_batch))
        full_indices = (tf.reshape(batch_idx, [-1]) * tf.to_int32(num_boxes) +
                        tf.reshape(indices, [-1]))
        

        neg_conf_loss = tf.gather(tf.reshape(conf_loss, [-1]),
                                  full_indices)
        neg_conf_loss = tf.reshape(neg_conf_loss,
                                   [batch_size, num_neg_batch])
        neg_conf_loss = tf.reduce_sum(neg_conf_loss, axis=1)

        # 求loss總和
        total_loss = K.sum(pos_conf_loss + neg_conf_loss)/K.cast(batch_size,K.dtype(pos_conf_loss))

        total_loss +=  K.sum(self.alpha * pos_loc_loss)/K.cast(batch_size,K.dtype(pos_loc_loss))
        return total_loss

訓練自己的ssd模型

ssd整體的文件夾構架如下:
在這裏插入圖片描述
本文使用VOC格式進行訓練。
訓練前將標籤文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
在這裏插入圖片描述
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在這裏插入圖片描述
在訓練前利用voc2ssd.py文件生成對應的txt。
在這裏插入圖片描述
再運行根目錄下的voc_annotation.py,運行前需要將classes改成你自己的classes。

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

在這裏插入圖片描述
就會生成對應的2007_train.txt,每一行對應其圖片位置及其真實框的位置。
在這裏插入圖片描述
在訓練前需要修改model_data裏面的voc_classes.txt文件,需要將classes改成你自己的classes。
在這裏插入圖片描述
運行train.py即可開始訓練。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章