1. 程式人生 > >深度學習目標檢測系列:一文弄懂YOLO演算法|附Python原始碼

深度學習目標檢測系列:一文弄懂YOLO演算法|附Python原始碼

在之前的文章中,介紹了計算機視覺領域中目標檢測的相關方法——RCNN系列演算法原理,以及Faster RCNN的實現。這些演算法面臨的一個問題,不是端到端的模型,幾個構件拼湊在一起組成整個檢測系統,操作起來比較複雜,本文將介紹另外一個端到端的方法——YOLO演算法,該方法操作簡便且模擬速度快,效果也不差。

YOLO演算法是什麼?

       YOLO框架(You Only Look Once)與RCNN系列演算法不一樣,是以不同的方式處理物件檢測。它將整個影象放在一個例項中,並預測這些框的邊界框座標和及所屬類別概率。使用YOLO演算法最大優的點是速度極快,每秒可處理45幀,也能夠理解一般的物件表示。

YOLO框架如何運作?

       在本節中,將介紹YOLO用於檢測給定影象中的物件的處理步驟。

  • 首先,輸入影象:

 

  • 然後,YOLO將輸入影象劃分為網格形式(例如3 X 3):

 

  • 最後,對每個網格應用影象分類和定位處理,獲得預測物件的邊界框及其對應的類概率。

       整個過程是不是很清晰,下面逐一詳細介紹。首先需要將標記資料傳遞給模型以進行訓練。假設已將影象劃分為大小為3 X 3的網格,且總共只有3個類別,分別是行人(c1)、汽車(c2)和摩托車(c3)。因此,對於每個單元格,標籤y將是一個八維向量:


其中:

  • pc定義物件是否存在於網格中(存在的概率);
  • bx、by、bh、bw指定邊界框;
  • c1、c2、c3代表類別。如果檢測物件是汽車,則c2位置處的值將為1,c1和c3處的值將為0;

       假設從上面的例子中選擇第一個網格:


       由於此網格中沒有物件,因此pc將為零,此網格的y標籤將為:


        意味著其它值是什麼並不重要,因為網格中沒有物件。下面舉例另一個有車的網格(c2=1):


       在為此網格編寫y標籤之前,首先要了解YOLO如何確定網格中是否存在實際物件。大圖中有兩個物體(兩輛車),因此YOLO將取這兩個物體的中心點,物體將被分配到包含這些物體中心的網格中。中心點左側網格的y標籤會是這樣的:


       由於此網格中存在物件,因此pc將等於1,bx、by、bh、bw將相對於正在處理的特定網格單元計算。由於檢測出的物件是汽車,所以c2=1,c1和c3均為0。對於9個網格中的每一個單元格,都具有八維輸出向量。最終的輸出形狀為3X3X8
       使用上面的例子(輸入影象:100X100X3,輸出:3X3X8),模型將按如下方式進行訓練:


       使用經典的CNN網路構建模型,並進行模型訓練。在測試階段,將影象傳遞給模型,經過一次前向傳播就得到輸出y。為了簡單起見,使用3X3網格解釋這一點,但通常在實際場景中會採用更大的網格(比如19X19)。
       即使一個物件跨越多個網格,它也只會被分配到其中點所在的單個網格。可以通過增加更多網格來減少多個物件出現在同一網格單元中的機率。

如何編碼邊界框?

       如前所述,bx、by、bh和bw是相對於正在處理的網格單元計算而言的。下面通過一個例子來說明這一點。以包含汽車的右邊網格為例:


       由於bx、by、bh和bw將僅相對於該網格計算。此網格的y標籤將為:


       由於這個網格中有一個物件汽車,所以pc=1c2=1。現在,看看如何決定bx、by、bh和bw的取值。在YOLO中,分配給所有網格的座標都如下圖所示:


       bx、by是物件相對於該網格的中心點的x和y座標。在例子中,近似bx=0.4by=0.3


       bh是邊界框的高度與相應單元網格的高度之比,在例子中約為0.9:bh=0.9,bw是邊界框的寬度與網格單元的寬度之比,bw=0.5。此網格的y標籤將為:


       請注意,bx和by將始終介於0和1之間,因為中心點始終位於網格內,而在邊界框的尺寸大於網格尺寸的情況下,bh和bw可以大於1。

非極大值抑制|Non-Max Suppression

       這裡有一些思考的問題——如何判斷預測的邊界框是否是一個好結果(或一個壞結果)?單元格之間的交叉點,計算實際邊界框和預測的邊界框的並集交集。假設汽車的實際和預測邊界框如下所示:


       其中,紅色框是實際的邊界框,藍色框是預測的邊界框。如何判斷它是否是一個好的預測呢?IoU將計算這兩個框的並集交叉區域:

  • IoU =交叉面積/聯合的面積;
  • 在本例中:

    • IoU =黃色面積/綠色面積;

       如果IoU大於0.5,就可以說預測足夠好。0.5是在這裡採取的任意閾值,也可以根據具體問題進行更改。閾值越大,預測就越準確。
       還有一種技術可以顯著提高YOLO的效果——非極大值抑制。
       物件檢測演算法最常見的問題之一是,它不是一次僅檢測出一次物件,而可能獲得多次檢測結果。假設:


       上圖中,汽車不止一次被識別,那麼如何判定邊界框呢。非極大值抑可以解決這個問題,使得每個物件只能進行一次檢測。下面瞭解該方法的工作原理。

  • 1.它首先檢視與每次檢測相關的概率並取最大的概率。在上圖中,0.9是最高概率,因此首先選擇概率為0.9的方框:

 

  • 2.現在,它會檢視影象中的所有其他框。與當前邊界框較高的IoU的邊界框將被抑制。因此,在示例中,0.6和0.7概率的邊界框將被抑制:

 

  • 3.在部分邊界框被抑制後,它會從概率最高的所有邊界框中選擇下一個,在例子中為0.8的邊界框:

 

  • 4.再次計算與該邊界框相連邊界框的IoU,去掉較高IoU值的邊界框:

 

  • 5.重複這些步驟,得到最後的邊界框:

 

       以上就是非極大值抑制的全部內容,總結一下關於非極大值抑制演算法的要點:

  • 丟棄概率小於或等於預定閾值(例如0.5)的所有方框;
  • 對於剩餘的邊界框:
  • 選擇具有最高概率的邊界框並將其作為輸出預測;
  • 計算相關聯的邊界框的IoU值,捨去IoU大於閾值的邊界框;
  • 重複步驟2,直到所有邊界框都被視為輸出預測或被捨棄;

Anchor Boxes

       在上述內容中,每個網格只能識別一個物件。但是如果單個網格中有多個物件呢?這就行需要了解 Anchor Boxes的概念。假設將下圖按照3X3網格劃分:


       獲取物件的中心點,並根據其位置將物件分配給相應的網格。在上面的示例中,兩個物件的中心點位於同一網格中:


       上述方法只會獲得兩個邊界框其中的一個,但是如果使用Anchor Boxes,可能會輸出兩個邊界框!我們該怎麼做呢?首先,預先定義兩種不同的形狀,稱為Anchor Boxes。對於每個網格將有兩個輸出。這裡為了易於理解,這裡選取兩個Anchor Boxes,也可以根據實際情況增加Anchor Boxes的數量:

  • 沒有Anchor Boxes的YOLO輸出標籤如下所示:

  • 有Anchor Boxes的YOLO輸出標籤如下所示:

 


       前8行屬於Anchor Boxes1,其餘8行屬於Anchor Boxes2。基於邊界框和框形狀的相似性將物件分配給Anchor Boxes。由於Anchor Boxes1的形狀類似於人的邊界框,後者將被分配給Anchor Boxes1,並且車將被分配給Anchor Boxes2.在這種情況下的輸出,將是3X3X16大小。
       因此,對於每個網格,可以根據Anchor Boxes的數量檢測兩個或更多個物件。

結合思想

       在本節中,首先介紹如何訓練YOLO模型,然後是新的影象進行預測。

訓練

       訓練模型時,輸入資料是由影象及其相應的y標籤構成。樣例如下:


       假設每個網格有兩個Anchor Boxes,並劃分為3X3網格,並且有3個不同的類別。因此,相應的y標籤具有3X3X16的形狀。訓練過程的完成方式就是將特定形狀的影象對映到對應3X3X16大小的目標。

測試

       對於每個網格,模型將預測·3X3X16·大小的輸出。該預測中的16個值將與訓練標籤的格式相同。前8個值將對應於Anchor Boxes1,其中第一個值將是該網路中物件的概率,2-5的值將是該物件的邊界框座標,最後三個值表明物件屬於哪個類。以此類推。
       最後,非極大值抑制方法將應用於預測框以獲得每個物件的單個預測結果。
       以下是YOLO演算法遵循的確切維度和步驟:

  • 準備對應的影象(608,608,3);
  • 將影象傳遞給卷積神經網路(CNN),該網路返回(19,19,5,85)維輸出;
  • 輸出的最後兩個維度被展平以獲得(19,19,425)的輸出量:

    • 19×19網格的每個單元返回425個數字;
    • 425=5 * 85,其中5是每個網格的Anchor Boxes數量;
    • 85= 5+80,其中5表示(pc、bx、by、bh、bw),80是檢測的類別數;
  • 最後,使用IoU和非極大值抑制去除重疊框;

YOLO演算法實現

       本節中用於實現YOLO的程式碼來自Andrew NG的GitHub儲存庫,需要下載此zip檔案,其中包含執行此程式碼所需的預訓練權重。
       首先定義一些函式,這些函式將用來選擇高於某個閾值的邊界框,並對其應用非極大值抑制。首先,匯入所需的庫:

import os
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
import scipy.io
import scipy.misc
import numpy as np
import pandas as pd
import PIL
import tensorflow as tf
from skimage.transform import resize
from keras import backend as K
from keras.layers import Input, Lambda, Conv2D
from keras.models import load_model, Model
from yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxes
from yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body

%matplotlib inline

然後,實現基於概率和閾值過濾邊界框的函式:

def yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = .6):
    box_scores = box_confidence*box_class_probs
    box_classes = K.argmax(box_scores,-1)
    box_class_scores = K.max(box_scores,-1)
    filtering_mask = box_class_scores>threshold
    scores = tf.boolean_mask(box_class_scores,filtering_mask)
    boxes = tf.boolean_mask(boxes,filtering_mask)
    classes = tf.boolean_mask(box_classes,filtering_mask)
 
    return scores, boxes, classes

之後,實現計算IoU的函式:

def iou(box1, box2):
    xi1 = max(box1[0],box2[0])
    yi1 = max(box1[1],box2[1])
    xi2 = min(box1[2],box2[2])
    yi2 = min(box1[3],box2[3])
    inter_area = (yi2-yi1)*(xi2-xi1)
    box1_area = (box1[3]-box1[1])*(box1[2]-box1[0])
    box2_area = (box2[3]-box2[1])*(box2[2]-box2[0])
    union_area = box1_area+box2_area-inter_area
    iou = inter_area/union_area
 
    return iou

然後,實現非極大值抑制的函式:

def yolo_non_max_suppression(scores, boxes, classes, max_boxes = 10, iou_threshold = 0.5):
    max_boxes_tensor = K.variable(max_boxes, dtype='int32')
    K.get_session().run(tf.variables_initializer([max_boxes_tensor]))
    nms_indices = tf.image.non_max_suppression(boxes,scores,max_boxes,iou_threshold)
    scores = K.gather(scores,nms_indices)
    boxes = K.gather(boxes,nms_indices)
    classes = K.gather(classes,nms_indices)

    return scores, boxes, classes

隨機初始化下大小為(19,19,5,85)的輸出向量:

yolo_outputs = (tf.random_normal([19, 19, 5, 1], mean=1, stddev=4, seed = 1),
                   tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1),
                   tf.random_normal([19, 19, 5, 2], mean=1, stddev=4, seed = 1),
                   tf.random_normal([19, 19, 5, 80], mean=1, stddev=4, seed = 1))

最後,實現一個將CNN的輸出作為輸入並返回被抑制的邊界框的函式:

def yolo_eval(yolo_outputs, image_shape = (720., 1280.), max_boxes=10, score_threshold=.6, iou_threshold=.5):
    box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs
    boxes = yolo_boxes_to_corners(box_xy, box_wh)
    scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = score_threshold)
    boxes = scale_boxes(boxes, image_shape)
    scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes, iou_threshold)

    return scores, boxes, classes

使用yolo_eval函式對之前建立的隨機輸出向量進行預測:

scores, boxes, classes = yolo_eval(yolo_outputs)
with tf.Session() as test_b:
    print("scores[2] = " + str(scores[2].eval()))
    print("boxes[2] = " + str(boxes[2].eval()))
    print("classes[2] = " + str(classes[2].eval()))

 

29


score表示物件在影象中的可能性,boxes返回檢測到的物件的(x1,y1,x2,y2)座標,classes表示識別物件所屬的類。
現在,在新的影象上使用預訓練的YOLO演算法,看看其工作效果:

sess = K.get_session()
class_names = read_classes("model_data/coco_classes.txt")
anchors = read_anchors("model_data/yolo_anchors.txt")

yolo_model = load_model("model_data/yolo.h5")

在載入類別資訊和預訓練模型之後,使用上面定義的函式來獲取·yolo_outputs·。

yolo_outputs = yolo_head(yolo_model.output, anchors, len(class_names))

之後,定義一個函式來預測邊界框並在影象上標記邊界框:

def predict(sess, image_file):
    image, image_data = preprocess_image("images/" + image_file, model_image_size = (608, 608))
    out_scores, out_boxes, out_classes = sess.run([scores, boxes, classes], feed_dict={yolo_model.input: image_data, K.learning_phase(): 0})

    print('Found {} boxes for {}'.format(len(out_boxes), image_file))

    # Generate colors for drawing bounding boxes.
    colors = generate_colors(class_names)

    # Draw bounding boxes on the image file
    draw_boxes(image, out_scores, out_boxes, out_classes, class_names, colors)

    # Save the predicted bounding box on the image
    image.save(os.path.join("out", image_file), quality=90)

    # Display the results in the notebook
    output_image = scipy.misc.imread(os.path.join("out", image_file))

    plt.figure(figsize=(12,12))
    imshow(output_image)

    return out_scores, out_boxes, out_classes

接下來,將使用預測函式讀取影象並進行預測:

img = plt.imread('images/img.jpg')
image_shape = float(img.shape[0]), float(img.shape[1])
scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)

最後,輸出預測結果:

out_scores, out_boxes, out_classes = predict(sess, "img.jpg")

 


以上就是YOLO演算法的全部內容

 


原文連結
本文為雲棲社群原創內容,未經允許不得轉載。