本节翻译自: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

Pytorch从0开始实现YOLO V3指南 part5——设计输入和输出的流程的更多相关文章

  1. Pytorch从0开始实现YOLO V3指南 part1——理解YOLO的工作

    本教程翻译自https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/ 视频展示:https://w ...

  2. Pytorch从0开始实现YOLO V3指南 part3——实现网络前向传播

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  3. Pytorch从0开始实现YOLO V3指南 part2——搭建网络结构层

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  4. Pytorch从0开始实现YOLO V3指南 part4——置信度阈值和非极大值抑制

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  5. YOLO v3

    yolo为you only look once. 是一个全卷积神经网络(FCN),它有75层卷积层,包含跳跃式传递和降采样,没有池化层,当stide=2时用做降采样. yolo的输出是一个特征映射(f ...

  6. Yolo V3整体思路流程详解!

    结合开源项目tensorflow-yolov3(https://link.zhihu.com/?target=https%3A//github.com/YunYang1994/tensorflow-y ...

  7. 一文看懂YOLO v3

    论文地址:https://pjreddie.com/media/files/papers/YOLOv3.pdf论文:YOLOv3: An Incremental Improvement YOLO系列的 ...

  8. (转载)PyTorch代码规范最佳实践和样式指南

    A PyTorch Tools, best practices & Styleguide 中文版:PyTorch代码规范最佳实践和样式指南 This is not an official st ...

  9. 深度学习笔记(十三)YOLO V3 (Tensorflow)

    [代码剖析]   推荐阅读! SSD 学习笔记 之前看了一遍 YOLO V3 的论文,写的挺有意思的,尴尬的是,我这鱼的记忆,看完就忘了  于是只能借助于代码,再看一遍细节了. 源码目录总览 tens ...

随机推荐

  1. 发布nuget包的正确姿势---cicd自动打包发布nuget包

    最轻便的发布nuget包方式,方便cicd自动打包发布nuget包 首先新建项目 项目名随便取,这里就叫它GuiH.ClassLibrary 默认即可,需要改目标版本时,等创建好再改 项目创建好了 随 ...

  2. K8S+Jenkins自动化构建微服务项目(后续)

    因为之前写过基于K8S部署jenkins master/slave平台,在这个的基础上构建微服务到K8S集群中 Jenkins-slave构建微服务项目到K8S集群 1.微服务项目上传到git仓库 这 ...

  3. mmdetection获取最高map的epoch

    自动从训练结果中获取最高的mAP所对应的epoch. <code>import json import os ''' :param work_dir 训练结果目录 :return 最好的m ...

  4. 关于C++类定义中不能声明该类对象,而Java中可以的原因

    相信接触过C++的人,在学习Java的过程当中,会遇到这样一个问题:在Java中常常会在类定义中声明一个该类的对象(例如Person类定义中声明一些叫parents之类的Person对象),但是在C+ ...

  5. Oracle 定时任务增删改查

    创建 -- 创建定时任务 DECLARE jobno NUMBER; BEGIN dbms_job.submit ( jobno, -- 定时器ID,系统自动获得 'PRC_INSERT;', -- ...

  6. 【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack

    前景回顾 [mq]从零开始实现 mq-01-生产者.消费者启动 [mq]从零开始实现 mq-02-如何实现生产者调用消费者? [mq]从零开始实现 mq-03-引入 broker 中间人 [mq]从零 ...

  7. GB/T 25000.51-2016 系统与软件工程、系统与软件质量要求和评价 第51部分

    中科软测认证中心(软件测评) 1.支持GB/T 25000.51的质量特性 (1)产品质量模型及特性 功能性 功能完备性 功能正确性 功能适合性 功能性的依从性 性能效率 时间特性 资源利用率 容量 ...

  8. Spring Security之用户名+密码登录

    自定义用户认证逻辑 处理用户信息获取逻辑 实现UserDetailsService接口 @Service public class MyUserDetailsService implements Us ...

  9. 709. To Lower Case - LeetCode

    Question 709. To Lower Case Sollution 题目大意:字符串大写转小写 思路: 直接调用Java API函数 字符串转char数组,遍历数组,判断如果大写就转小写 Ja ...

  10. 535. Encode and Decode TinyURL - LeetCode

    Question 535. Encode and Decode TinyURL Solution 题目大意:实现长链接加密成短链接,短链接解密成长链接 思路:加密成短链接+key,将长链接按key保存 ...