从零开始Pytorch-YOLOv3【笔记】(五)设计输入和输出流程


前言

上一篇:

这一部分,原作者又进行了一部分更新,因此机器之心的翻译有所出入,这里给出原文链接:How to implement a YOLO (v3) object detector from scratch in PyTorch: Part 5

更新的部分:如果你在30/03/2018之前访问过这篇文章,我们将一个大小适中的图片调整为 Darknet 的输入大小的方法就是简单地重新调整尺寸。然而,在最初的实现中,图像会调整大小,保持长宽比完整,并填充左边的部分。例如,如果我们将一个1900 x 1280的图像调整为416 x 415,调整后的图像看起来会是这样的。
在准备输入方面的这种差异导致早期实现的性能略低于原始实现。然而,这篇文章已经被更新以包含在最初的实现中遵循的调整大小的方法。

letter_box

上一篇我们通过置信度阈值和非极大值抑制过滤得到了张量形式的预测结果,在这一部分,我们将为我们的检测器构建输入和输出流程。

这涉及到从磁盘读取图像,做出预测,使用预测结果在图像上绘制边界框,然后将它们保存到磁盘上。我们也会介绍如何让检测器在相机馈送或视频上实时工作。我们将引入一些命令行标签,以便能使用该网络的各种超参数进行一些实验。接下来就开始吧。

注:这部分需要安装 OpenCV 3。

在我们的检测器文件中创建一个 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 文件:https://raw.githubusercontent.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch/master/data/coco.names。这个文件包含了 COCO 数据集中目标的名称。在你的检测器目录中创建一个文件夹 data。如果你使用的 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()

读取输入图像

从磁盘读取图像或从目录读取多张图像。图像的路径存储在一个名为 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 的图像输入格式是(batch x 通道 x 高度 x 宽度),其通道顺序为 RGB。因此,我们在 util.py 中写了一个函数 prep_image 来将 numpy 数组转换成 PyTorch 的输入格式。

在编写这个函数之前,我们必须编写一个函数letter_box来调整图像的大小,保持长宽比的一致性,并用color(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 = (letterbox_image(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))]))  # Python map() 函数:根据函数对指定序列做映射
#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)

# 创建batches

if CUDA:
    im_dim_list = im_dim_list.cuda()

创建baches

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

检测循环

我们在 batch 上迭代,生成预测结果,将我们必须执行检测的所有图像的预测张量(形状为 Dx8,write_results 函数的输出)连接起来。

对于每个 batch,我们都会测量检测所用的时间,即测量获取输入到 write_results 函数得到输出之间所用的时间。在 write_prediction 返回的输出中,其中一个属性是 batch 中图像的索引。我们对这个特定属性执行转换,使其现在能代表 imlist 中图像的索引,该列表包含了所有图像的地址。

在那之后,我们 print 每个检测结果所用的时间以及每张图像中检测到的目标。

如果 write_results 函数在 batch 上的输出是一个 int 值(0),也就是说没有检测结果,那么我们就继续跳过循环的其余部分。

start_det_loop = time.time()
for i, batch in enumerate(im_batches):
#load the image 
    start = time.time()
    if CUDA:
        batch = batch.cuda()
    with torch.no_grad():
        prediction = model(Variable(batch), 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()       
try:
    output
except NameError:
    print ("No detections were made")
    exit()