睿智的目標檢測25—Keras搭建M2Det目標檢測平臺

學習前言

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

什麼是M2det目標檢測算法

常見的特徵提取方法如圖所示有SSD形,FPN形,STDN形:

SSD型:使用了主幹網絡的最後兩層,再加上4個使用stride=2卷積的下采樣層構成;
FPN型:也稱爲U型網絡,經過上採樣操作,然後對應融合相同的scale;
STDN型:基於DenseNet的最後一個dense block,通過池化和scale-transfer操作來構建;

這三者有一定的缺點:
一是均基於分類網絡作爲主幹提取,對目標檢測任務而言特徵表示可能不夠;二是每個feature map僅由主幹網絡的single level給出,不夠全面

M2det論文新提出MLFPN型,整體思想是Multi-level&Multi-scale。是一種更加有效的適合於檢測的特徵金字塔結構。在這裏插入圖片描述

源碼下載

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

M2det實現思路

一、預測部分

1、主幹網絡介紹

在這裏插入圖片描述
M2det採用可以採用VGG和ResNet101作爲主幹特徵提取網絡,上圖的backbone network指的就是VGG和Resnet101,本文以VGG爲例介紹。

M2DET採用的主幹網絡是VGG網絡,關於VGG的介紹大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878

在m2det中,我們去掉了全部的全連接層,只保留了卷積層和最大池化層,即Conv1到Conv5。

1、一張原始圖片被resize到(320,320,3)。
2、conv1兩次[3,3]卷積網絡,輸出的特徵層爲64,輸出爲(320,320,64),再2X2最大池化,輸出net爲(160,160,64)。
3、conv2兩次[3,3]卷積網絡,輸出的特徵層爲128,輸出net爲(160,160,128),再2X2最大池化,輸出net爲(80,80,128)。
4、conv3三次[3,3]卷積網絡,輸出的特徵層爲256,輸出net爲(80,80,256),再2X2最大池化,輸出net爲(40,40,256)。
5、conv4三次[3,3]卷積網絡,輸出的特徵層爲512,輸出net爲(40,40,512),再2X2最大池化,此時不進行池化,輸出net爲(40,40,512)。conv4-3的結果會進入FFM1進行特徵的融合。
6、conv5三次[3,3]卷積網絡,輸出的特徵層爲1024,輸出net爲(40,40,1024),再2X2最大池化,輸出net爲(20,20,1024)。池化後的結果會進入FFM1進行特徵的融合。

def VGG16(inputs):

    net = {} 
    image_input = inputs
    net['input'] = image_input
    # 第一個卷積部分
    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'])

    # 第二個卷積部分
    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'])
    y0 = net['pool2']
    # 第三個卷積部分
    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'])
    y1 = net['pool3']
    # 第四個卷積部分
    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'])
    y2 = net['conv4_3']
    # 第五個卷積部分
    net['conv5_1'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_1')(net['conv4_3'])
    net['conv5_2'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_2')(net['conv5_1'])
    net['conv5_3'] = Conv2D(1024, kernel_size=(3,3),
                                   activation='relu',
                                   padding='same',
                                   name='conv5_3')(net['conv5_2'])
    net['pool5'] = MaxPooling2D((3, 3), strides=(2, 2), padding='same',
                                name='pool5')(net['conv5_3'])
    y3 = net['pool5']
    model = Model(inputs, [y0,y1,y2,y3], name='resnet50')

    return model

2、FFM1特徵初步融合

在這裏插入圖片描述
FFM1具體的結構如下:

在這裏插入圖片描述
FFM1會對VGG提取到的特徵進行初步融合。

在利用VGG進行特徵提取的時候,我們會取出shape爲(40,40,512)、(20,20,1024)的特徵層進行下一步的操作。

在FFM1中,其會對(20,20,1024)的特徵層進行進行一個通道數爲512、卷積核大小爲3x3、步長爲1x1的卷積,然後再進行上採樣,使其Shape變爲(40,40,512);

同時會對(40,40,512)的特徵層進行進行一個通道數爲256、卷積核大小爲1x1,步長爲1x1的卷積,使其Shape變爲(40,40,256);

然後將兩個卷積後的結果進行堆疊,變成一個(40,40,768)的初步融合特徵層
實現代碼爲:

def FFMv1(C4, C5, feature_size_1=256, feature_size_2=512,
          name='FFMv1'):
    # 40,40,256
    F4 = conv2d(C4, filters=feature_size_1, kernel_size=(3, 3), strides=(1, 1), padding='same', name='F4')
    # 20,20,512
    F5 = conv2d(C5, filters=feature_size_2, kernel_size=(1, 1), strides=(1, 1), padding='same', name='F5')
    # 40,40,512
    F5 = keras.layers.UpSampling2D(size=(2, 2), name='F5_Up')(F5)

    outputs = keras.layers.Concatenate(name=name)([F4, F5])
    # 40,40,768
    return outputs

3、細化U型模塊TUM

在這裏插入圖片描述
Tum的結構具體如下:
在這裏插入圖片描述
當我們給Tum輸入一個(40,40,256)的有效特徵層之後,Tum會對輸入進來的特徵層進行U型的特徵提取,這裏的結構比較類似特徵金字塔的結構,先對特徵層進行不斷的特徵壓縮,然後再不斷的上採樣進行特徵融合,利用Tum我們可以獲得6個有效特徵層,大小分別是(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)。

def TUM(stage, inputs, feature_size=256, name="TUM"):
    # 128
    output_features = feature_size // 2

    size_buffer = []

    # 40,40,256
    f1 = inputs
    # 20,20,256
    f2 = conv2d(f1, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f2')
    # 10,10,256
    f3 = conv2d(f2, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f3')
    # 5,5,256   
    f4 = conv2d(f3, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f4')
    # 3,3,256
    f5 = conv2d(f4, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='same',name=name + "_" + str(stage) + '_f5')
    # 1,1,256
    f6 = conv2d(f5, filters=feature_size, kernel_size=(3, 3), strides=(2, 2), padding='valid',name=name + "_" + str(stage) + '_f6')

    # 40,40
    size_buffer.append([int(f1.shape[2])] * 2)
    # 20,20
    size_buffer.append([int(f2.shape[2])] * 2)
    # 10,10
    size_buffer.append([int(f3.shape[2])] * 2)
    # 5,5
    size_buffer.append([int(f4.shape[2])] * 2)
    # 3,3
    size_buffer.append([int(f5.shape[2])] * 2)
    
    # print(size_buffer)
    level = 2
    c6 = f6
    # 1,1,256
    c5 = conv2d(c6, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same',name=name + "_" + str(stage) + '_c5')
    # 3,3,256
    c5 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[4]), name=name + "_" + str(stage) + '_upsample_add5')(c5)
    c5 = keras.layers.Add()([c5, f5])
 
    # 3,3,256
    c4 = conv2d(c5, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c4')
    # 5,5,256
    c4 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[3]), name=name + "_" + str(stage) + '_upsample_add4')(c4)
    c4 = keras.layers.Add()([c4, f4])

    # 5,5,256
    c3 = conv2d(c4, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c3')
    # 10,10,256
    c3 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[2]), name=name + "_" + str(stage) + '_upsample_add3')(c3)
    c3 = keras.layers.Add()([c3, f3])

    # 10,10,256
    c2 = conv2d(c3, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c2')
    # 20,20,256
    c2 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[1]), name=name + "_" + str(stage) + '_upsample_add2')(c2)
    c2 = keras.layers.Add()([c2, f2])

    # 20,20,256
    c1 = conv2d(c2, filters=feature_size, kernel_size=(3, 3), strides=(1, 1), padding='same', name=name + "_" + str(stage) + '_c1')
    # 40,40,256
    c1 = keras.layers.Lambda(lambda x: tf.image.resize_bilinear(x, size=size_buffer[0]), name=name + "_" + str(stage) + '_upsample_add1')(c1)
    c1 = keras.layers.Add()([c1, f1])

    level = 3

    # 40,40,128 
    o1 = conv2d(c1, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o1')
    # 20,20,128
    o2 = conv2d(c2, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o2')
    # 10,10,128
    o3 = conv2d(c3, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o3')
    # 5,5,128
    o4 = conv2d(c4, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o4')
    # 3,3,128
    o5 = conv2d(c5, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o5')
    # 1,1,128
    o6 = conv2d(c6, filters=output_features, kernel_size=(1, 1), strides=(1, 1), padding='valid',name=name + "_" + str(stage) + '_o6')

    outputs = [o1, o2, o3, o4, o5, o6]

    return outputs

4、FFM2特徵加強融合

通過TUM,我們可以獲得六個有效特徵層,爲了進一步加強網絡的特徵提取能力,M2det將6個有效特徵層中的(40,40,128)特徵層取出,和FFM1提取出來的初步融合特徵層進行加強融合,再次輸出一個(40,40,256)的加強融合的特徵層。
在這裏插入圖片描述
此時FFM2輸出的加強融合特徵層可以再一次傳入到TUM中進行U形特徵提取。
在這裏插入圖片描述
如上圖所示,我們可以進一步利用多個TUM模塊進行特徵提取,利用多個TUM模塊我們可以獲得多次有效特徵層。

TUM模塊的數量我們可以根據自身需要進行修改,本文使用4次TUM模塊,可以分別獲得四次(40,40,128)、(20,20,128)、(10,10,128)、(5,5,128)、(3,3,128)、(1,1,128)的有效特徵層。(論文中做了實驗,用8次TUM模塊會有比較好的效果)。

我們可以將獲得的有效特徵層,按照shape進行堆疊,最終獲得(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六個有效特徵層。

def FFMv2(stage, base, tum, base_size=(40,40,768), tum_size=(40,40,128), feature_size=128, name='FFMv2'):

    # 40,40,128
    outputs = conv2d(base, filters=feature_size, kernel_size=(1, 1), strides=(1, 1), padding='same', name=name+"_"+str(stage) + '_base_feature')
    outputs = keras.layers.Concatenate(name=name+"_"+str(stage))([outputs, tum])
    # 40,40,256
    return outputs

def _create_feature_pyramid(base_feature, stage=8):
    features = [[],[],[],[],[],[]]
    # 將輸入進來的
    inputs = keras.layers.Conv2D(filters=256, kernel_size=1, strides=1, padding='same')(base_feature)
    # 第一個TUM模塊
    outputs = TUM(1,inputs)
    max_output = outputs[0]
    for j in range(len(features)):
        features[j].append(outputs[j])

    # 第2,3,4個TUM模塊,需要將上一個Tum模塊輸出的40x40x128的內容,傳入到下一個Tum模塊中
    for i in range(2, stage+1):
        # 將Tum模塊的輸出和基礎特徵層傳入到FFmv2層當中
        # 輸入爲base_feature 40x40x768,max_output 40x40x128
        # 輸出爲40x40x256
        inputs = FFMv2(i - 1,base_feature, max_output)
        # 輸出爲40x40x128、20x20x128、10x10x128、5x5x128、3x3x128、1x1x128
        outputs = TUM(i,inputs)

        max_output = outputs[0]
        for j in range(len(features)):
            features[j].append(outputs[j])
    # 進行了4次TUM
    # 將獲得的同樣大小的特徵層堆疊到一起
    concatenate_features = []
    for feature in features:
        concat = keras.layers.Concatenate()([f for f in feature])
        concatenate_features.append(concat)
    return concatenate_features

5、注意力機制模塊SFAM

在這裏插入圖片描述
注意力機制模塊如下:
在這裏插入圖片描述
其會對上一步獲得的(40,40,512)、(20,20,512)、(10,10,512)、(5,5,512)、(3,3,512)、(1,1,512)六個有效特徵層。進行各個通道的注意力機制調整,判斷每一個通道數應該有的權重。

# 注意力機制
def SE_block(inputs, input_size, compress_ratio=16, name='SE_block'):
    pool = keras.layers.GlobalAveragePooling2D()(inputs)
    reshape = keras.layers.Reshape((1, 1, input_size[2]))(pool)

    fc1 = keras.layers.Conv2D(filters=input_size[2] // compress_ratio, kernel_size=1, strides=1, padding='valid',
                              activation='relu', name=name+'_fc1')(reshape)
    fc2 = keras.layers.Conv2D(filters=input_size[2], kernel_size=1, strides=1, padding='valid', activation='sigmoid',
                              name=name+'_fc2')(fc1)

    reweight = keras.layers.Multiply(name=name+'_reweight')([inputs, fc2])

    return reweight


def SFAM(feature_pyramid,input_sizes, compress_ratio=16, name='SFAM'):
    outputs = []
    for i in range(len(input_sizes)):
        input_size = input_sizes[i]
        _input = feature_pyramid[i]
        _output = SE_block(_input, input_size, compress_ratio=compress_ratio, name='SE_block_' + str(i))

        outputs.append(_output)
    return outputs

6、從特徵獲取預測結果

通過第五步,我們獲取了6個融合了注意力機制的有效特徵層。

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

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

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

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

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

def m2det(num_classes,inputs, num_anchors=6, name='m2det',backbone='mobilenet'):
    if inputs==None:
        inputs = keras.layers.Input(shape=(320, 320, 3))
    else:
        inputs = inputs
    if backbone=='mobilenet':
        C3, C4, C5 = MobileNet(inputs).outputs[1:]
    elif backbone=='resnet':
        C3, C4, C5 = ResNet50(inputs).outputs[1:]
    elif backbone=="vgg":
        C3, C4, C5 = VGG16(inputs).outputs[1:]

    # 40,40,768
    base_feature = FFMv1(C4, C5, feature_size_1=256, feature_size_2=512)

    if backbone=='mobilenet':
        feature_pyramid = _create_feature_pyramid(base_feature, stage=4)
    elif backbone=='resnet':
        feature_pyramid = _create_feature_pyramid(base_feature, stage=4)
    elif backbone=="vgg":
        feature_pyramid = _create_feature_pyramid(base_feature, stage=4)

    feature_pyramid_sizes = _calculate_input_sizes(feature_pyramid)

    outputs = SFAM(feature_pyramid,feature_pyramid_sizes)

    regressions = []
    classifications = []
    for feature in outputs:
        classification = keras.layers.Conv2D(filters=num_classes * num_anchors,kernel_size=3,strides=1,padding='same')(feature)
        classification = keras.layers.Reshape((-1, num_classes))(classification)
        classification = keras.layers.Activation('softmax')(classification)

        regression = keras.layers.Conv2D(filters=num_anchors * 4,kernel_size=3,strides=1,padding='same')(feature)
        regression = keras.layers.Reshape((-1, 4))(regression)

        regressions.append(regression)
        classifications.append(classification)
    
    regressions = keras.layers.Concatenate(axis=1, name="regression")(regressions)
    classifications = keras.layers.Concatenate(axis=1, name="classification")(classifications)
    pyramids = [regressions,classifications]

    return keras.models.Model(inputs=inputs, outputs=pyramids, name=name)

7、預測結果的解碼

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

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

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

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

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

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

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

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

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

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

實現代碼如下:

def decode_boxes(self, mbox_loc, mbox_priorbox):
    # 獲得先驗框的寬與高
    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 * 0.1
    decode_bbox_center_x += prior_center_x
    decode_bbox_center_y = mbox_loc[:, 1] * prior_height * 0.1
    decode_bbox_center_y += prior_center_y
    
    # 真實框的寬與高的求取
    decode_bbox_width = np.exp(mbox_loc[:, 2] * 0.2)
    decode_bbox_width *= prior_width
    decode_bbox_height = np.exp(mbox_loc[:, 3] * 0.2)
    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, mbox_priorbox, background_label_id=0, keep_top_k=200,
                    confidence_threshold=0.4):
    
    # 網絡預測的結果
    mbox_loc = predictions[0]
    # 先驗框
    mbox_priorbox = mbox_priorbox
    # 置信度
    mbox_conf = predictions[1]
    results = []
    # 對每一個圖片進行處理
    for i in range(len(mbox_loc)):
        results.append([])
        decode_bbox = self.decode_boxes(mbox_loc[i], mbox_priorbox)
        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]
    # 獲得,在所有預測結果裏面,置信度比較高的框
    # 還有,利用先驗框和m2det的預測結果,處理獲得了真實框(預測框)的位置
    return results

8、在原圖上進行繪製

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

二、訓練部分

1、真實框的處理

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

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

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

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

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

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

實現代碼如下:

def iou(self, box):
    # 計算出每個真實框與所有的先驗框的iou
    # 判斷真實框與先驗框的重合情況
    inter_upleft = np.maximum(self.priors[:, :2], box[:2])
    inter_botright = np.minimum(self.priors[:, 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.priors[:, 2] - self.priors[:, 0])*(self.priors[:, 3] - self.priors[:, 1])
    # 計算iou
    union = area_true + area_gt - inter

    iou = inter / union
    return iou

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]
    # 先計算真實框的中心與長寬
    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])
    
    # 逆向求取RFB應該有的預測結果
    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] /= 0.1

    encoded_box[:, 2:4][assign_mask] = np.log(box_wh / assigned_priors_wh)
    # 除以0.2
    encoded_box[:, 2:4][assign_mask] /= 0.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 + 1 + 1 + self.num_classes + 1))

    assignment[:, 4] = 0.0        
    assignment[:, 5] = 1
    assignment[:, -1] = 0.0
    if len(boxes) == 0:
        return assignment
        
    # (n, num_priors, 5)
    encoded_boxes = np.apply_along_axis(self.encode_box, 1, boxes[:, :4])
    # 每一個真實框的編碼後的值,和iou
    # (n, num_priors)
    encoded_boxes = encoded_boxes.reshape(-1, self.num_priors, 5)

    # 取重合程度最大的先驗框,並且獲取這個先驗框的index
    # (num_priors)
    best_iou = encoded_boxes[:, :, -1].max(axis=0)
    # (num_priors)
    best_iou_idx = encoded_boxes[:, :, -1].argmax(axis=0)
    # (num_priors)
    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
    assignment[:, 5][best_iou_mask] = 0
    assignment[:, 6:-1][best_iou_mask] = boxes[best_iou_idx, 4:]
    assignment[:, -1][best_iou_mask] = 1
    # 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的

    return assignment

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

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

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

實現代碼如下:

def rand(a=0, b=1):
    return np.random.rand()*(b-a) + a
    
def softmax_loss(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 conf_loss(neg_pos_ratio = 3,negatives_for_hard = 100):
    def _conf_loss(y_true, y_pred):
        batch_size = tf.shape(y_true)[0]
        num_boxes = tf.to_float(tf.shape(y_true)[1])
        
        labels         = y_true[:, :, :-1]
        classification = y_pred

        cls_loss = softmax_loss(labels, classification)
        
        num_pos = tf.reduce_sum(y_true[:, :, -1], axis=-1)
        
        pos_conf_loss = tf.reduce_sum(cls_loss * y_true[:, :, -1],
                                      axis=1)
        # 獲取一定的負樣本
        num_neg = tf.minimum(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) * 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)

        max_confs = tf.reduce_max(y_pred[:, :, 1:-1],
                                  axis=2)

        # 取top_k個置信度,作爲負樣本
        x, indices = tf.nn.top_k(max_confs * (1 - y_true[:, :, -1]),
                                 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(cls_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)


        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)
        total_loss /= tf.reduce_sum(num_pos)
        # total_loss = tf.Print(total_loss,[labels,full_indices,tf.reduce_sum(pos_conf_loss)/tf.reduce_sum(num_pos),tf.reduce_sum(neg_conf_loss)/tf.reduce_sum(num_pos),tf.reduce_sum(num_pos)])
        return total_loss
    return _conf_loss


def smooth_l1(sigma=1.0):
    sigma_squared = sigma ** 2

    def _smooth_l1(y_true, y_pred):
        # y_true [batch_size, num_anchor, 4+1]
        # y_pred [batch_size, num_anchor, 4]
        regression        = y_pred
        regression_target = y_true[:, :, :-1]
        anchor_state      = y_true[:, :, -1]

        # 找到正樣本
        indices           = tf.where(keras.backend.equal(anchor_state, 1))
        regression        = tf.gather_nd(regression, indices)
        regression_target = tf.gather_nd(regression_target, indices)

        # 計算 smooth L1 loss
        # f(x) = 0.5 * (sigma * x)^2          if |x| < 1 / sigma / sigma
        #        |x| - 0.5 / sigma / sigma    otherwise
        regression_diff = regression - regression_target
        regression_diff = keras.backend.abs(regression_diff)
        regression_loss = backend.where(
            keras.backend.less(regression_diff, 1.0 / sigma_squared),
            0.5 * sigma_squared * keras.backend.pow(regression_diff, 2),
            regression_diff - 0.5 / sigma_squared
        )

        normalizer = keras.backend.maximum(1, keras.backend.shape(indices)[0])
        normalizer = keras.backend.cast(normalizer, dtype=keras.backend.floatx())
        loss = keras.backend.sum(regression_loss) / normalizer

        return loss

    return _smooth_l1

訓練自己的M2det模型

M2det整體的文件夾構架如下:
在這裏插入圖片描述
本文使用VOC格式進行訓練。
訓練前將標籤文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
在這裏插入圖片描述
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在這裏插入圖片描述
在訓練前利用voc2M2det.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即可開始訓練。
在這裏插入圖片描述

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