Pytorch從0開始實現YOLO V3指南 part5——設計輸入和輸出的流程


本節翻譯自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-5/

在前一節最后,我們實現了一個將網絡輸出轉換為檢測預測的函數。現在我們已經有了一個檢測器了,剩下的就是創建輸入和輸出的流程。

 

必要條件:

1.此系列教程的Part1到Part4。

2.Pytorch的基本知識,包括如何使用nn.Module,nn.Sequential,torch.nn.parameter類構建常規的結構

3.OpenCV的基礎知識

 

EDIT: 如果你在2018年3月30日之前訪問過這篇文章,我們將任意大小的圖片調整為Darknet的輸入大小的方法就是resize。然而在原始的實現中,調整圖像的大小時,需要保持長寬比不變,並填充遺漏的部分。例如,如果我們將1900 x 1280的圖像調整為416 x 415,那么調整后的圖像應該是這樣的。

對於輸入處理的差異導致早期實現的性能略低於原始實現。現在這篇文章已經進行了更新,遵循了原始實現中調整大小的方法。

在這一部分中,我們將構建檢測器的輸入和輸出管道。這包括從磁盤讀取圖像,進行預測,使用預測結果在圖像上繪制邊界框,然后將它們保存到磁盤。我們還將介紹如何讓檢測器實時工作在一個攝像機或視頻中。我們將介紹一些命令行標志,以允許對網絡的各種超參數進行一些實驗。那么讓我們開始吧!

 注意:這部分需要安裝opencv3。

 

 創建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

 

 

創建命令行參數:

因為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。

 

加載網絡:

這里下載coco.names文件,該文件包含COCO數據集中對象的名稱。在檢測器目錄中創建文件夾數據。同樣如果你在linux上工作,可以輸入。

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

然后,我們在程序中加載該文件。

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()

 

 

讀入輸入圖片:

從磁盤或目錄中讀取圖像。將圖像的路徑存儲在一個名為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的圖像輸入格式為(批量x通道x高x寬),通道順序為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()

 

創建批:

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)] 

 

 

檢測循環體:

我們對批次進行迭代,生成預測並連接所有圖像的預測張量(write_results函數的輸出,維度為D*8)。

對於每個批,我們將檢測所花費的時間定義為從接收輸入到write_results函數產生輸出之間所花費的時間。在write_prediction返回的輸出中,其中一個屬性是批中圖像的索引。我們將其轉換成在imlist中圖像的索引,imlist列表包含所有圖像的地址。

之后,我們打印每次檢測所花費的時間以及在每張圖像中檢測到的對象。如果批的write_results函數的輸出是int(0)就意味着沒有檢測,那么我們使用continue跳過rest循環。

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作業實際結束之前打印出來,可能會導致錯誤的時間。

現在,我們已經在tensor輸出中檢測到了所有的圖像。讓我們在圖像上繪制邊界框吧!

 

在圖像上繪制邊界框:

我們使用try-catch塊來檢查是否進行了一次檢測。如果沒有則退出程序。

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

 

在繪制邊界框之前,輸出tensor中包含的預測符合網絡的輸入大小而不是圖像的原始大小。因此,在繪制邊界框之前,讓我們將每個邊界框的角屬性轉換為圖像的原始維度。

在繪制邊界框之前,輸出tensor中包含的預測是對填充圖像的預測,而不是對原始圖像的預測。僅僅將它們重新縮放到輸入圖像的維數在這里是行不通的。首先,我們需要將邊界框的坐標轉換到相對於包含原始圖像的填充圖像上的邊界。

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中,我們通過縮放因子調整了圖像的兩個維度的大小(請記住,兩個維度都用一個公共因子來划分,以保持長寬比)。現在,我們撤消這個重新縮放,以獲得原始圖像上的邊框的坐標。

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])

 

 如果圖像中有很多邊界框,用一種顏色繪制它們可能不太好。將此文件下載到檢測器文件夾,這是一個pickle文件,其中包含許多顏色可供隨機選擇。

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

 

 現在我們來寫繪制邊界框的函數。(x中的信息是圖像索引、4個角坐標、目標置信度得分、最大置信類得分、該類的索引)

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中隨機選擇一個顏色來繪制矩形。它還在包圍框的左上角創建一個填充矩形,並在填充矩形中寫入檢測到的對象的類。-1是cv2.rectangle函數用於創建填充矩形的參數。

 我們的write函數是局部定義的以便它可以訪問顏色列表。我們也可以用顏色作為參數,但是那樣我們就只能用一種顏色。

完成這個函數定義后,現在讓我們在圖像上繪制邊界框。

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()

 

 

打印時間日志:

在檢測器的最后,我們將打印一個日志,其中包含執行代碼的哪一部分花費了多長時間。這對我們比較不同的超參數如何影響檢測器的速度時很重要。可以在命令行上執行腳本檢測.py時設置超參數,如批大小、對象置信度和NMS閾值(分別通過bs、confidence和nms_thresh這些標志傳遞)。

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()

 

 

測試目標檢測器:

在終端輸入:

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

 

產生輸出:

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目錄中。

 

在視頻/攝像機上運行檢測器:

在視頻或網絡攝像頭上運行檢測器,代碼幾乎保持不變,只是我們不需要遍歷批次,而是遍歷視頻的幀。

 在github存儲庫中的video.py文件中可以找到在視頻上運行檢測器的代碼。除了一些更改之外,代碼與detector .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將檢測圖像寫入磁盤,而是用cv2.imshow顯示繪制了邊界框的圖像。如果用戶按下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     

 

 

結論:

在本系列教程中,我們從零開始實現了一個目標檢測器,並為達到這個目標而歡呼。我認為能夠寫出高效的代碼是深度學習實踐者被低估的技能之一。無論你的想法多么具有革命性,除非你能對它進行測試,否則它毫無用處,為此你需要有很強的編程技能。

我還意識到,在深度學習中學習任何topic的最佳方法都是實現代碼。當你在閱讀一篇文章的時候一些細微之處你可能會錯過,編程會迫使你注意topic的每個細微之處。我希望本系列教程能夠作為一個練習,鍛煉你作為一個深度學習實踐者的技能。

 

Further Reading

  1. PyTorch tutorial
  2. OpenCV Basics
  3. Python ArgParse

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM