睿智的目標檢測18——Keras搭建Faster-RCNN目標檢測平臺

學習前言

最近對實例分割感興趣了,不過實例分割MaskRCNN是基於FasterRCNN的,之前學了非常多的One-Stage的目標檢測算法,對FasterRCNN並不感興趣,這次我們來學學FasterRCNN。
在這裏插入圖片描述

什麼是FasterRCNN目標檢測算法

在這裏插入圖片描述
Faster-RCNN是一個非常有效的目標檢測算法,雖然是一個比較早的論文, 但它至今仍是許多目標檢測算法的基礎。

Faster-RCNN作爲一種two-stage的算法,與one-stage的算法相比,two-stage的算法更加複雜且速度較慢,但是檢測精度會更高。

事實上也確實是這樣,Faster-RCNN的檢測效果非常不錯,但是檢測速度與訓練速度有待提高。

源碼下載

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

Faster-RCNN實現思路

一、預測部分

1、主幹網絡介紹

在這裏插入圖片描述
Faster-RCNN可以採用多種的主幹特徵提取網絡,常用的有VGG,Resnet,Xception等等,本文采用的是Resnet網絡,關於Resnet的介紹大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102790260

FasterRcnn對輸入進來的圖片尺寸沒有固定,但是一般會把輸入進來的圖片短邊固定成600,如輸入一張1200x1800的圖片,會把圖片不失真的resize到600x900上。

ResNet50有兩個基本的塊,分別名爲Conv Block和Identity Block,其中Conv Block輸入和輸出的維度是不一樣的,所以不能連續串聯,它的作用是改變網絡的維度;Identity Block輸入維度和輸出維度相同,可以串聯,用於加深網絡的。
Conv Block的結構如下:
在這裏插入圖片描述
Identity Block的結構如下:
在這裏插入圖片描述
這兩個都是殘差網絡結構。

Faster-RCNN的主幹特徵提取網絡部分只包含了長寬壓縮了四次的內容,第五次壓縮後的內容在ROI中使用。即Faster-RCNN在主幹特徵提取網絡所用的網絡層如圖所示。
以輸入的圖片爲600x600爲例,shape變化如下:
在這裏插入圖片描述
最後一層的輸出就是公用特徵層。

實現代碼:

def identity_block(input_tensor, kernel_size, filters, stage, block):

    filters1, filters2, filters3 = filters

    conv_name_base = 'res' + str(stage) + block + '_branch'
    bn_name_base = 'bn' + str(stage) + block + '_branch'

    x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
    x = BatchNormalization(name=bn_name_base + '2a')(x)
    x = Activation('relu')(x)

    x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b')(x)
    x = BatchNormalization(name=bn_name_base + '2b')(x)
    x = Activation('relu')(x)

    x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
    x = BatchNormalization(name=bn_name_base + '2c')(x)

    x = layers.add([x, input_tensor])
    x = Activation('relu')(x)
    return x


def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2)):

    filters1, filters2, filters3 = filters

    conv_name_base = 'res' + str(stage) + block + '_branch'
    bn_name_base = 'bn' + str(stage) + block + '_branch'

    x = Conv2D(filters1, (1, 1), strides=strides,
               name=conv_name_base + '2a')(input_tensor)
    x = BatchNormalization(name=bn_name_base + '2a')(x)
    x = Activation('relu')(x)

    x = Conv2D(filters2, kernel_size, padding='same',
               name=conv_name_base + '2b')(x)
    x = BatchNormalization(name=bn_name_base + '2b')(x)
    x = Activation('relu')(x)

    x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
    x = BatchNormalization(name=bn_name_base + '2c')(x)

    shortcut = Conv2D(filters3, (1, 1), strides=strides,
                      name=conv_name_base + '1')(input_tensor)
    shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)

    x = layers.add([x, shortcut])
    x = Activation('relu')(x)
    return x


def ResNet50(inputs):

    img_input = inputs

    x = ZeroPadding2D((3, 3))(img_input)
    x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(x)
    x = BatchNormalization(name='bn_conv1')(x)
    x = Activation('relu')(x)

    x = MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x)

    x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1))
    x = identity_block(x, 3, [64, 64, 256], stage=2, block='b')
    x = identity_block(x, 3, [64, 64, 256], stage=2, block='c')


    x = conv_block(x, 3, [128, 128, 512], stage=3, block='a')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='b')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='c')
    x = identity_block(x, 3, [128, 128, 512], stage=3, block='d')

    x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e')
    x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f')

    return x

2、獲得Proposal建議框

在這裏插入圖片描述
獲得的公用特徵層在圖像中就是Feature Map,其有兩個應用,一個是和ROIPooling結合使用、另一個是進行一次3x3的卷積後,進行一個9通道的1x1卷積,還有一個36通道的1x1卷積。

在Faster-RCNN中,num_priors也就是先驗框的數量就是9,所以兩個1x1卷積的結果實際上也就是:

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

9 x 1的卷積 用於預測 公用特徵層上 每一個網格點上 每一個預測框內部是否包含了物體。

當我們輸入的圖片的shape是600x600x3的時候,公用特徵層的shape就是38x38x1024,相當於把輸入進來的圖像分割成38x38的網格,然後每個網格存在9個先驗框,這些先驗框有不同的大小,在圖像上密密麻麻。

9 x 4的卷積的結果會對這些先驗框進行調整,獲得一個新的框。
9 x 1的卷積會判斷上述獲得的新框是否包含物體。

到這裏我們可以獲得了一些有用的框,這些框會利用9 x 1的卷積判斷是否存在物體。

到此位置還只是粗略的一個框的獲取,也就是一個建議框。然後我們會在建議框裏面繼續找東西。

實現代碼爲:

def get_rpn(base_layers, num_anchors):
    x = Conv2D(512, (3, 3), padding='same', activation='relu', kernel_initializer='normal', name='rpn_conv1')(base_layers)

    x_class = Conv2D(num_anchors, (1, 1), activation='sigmoid', kernel_initializer='uniform', name='rpn_out_class')(x)
    x_regr = Conv2D(num_anchors * 4, (1, 1), activation='linear', kernel_initializer='zero', name='rpn_out_regress')(x)
    
    x_class = Reshape((-1,1),name="classification")(x_class)
    x_regr = Reshape((-1,4),name="regression")(x_regr)
    return [x_class, x_regr, base_layers]

3、Proposal建議框的解碼

通過第二步我們獲得了38x38x9個先驗框的預測結果。預測結果包含兩部分。

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

9 x 1的卷積 用於預測 公用特徵層上 每一個網格點上 每一個預測框內部是否包含了物體。

相當於就是將整個圖像分成38x38個網格;然後從每個網格中心建立9個先驗框,一共38x38x9個,12996個先驗框。

當輸入圖像shape不同時,先驗框的數量也會發生改變。
在這裏插入圖片描述
先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調整。

9 x 4中的9表示了這個網格點所包含的先驗框數量,其中的4表示了框的中心與長寬的調整情況。

實現代碼如下:


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

4、對Proposal建議框加以利用(RoiPoolingConv)

在這裏插入圖片描述
讓我們對建議框有一個整體的理解:
事實上建議框就是對圖片哪一個區域有物體存在進行初步篩選。

通過主幹特徵提取網絡,我們可以獲得一個公用特徵層,當輸入圖片爲600x600x3的時候,它的shape是38x38x1024,然後建議框會對這個公用特徵層進行截取。

其實公用特徵層裏面的38x38對應着圖片裏的38x38個區域,38x38中的每一個點相當於這個區域內部所有特徵的濃縮。

建議框會對這38x38個區域進行截取,也就是認爲這些區域裏存在目標,然後將截取的結果進行resize,resize到14x14x1024的大小。

每次輸入的建議框的數量默認情況是32。

然後再對每個建議框再進行Resnet原有的第五次壓縮。壓縮完後進行一個平均池化,再進行一個Flatten,最後分別進行一個num_classes的全連接和(num_classes-1)x4全連接。

num_classes的全連接用於對最後獲得的框進行分類,(num_classes-1)x4全連接用於對相應的建議框進行調整,之所以-1是不包括被認定爲背景的框。

通過這些操作,我們可以獲得所有建議框的調整情況,和這個建議框調整後框內物體的類別。

事實上,在上一步獲得的建議框就是ROI的先驗框

對Proposal建議框加以利用的過程與shape變化如圖所示:
在這裏插入圖片描述
建議框調整後的結果就是最終的預測結果了,可以在圖上進行繪畫了。

class RoiPoolingConv(Layer):
    def __init__(self, pool_size, num_rois, **kwargs):
        self.dim_ordering = K.image_dim_ordering()
        assert self.dim_ordering in {'tf', 'th'}, 'dim_ordering must be in {tf, th}'
        self.pool_size = pool_size
        self.num_rois = num_rois
        super(RoiPoolingConv, self).__init__(**kwargs)
    def build(self, input_shape):
        self.nb_channels = input_shape[0][3]
    def compute_output_shape(self, input_shape):
        return None, self.num_rois, self.pool_size, self.pool_size, self.nb_channels
    def call(self, x, mask=None):
        assert(len(x) == 2)
        img = x[0]
        rois = x[1]
        outputs = []
        for roi_idx in range(self.num_rois):
            x = rois[0, roi_idx, 0]
            y = rois[0, roi_idx, 1]
            w = rois[0, roi_idx, 2]
            h = rois[0, roi_idx, 3]
            x = K.cast(x, 'int32')
            y = K.cast(y, 'int32')
            w = K.cast(w, 'int32')
            h = K.cast(h, 'int32')
            rs = tf.image.resize_images(img[:, y:y+h, x:x+w, :], (self.pool_size, self.pool_size))
            outputs.append(rs)
        final_output = K.concatenate(outputs, axis=0)
        final_output = K.reshape(final_output, (1, self.num_rois, self.pool_size, self.pool_size, self.nb_channels))
        final_output = K.permute_dimensions(final_output, (0, 1, 2, 3, 4))
        return final_output
        
def identity_block_td(input_tensor, kernel_size, filters, stage, block, trainable=True):
    nb_filter1, nb_filter2, nb_filter3 = filters
    if K.image_dim_ordering() == 'tf':
        bn_axis = 3
    else:
        bn_axis = 1

    conv_name_base = 'res' + str(stage) + block + '_branch'
    bn_name_base = 'bn' + str(stage) + block + '_branch'

    x = TimeDistributed(Conv2D(nb_filter1, (1, 1), trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2a')(input_tensor)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2a')(x)
    x = Activation('relu')(x)

    x = TimeDistributed(Conv2D(nb_filter2, (kernel_size, kernel_size), trainable=trainable, kernel_initializer='normal',padding='same'), name=conv_name_base + '2b')(x)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2b')(x)
    x = Activation('relu')(x)

    x = TimeDistributed(Conv2D(nb_filter3, (1, 1), trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2c')(x)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2c')(x)

    x = Add()([x, input_tensor])
    x = Activation('relu')(x)

    return x

def conv_block_td(input_tensor, kernel_size, filters, stage, block, input_shape, strides=(2, 2), trainable=True):
    nb_filter1, nb_filter2, nb_filter3 = filters
    if K.image_dim_ordering() == 'tf':
        bn_axis = 3
    else:
        bn_axis = 1

    conv_name_base = 'res' + str(stage) + block + '_branch'
    bn_name_base = 'bn' + str(stage) + block + '_branch'

    x = TimeDistributed(Conv2D(nb_filter1, (1, 1), strides=strides, trainable=trainable, kernel_initializer='normal'), input_shape=input_shape, name=conv_name_base + '2a')(input_tensor)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2a')(x)
    x = Activation('relu')(x)

    x = TimeDistributed(Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same', trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2b')(x)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2b')(x)
    x = Activation('relu')(x)

    x = TimeDistributed(Conv2D(nb_filter3, (1, 1), kernel_initializer='normal'), name=conv_name_base + '2c', trainable=trainable)(x)
    x = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '2c')(x)

    shortcut = TimeDistributed(Conv2D(nb_filter3, (1, 1), strides=strides, trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '1')(input_tensor)
    shortcut = TimeDistributed(BatchNormalization(axis=bn_axis), name=bn_name_base + '1')(shortcut)

    x = Add()([x, shortcut])
    x = Activation('relu')(x)
    return x


def classifier_layers(x, input_shape, trainable=False):
    x = conv_block_td(x, 3, [512, 512, 2048], stage=5, block='a', input_shape=input_shape, strides=(2, 2), trainable=trainable)
    x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='b', trainable=trainable)
    x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='c', trainable=trainable)
    x = TimeDistributed(AveragePooling2D((7, 7)), name='avg_pool')(x)

    return x
    
def get_classifier(base_layers, input_rois, num_rois, nb_classes=21, trainable=False):
    pooling_regions = 14
    input_shape = (num_rois, 14, 14, 1024)
    out_roi_pool = RoiPoolingConv(pooling_regions, num_rois)([base_layers, input_rois])
    out = classifier_layers(out_roi_pool, input_shape=input_shape, trainable=True)
    out = TimeDistributed(Flatten())(out)
    out_class = TimeDistributed(Dense(nb_classes, activation='softmax', kernel_initializer='zero'), name='dense_class_{}'.format(nb_classes))(out)
    out_regr = TimeDistributed(Dense(4 * (nb_classes-1), activation='linear', kernel_initializer='zero'), name='dense_regress_{}'.format(nb_classes))(out)
    return [out_class, out_regr]

5、在原圖上進行繪製

在第四步的結尾,我們對建議框進行再一次進行解碼後,我們可以獲得預測框在原圖上的位置,而且這些預測框都是經過篩選的。這些篩選後的框可以直接繪製在圖片上,就可以獲得結果了。

6、整體的執行流程

在這裏插入圖片描述
幾個小tip:
1、共包含了兩次解碼過程。
2、先進行粗略的篩選再細調。
3、第一次獲得的建議框解碼後的結果是對共享特徵層featuremap進行截取。

二、訓練部分

Faster-RCNN的訓練過程和它的預測過程一樣,分爲兩部分,首先要訓練獲得建議框網絡,然後再訓練後面利用ROI獲得預測結果的網絡。

1、建議框網絡的訓練

公用特徵層如果要獲得建議框的預測結果,需要再進行一次3x3的卷積後,進行一個9通道的1x1卷積,還有一個36通道的1x1卷積。

在Faster-RCNN中,num_priors也就是先驗框的數量就是9,所以兩個1x1卷積的結果實際上也就是:

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

9 x 1的卷積 用於預測 公用特徵層上 每一個網格點上 每一個預測框內部是否包含了物體。

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

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

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

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

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

實現代碼如下:

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]
    # 逆向編碼,將真實框轉化爲Retinanet預測結果的格式
    # 先計算真實框的中心與長寬
    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
    encoded_box[:, :2][assign_mask] *= 4

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

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

但是由於原始圖片中可能存在多個真實框,可能同一個先驗框會與多個真實框重合度較高,我們只取其中與真實框重合度最高的就可以了。

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

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

實現代碼如下:

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]
    # 逆向編碼,將真實框轉化爲Retinanet預測結果的格式
    # 先計算真實框的中心與長寬
    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
    encoded_box[:, :2][assign_mask] *= 4

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

def ignore_box(self, box):
    iou = self.iou(box)
    
    ignored_box = np.zeros((self.num_priors, 1))

    # 找到每一個真實框,重合程度較高的先驗框
    assign_mask = (iou > self.ignore_threshold)&(iou<self.overlap_threshold)

    if not assign_mask.any():
        assign_mask[iou.argmax()] = True
        
    ignored_box[:, 0][assign_mask] = iou[assign_mask]
    return ignored_box.ravel()


def assign_boxes(self, boxes, anchors):
    self.num_priors = len(anchors)
    self.priors = anchors
    assignment = np.zeros((self.num_priors, 4 + 1))

    assignment[:, 4] = 0.0
    if len(boxes) == 0:
        return assignment
        
    # 對每一個真實框都進行iou計算
    ingored_boxes = np.apply_along_axis(self.ignore_box, 1, boxes[:, :4])
    # 取重合程度最大的先驗框,並且獲取這個先驗框的index
    ingored_boxes = ingored_boxes.reshape(-1, self.num_priors, 1)
    # (num_priors)
    ignore_iou = ingored_boxes[:, :, 0].max(axis=0)
    # (num_priors)
    ignore_iou_mask = ignore_iou > 0

    assignment[:, 4][ignore_iou_mask] = -1

    # (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
    # 通過assign_boxes我們就獲得了,輸入進來的這張圖片,應該有的預測結果是什麼樣子的
    return assignment

focal會忽略一些重合度相對較高但是不是非常高的先驗框,一般將重合度在0.3-0.7之間的先驗框進行忽略。

2、Roi網絡的訓練

通過上一步已經可以對建議框網絡進行訓練了,建議框網絡會提供一些位置的建議,在ROI網絡部分,其會將建議框根據進行一定的截取,並獲得對應的預測結果,事實上就是將上一步建議框當作了ROI網絡的先驗框。

因此,我們需要計算所有建議框和真實框的重合程度,並進行篩選,如果某個真實框和建議框的重合程度大於0.5則認爲該建議框爲正樣本,如果重合程度小於0.5大於0.1則認爲該建議框爲負樣本

因此我們可以對真實框進行編碼,這個編碼是相對於建議框的,也就是,當我們存在這些建議框的時候,我們的ROI預測網絡需要有什麼樣的預測結果才能將這些建議框調整成真實框。

每次訓練我們都放入32個建議框進行訓練,同時要注意正負樣本的平衡。
實現代碼如下:

# 編碼
def calc_iou(R, config, all_boxes, width, height, num_classes):
    # print(all_boxes)
    bboxes = all_boxes[:,:4]
    gta = np.zeros((len(bboxes), 4))
    for bbox_num, bbox in enumerate(bboxes):
        gta[bbox_num, 0] = int(round(bbox[0]*width/config.rpn_stride))
        gta[bbox_num, 1] = int(round(bbox[1]*height/config.rpn_stride))
        gta[bbox_num, 2] = int(round(bbox[2]*width/config.rpn_stride))
        gta[bbox_num, 3] = int(round(bbox[3]*height/config.rpn_stride))
    x_roi = []
    y_class_num = []
    y_class_regr_coords = []
    y_class_regr_label = []
    IoUs = []
    # print(gta)
    for ix in range(R.shape[0]):
        x1 = R[ix, 0]*width/config.rpn_stride
        y1 = R[ix, 1]*height/config.rpn_stride
        x2 = R[ix, 2]*width/config.rpn_stride
        y2 = R[ix, 3]*height/config.rpn_stride
        
        x1 = int(round(x1))
        y1 = int(round(y1))
        x2 = int(round(x2))
        y2 = int(round(y2))
        # print([x1, y1, x2, y2])
        best_iou = 0.0
        best_bbox = -1
        for bbox_num in range(len(bboxes)):
            curr_iou = iou([gta[bbox_num, 0], gta[bbox_num, 1], gta[bbox_num, 2], gta[bbox_num, 3]], [x1, y1, x2, y2])
            if curr_iou > best_iou:
                best_iou = curr_iou
                best_bbox = bbox_num
        # print(best_iou)
        if best_iou < config.classifier_min_overlap:
            continue
        else:
            w = x2 - x1
            h = y2 - y1
            x_roi.append([x1, y1, w, h])
            IoUs.append(best_iou)

            if config.classifier_min_overlap <= best_iou < config.classifier_max_overlap:
                label = -1
            elif config.classifier_max_overlap <= best_iou:
                
                label = int(all_boxes[best_bbox,-1])
                cxg = (gta[best_bbox, 0] + gta[best_bbox, 2]) / 2.0
                cyg = (gta[best_bbox, 1] + gta[best_bbox, 3]) / 2.0

                cx = x1 + w / 2.0
                cy = y1 + h / 2.0

                tx = (cxg - cx) / float(w)
                ty = (cyg - cy) / float(h)
                tw = np.log((gta[best_bbox, 2] - gta[best_bbox, 0]) / float(w))
                th = np.log((gta[best_bbox, 3] - gta[best_bbox, 1]) / float(h))
            else:
                print('roi = {}'.format(best_iou))
                raise RuntimeError
        # print(label)
        class_label = num_classes * [0]
        class_label[label] = 1
        y_class_num.append(copy.deepcopy(class_label))
        coords = [0] * 4 * (num_classes - 1)
        labels = [0] * 4 * (num_classes - 1)
        if label != -1:
            label_pos = 4 * label
            sx, sy, sw, sh = config.classifier_regr_std
            coords[label_pos:4+label_pos] = [sx*tx, sy*ty, sw*tw, sh*th]
            labels[label_pos:4+label_pos] = [1, 1, 1, 1]
            y_class_regr_coords.append(copy.deepcopy(coords))
            y_class_regr_label.append(copy.deepcopy(labels))
        else:
            y_class_regr_coords.append(copy.deepcopy(coords))
            y_class_regr_label.append(copy.deepcopy(labels))

    if len(x_roi) == 0:
        return None, None, None, None

    X = np.array(x_roi)
    # print(X)
    Y1 = np.array(y_class_num)
    Y2 = np.concatenate([np.array(y_class_regr_label),np.array(y_class_regr_coords)],axis=1)

    return np.expand_dims(X, axis=0), np.expand_dims(Y1, axis=0), np.expand_dims(Y2, axis=0), IoUs
# 正負樣本平衡
X2, Y1, Y2, IouS = calc_iou(R, config, boxes[0], width, height, NUM_CLASSES)

if X2 is None:
    rpn_accuracy_rpn_monitor.append(0)
    rpn_accuracy_for_epoch.append(0)
    continue

neg_samples = np.where(Y1[0, :, -1] == 1)
pos_samples = np.where(Y1[0, :, -1] == 0)

if len(neg_samples) > 0:
    neg_samples = neg_samples[0]
else:
    neg_samples = []

if len(pos_samples) > 0:
    pos_samples = pos_samples[0]
else:
    pos_samples = []

rpn_accuracy_rpn_monitor.append(len(pos_samples))
rpn_accuracy_for_epoch.append((len(pos_samples)))

if len(neg_samples)==0:
    continue

if len(pos_samples) < config.num_rois//2:
    selected_pos_samples = pos_samples.tolist()
else:
    selected_pos_samples = np.random.choice(pos_samples, config.num_rois//2, replace=False).tolist()
try:
    selected_neg_samples = np.random.choice(neg_samples, config.num_rois - len(selected_pos_samples), replace=False).tolist()
except:
    selected_neg_samples = np.random.choice(neg_samples, config.num_rois - len(selected_pos_samples), replace=True).tolist()

sel_samples = selected_pos_samples + selected_neg_samples
loss_class = model_classifier.train_on_batch([X, X2[:, sel_samples, :]], [Y1[:, sel_samples, :], Y2[:, sel_samples, :]])

訓練自己的Faster-RCNN模型

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

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