Part5:設計輸入輸出通道

  Part5:設計輸入輸出通道   

在這一個部分中,我們會爲檢測器構建一個輸入輸出的通道。這涉及到從磁盤上讀取圖片進行預測、使用預測在圖片上繪製邊界框並且把它們保存在磁盤上。我們還會涉及如何讓檢測器在攝像機或者是在視頻上實時的工作。我們將會介紹一些命令行標誌,來允許對網絡的超參數進行一些實驗。讓我們開始吧。

在你的檢測器文件夾下創建一個detector.py文件,在文件的上部添加一些必要的導入。

from __future__ import division
import time
import torch 
import torch.nn as nn
from torch.autograd import Variable
import numpy as np
import cv2 
from util import *
import argparse
import os 
import os.path as osp
from darknet import Darknet
import pickle as pkl
import pandas as pd
import random

1 創建命令行聲明

detector.py是一個運行檢測器的文件,因此,需要一個輸入的命令行,用來配置相關參數。我們可以使用Python的ArgParse模塊來實現。

def arg_parse():
    """
    Parse arguements to the detect module
    
    """
    
    parser = argparse.ArgumentParser(description='YOLO v3 Detection Module')
   
    parser.add_argument("--images", dest = 'images', help = 
                        "Image / Directory containing images to perform detection upon",
                        default = "imgs", type = str)
    parser.add_argument("--det", dest = 'det', help = 
                        "Image / Directory to store detections to",
                        default = "det", type = str)
    parser.add_argument("--bs", dest = "bs", help = "Batch size", default = 1)
    parser.add_argument("--confidence", dest = "confidence", help = "Object Confidence to filter predictions", default = 0.5)
    parser.add_argument("--nms_thresh", dest = "nms_thresh", help = "NMS Threshhold", default = 0.4)
    parser.add_argument("--cfg", dest = 'cfgfile', help = 
                        "Config file",
                        default = "cfg/yolov3.cfg", type = str)
    parser.add_argument("--weights", dest = 'weightsfile', help = 
                        "weightsfile",
                        default = "yolov3.weights", type = str)
    parser.add_argument("--reso", dest = 'reso', help = 
                        "Input resolution of the network. Increase to increase accuracy. Decrease to increase speed",
                        default = "416", type = str)
    
    return parser.parse_args()
    
args = arg_parse()
images = args.images
batch_size = int(args.bs)
confidence = float(args.confidence)
nms_thesh = float(args.nms_thresh)
start = 0
CUDA = torch.cuda.is_available()

它們當中重比較要的參數是images(用來確定輸入圖片或者是圖片的目錄),det(我們要保存檢測器的目錄),reso(輸入圖片的分辨率,可以用於速度-準確率的折中),cfg(選擇的配置文件)以及weightfile。

2 加載網絡

這裏可以下載coco.name文件,這個文件包含coco數據集中目標物體的名稱。在你的檢測器目錄下創建一個data文件夾。相似的(equivalently),如果在linux環境下,你可以鍵入:

mkdir data
cd data
wget https://raw.githubusercontent.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch/master/data/coco.names

然後,我們在你的程序中加載種類文件

num_classes = 80    #For COCO
classes = load_classes("data/coco.names")

load_classes是一個在util.py文件中的一個函數,讓返回一個詞典,將每個類的索引映射到這類的名字。

def load_classes(namesfile):
    fp = open(namesfile, "r")
    names = fp.read().split("\n")[:-1]
    return names

初始化網絡並且加載權重

#Set up the neural network
print("Loading network.....")
model = Darknet(args.cfgfile)
model.load_weights(args.weightsfile)
print("Network successfully loaded")

model.net_info["height"] = args.reso
inp_dim = int(model.net_info["height"])
assert inp_dim % 32 == 0 
assert inp_dim > 32

#If there's a GPU availible, put the model on GPU
if CUDA:
    model.cuda()

#Set the model in evaluation mode
model.eval()

3 讀取輸入圖片

從磁盤中或者從目錄中讀取圖片。圖片的地址保存在imlist列表中。

read_dir = time.time()
#Detection phase
try:
    imlist = [osp.join(osp.realpath('.'), images, img) for img in os.listdir(images)]
except NotADirectoryError:
    imlist = []
    imlist.append(osp.join(osp.realpath('.'), images))
except FileNotFoundError:
    print ("No file or directory with the name {}".format(images))
    exit()

read_dir是一個用於測量時間的檢查點。如果保存檢測的目錄不存在,通過det標誌定義,我們就創建它。

if not os.path.exists(args.det):
    os.makedirs(args.det)

我們使用OpenCV去加載圖片

load_batch = time.time()
loaded_ims = [cv2.imread(x) for x in imlist]

load_batch又是一個檢查點。OpenCV以一個numpy數組的形式加載一個圖片,顏色通道的順序是BGR。PyTorch的圖片輸入格式是(Batches * Channels * Height * Width),顏色通道是RGB。因此,我們在util.py中編寫prep_image函數來講numpy數組轉換成一個PyTorch的輸入格式。在編寫這個函數之前,我們必須編寫一個letterbox_image的函數來調整我們的圖片,保持縱橫比不變,在剩餘的部分填充(128,128,128)的顏色。

def letterbox_image(img, inp_dim):
    '''resize image with unchanged aspect ratio using padding'''
    img_w, img_h = img.shape[1], img.shape[0]
    w, h = inp_dim
    new_w = int(img_w * min(w/img_w, h/img_h))
    new_h = int(img_h * min(w/img_w, h/img_h))
    resized_image = cv2.resize(img, (new_w,new_h), interpolation = cv2.INTER_CUBIC)
    
    canvas = np.full((inp_dim[1], inp_dim[0], 3), 128)

    canvas[(h-new_h)//2:(h-new_h)//2 + new_h,(w-new_w)//2:(w-new_w)//2 + new_w,  :] = resized_image
    
    return canvas

現在,我們編寫一個函數,它輸入OpenCV的圖片並且把它轉換成我們網絡的輸入。

def prep_image(img, inp_dim):
    """
    Prepare image for inputting to the neural network. 
    
    Returns a Variable 
    """

    img = cv2.resize(img, (inp_dim, inp_dim))
    img = img[:,:,::-1].transpose((2,0,1)).copy()
    img = torch.from_numpy(img).float().div(255.0).unsqueeze(0)
    return img

除了變化之後的圖片,我們也要保存原來圖片的序列,im_dim_list,保存原來圖片尺寸的隊列。

#PyTorch Variables for images
im_batches = list(map(prep_image, loaded_ims, [inp_dim for x in range(len(imlist))]))

#List containing dimensions of original images
im_dim_list = [(x.shape[1], x.shape[0]) for x in loaded_ims]
im_dim_list = torch.FloatTensor(im_dim_list).repeat(1,2)

if CUDA:
    im_dim_list = im_dim_list.cuda()

4 創建批量

leftover = 0
if (len(im_dim_list) % batch_size):
   leftover = 1

if batch_size != 1:
   num_batches = len(imlist) // batch_size + leftover            
   im_batches = [torch.cat((im_batches[i*batch_size : min((i +  1)*batch_size,
                       len(im_batches))]))  for i in range(num_batches)]  

5 檢測循環

我們迭代了批量,生成了預測,並且將所有我們需要進行預測的圖片的預測張量連接起來。我們將檢測花費的時間度量爲從獲得輸入到write_results函數產生輸出的時間。在write_prediction函數的輸出中,每一個屬性都是圖片在批量中的索引。我們以這樣一個方式來改變這些特別的屬性,它現在代表imlist中圖片的索引,這個列表包含所有圖片的地址。

之後,我們打印每個檢測和每個圖片中物體檢測的時間。如果每個批量的write_resluts函數的輸出都是0,這意味着沒有檢測物體,我們使用continue來跳過剩下的循環。

write = 0
start_det_loop = time.time()
for i, batch in enumerate(im_batches):
    #load the image 
    start = time.time()
    if CUDA:
        batch = batch.cuda()

    prediction = model(Variable(batch, volatile = True), CUDA)

    prediction = write_results(prediction, confidence, num_classes, nms_conf = nms_thesh)

    end = time.time()

    if type(prediction) == int:

        for im_num, image in enumerate(imlist[i*batch_size: min((i +  1)*batch_size, len(imlist))]):
            im_id = i*batch_size + im_num
            print("{0:20s} predicted in {1:6.3f} seconds".format(image.split("/")[-1], (end - start)/batch_size))
            print("{0:20s} {1:s}".format("Objects Detected:", ""))
            print("----------------------------------------------------------")
        continue

    prediction[:,0] += i*batch_size    #transform the atribute from index in batch to index in imlist 

    if not write:                      #If we have't initialised output
        output = prediction  
        write = 1
    else:
        output = torch.cat((output,prediction))

    for im_num, image in enumerate(imlist[i*batch_size: min((i +  1)*batch_size, len(imlist))]):
        im_id = i*batch_size + im_num
        objs = [classes[int(x[-1])] for x in output if int(x[0]) == im_id]
        print("{0:20s} predicted in {1:6.3f} seconds".format(image.split("/")[-1], (end - start)/batch_size))
        print("{0:20s} {1:s}".format("Objects Detected:", " ".join(objs)))
        print("----------------------------------------------------------")

    if CUDA:
        torch.cuda.synchronize()   

這個代碼行torch.cuda.synchronize確保cuda的內核和CPU的保持同步。否則,CUDA內核會在GPU作業進入隊列和GPU作業完成之前(異步調用)將控制權返回給CPU。如果end = time()在GPU作業實際結束之前被打印出來,那麼這可能會導致錯誤的時間。現在,我們已經讓所有圖片的檢測都在我們的輸出張量中。讓我們在圖片上畫邊界框吧。

6 在圖片上繪製邊界框

我們使用try-catch代碼塊來檢查這裏時候有檢測。如果沒有,退出程序

try:
    output
except NameError:
    print ("No detections were made")
    exit()

在我們繪製邊界框之前,我們這個在我們輸出張量裏面的預測,遵從我們網絡輸入的尺寸,並不是圖片的原始尺寸。因此在我們繪製邊界框之前,我們要將每個邊界框的邊角屬性轉換成圖像原始的尺寸。在我們繪製邊界框之前,包含我們輸出張量的預測是在padding上做出的預測,不是原始的圖片。僅僅把他們的調整成輸出圖片的尺寸在這裏並不管用。我們首先需要將要測量的盒子的座標轉換爲包含原始圖像的填充圖像的區域邊界。

im_dim_list = torch.index_select(im_dim_list, 0, output[:,0].long())

scaling_factor = torch.min(inp_dim/im_dim_list,1)[0].view(-1,1)


output[:,[1,3]] -= (inp_dim - scaling_factor*im_dim_list[:,0].view(-1,1))/2
output[:,[2,4]] -= (inp_dim - scaling_factor*im_dim_list[:,1].view(-1,1))/2

現在,我們的座標服從在填充區域上的我們圖片的尺寸。但是,在letterbox_image函數中,我們通過縮放因子(scaling factor)調整我們圖片的尺寸(記住所有尺寸都被相同的因子所除去維持寬高比)。我們現在撤銷這個縮放因子來得到在原始圖片上的邊界框的座標。

output[:,1:5] /= scaling_factor

現在,讓問剪輯任何邊界框,可能有邊界外的圖像到我們的圖像的邊緣。

for i in range(output.shape[0]):
    output[i, [1,3]] = torch.clamp(output[i, [1,3]], 0.0, im_dim_list[i,0])
    output[i, [2,4]] = torch.clamp(output[i, [2,4]], 0.0, im_dim_list[i,1])

如果圖片上有太多的邊界框,用一種顏色繪製可能不是一個好的辦法。向你的檢測器文件夾中下載這個文件。這個pickled文件包含許多顏色可供隨機選擇。

class_load = time.time()
colors = pkl.load(open("pallete", "rb"))

現在,讓我們編寫一個函數去繪製邊界框。

draw = time.time()

def write(x, results, color):
    c1 = tuple(x[1:3].int())
    c2 = tuple(x[3:5].int())
    img = results[int(x[0])]
    cls = int(x[-1])
    label = "{0}".format(classes[cls])
    cv2.rectangle(img, c1, c2,color, 1)
    t_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_PLAIN, 1 , 1)[0]
    c2 = c1[0] + t_size[0] + 3, c1[1] + t_size[1] + 4
    cv2.rectangle(img, c1, c2,color, -1)
    cv2.putText(img, label, (c1[0], c1[1] + t_size[1] + 4), cv2.FONT_HERSHEY_PLAIN, 1, [225,255,255], 1);
    return img

上面的函數繪製了一個矩形,它的顏色是從colors中隨機選擇的。它還在邊界框的左上角創建了一個填充的矩形,並且寫上了這個填充矩形所檢測到的物體的種類。cv2.rectangle方程的參數-1用來創建一個填充的矩形。我們定義了一個局部函數write,它可以接受一個colors列表。我們還可以接收colors作爲我們的參數,但是它只能允許我們每張圖片使用一種顏色,這樣可能違背了我們想要達到的目的。因爲我們已經定義了這個函數,讓我們現在就在圖片上繪製邊界框吧。

list(map(lambda x: write(x, loaded_ims), output))

上面的片段代碼原地修改了loaded_ims中的圖片。每張圖片都在保存之前在圖片名字前添加了"det_"的前綴。我們創建了一個用來保存我們檢測圖片的地址列表,

det_names = pd.Series(imlist).apply(lambda x: "{}/det_{}".format(args.det,x.split("/")[-1]))

最後,我們將檢測到的圖片寫到det_names中的地址中

list(map(cv2.imwrite, det_names, loaded_ims))
end = time.time()

7 打印時間彙總

在我們檢測器的最後,我們將會打印一個彙總,包含代碼塊花費多長時間來運行。當我們比較不同的超參數是怎樣影響檢測器速度的時候是非常有用的。當我們在命令行執行detection.py文件的時候,超參數,比如批量的大小,目標物體置信度和NMS的閾值。

print("SUMMARY")
print("----------------------------------------------------------")
print("{:25s}: {}".format("Task", "Time Taken (in seconds)"))
print()
print("{:25s}: {:2.3f}".format("Reading addresses", load_batch - read_dir))
print("{:25s}: {:2.3f}".format("Loading batch", start_det_loop - load_batch))
print("{:25s}: {:2.3f}".format("Detection (" + str(len(imlist)) +  " images)", output_recast - start_det_loop))
print("{:25s}: {:2.3f}".format("Output Processing", class_load - output_recast))
print("{:25s}: {:2.3f}".format("Drawing Boxes", end - draw))
print("{:25s}: {:2.3f}".format("Average time_per_img", (end - load_batch)/len(imlist)))
print("----------------------------------------------------------")


torch.cuda.empty_cache()

8 檢測目標檢測器

python detect.py --images dog-cycle-car.png --det det

計算輸出:

(下面的代碼是在cpu上運行(is run on),期望在gpu上跑的更快,在一個gpu tesla K80上大約0.1秒/圖片)

Loading network.....
Network successfully loaded
dog-cycle-car.png    predicted in  2.456 seconds
Objects Detected:    bicycle truck dog
----------------------------------------------------------
SUMMARY
----------------------------------------------------------
Task                     : Time Taken (in seconds)

Reading addresses        : 0.002
Loading batch            : 0.120
Detection (1 images)     : 2.457
Output Processing        : 0.002
Drawing Boxes            : 0.076
Average time_per_img     : 2.657
----------------------------------------------------------

一個名叫det_dog-cycle-car.png的存在保存在det目錄下。

9 在視頻/網絡攝像頭上運行檢測器

爲了在視頻或者網絡攝像頭上運行這個檢測器,代碼幾乎不變,只是我們不需要迭代批量,而是迭代視頻的幀。在視頻上運行檢測器的代碼可以在github倉庫的video.py文件中找到。

首先,我們在OpenCV中打開視頻/攝像頭

videofile = "video.avi" #or path to the video file. 

cap = cv2.VideoCapture(videofile)  

#cap = cv2.VideoCapture(0)  for webcam

assert cap.isOpened(), 'Cannot capture source'

frames = 0

我們以迭代圖片的方式迭代幀。很多地方的代碼得到簡化,因爲我們不再需要處理批,而是一次值需要處理一張圖片。這是因爲我們一次只能得到一幀。這包括使用元組代替im_dim_list張量和write函數中的小的改變。每次迭代,我們追蹤在名叫frames的變量中捕獲的幀的數量。我們用這個數量除以從第一幀圖片到打印視頻的FPS所消耗的時間。

我們使用cv2.imshow去顯示這個幀,邊界框已經在上面繪製了,而不是使用cv2.imwrit向磁盤中寫入檢測圖片。如果使用者安歇q按鍵,代碼跳出循環,視頻結束。

frames = 0  
start = time.time()

while cap.isOpened():
    ret, frame = cap.read()
    
    if ret:   
        img = prep_image(frame, inp_dim)
#        cv2.imshow("a", frame)
        im_dim = frame.shape[1], frame.shape[0]
        im_dim = torch.FloatTensor(im_dim).repeat(1,2)   
                     
        if CUDA:
            im_dim = im_dim.cuda()
            img = img.cuda()

        output = model(Variable(img, volatile = True), CUDA)
        output = write_results(output, confidence, num_classes, nms_conf = nms_thesh)


        if type(output) == int:
            frames += 1
            print("FPS of the video is {:5.4f}".format( frames / (time.time() - start)))
            cv2.imshow("frame", frame)
            key = cv2.waitKey(1)
            if key & 0xFF == ord('q'):
                break
            continue
        output[:,1:5] = torch.clamp(output[:,1:5], 0.0, float(inp_dim))

        im_dim = im_dim.repeat(output.size(0), 1)/inp_dim
        output[:,1:5] *= im_dim

        classes = load_classes('data/coco.names')
        colors = pkl.load(open("pallete", "rb"))

        list(map(lambda x: write(x, frame), output))
        
        cv2.imshow("frame", frame)
        key = cv2.waitKey(1)
        if key & 0xFF == ord('q'):
            break
        frames += 1
        print(time.time() - start)
        print("FPS of the video is {:5.2f}".format( frames / (time.time() - start)))
    else:
        break  

10 總結

在這一段中,我將原文直接貼在這邊(因爲很難翻譯作者的意思)

In this series of tutorials, we have implemented an object detector from scratch, and cheers for reaching this far. I still think being able to churn out efficient code is one of the most underrated skills a deep learning practitioner can have. However revolutionary your idea you maybe, it's of no use unless you can test it. For that, you need to have strong coding skills.

 I've also learned that the the best way to learn about any topic in deep learning is to implement code. It forces you to glance over the minute yet fundamental subtleties of a topic that you may miss out on when you're reading a paper. I hope this tutorial series has served as an exercise in honing your skills as a deep learning practitioner.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章