MXNET深度學習框架-28-SSD(單發多框檢測器)目標檢測

        有關SSD的基本原理本文不做多餘的贅述,具體參考其他大神的博文,如:目標檢測之SSD

在這裏插入圖片描述
本章我們利用SSD來檢測圖片中的皮卡丘:
在這裏插入圖片描述
mxnet中已經有一個皮卡丘的檢測數據集,下面對它進行下載:

# 下載皮卡丘數據集
root_url = ('https://apache-mxnet.s3-accelerate.amazonaws.com/'
                'gluon/dataset/pikachu/')
dataset = {'train.rec': 'e6bcb6ffba1ac04ff8a9b1115e650af56ee969c8',
               'train.idx': 'dcf7318b2602c06428b9988470c731621716c393',
               'val.rec': 'd6c33f799b4d058e82f2cb5bd9a976f69d72d520'}
data_dir="pikaqiu_object/"
for k, v in dataset.items():
    gutils.download(root_url + k, data_dir+ k, sha1_hash=v)

當文件下載好之後,我們寫一段程序把它讀進來:

# 讀取數據
def load_data_pikachu(batch_size, edge_size=256):  # edge_size:輸出圖像的寬和高
    class_names=["pikaqiu"]
    num_class=len(class_names)
    train_iter = image.ImageDetIter(
        path_imgrec='pikaqiu_object/train.rec',
        path_imgidx='pikaqiu_object/train.idx',
        batch_size=batch_size,
        data_shape=(3, edge_size, edge_size),  # 輸出圖像的形狀
        shuffle=True,  # 以隨機順序讀取數據集
        rand_crop=1,  # 隨機裁剪的概率爲1
        min_object_covered=0.95, max_attempts=200)
    val_iter = image.ImageDetIter(
        path_imgrec='pikaqiu_object/val.rec', batch_size=batch_size,
        data_shape=(3, edge_size, edge_size), shuffle=False)
    return train_iter, val_iter,class_names,num_class

之後把它打印出來看看:

batch_size=32
edge_size=256
train_data,test_data,class_names,num_class=load_data_pikachu(batch_size,edge_size)
batch=train_data.next()
print(batch)

結果:
在這裏插入圖片描述
        可以看到,圖像數據依舊與之前分類數據一樣,是一個batch×channel×h×w的類型。但是label不再是一個單個數字了,而是一個多標籤數組,其中的32表示batch,1表示1張圖片裏面只有1個皮卡丘,5包含5個數字,第1個數字表示邊界框的類別,後面4個數字表示矩形框的座標(左上角x,y和右下角x,y座標)。

顯示一下圖片看看:

def box_to_rect(box,color):
    box=box.asnumpy()
    return plt.Rectangle((box[0],box[1]),box[2]-box[0],box[3]-box[1],
                         fill=False,edgecolor=color,linewidth=3)
_,figs=plt.subplots(3,3,figsize=(6,6))
for i in range(3):
    for j in range(3):
        img,labels=batch.data[0][3*i+j],batch.label[0][3*i+j]
        img=img.transpose((1,2,0))
        figs[i][j].imshow(img.clip(0,255).asnumpy()/255)
        for label in labels:
            rect=box_to_rect(label[1:5]*edge_size,"red")
            figs[i][j].add_patch(rect)
        figs[i][j].axes.get_xaxis().set_visible(False)
        figs[i][j].axes.get_yaxis().set_visible(False)
plt.show()

結果:
在這裏插入圖片描述

SSD模型

1、錨框:默認的邊界框
        因爲邊框可以出現在圖片中的任意位置,並且可以有任意大小,爲了簡化計算,SSD與Faster RCNN一樣使用一些默認的邊界框,或者稱之爲錨框(anchor box),作爲搜索起點。總的來說,對於輸入的每個像素點,以其爲中心,採樣數個不同形狀,不太大小的邊界框,假設圖片輸入大小爲w×hw×h,則:
        1)給定大小s(0,1]s\in(0,1],那麼生成的邊界框形狀爲ws×hsws×hs
        2)給定比例(長寬比)r>0r>0,那麼生成的邊界框形狀爲wr×hrw\sqrt{r}×\frac{h}{\sqrt{r}}(爲什麼這樣定義?因爲這樣計算的話框的面積是不變的)。

在採樣的時候我們提供nn個大小(sizes)和mm個比例(ratios),爲了計算簡單,這裏不生成mnmn個錨框,而是n+m1n+m-1個。其中第ii個錨框使用:
        1)sizes[i][i]和ratios[0][0],ini\leq n
        2)sizes[0][0]和ratios[in][i-n]i>ni>n

接下來演示在mxnet中如何生成錨框:

from mxnet import contrib
# (輸入x)NCHW
x=nd.random_normal(shape=(1,3,40,40))
y=contrib.nd.MultiBoxPrior(x,sizes=[0.75,0.5,0.1],ratios=[1,2,0.5])# 3+3-1=5,共有5種
print(y.shape)

結果:
在這裏插入圖片描述

        從結果中我們可以看到錨框y的形狀爲[1,8000,4],它分別表示爲[批量大小,錨框個數(40×40×5),錨框的4個座標值],其中 4個座標值分別已除以圖像的寬和高,因此值域均爲0和1之間。

比如,隨即找個點(20,20),接下來畫出以(20,20)爲中心的所有錨框[x_min,y_min,x_max,y_max]:

print(boxes[20,20,0,:]) # 以20,20爲中心的第一個框

# 畫出以(20,20)爲中心的所有錨框[x_min,y_min,x_max,y_max]
colors=['blue','red','green','black','magenta'] # 總共5個錨框


anchors=boxes[20,20,:,:]
image=nd.ones(shape=(40,40,3)).asnumpy()
plt.imshow(image)
for i in range(anchors.shape[0]):
    plt.gca().add_patch(box_to_rect(anchors[i,:]*40,colors[i])) # 40是原圖像的寬高
plt.show()

結果:
在這裏插入圖片描述
在這裏插入圖片描述
        可以看到,以像素點座標爲(20,20)的周圍生成了5個大小和形狀不同的錨框(所有像素點周圍都會生成5個錨框)。

2、預測物體類別
        (1)對於每一個錨框,我們需要預測它是不是包含了我們所需要的物體,還是它只是一個背景。這裏我們使用一個3X3的卷積來做預測,設置pad=1(邊緣填充),使得輸入與輸出的大小一樣。同時,輸出的通道數爲num_anchors×(num_classes+1)(這裏的“1”是背景)。每個通道對應一個錨框對某類的置信度。比如輸出是y,那麼對應輸入中第n個樣本的第(i,j)像素的置信度值在y[n,:,i,j]中。具體來說,對於以[i,j]爲中心的第a個錨框:
        1)通道a*[num_class+1]是其只包含背景的分數;
        2)通道a*[num_class+1]+b+1是包含第b個物體的分數。

# 定義類別分類器
def class_predict(num_anchors,num_classes):
    return gn.nn.Conv2D(num_anchors*(num_classes+1),3,padding=1)
cls_pre=class_predict(5,10)
cls_pre.initialize()
k=nd.random_normal(shape=(2,3,10,10))
print(cls_pre(k).shape)

結果:
在這裏插入圖片描述
3、預測邊界框
        因爲真實的邊界框可以是任何形狀,我們需要預測如何從一個錨框變成真實的邊界框,這個變換可以從一個長爲4的向量來描述。同上一樣,我們用一個有num_anchors×4通道的卷積。假設輸出是y,那麼對應輸入中第n個樣本的第(i,j)像素爲中心的錨框的轉換在y[n,:,i,j]中。具體來說,對於第a個錨框,它的變換在a×4到a×4+3裏。

def box_predict(num_anchors):
    return gn.nn.Conv2D(num_anchors *4, 3, padding=1)

box_pre=box_predict(10)
box_pre.initialize()
print(box_pre(k).shape)

結果:
在這裏插入圖片描述
4、減半模塊
        爲了進一步提升檢測精度,單單經過一個卷積塊往往是不夠的,所以需要再進行幾個卷積塊的抽取,更高層次的去檢測相關物體,同時,使用池化方法使得圖像長寬減半。

def sample_dowm(num_filters):
    out=gn.nn.Sequential()
    for _ in range(2):
        out.add(gn.nn.Conv2D(num_filters,3,1,1))
        out.add(gn.nn.BatchNorm(in_channels=num_filters))
        out.add(gn.nn.Activation("relu"))
    out.add(gn.nn.MaxPool2D(2))
    return out
blk=sample_dowm(10)
blk.initialize()
m=nd.random_normal(shape=(2,3,8,8))
print(blk(m).shape)

結果:
在這裏插入圖片描述
5、合併來自不同層的預測輸出
        由於SSD會在多個層上做預測,每個層由於長寬、錨框不一樣,導致輸出的數據形狀會不一樣,這裏我們用物體類別預測作爲樣例,邊框預測也是類似的。

        我們首先定義一個特定大小的圖片,然後對它輸出類別進行預測,然後減半,再輸出預測類別。

input=nd.random_normal(shape=(2,8,20,20))
print("input:",input.shape)

cls_pred1=class_predict(5,10)
cls_pred1.initialize()
pred1=cls_pred1(input)
print("pre result 1:",pred1.shape)

ds=sample_dowm(16)
ds.initialize()
input=ds(input)
print("down input:",input.shape)

cls_pred2=class_predict(3,10)
cls_pred2.initialize()
pred2=cls_pred2(input)
print("pre result 2:",pred2.shape)

結果:
在這裏插入圖片描述
        因爲loss支持的是一個單值,而這裏輸出的結果是一個4D的矩陣,所以我們需要把4D轉爲2D。通過觀察我們可以發現:
在這裏插入圖片描述
        除了batch爲2以外,後面的值都可以變動,把它像全連接層輸入那樣,後面3個值乘起來不就好了,最後直接融合就完成了。

def flatten_prediction(pred):
    return pred.transpose(axes=(0,2,3,1)).flatten()
def concat_prediction(preds): # preds是一個list數組
    return nd.concat(*preds,dim=1)
# 把它們轉換並融合
print("==========")
y1=flatten_prediction(pred1)
print("Flatten class pre 1:",y1.shape)
y2=flatten_prediction(pred2)
print("Flatten class pre 2:",y2.shape)
y_all=concat_prediction([y1,y2])
print("concat result:",y_all.shape)

結果:
在這裏插入圖片描述
6、主體網絡

'''---主體網絡---'''
# 主體網絡就是第一個卷積塊,通常來說會用一個比較好的模型來抽取特徵(這裏隨便定義一個模型)
def body():
    out=gn.nn.HybridSequential()
    for nfilter in [16,32,64]:
        out.add(sample_dowm(num_filters=nfilter))
    return out
bnet=body()
bnet.initialize()
print(bnet(k).shape)

結果:
在這裏插入圖片描述
7、創建SSD
        前期工作都做好了,現在我們來創建一個簡單的SSD模型,這裏定義的模型包含4塊,主體網絡、三個減半模塊、五個物體類別和邊框預測模塊。其中,預測分別應用在主體網絡、三個減半模塊和最後的全局平均池化上。

# 簡單的SSD模型
def SSD_Model(num_anchors,num_classes):
    downsamples=gn.nn.HybridSequential()
    for _ in range(3):
        downsamples.add(sample_dowm(128))
    class_predicts=gn.nn.HybridSequential()
    box_predicts=gn.nn.HybridSequential()
    for _ in range(5):
        class_predicts.add(class_predict(num_anchors,num_classes))
        box_predicts.add(box_predict(num_anchors))
    model=gn.nn.HybridSequential()
    model.add(body(),downsamples,class_predicts,box_predicts)
    return model
# 計算預測
def ssd_forward(x,model,sizes,ratios):
    body,downsamples,class_predicts,box_predicts=model
    anchors,class_preds,box_preds=[],[],[]
    x=body(x)
    for i in range(5):
        anchors.append(contrib.nd.MultiBoxPrior(x,sizes=sizes[i],ratios=ratios[i]))
        class_preds.append(flatten_prediction(class_predicts[i](x)))
        box_preds.append(flatten_prediction(box_predicts[i](x)))
        print("predict scale",i,x.shape,"with",anchors[-1].shape[1],"anchors")
        if i<3:
            x=downsamples[i](x)
        elif i==3:
            x=nd.Pooling(x,global_pool=True,pool_type="max",
                         kernel=(x.shape[2],x.shape[3]))
    # 融合結果
    return (concat_prediction(anchors),concat_prediction(class_preds),
            concat_prediction(box_preds))
'''---完整的模型---'''
class ToySSD(gn.Block):
    def __init__(self,num_classes,**kwargs):
        super(ToySSD, self).__init__(**kwargs)
        self.sizes=[[0.2,0.272],[0.37,0.447],[0.54,0.618],[0.7,0.8],[0.88,0.9]]
        self.ratios=[[1,2,0.5]]*5
        self.num_classes=num_classes
        num_anchors=len(self.sizes[0]+self.ratios[0])-1
        with self.name_scope():
            self.model=SSD_Model(num_anchors,num_classes)
    def forward(self, x):
        anchors,class_preds,box_preds=ssd_forward(x,self.model,self.sizes,
                                                  self.num_classes)
        class_preds=class_preds.reshape(shape=(0,-1,self.num_classes+1)) #0表示不管有多少個batch_size,copy過來
        return anchors,class_preds,box_preds

看一下圖片的形狀是如何變化的:

print("====================")
net=ToySSD(num_classes=2)
net.initialize()
x=batch.data[0][0:1]
print("x:",x.shape)
anchors_,class_pre,box_pre=net(x)
print("output anchors:",anchors_.shape)
print("output class_pre:",class_pre.shape)
print("output box_pre:",box_pre.shape)

結果:
在這裏插入圖片描述

8、損失函數
        在迴歸問題上,我們經常用的兩類損失函數分別爲L1和L2,但由於L2對較大誤差的懲罰力度過大,所以魯棒性較差。由於目標檢測涉及到分類和迴歸,本章則使用交叉熵和L1損失來進行實驗。
Smooth L1損失——迴歸預測
        雖說L1損失魯棒性更好,但它有一個很明顯的缺陷:它在原點處是斷的(不光滑),所以導致它在原點有無數個導數,針對這個現象,Smooth L1損失出現了,它是光滑後的L1,什麼意思?看下圖:
在這裏插入圖片描述
        從圖中可以看到,橘色或綠色的函數,它們在原點處是一段光滑的曲線,且導數只有一個,對於Smooth L1函數,有:
f(n)={(σx)2/2,if x<1/σ2 x0.5,otherwise f(n) = \begin{cases} (\sigma x)^2/2, & \text{if $|x|<1/\sigma^2$ } \\ |x|-0.5, & \text{otherwise} \end{cases}
代碼:

class smoothLoss(gn.loss.Loss): # box reg loss
    def __init__(self,batch_axis=0,**kwargs):
        super(smoothLoss, self).__init__(None,batch_axis,**kwargs)
    def hybrid_forward(self, F,output,label, mask, *args, **kwargs):
        loss=F.smooth_l1((output-label)*mask,scalar=1.0) # 參數mask,用來屏蔽不需要被懲罰的負例樣本
        return loss.mean(self._batch_axis,exclude=True)

交叉熵損失函數
        對於分類,本文依舊使用交叉熵損失函數。
9、準確率計算

# 我們可以沿用準確率評價分類結果。因爲使用了 L1 範數損失,我們用平均絕對誤差評價邊界框的預測結果。
def cls_eval(cls_preds, cls_labels):
    # 由於類別預測結果放在最後一維,argmax需要指定最後一維
    return (cls_preds.argmax(axis=-1) == cls_labels).sum().asscalar()

def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
    return ((bbox_labels - bbox_preds) * bbox_masks).abs().sum().asscalar()

10、loss計算

def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    cls = cls_loss(cls_preds, cls_labels)
    bbox = bbox_loss(bbox_preds , bbox_labels, bbox_masks)
    return 0.1*cls + bbox # 對分類做了加權

11、訓練

for epoch in range(100):
    acc_sum, mae_sum, n, m = 0.0, 0.0, 0, 0
    train_data.reset()  # 從頭讀取數據
    start = time.time()
    for batch in train_data:
        X = batch.data[0].as_in_context(ctx)
        Y = batch.label[0].as_in_context(ctx)
        with ag.record():
            # 生成多尺度的錨框,爲每個錨框預測類別和偏移量
            anchors, cls_preds, bbox_preds = net(X)
            # 爲每個錨框標註類別和偏移量
            bbox_labels, bbox_masks, cls_labels = contrib.nd.MultiBoxTarget(
                anchors, Y, cls_preds.transpose((0, 2, 1)))
            # 根據類別和偏移量的預測和標註值計算損失函數
            l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
                          bbox_masks)
        l.backward()
        trainer.step(batch_size)
        acc_sum += cls_eval(cls_preds, cls_labels)
        n += cls_labels.size
        mae_sum += bbox_eval(bbox_preds, bbox_labels, bbox_masks)
        m += bbox_labels.size

    if (epoch + 1) % 10 == 0:
        print('epoch %2d, class acc %f, bbox mae %f, time %.1f sec' % (
            epoch + 1, acc_sum / n, mae_sum / m, time.time() - start))

12、檢測

img = im.imread('F:/test/pikaqiu.jpg')
feature = im.imresize(img, 256, 256).astype('float32')
X = feature.transpose((2, 0, 1)).expand_dims(axis=0)
src=cv.imread("F:/test/pikaqiu.jpg")

net.load_parameters("SSD.params")


def predict(X):
    anchors, cls_preds, bbox_preds = net(X.as_in_context(ctx))
    cls_probs = cls_preds.softmax().transpose((0, 2, 1))
    output = contrib.nd.MultiBoxDetection(cls_probs, bbox_preds, anchors)
    idx = [i for i, row in enumerate(output[0]) if row[0].asscalar() != -1]
    return output[0, idx]

output = predict(X)

def display(img, output, threshold):
    for row in output:
        score = row[1].asscalar()
        if score < threshold:
            continue
        h, w = img.shape[0:2]
        bbox = row[2:6].asnumpy()

        cv.rectangle(src,(int(bbox[0]*w),int(bbox[1]*h)),(int(bbox[2]*w),int(bbox[3]*h)),(0,0,255),2)
        cv.putText(src,str(score),(int(bbox[0]*w),int(bbox[1]*h-10)),
                   cv.FONT_HERSHEY_PLAIN,1.0,(0,255,0),2)
    cv.imshow(" ",src)
    cv.waitKey(0)
display(img, output, threshold=0.7)

檢測結果:
在這裏插入圖片描述

放上所有代碼:

import mxnet as mx
import mxnet.ndarray as nd
import mxnet.autograd as ag
import mxnet.gluon as gn
import mxnet.initializer as init
import numpy as np
import zipfile
import matplotlib.pyplot as plt
import cv2 as cv
from mxnet import contrib
import mxnet.image as im
import time

from mxnet.gluon import utils as gutils
# 下載皮卡丘數據集
# root_url = ('https://apache-mxnet.s3-accelerate.amazonaws.com/'
#                 'gluon/dataset/pikachu/')
# dataset = {'train.rec': 'e6bcb6ffba1ac04ff8a9b1115e650af56ee969c8',
#                'train.idx': 'dcf7318b2602c06428b9988470c731621716c393',
#                'val.rec': 'd6c33f799b4d058e82f2cb5bd9a976f69d72d520'}
# data_dir="pikaqiu_object/"
# for k, v in dataset.items():
#     gutils.download(root_url + k, data_dir+ k, sha1_hash=v)

# 讀取數據
def load_data_pikachu(batch_size, edge_size=256):  # edge_size:輸出圖像的寬和高
    class_names=["pikaqiu"]
    num_class=len(class_names)
    train_iter = im.ImageDetIter(
        path_imgrec='pikaqiu_object/train.rec',
        path_imgidx='pikaqiu_object/train.idx',
        batch_size=batch_size,
        data_shape=(3, edge_size, edge_size),  # 輸出圖像的形狀
        shuffle=True,  # 以隨機順序讀取數據集
        rand_crop=1,  # 隨機裁剪的概率爲1
        min_object_covered=0.95, max_attempts=200)
    val_iter = im.ImageDetIter(
        path_imgrec='pikaqiu_object/val.rec', batch_size=batch_size,
        data_shape=(3, edge_size, edge_size), shuffle=False)
    return train_iter, val_iter,class_names,num_class

batch_size=32
edge_size=256
train_data,test_data,class_names,num_class=load_data_pikachu(batch_size,edge_size)
batch=train_data.next()
print(batch)

def box_to_rect(box,color):
    box=box.asnumpy()
    return plt.Rectangle((box[0],box[1]),box[2]-box[0],box[3]-box[1],
                         fill=False,edgecolor=color,linewidth=3)
_,figs=plt.subplots(3,3,figsize=(6,6))
for i in range(3):
    for j in range(3):
        img,labels=batch.data[0][3*i+j],batch.label[0][3*i+j]
        img=img.transpose((1,2,0))
        figs[i][j].imshow(img.clip(0,255).asnumpy()/255)
        for label in labels:
            rect=box_to_rect(label[1:5]*edge_size,"red")
            figs[i][j].add_patch(rect)
        figs[i][j].axes.get_xaxis().set_visible(False)
        figs[i][j].axes.get_yaxis().set_visible(False)
plt.show()

# (輸入x)NCHW
x=nd.random_normal(shape=(1,3,40,40))
y=contrib.nd.MultiBoxPrior(x,sizes=[0.75,0.5,0.1],ratios=[1,2,0.5])# 3+3-1=5,共有5種
print(y.shape)
boxes = y.reshape((40, 40, -1, 4))
print(boxes.shape)

print(boxes[20,20,0,:]) # 以20,20爲中心的第一個框

# 畫出以(20,20)爲中心的所有錨框[x_min,y_min,x_max,y_max]
colors=['blue','red','green','black','magenta'] # 總共5個錨框


anchors=boxes[20,20,:,:]
image=nd.ones(shape=(40,40,3)).asnumpy()
plt.imshow(image)
for i in range(anchors.shape[0]):
    plt.gca().add_patch(box_to_rect(anchors[i,:]*40,colors[i])) # 40是原圖像的寬高
plt.show()

# 定義類別分類器
def class_predict(num_anchors,num_classes):
    return gn.nn.Conv2D(num_anchors*(num_classes+1),3,padding=1)
cls_pre=class_predict(5,10)
cls_pre.initialize()
k=nd.random_normal(shape=(2,3,10,10))
print(cls_pre(k).shape)

def box_predict(num_anchors):
    return gn.nn.Conv2D(num_anchors*4, 3, padding=1)

box_pre=box_predict(10)
box_pre.initialize()
print(box_pre(k).shape)

def sample_dowm(num_filters):
    out=gn.nn.HybridSequential()
    for _ in range(2):
        out.add(gn.nn.Conv2D(num_filters,3,1,1))
        out.add(gn.nn.BatchNorm(in_channels=num_filters))
        out.add(gn.nn.Activation("relu"))
    out.add(gn.nn.MaxPool2D(2))
    return out
blk=sample_dowm(10)
blk.initialize()
m=nd.random_normal(shape=(2,3,8,8))
print(blk(m).shape)

input=nd.random_normal(shape=(2,8,20,20))
print("input:",input.shape)

cls_pred1=class_predict(5,10)
cls_pred1.initialize()
pred1=cls_pred1(input)
print("pre result 1:",pred1.shape)

ds=sample_dowm(16)
ds.initialize()
input=ds(input)
print("down input:",input.shape)

cls_pred2=class_predict(3,10)
cls_pred2.initialize()
pred2=cls_pred2(input)
print("pre result 2:",pred2.shape)

def flatten_prediction(pred):
    return pred.transpose(axes=(0,2,3,1)).flatten()
def concat_prediction(preds): # preds是一個list數組
    return nd.concat(*preds,dim=1)
# 把它們轉換並融合
print("==========")
y1=flatten_prediction(pred1)
print("Flatten class pre 1:",y1.shape)
y2=flatten_prediction(pred2)
print("Flatten class pre 2:",y2.shape)
y_all=concat_prediction([y1,y2])
print("concat result:",y_all.shape)

'''---主體網絡---'''
# 主體網絡就是第一個卷積塊,通常來說會用一個比較好的模型來抽取特徵(這裏隨便定義一個模型)
def body():
    out=gn.nn.HybridSequential()
    for nfilter in [16,32,64]:
        out.add(sample_dowm(num_filters=nfilter))
    return out
bnet=body()
bnet.initialize()
print(bnet(k).shape)

# 簡單的SSD模型
def SSD_Model(num_anchors,num_classes):
    downsamples=gn.nn.HybridSequential()
    for _ in range(3):
        downsamples.add(sample_dowm(128))
    class_predicts=gn.nn.HybridSequential()
    box_predicts=gn.nn.HybridSequential()
    for _ in range(5):
        class_predicts.add(class_predict(num_anchors,num_classes))
        box_predicts.add(box_predict(num_anchors))
    model=gn.nn.HybridSequential()
    model.add(body(),downsamples,class_predicts,box_predicts)
    return model
# 計算預測
def ssd_forward(x,model,sizes,ratios,verbose=False):
    body,downsamples,class_predicts,box_predicts=model
    anchors,class_preds,box_preds=[],[],[]
    x=body(x)
    for i in range(5):
        anchors.append(contrib.nd.MultiBoxPrior(x,sizes=sizes[i],ratios=ratios[i]))
        class_preds.append(flatten_prediction(class_predicts[i](x)))
        box_preds.append(flatten_prediction(box_predicts[i](x)))
        if verbose:
            print("predict scale",i,x.shape,"with",anchors[-1].shape[1],"anchors")
        if i<3:
            x=downsamples[i](x)
        elif i==3:
            x=nd.Pooling(x,global_pool=True,pool_type="max",
                         kernel=(x.shape[2],x.shape[3]))
    # 融合結果
    return (concat_prediction(anchors),concat_prediction(class_preds),
            concat_prediction(box_preds))
'''---完整的模型---'''
class ToySSD(gn.Block):
    def __init__(self,num_classes,verbose=False,**kwargs):
        super(ToySSD, self).__init__(**kwargs)
        self.sizes=[[0.2,0.272],[0.37,0.447],[0.54,0.618],[0.7,0.8],[0.88,0.9]]
        self.ratios=[[1,2,0.5]]*5
        self.num_classes=num_classes
        self.verbose = verbose
        num_anchors=len(self.sizes[0]+self.ratios[0])-1
        with self.name_scope():
            self.model=SSD_Model(num_anchors,num_classes)
    def forward(self, x):
        anchors,class_preds,box_preds=ssd_forward(x,self.model,self.sizes,self.ratios,self.verbose)
        class_preds=class_preds.reshape(shape=(0,-1,self.num_classes+1)) #0表示不管有多少個batch_size,copy過來
        return anchors,class_preds,box_preds

# 看一下圖片的形狀是如何變化的
ctx=mx.gpu()
print("====================")
net_test=ToySSD(num_classes=1,verbose=True)
net_test.initialize(init=init.Xavier(),ctx=ctx)
net_test.hybridize()
x=batch.data[0][0:1].as_in_context(ctx)
print("x:",x.shape)
anchors_,class_pre,box_pre=net_test(x)
print("output anchors:",anchors_.shape)
print("output class_pre:",class_pre.shape)
print("output box_pre:",box_pre.shape)

# 訓練網絡
print("====================")
net=ToySSD(num_classes=1)
net.initialize(init=init.Xavier(),ctx=ctx)
net.hybridize()
class smoothLoss(gn.loss.Loss): # box reg loss
    def __init__(self,batch_axis=0,**kwargs):
        super(smoothLoss, self).__init__(None,batch_axis,**kwargs)
    def hybrid_forward(self, F,output,label, mask, *args, **kwargs):
        loss=F.smooth_l1((output-label)*mask,scalar=1.0) # 參數mask,用來屏蔽不需要被懲罰的負例樣本
        return loss.mean(self._batch_axis,exclude=True)
cls_loss = gn.loss.SoftmaxCrossEntropyLoss()
bbox_loss = smoothLoss()

def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    cls = cls_loss(cls_preds, cls_labels)
    bbox = bbox_loss(bbox_preds , bbox_labels, bbox_masks)
    return 0.1*cls + bbox # 對分類做了加權
# 我們可以沿用準確率評價分類結果。因爲使用了 L1 範數損失,我們用平均絕對誤差評價邊界框的預測結果。
def cls_eval(cls_preds, cls_labels):
    # 由於類別預測結果放在最後一維,argmax需要指定最後一維
    return (cls_preds.argmax(axis=-1) == cls_labels).sum().asscalar()

def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
    return ((bbox_labels - bbox_preds) * bbox_masks).abs().sum().asscalar()

trainer = gn.Trainer(net.collect_params(), 'sgd',
                        {'learning_rate': 0.2, 'wd': 5e-4})

for epoch in range(100):
    acc_sum, mae_sum, n, m = 0.0, 0.0, 0, 0
    train_data.reset()  # 從頭讀取數據
    start = time.time()
    for batch in train_data:
        X = batch.data[0].as_in_context(ctx)
        Y = batch.label[0].as_in_context(ctx)
        with ag.record():
            # 生成多尺度的錨框,爲每個錨框預測類別和偏移量
            anchors, cls_preds, bbox_preds = net(X)
            # 爲每個錨框標註類別和偏移量
            bbox_labels, bbox_masks, cls_labels = contrib.nd.MultiBoxTarget(
                anchors, Y, cls_preds.transpose((0, 2, 1)))
            # 根據類別和偏移量的預測和標註值計算損失函數
            l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
                          bbox_masks)
        l.backward()
        trainer.step(batch_size)
        acc_sum += cls_eval(cls_preds, cls_labels)
        n += cls_labels.size
        mae_sum += bbox_eval(bbox_preds, bbox_labels, bbox_masks)
        m += bbox_labels.size

    if (epoch + 1) % 10 == 0:
        print('epoch %2d, class acc %f, bbox mae %f, time %.1f sec' % (
            epoch + 1, acc_sum / n, mae_sum / m, time.time() - start))

img = im.imread('F:/test/pikaqiu.jpg')
feature = im.imresize(img, 256, 256).astype('float32')
X = feature.transpose((2, 0, 1)).expand_dims(axis=0)
src=cv.imread("F:/test/pikaqiu.jpg")




def predict(X):
    anchors, cls_preds, bbox_preds = net(X.as_in_context(ctx))
    cls_probs = cls_preds.softmax().transpose((0, 2, 1))
    output = contrib.nd.MultiBoxDetection(cls_probs, bbox_preds, anchors)
    idx = [i for i, row in enumerate(output[0]) if row[0].asscalar() != -1]
    return output[0, idx]

output = predict(X)

def display(img, output, threshold):
    for row in output:
        score = row[1].asscalar()
        if score < threshold:
            continue
        h, w = img.shape[0:2]
        bbox = row[2:6].asnumpy()

        cv.rectangle(src,(int(bbox[0]*w),int(bbox[1]*h)),(int(bbox[2]*w),int(bbox[3]*h)),(0,0,255),2)
        cv.putText(src,str(score),(int(bbox[0]*w),int(bbox[1]*h-10)),
                   cv.FONT_HERSHEY_PLAIN,1.0,(0,255,0),2)
    cv.imshow(" ",src)
    cv.waitKey(0)
display(img, output, threshold=0.7)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章