1. 程式人生 > >目標檢測中tensorflow常用API以及備選框篩選程式碼分析

目標檢測中tensorflow常用API以及備選框篩選程式碼分析

       目標檢測演算法中,因為產生的備選框特別多,需要刪減。而刪減的方法是NMS(非極大抑制演算法)。網上很多演算法是自己編寫功能程式碼。但是這不是tensorflow中自帶的功能,所以在使用tensorflow恢復模型的時候,sess並不能hold住他們。因此別人需要用的時候,還需要額外的配置這些程式碼,如果採用TensorFlow自帶的一些功能,那麼呼叫pb檔案的時候就可以直接實現一步到位的結果。下面結合一下YOLO3中的yolo_predict中的程式碼,介紹一下tensorflow中對目標檢測演算法比較常見的一些功能。

    def predict(self, inputs, image_shape):
        """
        Introduction
        ------------
            構建預測模型
        Parameters
        ----------
            inputs: 處理之後的輸入圖片
            image_shape: 影象原始大小
        Returns
        -------
            boxes: 物體框座標
            scores: 物體概率值
            classes: 物體類別
        """
        model = yolo(config.norm_epsilon, config.norm_decay, self.anchors_path, self.classes_path, pre_train = False)
        output = model.yolo_inference(inputs, config.num_anchors // 3, config.num_classes, training = False)
        boxes, scores, classes = self.eval(output, image_shape, max_boxes = 20)
        return boxes, scores, classes

在經過model.yolo_inference()後,是輸出的三個Tensor,這在我之前的一篇文章裡面有過介紹。但是生成的備選框和每個備選框對應的分數太多。我們需要通過自己設定的threshold進行賽選。這個賽選就是通過eval()方法實現的。

  def eval(self, yolo_outputs, image_shape, max_boxes = 20):
        """
        Introduction
        ------------
            根據Yolo模型的輸出進行非極大值抑制,獲取最後的物體檢測框和物體檢測類別
        Parameters
        ----------
            yolo_outputs: yolo模型輸出
            image_shape: 圖片的大小
            max_boxes:  最大box數量
        Returns
        -------
            boxes_: 物體框的位置
            scores_: 物體類別的概率
            classes_: 物體類別
        """
        anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
        boxes = []
        box_scores = []
        input_shape = tf.shape(yolo_outputs[0])[1 : 3] * 32
        # 對三個尺度的輸出獲取每個預測box座標和box的分數,score計算為置信度x類別概率
        for i in range(len(yolo_outputs)):
            _boxes, _box_scores = self.boxes_and_scores(yolo_outputs[i], self.anchors[anchor_mask[i]], len(self.class_names), input_shape, image_shape)
            boxes.append(_boxes)
            box_scores.append(_box_scores)
        boxes = tf.concat(boxes, axis = 0)
        box_scores = tf.concat(box_scores, axis = 0)

        mask = box_scores >= self.obj_threshold
        max_boxes_tensor = tf.constant(max_boxes, dtype = tf.int32)
        boxes_ = []
        scores_ = []
        classes_ = []
        for c in range(len(self.class_names)):
            class_boxes = tf.boolean_mask(boxes, mask[:, c])
            class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])
            nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold = self.nms_threshold)
            class_boxes = tf.gather(class_boxes, nms_index)
            class_box_scores = tf.gather(class_box_scores, nms_index)
            classes = tf.ones_like(class_box_scores, 'int32') * c
            boxes_.append(class_boxes)
            scores_.append(class_box_scores)
            classes_.append(classes)
        boxes_ = tf.concat(boxes_, axis = 0)
        scores_ = tf.concat(scores_, axis = 0)
        classes_ = tf.concat(classes_, axis = 0)
        return boxes_, scores_, classes_

     1. tf.shape():

這個不用多說了,就是獲取tensor的維度。

input_shape = tf.shape(yolo_outputs[0])[1 : 3] * 32

這句程式碼就是恢復darknet網路輸入的圖片尺寸(這裡相對於原圖尺寸也是經過resize的)。下面經過三次迴圈,分別求出尺寸為13,26,52的Tensor對應的備選框和分數進行賽選。從而程式進入:

def boxes_and_scores(self, feats, anchors, classes_num, input_shape, image_shape):
    def boxes_and_scores(self, feats, anchors, classes_num, input_shape, image_shape):
        """
        Introduction
        ------------
            將預測出的box座標轉換為對應原圖的座標,然後計算每個box的分數
        Parameters
        ----------
            feats: yolo輸出的feature map
            anchors: anchor的位置
            class_num: 類別數目
            input_shape: 輸入大小
            image_shape: 圖片大小
        Returns
        -------
            boxes: 物體框的位置
            boxes_scores: 物體框的分數,為置信度和類別概率的乘積
        """
        box_xy, box_wh, box_confidence, box_class_probs = self._get_feats(feats, anchors, classes_num, input_shape)
        boxes = self.correct_boxes(box_xy, box_wh, input_shape, image_shape)
        boxes = tf.reshape(boxes, [-1, 4])
        box_scores = box_confidence * box_class_probs
        box_scores = tf.reshape(box_scores, [-1, classes_num])
        return boxes, box_scores

接著進入:

def _get_feats(self, feats, anchors, num_classes, input_shape):
   def _get_feats(self, feats, anchors, num_classes, input_shape):
        """
        Introduction
        ------------
            根據yolo最後一層的輸出確定bounding box
        Parameters
        ----------
            feats: yolo模型最後一層輸出
            anchors: anchors的位置
            num_classes: 類別數量
            input_shape: 輸入大小
        Returns
        -------
            box_xy, box_wh, box_confidence, box_class_probs
        """
        num_anchors = len(anchors)
        anchors_tensor = tf.reshape(tf.constant(anchors, dtype=tf.float32), [1, 1, 1, num_anchors, 2])
        grid_size = tf.shape(feats)[1:3]
        predictions = tf.reshape(feats, [-1, grid_size[0], grid_size[1], num_anchors, num_classes + 5])
        # 這裡構建13*13*1*2的矩陣,對應每個格子加上對應的座標
        grid_y = tf.tile(tf.reshape(tf.range(grid_size[0]), [-1, 1, 1, 1]), [1, grid_size[1], 1, 1])
        grid_x = tf.tile(tf.reshape(tf.range(grid_size[1]), [1, -1, 1, 1]), [grid_size[0], 1, 1, 1])
        grid = tf.concat([grid_x, grid_y], axis=-1)
        grid = tf.cast(grid, tf.float32)
        # 將x,y座標歸一化為佔416的比例
        box_xy = (tf.sigmoid(predictions[..., :2]) + grid) / tf.cast(grid_size[::-1], tf.float32)
        # 將w,h也歸一化為佔416的比例
        box_wh = tf.exp(predictions[..., 2:4]) * anchors_tensor / tf.cast(input_shape[::-1], tf.float32)
        box_confidence = tf.sigmoid(predictions[..., 4:5])
        box_class_probs = tf.sigmoid(predictions[..., 5:])
        return box_xy, box_wh, box_confidence, box_class_probs

      2. tf.range():

進入函式內部可以看到其定義為: range(start, limit=None, delta=1, dtype=None, name="range"):就是產生一個等差數列。delta為步子的大小。以尺寸為13的那個Tensor來講tf.range(13) = [0,1,2,...,...,12].

     3. tf.title()

tf.tile()應用於需要張量擴充套件的場景,具體說來就是:  如果現有一個形狀如[widthheight]的張量,需要得到一個基於原張量的,形狀如[batch_size,width,height]的張量,其中每一個batch的內容都和原張量一模一樣(引用自)

    最終形成了gridx = gridy =[13,13,1,1],grid = tf.cast(grid,tf.float32).,其shape = (13,13,1,2),表示在13*13的矩陣,其channel為1,每一個channel裡面有兩個資料表示也就是他們的座標值。

     然後通過yolo的特徵圖到darknet的輸入圖直接的對映關係,輸出備選框在輸入影象的座標和長寬,每一個框的置信度以及類別分數。

隨後程式進入:

def correct_boxes(self, box_xy, box_wh, input_shape, image_shape):
 def correct_boxes(self, box_xy, box_wh, input_shape, image_shape):
        """
        Introduction
        ------------
            計算物體框預測座標在原圖中的位置座標
        Parameters
        ----------
            box_xy: 物體框左上角座標
            box_wh: 物體框的寬高
            input_shape: 輸入的大小
            image_shape: 圖片的大小
        Returns
        -------
            boxes: 物體框的位置
        """
        box_yx = box_xy[..., ::-1]
        box_hw = box_wh[..., ::-1]
        input_shape = tf.cast(input_shape, dtype = tf.float32)
        image_shape = tf.cast(image_shape, dtype = tf.float32)
        new_shape = tf.round(image_shape * tf.reduce_min(input_shape / image_shape))
        offset = (input_shape - new_shape) / 2. / input_shape
        scale = input_shape / new_shape
        box_yx = (box_yx - offset) * scale
        box_hw *= scale

        box_mins = box_yx - (box_hw / 2.)
        box_maxes = box_yx + (box_hw / 2.)
        boxes = tf.concat([
            box_mins[..., 0:1],
            box_mins[..., 1:2],
            box_maxes[..., 0:1],
            box_maxes[..., 1:2]
        ], axis = -1)
        boxes *= tf.concat([image_shape, image_shape], axis = -1)
        return boxes

這個函式主要是計算了從輸入影象尺寸到原圖的對映。

4:tf.round():

四捨五入的函式,切輸入必須是浮點型。

再次回到eval函式,此後經過三次迴圈,把輸出的所有框和框對應的類的分數資訊都分別加入到了boxes和box_scores.

然後對box_scores的分數大於threshold的進行賽選留下。

5:tf.boolean_mask():

這個函式的作用就是對通過threshold的box_scores和boxses進行保留。其作用可參考該文章

6:tf.image.non_max_suppression():

這個就是TensorFlow自帶的計算NMS的函式。非常方便。

7:gather(params, indices, validate_indices=None, name=None, axis=0)

這張圖很清楚瞭解釋了該函式的用法。

8:tf.ones_like():

建立一個將所有元素都設定為1的Tensor。