热视觉:使用 Python 和 OpenCV 从图像中测量第一个温度
目录
热视觉:用 Python 和 OpenCV 从图像中测量你的第一个温度
在今天的课程中,您将学习热/中远红外视觉的基础知识。完成本课后,您将了解如何从热图像中的每个像素测量第一个温度值,包括:
- 热成像格式:灰色 8 与灰色 16
- 从热成像中测量你的第一次体温
- 通过热感摄像机/摄像机测量您的第一次体温
本教程是关于红外视觉基础知识的 4 部分系列中的第 2 部分:
- 红外视觉介绍:近红外与中远红外图像 (建议更好地理解今天的教程)
- 热视觉:用 Python 和 OpenCV (今日教程)
- 热视觉:使用 Python 和 OpenCV 的发热探测器(初始项目)
- 热视觉:用 PyTorch 和 YOLOv5 探测夜间物体(真实项目)
本课结束时,您将通过一种非常简单的方式,仅使用 Python 和 OpenCV,测量热图像和热视频中每个像素的温度值。此外,如果你手头有这样一台神奇的摄像机,你还可以从热感摄像机获得视频流和实时温度值。
要了解如何从热图像中的每个像素测量你的第一个温度值, 继续阅读。
热视觉:用 Python 和 OpenCV 从图像中测量你的第一个温度
热成像格式:灰色 8 对灰色 16
灰色 8
在我们开始测量每个像素的温度值之前,我们需要了解热感相机/图像提供的不同基本图像格式。
Figure 1: Color RGB visible-light image (left) vs. Black and White or Grayscale visible-light image (center) vs. Gray8 thermal image (right).
当图像在灰度空间中表示时(图 1 、中心),每个像素仅由单个通道或值表征,通常在 0 到 255 之间(即黑白)。
这种格式如图图 1 ( 右)所示,被称为灰度或灰色 8 (8 位= 0 到 255 的像素值),是热视觉中最广泛的格式。
如图图 2 所示,gray8 图像用不同的色图或色标着色,以增强热图像中温度分布的可视化。这些贴图被称为热调色板。
Figure 2: Gray8 thermal colored images. Colormaps from OpenCV: Grayscale (top-left), Inferno (top-right), Jet (bottom-left), and Viridis (bottom-right). For extra information, you can visit ColorMaps in OpenCV.
例如,Inferno 调色板显示紫色(0)和黄色(255)之间的温度变化。
灰色 16
太好了,现在我们明白这些令人惊叹的彩色图像是从哪里来的了,但是我们如何计算温度呢(你可能想知道)?
出于这个目的,热感相机也提供灰度图像。这种格式不是使用从 0 到 255 的像素值(图 3 、左),而是将信息编码为 16 位,即 0 到 65535 个值(图 3 、右)。
但是,为什么呢?
我们将在下一节中发现它,但首先,让我们应用到目前为止所学的知识。
在下面的一小段代码中,您将学习如何:
- 打开一个 gray16 热图像(
lighter_gray16_image.tiff
) - 将其转换为灰色 8 图像
- 用不同的热调色板给它上色
您可能已经注意到,这个图像有一种特殊的格式,TIFF(标记图像文件格式),用来存储 16 位信息。它已经被一个负担得起的热感相机拍摄到:RGM vision thermal cam 1(如果你有兴趣发现热感视觉世界,这是一个极好的开始选项)。
配置您的开发环境
要遵循这个指南,您需要在您的系统上安装 OpenCV 库。
幸运的是,OpenCV 可以通过 pip 安装:
$ pip install opencv-contrib-python
如果您需要帮助配置 OpenCV 的开发环境,我们强烈推荐阅读我们的 pip 安装 OpenCV 指南——它将在几分钟内让您启动并运行。
在配置开发环境时遇到了问题?
说了这么多,你是:
- 时间紧迫?
- 了解你雇主的行政锁定系统?
- 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
- 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***
*那今天就加入 PyImageSearch 大学吧!
获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。
最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!
项目结构
我们首先需要回顾我们的项目目录结构。
首先访问本教程的 “下载” 部分,检索源代码和示例图像。
让我们检查一下简单的项目结构:
$ tree --dirsfirst
.
├── gray8_vs_gray16.py
├── measure_image_temperature.py
├── measure_video_temperature.py
├── measure_camera_video_temperature.py
├── lighter_gray16_image.tiff
└── gray16_sequence
├── gray16_frame_000.tiff
├── gray16_frame_001.tiff
├── gray16_frame_002.tiff
├── ...
└── gray16_frame_069.tiff
1 directory, 76 files
每个 Python 文件对应于教程的 4 个部分。lighter_gray16_image.tiff
文件是我们的 16 位/灰度 16 热图像。
打开项目目录结构中的gray8_vs_gray16.py
文件,插入以下代码导入 NumPy 和 OpenCV 库:
# import the necessary packages
import numpy as np
import cv2
首先,我们将开始打开 gray16 热图像:
# open the gray16 image
gray16_image = cv2.imread("lighter_gray16_image.tiff", cv2.IMREAD_ANYDEPTH)
标志允许我们以 16 位格式打开 gray16 图像。
然后,我们将它转换成灰色图像,以便能够正确处理和可视化:
# convert the gray16 image into a gray8
gray8_image = np.zeros((120, 160), dtype=np.uint8)
gray8_image = cv2.normalize(gray16_image, gray8_image, 0, 255, cv2.NORM_MINMAX)
gray8_image = np.uint8(gray8_image)
在的第 9、10 和 11 行上,我们分别创建了一个空的160x120
图像,我们将 gray16 图像从 0-65,553 (16 位)归一化到 0-255 (8 位),并确保最终图像是 8 位图像。
我们使用我们最喜欢的 OpenCV colormap ***** 为 gray8 图像着色,以获得不同的热调色板:
# color the gray8 image using OpenCV colormaps
inferno_palette = cv2.applyColorMap(gray8_image, cv2.COLORMAP_INFERNO)
jet_palette = cv2.applyColorMap(gray8_image, cv2.COLORMAP_JET)
viridis_palette = cv2.applyColorMap(gray8_image, cv2.COLORMAP_VIRIDIS)
(*) 请访问 OpenCV 中的色彩图,确保您选择的色彩图在您的 OpenCV 版本中可用。在这种情况下,我们使用的是 OpenCV 4.5.4。
最后,我们展示结果:
# show the different thermal color palettes
cv2.imshow("gray8", gray8_image)
cv2.imshow("inferno", inferno_palette)
cv2.imshow("jet", jet_palette)
cv2.imshow("viridis", viridis_palette)
cv2.waitKey(0)
从热成像中测量你的第一个温度
既然我们已经了解了基本的热成像格式,我们就可以从热成像中测量我们的第一个温度了!
我们离开了上一节,想知道为什么灰色图像有助于确定温度。
答案很简单:16 位像素值为我们计算每个像素的温度提供了足够的信息。
重要: 此时,你应该检查信息是如何在你的热/图像相机中被编码的!
但是不用担心;我们将通过查看用 RGMVision ThermalCAM 1 拍摄的样本图像lighter_gray16_image.tiff
中的温度来简化这一步骤。
这款相机遵循:
(1)温度(°C)=(Gray _ 16 _ value)/100(K)-绝对零度(K)。
例如,如果我们得到的 gray16 值为 40,000,则以摄氏度表示的结果温度为:
(2)温度(°C)= 40000/100(K)–273.15(K)= 126.85°C,
这意味着
(3)温度= 126.85 摄氏度= 260.33 华氏度
有了这些信息,我们开始编码吧!
打开measure_image_temperature.py
文件,导入 NumPy 和 OpenCV 库:
# import the necessary packages
import numpy as np
import cv2
打开 gray16 热像,lighter_gray16_image.tiff
如前一节:
# open the gray16 image
gray16_image = cv2.imread("lighter_gray16_image.tiff ", cv2.IMREAD_ANYDEPTH)
标志允许我们以 16 位格式打开 gray16 图像。
我们将测量所需像素的温度值。先说火焰中间的一个热值。由于 RGMVision ThermalCAM 1 提供160x120
图像,我们可以选择例如x, y = (90, 40)
值,如图图 4 所示。
# get the first gray16 value
# pixel coordinates
x = 90
y = 40
pixel_flame_gray16 = gray16_image [y, x]
现在,我们可以应用等式(1 ),得到以°C 或°F 为单位的温度值:
# calculate temperature value in ° C
pixel_flame_gray16 = (pixel_flame_gray16 / 100) - 273.15
# calculate temperature value in ° F
pixel_flame_gray16 = (pixel_flame_gray16 / 100) * 9 / 5 - 459.67
就是这样!就这么简单!
最后,我们显示灰度 16 和灰度 8 图像中的值:
# convert the gray16 image into a gray8 to show the result
gray8_image = np.zeros((120,160), dtype=np.uint8)
gray8_image = cv2.normalize(gray16_image, gray8_image, 0, 255, cv2.NORM_MINMAX)
gray8_image = np.uint8(gray8_image)
# write pointer
cv2.circle(gray8_image, (x, y), 2, (0, 0, 0), -1)
cv2.circle(gray16_image, (x, y), 2, (0, 0, 0), -1)
# write temperature value in gray8 and gray16 image
cv2.putText(gray8_image,"{0:.1f} Fahrenheit".format(pixel_flame_gray16),(x - 80, y - 15), cv2.FONT_HERSHEY_PLAIN, 1,(255,0,0),2)
cv2.putText(gray16_image,"{0:.1f} Fahrenheit".format(pixel_flame_gray16),(x - 80, y - 15), cv2.FONT_HERSHEY_PLAIN, 1,(255,0,0),2)
# show result
cv2.imshow("gray8-fahrenheit", gray8_image)
cv2.imshow("gray16-fahrenheit", gray16_image)
cv2.waitKey(0)
图 5 显示了结果。
Figure 5: Gray8 thermal image with the measured temperature value of a flame pixel in °C (top-left) and in °F (bottom-left). Gray16 thermal image with the measured temperature value of a flame pixel in °C (top-right) and in °F (bottom-right).
我们不显示度数符号“”,因为默认的 OpenCV 字体遵循有限的 ASCII,不提供这种符号。如果你对它感兴趣,请使用 PIL (Python 图像库)。
如果我们想验证这个值是否有意义,我们可以获得一个不同的温度点,在这种情况下,一个更冷的温度点:点燃打火机的手。
# get the second gray16 value
# pixel coordinates
x = 90
y = 100
我们简单地改变坐标,得到如图 6 所示的结果。
Figure 6: Gray8 thermal image with the measured temperature value of a hand pixel in °C (top-left) and in °F (bottom-left). Gray16 thermal image with the measured temperature value of a hand pixel in °C (top-right) and in °F (bottom-right).
用热感摄像机/照相机测量你的第一次体温
**在这最后一节,我们将再次应用我们在本教程中学到的东西,但来自一个热视频源:一个热摄像机或热视频文件。
测量来自热视频的温度
如果你手头没有这些令人难以置信的相机,不要担心,我们将使用一个样本视频序列,gray16_sequence
文件夹,提取自 RGMVision ThermalCAM 1 。
在下面这段代码中,您将学会如何轻松地做到这一点。
打开measure_video_temperature.py
并导入 NumPy、OpenCV、OS 和 argparse 库:
# import the necessary packages
import cv2
import numpy as np
import os
import argparse
如果您熟悉 PyImageSearch 教程,那么您已经知道 argparse Python 库。我们用它在运行时给程序提供额外的信息(例如,命令行参数)。在这种情况下,我们将使用它来指定我们的热视频路径:
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video", required=True, help="path of the video sequence")
args = vars(ap.parse_args())
下面的代码向您展示了如何实现一个鼠标指针来轻松显示所选像素的温度,而不是选择一个固定点:
# create mouse global coordinates
x_mouse = 0
y_mouse = 0
# create thermal video fps variable (8 fps in this case)
fps = 8
# mouse events function
def mouse_events(event, x, y, flags, param):
# mouse movement event
if event == cv2.EVENT_MOUSEMOVE:
# update global mouse coordinates
global x_mouse
global y_mouse
x_mouse = x
y_mouse = y
# set up mouse events and prepare the thermal frame display
gray16_frame = cv2.imread("lighter_gray16_image.tiff", cv2.IMREAD_ANYDEPTH)
cv2.imshow('gray8', gray16_frame)
cv2.setMouseCallback('gray8', mouse_events)
从第 20-29 行,我们定义了鼠标事件函数。首先,当我们检测到鼠标移动(cv2.EVENT_MOUSEMOVE
)时,我们更新先前的x
和y
像素坐标。
我们建议您查看使用 Python 和 OpenCV 捕获鼠标点击事件的教程,以更深入地了解鼠标捕获事件。
# loop over the thermal video frames
for image in sorted(os.listdir(args["video"])):
# filter .tiff files (gray16 images)
if image.endswith(".tiff"):
# define the gray16 frame path
file_path = os.path.join(args["video"], image)
# open the gray16 frame
gray16_frame = cv2.imread(file_path, cv2.IMREAD_ANYDEPTH)
# calculate temperature
temperature_pointer = gray16_frame[y_mouse, x_mouse]
temperature_pointer = (temperature_pointer / 100) - 273.15
temperature_pointer = (temperature_pointer / 100) * 9 / 5 - 459.67
# convert the gray16 frame into a gray8
gray8_frame = np.zeros((120, 160), dtype=np.uint8)
gray8_frame = cv2.normalize(gray16_frame, gray8_frame, 0, 255, cv2.NORM_MINMAX)
gray8_frame = np.uint8(gray8_frame)
# colorized the gray8 frame using OpenCV colormaps
gray8_frame = cv2.applyColorMap(gray8_frame, cv2.COLORMAP_INFERNO)
# write pointer
cv2.circle(gray8_frame, (x_mouse, y_mouse), 2, (255, 255, 255), -1)
# write temperature
cv2.putText(gray8_frame, "{0:.1f} Fahrenheit".format(temperature_pointer), (x_mouse - 40, y_mouse - 15), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1)
# show the thermal frame
cv2.imshow("gray8", gray8_frame)
# wait 125 ms: RGMVision ThermalCAM1 frames per second = 8
cv2.waitKey(int((1 / fps) * 1000))
循环热视频序列,gray16_sequence
文件夹,并获得 gray16 温度值(第 37-46 行)。为了简化过程,我们使用 gray16 帧序列 TIFF 图像,而不是 gray16 视频文件(常见的压缩视频文件往往会丢失信息)。
在的第 49-51 行上,我们应用等式(1)并以 C(或 F)为单位计算我们的热感鼠标指针的温度:x_mouse
和y_mouse
。
然后,我们获得 gray8 图像来显示每帧的结果。
在第 71 行的上,我们等待了 125 毫秒,因为我们的 RGMVision ThermalCAM 1 提供了一个 8 fps 的流。
用热感相机测量温度
作为本教程的最后一点,我们将学习如何在 UVC (USB 视频类)热感摄像机的每一帧中循环,在我们的例子中,是 RGMVision ThermalCAM 1 ,同时测量和显示鼠标像素的温度。
打开measure_camera_video_temperature.py
并导入 NumPy 和 OpenCV 库:
# import the necessary packages
import cv2
import numpy as np
按照上面的**“从热视频测量温度”**部分定义鼠标事件功能:
# create mouse global coordinates
x_mouse = 0
y_mouse = 0
# mouse events function
def mouse_events(event, x, y, flags, param):
# mouse movement event
if event == cv2.EVENT_MOUSEMOVE:
# update global mouse coordinates
global x_mouse
global y_mouse
x_mouse = x
y_mouse = y
设置热感摄像机指数和分辨率,在我们的例子中,160x120
:
# set up the thermal camera index (thermal_camera = cv2.VideoCapture(0, cv2.CAP_DSHOW) on Windows OS)
thermal_camera = cv2.VideoCapture(0)
# set up the thermal camera resolution
thermal_camera.set(cv2.CAP_PROP_FRAME_WIDTH, 160)
thermal_camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 120)
在第 22 行,你应该选择你的相机 ID。如果您使用的是 Windows 操作系统,请确保指定您的后端视频库,例如,Direct Show (DSHOW): thermal_camera = cv2.VideoCapture(0, cv2.CAP_DSHOW)
。
欲了解更多信息,请访问带 OpenCV 概述的视频 I/O。
将热感摄像机设置为 gray16 源,并接收 raw 格式的数据。
# set up the thermal camera to get the gray16 stream and raw data
thermal_camera.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('Y','1','6',' '))
thermal_camera.set(cv2.CAP_PROP_CONVERT_RGB, 0)
第 30 行阻止 RGB 转换。
设置鼠标事件并准备热帧显示(行 33-35 ):
# set up mouse events and prepare the thermal frame display
grabbed, frame_thermal = thermal_camera.read()
cv2.imshow('gray8', frame_thermal)
cv2.setMouseCallback('gray8', mouse_events)
循环热摄像机帧,计算灰度 16 温度值(第 38-63 行)。
# loop over the thermal camera frames
while True:
# grab the frame from the thermal camera stream
(grabbed, thermal_frame) = thermal_camera.read()
# calculate temperature
temperature_pointer = thermal_frame[y_mouse, x_mouse]
# temperature_pointer = (temperature_pointer / 100) - 273.15
temperature_pointer = (temperature_pointer / 100) * 9 / 5 - 459.67
# convert the gray16 image into a gray8
cv2.normalize(thermal_frame, thermal_frame, 0, 255, cv2.NORM_MINMAX)
thermal_frame = np.uint8(thermal_frame)
# colorized the gray8 image using OpenCV colormaps
thermal_frame = cv2.applyColorMap(thermal_frame, cv2.COLORMAP_INFERNO)
# write pointer
cv2.circle(thermal_frame, (x_mouse, y_mouse), 2, (255, 255, 255), -1)
# write temperature
cv2.putText(thermal_frame, "{0:.1f} Fahrenheit".format(temperature_pointer), (x_mouse - 40, y_mouse - 15), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1)
# show the thermal frame
cv2.imshow('gray8', thermal_frame)
cv2.waitKey(1)
# do a bit of cleanup
thermal_camera.release()
cv2.destroyAllWindows()
我们在第 49 行和第 50 行将灰色 16 图像转换为灰色 8,以显示结果。
然后我们应用我们最喜欢的热调色板,就像我们已经学过的,写下温度值和指针位置。
在这里!
一个使用热感相机和 OpenCV 的实时鼠标温度测量仪!
这最后一部分代码和 RGMVision ThermalCAM 1 可在 RGM 视觉上获得。
汇总
在本教程中,我们已经了解了灰度 8 和灰度 16 图像(即最常见的热成像格式)之间的差异。我们学会了从图像中测量我们的第一个温度,只使用 Python 和 OpenCV 在不同的调色板中显示结果。更进一步,我们还发现了如何实时计算视频流和 UVC 热感相机的每个像素温度。
在下一个教程中,我们将实现一个简化的面部温度测量解决方案,这是 COVID 疫情中一个有价值的方法。您将能够将这些知识应用到您自己的实际项目中,例如,以简单明了的方式使用您的 Raspberry Pi。
引用信息
Garcia-Martin,R. “热视觉:使用 Python 和 OpenCV 从图像中测量第一个温度”, PyImageSearch ,P. Chugh,A. R. Gosthipaty,S. Huot,K. Kidriavsteva 和 R. Raha 编辑。,2022 年,【https://pyimg.co/mns3e
@incollection{
Garcia-Martin_2022_Measuring,
author = {
Raul Garcia-Martin},
title = {
Thermal Vision: Measuring Your First Temperature from an Image with {
P}ython and {
OpenCV}},
booktitle = {
PyImageSearch},
editor = {
Puneet Chugh and Aritra Roy Gosthipaty and Susan Huot and Kseniia Kidriavsteva and Ritwik Raha},
year = {
2022},
note = {
https://pyimg.co/mns3e},
}
要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!***
热视觉:用 PyTorch 和 YOLOv5 探测夜间目标(真实项目)
目录
热视觉:用 PyTorch 和 YOLOv5 进行夜间目标探测
在今天的教程中,您将使用深度学习并结合 Python 和 OpenCV 来检测热图像中的对象。正如我们已经发现的,热感相机让我们在绝对黑暗的环境中也能看到东西,所以我们将学习如何在任何可见光条件下探测物体!
本课包括:
- 通过 PyTorch 和 YOLOv5 进行深度学习的对象检测
- 发现前视红外热启动器数据集
- 使用 PyTorch 和 YOLOv5 进行热目标检测
本教程是我们关于红外视觉基础知识的 4 部分课程的最后一部分:
- 红外视觉介绍:近中远红外图像
- 热视觉:用 Python 和 OpenCV 从图像中测量你的第一个温度
- 热视觉:带 Python 和 OpenCV 的发热探测器(入门项目)
- 热视觉:用 PyTorch 和 YOLOv5 进行夜间物体探测(真实项目) (今日教程)
在本课结束时,您将学习如何使用热图像和深度学习以非常快速、简单和最新的方式检测不同的对象,仅使用四段代码!
要了解如何利用 YOLOv5 使用您的自定义热成像数据集, 继续阅读 。
热视觉:用 PyTorch 和 YOLOv5 进行夜间目标探测
通过 PyTorch 和 YOLOv 进行深度学习的物体检测 5
在我们的上一篇教程中,我们介绍了如何在实际解决方案中应用使用 Python、OpenCV 和传统机器学习方法从热图像中测量的温度。
从这一点出发,并基于本课程中涵盖的所有内容,PyImageSearch 团队激发您的想象力,在任何热成像情况下脱颖而出,但之前会为您提供这种令人难以置信的组合的另一个强大而真实的例子:计算机视觉+热成像。
在这种情况下,我们将了解计算机如何在黑暗中实时区分不同的对象类别。
在开始本教程之前,为了更好地理解,我们鼓励你在 PyImageSearch 大学参加 Torch Hub 系列课程,或者获得一些 PyTorch 和深度学习的经验。和所有 PyImageSearch 大学课程一样,我们会一步一步涵盖各个方面。
正如在 Torch Hub 系列#3 中所解释的:YOLOv5 和 SSD——关于对象检测的模型,yolov 5————你只看一次 ( 图 1 ,2015)版本 5——是最强大的最先进的卷积神经网络模型之一的第五个版本。这种快速对象检测器模型通常在 COCO 数据集上训练,这是一个开放访问的微软 RGB 成像数据库,由 33 万张图像、91 个对象类和 250 万个标记实例组成。
这种强大的组合使 YOLOv5 成为即使在我们的定制成像数据集中检测对象的完美模型。为了获得热物体检测器,我们将使用迁移学习(即,在专门为自动驾驶汽车解决方案收集的真实热成像数据集上训练 COCO 预训练的 YOLOv5 模型)。
发现前视红外热启动器数据集
我们将用来训练预训练 YOLOv5 模型的热成像数据集是免费的 Teledyne FLIR ADAS 数据集。
该数据库由 14,452 张灰度 8 和灰度 16 格式的热图像组成,据我们所知,这允许我们测量任何像素温度。用车载热感相机在加州的一些街道上拍摄的 14452 张灰度图像都被手工加上了边框,如图 2 所示**。我们将使用这些注释(标签+边界框)来检测该数据集中预定义的四个类别中的四个不同的对象类别:car
、person
、bicycle
和dog
。**
提供了一个带有 COCO 格式注释的 JSON 文件。为了简化本教程,我们给出了 YOLOv5 PyTorch 格式的注释。你可以找到一个labels
文件夹,里面有每个 gray8 图像的单独注释。
我们还将数据集减少到 1,772 张图像:1000 张用于训练我们预先训练的 YOLOv5 模型,772 张用于验证它(即,大约 60-40%的训练-验证分离)。这些图像是从原始数据集的训练部分中选择的。
利用 PyTorch 和 YOLOv 探测热物体 5
一旦我们学会了到目前为止看到的所有概念…我们来玩吧!
配置您的开发环境
要遵循这个指南,您需要在您的系统上安装 OpenCV 库。
幸运的是,OpenCV 可以通过 pip 安装:
$ pip install opencv-contrib-python
如果您需要帮助配置 OpenCV 的开发环境,我们强烈推荐阅读我们的 pip 安装 OpenCV 指南——它将在几分钟内让您启动并运行。
在配置开发环境时遇到了问题?
说了这么多,你是:
- 时间紧迫?
- 了解你雇主的行政锁定系统?
- 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
- 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***
*那今天就加入 PyImageSearch 大学吧!
获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。
最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!
项目结构
我们首先需要回顾我们的项目目录结构。
首先访问本教程的 “下载” 部分,检索源代码和示例图像。
从这里,看一下目录结构:
$ tree --dirsfirst
.
└── yolov5
├── data
├── models
├── utils
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── ...
└── val.py
1 directory, XX files
我们通过克隆官方的 YOLOv5 库来建立这个结构。
# clone the yolov5 repository from GitHub and install some necessary packages (requirements.txt file)
!git clone https://github.com/ultralytics/yolov5
%cd yolov5
%pip install -qr requirements.txt
参见行 2 和 3 上的代码。
注意,我们还安装了在requirements.txt
文件(第 4 行)中指出的所需库:Matplotlib、NumPy、OpenCV、PyTorch 等。
在yolov5
文件夹中,我们可以找到在我们的任何项目中使用 YOLOv5 所需的所有文件:
data
:包含管理 COCO 等不同数据集所需的信息。- 我们可以在另一种标记语言(YAML)格式中找到所有的 YOLOv5 CNN 结构,这是一种用于编程语言的对人类友好的数据序列化语言。
utils
:包括一些必要的 Python 文件来管理训练、数据集、信息可视化和通用项目工具。
yolov5
文件中的其余文件是必需的,但我们将只运行其中的两个:
- 是一个用来训练我们的模型的文件,它是我们上面克隆的存储库的一部分
[detect.py](https://github.com/ultralytics/yolov5/blob/master/detect.py)
:是一个通过推断检测到的对象来测试我们的模型的文件,它也是我们上面克隆的存储库的一部分
thermal_imaging_dataset
文件夹包括我们的 1,772 张灰度热成像图片。该文件夹包含图像(thermal_imaging_dataset/images
)和标签(thermal_imaging_dataset/labels
),分别被分成训练集和验证集、train
和val
文件夹。
thermal_imaging_video_test.mp4
是视频文件,我们将在其上测试我们的热目标检测模型。它包含 4,224 个以 30 帧/秒的速度获取的带有街道和高速公路场景的热帧。
# import PyTorch and check versions
import torch
from yolov5 import utils
display = utils.notebook_init()
打开您的yolov5.py
文件并导入所需的包(第 7 行和第 8 行),如果您正在使用 Google Colab 上的 Jupyter 笔记本,请检查您的笔记本功能(第 9 行)。
检查您的环境是否包含 GPU ( 图 3 ),以便在合理的时间内成功运行我们的下一个培训流程。
预训练
正如我们已经提到的,我们将使用迁移学习在我们的热成像数据集上训练我们的对象检测器模型,使用在 COCO 数据集上预先训练的 YOLOv5 CNN 架构作为起点。
为此,所选的经过训练的 YOLOv5 型号是 YOLOv5s 版本,因为它具有高速度精度性能。
训练
在设置好环境并满足所有要求后,让我们来训练我们的预训练模型!
# train pretrained YOLOv5s model on the custom thermal imaging dataset,
# basic parameters:
# - image size (img): image size of the thermal dataset is 640 x 512, 640 passed
# - batch size (batch): 16 by default, 16 passed
# - epochs (epochs): number of epochs, 30 passed
# - dataset (data): dataset in .yaml file format, custom thermal image dataset passed
# - pre-trained YOLOv5 model (weights): YOLOv5 model version, YOLOv5s (small version) passed
!python train.py --img 640 --batch 16 --epochs 30 --data thermal_image_dataset.yaml --weights yolov5s.pt
在第 18 行上,导入 PyTorch 和 YOLOv5 实用程序(第 7-9 行)后,我们通过指定以下参数运行train.py
文件:
-
img
:要通过我们模型的训练图像的图像大小。在我们的例子中,热图像有一个640x512
分辨率,所以我们指定最大尺寸,640 像素。 -
batch
:批量大小。我们设置了 16 个图像的批量大小。 -
epochs
:训练时代。在一些测试之后,我们将 30 个时期确定为一个很好的迭代次数。 -
data
: YAML 数据集文件。图 4 显示了我们的数据集文件。它指向 YOLOv5 数据集的结构,前面解释过:thermal_imaging_datasimg/train
thermal_imaging_dataset/labels/train
,用于训练,
thermal_imaging_datasimg/val
thermal_imaging_dataset/labels/val
,用于验证。
还表示班级的数量
nc: 4
,以及班级名称names: ['bicycle', 'car', 'dog', 'person']
。这个 YAML 数据集文件应该位于
yolov5/data
中。 -
weights
:在 COCO 数据集上计算预训练模型的权重,在我们的例子中是 YOLOv5s。yolov5s.pt
文件是包含这些权重的预训练模型,位于yolov5/models
。
这就是我们训练模型所需要的!
让我们看看结果吧!
在 0.279 小时内在 GPU NVIDIA Tesla T4 中完成 30 个纪元后,我们的模型已经学会检测类别person
、car
、bicycle
和dog
,达到平均 50.7%的平均精度,mAP (IoU = 0.5) = 0.507,如图图 5 所示。这意味着我们所有类的平均预测值为 50.7%,交集为 0.5(IoU,图 6 )。
如图图 6 所示,当比较原始和预测时,并集上的交集(IoU)是边界框的右重叠。
因此,对于我们的person
类,我们的模型平均正确地检测到 77.7%的情况,考虑到当有 50%或更高的边界框交集时的正确预测。
图 7 比较了两幅原始图像、它们的手绘边界框以及它们的预测结果。
尽管这超出了本教程的范围,但重要的是要注意我们的数据集是高度不平衡的,对于我们的bicycle
和dog
类,分别只有 280 和 31 个标签。这就是为什么我们分别得到 mAP bicycle
(IoU = 0.5) = 0.456 和 mAP dog
(IoU = 0.5) = 0.004。
最后,为了验证我们的结果,图 8 显示了在训练(左上)和验证(左下)过程中的分类损失,以及在 IoU 50% ( 中右)时的平均精度,mAP (IoU = 0.5),所有类别经过 30 个时期。
但是现在,让我们来测试我们的模型!
检测
为此,我们将使用位于项目根的thermal_imaging_video_test.mp4
,通过 Python 文件detect.py
将它传递到我们的模型层。
# test the trained model (night_object_detector.pt) on a thermal imaging video,
# parameters:
# - trained model (weights): model trained in the previous step, night_object_detector.pt passed
# - image size (img): frame size of the thermal video is 640 x 512, 640 passed
# - confidence (conf): confidence threshold, only the inferences higher than this value will be shown, 0.35 passed
# - video file (source): thermal imaging video, thermal_imaging_video.mp4 passed
!python detect.py --weights runs/train/exp/weights/best.pt --img 640 --conf 0.35 --source ../thermal_imaging_video.mp4
第 27 行显示了如何做。
我们通过指定以下参数来运行detect.py
:
- 指向我们训练好的模型。在
best.pt
文件(runs/train/exp/weights/best.pt
)中收集的计算重量。 img
:将通过我们模型的测试图像的图像尺寸。在我们的例子中,视频中的热图像具有640x512
分辨率,因此我们将最大尺寸指定为 640 像素。conf
:每次检测的置信度。该阈值建立了检测的概率水平,根据该水平,检测被认为是正确的,因此被显示。我们设定置信度为 35%。source
:测试模型的图像,在我们的例子中,是视频文件thermal_imaging_video.mp4
。
来测试一下吧!
图 9 展示了我们良好结果的 GIF 图!
正如我们已经指出的,该视频的夜间物体检测已经获得了 35%的置信度。为了修改这个因素,我们应该检查图 10 中的曲线,该曲线绘制了精度与置信度的关系。
汇总
我们要感谢超极本的伟大工作。我们发现他们的[train.py](https://github.com/ultralytics/yolov5/blob/master/train.py)
和[detect.py](https://github.com/ultralytics/yolov5/blob/master/detect.py)
文件非常棒,所以我们把它们放在了这个帖子里。
在本教程中,我们学习了如何在任何光线条件下检测不同的物体,结合热视觉和深度学习,使用 CNN YOLOv5 架构和我们的自定义热成像数据集。
为此,我们发现了如何在 FLIR Thermal Starter 数据集上训练最先进的 YOLOv5 模型,该模型先前是使用 Microsoft COCO 数据集训练的。
尽管热图像与 COCO 数据集的常见 RGB 图像完全不同,但获得的出色性能和结果显示了 YOLOv5 模型的强大功能。
我们可以得出结论,人工智能如今经历了令人难以置信的有用范式。
本教程向您展示了如何在实际应用中应用热视觉和深度学习(例如,自动驾驶汽车)。如果你想了解这个令人敬畏的话题,请查看 PyImageSearch 大学的自动驾驶汽车课程。
PyImageSearch 团队希望您已经喜欢并深入理解了本红外视觉基础课程中教授的所有概念。
下节课再见!
引用信息
Garcia-Martin,R. “热视觉:用 PyTorch 和 YOLOv5 进行夜间目标探测”(真实项目), PyImageSearch ,P. Chugh,A. R. Gosthipaty,S. Huot,K. Kidriavsteva 和 R. Raha 编辑。,2022 年,【https://pyimg.co/p2zsm
@incollection{
RGM_2022_PYTYv5,
author = {
Raul Garcia-Martin},
title = {
Thermal Vision: Night Object Detection with {
PyTorch} and {
YOLOv5} (real project)},
booktitle = {
PyImageSearch},
editor = {
Puneet Chugh and Aritra Roy Gosthipaty and Susan Huot and Kseniia Kidriavsteva and Ritwik Raha},
year = {
2022},
note = {
https://pyimg.co/p2zsm},
}
要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*
阈值处理:使用 OpenCV 的简单图像分割
原文:https://pyimagesearch.com/2014/09/08/thresholding-simple-image-segmentation-using-opencv/
悲剧。令人心碎。无法忍受。
这是我用来形容过去一周的三个词。
大约一周前,我的一个儿时密友在一场悲惨的车祸中去世了。
我和他一起上小学和中学。我们夏天在我的车道上玩滑板,冬天在我的后院玩滑雪板和雪橇。
从上周开始,我一直在旅行,参加他的葬礼。每一刻都极度痛苦。我发现很难集中注意力,我的思绪总是回到十多年前的记忆中。
老实说,我写这篇博客是一个奇迹。
但是如果有什么是我相信的,那就是生活应该被庆祝。没有比对爱人的怀旧回忆更好的庆祝方式了。
早在中学时代,我和他曾经浏览过 CCS 目录(是的,实际目录;他们没有在网上列出他们所有的产品,即使他们列出了,我们的互联网是拨号上网的,使用是由父母控制的。
我们会幻想下一步要买哪种滑板,或者更确切地说,我们会要求父母给我们买哪种滑板作为生日礼物。
像在上学前、上学中和放学后翻阅 CCS 目录这样的小回忆让我喜笑颜开。它们让日子过得更轻松。
所以在这篇文章中,我将展示如何使用 Python 和 OpenCV 执行基本的图像分割。
我们还会给它一个小小的滑板主题,只是为了向一位朋友致敬,他的记忆在我脑海中占据了很重的位置。
OpenCV 和 Python 版本:
这个例子将运行在 Python 2.7/Python 3.4+ 和 OpenCV 2.4.X/OpenCV 3.0+ 上。
cv2.threshold
功能
让我们先来看看 cv2.threshold 函数的签名:
(T, threshImage) = cv2.threshold(src, thresh, maxval, type)
第一个参数是我们的源图像,或者我们想要对其执行阈值处理的图像。这个图像应该是灰度的。
第二个参数thresh
是用于对灰度图像中的像素强度进行分类的阈值。
第三个参数maxval
,是图像中任何给定像素通过thresh
测试时使用的像素值。
最后,第四个参数是要使用的阈值方法。type
值可以是以下任一值:
cv2.THRESH_BINARY
cv2.THRESH_BINARY_INV
cv2.THRESH_TRUNC
cv2.THRESH_TOZERO
cv2.THRESH_TOZERO_INV
听起来很复杂?事实并非如此——我将为您展示每种阈值类型的示例。
然后,cv2.threshold
返回一个由两个值组成的元组。第一个值T
是用于阈值处理的值。在我们的例子中,这个值与我们传递给cv2.threshold
函数的thresh
值相同。
第二个值是我们实际的阈值图像。
无论如何,让我们继续探索一些代码。
阈值处理:使用 OpenCV 的简单图像分割
图像分割有多种形式。
聚类。压缩。边缘检测。区域增长。图形划分。分水岭。这样的例子不胜枚举。
但是在一开始,只有最基本的图像分割类型:阈值分割。
让我们看看如何使用 OpenCV 执行简单的图像分割。打开您最喜欢的编辑器,创建一个名为threshold.py
的文件,让我们开始吧:
# import the necessary packages
import argparse
import cv2
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True,
help = "Path to the image to be thresholded")
ap.add_argument("-t", "--threshold", type = int, default = 128,
help = "Threshold value")
args = vars(ap.parse_args())
# load the image and convert it to grayscale
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# initialize the list of threshold methods
methods = [
("THRESH_BINARY", cv2.THRESH_BINARY),
("THRESH_BINARY_INV", cv2.THRESH_BINARY_INV),
("THRESH_TRUNC", cv2.THRESH_TRUNC),
("THRESH_TOZERO", cv2.THRESH_TOZERO),
("THRESH_TOZERO_INV", cv2.THRESH_TOZERO_INV)]
# loop over the threshold methods
for (threshName, threshMethod) in methods:
# threshold the image and show it
(T, thresh) = cv2.threshold(gray, args["threshold"], 255, threshMethod)
cv2.imshow(threshName, thresh)
cv2.waitKey(0)
我们将从导入我们需要的两个包开始,线 2 和 3 上的argparse
和cv2
。
从那里,我们将在第 6-11 行解析我们的命令行参数。这里我们需要两个参数。第一个,--image
,是我们想要阈值化的图像的路径。第二个,--threshold
,是将传递给cv2.threshold
函数的阈值。
从那里,我们将从磁盘加载图像,并在第 14 行和第 15 行将其转换为灰度。我们转换为灰度,因为cv2.threshold
期望一个单通道图像。
第 18-23 行定义了我们的阈值方法列表。
我们从第 26 行的开始循环我们的阈值方法。
从那里,我们在第 28 行的上应用实际阈值方法。我们将灰度图像作为第一个参数传递,将命令行提供的阈值作为第二个参数传递,将 255(白色)作为阈值测试通过时的值作为第三个参数传递,最后将阈值方法本身作为最后一个参数传递。
最后,阈值图像显示在行 29 和 30 上。
我们来看一些结果。打开您的终端,导航到我们的代码目录,并执行以下命令:
$ python threshold.py --image images/skateboard_decks.png --threshold 245
在本例中,我们使用值 245 进行阈值测试。如果输入图像中的像素通过了阈值测试,它的值将被设置为 255。
现在,让我们来看看结果:
Figure 1: Applying cv2.threshold with cv2.THRESH_BINARY.
使用cv2.THRESH_BINARY
将滑板分割成白色背景的黑色。
要反转颜色,只需使用cv2.THRESH_BINARY_INV
,如下所示:
Figure 2: Applying cv2.threshold with cv2.THRESH_BINARY_INV.
还不错。我们还能做什么?
Figure 3: Applying cv2.threshold with cv2.THRESH_TRUNC.
如果源像素不大于提供的阈值,使用cv2.THRESH_TRUNC
保持像素强度不变。
然后我们有cv2.THRESH_TOZERO
,如果源像素不大于提供的阈值,它将源像素设置为零:
Figure 4: Applying cv2.threshold with cv2.THRESH_TOZERO.
最后,我们也可以使用cv2.THRESH_TOZERO_INV
来反转这个行为:
Figure 5: Applying cv2.threshold with cv2.THRESH_TOZERO_INV.
没什么!
摘要
在这篇博文中,我向你展示了如何执行最基本的图像分割形式:阈值分割。
为了执行阈值处理,我们使用了cv2.threshold
函数。
OpenCV 为我们提供了五种基本的阈值分割方法,包括:cv2.THRESH_BINARY
、cv2.THRESH_BINARY_INV
、cv2.THRESH_TRUNC
、cv2.THRESH_TOZERO
、cv2.THRESH_TOZERO_INV
。
最重要的是,确保使用thresh
值,因为它会根据您提供的值给出不同的结果。
在以后的文章中,我将向你展示如何用自动确定阈值,不需要调整参数!
竖起大拇指:手势识别。
原文:https://pyimagesearch.com/2015/02/05/thumbs-hand-gesture-recognition/
我需要为这篇文章道歉——如果有一些明显的语法错误,我很抱歉。
你看,我熬了一个通宵。
我的眼睛布满血丝,呆滞无神,就像《行尸走肉》里的某样东西。我的大脑感觉像被大锤敲打的土豆泥。我真的真的需要洗个澡——出于某种原因,我认为在从健身房回来后立即开始黑客马拉松是个好主意。
但是在接下来的 10 个小时里,我想发布一个关于这个通宵黑客马拉松的目的的快速更新…
昨天,PyImageSearch 大师 Kickstarter 实现了它的第一个目标(为你的移动设备开发计算机视觉应用)。我需要想出第二个连续目标。经过几分钟的反复思考,我想到了:
手势识别。
手势识别是 PyImageSearch 博客上 点击率最高的教程 之一。每天我都会收到至少 2-3 封邮件,询问如何用 Python 和 OpenCV 进行手势识别。
让我告诉你,如果我们达到了 PyImageSearch 大师 Kickstarter 的第二个延伸目标,我将在课程中介绍手势识别!
因此,如果你对加入 PyImageSearch 大师计算机视觉课程犹豫不决,现在是时候了!在你们的帮助下,我们将能够在 PyImageSearch Gurus 中实现手势识别。
记住,PyImageSearch 大师们的大门将在一周后关闭,直到八月才会再次打开。
课程还有一些空位, 所以不要等着错过!
现在就行动起来,在大门关闭之前,在 PyImageSearch 大师中获得自己的一席之地。
是时候了。PyImageSearch 大师 Kickstarter 正式上线。
原文:https://pyimagesearch.com/2015/01/14/time-pyimagesearch-gurus-kickstarter-officially-live/
PyImageSaerch Gurus Kickstarter 正式上线了!
您可以使用以下链接支持 PyImageSearch 大师 Kickstarter 活动:
记住,只有为数不多的低价早鸟名额&提前入场 — 如果你想得到你的位置,你一定要马上行动!
非常感谢你对我和 PyImageSearch 博客的支持。
希望能在另一边见到你!
火炬中心系列#1:火炬中心简介
原文:https://pyimagesearch.com/2021/12/20/torch-hub-series-1-introduction-to-torch-hub/
在本教程中,您将学习 PyTorch 火炬中心的基础知识。
本课是关于火炬中心的 6 部分系列的第 1 部分:
- 火炬轮毂系列#1:火炬轮毂简介(本教程)
- 火炬中心系列#2: VGG 和雷斯内特
- 火炬轮毂系列#3: YOLO v5 和 SSD——物体检测模型
- 火炬轮毂系列# 4:PGAN——甘模型
- 火炬轮毂系列# 5:MiDaS——深度估计模型
- 火炬中枢系列#6:图像分割
要学习如何使用火炬中枢, 只要坚持阅读。
火炬中心介绍
那是 2020 年,我和我的朋友们夜以继日地完成我们最后一年的项目。像我们这一年的大多数学生一样,我们决定把它留到最后是个好主意。
这不是我们最明智的想法。接下来是永无休止的模型校准和训练之夜,烧穿千兆字节的云存储,并维护深度学习模型结果的记录。
我们为自己创造的环境不仅损害了我们的效率,还影响了我们的士气。由于我的其他队友的个人才华,我们设法完成了我们的项目。
回想起来,我意识到如果我们选择了一个更好的生态系统来工作,我们的工作会更有效率,也更令人愉快。
幸运的是,你不必犯和我一样的错误。
PyTorch 的创建者经常强调,这一计划背后的一个关键意图是弥合研究和生产之间的差距。PyTorch 现在在许多领域与它的同时代人站在一起,在研究和生产生态系统中被平等地利用。
他们实现这一目标的方法之一是通过火炬中心。火炬中心作为一个概念,是为了进一步扩展 PyTorch 作为一个基于生产的框架的可信度。在今天的教程中,我们将学习如何利用 Torch Hub 来存储和发布预先训练好的模型,以便广泛使用。
什么是火炬中心?
在计算机科学中,许多人认为研究和生产之间桥梁的一个关键拼图是可重复性。基于这一理念,PyTorch 推出了 Torch Hub,这是一个应用程序可编程接口(API ),它允许两个程序相互交互,并增强了工作流程,便于研究再现。
Torch Hub 允许您发布预先训练好的模型,以帮助研究共享和再现。利用 Torch Hub 的过程很简单,但是在继续之前,让我们配置系统的先决条件!
配置您的开发环境
要遵循这个指南,您需要在您的系统上安装 OpenCV 库。
幸运的是,OpenCV 可以通过 pip 安装:
$ pip install opencv-contrib-python
如果你需要帮助为 OpenCV 配置开发环境,我强烈推荐阅读我们的 pip 安装 OpenCV 指南——它将在几分钟内让你启动并运行。
在配置开发环境时遇到了问题?
说了这么多,你是:
- 时间紧迫?
- 了解你雇主的行政锁定系统?
- 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
- 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***
*那今天就加入 PyImageSearch 大学吧!
获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。
最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!
项目结构
我们首先需要回顾我们的项目结构。
首先访问本教程的 “下载” 部分,检索源代码和示例图像。
在转到目录之前,我们先来看看图 2 中的项目结构。
今天,我们将使用两个目录。这有助于您更好地理解 Torch Hub 的使用。
子目录是我们初始化和训练模型的地方。在这里,我们将创建一个hubconf.py
脚本。hubconf.py
脚本包含名为entry_points
的可调用函数。这些可调用的函数初始化并返回用户需要的模型。因此,这个脚本将把我们自己创建的模型连接到 Torch Hub。
在我们的主项目目录中,我们将使用torch.hub.load
从 Torch Hub 加载我们的模型。在用预先训练的权重加载模型之后,我们将在一些样本数据上对其进行评估。
火炬中心概观
Torch Hub 已经托管了一系列用于各种任务的模型,如图 3 所示。
如你所见,Torch Hub 在其官方展示中总共接受了 42 个研究模型。每个模型属于以下一个或多个标签:音频、生成、自然语言处理(NLP)、可脚本化和视觉。这些模型也已经在广泛接受的基准数据集上进行了训练(例如, Kinetics 400 和 COCO 2017 )。
使用torch.hub.load
函数很容易在您的项目中使用这些模型。让我们来看一个它是如何工作的例子。
我们将查看火炬中心的官方文件using a DCGAN在 fashion-gen 上接受培训来生成一些图片。
(如果你想了解更多关于 DCGANs 的信息,一定要看看这个 博客 。)
# USAGE
# python inference.py
# import the necessary packages
import matplotlib.pyplot as plt
import torchvision
import argparse
import torch
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--num-images", type=int, default=64,
help="# of images you want the DCGAN to generate")
args = vars(ap.parse_args())
# check if gpu is available for use
useGpu = True if torch.cuda.is_available() else False
# load the DCGAN model
model = torch.hub.load("facebookresearch/pytorch_GAN_zoo:hub", "DCGAN",
pretrained=True, useGPU=useGpu)
在的第 11-14 行,我们创建了一个参数解析器,让用户可以更自由地选择生成图像的批量大小。
要使用脸书研究所预训练的 DCGAN 模型,我们只需要torch.hub.load
函数,如**第 20 和 21 行所示。**这里的torch.hub.load
函数接受以下参数:
repo_or_dir
:如果source
参数被设置为github
,则格式为repo_owner/repo_name:branch/tag_name
的存储库名称。否则,它将指向您的本地计算机中所需的路径。entry_point
:要在 torch hub 中发布模型,您需要在您的存储库/目录中有一个名为hubconf.py
的脚本。在该脚本中,您将定义称为入口点的普通可调用函数。调用入口点以返回期望的模型。稍后你会在这篇博客中了解更多关于entry_point
的内容。pretrained
和useGpu
:这些属于这个函数的*args
或参数旗帜。这些参数用于可调用模型。
现在,这不是火炬中心提供的唯一主要功能。您可以使用其他几个值得注意的函数,比如torch.hub.list
来列出所有属于存储库的可用入口点(可调用函数),以及torch.hub.help
来显示目标入口点的文档 docstring。
# generate random noise to input to the generator
(noise, _) = model.buildNoiseData(args["num_images"])
# turn off autograd and feed the input noise to the model
with torch.no_grad():
generatedImages = model.test(noise)
# reconfigure the dimensions of the images to make them channel
# last and display the output
output = torchvision.utils.make_grid(generatedImages).permute(
1, 2, 0).cpu().numpy()
plt.imshow(output)
plt.show()
在第 24 行的上,我们使用一个名为buildNoiseData
的被调用模型专有的函数来生成随机输入噪声,同时牢记输入大小。
关闭自动渐变( Line 27 ),我们通过给模型添加噪声来生成图像。
在绘制图像之前,我们对第 32-35 行的图像进行了维度整形(由于 PyTorch 使用通道优先张量,我们需要使它们再次成为通道最后张量)。输出将类似于图 4** 。**
瞧吧!这就是你使用预先训练的最先进的 DCGAN 模型所需要的一切。在火炬中心使用预先训练好的模型是那容易。然而,我们不会就此止步,不是吗?
打电话给一个预先训练好的模型来看看最新的最先进的研究表现如何是好的,但是当我们使用我们的研究产生最先进的结果时呢?为此,我们接下来将学习如何在 Torch Hub 上发布我们自己创建的模型。
在 PyTorch 车型上使用 Torch Hub
让我们回到 2021 年 7 月 12 日,Adrian Rosebrock 发布了一篇博文,教你如何在 PyTorch 上构建一个简单的 2 层神经网络。该博客教你定义自己的简单神经网络,并在用户生成的数据上训练和测试它们。
今天,我们将训练我们的简单神经网络,并使用 Torch Hub 发布它。我不会对代码进行全面剖析,因为已经有相关教程了。关于构建一个简单的神经网络的详细而精确的探究,请参考这篇博客。
构建简单的神经网络
接下来,我们将检查代码的突出部分。为此,我们将进入子目录。首先,让我们在mlp.py
中构建我们的简单神经网络!
# import the necessary packages
from collections import OrderedDict
import torch.nn as nn
# define the model function
def get_training_model(inFeatures=4, hiddenDim=8, nbClasses=3):
# construct a shallow, sequential neural network
mlpModel = nn.Sequential(OrderedDict([
("hidden_layer_1", nn.Linear(inFeatures, hiddenDim)),
("activation_1", nn.ReLU()),
("output_layer", nn.Linear(hiddenDim, nbClasses))
]))
# return the sequential model
return mlpModel
行 6 上的get_training_model
函数接受参数(输入大小、隐藏层大小、输出类别)。在函数内部,我们使用nn.Sequential
创建一个 2 层神经网络,由一个带有 ReLU activator 的隐藏层和一个输出层组成(第 8-12 行)。
训练神经网络
我们不会使用任何外部数据集来训练模型。相反,我们将自己生成数据点。让我们进入train.py
。
# import the necessary packages
from pyimagesearch import mlp
from torch.optim import SGD
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
import torch.nn as nn
import torch
import os
# define the path to store your model weights
MODEL_PATH = os.path.join("output", "model_wt.pth")
# data generator function
def next_batch(inputs, targets, batchSize):
# loop over the dataset
for i in range(0, inputs.shape[0], batchSize):
# yield a tuple of the current batched data and labels
yield (inputs[i:i + batchSize], targets[i:i + batchSize])
# specify our batch size, number of epochs, and learning rate
BATCH_SIZE = 64
EPOCHS = 10
LR = 1e-2
# determine the device we will be using for training
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print("[INFO] training using {}...".format(DEVICE))
首先,我们在第 11 行创建一个路径来保存训练好的模型权重,稍后会用到。第 14-18 行上的next_batch
函数将作为我们项目的数据生成器,产生用于高效训练的批量数据。
接下来,我们设置超参数(第 21-23 行),如果有兼容的 GPU 可用,则将我们的DEVICE
设置为cuda
(第 26 行)。
# generate a 3-class classification problem with 1000 data points,
# where each data point is a 4D feature vector
print("[INFO] preparing data...")
(X, y) = make_blobs(n_samples=1000, n_features=4, centers=3,
cluster_std=2.5, random_state=95)
# create training and testing splits, and convert them to PyTorch
# tensors
(trainX, testX, trainY, testY) = train_test_split(X, y,
test_size=0.15, random_state=95)
trainX = torch.from_numpy(trainX).float()
testX = torch.from_numpy(testX).float()
trainY = torch.from_numpy(trainY).float()
testY = torch.from_numpy(testY).float()
在的第 32 行和第 33 行,我们使用make_blobs
函数来模拟实际三类数据集的数据点。使用 scikit-learn 的 train_test_split
函数,我们创建数据的训练和测试分割。
# initialize our model and display its architecture
mlp = mlp.get_training_model().to(DEVICE)
print(mlp)
# initialize optimizer and loss function
opt = SGD(mlp.parameters(), lr=LR)
lossFunc = nn.CrossEntropyLoss()
# create a template to summarize current training progress
trainTemplate = "epoch: {} test loss: {:.3f} test accuracy: {:.3f}"
在第 45 行上,我们从mlp.py
模块调用get_training_model
函数并初始化模型。
我们选择随机梯度下降作为优化器(第 49 行),交叉熵损失作为损失函数(第 50 行)。
第 53 行上的trainTemplate
变量将作为字符串模板打印精度和损耗。
# loop through the epochs
for epoch in range(0, EPOCHS):
# initialize tracker variables and set our model to trainable
print("[INFO] epoch: {}...".format(epoch + 1))
trainLoss = 0
trainAcc = 0
samples = 0
mlp.train()
# loop over the current batch of data
for (batchX, batchY) in next_batch(trainX, trainY, BATCH_SIZE):
# flash data to the current device, run it through our
# model, and calculate loss
(batchX, batchY) = (batchX.to(DEVICE), batchY.to(DEVICE))
predictions = mlp(batchX)
loss = lossFunc(predictions, batchY.long())
# zero the gradients accumulated from the previous steps,
# perform backpropagation, and update model parameters
opt.zero_grad()
loss.backward()
opt.step()
# update training loss, accuracy, and the number of samples
# visited
trainLoss += loss.item() * batchY.size(0)
trainAcc += (predictions.max(1)[1] == batchY).sum().item()
samples += batchY.size(0)
# display model progress on the current training batch
trainTemplate = "epoch: {} train loss: {:.3f} train accuracy: {:.3f}"
print(trainTemplate.format(epoch + 1, (trainLoss / samples),
(trainAcc / samples)))
循环训练时期,我们初始化损失(行 59-61 )并将模型设置为训练模式(行 62 )。
使用next_batch
函数,我们遍历一批批训练数据(第 65 行)。在将它们加载到设备(线 68 )后,在线 69 上获得数据批次的预测。这些预测然后被输入到损失函数中进行损失计算(第 70 行)。
使用zero_grad
( 线 74 )冲洗梯度,然后在线 75 上反向传播。最后,在行 76 上更新优化器参数。
对于每个时期,训练损失、精度和样本大小变量被升级(行 80-82 ),并使用行 85 上的模板显示。
# initialize tracker variables for testing, then set our model to
# evaluation mode
testLoss = 0
testAcc = 0
samples = 0
mlp.eval()
# initialize a no-gradient context
with torch.no_grad():
# loop over the current batch of test data
for (batchX, batchY) in next_batch(testX, testY, BATCH_SIZE):
# flash the data to the current device
(batchX, batchY) = (batchX.to(DEVICE), batchY.to(DEVICE))
# run data through our model and calculate loss
predictions = mlp(batchX)
loss = lossFunc(predictions, batchY.long())
# update test loss, accuracy, and the number of
# samples visited
testLoss += loss.item() * batchY.size(0)
testAcc += (predictions.max(1)[1] == batchY).sum().item()
samples += batchY.size(0)
# display model progress on the current test batch
testTemplate = "epoch: {} test loss: {:.3f} test accuracy: {:.3f}"
print(testTemplate.format(epoch + 1, (testLoss / samples),
(testAcc / samples)))
print("")
# save model to the path for later use
torch.save(mlp.state_dict(), MODEL_PATH)
我们将模型设置为eval
模式进行模型评估,并在训练阶段进行同样的操作,除了反向传播。
在第 121 行,我们有保存模型权重以备后用的最重要步骤。
让我们来评估我们的模型的划时代的性能!
[INFO] training using cpu...
[INFO] preparing data...
Sequential(
(hidden_layer_1): Linear(in_features=4, out_features=8, bias=True)
(activation_1): ReLU()
(output_layer): Linear(in_features=8, out_features=3, bias=True)
)
[INFO] epoch: 1...
epoch: 1 train loss: 0.798 train accuracy: 0.649
epoch: 1 test loss: 0.788 test accuracy: 0.613
[INFO] epoch: 2...
epoch: 2 train loss: 0.694 train accuracy: 0.665
epoch: 2 test loss: 0.717 test accuracy: 0.613
[INFO] epoch: 3...
epoch: 3 train loss: 0.635 train accuracy: 0.669
epoch: 3 test loss: 0.669 test accuracy: 0.613
...
[INFO] epoch: 7...
epoch: 7 train loss: 0.468 train accuracy: 0.693
epoch: 7 test loss: 0.457 test accuracy: 0.740
[INFO] epoch: 8...
epoch: 8 train loss: 0.385 train accuracy: 0.861
epoch: 8 test loss: 0.341 test accuracy: 0.973
[INFO] epoch: 9...
epoch: 9 train loss: 0.286 train accuracy: 0.980
epoch: 9 test loss: 0.237 test accuracy: 0.993
[INFO] epoch: 10...
epoch: 10 train loss: 0.211 train accuracy: 0.985
epoch: 10 test loss: 0.173 test accuracy: 0.993
因为我们是根据我们设定的范例生成的数据进行训练,所以我们的训练过程很顺利,最终达到了 0.985 的训练精度。
配置hubconf.py
脚本
模型训练完成后,我们的下一步是在 repo 中配置hubconf.py
文件,使我们的模型可以通过 Torch Hub 访问。
# import the necessary packages
import torch
from pyimagesearch import mlp
# define entry point/callable function
# to initialize and return model
def custom_model():
""" # This docstring shows up in hub.help()
Initializes the MLP model instance
Loads weights from path and
returns the model
"""
# initialize the model
# load weights from path
# returns model
model = mlp.get_training_model()
model.load_state_dict(torch.load("model_wt.pth"))
return model
如前所述,我们在 7 号线的上创建了一个名为custom_model
的入口点。在entry_point
内部,我们从mlp.py
模块(第 16 行)初始化简单的神经网络。接下来,我们加载之前保存的权重(第 17 行)。当前的设置使得这个函数将在您的项目目录中寻找模型权重。您可以在云平台上托管权重,并相应地选择路径。
现在,我们将使用 Torch Hub 访问该模型,并在我们的数据上测试它。
用torch.hub.load
来称呼我们的模型
回到我们的主项目目录,让我们进入hub_usage.py
脚本。
# USAGE
# python hub_usage.py
# import the necessary packages
from pyimagesearch.data_gen import next_batch
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
import torch.nn as nn
import argparse
import torch
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-b", "--batch-size", type=int, default=64,
help="input batch size")
args = vars(ap.parse_args())
在导入必要的包之后,我们为用户创建一个参数解析器(第 13-16 行)来输入数据的批量大小。
# load the model using torch hub
print("[INFO] loading the model using torch hub...")
model = torch.hub.load("cr0wley-zz/torch_hub_test:main",
"custom_model")
# generate a 3-class classification problem with 1000 data points,
# where each data point is a 4D feature vector
print("[INFO] preparing data...")
(X, Y) = make_blobs(n_samples=1000, n_features=4, centers=3,
cluster_std=2.5, random_state=95)
# create training and testing splits, and convert them to PyTorch
# tensors
(trainX, testX, trainY, testY) = train_test_split(X, Y,
test_size=0.15, random_state=95)
trainX = torch.from_numpy(trainX).float()
testX = torch.from_numpy(testX).float()
trainY = torch.from_numpy(trainY).float()
testY = torch.from_numpy(testY).float()
在的第 20 行和第 21 行,我们使用torch.hub.load
来初始化我们自己的模型,就像前面我们加载 DCGAN 模型一样。模型已经初始化,权重已经根据子目录中的hubconf.py
脚本中的入口点加载。正如你所注意到的,我们给子目录github
作为参数。
现在,为了评估模型,我们将按照我们在模型训练期间创建的相同方式创建数据(第 26 行和第 27 行),并使用train_test_split
创建数据分割(第 31-36 行)。
# initialize the neural network loss function
lossFunc = nn.CrossEntropyLoss()
# set device to cuda if available and initialize
# testing loss and accuracy
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
testLoss = 0
testAcc = 0
samples = 0
# set model to eval and grab a batch of data
print("[INFO] setting the model in evaluation mode...")
model.eval()
(batchX, batchY) = next(next_batch(testX, testY, args["batch_size"]))
在第 39 行上,我们初始化交叉熵损失函数,如在模型训练期间所做的。我们继续初始化第 44-46 行的评估指标。
将模型设置为评估模式(行 50 ),并抓取单批数据供模型评估(行 51 )。
# initialize a no-gradient context
with torch.no_grad():
# load the data into device
(batchX, batchY) = (batchX.to(DEVICE), batchY.to(DEVICE))
# pass the data through the model to get the output and calculate
# loss
predictions = model(batchX)
loss = lossFunc(predictions, batchY.long())
# update test loss, accuracy, and the number of
# samples visited
testLoss += loss.item() * batchY.size(0)
testAcc += (predictions.max(1)[1] == batchY).sum().item()
samples += batchY.size(0)
print("[INFO] test loss: {:.3f}".format(testLoss / samples))
print("[INFO] test accuracy: {:.3f}".format(testAcc / samples))
关闭自动梯度(行 54 ),我们将该批数据加载到设备中,并将其输入到模型中(行 56-60 )。lossFunc
继续计算线 61 上的损耗。
在损失的帮助下,我们更新了第 66 行上的精度变量,以及一些其他度量,如样本大小(行 67 )。
让我们看看这个模型的效果如何!
[INFO] loading the model using torch hub...
[INFO] preparing data...
[INFO] setting the model in evaluation mode...
Using cache found in /root/.cache/torch/hub/cr0wley-zz_torch_hub_test_main
[INFO] test loss: 0.086
[INFO] test accuracy: 0.969
由于我们使用训练模型时使用的相同范例创建了我们的测试数据,因此它的表现与预期一致,测试精度为 0.969 。
摘要
在当今的研究领域,结果的再现是多么重要,这一点我怎么强调都不为过。特别是在机器学习方面,我们已经慢慢地达到了一个新的研究想法变得日益复杂的地步。在这种情况下,研究人员拥有一个平台来轻松地将他们的研究和结果公之于众,这是一个巨大的负担。
作为一名研究人员,当您已经有足够多的事情要担心时,拥有一个工具,使用一个脚本和几行代码来公开您的模型和结果,对我们来说是一个巨大的福音。当然,作为一个项目,随着时间的推移,火炬中心将更多地根据用户的需求进行发展。尽管如此,Torch Hub 的创建所倡导的生态系统将帮助未来几代人的机器学习爱好者。
引用信息
Chakraborty,D. “火炬中心系列#1:火炬中心简介”, PyImageSearch ,2021,https://PyImageSearch . com/2021/12/20/Torch-Hub-Series-1-Introduction-to-Torch-Hub/
@article{
dev_2021_THS1,
author = {
Devjyoti Chakraborty},
title = {
{
Torch Hub} Series \#1: Introduction to {
Torch Hub}},
journal = {
PyImageSearch},
year = {
2021},
note = {
https://pyimagesearch.com/2021/12/20/torch-hub-series-1-introduction-to-torch-hub/},
}
要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*
火炬中心系列#2: VGG 和雷斯内特
原文:https://pyimagesearch.com/2021/12/27/torch-hub-series-2-vgg-and-resnet/
在之前的教程中,我们学习了火炬中心背后的精髓及其概念。然后,我们使用 Torch Hub 的复杂性发布了我们的模型,并通过它进行访问。但是,当我们的工作需要我们使用 Torch Hub 上众多功能强大的模型之一时,会发生什么呢?
在本教程中,我们将学习如何利用最常见的模型称为使用火炬枢纽的力量:VGG 和 ResNet 模型家族。我们将学习这些模型背后的核心思想,并针对我们选择的任务对它们进行微调。
本课是关于火炬中心的 6 部分系列的第 2 部分:
- 火炬中心系列# 1:火炬中心介绍
- 火炬毂系列#2: VGG 和雷斯内特(本教程)
- 火炬轮毂系列#3: YOLO v5 和 SSD——实物检测模型
- 火炬轮毂系列# 4:—甘上模
- 火炬轮毂系列# 5:MiDaS——深度估计模型
- 火炬中枢系列#6:图像分割
要了解如何利用火炬枢纽来驾驭 VGG 网和雷斯网的力量, 继续阅读。
火炬中心系列#2: VGG 和雷斯内特
VGG 和雷斯内特
说实话,在每一个深度学习爱好者的生活中,迁移学习总会发挥巨大的作用。我们并不总是拥有从零开始训练模型的必要硬件,尤其是在千兆字节的数据上。云环境确实让我们的生活变得更轻松,但它们的使用显然是有限的。
现在,你可能想知道我们是否必须在机器学习的旅程中尝试我们所学的一切。使用图 1 可以最好地解释这一点。
在机器学习领域,理论和实践是同等重要的。按照这种观念,硬件限制会严重影响你的机器学习之旅。谢天谢地,机器学习社区的好心人通过在互联网上上传预先训练好的模型权重来帮助我们绕过这些问题。这些模型在巨大的数据集上训练,使它们成为非常强大的特征提取器。
您不仅可以将这些模型用于您的任务,还可以将它们用作基准。现在,您一定想知道在特定数据集上训练的模型是否适用于您的问题所特有的任务。
这是一个非常合理的问题。但是想一想完整的场景。例如,假设您有一个在 ImageNet 上训练的模型(1400 万个图像和 20,000 个类)。在这种情况下,由于您的模型已经是一个熟练的特征提取器,因此针对类似的和更具体的图像分类对其进行微调将会给您带来好的结果。由于我们今天的任务是微调一个 VGG/雷斯网模型,我们将看到我们的模型从第一个纪元开始是多么熟练!
由于网上有大量预先训练好的模型权重,Torch Hub 可以识别所有可能出现的问题,并通过将整个过程浓缩到一行来解决它们。因此,您不仅可以在本地系统中加载 SOTA 模型,还可以选择是否需要对它们进行预训练。
事不宜迟,让我们继续本教程的先决条件。
配置您的开发环境
要遵循这个指南,您需要在您的系统上安装 PyTorch 框架。
幸运的是,它是 pip 可安装的:
$ pip install pytorch
如果您需要帮助配置 OpenCV 的开发环境,我们强烈推荐阅读我们的 pip 安装 OpenCV 指南——它将在几分钟内让您启动并运行。
在配置开发环境时遇到了问题?
说了这么多,你是:
- 时间紧迫?
- 了解你雇主的行政锁定系统?
- 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
- 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***
*那今天就加入 PyImageSearch 大学吧!
获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。
最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!
项目结构
我们首先需要回顾我们的项目目录结构。
首先访问本教程的 “下载” 部分,检索源代码和示例图像。
从这里,看一下目录结构:
$ tree .
.
├── inference.py
├── pyimagesearch
│ ├── classifier.py
│ ├── config.py
│ └── datautils.py
└── train.py
1 directory, 5 files
在pyimagesearch
中,我们有 3 个脚本:
classifier.py
:容纳项目的模型架构config.py
:包含项目的端到端配置管道datautils.py
:包含了我们将在项目中使用的两个数据实用函数
在父目录中,我们有两个脚本:
- 根据我们训练的模型权重进行推断
train.py
:在所需数据集上训练模型
VGG 和 ResNet 架构概述
论文中介绍了 VGG16 架构的超深卷积网络用于大规模图像识别它借鉴了 AlexNet 的核心思想,同时用多个3×3
卷积滤波器取代了大尺寸的卷积滤波器。在图 3 中,我们可以看到完整的架构。
过多卷积滤波器的小滤波器尺寸和网络的深度架构胜过当时的许多基准模型。因此,直到今天,VGG16 仍被认为是 ImageNet 数据集的最新模型。
不幸的是,VGG16 有一些重大缺陷。首先,由于网络的性质,它有几个权重参数。这不仅使模型更重,而且增加了这些模型的推理时间。
理解了 VGG 篮网的局限性,我们继续关注他们的精神继承者;雷斯网。由何和介绍,ResNets 的想法背后的纯粹天才不仅在许多情况下超过了 Nets,而且他们的架构也使推理时间更快。
ResNets 背后的主要思想可以在图 4 中看到。
这种架构被称为“剩余块”正如您所看到的,一层的输出不仅会被提供给下一层,还会进行一次跳跃,并被提供给架构中的另一层。
现在,这个想法立刻消除了渐变消失的可能性。但是这里的主要思想是来自前面层的信息在后面的层中保持活跃。因此,精心制作的特征映射阵列在自适应地决定这些残余块层的输出中起作用。
ResNet 被证明是机器学习社区的一次巨大飞跃。ResNet 不仅在构思之初就超越了许多深度架构,而且还引入了一个全新的方向,告诉我们如何让深度架构变得更好。
有了这两个模型的基本思想之后,让我们开始编写代码吧!
熟悉我们的数据集
对于今天的任务,我们将使用来自 Kaggle 的简单二元分类狗&猫数据集。这个 217.78 MB 的数据集包含 10,000 幅猫和狗的图像,以 80-20 的训练与测试比率分割。训练集包含 4000 幅猫和 4000 幅狗的图像,而测试集分别包含 1000 幅猫和 1000 幅狗的图像。使用较小的数据集有两个原因:
- 微调我们的分类器将花费更少的时间
- 展示预训练模型适应具有较少数据的新数据集的速度
配置先决条件
首先,让我们进入存储在pyimagesearch
目录中的config.py
脚本。该脚本将包含完整的训练和推理管道配置值。
# import the necessary packages
import torch
import os
# define the parent data dir followed by the training and test paths
BASE_PATH = "dataset"
TRAIN_PATH = os.path.join(BASE_PATH, "training_set")
TEST_PATH = os.path.join(BASE_PATH, "test_set")
# specify ImageNet mean and standard deviation
MEAN = [0.485, 0.456, 0.406]
STD = [0.229, 0.224, 0.225]
# specify training hyperparameters
IMAGE_SIZE = 256
BATCH_SIZE = 128
PRED_BATCH_SIZE = 4
EPOCHS = 15
LR = 0.0001
# determine the device type
DEVICE = torch.device("cuda") if torch.cuda.is_available() else "cpu"
# define paths to store training plot and trained model
PLOT_PATH = os.path.join("output", "model_training.png")
MODEL_PATH = os.path.join("output", "model.pth")
我们首先在第 6 行初始化数据集的基本训练。然后,在第 7 行和第 8 行,我们使用os.path.join
来指定数据集的训练和测试文件夹。
在第行第 11 和 12 行,我们指定了稍后创建数据集实例时所需的 ImageNet 平均值和标准偏差。这样做是因为模型是通过这些平均值和标准偏差值预处理的预训练数据,我们将尽可能使我们的当前数据与之前训练的数据相似。
接下来,我们为超参数赋值,如图像大小、批量大小、时期等。(第 15-19 行)并确定我们将训练我们模型的设备(第 22 行)。
我们通过指定存储训练图和训练模型权重的路径来结束我们的脚本(行 25 和 26 )。
为我们的数据管道创建实用函数
我们创建了一些函数来帮助我们处理数据管道,并在datautils.py
脚本中对它们进行分组,以便更好地处理我们的数据。
# import the necessary packages
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader
from torch.datasets import Subset
def get_dataloader(dataset, batchSize, shuffle=True):
# create a dataloader
dl = DataLoader(dataset, batch_size=batchSize, shuffle=shuffle)
# return the data loader
return dl
我们的第一个效用函数是get_dataloader
函数。它将数据集、批处理大小和一个布尔变量shuffle
作为其参数(第 6 行),并返回一个 PyTorch dataloader
实例(第 11 行)。
def train_val_split(dataset, valSplit=0.2):
# grab the total size of the dataset
totalSize = len(dataset)
# perform training and validation split
(trainIdx, valIdx) = train_test_split(list(range(totalSize)),
test_size=valSplit)
trainDataset = Subset(dataset, trainIdx)
valDataset = Subset(dataset, valIdx)
# return training and validation dataset
return (trainDataset, valDataset)
接下来,我们创建一个名为train_val_split
的函数,它接受数据集和一个验证分割百分比变量作为参数(第 13 行)。由于我们的数据集只有训练和测试目录,我们使用 PyTorch dataset
子集特性将训练集分成训练和验证集。
我们首先使用train_test_split
函数为我们的分割创建索引,然后将这些索引分配给子集(第 20 行和第 21 行)。该功能将返回训练和验证数据子集(行 24 )。
为我们的任务创建分类器
我们的下一个任务是为猫狗数据集创建一个分类器。请记住,我们不是从零开始训练我们的调用模型,而是对它进行微调。为此,我们将继续下一个脚本,即classifier.py
。
# import the necessary packages
from torch.nn import Linear
from torch.nn import Module
class Classifier(Module):
def __init__(self, baseModel, numClasses, model):
super().__init__()
# initialize the base model
self.baseModel = baseModel
# check if the base model is VGG, if so, initialize the FC
# layer accordingly
if model == "vgg":
self.fc = Linear(baseModel.classifier[6].out_features,
numClasses)
# otherwise, the base model is of type ResNet so initialize
# the FC layer accordingly
else:
self.fc = Linear(baseModel.fc.out_features, numClasses)
在我们的Classifier
模块(第 5 行)中,构造函数接受以下参数:
- 因为我们将调用 VGG 或 ResNet 模型,我们已经覆盖了我们架构的大部分。我们将把调用的基础模型直接插入到我们的架构中的第 9 行上。
numClasses
:将决定我们架构的输出节点。对于我们的任务,该值为 2。- 一个字符串变量,它将告诉我们我们的基本模型是 VGG 还是 ResNet。因为我们必须为我们的任务创建一个单独的输出层,所以我们必须获取模型的最终线性层的输出。但是,每个模型都有不同的方法来访问最终的线性图层。因此,该
model
变量将有助于相应地选择特定于车型的方法(第 14 行和第 20 行)。
注意,对于 VGGnet,我们使用命令baseModel.classifier[6].out_features
,而对于 ResNet,我们使用baseModel.fc.out_features
。这是因为这些模型有不同的命名模块和层。所以我们必须使用不同的命令来访问每一层的最后一层。因此,model
变量对于我们的代码工作非常重要。
def forward(self, x):
# pass the inputs through the base model to get the features
# and then pass the features through of fully connected layer
# to get our output logits
features = self.baseModel(x)
logits = self.fc(features)
# return the classifier outputs
return logits
转到forward
函数,我们简单地在行 26 上获得基本模型的输出,并通过我们最终的完全连接层(行 27 )来获得模型输出。
训练我们的自定义分类器
先决条件排除后,我们继续进行train.py
。首先,我们将训练我们的分类器来区分猫和狗。
# USAGE
# python train.py --model vgg
# python train.py --model resnet
# import the necessary packages
from pyimagesearch import config
from pyimagesearch.classifier import Classifier
from pyimagesearch.datautils import get_dataloader
from pyimagesearch.datautils import train_val_split
from torchvision.datasets import ImageFolder
from torchvision.transforms import Compose
from torchvision.transforms import ToTensor
from torchvision.transforms import RandomResizedCrop
from torchvision.transforms import RandomHorizontalFlip
from torchvision.transforms import RandomRotation
from torchvision.transforms import Normalize
from torch.nn import CrossEntropyLoss
from torch.nn import Softmax
from torch import optim
from tqdm import tqdm
import matplotlib.pyplot as plt
import argparse
import torch
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-m", "--model", type=str, default="vgg",
choices=["vgg", "resnet"], help="name of the backbone model")
args = vars(ap.parse_args())
在第 6-23 行中,我们拥有培训模块所需的所有导入。不出所料,这是一个相当长的列表!
为了便于访问和选择,我们在第 26 行的处创建了一个参数解析器,在第 27-29 行的处添加了参数模型选项(VGG 或雷斯尼)。
接下来的一系列代码块是我们项目中非常重要的部分。例如,为了微调模型,我们通常冻结预训练模型的层。然而,在消融不同的场景时,我们注意到保持卷积层冻结,但是完全连接的层解冻以用于进一步的训练,这有助于我们的结果。
# check if the name of the backbone model is VGG
if args["model"] == "vgg":
# load VGG-11 model
baseModel = torch.hub.load("pytorch/vision:v0.10.0", "vgg11",
pretrained=True, skip_validation=True)
# freeze the layers of the VGG-11 model
for param in baseModel.features.parameters():
param.requires_grad = False
从火炬中心调用的 VGG 模型架构(线 34 和 35 )被分成几个子模块,以便于访问。卷积层被分组在一个名为features
的模块下,而下面完全连接的层被分组在一个名为classifier
的模块下。由于我们只需要冻结卷积层,我们直接访问第 38 行上的参数,并通过将requires_grad
设置为False
来冻结它们,保持classifier
模块层不变。
# otherwise, the backbone model we will be using is a ResNet
elif args["model"] == "resnet":
# load ResNet 18 model
baseModel = torch.hub.load("pytorch/vision:v0.10.0", "resnet18",
pretrained=True, skip_validation=True)
# define the last and the current layer of the model
lastLayer = 8
currentLayer = 1
# loop over the child layers of the model
for child in baseModel.children():
# check if we haven't reached the last layer
if currentLayer < lastLayer:
# loop over the child layer's parameters and freeze them
for param in child.parameters():
param.requires_grad = False
# otherwise, we have reached the last layers so break the loop
else:
break
# increment the current layer
currentLayer += 1
在基本模型是 ResNet 的情况下,有几种方法可以解决这个问题。要记住的主要事情是,在 ResNet 中,我们只需要保持最后一个完全连接的层不冻结。相应地,在第 48 行和第 49 行,我们设置了最后一层和当前层索引。
在第 52 行的 ResNet 的可用层上循环,我们冻结所有层,除了最后一层(行 55-65 )。
# define the transform pipelines
trainTransform = Compose([
RandomResizedCrop(config.IMAGE_SIZE),
RandomHorizontalFlip(),
RandomRotation(90),
ToTensor(),
Normalize(mean=config.MEAN, std=config.STD)
])
# create training dataset using ImageFolder
trainDataset = ImageFolder(config.TRAIN_PATH, trainTransform)
我们继续创建输入管道,从 PyTorch transform
实例开始,它可以自动调整大小、规范化和增加数据,没有太多麻烦(第 68-74 行)。
我们通过使用另一个名为ImageFolder
的 PyTorch 实用函数来完成它,该函数将自动创建输入和目标数据,前提是目录设置正确(第 77 行)。
# create training and validation data split
(trainDataset, valDataset) = train_val_split(dataset=trainDataset)
# create training and validation data loaders
trainLoader = get_dataloader(trainDataset, config.BATCH_SIZE)
valLoader = get_dataloader(valDataset, config.BATCH_SIZE)
使用我们的train_val_split
效用函数,我们将训练数据集分成一个训练和验证集(第 80 行)。接下来,我们使用来自datautils.py
的get_dataloader
实用函数来创建我们数据的 PyTorch dataloader
实例(第 83 行和第 84 行)。这将允许我们以一种类似生成器的方式无缝地向模型提供数据。
# build the custom model
model = Classifier(baseModel=baseModel.to(config.DEVICE),
numClasses=2, model=args["model"])
model = model.to(config.DEVICE)
# initialize loss function and optimizer
lossFunc = CrossEntropyLoss()
lossFunc.to(config.DEVICE)
optimizer = optim.Adam(model.parameters(), lr=config.LR)
# initialize the softmax activation layer
softmax = Softmax()
继续我们的模型先决条件,我们创建我们的定制分类器并将其加载到我们的设备上(第 87-89 行)。
我们已经使用交叉熵作为我们今天任务的损失函数和 Adam 优化器(第 92-94 行)。此外,我们使用单独的softmax
损失来帮助我们增加培训损失(第 97 行)。
# calculate steps per epoch for training and validation set
trainSteps = len(trainDataset) // config.BATCH_SIZE
valSteps = len(valDataset) // config.BATCH_SIZE
# initialize a dictionary to store training history
H = {
"trainLoss": [],
"trainAcc": [],
"valLoss": [],
"valAcc": []
}
训练时期之前的最后一步是设置训练步骤和验证步骤值,然后创建一个存储所有训练历史的字典(行 100-109 )。
# loop over epochs
print("[INFO] training the network...")
for epoch in range(config.EPOCHS):
# set the model in training mode
model.train()
# initialize the total training and validation loss
totalTrainLoss = 0
totalValLoss = 0
# initialize the number of correct predictions in the training
# and validation step
trainCorrect = 0
valCorrect = 0
在训练循环中,我们首先将模型设置为训练模式( Line 115 )。接下来,我们初始化训练损失、确认损失、训练和确认精度变量(第 118-124 行)。
# loop over the training set
for (image, target) in tqdm(trainLoader):
# send the input to the device
(image, target) = (image.to(config.DEVICE),
target.to(config.DEVICE))
# perform a forward pass and calculate the training loss
logits = model(image)
loss = lossFunc(logits, target)
# zero out the gradients, perform the backpropagation step,
# and update the weights
optimizer.zero_grad()
loss.backward()
optimizer.step()
# add the loss to the total training loss so far, pass the
# output logits through the softmax layer to get output
# predictions, and calculate the number of correct predictions
totalTrainLoss += loss.item()
pred = softmax(logits)
trainCorrect += (pred.argmax(dim=-1) == target).sum().item()
遍历完整的训练集,我们首先将数据和目标加载到设备中(行 129 和 130 )。接下来,我们简单地通过模型传递数据并获得输出,然后将预测和目标插入我们的损失函数(第 133 行和第 134 行),
第 138-140 行是标准 PyTorch 反向传播步骤,其中我们将梯度归零,执行反向传播,并更新权重。
接下来,我们将损失添加到我们的总训练损失中(行 145 ),通过 softmax 传递模型输出以获得孤立的预测值,然后将其添加到trainCorrect
变量中。
# switch off autograd
with torch.no_grad():
# set the model in evaluation mode
model.eval()
# loop over the validation set
for (image, target) in tqdm(valLoader):
# send the input to the device
(image, target) = (image.to(config.DEVICE),
target.to(config.DEVICE))
# make the predictions and calculate the validation
# loss
logits = model(image)
valLoss = lossFunc(logits, target)
totalValLoss += valLoss.item()
# pass the output logits through the softmax layer to get
# output predictions, and calculate the number of correct
# predictions
pred = softmax(logits)
valCorrect += (pred.argmax(dim=-1) == target).sum().item()
验证过程中涉及的大部分步骤与培训过程相同,除了以下几点:
- 模型被设置为评估模式(行 152 )
- 权重没有更新
# calculate the average training and validation loss
avgTrainLoss = totalTrainLoss / trainSteps
avgValLoss = totalValLoss / valSteps
# calculate the training and validation accuracy
trainCorrect = trainCorrect / len(trainDataset)
valCorrect = valCorrect / len(valDataset)
# update our training history
H["trainLoss"].append(avgTrainLoss)
H["valLoss"].append(avgValLoss)
H["trainAcc"].append(trainCorrect)
H["valAcc"].append(valCorrect)
# print the model training and validation information
print(f"[INFO] EPOCH: {
epoch + 1}/{
config.EPOCHS}")
print(f"Train loss: {
avgTrainLoss:.6f}, Train accuracy: {
trainCorrect:.4f}")
print(f"Val loss: {
avgValLoss:.6f}, Val accuracy: {
valCorrect:.4f}")
在退出训练循环之前,我们计算平均损失(行 173 和 174 )以及训练和验证精度(行 177 和 178 )。
然后,我们继续将这些值添加到我们的训练历史字典中(行 181-184 )。
# plot the training loss and accuracy
plt.style.use("ggplot")
plt.figure()
plt.plot(H["trainLoss"], label="train_loss")
plt.plot(H["valLoss"], label="val_loss")
plt.plot(H["trainAcc"], label="train_acc")
plt.plot(H["valAcc"], label="val_acc")
plt.title("Training Loss and Accuracy on Dataset")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(config.PLOT_PATH)
# serialize the model state to disk
torch.save(model.module.state_dict(), config.MODEL_PATH)
在完成我们的训练脚本之前,我们绘制所有的训练字典变量(行 192-201 )并保存图形(行 202 )。
我们最后的任务是将模型权重保存在之前定义的路径中( Line 205 )。
让我们看看每个时期的值是什么样的!
[INFO] training the network...
100%|██████████| 50/50 [01:24<00:00, 1.68s/it]
100%|██████████| 13/13 [00:19<00:00, 1.48s/it]
[INFO] EPOCH: 1/15
Train loss: 0.289117, Train accuracy: 0.8669
Val loss: 0.217062, Val accuracy: 0.9119
100%|██████████| 50/50 [00:47<00:00, 1.05it/s]
100%|██████████| 13/13 [00:11<00:00, 1.10it/s]
[INFO] EPOCH: 2/15
Train loss: 0.212023, Train accuracy: 0.9039
Val loss: 0.223640, Val accuracy: 0.9025
100%|██████████| 50/50 [00:46<00:00, 1.07it/s]
100%|██████████| 13/13 [00:11<00:00, 1.15it/s]
[INFO] EPOCH: 3/15
...
Train loss: 0.139766, Train accuracy: 0.9358
Val loss: 0.187595, Val accuracy: 0.9194
100%|██████████| 50/50 [00:46<00:00, 1.07it/s]
100%|██████████| 13/13 [00:11<00:00, 1.15it/s]
[INFO] EPOCH: 13/15
Train loss: 0.134248, Train accuracy: 0.9425
Val loss: 0.146280, Val accuracy: 0.9437
100%|██████████| 50/50 [00:47<00:00, 1.05it/s]
100%|██████████| 13/13 [00:11<00:00, 1.12it/s]
[INFO] EPOCH: 14/15
Train loss: 0.132265, Train accuracy: 0.9428
Val loss: 0.162259, Val accuracy: 0.9319
100%|██████████| 50/50 [00:47<00:00, 1.05it/s]
100%|██████████| 13/13 [00:11<00:00, 1.16it/s]
[INFO] EPOCH: 15/15
Train loss: 0.138014, Train accuracy: 0.9409
Val loss: 0.153363, Val accuracy: 0.9313
我们预训练的模型精度在第一个历元就已经接近 90%
。到了第 13
个时期,数值在大约**~94%**
处饱和。从这个角度来看,在不同数据集上训练的预训练模型在它以前没有见过的数据集上以大约 86%
的精度开始。这就是它学会提取特征的程度。
在图 5 中绘制了指标的完整概述。
测试我们微调过的模型
随着我们的模型准备就绪,我们将继续我们的推理脚本,inference.py
。
# USAGE
# python inference.py --model vgg
# python inference.py --model resnet
# import the necessary packages
from pyimagesearch import config
from pyimagesearch.classifier import Classifier
from pyimagesearch.datautils import get_dataloader
from torchvision.datasets import ImageFolder
from torchvision.transforms import Compose
from torchvision.transforms import ToTensor
from torchvision.transforms import Resize
from torchvision.transforms import Normalize
from torchvision import transforms
from torch.nn import Softmax
from torch import nn
import matplotlib.pyplot as plt
import argparse
import torch
import tqdm
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-m", "--model", type=str, default="vgg",
choices=["vgg", "resnet"], help="name of the backbone model")
args = vars(ap.parse_args())
因为我们必须在加载权重之前初始化我们的模型,所以我们需要正确的模型参数。为此,我们在第 23-26 行的中创建了一个参数解析器。
# initialize test transform pipeline
testTransform = Compose([
Resize((config.IMAGE_SIZE, config.IMAGE_SIZE)),
ToTensor(),
Normalize(mean=config.MEAN, std=config.STD)
])
# calculate the inverse mean and standard deviation
invMean = [-m/s for (m, s) in zip(config.MEAN, config.STD)]
invStd = [1/s for s in config.STD]
# define our denormalization transform
deNormalize = transforms.Normalize(mean=invMean, std=invStd)
# create the test dataset
testDataset = ImageFolder(config.TEST_PATH, testTransform)
# initialize the test data loader
testLoader = get_dataloader(testDataset, config.PRED_BATCH_SIZE)
由于我们将在完整的测试数据集上计算我们的模型的准确性,我们在第 29-33 行的上为我们的测试数据创建一个 PyTorch transform
实例。
因此,我们计算反平均值和反标准偏差值,我们用它们来创建一个transforms.Normalize
实例(第 36-40 行)。这样做是因为数据在输入到模型之前经过了预处理。出于显示目的,我们必须将图像恢复到原始状态。
使用ImageFolder
实用函数,我们创建我们的测试数据集实例,并将其提供给之前为测试dataLoader
实例创建的get_dataloader
函数(第 43-46 行)。
# check if the name of the backbone model is VGG
if args["model"] == "vgg":
# load VGG-11 model
baseModel = torch.hub.load("pytorch/vision:v0.10.0", "vgg11",
pretrained=True, skip_validation=True)
# otherwise, the backbone model we will be using is a ResNet
elif args["model"] == "resnet":
# load ResNet 18 model
baseModel = torch.hub.load("pytorch/vision:v0.10.0", "resnet18",
pretrained=True, skip_validation=True)
如前所述,由于我们必须再次初始化模型,我们检查给定的模型参数,并相应地使用 Torch Hub 加载模型(第 49-58 行)。
# build the custom model
model = Classifier(baseModel=baseModel.to(config.DEVICE),
numClasses=2, vgg = False)
model = model.to(config.DEVICE)
# load the model state and initialize the loss function
model.load_state_dict(torch.load(config.MODEL_PATH))
lossFunc = nn.CrossEntropyLoss()
lossFunc.to(config.DEVICE)
# initialize test data loss
testCorrect = 0
totalTestLoss = 0
soft = Softmax()
在第 61-66 行上,我们初始化模型,将其存储在我们的设备上,并在模型训练期间加载先前获得的权重。
正如我们在train.py
脚本中所做的,我们选择交叉熵作为我们的损失函数(第 67 行,并初始化测试损失和准确性(第 71 行和第 72 行)。
# switch off autograd
with torch.no_grad():
# set the model in evaluation mode
model.eval()
# loop over the validation set
for (image, target) in tqdm(testLoader):
# send the input to the device
(image, target) = (image.to(config.DEVICE),
target.to(config.DEVICE))
# make the predictions and calculate the validation
# loss
logit = model(image)
loss = lossFunc(logit, target)
totalTestLoss += loss.item()
# output logits through the softmax layer to get output
# predictions, and calculate the number of correct predictions
pred = soft(logit)
testCorrect += (pred.argmax(dim=-1) == target).sum().item()
关闭自动梯度(行 76 ),我们在**行 78 将模型设置为评估模式。**然后,在测试图像上循环,我们将它们提供给模型,并通过损失函数传递预测和目标(第 81-89 行)。
通过 softmax 函数(**第 95 和 96 行)**传递预测来计算精确度。
# print test data accuracy
print(testCorrect/len(testDataset))
# initialize iterable variable
sweeper = iter(testLoader)
# grab a batch of test data
batch = next(sweeper)
(images, labels) = (batch[0], batch[1])
# initialize a figure
fig = plt.figure("Results", figsize=(10, 10 ))
现在我们将看看测试数据的一些具体情况并显示它们。为此,我们在行 102 上初始化一个可迭代变量,并抓取一批数据(行 105 )。
# switch off autograd
with torch.no_grad():
# send the images to the device
images = images.to(config.DEVICE)
# make the predictions
preds = model(images)
# loop over all the batch
for i in range(0, config.PRED_BATCH_SIZE):
# initialize a subplot
ax = plt.subplot(config.PRED_BATCH_SIZE, 1, i + 1)
# grab the image, de-normalize it, scale the raw pixel
# intensities to the range [0, 255], and change the channel
# ordering from channels first tp channels last
image = images[i]
image = deNormalize(image).cpu().numpy()
image = (image * 255).astype("uint8")
image = image.transpose((1, 2, 0))
# grab the ground truth label
idx = labels[i].cpu().numpy()
gtLabel = testDataset.classes[idx]
# grab the predicted label
pred = preds[i].argmax().cpu().numpy()
predLabel = testDataset.classes[pred]
# add the results and image to the plot
info = "Ground Truth: {}, Predicted: {}".format(gtLabel,
predLabel)
plt.imshow(image)
plt.title(info)
plt.axis("off")
# show the plot
plt.tight_layout()
plt.show()
我们再次关闭自动渐变,并对之前获取的一批数据进行预测(第 112-117 行)。
在批处理中循环,我们抓取单个图像,反规格化它们,重新缩放它们,并固定它们的尺寸以使它们可显示(行 127-130 )。
基于当前正在考虑的图像,我们首先抓取它的地面真实标签(行 133 和 134 )以及它们在行 137 和 138 上对应的预测标签,并相应地显示它们(行 141-145 )。
微调模型的结果
在整个测试数据集上,我们的 ResNet 支持的定制模型产生了 97.5%的准确率。在图 6-9 中,我们看到显示的一批数据,以及它们对应的基础事实和预测标签。
凭借 97.5%的准确度,您可以放心,这一性能水平不仅适用于该批次,还适用于所有T2 批次。您可以重复运行sweeper
变量来获得不同的数据批次,以便自己查看。
总结
今天的教程不仅展示了如何利用 Torch Hub 的模型库,还提醒了我们预先训练的模型在我们日常的机器学习活动中有多大的帮助。
想象一下,如果您必须为您选择的任何任务从头开始训练一个像 ResNet 这样的大型架构。这将需要更多的时间,而且肯定需要更多的纪元。至此,您肯定会欣赏 PyTorch Hub 背后的理念,即让使用这些最先进模型的整个过程更加高效。
从我们上一周的教程中离开的地方继续,我想强调 PyTorch Hub 仍然很粗糙,它仍然有很大的改进空间。当然,我们离完美的版本越来越近了!
引用信息
Chakraborty,D. “火炬中心系列#2: VGG 和雷斯内特”, PyImageSearch ,2021,https://PyImageSearch . com/2021/12/27/Torch-Hub-Series-2-vgg-and-ResNet/
@article{
dev_2021_THS2,
author = {
Devjyoti Chakraborty},
title = {
{
Torch Hub} Series \#2: {
VGG} and {
ResNet}},
journal = {
PyImageSearch},
year = {
2021},
note = {
https://pyimagesearch.com/2021/12/27/torch-hub-series-2-vgg-and-resnet/},
}
要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*
Torch Hub 系列#3: YOLOv5 和 SSD——对象检测模型
原文:https://pyimagesearch.com/2022/01/03/torch-hub-series-3-yolov5-and-ssd-models-on-object-detection/
在我的童年,电影《间谍小子》是我最喜欢看的电视节目之一。看到我这个年龄的孩子使用未来的小工具来拯救世界并赢得胜利可能是一个常见的比喻,但看起来仍然很有趣。在像喷气背包和自动驾驶汽车这样的东西中,我最喜欢的是智能太阳镜,它可以识别你周围的物体和人(它还可以兼作双目望远镜),有点像图 1 。
可以理解的是,这些小玩意在现实生活中的概念在当时很难理解。但是,现在已经到了 2022 年,一家自动驾驶汽车公司(特斯拉)已经处于电机行业的顶端,从实时视频中检测物体简直易如反掌!
所以今天,除了理解一个年轻的我的狂热梦想,我们将看到 PyTorch Hub 如何使探索这些领域变得容易。
在本教程中,我们将学习 YOLOv5 和 SSD300 等模型背后的直觉,并使用 Torch Hub 驾驭它们的力量。
本课是关于火炬中心的 6 部分系列的第 3 部分:
- 火炬中心系列# 1:火炬中心介绍
- 火炬枢纽系列#2: VGG 和雷斯内特
- 火炬轮毂系列#3: YOLOv5 和 SSD——物体检测模型(本教程)
- 火炬轮毂系列# 4:—甘上模
- 火炬轮毂系列# 5:MiDaS——深度估计模型
- 火炬中枢系列#6:图像分割
要学习如何使用 YOLOv5 和 SSD300, 继续阅读。
火炬轮毂系列#3: YOLOv5 和****SSD——物体检测模型
物体检测一目了然
乍一看,物体检测无疑是一个非常诱人的领域。让机器识别图像中某个对象的确切位置,让我相信我们离实现模仿人脑的梦想又近了一步。但即使我们把它放在一边,它在当今世界也有各种各样的重要用法。从人脸检测系统到帮助自动驾驶汽车安全导航,这样的例子不胜枚举。但是它到底是如何工作的呢?
实现物体检测的方法有很多,以机器学习为核心思想。例如,在这篇关于在 PyTorch 中从头开始训练一个物体检测器的博客文章中,我们有一个简单的架构,它接收图像作为输入并输出 5 样东西;检测到的对象的类别以及对象边界框的高度和宽度的起始值和结束值。
本质上,我们获取带注释的图像,并通过一个简单的输出大小为 5 的 CNN 传递它们。因此,就像机器学习中开发的每一个新事物一样,更复杂和错综复杂的算法随之而来,以改进它。
请注意,如果您考虑我在上一段中提到的方法,它可能只适用于检测单个对象的图像。然而,当多个物体在一张图像中时,这几乎肯定会遇到障碍。因此,为了解决这一问题以及效率等其他限制,我们转向 YOLO(v1)。
YOLO,或“你只看一次”(2015),介绍了一种巧妙的方法来解决简单的 CNN 探测器的缺点。我们将每幅图像分割成一个 S × S 网格,得到每个细胞对应的物体位置。当然,有些单元格不会有任何对象,有些则会出现在多个单元格中。看一下图二。
了解完整图像中对象的中点、高度和宽度非常重要。然后,每个像元将输出一个概率值(像元中有一个对象的概率)、检测到的对象类以及像元特有的边界框值。
即使每个单元只能检测一个对象,多个单元的存在也会使约束无效。结果可以在图 3 中看到。
尽管取得了巨大的成果,但 YOLOv1 有一个重大缺陷;图像中对象的接近程度经常会使模型错过一些对象。自问世以来,已经出版了几个后继版本,如 YOLOv2、YOLOv3 和 YOLOv4,每一个都比前一个版本更好更快。这就把我们带到了今天的焦点之一,YOLOv5。
YOLOv5 的创造者 Glenn Jocher 决定不写论文,而是通过 GitHub 开源该模型。最初,这引起了很多关注,因为人们认为结果是不可重复的。然而,这种想法很快被打破了,今天,YOLOv5 是火炬中心展示区的官方最先进的模型之一。
要了解 YOLOv5 带来了哪些改进,我们还得回到 YOLOv2。其中,YOLOv2 引入了锚盒的概念。一系列预定的边界框、锚框具有特定的尺寸。根据训练数据集中的对象大小选择这些框,以捕捉要检测的各种对象类的比例和纵横比。网络预测对应于锚框而不是边界框本身的概率。
但在实践中,悬挂 YOLO 模型通常是在 COCO 数据集上训练的。这导致了一个问题,因为自定义数据集可能没有相同的锚框定义。YOLOv5 通过引入自动学习锚盒来解决这个问题。它还利用镶嵌增强,混合随机图像,使您的模型擅长识别较小比例的对象。
今天的第二个亮点是用于物体检测的固态硬盘或单次多盒探测器型号。SSD300 最初使用 VGG 主干进行娴熟的特征检测,并利用了 Szegedy 在 MultiBox 上的工作,这是一种快速分类不可知边界框坐标建议的方法,启发了 SSD 的边界框回归算法。图 4 展示了 SSD 架构。
受 inception-net 的启发,Szegedy 创建的多盒架构利用了多尺度卷积架构。Multibox 使用一系列正常的卷积和1×1
过滤器(改变通道大小,但保持高度和宽度不变)来合并多尺度边界框和置信度预测模型。
SSD 以利用多尺度特征地图而不是单一特征地图进行检测而闻名。这允许更精细的检测和更精细的预测。使用这些特征图,生成了用于对象预测的锚框。
它问世时就已经超越了它的同胞,尤其是在速度上。今天我们将使用 Torch Hub 展示的 SSD,它使用 ResNet 而不是 VGG 网络作为主干。此外,其他一些变化,如根据现代卷积物体探测器论文的速度/精度权衡移除一些层,也被应用到模型中。
今天,我们将学习如何使用 Torch Hub 利用这些模型的功能,并使用我们的自定义数据集对它们进行测试!
配置您的开发环境
要遵循这个指南,您需要在您的系统上安装 OpenCV 库。
幸运的是,OpenCV 可以通过 pip 安装:
$ pip install opencv-contrib-python
如果您需要帮助配置 OpenCV 的开发环境,我们强烈推荐阅读我们的 pip 安装 OpenCV 指南——它将在几分钟内让您启动并运行。
在配置开发环境时遇到了问题?
说了这么多,你是:
- 时间紧迫?
- 了解你雇主的行政锁定系统?
- 想要跳过与命令行、包管理器和虚拟环境斗争的麻烦吗?
- 准备好在您的 Windows、macOS 或 Linux 系统上运行代码***?***
*那今天就加入 PyImageSearch 大学吧!
获得本教程的 Jupyter 笔记本和其他 PyImageSearch 指南,这些指南是 预先配置的 **,可以在您的网络浏览器中运行在 Google Colab 的生态系统上!**无需安装。
最棒的是,这些 Jupyter 笔记本可以在 Windows、macOS 和 Linux 上运行!
项目结构
我们首先需要回顾我们的项目目录结构。
首先访问本教程的 “下载” 部分,检索源代码和示例图像。
从这里,看一下目录结构:
!tree .
.
├── output
│ ├── ssd_output
│ │ └── ssd_output.png
│ └── yolo_output
│ └── yolo_output.png
├── pyimagesearch
│ ├── config.py
│ ├── data_utils.py
├── ssd_inference.py
└── yolov5_inference.py
首先,我们有output
目录,它将存放我们将从每个模型获得的输出。
在pyimagesearch
目录中,我们有两个脚本:
- 这个脚本包含了项目的端到端配置管道
- 这个脚本包含了一些用于数据处理的帮助函数
在主目录中,我们有两个脚本:
ssd_inference.py
:这个脚本包含自定义映像的 SSD 模型推断。yolov5_inference.py
:这个脚本包含了定制图像的 YOLOv5 模型推理。
下载数据集
第一步是根据我们的需求配置数据集。像在之前的教程中一样,我们将使用来自 Kaggle 的狗&猫图像数据集,因为它相对较小。
$ mkdir ~/.kaggle
$ cp <path to your kaggle.json> ~/.kaggle/
$ chmod 600 ~/.kaggle/kaggle.json
$ kaggle datasets download -d chetankv/dogs-cats-images
$ unzip -qq dogs-cats-images.zip
$ rm -rf "/content/dog vs cat"
要使用数据集,您需要有自己独特的kaggle.json
文件来连接到 Kaggle API ( 第 2 行)。线 3 上的chmod 600
命令给予用户读写文件的完全权限。
接下来是kaggle datasets download
命令(第 4 行)允许你下载他们网站上的任何数据集。最后,解压文件并删除不必要的附加内容(第 5 行和第 6 行)。
让我们转到配置管道。
配置先决条件
在pyimagesearch
目录中,您会发现一个名为config.py
的脚本。这个脚本将包含我们项目的完整的端到端配置管道。
# import the necessary packages
import torch
import os
# define the root directory followed by the test dataset paths
BASE_PATH = "dataset"
TEST_PATH = os.path.join(BASE_PATH, "test_set")
# specify image size and batch size
IMAGE_SIZE = 300
PRED_BATCH_SIZE = 4
# specify threshold confidence value for ssd detections
THRESHOLD = 0.50
# determine the device type
DEVICE = torch.device("cuda") if torch.cuda.is_available() else "cpu"
# define paths to save output
OUTPUT_PATH = "output"
SSD_OUTPUT = os.path.join(OUTPUT_PATH, "ssd_output")
YOLO_OUTPUT = os.path.join(OUTPUT_PATH, "yolo_output")
在第 6 行的上,我们有指向数据集目录的BASE_PATH
变量。由于我们将只使用模型来运行推理,我们将只需要测试集(第 7 行)。
在第 10 行,我们有一个名为IMAGE_SIZE
的变量,设置为300
。这是 SSD 模型的一个要求,因为它是根据大小300 x 300
图像训练的。预测批量大小设置为4
( 第 11 行),但是鼓励读者尝试不同的大小。
接下来,我们有一个名为THRESHOLD
的变量,它将作为 SSD 模型结果的置信度值阈值,即只保留置信度值大于阈值的结果(第 14 行)。
建议您为今天的项目准备一个兼容 CUDA 的设备( Line 17 ),但是由于我们不打算进行任何繁重的训练,CPU 应该也能正常工作。
最后,我们创建了路径来保存从模型推断中获得的输出(第 20-22 行)。
为数据管道创建辅助函数
在我们看到运行中的模型之前,我们还有一项任务;为数据处理创建辅助函数。为此,转到位于pyimagesearch
目录中的data_utils.py
脚本。
# import the necessary packages
from torch.utils.data import DataLoader
def get_dataloader(dataset, batchSize, shuffle=True):
# create a dataloader and return it
dataLoader= DataLoader(dataset, batch_size=batchSize,
shuffle=shuffle)
return dataLoader
get_dataloader
( **第 4 行)**函数接受数据集、批量大小和随机参数,返回一个PyTorch Dataloader
( 第 6 行和第 7 行)实例。Dataloader
实例解决了许多麻烦,这需要为巨大的数据集编写单独的定制生成器类。
def normalize(image, mean=128, std=128):
# normalize the SSD input and return it
image = (image * 256 - mean) / std
return image
脚本中的第二个函数normalize
,专门用于我们将发送到 SSD 模型的图像。它将image
、平均值和标准偏差值作为输入,对它们进行归一化,并返回归一化图像(第 10-13 行)。
在 YOLOv5 上测试自定义图像
先决条件得到满足后,我们的下一个目的地是yolov5_inference.py
。我们将准备我们的自定义数据,并将其提供给 YOLO 模型。
# import necessary packages
from pyimagesearch.data_utils import get_dataloader
import pyimagesearch.config as config
from torchvision.transforms import Compose, ToTensor, Resize
from sklearn.model_selection import train_test_split
from torchvision.datasets import ImageFolder
from torch.utils.data import Subset
import matplotlib.pyplot as plt
import numpy as np
import random
import torch
import cv2
import os
# initialize test transform pipeline
testTransform = Compose([
Resize((config.IMAGE_SIZE, config.IMAGE_SIZE)), ToTensor()])
# create the test dataset
testDataset = ImageFolder(config.TEST_PATH, testTransform)
# initialize the test data loader
testLoader = get_dataloader(testDataset, config.PRED_BATCH_SIZE)
首先,我们在第 16 行和第 17 行上创建一个 PyTorch 转换实例。使用 PyTorch 的另一个名为ImageFolder
的恒星数据实用函数,我们可以直接创建一个 PyTorch 数据集实例(第 20 行)。然而,为了使这个函数工作,我们需要数据集与这个项目具有相同的格式。
一旦我们有了数据集,我们就将它传递给预先创建的get_dataloader
函数,以获得一个类似 PyTorch Dataloader 实例的生成器(第 23 行)。
# initialize the yolov5 using torch hub
yoloModel = torch.hub.load("ultralytics/yolov5", "yolov5s")
# initialize iterable variable
sweeper = iter(testLoader)
# initialize image
imageInput = []
# grab a batch of test data
print("[INFO] getting the test data...")
batch = next(sweeper)
(images, _) = (batch[0], batch[1])
# send the images to the device
images = images.to(config.DEVICE)
在线 26 上,使用焊炬集线器调用 YOLOv5。简单回顾一下,torch.hub.load
函数将 GitHub 存储库和所需的入口点作为它的参数。入口点是函数的名称,在这个名称下,模型调用位于所需存储库的hubconf.py
脚本中。
下一步对我们的项目至关重要。我们有很多方法可以从数据集中随机获取一批图像。然而,当我们处理越来越大的数据集时,依靠循环获取数据的效率会降低。
记住这一点,我们将使用一种比循环更有效的方法。我们可以选择使用第 29 行的可迭代变量随机抓取数据。所以每次你运行第 36 行上的命令,你会得到不同的一批数据。
在行 40 上,我们将抓取的数据加载到我们将用于计算的设备上。
# loop over all the batch
for index in range(0, config.PRED_BATCH_SIZE):
# grab each image
# rearrange dimensions to channel last and
# append them to image list
image = images[index]
image = image.permute((1, 2, 0))
imageInput.append(image.cpu().detach().numpy()*255.0)
# pass the image list through the model
print("[INFO] getting detections from the test data...")
results = yoloModel(imageInput, size=300)
在第 43 行,我们有一个循环,在这个循环中我们检查抓取的图像。然后,取每幅图像,我们重新排列维度,使它们成为通道最后的,并将结果添加到我们的imageInput
列表中(第 47-49 行)。
接下来,我们将列表传递给 YOLOv5 模型实例(第 53 行)。
# get random index value
randomIndex = random.randint(0,len(imageInput)-1)
# grab index result from results variable
imageIndex= results.pandas().xyxy[randomIndex]
# convert the bounding box values to integer
startX = int(imageIndex["xmin"][0])
startY = int(imageIndex["ymin"][0])
endX = int(imageIndex["xmax"][0])
endY = int(imageIndex["ymax"][0])
# draw the predicted bounding box and class label on the image
y = startY - 10 if startY - 10 > 10 else startY + 10
cv2.putText(imageInput[randomIndex], imageIndex["name"][0],
(startX, y+10), cv2.FONT_HERSHEY_SIMPLEX,0.65, (0, 255, 0), 2)
cv2.rectangle(imageInput[randomIndex],
(startX, startY), (endX, endY),(0, 255, 0), 2)
# check to see if the output directory already exists, if not
# make the output directory
if not os.path.exists(config.YOLO_OUTPUT):
os.makedirs(config.YOLO_OUTPUT)
# show the output image and save it to path
plt.imshow(imageInput[randomIndex]/255.0)
# save plots to output directory
print("[INFO] saving the inference...")
outputFileName = os.path.join(config.YOLO_OUTPUT, "output.png")
plt.savefig(outputFileName)
第 56 行上的randomIndex
变量将作为我们选择的索引,同时访问我们将要显示的图像。使用其值,在行 59** 上访问相应的边界框结果。**
我们在行 62-65 上分割图像的特定值(起始 X、起始 Y、结束 X 和结束 Y 坐标)。我们必须使用imageIndex["Column Name"][0]
格式,因为results.pandas().xyxy[randomIndex]
返回一个数据帧。假设在给定的图像中有一个检测,我们必须通过调用所需列的第零个索引来访问它的值。
使用这些值,我们分别使用cv2.putText
和cv2.rectangle
在第 69-72 行的上绘制标签和边界框。给定坐标,这些函数将获取所需的图像并绘制出所需的必需品。
最后,在使用plt.imshow
绘制图像时,我们必须缩小数值(第 80 行)。
这就是 YOLOv5 在自定义图像上的结果!我们来看看图 6-8 中的一些结果。
正如我们从结果中看到的,预训练的 YOLOv5 模型在所有图像上定位得相当好。
在固态硬盘型号上测试定制映像
对于 SSD 模型的推理,我们将遵循类似于 YOLOv5 推理脚本中的模式。
# import the necessary packages
from pyimagesearch.data_utils import get_dataloader
from pyimagesearch.data_utils import normalize
from pyimagesearch import config
from torchvision.datasets import ImageFolder
from torch.utils.data import Subset
from sklearn.model_selection import train_test_split
from torchvision.transforms import Compose
from torchvision.transforms import ToTensor
from torchvision.transforms import Resize
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
import random
import torch
import cv2
import os
# initialize test transform pipeline
testTransform = Compose([
Resize((config.IMAGE_SIZE, config.IMAGE_SIZE)), ToTensor()])
# create the test dataset and initialize the test data loader
testDataset = ImageFolder(config.TEST_PATH, testTransform)
testLoader = get_dataloader(testDataset, config.PRED_BATCH_SIZE)
# initialize iterable variable
sweeper = iter(testLoader)
# list to store permuted images
imageInput = []
如前所述,我们在的第 20 和 21 行创建 PyTorch 转换实例。然后,使用ImageFolder
实用函数,我们根据需要创建数据集实例,然后在第 24 和 25 行的上创建Dataloader
实例。
可迭代变量sweeper
在线 28 上初始化,以便于访问测试数据。接下来,为了存储我们将要预处理的图像,我们初始化一个名为imageInput
( 第 31 行)的列表。
# grab a batch of test data
print("[INFO] getting the test data...")
batch = next(sweeper)
(images, _ ) = (batch[0], batch[1])
# switch off autograd
with torch.no_grad():
# send the images to the device
images = images.to(config.DEVICE)
# loop over all the batch
for index in range(0, config.PRED_BATCH_SIZE):
# grab the image, de-normalize it, scale the raw pixel
# intensities to the range [0, 255], and change the channel
# ordering from channels first tp channels last
image = images[index]
image = image.permute((1, 2, 0))
imageInput.append(image.cpu().detach().numpy())
从 YOLOv5 推理脚本再次重复上面代码块中显示的过程。我们抓取一批数据(第 35 行和第 36 行)并循环遍历它们,将每个数据重新排列到最后一个通道,并将它们添加到我们的imageInput
列表(第 39-50 行)。
# call the required entry points
ssdModel = torch.hub.load("NVIDIA/DeepLearningExamples:torchhub",
"nvidia_ssd")
utils = torch.hub.load("NVIDIA/DeepLearningExamples:torchhub",
"nvidia_ssd_processing_utils")
# flash model to the device and set it to eval mode
ssdModel.to(config.DEVICE)
ssdModel.eval()
# new list for processed input
processedInput = []
# loop over images and preprocess them
for image in imageInput:
image = normalize (image)
processedInput.append(image)
# convert the preprocessed images into tensors
inputTensor = utils.prepare_tensor(processedInput)
在第 53 行和第 54 行上,我们使用torch.hub.load
函数来:
- 通过调用相应的存储库和入口点名称来调用 SSD 模型
- 根据 SSD 模型的需要,调用一个额外的实用函数来帮助预处理输入图像。
然后将模型加载到我们正在使用的设备中,并设置为评估模式(第 59 行和第 60 行)。
在第 63 行的上,我们创建了一个空列表来保存预处理后的输入。然后,循环遍历这些图像,我们对它们中的每一个进行归一化,并相应地添加它们(第 66-68 行)。最后,为了将预处理后的图像转换成所需的张量,我们使用了之前调用的效用函数(第 71 行)。
# turn off auto-grad
print("[INFO] getting detections from the test data...")
with torch.no_grad():
# feed images to model
detections = ssdModel(inputTensor)
# decode the results and filter them using the threshold
resultsPerInput = utils.decode_results(detections)
bestResults = [utils.pick_best(results,
config.THRESHOLD) for results in resultsPerInput]
关闭自动梯度,将图像张量输入 SSD 模型(第 75-77 行)。
在第 80 行的上,我们使用来自utils
的另一个名为decode_results
的函数来获得对应于每个输入图像的所有结果。现在,由于 SSD 为您提供了 8732 个检测,我们将使用之前在config.py
脚本中设置的置信度阈值,只保留置信度超过 50%的检测(第 81 行和第 82 行)。
这意味着bestResults
列表包含边界框值、对象类别和置信度值,对应于它在给出检测输出时遇到的每个图像。这样,这个列表的索引将直接对应于我们的输入列表的索引。
# get coco labels
classesToLabels = utils.get_coco_object_dictionary()
# loop over the image batch
for image_idx in range(len(bestResults)):
(fig, ax) = plt.subplots(1)
# denormalize the image and plot the image
image = processedInput[image_idx] / 2 + 0.5
ax.imshow(image)
# grab bbox, class, and confidence values
(bboxes, classes, confidences) = bestResults[image_idx]
由于我们没有办法将类的整数结果解码成它们对应的标签,我们将借助来自utils
的另一个函数get_coco_object_dictionary
( 第 85 行)。
下一步是将结果与其对应的图像进行匹配,并在图像上绘制边界框。相应地,使用图像索引抓取相应的图像并将其反规格化(第 88-93 行)。
使用相同的索引,我们从结果中获取边界框结果、类名和置信度值(第 96 行)。
# loop over the detected bounding boxes
for idx in range(len(bboxes)):
# scale values up according to image size
(left, bot, right, top) = bboxes[idx ] * 300
# draw the bounding box on the image
(x, y, w, h) = [val for val in [left, bot, right - left,
top - bot]]
rect = patches.Rectangle((x, y), w, h, linewidth=1,
edgecolor="r", facecolor="none")
ax.add_patch(rect)
ax.text(x, y,
"{} {:.0f}%".format(classesToLabels[classes[idx] - 1],
confidences[idx] * 100),
bbox=dict(facecolor="white", alpha=0.5))
由于单个图像可能有多个检测,我们创建一个循环并开始迭代可用的边界框(行 99 )。边界框结果在 0 和 1 的范围内。所以在解包边界框值时,根据图像的高度和宽度缩放它们是很重要的(第 101 行)。
现在,SSD 模型输出左、下、右和上坐标,而不是 YOLOv5s 的起始 X、起始 Y、结束 X 和结束 Y 值。因此,我们必须计算起始 X、起始 Y、宽度和高度,以便在图像上绘制矩形(行 104-107 )。
最后,我们在第 109-112 行的函数的帮助下添加对象类名。
# check to see if the output directory already exists, if not
# make the output directory
if not os.path.exists(config.SSD_OUTPUT):
os.makedirs(config.SSD_OUTPUT)
# save plots to output directory
print("[INFO] saving the inference...")
outputFileName = os.path.join(config.SSD_OUTPUT, "output.png")
plt.savefig(outputFileName)
我们通过将输出图像文件保存到我们之前设置的位置(行 121 和 120 )来结束脚本,并绘制出我们的图像。
让我们看看脚本的运行情况!
在图 9-11 中,我们有 SSD 模型对来自自定义数据集的图像预测的边界框。
在图 11 中,SSD 模型设法找出了小狗,这是值得称赞的,它几乎将自己伪装成了它的父母。否则,SSD 模型在大多数图像上表现得相当好,置信度值告诉我们它对其预测的确信程度。
总结
由于物体检测已经成为我们生活的一个主要部分,因此能够获得可以复制高水平研究/行业水平结果的模型对于学习人群来说是一个巨大的福音。
本教程再次展示了 Torch Hub 简单而出色的入口点调用系统,在这里,我们可以调用预训练的全能模型及其辅助助手函数来帮助我们更好地预处理数据。这整个过程的美妙之处在于,如果模型所有者决定将更改推送到他们的存储库中,而不是通过许多过程来更改托管数据,他们需要将他们的更改推送到他们的存储库本身。
随着处理托管数据的整个过程的简化,PyTorch 在与 GitHub 的整个合作过程中取得了成功。这样,即使是调用模型的用户也可以在存储库中了解更多信息,因为它必须是公开的。
我希望本教程可以作为在您的定制任务中使用这些模型的良好起点。指导读者尝试他们的图像或考虑使用这些模型来完成他们的定制任务!
引用信息
Chakraborty,D. “火炬中心系列#3: YOLOv5 和 SSD-物体探测模型”, PyImageSearch ,2022 年,https://PyImageSearch . com/2022/01/03/Torch-Hub-Series-3-yolov 5-SSD-物体探测模型/
@article{
dev_2022_THS3,
author = {
Devjyoti Chakraborty},
title = {
{
Torch Hub} Series \#3: {
YOLOv5} and {
SSD} — Models on Object Detection},
journal = {
PyImageSearch},
year = {
2022},
note = {
https://pyimagesearch.com/2022/01/03/torch-hub-series-3-yolov5-and-ssd-models-on-object-detection/},
}
要下载这篇文章的源代码(并在未来教程在 PyImageSearch 上发布时得到通知),只需在下面的表格中输入您的电子邮件地址!*