之前的文章介绍了YOLOv5的网络结构🚀与目录结构源码🚀的详细解读,今天带来的是YOLOv5的 detect.py 代码逐行解读以及注释,废话不多说,让我们一起学习YOLOv5的 detect.py 源码吧!
YOLOv5所使用版本:v6.1(由于YOLOV5版本一直在更新,不同版本代码略有差异,但差别不大,可供用来学习。)
YOLOv5 源码地址:GitHub - ultralytics/yolov5 at v6.1
其他学习YOLOv5直通车🚀:
YOLOv5【使用云GPU进行训练】超详细教程!!!🚀🚀
YOLOv5【使用云GPU连接本地Pycharm进行训练】超详细教程!!!🚀🚀
YOLOv5【网络结构】超详细解读!!✨✨
YOLOv5【目录结构源码】超详细解读!!!🚀🚀
detect.py 执行主要流程🚀
detect.py 执行主要流程分为四大块,主要流程如下✨✨:1.导入相关的库及配置文件---->2.导入完包以后执行main函数---->3.执行main函数时用到parse_opt()这个函数,它的功能主要是解析参数,在parse_opt()执行完成之后,会将opt传给函数main()---->4.main()函数中调用了函数run(),run()主要分为了七个部分:(1)传入参数、(2)初始化配置、(3)保存结果、(4)加载模型的权重、(5)加载待预测的数据、(6)执行模型的推理过程、(7)打印输出信息。
接下来让我们按照执行流程来依次解析detect.py源码把!!!✨✨
一、导入相关的库及配置文件🚀
我们先从 detect.py 导入库以及相关配置文件开始介绍😊
1.1 ✨
导入所需要的相关库✨
'''------------------1.1✨导入所需要的相关库✨--------------------'''
import argparse # 解析命令行参数的库
import os # 与操作系统进行交互的文件库 包含文件路径操作与解析
import sys # 包含了与python解释器和它的环境有关的函数。
from pathlib import Path # Path能够更加方便得对字符串路径进行处理
import cv2 # 开源的库平台计算机视觉库。有很强大的图片处理功能,可实现图像处理和计算机视觉方面的很多通用算法。
import torch #pytorch 深度学习库
import torch.backends.cudnn as cudnn #提供了一个接口,用于使用cuDNN库,在NVIDIA GPU上高效地进行深度学习
1.argparse: 这个库允许用户为Python脚本指定命令行参数。它简化了处理命令行输入的过程,它是一个用于命令行参数解析的模块,通过在程序中定义好我们需要的参数,argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息。
2.os: 这个库提供了一种与操作系统交互的方式,比如创建和删除目录,列出文件等等。它提供了多种操作系统的接口,通过os模块提供的操作系统接口,我们可以对操作系统里文件、终端、进程等进行操作。
3.sys:这个库提供了访问解释器使用或维护的一些变量(如传递给Python脚本的命令行参数),以及与解释器强烈交互的函数,它是与python解释器交互的一个接口,该模块提供对解释器使用或维护的一些变量的访问和获取,它提供了许多函数和变量来处理 Python 运行时环境的不同部分。
4.pathlib:这个库提供了一种面向对象的方式来与文件系统交互,可以让代码更简洁、更易读。
5.cv2:这个库是一个开源的库平台计算机视觉库。有很强大的图片处理功能,可实现图像处理和计算机视觉方面的很多通用算法。
6.torch:这个库是主要的Pytorch库。它提供了构建、训练和评估神经网络的工具。
7.torch.backends. cudnn: 导入了torch.backends.cudnn库,它提供了一个接口,用于使用cuDNN库,在NVIDIA GPU上高效地进行深度学习。cudnn模块是一个PyTorch库的扩展。
1.2 ✨路径代码✨
这段代码主要作用: 将当前项目添加到系统路径上,并将项目的绝对路径转换成相对路径保存在ROOT中。
'''----------------------1.2✨路径代码✨----------------------'''
FILE = Path(__file__).resolve() # __file__指的是当前文件(即detect.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/detect.py
ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径
1. FILE = Path(__file__).resolve() :获取detect.py在电脑中的绝对路径。
2.ROOT = FILE.parents[0] :接使用parents[0]属性获取该文件的父级目录,即YOLOv5根目录,并将其赋值给变量ROOT。
3.if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) :如果ROOT不在sys.path中,就将ROOT添加到运行路径上,以便Python能够找到该目录中的其他模块。
4.ROOT = Path(os.path.relpath(ROOT, Path.cwd())) :将ROOT路径对象相对于当前工作目录转换为一个相对路径,并将结果赋值给ROOT变量。
这样做原因:在不同的操作系统和环境下,路径的表示方式可能有所不同。将路径表示为相对路径可以确保代码在不同的环境中具有相同的行为。
1.3 ✨导入自定义辅助库✨
这段代码导入了YOLOv5的许多辅助模块,以便进行物体检测和识别的相关任务。上一步已经加载路径,现在进行导入,顺序不可以调换。具体来说,代码从如下几个文件中导入了部分函数和类:
'''-------------------- 1.3 ✨导入自定义辅助库✨-----------------'''
from models.common import DetectMultiBackend #定义了一些通用的函数和类,比如图像的处理、非极大值抑制等等。
from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams #定义了两个类,LoadImages和LoadStreams,它们可以加载图像或视频帧,并对它们进行一些预处理,以便进行物体检测或识别。
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh) #定义了一些常用的工具函数,比如检查文件是否存在、检查图像大小是否符合要求、打印命令行参数等等。
from utils.plots import Annotator, colors, save_one_box #定义了Annotator类,可以在图像上绘制矩形框和标注信息。
from utils.torch_utils import select_device, time_sync #定义了一些与PyTorch有关的工具函数,比如选择设备、同步时间等等。
1.models.common.py: 这个文件定义了一些通用的函数和类,比如图像的处理、非极大值抑制等等。
2.utils.datasets.py: 这个文件定义了两个类,LoadImages和LoadStreams,它们可以加载图像或视频帧,并对它们进行一些预处理,以便进行物体检测或识别。
3.utils.general.py: 这个文件定义了一些常用的工具函数,比如检查文件是否存在、检查图像大小是否符合要求、打印命令行参数等等。
4.utils.plots.py: 这个文件定义了Annotator类,可以在图像上绘制矩形框和标注信息。
5.utils.torch_utils.py: 这个文件定义了一些与PyTorch有关的工具函数,比如选择设备、同步时间等等。
通过导入这些辅助模块,可以更方便地进行目标检测的相关任务,并且减少了代码的复杂度和冗余。
二、main() 函数🚀
这是程序的主函数。它调用了 check_requirements() 函数和 run() 函数,并将命令行参数 opt 转换为字典作为参数传递给 run() 函数。
'''----------------------二、main函数🚀-------------------------'''
def main(opt):
# 检查环境/打印参数,主要是requrement.txt的包是否安装,用彩色显示设置的参数
check_requirements(exclude=('tensorboard', 'thop'))
# 执行run()函数
run(**vars(opt))
# 命令使用
# python detect.py --weights runs/train/exp_yolov5s/weights/best.pt --source data/images/fishman.jpg # webcam
if __name__ == "__main__":
opt = parse_opt() # 解析参数
main(opt) # 执行主函数
1.check_requirements(exclude=('tensorboard', 'thop')) :检查程序所需的依赖项是否已安装。
2.run(**vars(opt)) : 将 opt 变量的属性和属性值作为关键字参数传递给 run() 函数。
3.if name == ‘main’:就是控制执行代码的过程,让的文件作为脚本直接执行,才会被执行,而 import 到其他脚本中是不会被执行的。
4.opt = parse_opt() :解析命令行传进的参数。该段代码分为三部分,第一部分定义了一些可以传导的参数类型,第二部分对于imgsize部分进行了额外的判断(640*640),第三部分打印所有参数信息,opt变量存储所有的参数信息,并返回。
5.main(opt): 执行命令行参数。该段代码分为两部分,第一部分首先完成对于requirements.txt的检查,检测这些依赖包有没有安装;第二部分,将opt变量参数传入,执行run函数。
三、parse_opt()函数🚀
执行main函数时用到并parse_opt()这个函数,它的功能主要是为模型进行推理时提供参数,在parse_opt()执行完成之后,会将opt传给函数main()。
接下来让我们逐行解析一下 parse_opt() 函数的相关参数:
3.1 '--weights' ⭐
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)')
这个参数是指定权重文件的路径,默认是yolov5s.pt,可以使用自己训练的权重,也可以使用官网提供的权重,下载好后放在根目录就好。默认官网的权重yolov5s.pt (yolov5n.pt/yolov5s.pt/yolov5m.pt/yolov5l.pt/yolov5x.pt/区别在于网络的宽度和深度以此增加)。
下图是官方提供的预训练权重:
3.2 '--source' ⭐
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam')
这个设置的参数是:指定网络输入的测试数据的路径文件夹,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以指定具体的文件或者扩展名。默认是 data/images 文件夹,测试的时候默认测试此文件夹下的图片。
3.3 '--data' ⭐
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path')
这个设置的参数是:配置数据的文件路径,默认为COCO128数据集的配置文件路径。包括数据集的下载路径和一些基本信息,在预测时如果不自己指定数据集,系统会自己下载coco128数据集。
3.4 '--imgsz' ⭐
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
这个设置的参数是:模型在检测图片前会把图片resize成640 × 640的尺寸,然后再输入进网络里。
3.5 '--conf-thres' ⭐
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
这个设置的参数是:置信度阈值,默认为 0.25。表示预测置信度大于0.5的值才会被框选出来。置信度:指网络对检测出来的目标正确的相信程度。当参数设置为0时,网络只要认为检测目标有一丢丢的正确,就会被框选出来。
(为了方便理解接下来我分别将--conf-thres设置为0,0.25,0.67来看一下检测结果)
--conf-thres=0 | |
--conf-thres=0.25 | |
--conf-thres=0.67 |
3.6 '--iou-thres' ⭐
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
这个设置的参数是:非极大抑制时的 IoU 阈值,默认为 0.45.
3.6.1 NMS与IoU✨
非极大值抑制算法(NMS):执行目标检测任务时,可能对同一目标进行多次检测,得到多个检测框。而NMS算法时一种确保对每个对象只得到一个检测框的方法。
IoU:计算的是 “预测的边框” 和 “真实的边框” 的交集和并集的比值。IoU的值越高也说明A框与B框重合程度越高,代表模型预测越准确。反之,IoU越低模型性能越差。
关于NMS与IoU详细可以看 我之前的这篇文章:直通车🚀
✨✨非极大值抑制的流程如下✨✨:
- 根据置信度得分进行排序
- 选择置信度最高的边界框添加到最终输出列表中,将其从边界框列表中删除
- 计算所有边界框的面积
- 计算置信度最高的边界框与其它候选框的IoU。
- 删除IoU大于阈值的边界框
- 重复上述过程,直至边界框列表为空。
3.6.2 设置阈值例子✨
(为了方便理解接下来我分别将--iou-thres设置为0.45,0.9,1来看一下检测结果)
--iou-thres=0.45 | |
--iou-thres=0.8 | |
--iou-thres=1 |
3.7 '--max-det' ⭐
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
这个设置的参数是:保留的最大检测框(检测目标)数量,每张图片中检测目标的个数最多为1000类。
(为了方便理解接下来我分别将--max-det设置为0,1,3,6来看一下检测结果)
--max-det=0 | |
--max-det=1 | |
--max-det=2 | |
--max-det=6 |
3.8 '--device' ⭐
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
这个设置的参数是:预测时使用的设备,按照自己需求可以选择GPU或CPU。填写的是cuda 设备的 ID(例如 0,1,2,3或者是 'cpu'),显卡编号可以使用nvidia-smi
指令来查看。
注意: action='store_true' ⭐
action=store_true是argparse模块中的一个参数,用于指定当命令行参数存在时,将其值设置为True。如果命令行参数不存在,则该值将保持为默认值(通常为False)。这个参数通常用于开关选项,我们要去使用命令行参数去激活他。
举例说明:
import argparse
parser = argparse.ArgumentParser(description='test.py')
parser.add_argument('--cuda', type=bool, default=True, help='use cuda')
parser.add_argument('--cpu',action='store_true',help='use cpu')
args = parser.parse_args()
print("cuda: ",args.cuda)
print("cpu: ",args.cpu)
如果运行命令为:python test.py
则输出为:
cuda: True
cpu: False
如果运行命令为:python test.py --cpu
则输出为:
cuda: True
cpu: True
action=store_true 相当于一个开关,要使用命令行参数(python test.py --参数)打开这个开关,接下来我们看下面几个例子吧:
3.9 '--view-img' ⭐
parser.add_argument('--view-img', action='store_true', help='show results')
这个设置的参数是:是否将检测结果实时的展示出来(一闪而过)。
(因为有action=store_true,为了方便理解接下来我使用命令行参数,并展示下结果)
终端输入:
python detect.py --view-img
如果我使用命令行参数指定了这个参数的话,就相当于打开了--view-img 的开关,那么模型每检测出一张就会显示出一张,直到所有图片检测完成。如果我不用命令行参数指定这个参数,那么模型就不会一张一张的显示出来,其他有action=store_true的参数同理。
结果图如下:
3.10 '--save-txt' ⭐
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
这个设置的参数是:是否将检测结果(类别信息以及检测框位置信息)保存起来,并保存成.txt 格式。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --save-txt
可以看到输出结果多了一个labels 文件夹:
打开文件夹可以看见每张预测图片对应的.txt文件:
每个文件里都保存着检测的结果包含:类别信息以及检测框位置信息(红框:类别,黄框:位置信息)。
3.11 '--save-conf' ⭐
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
这个设置的参数是:是否将检测结果的置信度保存起来,并保存成.txt 格式。
注意:必须和--save-txt 配合使用!!!单独使用不报错,但是也没有效果。
即终端输入:(这里面命令行参数进行了组合使用,同理,其他命令行参数也可以组合使用!)
python detect.py --save-txt --save-conf
可以看到和上面相比多了一行数据(绿框),这行代表的就是置信度(红框:类别,黄框:位置信息,绿框:置信度)。
3.12 '--save-conf' ⭐
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
这个设置的参数是:是否把模型检测的结果裁剪下来,并保存在crops文件夹下。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --save-crop
可以看到exp文件夹下多了一个crops文件夹,将相同种检测的类别保存在一个文件夹下:
下面展示一下几张裁剪下来的person图,这里展示海贼王的啦哈哈哈哈~~
3.13 '--nosave' ⭐
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
这个设置的参数是:不保存图片、视频等预测结果。不设置--nosave 在runs/detect/exp*/会出现预测的结果,若设置了--nosave,则只会产生空文件夹,文件夹里无任何预测结果。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --nosave
只会产生一个空的exp(exp25),无任何预测结果。
注意:我们可以利用这个参数以及结合上面的其他参数来获取我们想要的检测结果相关信息!!!比如:
正常的预测exp文件夹只有预测之后的图片:
我们完全可以利用命令行参数来获取我们想要的检测结果信息,比如我们不想要预测之后的图片,只想要检测结果裁剪后的图片以及label,我们可以结合--nosave将检测图片清空不保存,再使用--save-crop与--sace-label来获取裁剪后的图片及label:
(为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
detect.py --nosave --save-crop --save-txt
可以看到产生的文件夹exp26中只显示我们想要的检测结果的信息😍😍😍
3.14 '--classes' ⭐
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
这个设置的参数是:根据类别编号,仅检测指定类别,检测类别可以多个。
注意:nargs=‘+’ 与‘action=store_true’ 一样都使用命令行参数。nargs是用来说明传入的参数个数,’+’ 表示传入至少一个参数,若关联多个参数,在命令行运行时,输入两个数字,以空格隔开就好。
若只检测‘0’这个类别,则可在终端输入:
python detect.py --classes 0
若要检测多个类别(‘0‘,’1‘,’4‘),则在终端输入:
python detect.py --class 0 1 4
(不同类别直接用空格隔开。)
接下来说一下classes(类别)参数,可以参考coco128.yaml 配置文件,里面介绍了80种类别,类别编号从0开始,依次往下排列:
在上图中可以看到人’person‘这个参数为第一个,因此它的编号为’0‘,那我这里给classes指定参数为’0‘,意思就是说只检测编号为’0‘的’person‘这个类别,为了方便理解接下来我使用命令行参数展示下结果:
终端输入:
python detect.py --nosave --classes 0
检测结果:
未使用--classes命令参数 | |
--classes=0(person) |
3.15 '--agnostic-nms' ⭐
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
这个设置的参数是:是否使用类别不敏感的非极大抑制(即不考虑类别信息),默认为False。
在训练阶段是不需要nms处理的,只有在验证或者是测试阶段才需要将预测结果进行非极大值抑制处理,让每个类都能独立执行NMS,在所有的边框上添加一个偏移量。偏移量仅取决于类ID,并且足够大,以便来自不同类的框不会重叠,来挑选最佳的正样本。
比如待检测图像中有一个长得很像梨的苹果,那在识别时这个苹果可能会被同时框上2个框:一个梨,一个苹果,开启agnostic-nms后,让两个类都能独立执行NMS,只框出一个正样本最佳的框。
3.16 '--arugment' ⭐
parser.add_argument('--augment', action='store_true', help='augmented inference')
这个设置的参数是: 是否使用数据增强进行推理,默认为False。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --augment
未使用--augment命令参数 | |
使用--augment命令参数 |
可以看到效果挺明显,一些数据被增强了。
3.17 '--arugment' ⭐
parser.add_argument('--visualize', action='store_true', help='visualize features')
这个设置的参数是:可视化特征图。若开启了参数,exp文件夹就会产生对应检测图片的文件夹,文件夹里是该图片的特征图信息文件。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --visualize
可以看到多生成了三个文件夹,文件夹里面是对于图片的特征图信息文件:
文件夹内特征图信息文件:
.npy格式的文件就是保存的模型文件,可以使用numpy读写。.png是特征图的图片文件。
下面可以看一下保存下来的特征图(以海贼图为例):
stage0 | |
stage3 | |
stage17 | |
stage23 |
3.18 '--update' ⭐
parser.add_argument('--update', action='store_true', help='update all models')
这个设置的参数是:对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False。
3.19 '--project' ⭐
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
这个设置的参数是:预测结果保存的项目目录路径,默认为 'ROOT/runs/detect'。
3.20 '--name' ⭐
parser.add_argument('--name', default='exp', help='save results to project/name')
这个设置的参数是:预测结果保存的子目录名称,默认为 'exp'。
3.21 '--exist-ok' ⭐
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
这个设置的参数是:覆盖已有结果。若指定了此参数,预测的结果保存在上一次保存的文件夹中,若不指定,每次预测结果则会保存一个新的文件夹中。
3.22 '--line-thickness' ⭐
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
这个设置的参数是:画 bounding box (检测框)时的线条宽度,默认为 3,数值越大,线条越粗(过粗会遮挡检测目标)。
--line-thickness=3 | |
--line-thickness=15 |
3.23 '--hide-labels' ⭐
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
这个设置的参数是:隐藏标签信息,只保留检测框。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --hide-labels
未使用--line-thickness命令参数 | |
使用--line-thickness命令参数 |
3.24 '--hide-conf' ⭐
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
这个设置的参数是:隐藏置信度。
(因为有action=store_true,为了方便理解接下来我使用命令行参数展示下结果)
终端输入:
python detect.py --hide-conf
未使用--hide-conf命令参数 | |
使用--hide-conf命令参数 |
3.25 '--half' ⭐
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
这个设置的参数是:是否使用 FP16 半精度进行推理,默认为 False。
3.26 '--dnn' ⭐
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
这个设置的参数是: 是否使用 OpenCV DNN 进行 ONNX 推理,默认为 False。
上面两个参数都没使用过,不太熟悉,对 Open CV感兴趣的可以看看这篇文章:直通车🚀OpenCV函数大全(超级详细版)-python操作_opencv函数文档_耿鬼喝椰汁的博客-CSDN博客
四、run()函数🚀
main()函数中调用了函数run(),run()主要分为了七个部分:(1)传入参数、(2)初始化配置、(3)保存结果、(4)加载模型的权重、(5)加载待预测的数据、(6)执行模型的推理过程、(7)打印输出信息。
接下来让我们按照执行流程来依次解析run函数吧!!!✨✨
4.1 传入参数 ✨✨
这段代码定义了run()函数,并设置了一系列参数,用于指定物体检测或识别的相关参数,在3.1-3.26中已经做了详细介绍。
'''------------------------4.1 传入参数 ✨✨-------------------------'''
@torch.no_grad() # 该标注使得方法中所有计算得出的tensor的requires_grad都自动设置为False,也就是说不进行梯度的计算(当然也就没办法反向传播了), 节约显存和算
def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s) 事先训练完成的权重文件,比如yolov5s.pt,默认 weights/,假如使用官方训练好的文件(比如yolov5s),则会自动下载
source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam 预测时的输入数据,可以是文件/路径/URL/glob, 输入是0的话调用摄像头作为输入,默认data/images/
# data=ROOT / 'data/coco128.yaml', # dataset.yaml path, data文件路径,包括类别/图片/标签等信息
imgsz=(640, 640), # inference size (pixels) 预测时的放缩后图片大小(因为YOLO算法需要预先放缩图片), 两个值分别是height, width。默认640*640
conf_thres=0.25, # confidence threshold 置信度阈值, 高于此值的bounding_box才会被保留。默认0.25,用在nms中
iou_thres=0.45, # NMS IOU threshold IOU阈值,高于此值的bounding_box才会被保留。默认0.45,用在nms中
max_det=1000, # maximum detections per image 一张图片上检测的最大目标数量,用在nms中
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu 所使用的GPU编号,如果使用CPU就写cpu
view_img=False, # show results 是否展示预测之后的图片或视频,默认False
save_txt=False, # save results to *.txt 是否将预测的框坐标以txt文件形式保存, 默认False, 使用--save-txt 在路径runs/detect/exp*/labels/*.txt下生成每张图片预测的txt文件
save_conf=False, # save confidences in --save-txt labels 是否将结果中的置信度保存在txt文件中,默认False
save_crop=False, # save cropped prediction boxes 是否保存裁剪后的预测框,默认为False, 使用--save-crop 在runs/detect/exp*/crop/剪切类别文件夹/ 路径下会保存每个接下来的目标
nosave=False, # do not save images/videos 不保存图片、视频, 要保存图片,不设置--nosave 在runs/detect/exp*/会出现预测的结果
classes=None, # filter by class: --class 0, or --class 0 2 3 过滤指定类的预测结果
agnostic_nms=False, # class-agnostic NMS 进行NMS去除不同类别之间的框, 默认False
augment=False, # augmented inference TTA测试时增强/多尺度预测,可以提分
visualize=False, # visualize features 是否可视化网络层输出特征
update=False, # update all models 如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
project=ROOT / 'runs/detect', # save results to project/name 预测结果保存的路径
name='exp', # save results to project/name 结果保存文件夹的命名前缀
exist_ok=False, # existing project/name ok, do not increment True: 推理结果覆盖之前的结果 False: 推理结果新建文件夹保存,文件夹名递增
line_thickness=3, # bounding box thickness (pixels) 绘制Bounding_box的线宽度
hide_labels=False, # hide labels 若为True: 隐藏标签
hide_conf=False, # hide confidences 若为True: 隐藏置信度
half=False, # use FP16 half-precision inference 是否使用半精度推理(节约显存)
dnn=False, # use OpenCV DNN for ONNX inference 是否使用OpenCV DNN预测
):
这里再介绍下这些参数:
1.weights: 模型权重文件的路径,默认为YOLOv5s的权重文件路径。
2.source: 输入图像或视频的路径或URL,或者使用数字0指代摄像头,默认为YOLOv5自带的测试图像文件夹。
3.data: 数据集文件的路径,默认为COCO128数据集的配置文件路径。
4.imgsz: 输入图像的大小,默认为640x640。
5.conf_thres: 置信度阈值,默认为0.25。
6.iou_thres: 非极大值抑制的IoU阈值,默认为0.45。
7.max_det: 每张图像的最大检测框数,默认为1000。
8.device: 使用的设备类型,默认为空,表示自动选择最合适的设备。
9.view_img: 是否在屏幕上显示检测结果,默认为False。
10.save_txt: 是否将检测结果保存为文本文件,默认为False。
11.save_conf: 是否在保存的文本文件中包含置信度信息,默认为False。
12.save_crop: 是否将检测出的目标区域保存为图像文件,默认为False。
13.nosave: 是否不保存检测结果的图像或视频,默认为False。
14.classes: 指定要检测的目标类别,默认为None,表示检测所有类别。
15.agnostic_nms: 是否使用类别无关的非极大值抑制,默认为False。
16.augment: 是否使用数据增强的方式进行检测,默认为False。
17.visualize: 是否可视化模型中的特征图,默认为False。
18.update: 是否自动更新模型权重文件,默认为False。
19.project: 结果保存的项目文件夹路径,默认为“runs/detect”。
20.name: 结果保存的文件名,默认为“exp”。
21.exist_ok: 如果结果保存的文件夹已存在,是否覆盖,默认为False,即不覆盖。
22.line_thickness: 检测框的线条宽度,默认为3。
23.hide_labels: 是否隐藏标签信息,默认为False,即显示标签信息。
24.hide_conf: 是否隐藏置信度信息,默认为False,即显示置信度信息。
25.half: 是否使用FP16的半精度推理模式,默认为False。
26.dnn: 是否使用OpenCV DNN作为ONNX推理的后端,默认为False。
4.2 初始化配置 ✨✨
这段代码主要用于处理输入来源。定义了一些布尔值区分输入是图片、视频、网络流还是摄像头。首先将source转换为字符串类型,然后判断是否需要保存输出结果。如果nosave和source的后缀不是.txt,则会保存输出结果。
接着根据source的类型,确定输入数据的类型:
若source的后缀是图像或视频格式之一,那么将is_file设置为True;
若source以rtsp://、rtmp://、http://、https://开头,那么将is_url设置为True;
若source是数字或以.txt结尾或是一个URL,那么将webcam设置为True;
若source既是文件又是URL,那么会调用check_file函数下载文件。
'''------------------------4.2 初始化配置 ✨✨---------------------'''
source = str(source) # 输入的路径变为字符串
save_img = not nosave and not source.endswith('.txt') # 是否保存图片和txt文件,如果nosave(传入的参数)为false且source的结尾不是txt则保存图片
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
# 判断source是不是视频/图像文件路径
# Path()提取文件名。suffix:最后一个组件的文件扩展名。若source是"D://YOLOv5/data/1.jpg", 则Path(source).suffix是".jpg", Path(source).suffix[1:]是"jpg"
# 而IMG_FORMATS 和 VID_FORMATS两个变量保存的是所有的视频和图片的格式后缀。
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
# 判断source是否是链接
# .lower()转化成小写 .upper()转化成大写 .title()首字符转化成大写,其余为小写, .startswith('http://')返回True or Flase
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)
# 判断是source是否是摄像头
# .isnumeric()是否是由数字组成,返回True or False
if is_url and is_file:
source = check_file(source) # 返回文件。如果source是一个指向图片/视频的链接,则下载输入数据
1. source = str(source) :将输入的路径变为字符串。
2.save_img = not nosave and not source.endswith('.txt') : 是否保存图片和txt文件,如果nosave(传入的参数)为false且source的结尾不是txt则保存图片。
3.is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) :判断source是不是视频/图像文件路径。Path()用来提取文件名。suffix:最后一个组件的文件扩展名。若source是"D://YOLOv5/data/1.jpg", 则Path(source).suffix是".jpg", Path(source).suffix[1:]是"jpg"而IMG_FORMATS 和 VID_FORMATS两个变量保存的是所有的视频和图片的格式后缀。
4. is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) : 1.判断source是否是链接 2. .lower()转化成小写 .upper()转化成大写 .title()首字符转化成大写,其余为小写, .startswith('http://')返回True or Flase。
5. webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file) :判断是source是否是摄像头。.isnumeric()是否是由数字组成,返回True or False
6. if is_url and is_file:
source = check_file(source) :返回文件。如果source是一个指向图片/视频的链接,则下载输入数据。
4.3 保存预测结果 ✨✨
这段代码主要是用于创建保存输出结果的目录。创建一个新的文件夹exp(在runs文件夹下)来保存运行的结果。
'''----------------------------4.3 保存预测结果 ✨✨------------------------'''
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # save_dir是保存运行结果的文件夹名,是通过递增的方式来命名的。第一次运行时路径是“runs\detect\exp”,第二次运行时路径是“runs\detect\exp1”
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # 根据前面生成的路径创建文件夹
1.save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) :save_dir是保存运行结果的文件夹名,是通过递增的方式来命名的。第一次运行时路径是“runs\detect\exp”,第二次运行时路径是“runs\detect\exp1” 。
2.(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) :根据前面生成的路径创建文件夹。
首先代码中的 project 指 run 函数中的 project,对应的是 runs/detect 的目录,name 对应 run 函数中的“name=exp”,然后进行拼接操作。使用increment_path函数来确保目录不存在,如果存在,则在名称后面添加递增的数字。然后判断 save_txt 是否为 true,save_txt 在 run 函数以及 parse_opt() 函数中都有相应操作,如果传入save_txt,新建 “labels” 文件夹存储结果。如果目录已经存在,而exist_ok为False,那么会抛出一个异常,指示目录已存在。如果exist_ok为True,则不会抛出异常,而是直接使用已经存在的目录。
4.4 加载模型权重 ✨✨
这段代码主要是用于选择设备、初始化模型和检查图像大小。首先调用select_device函数选择设备,如果device为空,则使用默认设备。然后使用DetectMultiBackend类来初始化模型,接着从模型中获取stride、names和pt等参数,最后调用check_img_size函数检查图像大小是否符合要求,如果不符合则进行调整。
'''-------------------------4.4 加载模型权重 ✨✨-----------------------'''
device = select_device(device) # 获取设备 CPU/CUDA
model = DetectMultiBackend(weights, device=device, dnn=dnn) # DetectMultiBackend定义在models.common模块中,是我们要加载的网络,其中weights参数就是输入时指定的权重文件(比如yolov5s.pt)
stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx
#stride:推理时所用到的步长,默认为32, 大步长适合于大目标,小步长适合于小目标
#names:保存推理结果名的列表,比如默认模型的值是['person', 'bicycle', 'car', ...]
#pt: 加载的是否是pytorch模型(也就是pt格式的文件)
#jit:当某段代码即将第一次被执行时进行编译,因而叫“即时编译”
#onnx:利用Pytorch我们可以将model.pt转化为model.onnx格式的权重,在这里onnx充当一个后缀名称,
#model.onnx就代表ONNX格式的权重文件,这个权重文件不仅包含了权重值,也包含了神经网络的网络流动信息以及每一层网络的输入输出信息和一些其他的辅助信息。
imgsz = check_img_size(imgsz, s=stride) #确保输入图片的尺寸imgsz能整除stride=32 如果不能则调整为能被整除并返回
half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
if pt:
model.model.half() if half else model.model.float() # 如果不是CPU,使用半进度(图片半精度/模型半精度)
1. device = select_device(device) : 获取设备 CPU/CUDA。
2.model = DetectMultiBackend(weights, device=device, dnn=dnn) : DetectMultiBackend定义在models.common模块中,是我们要加载的网络,其中weights参数就是输入时指定的权重文件(比如yolov5s.pt)
3.stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx:
- stride:推理时所用到的步长,默认为32, 大步长适合于大目标,小步长适合于小目标。
- names:保存推理结果名的列表,比如默认模型的值是['person', 'bicycle', 'car', ...] 。
- pt: 加载的是否是pytorch模型(也就是pt格式的文件)。
- jit:当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。
- onnx:利用Pytorch我们可以将model.pt转化为model.onnx格式的权重,在这里onnx充当一个后缀名称。
- model.onnx就代表ONNX格式的权重文件,这个权重文件不仅包含了权重值,也包含了神经网络的网络流动信息以及每一层网络的输入输出信息和一些其他的辅助信息。
4. imgsz = check_img_size(imgsz, s=stride) :确保输入图片的尺寸imgsz能整除stride=32 如果不能则调整为能被整除并返回。
5. half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
if pt:
model.model.half() if half else model.model.float() :如果不是CPU,使用半进度(图片半精度/模型半精度)
4.5 加载待预测的数据 ✨✨
这段代码通过输入的 source 参数来判断数据输入源(是摄像头还是从source文件下读取的)。
若输入源是摄像头:使用 LoadStreams 加载视频流,并设置 cudnn.benchmark = True 以加速常量图像大小的推理。
若输入源是source文件下读取的(图片/视频):则使用 LoadImages 加载图像。bs:batch_size(批量大小),这里表示 1 或视频流中的帧数,vid_path 和 vid_writer 分别是视频路径和视频编写器,初始化为长度为 batch_size 的空列表。
'''---------------------------4.5 加载待预测的数据 ✨✨-----------------------'''
# Dataloader 通过不同的输入源来设置不同的数据加载方式
if webcam: # 使用摄像头作为输入
view_img = check_imshow() # 检测cv2.imshow()方法是否可以执行,不能执行则抛出异常
cudnn.benchmark = True # 该设置可以加速预测
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit)# 加载输入数据流
'''
source:输入数据源;image_size 图片识别前被放缩的大小;stride:识别时的步长,
auto的作用可以看utils.augmentations.letterbox方法,它决定了是否需要将图片填充为正方形,如果auto=True则不需要
'''
bs = len(dataset) # batch_size 批大小
else: # 直接从source文件下读取图片
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)
bs = 1
# 保存视频的路径
vid_path, vid_writer = [None] * bs, [None] * bs # 前者是视频路径,后者是一个cv2.VideoWriter对
1. if webcam: :判断是否使用摄像头作为输入
2. view_img = check_imshow() :检测cv2.imshow()方法是否可以执行,不能执行则抛出异常。
3. cudnn.benchmark = True : 该设置可以加速预测
4. dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit) :加载输入数据流。
- source:输入数据源;image_size 图片识别前被放缩的大小;stride:识别时的步长,
- auto的作用可以看utils.augmentations.letterbox方法,它决定了是否需要将图片填充为正方形,如果auto=True则不需要。
5. bs = len(dataset) :batch_size 批大小。
6.else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)
bs = 1: 判断,若不是从摄像头读取数据,则直接从source文件下读取数据。
7. vid_path, vid_writer = [None] * bs, [None] * bs :前者是视频路径,后者是一个cv2.VideoWriter对
4.6 执行模型推理过程 ✨✨
执行模型推理过程:把图片或者视频输入模型,产生一个预测结果,并用检测框标记出来。
步骤:1. 让模型进行一个预热,然后定义 dt,seen 两个变量,遍历 dataset ,整理图片信息并进行向前推理、NMS操作;2.进行预测,根据 run 函数里面的置信度以及IOU参数,进行信息过滤;对检测框进行后续处理,画框选择,坐标映射(640*640坐标映射为原图坐标),是否保存绘画结果。
4.6.1 ‘warmup’ 模型预热 ⭐
这段代码让模型进行了一个预热,然后定义 dt,seen 两个变量,遍历 dataset ,整理图片信息。
'''--------------------4.6.1 ‘warmup’ 模型预热⭐-------------------------'''
# Run inference
model.warmup(imgsz=(1 if pt else bs, 3, *imgsz), half=half) # warmup
dt, seen = [0.0, 0.0, 0.0], 0 # dt: 存储每一步骤的耗时 ,seen: 计数功能,已经处理完了多少帧图片
for path, im, im0s, vid_cap, s in dataset:
'''
path:文件路径(即source)
im: resize后的图片(经过了放缩操作)
im0s: 原始图片
vid_cap=none
s: 图片的基本信息,比如路径,大小
'''
'''以下部分整理图片信息'''
t1 = time_sync() # 获取当前时间
im = torch.from_numpy(im).to(device) # 将图片放到指定设备(如GPU)上识别。#torch.size=[3,640,480]
im = im.half() if half else im.float() # uint8 to fp16/32 # 把输入从整型转化为半精度/全精度浮点数。
im /= 255 # 0 - 255 to 0.0 - 1.0 归一化,所有像素点除以255
if len(im.shape) == 3:
im = im[None] # expand for batch dim 添加一个第0维。缺少batch这个尺寸,所以将它扩充一下,变成[1,3,640,480]
t2 = time_sync() # 获取当前时间
dt[0] += t2 - t1 # 记录该阶段耗时
1.model.warmup(imgsz=(1 if pt else bs, 3, *imgsz), half=half) : warmup,进行模型热身。
2.dt, seen = [0.0, 0.0, 0.0], 0 :dt: 存储每一步骤的耗时 ,seen: 计数功能,已经处理完了多少帧图片
3.for path, im, im0s, vid_cap, s in dataset:
- path:文件路径(即source)
- im: resize后的图片(经过了放缩操作)
- im0s: 原始图片
- vid_cap=none
- s: 图片的基本信息,比如路径,大小
'''以下部分整理图片信息'''
4. t1 = time_sync() :获取当前时间。
5.im = torch.from_numpy(im).to(device) :将图片放到指定设备(如GPU)上识别。
6. im = im.half() if half else im.float() :把输入从整型转化为半精度/全精度浮点数。
7.im /= 255 # 0 - 255 to 0.0 - 1.0 :归一化,所有像素点除以255。
8.if len(im.shape) == 3:
im = im[None] # expand for batch dim :添加一个第0维。缺少batch这个尺寸,所以将它扩充一下。
9.t2 = time_sync() :获取当前时间。
10.dt[0] += t2 - t1 :记录该阶段耗时。
4.6.2 'Inference'与'NMS' 推理以及非极大值抑制 ⭐
'''-----------------4.6.2 'Inference'与'NMS' 推理以及非极大值抑制 ⭐--------------------'''
# Inference
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize # 可视化文件路径。如果为True则保留推理过程中的特征图,保存在runs文件夹中else False
# 推理结果,pred保存的是所有的bound_box的信息,
pred = model(im, augment=augment, visualize=visualize) #模型预测出来的所有检测框,torch.size=[1,18900,85]
t3 = time_sync()
dt[1] += t3 - t2
# NMS
# 执行非极大值抑制,返回值为过滤后的预测框
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
'''
pred: 网络的输出结果
conf_thres: 置信度阈值
iou_thres: iou阈值
classes: 是否只保留特定的类别 默认为None
agnostic_nms: 进行nms是否也去除不同类别之间的框
max_det: 检测框结果的最大数量 默认1000
'''
# 预测+NMS的时间
dt[2] += time_sync() - t3
这段代码让模型对每张图片向前推理且使用NMS去除多余框。
1.# Inference:
(1)visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False:可视化文件路径。如果为True则保留推理过程中的特征图,保存在runs文件夹中。
(2) pred = model(im, augment=augment, visualize=visualize) :推理结果,pred保存的是所有的bound_box的信息,。
t3 = time_sync()
dt[1] += t3 - t2
2.#NMS:执行非极大值抑制,返回值为过滤后的预测框。
(1)pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
- pred: 网络的输出结果
- conf_thres: 置信度阈值
- iou_thres: iou阈值
- classes: 是否只保留特定的类别 默认为None
- agnostic_nms: 进行nms是否也去除不同类别之间的框
- max_det: 检测框结果的最大数量 默认1000
(2)dt[2] += time_sync() - t3 # 预测+NMS的时间
4.6.3 ‘Process predictions’ 预测过程 ⭐
这段代码比较长,我们接下来分开讲解:
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(一)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码使用了一个循环来遍历检测结果列表中的每个物体,并对每个物体进行处理。具体描述如下:
for循环中的变量“i”是一个索引变量,表示当前正在处理第几个数据,而变量"det"则表示当前数据的检测结果。循环体中的第一行代码 "seen += 1" 用于增加一个计数器,记录已处理的物体数量。
接下来,根据if语句进行判断是否使用网络摄像头来判断处理单张数据or批量数据。
若使用的是网络摄像头,则代码会遍历每个数据并复制一份备份到变量"im0"中,同时将当前数据的路径和计数器记录到变量"p"和"frame"中。最后,将当前处理的数据索引和相关信息记录到字符串变量"s"中。
如果没有使用网络摄像头,则会直接使用"im0s"变量中的图像,将图像路径和计数器记录到变量"p"和"frame"中。同时,还会检查数据集中是否有"frame"属性,如果有,则将其值记录到变量"frame"中。
# Process predictions
# 把所有的检测框画到原图中
for i, det in enumerate(pred): # per image 每次迭代处理一张图片
'''
i:每个batch的信息
det:表示5个检测框的信息
'''
seen += 1 #seen是一个计数的功能
if webcam: # 如果输入源是webcam则batch_size>=1 取出dataset中的一张图片
p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' # s后面拼接一个字符串i
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
'''
大部分我们一般都是从LoadImages流读取本都文件中的照片或者视频 所以batch_size=1
p: 当前图片/视频的绝对路径 如 F:\yolo_v5\yolov5-U\data\images\bus.jpg
s: 输出信息 初始为 ''
im0: 原始图片 letterbox + pad 之前的图片
frame: 视频流,此次取的是第几张图片
'''
1. for i, det in enumerate(pred): : per image 每次迭代处理一张图片。
- i:每个batch的信息
- det:表示5个检测框的信息
2. seen += 1 :seen是一个计数的功能。
3. if webcam: :如果输入源是webcam则batch_size>=1 取出dataset中的一张图片。
4. p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' : s后面拼接一个字符串i
5.else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
- p: 当前图片/视频的绝对路径 如 D:\yolo_v5\data\images\bus.jpg
- s: 输出信息 初始为 ' '
- im0: 原始图片 letterbox + pad 之前的图片
- frame: 视频流,此次取的是第几张图片
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(二)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码功能:1.将图像路径转换为"Path"对象。使用"save_dir"变量中的路径和图像文件名来构建保存检测结果图像的完整路径,并将其保存在变量"save_path"中。根据数据集的模式("image"或"video")来构建保存检测结果标签的文件路径,并将其保存在变量"txt_path"中。
2.在处理图像路径和文件路径之后,将图像的尺寸信息添加到字符串变量"s"中,以便于打印。接着,计算归一化增益"gn",并将其保存在变量中,以便后续使用。
3.根据是否需要保存截取图像的标志"save_crop"来选择是否要对原始图像进行复制,以备保存截取图像时使用。并创建了一个"Annotator"对象,以便于在图像上绘制检测结果。
p = Path(p) #首先将图像路径转换为"Path"对象。
save_path = str(save_dir / p.name) # 图片/视频的保存路径save_path
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # 设置保存框坐标的txt文件路径,每张图片对应一个框坐标信息
s += '%gx%g ' % im.shape[2:] # 设置输出图片信息。图片shape (w, h)
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # 得到原图的宽和高
imc = im0.copy() if save_crop else im0 # for save_crop # 保存截图。如果save_crop的值为true,则将检测到的bounding_box单独保存成一张图片。
annotator = Annotator(im0, line_width=line_thickness, example=str(names)) # 得到一个绘图的类,类中预先存储了原图、线条宽度、类名。
p = Path(p) :首先将图像路径转换为"Path"对象。
1.save_path = str(save_dir / p.name) :图片/视频的保存路径save_path
2.txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') :设置保存框坐标的txt文件路径,每张图片对应一个框坐标信息。
3.s += '%gx%g ' % im.shape[2:] :设置输出图片信息。
4.gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] : 得到原图的宽和高
5.imc = im0.copy() if save_crop else im0 :保存检测截图。如果save_crop的值为true,则将检测到的bounding_box单独保存成一张图片。
6.annotator = Annotator(im0, line_width=line_thickness, example=str(names)) :得到一个绘图的类,类中预先存储了原图、线条宽度、类名。
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(三)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码功能:首先,将检测结果中的物体坐标从缩放后的图像大小还原回原始图像的大小。这里使用了一个名为"scale_coords"的函数来进行缩放,该函数的作用是将物体坐标从缩放前的大小变换到缩放后的大小。最后遍历每个物体,将其类别和数量添加到字符串变量"s"中。
if len(det): # 判断有没有框
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round() #scale_coords:坐标映射功能
#将预测信息映射到原图
# 将标注的bounding_box大小调整为和原图一致(因为训练时原图经过了放缩)此时坐标格式为xyxy
# 打印检测到的类别数量
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum() # 检测每个类别
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # 加入到字符串中
1.if len(det): : 判断有没有框
2.det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round() : scale_coords:坐标映射功能。将预测信息映射到原图并将标注的bounding_box大小调整为和原图一致(因为训练时原图经过了放缩)此时坐标格式为xyxy。
3.for c in det[:, -1].unique(): :打印检测到的类别数量
4.n = (det[:, -1] == c).sum() : 检测每个类别
5.s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " : 加入到字符串中。
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(四)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码是打印目标检测结果的一些操作.
这段代码功能:
1.如果存在目标检测结果,则代码会执行下一步操作,这里是将检测结果写入文件或在图像上添加框并保存。
2.如果需要将检测结果写入文件,则将检测结果中的物体坐标转换为相对于原始图像的归一化坐标,并将其写入到以图像文件名命名的".txt"文件中。在写入文件时,代码将包含类别、位置和可选置信度等信息。文件的保存路径是变量"txt_path"。
3.如果需要保存检测结果图像或者在图像上绘制框,每个物体添加一个边界框,并将其标记在图像上。具体来说,将边界框选择一个颜色,并在边界框周围添加标签(可选)。
4.如果需要将边界框截取出来保存,则调用名为"save_one_box"的函数,将边界框从图像中截取出来,并将其保存到特定的文件夹中。
这些操作都是基于一些设置变量(如"save_txt"、"save_img"等)来控制的,这些变量决定了检测结果是否应该写入文件或图像。
5.最后,如果需要在窗口中查看检测结果,则代码会在图像上绘制边界框并显示图像。
# Write results
for *xyxy, conf, cls in reversed(det): # 保存预测结果:txt/图片画框/crop-image
if save_txt: # 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh # 将xyxy(左上角+右下角)格式转为xywh(中心点+宽长)格式,并归一化,转化为列表再保存
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # line的形式是: ”类别 x y w h“,若save_conf为true,则line的形式是:”类别 x y w h 置信度“
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
if save_img or save_crop or view_img: # 在原图上画框+将预测到的目标剪切出来保存成图片,保存在save_dir/crops下,在原图像画图或者保存结果
c = int(cls) # integer class # 类别标号
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}') # 类别名
annotator.box_label(xyxy, label, color=colors(c, True)) #绘制边框
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True) # 在原图上画框+将预测到的目标剪切出来保存成图片,保存在save_dir/crops下(单独保存)
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(五)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码是实现在输出窗口实时查看检测结果:
若需要在窗口中实时查看检测结果,则会使用OpenCV库中的函数将图像显示在窗口中,并等待1毫秒以便继续下一帧的检测。
代码会检查是否已经为当前图像创建了窗口(if p not in windows),并在必要时创建窗口,并使用图像名称来命名该窗口。窗口的名称是由变量"p"指定的图像路径名。
若检测到图像尚未在窗口中打开,则代码会创建一个新窗口并将图像显示在窗口中。如果图像已经在窗口中打开,则代码会直接更新窗口中的图像。
# Print time (inference-only)
# 打印耗时
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
# Stream results
# 如果设置展示,则show图片 / 视频
im0 = annotator.result() # im0是绘制好的图片
# 显示图片
if view_img:
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 暂停 1 millisecond
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(六)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码是设置保存图片和视频:
首先“save_img”判断是否是图片,如果是则保存路径和图片;如果是视频或流,需要重新创建视频文件。
# Save results (image with detections)
# 设置保存图片/视频
if save_img: # 如果save_img为true,则保存绘制完的图片
if dataset.mode == 'image': # 如果是图片,则保存
cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' 如果是视频或者"流"
if vid_path[i] != save_path: # new video vid_path[i] != save_path,说明这张图片属于一段新的视频,需要重新创建视频文件
vid_path[i] = save_path
# 以下的部分是保存视频文件
if isinstance(vid_writer[i], cv2.VideoWriter):
vid_writer[i].release() # release previous video writer
if vid_cap: # video
fps = vid_cap.get(cv2.CAP_PROP_FPS) # 视频帧速率 FPS
w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # 获取视频帧宽度
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 获取视频帧高度
else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path += '.mp4'
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer[i].write(im0)
✨✨✨✨✨✨✨✨✨✨✨✨✨✨(七)✨✨✨✨✨✨✨✨✨✨✨✨✨✨
这段代码是指打印时间(仅限推断)。
# 打印耗时
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
这预测阶段简直太多了,我要看哭了🥲🥲🥲🥲🥲让我们接着往下看吧:
4.6.4 ‘Print result’ 输出结果 ⭐
这部分代码用于打印结果,记录了一些总共的耗时,以及信息保存。输出结果包括每张图片的预处理、推理和NMS时间,以及结果保存的路径。如果update为True,则将模型更新,以修复SourceChangeWarning。
'''-------------------4.6.4 ‘Print result’ 输出结果 ⭐--------------------'''
# Print results
t = tuple(x / seen * 1E3 for x in dt) # speeds per image 平均每张图片所耗费时间
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t)
if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' # 标签保存的路径
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update:
strip_optimizer(weights) # update model (to fix SourceChangeWarning)
到目前为止,detect.py代码注释讲解就完成啦!!!!😍😍😍😍
五、detect.py代码注释整合🚀
'''------------------1.1✨导入所需要的相关库✨--------------------'''
import argparse # 解析命令行参数的库
import os # 与操作系统进行交互的文件库 包含文件路径操作与解析
import sys # 包含了与python解释器和它的环境有关的函数。
from pathlib import Path # Path能够更加方便得对字符串路径进行处理
import cv2 # 开源的库平台计算机视觉库。有很强大的图片处理功能,可实现图像处理和计算机视觉方面的很多通用算法。
import torch # pytorch 深度学习库
import torch.backends.cudnn as cudnn # 提供了一个接口,用于使用cuDNN库,在NVIDIA GPU上高效地进行深度学习
'''----------------------1.2✨路径代码✨----------------------'''
FILE = Path(__file__).resolve() # __file__指的是当前文件(即detect.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/detect.py
ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径
'''-------------------- 1.3✨导入自定义辅助库✨-----------------'''
from models.common import DetectMultiBackend #定义了一些通用的函数和类,比如图像的处理、非极大值抑制等等。
from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams #定义了两个类,LoadImages和LoadStreams,它们可以加载图像或视频帧,并对它们进行一些预处理,以便进行物体检测或识别。
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh) #定义了一些常用的工具函数,比如检查文件是否存在、检查图像大小是否符合要求、打印命令行参数等等。
from utils.plots import Annotator, colors, save_one_box #定义了Annotator类,可以在图像上绘制矩形框和标注信息。
from utils.torch_utils import select_device, time_sync #定义了一些与PyTorch有关的工具函数,比如选择设备、同步时间等等。
'''------------------------4.1 传入参数 ✨✨-------------------------'''
@torch.no_grad() # 该标注使得方法中所有计算得出的tensor的requires_grad都自动设置为False,也就是说不进行梯度的计算(当然也就没办法反向传播了), 节约显存和算
def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s) 事先训练完成的权重文件,比如yolov5s.pt,默认 weights/,假如使用官方训练好的文件(比如yolov5s),则会自动下载
source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam 预测时的输入数据,可以是文件/路径/URL/glob, 输入是0的话调用摄像头作为输入,默认data/images/
# data=ROOT / 'data/coco128.yaml', # dataset.yaml path, data文件路径,包括类别/图片/标签等信息
imgsz=(640, 640), # inference size (pixels) 预测时的放缩后图片大小(因为YOLO算法需要预先放缩图片), 两个值分别是height, width。默认640*640
conf_thres=0.25, # confidence threshold 置信度阈值, 高于此值的bounding_box才会被保留。默认0.25,用在nms中
iou_thres=0.45, # NMS IOU threshold IOU阈值,高于此值的bounding_box才会被保留。默认0.45,用在nms中
max_det=1000, # maximum detections per image 一张图片上检测的最大目标数量,用在nms中
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu 所使用的GPU编号,如果使用CPU就写cpu
view_img=False, # show results 是否展示预测之后的图片或视频,默认False
save_txt=False, # save results to *.txt 是否将预测的框坐标以txt文件形式保存, 默认False, 使用--save-txt 在路径runs/detect/exp*/labels/*.txt下生成每张图片预测的txt文件
save_conf=False, # save confidences in --save-txt labels 是否将结果中的置信度保存在txt文件中,默认False
save_crop=False, # save cropped prediction boxes 是否保存裁剪后的预测框,默认为False, 使用--save-crop 在runs/detect/exp*/crop/剪切类别文件夹/ 路径下会保存每个接下来的目标
nosave=False, # do not save images/videos 不保存图片、视频, 要保存图片,不设置--nosave 在runs/detect/exp*/会出现预测的结果
classes=None, # filter by class: --class 0, or --class 0 2 3 过滤指定类的预测结果
agnostic_nms=False, # class-agnostic NMS 进行NMS去除不同类别之间的框, 默认False
augment=False, # augmented inference TTA测试时增强/多尺度预测,可以提分
visualize=False, # visualize features 是否可视化网络层输出特征
update=False, # update all models 如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
project=ROOT / 'runs/detect', # save results to project/name 预测结果保存的路径
name='exp', # save results to project/name 结果保存文件夹的命名前缀
exist_ok=False, # existing project/name ok, do not increment True: 推理结果覆盖之前的结果 False: 推理结果新建文件夹保存,文件夹名递增
line_thickness=3, # bounding box thickness (pixels) 绘制Bounding_box的线宽度
hide_labels=False, # hide labels 若为True: 隐藏标签
hide_conf=False, # hide confidences 若为True: 隐藏置信度
half=False, # use FP16 half-precision inference 是否使用半精度推理(节约显存)
dnn=False, # use OpenCV DNN for ONNX inference 是否使用OpenCV DNN预测
):
'''------------------------4.2 初始化配置 ✨✨---------------------'''
source = str(source) # 输入的路径变为字符串
save_img = not nosave and not source.endswith('.txt') # 是否保存图片和txt文件,如果nosave(传入的参数)为false且source的结尾不是txt则保存图片
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
# 判断source是不是视频/图像文件路径
# Path()提取文件名。suffix:最后一个组件的文件扩展名。若source是"D://YOLOv5/data/1.jpg", 则Path(source).suffix是".jpg", Path(source).suffix[1:]是"jpg"
# 而IMG_FORMATS 和 VID_FORMATS两个变量保存的是所有的视频和图片的格式后缀。
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
# 判断source是否是链接
# .lower()转化成小写 .upper()转化成大写 .title()首字符转化成大写,其余为小写, .startswith('http://')返回True or Flase
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)
# 判断是source是否是摄像头
# .isnumeric()是否是由数字组成,返回True or False
if is_url and is_file:
source = check_file(source) # 返回文件。如果source是一个指向图片/视频的链接,则下载输入数据
'''----------------------------4.3 保存预测结果 ✨✨------------------------'''
save_dir = increment_path(Path(project) / name,
exist_ok=exist_ok) # save_dir是保存运行结果的文件夹名,是通过递增的方式来命名的。第一次运行时路径是“runs\detect\exp”,第二次运行时路径是“runs\detect\exp1”
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # 根据前面生成的路径创建文件夹
'''-------------------------4.4 加载模型权重 ✨✨-----------------------'''
device = select_device(device) # 获取设备 CPU/CUDA
model = DetectMultiBackend(weights, device=device,
dnn=dnn) # DetectMultiBackend定义在models.common模块中,是我们要加载的网络,其中weights参数就是输入时指定的权重文件(比如yolov5s.pt)
stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx
# stride:推理时所用到的步长,默认为32, 大步长适合于大目标,小步长适合于小目标
# names:保存推理结果名的列表,比如默认模型的值是['person', 'bicycle', 'car', ...]
# pt: 加载的是否是pytorch模型(也就是pt格式的文件)
# jit:当某段代码即将第一次被执行时进行编译,因而叫“即时编译”
# onnx:利用Pytorch我们可以将model.pt转化为model.onnx格式的权重,在这里onnx充当一个后缀名称,
# model.onnx就代表ONNX格式的权重文件,这个权重文件不仅包含了权重值,也包含了神经网络的网络流动信息以及每一层网络的输入输出信息和一些其他的辅助信息。
imgsz = check_img_size(imgsz, s=stride) # 确保输入图片的尺寸imgsz能整除stride=32 如果不能则调整为能被整除并返回
half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
if pt:
model.model.half() if half else model.model.float() # 如果不是CPU,使用半进度(图片半精度/模型半精度)
'''---------------------------4.5 加载待预测的数据 ✨✨-----------------------'''
# Dataloader 通过不同的输入源来设置不同的数据加载方式
if webcam: # 使用摄像头作为输入
view_img = check_imshow() # 检测cv2.imshow()方法是否可以执行,不能执行则抛出异常
cudnn.benchmark = True # 该设置可以加速预测
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit) # 加载输入数据流
'''
source:输入数据源;image_size 图片识别前被放缩的大小;stride:识别时的步长,
auto的作用可以看utils.augmentations.letterbox方法,它决定了是否需要将图片填充为正方形,如果auto=True则不需要
'''
bs = len(dataset) # batch_size 批大小
else: # 直接从source文件下读取图片
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)
bs = 1
# 保存视频的路径
vid_path, vid_writer = [None] * bs, [None] * bs # 前者是视频路径,后者是一个cv2.VideoWriter对
'''--------------------4.6.1 ‘warmup’ 模型预热⭐-------------------------'''
# Run inference
model.warmup(imgsz=(1 if pt else bs, 3, *imgsz), half=half) # warmup
dt, seen = [0.0, 0.0, 0.0], 0 # dt: 存储每一步骤的耗时 ,seen: 计数功能,已经处理完了多少帧图片
for path, im, im0s, vid_cap, s in dataset:
'''
path:文件路径(即source)
im: resize后的图片(经过了放缩操作)
im0s: 原始图片
vid_cap=none
s: 图片的基本信息,比如路径,大小
'''
'''以下部分整理图片信息'''
t1 = time_sync() # 获取当前时间
im = torch.from_numpy(im).to(device) # 将图片放到指定设备(如GPU)上识别。#torch.size=[3,640,480]
im = im.half() if half else im.float() # uint8 to fp16/32 # 把输入从整型转化为半精度/全精度浮点数。
im /= 255 # 0 - 255 to 0.0 - 1.0 归一化,所有像素点除以255
if len(im.shape) == 3:
im = im[None] # expand for batch dim 添加一个第0维。缺少batch这个尺寸,所以将它扩充一下,变成[1,3,640,480]
t2 = time_sync() # 获取当前时间
dt[0] += t2 - t1 # 记录该阶段耗时
'''-----------------4.6.2 'Inference'与'NMS' 推理以及非极大值抑制 ⭐--------------------'''
# Inference
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize # 可视化文件路径。如果为True则保留推理过程中的特征图,保存在runs文件夹中else False
# 推理结果,pred保存的是所有的bound_box的信息,
pred = model(im, augment=augment, visualize=visualize) #模型预测出来的所有检测框,torch.size=[1,18900,85]
t3 = time_sync()
dt[1] += t3 - t2
# NMS
# 执行非极大值抑制,返回值为过滤后的预测框
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
'''
pred: 网络的输出结果
conf_thres: 置信度阈值
iou_thres: iou阈值
classes: 是否只保留特定的类别 默认为None
agnostic_nms: 进行nms是否也去除不同类别之间的框
max_det: 检测框结果的最大数量 默认1000
'''
# 预测+NMS的时间
dt[2] += time_sync() - t3
# Process predictions
# 把所有的检测框画到原图中
for i, det in enumerate(pred): # per image 每次迭代处理一张图片
'''
i:每个batch的信息
det:表示5个检测框的信息
'''
seen += 1 #seen是一个计数的功能
if webcam: # 如果输入源是webcam则batch_size>=1 取出dataset中的一张图片
p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' # s后面拼接一个字符串i
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
'''
大部分我们一般都是从LoadImages流读取本都文件中的照片或者视频 所以batch_size=1
p: 当前图片/视频的绝对路径 如 F:\yolo_v5\yolov5-U\data\images\bus.jpg
s: 输出信息 初始为 ''
im0: 原始图片 letterbox + pad 之前的图片
frame: 视频流,此次取的是第几张图片
'''
p = Path(p) #首先将图像路径转换为"Path"对象。
save_path = str(save_dir / p.name) # 图片/视频的保存路径save_path
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # 设置保存框坐标的txt文件路径,每张图片对应一个框坐标信息
s += '%gx%g ' % im.shape[2:] # 设置输出图片信息。图片shape (w, h)
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # 得到原图的宽和高
imc = im0.copy() if save_crop else im0 # for save_crop # 保存截图。如果save_crop的值为true,则将检测到的bounding_box单独保存成一张图片。
annotator = Annotator(im0, line_width=line_thickness, example=str(names)) # 得到一个绘图的类,类中预先存储了原图、线条宽度、类名。
if len(det):# 判断有没有框
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round() #scale_coords:坐标映射功能
#将预测信息映射
# 打印检测到的类别数量
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum() # 检测每个类别
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # 加入到字符串中
# Write results
for *xyxy, conf, cls in reversed(det): # 保存预测结果:txt/图片画框/crop-image
if save_txt: # 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh # 将xyxy(左上角+右下角)格式转为xywh(中心点+宽长)格式,并归一化,转化为列表再保存
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # line的形式是: ”类别 x y w h“,若save_conf为true,则line的形式是:”类别 x y w h 置信度“
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
if save_img or save_crop or view_img: # 在原图上画框+将预测到的目标剪切出来保存成图片,保存在save_dir/crops下,在原图像画图或者保存结果
c = int(cls) # integer class # 类别标号
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}') # 类别名
annotator.box_label(xyxy, label, color=colors(c, True)) #绘制边框
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True) # 在原图上画框+将预测到的目标剪切出来保存成图片,保存在save_dir/crops下(单独保存)
# Print time (inference-only)
# 打印耗时
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
# Stream results
# 如果设置展示,则show图片 / 视频
im0 = annotator.result() # im0是绘制好的图片
# 显示图片
if view_img:
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 暂停 1 millisecond
# Save results (image with detections)
# 设置保存图片/视频
if save_img: # 如果save_img为true,则保存绘制完的图片
if dataset.mode == 'image': # 如果是图片,则保存
cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' 如果是视频或者"流"
if vid_path[i] != save_path: # new video vid_path[i] != save_path,说明这张图片属于一段新的视频,需要重新创建视频文件
vid_path[i] = save_path
# 以下的部分是保存视频文件
if isinstance(vid_writer[i], cv2.VideoWriter):
vid_writer[i].release() # release previous video writer
if vid_cap: # video
fps = vid_cap.get(cv2.CAP_PROP_FPS) # 视频帧速率 FPS
w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # 获取视频帧宽度
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 获取视频帧高度
else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path += '.mp4'
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer[i].write(im0)
# 打印耗时
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
'''-------------------4.6.4 ‘Print result’ 输出结果 ⭐--------------------'''
# Print results
t = tuple(x / seen * 1E3 for x in dt) # speeds per image 平均每张图片所耗费时间
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t)
if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' # 标签保存的路径
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update:
strip_optimizer(weights) # update model (to fix SourceChangeWarning)
def parse_opt():
parser = argparse.ArgumentParser()
"""
weights: 训练的权重路径,可以使用自己训练的权重,也可以使用官网提供的权重
默认官网的权重yolov5s.pt(yolov5n.pt/yolov5s.pt/yolov5m.pt/yolov5l.pt/yolov5x.pt/区别在于网络的宽度和深度以此增加)
source: 测试数据,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以是rtsp等视频流, 默认data/images
data: 配置数据文件路径, 包括image/label/classes等信息, 训练自己的文件, 需要作相应更改, 可以不用管
如果设置了只显示个别类别即使用了--classes = 0 或二者1, 2, 3等, 则需要设置该文件,数字和类别相对应才能只检测某一个类
imgsz: 网络输入图片大小, 默认的大小是640
conf-thres: 置信度阈值, 默认为0.25
iou-thres: 做nms的iou阈值, 默认为0.45
max-det: 保留的最大检测框数量, 每张图片中检测目标的个数最多为1000类
device: 设置设备CPU/CUDA, 可以不用设置
view-img: 是否展示预测之后的图片/视频, 默认False, --view-img 电脑界面出现图片或者视频检测结果
save-txt: 是否将预测的框坐标以txt文件形式保存, 默认False, 使用--save-txt 在路径runs/detect/exp*/labels/*.txt下生成每张图片预测的txt文件
save-conf: 是否将置信度conf也保存到txt中, 默认False
save-crop: 是否保存裁剪预测框图片, 默认为False, 使用--save-crop 在runs/detect/exp*/crop/剪切类别文件夹/ 路径下会保存每个接下来的目标
nosave: 不保存图片、视频, 要保存图片,不设置--nosave 在runs/detect/exp*/会出现预测的结果
classes: 设置只保留某一部分类别, 形如0或者0 2 3, 使用--classes = n, 则在路径runs/detect/exp*/下保存的图片为n所对应的类别, 此时需要设置data
agnostic-nms: 进行NMS去除不同类别之间的框, 默认False
augment: TTA测试时增强/多尺度预测
visualize: 是否可视化网络层输出特征
update: 如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
project:保存测试日志的文件夹路径
name:保存测试日志文件夹的名字, 所以最终是保存在project/name中
exist_ok: 是否重新创建日志文件, False时重新创建文件
line-thickness: 画框的线条粗细
hide-labels: 可视化时隐藏预测类别
hide-conf: 可视化时隐藏置信度
half: 是否使用F16精度推理, 半进度提高检测速度
dnn: 用OpenCV DNN预测
"""
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)')
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--view-img', action='store_true', help='show results')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--visualize', action='store_true', help='visualize features')
parser.add_argument('--update', action='store_true', help='update all models')
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
parser.add_argument('--name', default='exp', help='save results to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(FILE.stem, opt)
return opt
'''----------------------二、main函数🚀-------------------------'''
def main(opt):
# 检查环境/打印参数,主要是requrement.txt的包是否安装,用彩色显示设置的参数
check_requirements(exclude=('tensorboard', 'thop'))
# 执行run()函数
run(**vars(opt))
# 命令使用
# python detect.py --weights runs/train/exp_yolov5s/weights/best.pt --source data/images/fishman.jpg # webcam
if __name__ == "__main__":
opt = parse_opt() # 解析参数
main(opt) # 执行主函数
这篇YOLOv5的detect.py学习和总结到这里就结束啦,如果有什么问题可以在评论区留言呀~
其他学习YOLOv5直通车🚀:
YOLOv5【网络结构】超详细解读总结!!!建议收藏✨✨!
YOLOv5【目录结构源码】超详细注释解读!!!建议收藏✨✨! 使用云GPU进行yolov5的训练--傻瓜式保姆级教程!!!建议收藏✨✨!_
本地Pycharm连接远程服务器训练模型教程-yolov5为例-傻瓜式保姆级教程!!建议收藏✨✨!
如果帮助到大家,可以一键三连支持下~