python目標(biāo)檢測(cè)SSD算法預(yù)測(cè)部分源碼詳解
學(xué)習(xí)前言
……學(xué)習(xí)了很多有關(guān)目標(biāo)檢測(cè)的概念呀,咕嚕咕嚕,可是要怎么才能進(jìn)行預(yù)測(cè)呢,我看了好久的SSD源碼,將其中的預(yù)測(cè)部分提取了出來(lái),訓(xùn)練部分我還沒(méi)看懂
什么是SSD算法
SSD是一種非常優(yōu)秀的one-stage方法,one-stage算法就是目標(biāo)檢測(cè)和分類是同時(shí)完成的,其主要思路是均勻地在圖片的不同位置進(jìn)行密集抽樣,抽樣時(shí)可以采用不同尺度和長(zhǎng)寬比,然后利用CNN提取特征后直接進(jìn)行分類與回歸,整個(gè)過(guò)程只需要一步,所以其優(yōu)勢(shì)是速度快。
但是均勻的密集采樣的一個(gè)重要缺點(diǎn)是訓(xùn)練比較困難,這主要是因?yàn)檎龢颖九c負(fù)樣本(背景)極其不均衡(參見(jiàn)Focal Loss),導(dǎo)致模型準(zhǔn)確度稍低。
SSD的英文全名是Single Shot MultiBox Detector,Single shot說(shuō)明SSD算法屬于one-stage方法,MultiBox說(shuō)明SSD算法基于多框預(yù)測(cè)。(它真的不是固態(tài)硬盤(pán)啊~~~~~~)
講解構(gòu)架
本次教程的講解分為倆個(gè)部分,第一部分是ssd_vgg_300主體的源碼的講解,第二部分是如何調(diào)用ssd_vgg_300主體的源碼,即利用源碼進(jìn)行預(yù)測(cè)。
ssd_vgg_300主體的源碼的講解包括如下三個(gè)部分:
1、網(wǎng)絡(luò)部分,用于建立ssd網(wǎng)絡(luò),用于預(yù)測(cè)種類和框的位置。
2、先驗(yàn)框部分,根據(jù)每個(gè)特征層的shape,構(gòu)建出合適比例的框,同時(shí)可以減少運(yùn)算量。
3、解碼部分,根據(jù)網(wǎng)絡(luò)部分和先驗(yàn)框部分的輸出,對(duì)框的位置進(jìn)行解碼。
利用源碼進(jìn)行預(yù)測(cè)的講解包括以下三個(gè)部分:
1、如何對(duì)圖片進(jìn)行處理。
2、載入模型
3、預(yù)測(cè)過(guò)程中處理的流程。
在看本次算法前,建議先下載我簡(jiǎn)化過(guò)的源碼,配合觀看,在其中運(yùn)行demo即可執(zhí)行程序:
下載鏈接:https://pan.baidu.com/s/16UtXIfE-imrzjg_rx7xTKQ
提取碼:vpo2
ssd_vgg_300主體的源碼
本文使用的ssd_vgg_300的源碼源于
鏈接:https://pan.baidu.com/s/1Wi1t9bYpTJEu5j3cq3pUnA
提取碼:6hye
本文對(duì)其進(jìn)行了簡(jiǎn)化,只保留了預(yù)測(cè)部分,便于理順整個(gè)SSD的框架。
1、 大體框架
在只需要預(yù)測(cè)的情況下,需要保留ssd_vgg_300源碼的網(wǎng)絡(luò)部分、先驗(yàn)框部分和解碼部分。(這里只能使用圖片哈,因?yàn)閂Scode收縮后也不能只復(fù)制各個(gè)部分的函數(shù)名)
其中:
1、net函數(shù)用于構(gòu)建網(wǎng)絡(luò),其輸入值為shape為(None,300,300,3)的圖像,在其中會(huì)經(jīng)過(guò)許多層網(wǎng)絡(luò)結(jié)構(gòu),在這許多的網(wǎng)絡(luò)結(jié)構(gòu)中存在6個(gè)特征層,用于讀取框框,最終輸出predictions和locations,predictions和locations中包含6個(gè)層的預(yù)測(cè)結(jié)果和框的位置。
2、arg_scope用于初始化網(wǎng)絡(luò)每一個(gè)層的默認(rèn)參數(shù),該項(xiàng)目會(huì)用到slim框架,slim框架是一個(gè)輕量級(jí)的tensorflow框架,其參數(shù)初始化與slim中的函數(shù)相關(guān)。
3、anchors用于獲得先驗(yàn)框,先驗(yàn)框也是針對(duì)6個(gè)特征層的。
4、bboxes_decode用于結(jié)合先驗(yàn)框和locations獲得在img中框的位置,locations相當(dāng)于編碼過(guò)后的框的位置,這樣做可以方便SSD網(wǎng)絡(luò)學(xué)習(xí),bboxes_decode用于解碼,解碼后可以獲得img中框的位置。
2、net網(wǎng)絡(luò)構(gòu)建
# =============================網(wǎng)絡(luò)部分============================= # def net(self, inputs, is_training=True, update_feat_shapes=True, dropout_keep_prob=0.5, prediction_fn=slim.softmax, reuse=None, scope='ssd_300_vgg'): """ SSD 網(wǎng)絡(luò)定義,調(diào)用外部函數(shù),建立網(wǎng)絡(luò)層 """ r = ssd_net(inputs, num_classes=self.params.num_classes, feat_layers=self.params.feat_layers, anchor_sizes=self.params.anchor_sizes, anchor_ratios=self.params.anchor_ratios, normalizations=self.params.normalizations, is_training=is_training, dropout_keep_prob=dropout_keep_prob, prediction_fn=prediction_fn, reuse=reuse, scope=scope) return r
在net函數(shù)中,其調(diào)用了一個(gè)外部的函數(shù)ssd_net,我估計(jì)作者是為了讓代碼主體更簡(jiǎn)潔。
實(shí)際的構(gòu)建代碼在ssd_net函數(shù)中,網(wǎng)絡(luò)構(gòu)建代碼中使用了許多的slim.repeat,該函數(shù)用于重復(fù)構(gòu)建卷積層,具體構(gòu)建的層共11層,在進(jìn)行目標(biāo)檢測(cè)框的選擇時(shí),我們選擇其中的[‘block4’, ‘block7’, ‘block8’, ‘block9’, ‘block10’, ‘block11’]。
這里我們放出論文中的網(wǎng)絡(luò)結(jié)構(gòu)層。
通過(guò)該圖我們可以發(fā)現(xiàn),其網(wǎng)絡(luò)結(jié)構(gòu)如下:
1、首先通過(guò)了多個(gè)3X3卷積層、5次步長(zhǎng)為2的最大池化取出特征,形成了5個(gè)Block,其中第四個(gè)Block的shape為(?,38,38,512),該層用于提取小目標(biāo)(多次卷積后大目標(biāo)的特征保存的更好,小目標(biāo)特征會(huì)消失,需要在比較靠前的層提取小目標(biāo)特征)。
2、進(jìn)行一次卷積核膨脹dilate(關(guān)于卷積核膨脹的概念可以去網(wǎng)上搜索以下哈)。
3、讀取第七個(gè)Block7的特征,shape為(?,19,19,1024)
4、分別利用1x1和3x3卷積提取特征,在3x3卷積的時(shí)候使用步長(zhǎng)2,縮小特征數(shù)。獲取第八個(gè)Block8的特征,shape為(?,10,10,512)
5、重復(fù)步驟4,獲得9、10、11卷積層的特征,shape分別為(?,5,5,256)、(?,3,3,256)、(?,1,1,256)
此時(shí)網(wǎng)絡(luò)便構(gòu)建完了。
# =============================網(wǎng)絡(luò)部分============================= # ############################################################ # 該部分供SSDNet的net函數(shù)調(diào)用,用于建立網(wǎng)絡(luò) # # 返回predictions, localisations, logits, end_points # ############################################################ def ssd_net(inputs, num_classes=SSDNet.default_params.num_classes, feat_layers=SSDNet.default_params.feat_layers, anchor_sizes=SSDNet.default_params.anchor_sizes, anchor_ratios=SSDNet.default_params.anchor_ratios, normalizations=SSDNet.default_params.normalizations, is_training=True, dropout_keep_prob=0.5, prediction_fn=slim.softmax, reuse=None, scope='ssd_300_vgg'): """SSD net definition. """ # 建立網(wǎng)絡(luò) end_points = {} with tf.variable_scope(scope, 'ssd_300_vgg', [inputs], reuse=reuse): # Block1 ''' 相當(dāng)于執(zhí)行: net = self.conv2d(x,64,[3,3],scope = 'conv1_1') net = self.conv2d(net,64,[3,3],scope = 'conv1_2') ''' # (300,300,3) -> (300,300,64) -> (150,150,64) net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1') end_points['block1'] = net net = slim.max_pool2d(net, [2, 2], scope='pool1') # Block 2. ''' 相當(dāng)于執(zhí)行: net = self.conv2d(net,128,[3,3],scope = 'conv2_1') net = self.conv2d(net,128,[3,3],scope = 'conv2_2') ''' # (150,150,64) -> (150,150,128) -> (75,75,128) net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2') end_points['block2'] = net net = slim.max_pool2d(net, [2, 2], scope='pool2') # Block 3. ''' 相當(dāng)于執(zhí)行: net = self.conv2d(net,256,[3,3],scope = 'conv3_1') net = self.conv2d(net,256,[3,3],scope = 'conv3_2') net = self.conv2d(net,256,[3,3],scope = 'conv3_3') ''' # (75,75,128) -> (75,75,256) -> (38,38,256) net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3') end_points['block3'] = net net = slim.max_pool2d(net, [2, 2],stride = 2,padding = "SAME", scope='pool3') # Block 4. # 三次卷積 # (38,38,256) -> (38,38,512) -> block4_net -> (19,19,512) net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4') end_points['block4'] = net net = slim.max_pool2d(net, [2, 2],padding = "SAME", scope='pool4') # Block 5. # 三次卷積 # (19,19,512)->(19,19,512) net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5') end_points['block5'] = net net = slim.max_pool2d(net, [3, 3], stride=1,padding = "SAME", scope='pool5') # Block 6: dilate # 卷積核膨脹 # (19,19,512)->(19,19,1024) net = slim.conv2d(net, 1024, [3, 3], rate=6, scope='conv6') end_points['block6'] = net net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training) # Block 7: 1x1 conv # (19,19,1024)->(19,19,1024) net = slim.conv2d(net, 1024, [1, 1], scope='conv7') end_points['block7'] = net net = tf.layers.dropout(net, rate=dropout_keep_prob, training=is_training) # Block 8/9/10/11: 1x1 and 3x3 convolutions stride 2 (except lasts). # (19,19,1024)->(19,19,256)->(10,10,512) end_point = 'block8' with tf.variable_scope(end_point): net = slim.conv2d(net, 256, [1, 1], scope='conv1x1') net = custom_layers.pad2d(net, pad=(1, 1)) net = slim.conv2d(net, 512, [3, 3], stride=2, scope='conv3x3', padding='VALID') end_points[end_point] = net end_point = 'block9' # (10,10,512)->(10,10,128)->(5,5,256) with tf.variable_scope(end_point): net = slim.conv2d(net, 128, [1, 1], scope='conv1x1') net = custom_layers.pad2d(net, pad=(1, 1)) net = slim.conv2d(net, 256, [3, 3], stride=2, scope='conv3x3', padding='VALID') end_points[end_point] = net end_point = 'block10' # (5,5,256)->(5,5,128)->(3,3,256) with tf.variable_scope(end_point): net = slim.conv2d(net, 128, [1, 1], scope='conv1x1') net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID') end_points[end_point] = net end_point = 'block11' # (3,3,256)->(1,1,256) with tf.variable_scope(end_point): net = slim.conv2d(net, 128, [1, 1], scope='conv1x1') net = slim.conv2d(net, 256, [3, 3], scope='conv3x3', padding='VALID') end_points[end_point] = net # 預(yù)測(cè)和定位層 predictions = [] logits = [] localisations = [] for i, layer in enumerate(feat_layers): with tf.variable_scope(layer + '_box'): p, l = ssd_multibox_layer(end_points[layer], num_classes, anchor_sizes[i], anchor_ratios[i], normalizations[i]) predictions.append(prediction_fn(p)) logits.append(p) localisations.append(l) return predictions, localisations, logits, end_points ssd_net.default_image_size = 300
仔細(xì)看代碼的同學(xué)會(huì)發(fā)現(xiàn),除去層的構(gòu)建外,最后還多了一段循環(huán),那這個(gè)循環(huán)是做什么的呢?而且同學(xué)們可以感受到,雖然我們提取了特征層,但是這個(gè)特征層和預(yù)測(cè)值、框的位置又有什么關(guān)系呢?
這個(gè)循環(huán)就是用來(lái)將特征層轉(zhuǎn)化成預(yù)測(cè)值和框的位置的。
在循環(huán)中我們調(diào)用了ssd_multibox_layer函數(shù),該函數(shù)的作用如下:
1、讀取網(wǎng)絡(luò)的特征層
2、對(duì)網(wǎng)絡(luò)的特征層再次進(jìn)行卷積,該卷積分為兩部分,互不相干,分別用于預(yù)測(cè)種類和框的位置。
3、預(yù)測(cè)框的位置,以Block4為例,Block4的shape為(?,38,38,512),再次卷積后,使其shape變?yōu)??,38,38,num_anchors x 4),其中num_anchors是每個(gè)特征點(diǎn)中先驗(yàn)框的數(shù)量,4代表框的特點(diǎn),一個(gè)框需要4個(gè)特征才可以確定位置,最后再reshape為(?,38,38,num_anchors,4),代表38x38個(gè)特點(diǎn)中,第num_anchors個(gè)框下的4個(gè)特點(diǎn)。
4、預(yù)測(cè)種類,以Block4為例,Block4的shape為(?,38,38,512),再次卷積后,使其shape變?yōu)??,38,38,num_anchors x 21),其中num_anchors是每個(gè)特征點(diǎn)中先驗(yàn)框的數(shù)量,21代表預(yù)測(cè)的種類,包含背景,SSD算法共預(yù)測(cè)21個(gè)種類,最后再reshape為(?,38,38,num_anchors,21),代表38x38個(gè)特點(diǎn)中,第num_anchors個(gè)框下的21個(gè)預(yù)測(cè)結(jié)果。
該函數(shù)的輸出結(jié)果中:
location_pred的shape為(?,feat_block.shape[0],feat_block.shape[1], num_anchors,4)
class_pred的shape為(?,feat_block.shape[0],feat_block.shape[1],num_anchors,21)
具體執(zhí)行代碼如下:
############################################################ # 該部分供ssd_net函數(shù)調(diào)用,返回種類預(yù)測(cè)和位置預(yù)測(cè) # # 將特征層的內(nèi)容輸入,根據(jù)特征層返回預(yù)測(cè)結(jié)果 # ############################################################ def ssd_multibox_layer(inputs, num_classes, sizes, ratios=[1], normalization=-1, bn_normalization=False): reshape = [-1] + inputs.get_shape().as_list()[1:-1] # 去除第一個(gè)和最后一個(gè)得到shape net = inputs # 對(duì)第一個(gè)特征層進(jìn)行l(wèi)2標(biāo)準(zhǔn)化。 if normalization > 0: net = custom_layers.l2_normalization(net, scaling=True) # Number of anchors. num_anchors = len(sizes) + len(ratios) # Location. num_loc_pred = num_anchors * 4 loc_pred = slim.conv2d(net, num_loc_pred, [3, 3], activation_fn=None, scope='conv_loc') loc_pred = custom_layers.channel_to_last(loc_pred) loc_pred = tf.reshape(loc_pred, reshape + [num_anchors, 4]) # Class prediction. num_cls_pred = num_anchors * num_classes cls_pred = slim.conv2d(net, num_cls_pred, [3, 3], activation_fn=None, scope='conv_cls') cls_pred = custom_layers.channel_to_last(cls_pred) cls_pred = tf.reshape(cls_pred, reshape + [num_anchors, num_classes]) return cls_pred, loc_pred
3、anchor先驗(yàn)框生成
# ==========================生成先驗(yàn)框部分========================== # def anchors(self, img_shape, dtype=np.float32): """ 計(jì)算給定圖像形狀的默認(rèn)定位框,調(diào)用外部函數(shù),獲得先驗(yàn)框。 """ return ssd_anchors_all_layers(img_shape, self.params.feat_shapes, self.params.anchor_sizes, self.params.anchor_ratios, self.params.anchor_steps, self.params.anchor_offset, dtype)
在anchor函數(shù)中,其調(diào)用了一個(gè)外部的函數(shù)ssd_anchors_all_layers,用于構(gòu)建先驗(yàn)框。先驗(yàn)框的構(gòu)建和上述網(wǎng)絡(luò)的構(gòu)建關(guān)系不大,但是需要用到上述網(wǎng)絡(luò)net的特征層size,先驗(yàn)框的構(gòu)建目的是為了讓圖片構(gòu)建出合適比例的框,同時(shí)可以減少運(yùn)算量。
在進(jìn)入ssd_anchors_all_layers函數(shù)后,根據(jù)名字可以知道,該函數(shù)用于生成所有層的先驗(yàn)框,其會(huì)進(jìn)入一個(gè)循環(huán),該循環(huán)用于根據(jù)每個(gè)特征層的size進(jìn)行先驗(yàn)框的構(gòu)建
代碼如下:
############################################################ # 該部分供SSDNet的anchors函數(shù)調(diào)用,用于獲取先驗(yàn)框 # # 返回y,x,h,w的組和 # ############################################################ def ssd_anchors_all_layers(img_shape, layers_shape, anchor_sizes, anchor_ratios, anchor_steps, offset=0.5, dtype=np.float32): """ 對(duì)所有特征層進(jìn)行計(jì)算 """ layers_anchors = [] for i, s in enumerate(layers_shape): anchor_bboxes = ssd_anchor_one_layer(img_shape, s, anchor_sizes[i], anchor_ratios[i], anchor_steps[i], offset=offset, dtype=dtype) layers_anchors.append(anchor_bboxes) return layers_anchors
此時(shí)再調(diào)用ssd_anchor_one_layer,根據(jù)名字可以知道,該函數(shù)用于生成單層的先驗(yàn)框,該部分是先驗(yàn)框生成的核心。
輸入?yún)?shù)包括圖像大小img_shape,特征層大小feat_shape,先驗(yàn)框大小sizes,先驗(yàn)框長(zhǎng)寬比率sizes,先驗(yàn)框放大倍數(shù)step。
執(zhí)行過(guò)程:
1、根據(jù)feat_shape生成x、y的網(wǎng)格。
2、將x和y歸一化到0到1之間,這里的x和y對(duì)應(yīng)每個(gè)特征層的每一個(gè)點(diǎn),同時(shí)x,y對(duì)應(yīng)每個(gè)框的中心。
3、生成每個(gè)特征層的每個(gè)點(diǎn)對(duì)應(yīng)的num_anchors大小相同的h和w,即4、6、6、6、4、4,這里的h和w對(duì)應(yīng)著每一個(gè)點(diǎn)對(duì)應(yīng)的num_anchors個(gè)框中的h和w。
4、將h和w每個(gè)賦值,h[0]對(duì)應(yīng)比較小的正方形,h[1]對(duì)應(yīng)比較大的正方形,h[2]和h[3]對(duì)應(yīng)√2下不同的長(zhǎng)方形,h[4]和h[5]對(duì)應(yīng)√3下不同的長(zhǎng)方形。
輸出的參數(shù)包括:
X和Y的shape為(block.shape[0],block.shape[1],1)
H和w的shape為(boxes_len)
具體的執(zhí)行代碼如下:
############################################################ # 該部分供ssd_anchors_all_layers函數(shù)調(diào)用 # # 用于獲取單層的先驗(yàn)框返回y,x,h,w # ############################################################ def ssd_anchor_one_layer(img_shape, feat_shape, sizes, ratios, step, offset=0.5, dtype=np.float32): """ 輸入:圖像大小img_shape,特征層大小feat_shape,先驗(yàn)框大小sizes, 先驗(yàn)框長(zhǎng)寬比率sizes,先驗(yàn)框放大倍數(shù)step。 執(zhí)行過(guò)程: 生成x、y的網(wǎng)格。 將x和y歸一化到0到1之間。 生成每個(gè)特征層的每個(gè)點(diǎn)對(duì)應(yīng)的boxes_len大小相同的h和w,即4、6、6、6、4、4。 將h和w每個(gè)賦值,h[0]對(duì)應(yīng)比較小的正方形,h[1]對(duì)應(yīng)比較大的正方形, h[2]和h[3]對(duì)應(yīng)√2下不同的長(zhǎng)方形,h[4]和h[5]對(duì)應(yīng)√3下不同的長(zhǎng)方形。 輸出: X和Y的shape為(block.shape[0],block.shape[1],1) H和w的shape為(boxes_len) """ # 網(wǎng)格化 y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]] # 歸一化 y = (y.astype(dtype) + offset) * step / img_shape[0] x = (x.astype(dtype) + offset) * step / img_shape[1] # 拓充維度,便于后面decode計(jì)算 y = np.expand_dims(y, axis=-1) x = np.expand_dims(x, axis=-1) # 每一個(gè)點(diǎn)框框的數(shù)量 num_anchors = len(sizes) + len(ratios) h = np.zeros((num_anchors, ), dtype=dtype) w = np.zeros((num_anchors, ), dtype=dtype) # 第一個(gè)第二個(gè)框框是正方形 h[0] = sizes[0] / img_shape[0] w[0] = sizes[0] / img_shape[1] di = 1 if len(sizes) > 1: h[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[0] w[1] = math.sqrt(sizes[0] * sizes[1]) / img_shape[1] di += 1 for i, r in enumerate(ratios): h[i+di] = sizes[0] / img_shape[0] / math.sqrt(r) w[i+di] = sizes[0] / img_shape[1] * math.sqrt(r) return y, x, h, w
在看該部分的時(shí)候,需要結(jié)合參數(shù),所用參數(shù)如下:
img_shape=(300, 300) feat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)], # 先驗(yàn)框的size anchor_sizes=[(21., 45.), (45., 99.), (99., 153.), (153., 207.), (207., 261.), (261., 315.)], # 框的數(shù)量為4,6,6,6,4,4 # 框的數(shù)量為2+len(anchor_ratios[i]) anchor_ratios=[[2, .5], [2, .5, 3, 1./3], [2, .5, 3, 1./3], [2, .5, 3, 1./3], [2, .5], [2, .5]], # 放大倍數(shù) anchor_steps=[8, 16, 32, 64, 100, 300],
仔細(xì)研讀這段代碼會(huì)發(fā)現(xiàn)其設(shè)計(jì)非常巧妙哈。
x和y會(huì)執(zhí)行歸一化,到0,1之間,如果除去xy的最后一維進(jìn)行plot,其會(huì)呈現(xiàn)一個(gè)0到1的網(wǎng)格,以38x38的特征層的先驗(yàn)框?yàn)槔?,其繪制出的網(wǎng)格如下,其實(shí)每一個(gè)點(diǎn)對(duì)應(yīng)的就是每個(gè)框的中心點(diǎn)。
h和w對(duì)應(yīng)著每個(gè)框的高與寬,寬高成一定比例。
4、bboxes_decode框的解碼
# =============================解碼部分============================= # def bboxes_decode(self, feat_localizations, anchors, scope='ssd_bboxes_decode'): """ 進(jìn)行解碼操作 """ return ssd_common.tf_ssd_bboxes_decode( feat_localizations, anchors, prior_scaling=self.params.prior_scaling, scope=scope)
在bboxes_decode函數(shù)中,其調(diào)用了一個(gè)外部的函數(shù)ssd_common.tf_ssd_bboxes_decode,用于構(gòu)建框的解碼,其位于其它的文件中。
執(zhí)行框的解碼的原因是,利用net網(wǎng)絡(luò)預(yù)測(cè)得到的locations并不是實(shí)際的框的位置,其需要與先驗(yàn)框結(jié)合處理后才能得到最后的框的位置。
這里需要注意的是,decode的過(guò)程需要兩個(gè)參數(shù)的結(jié)合,分別是net網(wǎng)絡(luò)構(gòu)建得到的locations和anchor先驗(yàn)框生成得到的先驗(yàn)框。
在進(jìn)入ssd_common.tf_ssd_bboxes_decode函數(shù)后,其執(zhí)行過(guò)程與anchor先驗(yàn)框生成類似,內(nèi)部也有一個(gè)循環(huán),意味著要對(duì)每一個(gè)特征層進(jìn)行單獨(dú)的處理。
def tf_ssd_bboxes_decode(feat_localizations, anchors, prior_scaling=[0.1, 0.1, 0.2, 0.2], scope='ssd_bboxes_decode'): """ 從ssd網(wǎng)絡(luò)特性和先驗(yàn)框框計(jì)算相對(duì)邊界框。 """ with tf.name_scope(scope): bboxes = [] for i, anchors_layer in enumerate(anchors): bboxes.append( tf_ssd_bboxes_decode_layer(feat_localizations[i], anchors_layer, prior_scaling)) return bboxes
在如上的執(zhí)行過(guò)程中,內(nèi)部存在一個(gè)tf_ssd_bboxes_decode_layer函數(shù),該部分是先驗(yàn)框生成的核心,在tf_ssd_bboxes_decode_layer中,程序會(huì)對(duì)每一個(gè)特征層的框進(jìn)行解碼。
其輸入包括,一個(gè)特征層的框的預(yù)測(cè)定位feat_localizations,每一層的先驗(yàn)框anchors_layer,先驗(yàn)框比率prior_scaling
執(zhí)行過(guò)程:
1、 分解anchors_layer,因?yàn)閍nchors_layer由多個(gè)y,x,h,w構(gòu)成
2、 計(jì)算cx和cy,這里存在一個(gè)計(jì)算公式,公式論文中給出了。
3、 計(jì)算cw和ch,這里存在一個(gè)計(jì)算公式,公式論文中給出了。
4、 將[cy - ch / 2.0, cx - cw / 2.0, cy + ch / 2.0, cx + cw / 2.0]輸出,其對(duì)應(yīng)左上角角點(diǎn)和右下角角點(diǎn)。
其輸出包括:左上角角點(diǎn)和右下角角點(diǎn)的集合bboxes。
bboxes的shape為(?,block.shape[0],block.shape[1], boxes_len,4)
具體執(zhí)行代碼如下:
# =========================================================================== # # 編碼解碼部分 # =========================================================================== # def tf_ssd_bboxes_decode_layer(feat_localizations, anchors_layer, prior_scaling=[0.1, 0.1, 0.2, 0.2]): """ 其輸入包括,一個(gè)特征層的框的預(yù)測(cè)定位feat_localizations,每一層的先驗(yàn)框anchors_layer,先驗(yàn)框比率prior_scaling 執(zhí)行過(guò)程: 1、 分解anchors_layer,因?yàn)閍nchors_layer由多個(gè)y,x,h,w構(gòu)成 2、 計(jì)算cx和cy,這里存在一個(gè)計(jì)算公式 3、 計(jì)算cw和ch,這里存在一個(gè)計(jì)算公式 4、 將[cy - ch / 2.0, cx - cw / 2.0, cy + ch / 2.0, cx + cw / 2.0]輸出,其對(duì)應(yīng)左上角角點(diǎn)和右下角角點(diǎn)。 其輸出包括:左上角角點(diǎn)和右下角角點(diǎn)的集合bboxes。 bboxes的shape為(?,block.shape[0],block.shape[1], boxes_len,4) """ yref, xref, href, wref = anchors_layer # 計(jì)算中心點(diǎn)和它的寬長(zhǎng) cx = feat_localizations[:, :, :, :, 0] * wref * prior_scaling[0] + xref cy = feat_localizations[:, :, :, :, 1] * href * prior_scaling[1] + yref w = wref * tf.exp(feat_localizations[:, :, :, :, 2] * prior_scaling[2]) h = href * tf.exp(feat_localizations[:, :, :, :, 3] * prior_scaling[3]) # 計(jì)算左上角點(diǎn)和右下角點(diǎn) ymin = cy - h / 2. xmin = cx - w / 2. ymax = cy + h / 2. xmax = cx + w / 2. bboxes = tf.stack([ymin, xmin, ymax, xmax], axis=-1) return bboxes
解碼完后的bboxes表示某一個(gè)特征層中的框在真實(shí)圖像中的位置。
利用ssd_vgg_300進(jìn)行預(yù)測(cè)
預(yù)測(cè)步驟
進(jìn)行預(yù)測(cè)需要進(jìn)行以下步驟:
1、建立ssd對(duì)象
2、利用ssd_net = ssd_vgg_300.SSDNet()獲得網(wǎng)絡(luò),得到兩個(gè)tensorflow格式的預(yù)測(cè)結(jié)果。
3、載入ssd模型。
4、讀入圖片image_names。
5、將圖片預(yù)處理后,傳入網(wǎng)絡(luò)結(jié)構(gòu),獲得預(yù)測(cè)結(jié)果,預(yù)測(cè)結(jié)果包括 框的位置、每個(gè)框的預(yù)測(cè)結(jié)果。
6、利用ssd_bboxes_select函數(shù)選擇得分高于門(mén)限的框。
7、對(duì)所有的得分進(jìn)行排序,取出得分top400的框
8、非極大值抑制,該部分用于去除重復(fù)率過(guò)高的框。
9、在原圖中繪制框框。
具體預(yù)測(cè)過(guò)程
1、圖片的預(yù)處理
圖片預(yù)處理時(shí),需要調(diào)用如下代碼:
# 輸入圖片大小 net_shape = (300, 300) # data_format 設(shè)置為 "NHWC" 時(shí),排列順序?yàn)?[batch, height, width, channels] # 具體使用方法可以查看該網(wǎng)址:https://www.jianshu.com/p/d8a699745529 data_format = 'NHWC' # img_input的placeholder img_input = tf.placeholder(tf.uint8, shape = (None, None, 3)) # 對(duì)圖片進(jìn)行預(yù)處理,得到bbox_img和image_4d image_pre, labels_pre, bboxes_pre, bbox_img = ssd_vgg_preprocessing.preprocess_for_eval( img_input, None, None, net_shape, data_format, resize = ssd_vgg_preprocessing.Resize.WARP_RESIZE) # 由于只檢測(cè)一張圖片,所以需要在第一維添加一維度 image_4d = tf.expand_dims(image_pre, 0)
看起來(lái)代碼很長(zhǎng),特別是倒數(shù)第二段代碼,但是其實(shí)里面什么也沒(méi)有。
ssd_vgg_preprocessing.preprocess_for_eval的主要執(zhí)行過(guò)程就是:
1、將image減去voc2012得到的所有圖片的RGB平均值。
2、增加比例預(yù)處理框(這個(gè)的作用我不太懂,我覺(jué)得它的意思應(yīng)該就是這個(gè)圖片可能是一個(gè)大圖片里面截出的一小個(gè)圖片,需要對(duì)這個(gè)比例進(jìn)行縮放,但是實(shí)際運(yùn)用的時(shí)候應(yīng)該就是一個(gè)大圖片)。
3、將圖片resize到300x300。
4、判斷使用CPU還是GPU。
def preprocess_for_eval(image, labels, bboxes, out_shape=EVAL_SIZE, data_format='NHWC', difficults=None, resize=Resize.WARP_RESIZE, scope='ssd_preprocessing_train'): """ 預(yù)處理 """ with tf.name_scope(scope): if image.get_shape().ndims != 3: raise ValueError('Input must be of size [height, width, C>0]') # 將image減去voc2012得到的所有圖片的RGB平均值 image = tf.to_float(image) image = tf_image_whitened(image, [_R_MEAN, _G_MEAN, _B_MEAN]) # 增加比例預(yù)處理框 bbox_img = tf.constant([[0., 0., 1., 1.]]) if bboxes is None: bboxes = bbox_img else: bboxes = tf.concat([bbox_img, bboxes], axis=0) # 這一大段其實(shí)只調(diào)用了最后一個(gè)elif # 將圖片resize到300x300 if resize == Resize.NONE: # No resizing... pass elif resize == Resize.CENTRAL_CROP: # Central cropping of the image. image, bboxes = tf_image.resize_image_bboxes_with_crop_or_pad( image, bboxes, out_shape[0], out_shape[1]) elif resize == Resize.PAD_AND_RESIZE: # Resize image first: find the correct factor... shape = tf.shape(image) factor = tf.minimum(tf.to_double(1.0), tf.minimum(tf.to_double(out_shape[0] / shape[0]), tf.to_double(out_shape[1] / shape[1]))) resize_shape = factor * tf.to_double(shape[0:2]) resize_shape = tf.cast(tf.floor(resize_shape), tf.int32) image = tf_image.resize_image(image, resize_shape, method=tf.image.ResizeMethod.BILINEAR, align_corners=False) # Pad to expected size. image, bboxes = tf_image.resize_image_bboxes_with_crop_or_pad( image, bboxes, out_shape[0], out_shape[1]) elif resize == Resize.WARP_RESIZE: # Warp resize of the image. image = tf_image.resize_image(image, out_shape, method=tf.image.ResizeMethod.BILINEAR, align_corners=False) # 分割比例box bbox_img = bboxes[0] bboxes = bboxes[1:] # ……不知道干嘛 if difficults is not None: mask = tf.logical_not(tf.cast(difficults, tf.bool)) labels = tf.boolean_mask(labels, mask) bboxes = tf.boolean_mask(bboxes, mask) # 看使用cpu還是GPU if data_format == 'NCHW': image = tf.transpose(image, perm=(2, 0, 1)) return image, labels, bboxes, bbox_img
2、載入ssd模型
載入ssd模型分為以下幾步:
1、建立Session會(huì)話
2、建立ssd網(wǎng)絡(luò)
3、載入模型
執(zhí)行代碼如下:
# 載入ssd的模型 # 建立Session() isess = tf.Session() reuse = True if 'ssd_net' in locals() else None # 建立網(wǎng)絡(luò) ssd_net = ssd_vgg_300.SSDNet() with slim.arg_scope(ssd_net.arg_scope(data_format = data_format)): predictions, localisations, _, _ = ssd_net.net(image_4d, is_training = False, reuse = reuse) # 載入模型 ckpt_filename = 'D:/Collection/SSD-Tensorflow-master/logs/model.ckpt-18602' isess.run(tf.global_variables_initializer()) saver = tf.train.Saver() saver.restore(isess, ckpt_filename)
3、讀取圖片進(jìn)行預(yù)測(cè)
該部分需要進(jìn)行如下操作:
1、獲取先驗(yàn)框。
2、讀取圖片。
3、將圖片放入已經(jīng)完成載入的模型,得到predictions和locations。
4、將每個(gè)特征層的預(yù)測(cè)結(jié)果都進(jìn)行篩選,得分小于threshold的都剔除,并使得所有特征層的預(yù)測(cè)結(jié)果都并排存入一個(gè)list。
5、對(duì)所有的預(yù)測(cè)結(jié)果進(jìn)行得分的排序,取出top400的框框。
6、進(jìn)行非極大抑制,取出重復(fù)率過(guò)高的框。
7、在原圖中繪制框。
具體執(zhí)行代碼如下:
# 獲得所有先驗(yàn)框,六個(gè)特征層的 ssd_anchors = ssd_net.anchors(net_shape) def process_image(img, select_threshold = 0.5, nms_threshold = .45, net_shape = (300, 300)): # 運(yùn)行SSD模型 rimg, rpredictions, rlocalisations, rbbox_img = isess.run([image_4d, predictions, localisations, bbox_img], feed_dict = {img_input: img}) # 得到20個(gè)類的得分,框框的位置 rclasses, rscores, rbboxes = np_methods.ssd_bboxes_select( rpredictions, rlocalisations, ssd_anchors, select_threshold = select_threshold, img_shape = net_shape, num_classes = 21, decode = True) # 防止超出邊界 rbboxes = np_methods.bboxes_clip(rbbox_img, rbboxes) # 取出top400,并通過(guò)極大值抑制除去類似框 rclasses, rscores, rbboxes = np_methods.bboxes_sort(rclasses, rscores, rbboxes, top_k = 400) rclasses, rscores, rbboxes = np_methods.bboxes_nms(rclasses, rscores, rbboxes, nms_threshold = nms_threshold) # 在img里進(jìn)行等比例縮放resize rbboxes = np_methods.bboxes_resize(rbbox_img, rbboxes) return rclasses, rscores, rbboxes # 讀取圖片 img = mpimg.imread('./street.jpg') # 進(jìn)行圖片的預(yù)測(cè) rclasses, rscores, rbboxes = process_image(img) visualization.plt_bboxes(img, rclasses, rscores, rbboxes)
其中,預(yù)測(cè)結(jié)果篩選的代碼如下:
- 該部分首先解碼;
- 再將每個(gè)特征層進(jìn)行reshape完成平鋪;
- 讀出除去背景的得分;
- 將得分多余threshold的類進(jìn)行保存,小于的進(jìn)行剔除;
- 利用np.concatenate將結(jié)果同一排擺放。
def ssd_bboxes_select_layer(predictions_layer, localizations_layer, anchors_layer, select_threshold=0.5, img_shape=(300, 300), num_classes=21, decode=True): """ 選擇大于門(mén)限的框 """ # 對(duì)框進(jìn)行解碼 if decode: localizations_layer = ssd_bboxes_decode(localizations_layer, anchors_layer) # 將所有預(yù)測(cè)結(jié)果變?yōu)?維,第一維度維batch,第二維度為size,第三維度為class_num | 4 p_shape = predictions_layer.shape batch_size = p_shape[0] if len(p_shape) == 5 else 1 predictions_layer = np.reshape(predictions_layer, (batch_size, -1, p_shape[-1])) l_shape = localizations_layer.shape localizations_layer = np.reshape(localizations_layer, (batch_size, -1, l_shape[-1])) if select_threshold is None or select_threshold == 0: classes = np.argmax(predictions_layer, axis=2) scores = np.amax(predictions_layer, axis=2) mask = (classes > 0) classes = classes[mask] scores = scores[mask] bboxes = localizations_layer[mask] else: # 取出所有的預(yù)測(cè)結(jié)果 sub_predictions = predictions_layer[:, :, 1:] # 判斷哪里的預(yù)測(cè)結(jié)果大于門(mén)限 idxes = np.where(sub_predictions > select_threshold) # 如果大于門(mén)限則留下,并+1,除去背景 classes = idxes[-1]+1 # 取出所有分?jǐn)?shù) scores = sub_predictions[idxes] # 和框的位置 bboxes = localizations_layer[idxes[:-1]] return classes, scores, bboxes
對(duì)所有的預(yù)測(cè)結(jié)果進(jìn)行得分的排序,取出top400的框框的過(guò)程非常簡(jiǎn)單,代碼如下:
首先利用argsort對(duì)得分進(jìn)行排序,并從大到小排序得分的序號(hào);
取出種類classes、得分scores、框bboxes的top400個(gè)。
def bboxes_sort(classes, scores, bboxes, top_k=400): """ 進(jìn)行排序篩選 """ idxes = np.argsort(-scores) classes = classes[idxes][:top_k] scores = scores[idxes][:top_k] bboxes = bboxes[idxes][:top_k] return classes, scores, bboxes
進(jìn)行非極大抑制的過(guò)程也比較簡(jiǎn)單,具體代碼如下:
將bboxes中每一個(gè)框,從得分最高到得分最低依次與其之后所有的框比較;
IOU較小或者屬于不同類的框得到保留;
def bboxes_nms(classes, scores, bboxes, nms_threshold=0.45): """ 非極大抑制,去除重復(fù)率過(guò)大的框. """ keep_bboxes = np.ones(scores.shape, dtype=np.bool) for i in range(scores.size-1): if keep_bboxes[i]: # 計(jì)算重疊區(qū)域 overlap = bboxes_jaccard(bboxes[i], bboxes[(i+1):]) # 保留重疊區(qū)域不是很大或者種類不同的 keep_overlap = np.logical_or(overlap < nms_threshold, classes[(i+1):] != classes[i]) keep_bboxes[(i+1):] = np.logical_and(keep_bboxes[(i+1):], keep_overlap) # 保留重疊部分小或者種類不同的 idxes = np.where(keep_bboxes) return classes[idxes], scores[idxes], bboxes[idxes]
4、全部預(yù)測(cè)代碼
import os import math import random import numpy as np import tensorflow as tf import cv2 import matplotlib.pyplot as plt import matplotlib.image as mpimg import sys sys.path.append('./') from nets import ssd_vgg_300, ssd_common, np_methods from preprocessing import ssd_vgg_preprocessing from notebooks import visualization # 構(gòu)建slim框架。 slim = tf.contrib.slim # 輸入圖片大小 net_shape = (300, 300) # data_format 設(shè)置為 "NHWC" 時(shí),排列順序?yàn)?[batch, height, width, channels] # 具體使用方法可以查看:https://www.jianshu.com/p/d8a699745529。 data_format = 'NHWC' # img_input的placeholder img_input = tf.placeholder(tf.uint8, shape = (None, None, 3)) # 對(duì)圖片進(jìn)行預(yù)處理,得到bbox_img和image_4d image_pre, labels_pre, bboxes_pre, bbox_img = ssd_vgg_preprocessing.preprocess_for_eval( img_input, None, None, net_shape, data_format, resize = ssd_vgg_preprocessing.Resize.WARP_RESIZE) # 由于只檢測(cè)一張圖片,所以需要在第一維添加一維度 image_4d = tf.expand_dims(image_pre, 0) # 載入ssd的模型 # 建立Session() isess = tf.Session() reuse = True if 'ssd_net' in locals() else None # 建立網(wǎng)絡(luò) ssd_net = ssd_vgg_300.SSDNet() with slim.arg_scope(ssd_net.arg_scope(data_format = data_format)): predictions, localisations, _, _ = ssd_net.net(image_4d, is_training = False, reuse = reuse) # 載入模型 ckpt_filename = './logs/model.ckpt-1498' isess.run(tf.global_variables_initializer()) saver = tf.train.Saver() saver.restore(isess, ckpt_filename) # 獲得所有先驗(yàn)框,六個(gè)特征層的 ssd_anchors = ssd_net.anchors(net_shape) def process_image(img, select_threshold = 0.5, nms_threshold = .45, net_shape = (300, 300)): # 運(yùn)行SSD模型 rimg, rpredictions, rlocalisations, rbbox_img = isess.run([image_4d, predictions, localisations, bbox_img], feed_dict = {img_input: img}) # 得到20個(gè)類的得分,框框的位置 rclasses, rscores, rbboxes = np_methods.ssd_bboxes_select( rpredictions, rlocalisations, ssd_anchors, select_threshold = select_threshold, img_shape = net_shape, num_classes = 21, decode = True) # 防止超出邊界 rbboxes = np_methods.bboxes_clip(rbbox_img, rbboxes) # 取出top400,并通過(guò)極大值抑制除去類似框 rclasses, rscores, rbboxes = np_methods.bboxes_sort(rclasses, rscores, rbboxes, top_k = 400) rclasses, rscores, rbboxes = np_methods.bboxes_nms(rclasses, rscores, rbboxes, nms_threshold = nms_threshold) # 在img里進(jìn)行等比例縮放resize rbboxes = np_methods.bboxes_resize(rbbox_img, rbboxes) return rclasses, rscores, rbboxes # 讀取圖片 img = mpimg.imread('./street.jpg') # 進(jìn)行圖片的預(yù)測(cè) rclasses, rscores, rbboxes = process_image(img) visualization.plt_bboxes(img, rclasses, rscores, rbboxes)
以上就是python目標(biāo)檢測(cè)SSD算法預(yù)測(cè)部分源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于python目標(biāo)檢測(cè)SSD算法預(yù)測(cè)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python 中os模塊os.path.exists()的用法說(shuō)明
這篇文章主要介紹了python 中os模塊os.path.exists()的用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03python神經(jīng)網(wǎng)絡(luò)Keras?GhostNet模型的實(shí)現(xiàn)
這篇文章主要為大家介紹了python神經(jīng)網(wǎng)絡(luò)Keras?GhostNet模型的復(fù)現(xiàn)詳解,2022-05-05Python機(jī)器學(xué)習(xí)NLP自然語(yǔ)言處理基本操作之命名實(shí)例提取
自然語(yǔ)言處理(?Natural?Language?Processing,?NLP)是計(jì)算機(jī)科學(xué)領(lǐng)域與人工智能領(lǐng)域中的一個(gè)重要方向。它研究能實(shí)現(xiàn)人與計(jì)算機(jī)之間用自然語(yǔ)言進(jìn)行有效通信的各種理論和方法2021-11-11python實(shí)現(xiàn)決策樹(shù)、隨機(jī)森林的簡(jiǎn)單原理
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)決策樹(shù)、隨機(jī)森林的簡(jiǎn)單原理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03Python實(shí)現(xiàn)Restful API的例子
今天小編就為大家分享一篇Python實(shí)現(xiàn)Restful API的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08python使用for循環(huán)和海龜繪圖實(shí)現(xiàn)漂亮螺旋線
這篇文章主要為大家介紹了python使用for循環(huán)和海龜繪圖實(shí)現(xiàn)漂亮螺旋線實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06值得收藏,Python 開(kāi)發(fā)中的高級(jí)技巧
這篇文章主要介紹了Python 開(kāi)發(fā)中的高級(jí)技巧,非常不錯(cuò),具有收藏價(jià)值,感興趣的朋友一起看看吧2018-11-11python實(shí)現(xiàn)秒殺商品的微信自動(dòng)提醒功能(代碼詳解)
這篇文章主要介紹了python實(shí)現(xiàn)秒殺商品的微信自動(dòng)提醒功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04