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.

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