1. 程式人生 > >SSD-Tensorflow超詳細解析【一】:載入模型對圖片進行測試

SSD-Tensorflow超詳細解析【一】:載入模型對圖片進行測試

SSD-tensorflow——github下載地址:SSD-Tensorflow

目標檢測的塊速實現

下載完成之後我們開啟工程,可以看到如下圖所示的檔案佈局:


首先我們開啟checkpoints檔案,解壓縮ssd_300_vgg.ckpt.zip檔案到checkpoints目錄下面。注意:解壓到checkpoints資料夾下即可,不要有子資料夾。

然後開啟notebooks的ssd_tests.ipyb檔案(使用Jupyter或者ipython)。為了方便除錯,將其儲存為.py檔案,或者直接在notebooks下新建一個test.py檔案,然後複製如下程式碼:

# coding: utf-8
# In[1]:

import os
import math
import random

import numpy as np
import tensorflow as tf
import cv2

slim = tf.contrib.slim

# In[2]:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# In[3]:

import sys

sys.path.append('./')

# In[4]:

from nets import ssd_vgg_300, ssd_common, np_methods
from preprocessing import ssd_vgg_preprocessing
from notebooks import visualization

# In[5]:

# TensorFlow session: grow memory when needed. TF, DO NOT USE ALL MY GPU MEMORY!!!
gpu_options = tf.GPUOptions(allow_growth = True)
config = tf.ConfigProto(log_device_placement = False, gpu_options = gpu_options)
isess = tf.InteractiveSession(config = config)

# ## SSD 300 Model
#
# The SSD 300 network takes 300x300 image inputs. In order to feed any image, the latter is resize to this input shape (i.e.`Resize.WARP_RESIZE`). Note that even though it may change the ratio width / height, the SSD model performs well on resized images (and it is the default behaviour in the original Caffe implementation).
#
# SSD anchors correspond to the default bounding boxes encoded in the network. The SSD net output provides offset on the coordinates and dimensions of these anchors.

# In[6]:

# Input placeholder.
net_shape = (300, 300)
data_format = 'NHWC'
img_input = tf.placeholder(tf.uint8, shape = (None, None, 3))
# Evaluation pre-processing: resize to SSD net shape.
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)
image_4d = tf.expand_dims(image_pre, 0)

# Define the SSD model.
reuse = True if 'ssd_net' in locals() else None
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)

# Restore SSD model.
ckpt_filename = 'D:\py project\SSD-Tensorflow\checkpoints/ssd_300_vgg.ckpt'
# ckpt_filename = '../checkpoints/VGG_VOC0712_SSD_300x300_ft_iter_120000.ckpt'
isess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
saver.restore(isess, ckpt_filename)

# SSD default anchor boxes.
ssd_anchors = ssd_net.anchors(net_shape)

# ## Post-processing pipeline
#
# The SSD outputs need to be post-processed to provide proper detections. Namely, we follow these common steps:
#
# * Select boxes above a classification threshold;
# * Clip boxes to the image shape;
# * Apply the Non-Maximum-Selection algorithm: fuse together boxes whose Jaccard score > threshold;
# * If necessary, resize bounding boxes to original image shape.

# In[7]:

# Main image processing routine.
def process_image(img, select_threshold = 0.5, nms_threshold = .45, net_shape = (300, 300)):
    # Run SSD network.
    rimg, rpredictions, rlocalisations, rbbox_img = isess.run([image_4d, predictions, localisations, bbox_img],
                                                              feed_dict = {img_input: img})

    # Get classes and bboxes from the net outputs.
    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)
    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)
    # Resize bboxes to original image shape. Note: useless for Resize.WARP!
    rbboxes = np_methods.bboxes_resize(rbbox_img, rbboxes)
    return rclasses, rscores, rbboxes


# In[21]:

# Test on some demo image and visualize output.
path = 'D:\py project\SSD-Tensorflow\demo/'
image_names = sorted(os.listdir(path))

img = mpimg.imread(path + image_names[-1])
rclasses, rscores, rbboxes = process_image(img)

# visualization.bboxes_draw_on_img(img, rclasses, rscores, rbboxes, visualization.colors_plasma)
visualization.plt_bboxes(img, rclasses, rscores, rbboxes)

修改程式碼中的ckpt_filename為你的工程目錄下的chepoints下的ckpt檔案。path為你想要進行測試的圖片目錄(程式碼中只對該目錄下的最後一個資料夾進行測試,如果要想測試多幅圖片或者做成視訊的方式,需大家自行修改程式碼)。

SSD的網路結構

理論上來講,現在程式碼已經可以跑了。但是我們還是需要學習一下它是如何實現SSD這一目標檢測過程的。

我們開啟nets中的ssd_vgg_300.py檔案,裡面就是整個基於vgg的ssd目標檢測網路。其網路結構設計參照下圖:


下圖為程式碼的一個實現,


這個裡面實現的是input-conv-conv-block1-pool-conv-conv-block2-pool-conv-conv-conv-block3-pool-conv-conv-conv-block4-pool-conv-conv-conv-block5-pool-conv-block6-dropout-conv-block7-dropout-conv-conv-block8-conv-conv-block9-conv-conv-block11。

每一層的feature maps都是存到了end_points這個dic裡面,這是整個基本網路結構的設計。再看一下SSDParams裡面的定義:

    default_params = SSDParams(
        img_shape=(300, 300),    #輸入的size
        num_classes=21,        # 類數
        no_annotation_label=21,
        feat_layers=['block4', 'block7', 'block8', 'block9', 'block10', 'block11'],    #需要抽取並做額外卷積來做目標檢測的層
        feat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)],            #對應上面的層的feature maps的size大小
        anchor_size_bounds=[0.15, 0.90],        
        # anchor_size_bounds=[0.20, 0.90],
        anchor_sizes=[(21., 45.),       # anchor size,定義的default box的size,之後再計算anchor的時候用到
                      (45., 99.),
                      (99., 153.),
                      (153., 207.),
                      (207., 261.),
                      (261., 315.)],
        # anchor_sizes=[(30., 60.),
        #               (60., 111.),
        #               (111., 162.),
        #               (162., 213.),
        #               (213., 264.),
        #               (264., 315.)],
        anchor_ratios=[[2, .5],            # 長寬比,即論文裡面提到的 ratios(2,1/2,3,1/3)
                       [2, .5, 3, 1./3],
                       [2, .5, 3, 1./3],
                       [2, .5, 3, 1./3],
                       [2, .5],
                       [2, .5]],
        anchor_steps=[8, 16, 32, 64, 100, 300],    #caffe實現的時候使用的初始化anchor方法,後面會講到
        anchor_offset=0.5,        #偏移,計算中心點時用到
        normalizations=[20, -1, -1, -1, -1, -1],
        prior_scaling=[0.1, 0.1, 0.2, 0.2]
        )

其中feat-layers裡面儲存的是我們在目標檢測時需要用到的feature層,一共有6個層,其size對應feat_shapes裡面的數值。

我們再繼續看SSD是如何預測目標的類別以及框的位置的,下圖接SSD網路結構設計部分:


這裡的一個for迴圈依次計算feat_layers裡面的類別與位置——從block4、7、8、9、10、11,然後append到相應的數組裡面。對於裡面的細節,我們轉到ssd_multibox_layer,如下程式碼所示:

def ssd_multibox_layer(inputs,
                       num_classes,
                       sizes,
                       ratios=[1],
                       normalization=-1,
                       bn_normalization=False):
    """Construct a multibox layer, return a class and localization predictions.
    """
    net = inputs
    if normalization > 0:
        net = custom_layers.l2_normalization(net, scaling=True)    #對特徵層做l2的標準化處理
    # Number of anchors.
    num_anchors = len(sizes) + len(ratios)    #計算default box的數量,分別為 4 6 6 6 4 4

    # Location.
    num_loc_pred = num_anchors * 4        #預測的位置資訊= 4*num_anchors , 即 ymin,xmin,ymax,xmax.
    loc_pred = slim.conv2d(net, num_loc_pred, [3, 3], activation_fn=None,    #對特徵層做卷積,輸出channels為4*num_anchors
                           scope='conv_loc')
    loc_pred = custom_layers.channel_to_last(loc_pred)  # ensure data format be "NWHC"
    loc_pred = tf.reshape(loc_pred,
                          tensor_shape(loc_pred, 4)[:-1]+[num_anchors, 4])    #將得到的feature maps reshape為[N,W,H,num_anchors,4]
    # Class prediction.
    num_cls_pred = num_anchors * num_classes        # 預測的類別資訊 21*4=84
    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,
                          tensor_shape(cls_pred, 4)[:-1]+[num_anchors, num_classes]) # reshape 為[N,W,H,num_anchors,classes]
    return cls_pred, loc_pred

該段程式碼實現對指定feature layers的位置預測以及類別預測。首先計算anchors的數量,對於位置資訊,輸出16通道的feature map,將其reshape為[N,W,H,num_anchors,4]。對於類別資訊,輸出84通道的feature maps,再將其reshape為[N,W,H,num_anchors,num_classes]。返回計算得到的位置和類別預測。

anchor box的初始化

初始過程的思想大致如此:設定好相應的anchor size,ratios(長寬比),offset(偏移)等,對於每一層feature layers,計算出相對的anchor size,論文中如右所示:。smin=0.2。300*0.2=60,而SSDNet中定義的anchor size 為(21., 45.),(45., 99.),(99., 153.), (153., 207.),(207., 261.),(261., 315.)]。可見其大小為自己設定的,並非由公式得到的。

程式碼中的實現,以第一層feat_layer(block4,38x38)為例:對於y,x各生成一個相對應的網格矩陣,矩陣數值如下:


然後對其做標準化處理,比較簡單的方法為(y+offset)/feat_shape,以第一層為例,即(y+0.5)/38可得到歸一化的座標,歸一化的陣列如下:


然後定義anchors的寬度和高度,其寬度和高度分別對應論文中,以及當ratios=1時對應。其中ar即為定義好的anchor_ratios。以第一層為例,共有4個anchors,其中第一個的h和w為 21/300。第二個h和w為

,對應公式

第三個和第四個為,分別對應寬度和高度的計算公式。

其初始化主要由下面三個函式完成,其呼叫關係為從上到下:


def ssd_anchor_one_layer(img_shape,
                         feat_shape,
                         sizes,
                         ratios,
                         step,
                         offset=0.5,
                         dtype=np.float32):
    """Computer SSD default anchor boxes for one feature layer.

    Determine the relative position grid of the centers, and the relative
    width and height.

    Arguments:
      feat_shape: Feature shape, used for computing relative position grids;
      size: Absolute reference sizes;
      ratios: Ratios to use on these features;
      img_shape: Image shape, used for computing height, width relatively to the
        former;
      offset: Grid offset.

    Return:
      y, x, h, w: Relative x and y grids, and height and width.
    """
    # Compute the position grid: simple way.                    #計算中心點的歸一化距離
    # y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]]
    # y = (y.astype(dtype) + offset) / feat_shape[0]
    # x = (x.astype(dtype) + offset) / feat_shape[1]
    # Weird SSD-Caffe computation using steps values...
    y, x = np.mgrid[0:feat_shape[0], 0:feat_shape[1]]       #生成一個網格矩陣 y,x均為[38,38],其中y為從上到下從0到37。y為從左到右0到37
    y = (y.astype(dtype) + offset) * step / img_shape[0]    #以第一個元素為例,簡單方法為:(0+0.5)/38即為相對距離,而SSD-Caffe使用的是(0+0.5)*step/img_shape
    x = (x.astype(dtype) + offset) * step / img_shape[1]

    # Expand dims to support easy broadcasting.
    y = np.expand_dims(y, axis=-1)      # [38,38,1]
    x = np.expand_dims(x, axis=-1)

    # Compute relative height and width.
    # Tries to follow the original implementation of SSD for the order.
    num_anchors = len(sizes) + len(ratios)
    h = np.zeros((num_anchors, ), dtype=dtype)
    w = np.zeros((num_anchors, ), dtype=dtype)
    # Add first anchor boxes with ratio=1.
    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

最終返回y,x,h,w的數值資訊。

邊界框的選取


上面講了SSDNet的網路結構以及anchor_bbox的初始化,通過上述方法可以得到初步的類別與位置預測。這裡我們進入到np_methods.ssd_bboxes_select函式裡面,檢視SSD是如何選擇對應的bboxes的。

其函式如下(函式呼叫關係為從上往下):

def ssd_bboxes_select_layer(predictions_layer,
                            localizations_layer,
                            anchors_layer,
                            select_threshold=0.5,
                            img_shape=(300, 300),
                            num_classes=21,
                            decode=True):
    """Extract classes, scores and bounding boxes from features in one layer.

    Return:
      classes, scores, bboxes: Numpy arrays...
    """
    # First decode localizations features if necessary.
    if decode:
        localizations_layer = ssd_bboxes_decode(localizations_layer, anchors_layer)    #解碼成ymin,xmin,ymax,xmax的形式

    # Reshape features to: Batches x N x N_labels | 4.
    p_shape = predictions_layer.shape    #以第一層為例,這裡的值為(1,38,38,4,21)單幅圖片
    batch_size = p_shape[0] if len(p_shape) == 5 else 1 
    predictions_layer = np.reshape(predictions_layer,
                                   (batch_size, -1, p_shape[-1]))    #reshape之後為(1,5776,21) 
    l_shape = localizations_layer.shape    #(1,38,38,4,4) 為邊界框的座標資訊
    localizations_layer = np.reshape(localizations_layer,
                                     (batch_size, -1, l_shape[-1]))    #reshape之後為(1,5776,4)

    # Boxes selection: use threshold or score > no-label criteria.
    if select_threshold is None or select_threshold == 0:
        # Class prediction and scores: assign 0. to 0-class
        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:
        sub_predictions = predictions_layer[:, :, 1:]        #去掉背景類別
        idxes = np.where(sub_predictions > select_threshold)    #得到預測的類別中閾值大於select_threshold的索引值。
        classes = idxes[-1]+1    #因為去掉了背景類別,全體的類別索引減了1,所以該處的類需要+1
        scores = sub_predictions[idxes]    #得到所有已選定(大於給定閾值)的類別的分數。這裡的話陣列維度應該會大幅減少
        bboxes = localizations_layer[idxes[:-1]] #得到所有已選定的類的座標值

    return classes, scores, bboxes


對於每一層的feat_layer與anchor boxes,需要先對其decode,返回邊界框的ymin,xmin,ymax,xmax。再根據給定的閾值選擇大於其值的類和邊界框。並appen到一個數組中返回。

邊界框的裁剪

此時得到的rbboxes陣列中的部分值是大於1的,而在我們的歸一化表示中,整個框的寬度和高度都是等於1的,因此需要對其進行裁剪,保證最大值不超過1,最小值不小於0。其程式碼如下所示:

def bboxes_clip(bbox_ref, bboxes):
    """Clip bounding boxes with respect to reference bbox.
    """
    bboxes = np.copy(bboxes)
    bboxes = np.transpose(bboxes)
    bbox_ref = np.transpose(bbox_ref)            #其中 bbox_ref為一個[0,0,1,1]的陣列
    bboxes[0] = np.maximum(bboxes[0], bbox_ref[0])
    bboxes[1] = np.maximum(bboxes[1], bbox_ref[1])
    bboxes[2] = np.minimum(bboxes[2], bbox_ref[2])
    bboxes[3] = np.minimum(bboxes[3], bbox_ref[3])
    bboxes = np.transpose(bboxes)
    return bboxes

保留指定數量的邊界框

得到了裁剪之後的邊界框和類別,我們需要對其做一個降序排序,並保留指定數量的最優的那一部分。箇中引數可以自行調節,這裡只保留至多400個。其程式碼如下:

def bboxes_sort(classes, scores, bboxes, top_k=400):
    """Sort bounding boxes by decreasing order and keep only the top_k
    """
    # if priority_inside:
    #     inside = (bboxes[:, 0] > margin) & (bboxes[:, 1] > margin) & \
    #         (bboxes[:, 2] < 1-margin) & (bboxes[:, 3] < 1-margin)
    #     idxes = np.argsort(-scores)
    #     inside = inside[idxes]
    #     idxes = np.concatenate([idxes[inside], idxes[~inside]])
    idxes = np.argsort(-scores)
    classes = classes[idxes][:top_k]
    scores = scores[idxes][:top_k]
    bboxes = bboxes[idxes][:top_k]
    return classes, scores, bboxes
非極大值抑制(NMS)

在得到了指定數量的邊界框和類別之後。對於同一個類存在多個框的情況下,要找到一個最合適的,並去掉其他冗餘的框,需要進行非極大值抑制的操作,其程式碼實現如下:

def bboxes_nms(classes, scores, bboxes, nms_threshold=0.45):
    """Apply non-maximum selection to bounding boxes.
    """
keep_bboxes = np.ones(scores.shape, dtype=np.bool)
    for i in range(scores.size-1):
        if keep_bboxes[i]:
            # Computer overlap with bboxes which are following.
overlap = bboxes_jaccard(bboxes[i], bboxes[(i+1):])     #計算當前框與之後所有框的IOU
            # Overlap threshold for keeping + checking part of the same class
keep_overlap = np.logical_or(overlap < nms_threshold, classes[(i+1):] != classes[i]) #對於所有IOU小於0.45或者當前類別與之後類別不相同的位置,置為True
keep_bboxes[(i+1):] = np.logical_and(keep_bboxes[(i+1):], keep_overlap)         # 將上面得到的所有IOU小於0.45或類別不同的位置賦給keep_bboxes
idxes = np.where(keep_bboxes)
    return classes[idxes], scores[idxes], bboxes[idxes]

其中bboxes_jaccard即計算兩個框的IOU

邊界框的Resize

程式碼中有一步是對bboxes進行resize,使其恢復到original image shape。但是由於這裡使用的是歸一化的表示方式,所以意義不大,但還是貼一下程式碼:


目標檢測的視覺化

在完成了上述所有操作之後,我們得到了最終的classes,scores,localizations。我們需要對其進行視覺化,將所有的目標類別、分數以及邊界框的座標畫在我們的原圖上,這裡由如下程式碼完成:


def plt_bboxes(img, classes, scores, bboxes, figsize=(10,10), linewidth=1.5):
    """Visualize bounding boxes. Largely inspired by SSD-MXNET!
    """
    fig = plt.figure(figsize=figsize)
    plt.imshow(img)
    height = img.shape[0]
    width = img.shape[1]
    colors = dict()
    for i in range(classes.shape[0]):
        cls_id = int(classes[i])
        if cls_id >= 0:
            score = scores[i]
            if cls_id not in colors:
                colors[cls_id] = (random.random(), random.random(), random.random())
            ymin = int(bboxes[i, 0] * height)
            xmin = int(bboxes[i, 1] * width)
            ymax = int(bboxes[i, 2] * height)
            xmax = int(bboxes[i, 3] * width)
            rect = plt.Rectangle((xmin, ymin), xmax - xmin,
                                 ymax - ymin, fill=False,
                                 edgecolor=colors[cls_id],
                                 linewidth=linewidth)
            plt.gca().add_patch(rect)
            class_name = str(cls_id)
            plt.gca().text(xmin, ymin - 2,
                           '{:s} | {:.3f}'.format(class_name, score),
                           bbox=dict(facecolor=colors[cls_id], alpha=0.5),
                           fontsize=12, color='white')
    plt.show()

值得一提的是,因為邊界框的表示是歸一化的,所以要恢復成原圖的尺寸的話,只需要對相應的座標乘上原圖的Height和width即可。

最終的結果如下:


好了,本篇博文到此結束,下一篇講講如何利用SSD_tensorflow訓練自己的資料集。