学习资源站

10-YOLOv5源码逐行超详细注释与解读——验证部分

YOLOv5源码逐行超详细注释与解读(4)——验证部分val(test).py

  • detect.py: 推理部分。获取实际中最佳推理结果
  • train.py: 训练部分。读取数据集,加载模型并训练
  • val.py:验证部分。获取当前数据集上的最佳验证结果

 🚀一、导包与基本配置

1.1 导入安装好的python库

  1. '''============1.导入安装好的python库=========='''
  2. import argparse # 解析命令行参数的库
  3. import json # 实现字典列表和JSON字符串之间的相互解析
  4. import os # 与操作系统进行交互的文件库 包含文件路径操作与解析
  5. import sys # sys系统模块 包含了与Python解释器和它的环境有关的函数
  6. from pathlib import Path # Path将str转换为Path对象 使字符串路径易于操作的模块
  7. from threading import Thread # python中处理多线程的库
  8. import numpy as np # 矩阵计算基础库
  9. import torch # pytorch 深度学习库
  10. from tqdm import tqdm # 用于直观显示进度条的一个库

首先,导入一下常用的python库

  • argparse:  它是一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息
  • json:  实现字典列表和JSON字符串之间的相互解析
  • os: 它提供了多种操作系统的接口。通过os模块提供的操作系统接口,我们可以对操作系统里文件、终端、进程等进行操作
  • sys: 它是与python解释器交互的一个接口,该模块提供对解释器使用或维护的一些变量的访问和获取,它提供了许多函数和变量来处理 Python 运行时环境的不同部分
  • pathlib:  这个库提供了一种面向对象的方式来与文件系统交互,可以让代码更简洁、更易读
  • threading:  python中处理多线程的库

然后再导入一些 pytorch库

  • numpy:  科学计算库,提供了矩阵,线性代数,傅立叶变换等等的解决方案, 最常用的是它的N维数组对象
  • torch:   这是主要的Pytorch库。它提供了构建、训练和评估神经网络的工具
  • tqdm:  就是我们看到的训练时进度条显示

1.2 获取当前文件的绝对路径

  1. '''===================2.获取当前文件的绝对路径========================'''
  2. FILE = Path(__file__).resolve()# __file__指的是当前文件(即val.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/val.py
  3. ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
  4. if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
  5. sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
  6. ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径

这段代码会获取当前文件的绝对路径,并使用Path库将其转换为Path对象。

这一部分的主要作用有两个:

  • 将当前项目添加到系统路径上,以使得项目中的模块可以调用。
  • 将当前项目的相对路径保存在ROOT中,便于寻找项目中的文件。 

1.3 加载自定义模块

  1. '''===================3..加载自定义模块============================'''
  2. from models.common import DetectMultiBackend # yolov5的网络结构(yolov5)
  3. from utils.callbacks import Callbacks # 和日志相关的回调函数
  4. from utils.datasets import create_dataloader # 加载数据集的函数
  5. from utils.general import (LOGGER, NCOLS, box_iou, check_dataset, check_img_size, check_requirements, check_yaml,
  6. coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
  7. scale_coords, xywh2xyxy, xyxy2xywh) # 定义了一些常用的工具函数
  8. from utils.metrics import ConfusionMatrix, ap_per_class # 在YOLOv5中,fitness函数实现对 [P, R, mAP@.5, mAP@.5-.95] 指标进行加权
  9. from utils.plots import output_to_target, plot_images, plot_val_study # 定义了Annotator类,可以在图像上绘制矩形框和标注信息
  10. from utils.torch_utils import select_device, time_sync # 定义了一些与PyTorch有关的工具函数

这些都是用户自定义的库,由于上一步已经把路径加载上了,所以现在可以导入,这个顺序不可以调换。具体来说,代码从如下几个文件中导入了部分函数和类:

  • models.common:  yolov5的网络结构(yolov5)
  • utils.callbacks:  定义了回调函数,主要为logger服务
  • utils.datasets:  dateset和dateloader定义代码
  • utils.general.py:   定义了一些常用的工具函数,比如检查文件是否存在、检查图像大小是否符合要求、打印命令行参数等等
  • utils.metrics:   模型验证指标,包括ap,混淆矩阵等
  • utils.plots.py:    定义了Annotator类,可以在图像上绘制矩形框和标注信息
  • utils.torch_utils.py:   定义了一些与PyTorch有关的工具函数,比如选择设备、同步时间等 通过导入这些模块,可以更方便地进行目标检测的相关任务,并且减少了代码的复杂度和冗余

🚀二、保存信息

2.1 保存预测信息到txt文件

  1. '''======================1.保存预测信息到txt文件====================='''
  2. def save_one_txt(predn, save_conf, shape, file):
  3. # Save one txt result
  4. # gn = [w, h, w, h] 对应图片的宽高 用于后面归一化
  5. gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
  6. # 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
  7. for *xyxy, conf, cls in predn.tolist():
  8. # 将xyxy(左上角+右下角)格式转为xywh(中心点+宽高)格式,并归一化,转化为列表再保存
  9. xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
  10. # line的形式是: "类别 xywh",若save_conf为true,则line的形式是:"类别 xywh 置信度"
  11. line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
  12. # 将上述test得到的信息输出保存 输出为xywh格式 coco数据格式也为xywh格式
  13. with open(file, 'a') as f:
  14. # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
  15. f.write(('%g ' * len(line)).rstrip() % line + '\n')

这段代码主要是保存预测信息为txt文件

保存的信息为:

  • cls:  图片类别
  • xywh:  图片的中心点+宽高
  • conf:  置信度

首先获取图片的wh,也就是对应的宽高,然后把每个图片的预测信息分别存入save_dir/labels下的xxx.txt中。

接着将bbox的左上角点、右下角点坐标的格式,转换为bbox中心点 + bbox的w,h的格式,并进行归一化。即:xyxy(左上右下) ——> xywh(中心宽高)。

最后,将预测的类别和坐标值保存到对应图片image_name.txt文件中,路径默认为“runs\detect\exp*\labels”


2.2 保存预测信息到coco格式的json字典

  1. '''======================2.保存预测信息到coco格式的json字典====================='''
  2. def save_one_json(predn, jdict, path, class_map):
  3. # 储存格式 {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
  4. # 获取图片id
  5. image_id = int(path.stem) if path.stem.isnumeric() else path.stem
  6. # 获取预测框 并将xyxy转为xywh格式
  7. box = xyxy2xywh(predn[:, :4]) # xywh
  8. box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
  9. # 序列解包
  10. for p, b in zip(predn.tolist(), box.tolist()):
  11. jdict.append({'image_id': image_id, # 图片id 即属于哪张图片
  12. 'category_id': class_map[int(p[5])], # 类别 coco91class()从索引0~79映射到索引0~90
  13. 'bbox': [round(x, 3) for x in b], # 预测框坐标
  14. 'score': round(p[4], 5)}) # 预测得分

这段代码主要是保存coco格式的json文件字典

保存的信息为:

  • image_id:  图片id,即属于哪张图片
  • category_id:   类别,coco91class()从索引0~79映射到索引0~90
  • bbox:   预测框坐标
  • score:  预测得分

首先获取图片的id以及预测框,并把xyxy格式转化为xywh格式。

注意:之前的的xyxy格式是左上角右下角坐标 ,xywh是中心的坐标和宽高,而coco的json格式的框坐标是xywh(左上角坐标 + 宽高),所以 box[:, :2] -= box[:, 2:] / 2 这行代码是将中心点坐标 -> 左上角坐标。

然后再用zip()函数进行序列解包,逐一保存上述信息。 


🚀三、计算指标 

  1. '''========================三、计算指标==========================='''
  2. def process_batch(detections, labels, iouv):
  3. """
  4. Return correct predictions matrix.
  5. 返回每个预测框在10个IoU阈值上是TP还是FP
  6. Both sets of boxes are in (x1, y1, x2, y2) format.
  7. Arguments:
  8. detections (Array[N, 6]), x1, y1, x2, y2, conf, class
  9. labels (Array[M, 5]), class, x1, y1, x2, y2
  10. Returns:
  11. correct (Array[N, 10]), for 10 IoU levels
  12. """
  13. # 构建一个[pred_nums, 10]全为False的矩阵
  14. correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device)
  15. # 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]
  16. iou = box_iou(labels[:, 1:], detections[:, :4])
  17. # iou超过阈值而且类别正确,则为True,返回索引
  18. x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5])) # IoU above threshold and classes match
  19. # 如果存在符合条件的预测框
  20. if x[0].shape[0]: # 至少有一个TP
  21. # 将符合条件的位置构建成一个新的矩阵,第一列是行索引(表示gt索引),第二列是列索引(表示预测框索引),第三列是iou值
  22. matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detection, iou]
  23. if x[0].shape[0] > 1:
  24. # argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
  25. matches = matches[matches[:, 2].argsort()[::-1]]
  26. matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
  27. '''
  28. 参数return_index=True:表示会返回唯一值的索引,[0]返回的是唯一值,[1]返回的是索引
  29. matches[:, 1]:这里的是获取iou矩阵每个预测框的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
  30. 这个操作的含义:每个预测框最多只能出现一次,如果有一个预测框同时和多个gt匹配,只取其最大iou的一个
  31. '''
  32. # matches = matches[matches[:, 2].argsort()[::-1]]
  33. matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
  34. '''
  35. matches[:, 0]:这里的是获取iou矩阵gt的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
  36. 这个操作的含义: 每个gt也最多只能出现一次,如果一个gt同时匹配多个预测框,只取其匹配最大的那一个预测框
  37. '''
  38. # 以上操作实现了为每一个gt分配一个iou最高的类别的预测框,实现一一对应
  39. matches = torch.Tensor(matches).to(iouv.device)
  40. correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv
  41. '''
  42. 当前获得了gt与预测框的一一对应,其对于的iou可以作为评价指标,构建一个评价矩阵
  43. 需要注意,这里的matches[:, 1]表示的是为对应的预测框来赋予其iou所能达到的程度,也就是iouv的评价指标
  44. '''
  45. # 在correct中,只有与gt匹配的预测框才有对应的iou评价指标,其他大多数没有匹配的预测框都是全部为False
  46. return correct

这段代码主要是计算correct,来获取匹配预测框的iou信息。

这个函数主要有两个作用:

  • 作用1:对预测框与gt进行匹配 
  • 作用2:对匹配上的预测框进行iou数值判断,用True来填充,其余没有匹配上的预测框的所以行数全部设置为False

对于每张图像的预测框,需要筛选出能与gt匹配的框来进行相关的iou计算,设置了iou从0.5-0.95的10个梯度,如果匹配的预测框iou大于相对于的阈值,则在对应位置设置为True,否则设置为False;而对于没有匹配上的预测框全部设置为False。

Q:为什么要筛选?

这是因为一个gt只可能是一个类别,不可能是多个类别,所以需要取置信度最高的类别进行匹配。但是此时还可能多个gt和一个预测框匹配,同样的,为这个预测框分配iou值最高的gt,依次来实现一一配对。


🚀四、执行run()函数

4.1 设置参数

  1. '''======================1.设置参数====================='''
  2. @torch.no_grad()
  3. def run(data, # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息 train.py时传入data_dict
  4. weights=None, # 模型的权重文件地址 运行train.py=None 运行test.py=默认weights/yolov5s
  5. batch_size=32, # 前向传播的批次大小 运行test.py传入默认32 运行train.py则传入batch_size // WORLD_SIZE * 2
  6. imgsz=640, # 输入网络的图片分辨率 运行test.py传入默认640 运行train.py则传入imgsz_test
  7. conf_thres=0.001, # object置信度阈值 默认0.001
  8. iou_thres=0.6, # 进行NMS时IOU的阈值 默认0.6
  9. task='val', # 设置测试的类型 有train, val, test, speed or study几种 默认val
  10. device='', # 执行 val.py 所在的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu
  11. single_cls=False, # 数据集是否只有一个类别 默认False
  12. augment=False, # 测试时增强
  13. verbose=False, # 是否打印出每个类别的mAP 运行test.py传入默认Fasle 运行train.py则传入nc < 50 and final_epoch
  14. save_txt=False, # 是否以txt文件的形式保存模型预测框的坐标 默认True
  15. save_hybrid=False, # 是否保存预测每个目标的置信度到预测txt文件中 默认True
  16. save_conf=False, # 保存置信度
  17. save_json=False, # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),
  18. #运行test.py传入默认Fasle 运行train.py则传入is_coco and final_epoch(一般也是False)
  19. project=ROOT / 'runs/val', # 验证结果保存的根目录 默认是 runs/val
  20. name='exp', # 验证结果保存的目录 默认是exp 最终: runs/val/exp
  21. exist_ok=False, # 如果文件存在就increment name,不存在就新建 默认False(默认文件都是不存在的)
  22. half=True, # 使用 FP16 的半精度推理
  23. dnn=False, # 在 ONNX 推理时使用 OpenCV DNN 后段端
  24. model=None, # 如果执行val.py就为None 如果执行train.py就会传入( model=attempt_load(f, device).half() )
  25. dataloader=None, # 数据加载器 如果执行val.py就为None 如果执行train.py就会传入testloader
  26. save_dir=Path(''), # 文件保存路径 如果执行val.py就为‘’ , 如果执行train.py就会传入save_dir(runs/train/expn)
  27. plots=True, # 是否可视化 运行val.py传入,默认True
  28. callbacks=Callbacks(), # 回调函数
  29. compute_loss=None, # 损失函数 运行val.py传入默认None 运行train.py则传入compute_loss(train)
  30. ):

这段代码定义了run()函数,并设置了一系列参数,用于指定物体检测或识别的相关参数。

这些参数包括:

  • data:  数据集文件的路径,默认为COCO128数据集的配置文件路径
  • weights:  模型权重文件的路径,默认为YOLOv5s的权重文件路径
  • batch_size:   前向传播的批次大小,运行val.py传入默认32 。运行train.py则传入batch_size // WORLD_SIZE * 2
  • imgsz:  输入图像的大小,默认为640x640
  • conf_thres:  置信度阈值,默认为0.001
  • iou_thres:  非极大值抑制的iou阈值,默认为0.6
  • task:   设置测试的类型 有train, val, test, speed or study几种,默认val
  • device:  使用的设备类型,默认为空,表示自动选择最合适的设备
  • single_cls:   数据集是否只用一个类别,运行val.py传入默认False 运行train.py则传入single_cls
  • augment:  是否使用数据增强的方式进行检测,默认为False
  • verbose:   是否打印出每个类别的mAP,运行val.py传入默认Fasle。运行train.py则传入nc < 50 and final_epoch
  • save_txt:  是否将检测结果保存为文本文件,默认为False
  • save_hybrid:   是否保存 label+prediction hybrid results to *.txt 默认False
  • save_conf:  是否在保存的文本文件中包含置信度信息,默认为False
  • save_json:  是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签)运行test.py传入默认Fasle。运行train.py则传入is_coco and final_epoch(一般也是False)
  • project:  结果保存的项目文件夹路径,默认为“runs/val”
  • name:  结果保存的文件名,默认为“exp”
  • exist_ok:  如果结果保存的文件夹已存在,是否覆盖,默认为False,即不覆盖
  • half:  是否使用FP16的半精度推理模式,默认为False
  • dnn:  是否使用OpenCV DNN作为ONNX推理的后端,默认为False
  • model:  模型, 如果执行val.py就为None 如果执行train.py就会传入ema.ema(ema模型)
  • dataloader:  数据加载器, 如果执行val.py就为None 如果执行train.py就会传入testloader
  • save_dir:  文件保存路径, 如果执行val.py就为‘ ’ ,如果执行train.py就会传入save_dir(runs/train/expn)
  • plots:  是否可视化,运行val.py传入默认True,运行train.py则传入plots and final_epoch
  • callback:   回调函数
  • compute_loss:  损失函数,运行val.py传入默认None,运行train.py则传入compute_loss(train) 

4.2 初始化/加载模型以及设置设备

  1. '''======================2.初始化/加载模型以及设置设备====================='''
  2. # Initialize/load model and set device
  3. training = model is not None
  4. if training: # 通过 train.py 调用的run函数
  5. # 获得记录在模型中的设备 next为迭代器
  6. device, pt = next(model.parameters()).device, True
  7. # 精度减半
  8. # 如果设备类型不是cpu 则将模型由32位浮点数转换为16位浮点数
  9. half &= device.type != 'cpu' # half precision only supported on CUDA
  10. model.half() if half else model.float()
  11. else: # 直接通过 val.py 调用 run 函数
  12. # 调用torch_utils中select_device来选择执行程序时的设备
  13. device = select_device(device, batch_size=batch_size)
  14. # 路径
  15. # 调用genera.py中的increment_path函数来生成save_dir文件路径 run\test\expn
  16. save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
  17. # mkdir创建路径最后一级目录
  18. (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
  19. model = DetectMultiBackend(weights, device=device, dnn=dnn)
  20. stride, pt = model.stride, model.pt
  21. # 调用general.py中的check_img_size函数来检查图像分辨率能否被32整除
  22. imgsz = check_img_size(imgsz, s=stride) # check image size
  23. # 如果不是CPU,使用半进度(图片半精度/模型半精度)
  24. half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
  25. if pt:
  26. model.model.half() if half else model.model.float()
  27. else:
  28. half = False
  29. batch_size = 1 # export.py models default to batch-size 1
  30. device = torch.device('cpu')
  31. # 打印耗时
  32. LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')
  33. # Data
  34. # 调用general.py中的check_dataset函数来检查数据文件是否正常
  35. data = check_dataset(data) # check

这段代码主要是初始化并加载模型,并设置设备

​​​​首先判断模型是否存在,若不存在则训练为假。

接着判断是否是训练时调用run函数——执行train.py 如果是就使用训练时的设备(一般都是train),如果不是trin.py调用run函数——执行val.py,就调用select_device选择可用的设备,并生成save_dir + make dir + 加载模型model + check imgsz + 加载data配置信息

  • 训练时(train.py)调用:初始化模型参数、训练设备
  • 验证时(val.py)调用:初始化设备、save_dir文件路径、make dir、加载模型、check imgsz、 加载+check data配置信息

最后判断设备类型并仅仅单GPU支持一半的精度Half model 只能在单GPU设备上才能使用, 一旦使用half,不但模型需要设为half,输入模型的图片也需要设为half。如果设备类型不是CPU 则将模型由32位浮点数转换为16位浮点数。


4.3 加载配置

  1. '''======================3.加载配置====================='''
  2. # Configure
  3. # 将模型转换为测试模式 固定住dropout层和Batch Normalization层
  4. model.eval()
  5. # 通过 COCO 数据集的文件夹组织结构判断当前数据集是否为 COCO 数据集
  6. is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # COCO dataset
  7. # 确定检测的类别数目
  8. nc = 1 if single_cls else int(data['nc']) # number of classes
  9. # 计算mAP相关参数
  10. iouv = torch.linspace(0.5, 0.95, 10).to(device) # mAP@0.5:0.95 的iou向量
  11. # numel为pytorch预置函数 用来获取张量中的元素个数
  12. niou = iouv.numel()

这段代码主要是加载数据集的yaml配置文件

首先,通过model.eval()  启动模型验证模式,is_coco判断是否是coco数据集。

然后,确定检测的类别个数nc ,以及计算mAP相关参数,设置iou阈值从0.5-0.95取10个(0.05间隔)   所以iouv: [0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]


4.4 加载val数据集

  1. '''======================4.加载val数据集====================='''
  2. # Dataloader
  3. if not training:
  4. if pt and device.type != 'cpu':
  5. # 创建一张全为0的图片(四维张量)
  6. model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # warmup
  7. pad = 0.0 if task == 'speed' else 0.5
  8. task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
  9. # 调用datasets.py文件中的create_dataloader函数创建dataloader
  10. dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
  11. prefix=colorstr(f'{task}: '))[0]

这段代码主要是加载val数据集

判断是否是训练。如果不是训练——执行val.py调用run函数,就调用create_dataloader生成dataloader 如果是训练——执行train.py调用run函数,就不需要生成dataloader 可以直接从参数中传过来testloader。 

  • 训练时(train.py)调用:加载val数据集
  • 验证时(val.py)调用:不需要加载val数据集 直接从train.py 中传testloader

 4.5 初始化

  1. '''======================5.初始化====================='''
  2. # 初始化已完成测试的图片数量
  3. seen = 0
  4. # 调用matrics中函数 存储混淆矩阵
  5. confusion_matrix = ConfusionMatrix(nc=nc)
  6. # 获取数据集所有类别的类名
  7. names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
  8. # 调用general.py中的函数 获取coco数据集的类别索引
  9. class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
  10. # 设置tqdm进度条的显示信息
  11. s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
  12. # 初始化detection中各个指标的值
  13. dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
  14. # 初始化网络训练的loss
  15. loss = torch.zeros(3, device=device)
  16. # 初始化json文件涉及到的字典、统计信息、AP、每一个类别的AP、图片汇总
  17. jdict, stats, ap, ap_class = [], [], [], []
  18. pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar

这段代码主要是获取dataloader、初始化模型测试当中用到的相应参数

(1)初始化已完成测试图片数量,设置seen=0

(2)初始化混淆矩阵

(3)获取数据集类名coco数据集的类别索引 

(4)设置tqdm进度条的显示信息

(5)初始化p, r, f1, mp, mr, map50, map指标和初始化测试集的损失以及初始化json文件中的字典 统计信息、ap等


4.6 验证过程

4.6.1 开始验证前的预处理

  1. '''===6.1 开始验证前的预处理==='''
  2. for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
  3. # 获取当前时间
  4. t1 = time_sync()
  5. if pt:
  6. # 将图片数据拷贝到device(GPU)上面
  7. im = im.to(device, non_blocking=True)
  8. #对targets也做同样拷贝的操作
  9. targets = targets.to(device)
  10. # 将图片从64位精度转换为32位精度
  11. im = im.half() if half else im.float() # uint8 to fp16/32
  12. # 将图像像素值0-255的范围归一化到0-1的范围
  13. im /= 255 # 0 - 255 to 0.0 - 1.0
  14. # 四个变量分别代表batchsize、通道数目、图像高度、图像宽度
  15. nb, _, height, width = im.shape # batch size, channels, height, width
  16. # 获取当前时间
  17. t2 = time_sync()
  18. # dt[0]: 累计处理数据时间
  19. dt[0] += t2 - t1

这段代码主要是预处理图片和target

获取dataloader当中的batch、图片、目标、路径、形状信息等。


4.6.2 前项推理

  1. '''===6.2 前向推理==='''
  2. # Inference
  3. out, train_out = model(im) if training else model(im, augment=augment, val=True) # inference, loss outputs
  4. # 累计前向推理时间 dt[1]
  5. dt[1] += time_sync() - t2

这段代码主要是模型前项推理以及累计前项推理时间

  • out:   推理结果。1个 ,[bs, anchor_num*grid_w*grid_h, xywh+c+20classes] = [1, 19200+4800+1200, 25]
  • train_out:   训练结果。3个, [bs, anchor_num, grid_w, grid_h, xywh+c+20classes]如: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25] 

4.6.3 计算损失

  1. '''===6.3 计算损失==='''
  2. # Loss
  3. # compute_loss不为空 说明正在执行train.py 根据传入的compute_loss计算损失值
  4. if compute_loss:
  5. # loss 包含bounding box 回归的GIoU、object和class 三者的损失
  6. loss += compute_loss([x.float() for x in train_out], targets)[1] # box, obj, cls

这段代码主要是计算验证集损失

判断compute_loss是否为空,不为空则说明正在执行train.py ,根据传入的compute_loss计算损失值。

loss 包含bounding box 回归的GIoU、object和class 三者的损失

  • 分类损失(cls_loss):该损失用于判断模型是否能够准确地识别出图像中的对象,并将其分类到正确的类别中。
  • 置信度损失(obj_loss):该损失用于衡量模型预测的框(即包含对象的矩形)与真实框之间的差异。
  • 边界框损失(box_loss):该损失用于衡量模型预测的边界框与真实边界框之间的差异,这有助于确保模型能够准确地定位对象。

 4.6.4 NMS获得预测框

  1. '''===6.4 NMS获得预测框==='''
  2. # NMS
  3. # targets: [num_target, img_index+class_index+xywh] = [31, 6]
  4. targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
  5. # 提取bach中每一张图片的目标的label
  6. # lb: {list: bs} 第一张图片的target[17, 5] 第二张[1, 5] 第三张[7, 5] 第四张[6, 5]
  7. lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
  8. # 计算NMS过程所需要的时间
  9. t3 = time_sync()
  10. # 调用general.py中的函数 进行非极大值抑制操作
  11. out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
  12. # 累计NMS时间
  13. dt[2] += time_sync() - t3

这段代码主要是运行NMS 目标检测的后处理模块,用于删除冗余的bounding box
首先将真实框targetxywh (因为 target 是在 labelimg 中做了归一化的)映射到真实的图像尺寸
然后,在 NMS之前将数据集标签 targets 添加到模型预测中,这允许在数据集中自动标记(for autolabelling)其它对象(在pred中混入gt)并且mAP反映了新的混合标签。

最后调用general.py中的函数,进行NMS操作,并计算NMS过程所需要的时间,


4.6.5 统计真实框、预测框信息

  1. '''===6.5 统计真实框、预测框信息==='''
  2. # Metrics
  3. # si代表第si张图片,pred是对应图片预测的label信息
  4. for si, pred in enumerate(out):
  5. # 获取第si张图片的gt标签信息 包括class, x, y, w, h target[:, 0]为标签属于哪张图片的编号
  6. labels = targets[targets[:, 0] == si, 1:]
  7. # nl为图片检测到的目标个数
  8. nl = len(labels)
  9. # tcls为检测到的目标的类别 label矩阵的第一列
  10. tcls = labels[:, 0].tolist() if nl else [] # target class
  11. # 第si张图片对应的文件路径
  12. path, shape = Path(paths[si]), shapes[si][0]
  13. # 统计测试图片数量 +1
  14. seen += 1
  15. # 如果预测为空,则添加空的信息到stats里
  16. if len(pred) == 0:
  17. if nl: # 预测为空但同时有label信息
  18. # stats初始化为一个空列表[] 此处添加一个空信息
  19. # 添加的每一个元素均为tuple 其中第二第三个变量为一个空的tensor
  20. stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
  21. continue
  22. # Predictions
  23. # 预测
  24. if single_cls:
  25. pred[:, 5] = 0
  26. # 对pred进行深复制
  27. predn = pred.clone()
  28. # 调用general.py中的函数 将图片调整为原图大小
  29. scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
  30. # Evaluate
  31. # 预测框评估
  32. if nl:
  33. # 获得xyxy格式的框
  34. tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
  35. # 调用general.py中的函数 将图片调整为原图大小
  36. scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
  37. # 处理完gt的尺寸信息,重新构建成 (cls, xyxy)的格式
  38. labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space label
  39. # 对当前的预测框与gt进行一一匹配,并且在预测框的对应位置上获取iou的评分信息,其余没有匹配上的预测框设置为False
  40. correct = process_batch(predn, labelsn, iouv)
  41. if plots:
  42. # 计算混淆矩阵 confusion_matrix
  43. confusion_matrix.process_batch(predn, labelsn)
  44. else:
  45. # 返回一个形状为为pred.shape[0, 类型为torch.dtype,里面的每一个值都是0的tensor
  46. correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
  47. # 每张图片的结果统计到stats里
  48. stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) # (correct, conf, pcls, tcls)
  49. # Save/log
  50. # 保存预测信息到txt文件
  51. if save_txt:
  52. save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
  53. # 保存预测信息到json字典
  54. if save_json:
  55. save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
  56. callbacks.run('on_val_image_end', pred, predn, path, names, im[si])

这段代码主要是统计每张图片真实框和预测框的相关信息

首先统计每张图片的相关信息,如预测label信息标签gt信息等。然后统计检测到的目标个数和类别以及相对应的文件路径。

接着利用得到的上述信息进行目标的预测,并将结果保存同时输出日志,分别保存预测信息到image_name.txt文件coco格式的json字典

  • txt文件保存的预测信息:cls+xywh+conf 
  • jdict字典保存的预测信息:image_id + category_id + bbox + score

4.6.6 画出前三个batch图片的gt和pred框

  1. '''===6.6 画出前三个batch图片的gt和pred框==='''
  2. # Plot images
  3. # 画出前三个batch的图片的ground truth和预测框predictions(两个图)一起保存
  4. if plots and batch_i < 3:
  5. f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels
  6. Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
  7. '''
  8. Thread()函数为创建一个新的线程来执行这个函数 函数为plots.py中的plot_images函数
  9. target: 执行的函数 args: 传入的函数参数 daemon: 当主线程结束后, 由他创建的子线程Thread也已经自动结束了
  10. .start(): 启动线程 当thread一启动的时候, 就会运行我们自己定义的这个函数plot_images
  11. 如果在plot_images里面打开断点调试, 可以发现子线程暂停, 但是主线程还是在正常的训练(还是正常的跑)
  12. '''
  13. # 传入plot_images函数之前需要改变pred的格式 target则不需要改
  14. f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions
  15. Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()

这段代码主要是创建子进程进行绘图,画出前三个batch图片的gt和pred框

  • gt : 真实框,Ground truth box, 是人工标注的位置,存放在标注文件中
  • pred : 预测框,Prediction box, 是由目标检测模型计算输出的框

4.6.7 计算指标

  1. '''===6.7 计算指标==='''
  2. # Compute metrics
  3. # 将stats列表的信息拼接到一起
  4. stats = [np.concatenate(x, 0) for x in zip(*stats)] # 转换为对应格式numpy
  5. # stats[0].any(): stats[0]是否全部为False, 是则返回 False, 如果有一个为 True, 则返回 True
  6. if len(stats) and stats[0].any():
  7. # 计算上述测试过程中的各种性能指标
  8. p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
  9. ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
  10. mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
  11. nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
  12. else:
  13. nt = torch.zeros(1)

这段代码主要是计算评判分类效果的各种指标

correct [img_sum, 10] :整个数据集所有图片中所有预测框在每一个iou条件下是否是TP [1905, 10]

  • p:    [nc] 最大平均f1时每个类别的precision
  • r:    [nc] 最大平均f1时每个类别的recall
  • ap:    [71, 10] 数据集每个类别在10个iou阈值下的mAP
  • f1:    [nc] 最大平均f1时每个类别的f1
  • ap_class:    [nc] 返回数据集中所有的类别index

 conf [img_sum] :整个数据集所有图片中所有预测框的conf [1905]

  • ap50:   [nc] 所有类别的mAP@0.5   
  • ap:   [nc] 所有类别的mAP@0.5:0.95 

pcls [img_sum] :整个数据集所有图片中所有预测框的类别 [1905]

  • mp:   [1] 所有类别的平均precision(最大f1时)
  • mr:   [1] 所有类别的平均recall(最大f1时)
  • map50:   [1] 所有类别的平均mAP@0.5
  • map:   [1] 所有类别的平均mAP@0.5:0.95

 tcls [gt_sum] :整个数据集所有图片所有gt框的class [929]

  • nt:  [nc] 统计出整个数据集的gt框中数据集各个类别的个数 

4.6.8 打印日志   

  1. '''===6.8 打印日志==='''
  2. # Print results
  3. # 按照以下格式来打印测试过程的指标
  4. pf = '%20s' + '%11i' * 2 + '%11.3g' * 4 # print format
  5. LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
  6. # Print results per class
  7. # 打印每一个类别对应的性能指标
  8. if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
  9. for i, c in enumerate(ap_class):
  10. LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
  11. # Print speeds
  12. # 打印 推断/NMS过程/总过程 的在每一个batch上面的时间消耗
  13. t = tuple(x / seen * 1E3 for x in dt) # speeds per image
  14. if not training:
  15. shape = (batch_size, 3, imgsz, imgsz)
  16. LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)

4.6.9 保存验证结果   

  1. '''===6.9 保存验证结果==='''
  2. # Plots
  3. # 绘图
  4. if plots:
  5. # confusion_matrix.plot()函数绘制混淆矩阵
  6. confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
  7. # 调用Loggers中的on_val_end方法,将日志记录并生成一些记录的图片
  8. callbacks.run('on_val_end')
  9. # Save JSON
  10. # 采用之前保存的json文件格式预测结果 通过coco的api评估各个指标
  11. if save_json and len(jdict):
  12. w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
  13. # 注释的json格式
  14. anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
  15. # 预测的json格式
  16. pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
  17. # 在控制台打印coco的api评估各个指标,保存到json文件
  18. LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
  19. # 打开pred_json文件只用于写入
  20. with open(pred_json, 'w') as f: # w:打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
  21. # 测试集的标签也需要转成coco的json格式。将 dict==>json 序列化,用json.dumps()
  22. json.dump(jdict, f)
  23. try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
  24. check_requirements(['pycocotools'])
  25. # 以下过程为利用官方coco工具进行结果的评测
  26. from pycocotools.coco import COCO
  27. from pycocotools.cocoeval import COCOeval
  28. # 获取并初始化测试集标签的json文件
  29. anno = COCO(anno_json) # init annotations api
  30. # 初始化预测框的文件
  31. pred = anno.loadRes(pred_json) # init predictions api
  32. # 创建评估器
  33. eval = COCOeval(anno, pred, 'bbox')
  34. if is_coco:
  35. eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate
  36. # 评估
  37. eval.evaluate()
  38. eval.accumulate()
  39. # 展示结果
  40. eval.summarize()
  41. map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
  42. except Exception as e:
  43. LOGGER.info(f'pycocotools unable to run: {e}')

4.6.10 返回结果

  1. '''===6.10 返回结果==='''
  2. # Return results
  3. # 返回测试指标结果
  4. model.float() # 将模型转换为适用于训练的状态
  5. if not training:# 如果不是训练过程则将结果保存到对应的路径
  6. s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
  7. # 在控制台中打印保存结果
  8. LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
  9. maps = np.zeros(nc) + map
  10. for i, c in enumerate(ap_class):
  11. maps[c] = ap[i]
  12. # 返回对应的测试结果
  13. return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
  • mp:  [1] 所有类别的平均precision(最大f1时)
  • mr:  [1] 所有类别的平均recall(最大f1时)
  • map50:  [1] 所有类别的平均mAP@0.5
  • map : [1] 所有类别的平均mAP@0.5:0.95
  • val_box_loss : [1] 验证集回归损失
  • val_obj_loss:  [1] 验证集置信度损失
  • val_cls_loss:  [1] 验证集分类损失 maps: [80] 所有类别的mAP@0.5:0.95 t: {tuple: 3}
    • 0:  打印前向传播耗费的总时间
    • 1:  nms耗费总时间
    • 2:  总时间

🚀五、设置opt参数

  1. '''===============================================五、设置opt参数==================================================='''
  2. def parse_opt():
  3. parser = argparse.ArgumentParser()
  4. # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
  5. parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
  6. # 模型的权重文件地址yolov5s.pt
  7. parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
  8. # 前向传播的批次大小 默认32
  9. parser.add_argument('--batch-size', type=int, default=32, help='batch size')
  10. # 输入网络的图片分辨率 默认640
  11. parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
  12. # object置信度阈值 默认0.001
  13. parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
  14. # 进行NMS时IOU的阈值 默认0.6
  15. parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
  16. # 设置测试的类型 有train, val, test, speed or study几种 默认val
  17. parser.add_argument('--task', default='val', help='train, val, test, speed or study')
  18. # 测试的设备
  19. parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
  20. # 数据集是否只用一个类别 默认False
  21. parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
  22. # 测试是否使用TTA Test Time Augment 默认False
  23. parser.add_argument('--augment', action='store_true', help='augmented inference')
  24. # 是否打印出每个类别的mAP 默认False
  25. parser.add_argument('--verbose', action='store_true', help='report mAP by class')
  26. # 是否以txt文件的形式保存模型预测的框坐标, 默认False
  27. parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
  28. # 保存label+prediction杂交结果到对应.txt,默认False
  29. parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
  30. # 保存置信度
  31. parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
  32. # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签) 默认False
  33. parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
  34. # 测试保存的源文件 默认runs/val
  35. parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
  36. # 测试保存的文件地址 默认exp 保存在runs/val/exp下
  37. parser.add_argument('--name', default='exp', help='save to project/name')
  38. # 是否存在当前文件 默认False 一般是 no exist-ok 连用 所以一般都要重新创建文件夹
  39. parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
  40. # 是否使用半精度推理 默认False
  41. parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
  42. # 是否使用 OpenCV DNN对ONNX 模型推理
  43. parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
  44. # 解析上述参数
  45. opt = parser.parse_args()
  46. opt.data = check_yaml(opt.data)
  47. # |或 左右两个变量有一个为True 左边变量就为True
  48. opt.save_json |= opt.data.endswith('coco.yaml')
  49. opt.save_txt |= opt.save_hybrid
  50. print_args(FILE.stem, opt)
  51. return opt
  • data:  数据集文件的路径,默认为COCO128数据集的配置文件路径
  • weights:  模型权重文件的路径,默认为YOLOv5s的权重文件路径
  • batch_size:   前向传播的批次大小,运行val.py传入默认32 。运行train.py则传入batch_size // WORLD_SIZE * 2
  • imgsz:  输入图像的大小,默认为640x640
  • conf_thres:  置信度阈值,默认为0.001
  • iou_thres:  非极大值抑制的iou阈值,默认为0.6
  • task:   设置测试的类型 有train, val, test, speed or study几种,默认val
  • device:  使用的设备类型,默认为空,表示自动选择最合适的设备
  • single_cls:   数据集是否只用一个类别,运行val.py传入默认False 运行train.py则传入single_cls
  • augment:  是否使用数据增强的方式进行检测,默认为False
  • verbose:   是否打印出每个类别的mAP,运行val.py传入默认Fasle。运行train.py则传入nc < 50 and final_epoch
  • save_txt:  是否将检测结果保存为文本文件,默认为False
  • save_hybrid:   是否保存 label+prediction hybrid results to *.txt 默认False
  • save_conf:  是否在保存的文本文件中包含置信度信息,默认为False
  • save_json:  是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签)运行test.py传入默认Fasle。运行train.py则传入is_coco and final_epoch(一般也是False)
  • project:  结果保存的项目文件夹路径,默认为“runs/val”
  • name:  结果保存的文件名,默认为“exp”
  • exist_ok:  如果结果保存的文件夹已存在,是否覆盖,默认为False,即不覆盖
  • half:  是否使用FP16的半精度推理模式,默认为False
  • dnn:  是否使用OpenCV DNN作为ONNX推理的后端,默认为False

🚀六、执行main()函数

  1. '''==============================六、执行main()函数======================================'''
  2. def main(opt):
  3. # 检测requirements文件中需要的包是否安装好了
  4. check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
  5. # 如果task in ['train', 'val', 'test']就正常测试 训练集/验证集/测试集
  6. if opt.task in ('train', 'val', 'test'): # run normally
  7. if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
  8. LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
  9. run(**vars(opt))
  10. else:
  11. weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
  12. opt.half = True # FP16 for fastest results
  13. # 如果opt.task == 'speed' 就测试yolov5系列和yolov3-spp各个模型的速度评估
  14. if opt.task == 'speed': # speed benchmarks
  15. # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
  16. opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
  17. for opt.weights in weights:
  18. run(**vars(opt), plots=False)
  19. # 如果opt.task = ['study']就评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
  20. elif opt.task == 'study': # speed vs mAP benchmarks
  21. # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
  22. for opt.weights in weights:
  23. # 保存的文件名
  24. f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to
  25. # x坐标轴和y坐标
  26. x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
  27. for opt.imgsz in x: # img-size
  28. LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
  29. r, _, t = run(**vars(opt), plots=False)
  30. # 返回相关结果和时间
  31. y.append(r + t) # results and times
  32. # 将y输出保存
  33. np.savetxt(f, y, fmt='%10.4g') # save
  34. # 命令行执行命令将study文件进行压缩
  35. os.system('zip -r study.zip study_*.txt')
  36. # 调用plots.py中的函数 可视化各个指标
  37. plot_val_study(x=x) # plot
  38. # python val.py --data data/mask_data.yaml --weights runs/train/exp_yolov5s/weights/best.pt --img 640
  39. if __name__ == "__main__":
  40. opt = parse_opt()
  41. main(opt)

🚀七、val.py代码全部注释

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. """
  3. Validate a trained YOLOv5 model accuracy on a custom dataset
  4. Usage:
  5. $ python path/to/val.py --data coco128.yaml --weights yolov5s.pt --img 640
  6. """
  7. '''===============================================一、导入包==================================================='''
  8. '''======================1.导入安装好的python库====================='''
  9. import argparse # 解析命令行参数的库
  10. import json # 实现字典列表和JSON字符串之间的相互解析
  11. import os # 与操作系统进行交互的文件库 包含文件路径操作与解析
  12. import sys # sys系统模块 包含了与Python解释器和它的环境有关的函数
  13. from pathlib import Path # Path将str转换为Path对象 使字符串路径易于操作的模块
  14. from threading import Thread # python中处理多线程的库
  15. import numpy as np # 矩阵计算基础库
  16. import torch # pytorch 深度学习库
  17. from tqdm import tqdm # 用于直观显示进度条的一个库
  18. '''===================2.获取当前文件的绝对路径========================'''
  19. FILE = Path(__file__).resolve()# __file__指的是当前文件(即val.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/val.py
  20. ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
  21. if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
  22. sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
  23. ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径
  24. '''===================3..加载自定义模块============================'''
  25. from models.common import DetectMultiBackend # yolov5的网络结构(yolov5)
  26. from utils.callbacks import Callbacks # 和日志相关的回调函数
  27. from utils.datasets import create_dataloader # 加载数据集的函数
  28. from utils.general import (LOGGER, NCOLS, box_iou, check_dataset, check_img_size, check_requirements, check_yaml,
  29. coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
  30. scale_coords, xywh2xyxy, xyxy2xywh) # 定义了一些常用的工具函数
  31. from utils.metrics import ConfusionMatrix, ap_per_class # 在YOLOv5中,fitness函数实现对 [P, R, mAP@.5, mAP@.5-.95] 指标进行加权
  32. from utils.plots import output_to_target, plot_images, plot_val_study # 定义了Annotator类,可以在图像上绘制矩形框和标注信息
  33. from utils.torch_utils import select_device, time_sync # 定义了一些与PyTorch有关的工具函数
  34. '''===============================================二、保存信息==================================================='''
  35. '''======================1.保存预测信息到txt文件====================='''
  36. def save_one_txt(predn, save_conf, shape, file):
  37. # Save one txt result
  38. # gn = [w, h, w, h] 对应图片的宽高 用于后面归一化
  39. gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
  40. # 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
  41. for *xyxy, conf, cls in predn.tolist():
  42. # 将xyxy(左上角+右下角)格式转为xywh(中心点+宽长)格式,并归一化,转化为列表再保存
  43. xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
  44. # line的形式是: ”类别 x y w h“,若save_conf为true,则line的形式是:”类别 x y w h 置信度“
  45. line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
  46. # 保存预测类别和坐标值到对应图片image_name.txt文件中
  47. with open(file, 'a') as f:
  48. # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
  49. f.write(('%g ' * len(line)).rstrip() % line + '\n')
  50. '''======================2.保存预测信息到coco格式的json字典====================='''
  51. def save_one_json(predn, jdict, path, class_map):
  52. # Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
  53. # 获取图片id
  54. image_id = int(path.stem) if path.stem.isnumeric() else path.stem
  55. # 获取预测框 并将xyxy转为xywh格式
  56. box = xyxy2xywh(predn[:, :4]) # xywh
  57. # 之前的的xyxy格式是左上角右下角坐标 xywh是中心的坐标和宽高
  58. # 而coco的json格式的框坐标是xywh(左上角坐标 + 宽高)
  59. # 所以这行代码是将中心点坐标 -> 左上角坐
  60. box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
  61. # 序列解包
  62. for p, b in zip(predn.tolist(), box.tolist()):
  63. jdict.append({'image_id': image_id,
  64. 'category_id': class_map[int(p[5])],
  65. 'bbox': [round(x, 3) for x in b],
  66. 'score': round(p[4], 5)})
  67. '''
  68. image_id: 图片id 即属于哪张图片
  69. category_id: 类别 coco91class()从索引0~79映射到索引0~90
  70. bbox: 预测框坐标
  71. score: 预测得分
  72. '''
  73. '''===============================================三、计算指标==================================================='''
  74. def process_batch(detections, labels, iouv):
  75. """
  76. Return correct predictions matrix.
  77. 返回每个预测框在10个IoU阈值上是TP还是FP
  78. Both sets of boxes are in (x1, y1, x2, y2) format.
  79. Arguments:
  80. detections (Array[N, 6]), x1, y1, x2, y2, conf, class
  81. labels (Array[M, 5]), class, x1, y1, x2, y2
  82. Returns:
  83. correct (Array[N, 10]), for 10 IoU levels
  84. """
  85. # 构建一个[pred_nums, 10]全为False的矩阵
  86. correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device)
  87. # 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]
  88. iou = box_iou(labels[:, 1:], detections[:, :4])
  89. '''
  90. 首先iou >= iouv[0]:挑选出iou>0.5的所有预测框,进行筛选,shape为: [gt_nums, pred_nums]
  91. 同时labels[:, 0:1] == detections[:, 5]:构建出一个预测类别与真实标签是否相同的矩阵表, shape为: [gt_nums, pred_nums]
  92. 只有同时符合以上两点条件才被赋值为True,此时返回当前矩阵的一个行列索引,x是两个元祖x1,x2
  93. 点(x[0][i], x[1][i])就是符合条件的预测框
  94. '''
  95. # iou超过阈值而且类别正确,则为True,返回索引
  96. x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5])) # IoU above threshold and classes match
  97. # 如果存在符合条件的预测框
  98. if x[0].shape[0]: # 至少有一个TP
  99. # 将符合条件的位置构建成一个新的矩阵,第一列是行索引(表示gt索引),第二列是列索引(表示预测框索引),第三列是iou值
  100. matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detection, iou]
  101. if x[0].shape[0] > 1:
  102. # argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
  103. matches = matches[matches[:, 2].argsort()[::-1]]
  104. matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
  105. '''
  106. 参数return_index=True:表示会返回唯一值的索引,[0]返回的是唯一值,[1]返回的是索引
  107. matches[:, 1]:这里的是获取iou矩阵每个预测框的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
  108. 这个操作的含义:每个预测框最多只能出现一次,如果有一个预测框同时和多个gt匹配,只取其最大iou的一个
  109. '''
  110. # matches = matches[matches[:, 2].argsort()[::-1]]
  111. matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
  112. '''
  113. matches[:, 0]:这里的是获取iou矩阵gt的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
  114. 这个操作的含义: 每个gt也最多只能出现一次,如果一个gt同时匹配多个预测框,只取其匹配最大的那一个预测框
  115. '''
  116. # 以上操作实现了为每一个gt分配一个iou最高的类别的预测框,实现一一对应
  117. matches = torch.Tensor(matches).to(iouv.device)
  118. correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv
  119. '''
  120. 当前获得了gt与预测框的一一对应,其对于的iou可以作为评价指标,构建一个评价矩阵
  121. 需要注意,这里的matches[:, 1]表示的是为对应的预测框来赋予其iou所能达到的程度,也就是iouv的评价指标
  122. '''
  123. # 在correct中,只有与gt匹配的预测框才有对应的iou评价指标,其他大多数没有匹配的预测框都是全部为False
  124. return correct
  125. '''===============================================四、run()函数==================================================='''
  126. '''======================1.设置参数====================='''
  127. @torch.no_grad()
  128. def run(data, # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息 train.py时传入data_dict
  129. weights=None, # 模型的权重文件地址 运行train.py=None 运行test.py=默认weights/yolov5s
  130. batch_size=32, # 前向传播的批次大小 运行test.py传入默认32 运行train.py则传入batch_size // WORLD_SIZE * 2
  131. imgsz=640, # 输入网络的图片分辨率 运行test.py传入默认640 运行train.py则传入imgsz_test
  132. conf_thres=0.001, # object置信度阈值 默认0.001
  133. iou_thres=0.6, # 进行NMS时IOU的阈值 默认0.6
  134. task='val', # 设置测试的类型 有train, val, test, speed or study几种 默认val
  135. device='', # 执行 val.py 所在的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu
  136. single_cls=False, # dataloader中的最大 worker 数(线程个数)
  137. augment=False, # 数据集是否只有一个类别 默认False
  138. verbose=False, # 是否打印出每个类别的mAP 运行test.py传入默认Fasle 运行train.py则传入nc < 50 and final_epoch
  139. save_txt=False, # 是否以txt文件的形式保存模型预测框的坐标 默认True
  140. save_hybrid=False, # 是否保存预测每个目标的置信度到预测txt文件中 默认True
  141. save_conf=False, # 保存置信度
  142. save_json=False, # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),
  143. #运行test.py传入默认Fasle 运行train.py则传入is_coco and final_epoch(一般也是False)
  144. project=ROOT / 'runs/val', # 验证结果保存的根目录 默认是 runs/val
  145. name='exp', # 验证结果保存的目录 默认是exp 最终: runs/val/exp
  146. exist_ok=False, # 如果文件存在就increment name,不存在就新建 默认False(默认文件都是不存在的)
  147. half=True, # 使用 FP16 的半精度推理
  148. dnn=False, # 在 ONNX 推理时使用 OpenCV DNN 后段端
  149. model=None, # 如果执行val.py就为None 如果执行train.py就会传入( model=attempt_load(f, device).half() )
  150. dataloader=None, # 数据加载器 如果执行val.py就为None 如果执行train.py就会传入testloader
  151. save_dir=Path(''), # 文件保存路径 如果执行val.py就为‘’ , 如果执行train.py就会传入save_dir(runs/train/expn)
  152. plots=True, # 是否可视化 运行val.py传入,默认True
  153. callbacks=Callbacks(),
  154. compute_loss=None, # 损失函数 运行val.py传入默认None 运行train.py则传入compute_loss(train)
  155. ):
  156. '''======================2.初始化/加载模型以及设置设备====================='''
  157. # Initialize/load model and set device
  158. training = model is not None
  159. if training: # 通过 train.py 调用的run函数
  160. # 获得记录在模型中的设备 next为迭代器
  161. device, pt = next(model.parameters()).device, True
  162. # 精度减半
  163. # 如果设备类型不是cpu 则将模型由32位浮点数转换为16位浮点数
  164. half &= device.type != 'cpu' # half precision only supported on CUDA
  165. model.half() if half else model.float()
  166. # 如果不是train.py调用run函数(执行val.py脚本)就调用select_device选择可用的设备
  167. # 并生成save_dir + make dir + 加载模型model + check imgsz + 加载data配置信息
  168. else: # 直接通过 val.py 调用 run 函数
  169. # 调用torch_utils中select_device来选择执行程序时的设备
  170. device = select_device(device, batch_size=batch_size)
  171. # 路径
  172. # 调用genera.py中的increment_path函数来生成save_dir文件路径 run\test\expn
  173. save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
  174. # mkdir创建路径最后一级目录
  175. (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
  176. # 加载模型 只在运行test.py才需要自己加载model
  177. # 加载模型为32位浮点数模型(权重参数) 调用experimental.py文件中的attempt_load函数
  178. model = DetectMultiBackend(weights, device=device, dnn=dnn)
  179. stride, pt = model.stride, model.pt
  180. # 调用general.py中的check_img_size函数来检查图像分辨率能否被32整除
  181. imgsz = check_img_size(imgsz, s=stride) # check image size
  182. # 如果不是CPU,使用半进度(图片半精度/模型半精度)
  183. half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
  184. if pt:
  185. model.model.half() if half else model.model.float()
  186. else:
  187. half = False
  188. batch_size = 1 # export.py models default to batch-size 1
  189. device = torch.device('cpu')
  190. # 打印耗时
  191. LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')
  192. # Data
  193. # 调用general.py中的check_dataset函数来检查数据文件是否正常
  194. data = check_dataset(data) # check
  195. '''======================3.加载配置====================='''
  196. # Configure
  197. # 将模型转换为测试模式 固定住dropout层和Batch Normalization层
  198. model.eval()
  199. # 通过 COCO 数据集的文件夹组织结构判断当前数据集是否为 COCO 数据集
  200. is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # COCO dataset
  201. # 确定检测的类别数目
  202. nc = 1 if single_cls else int(data['nc']) # number of classes
  203. # 计算mAP相关参数
  204. # 设置iou阈值 从0.5-0.95取10个(0.05间隔) iou vector for mAP@0.5:0.95
  205. # iouv: [0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]
  206. iouv = torch.linspace(0.5, 0.95, 10).to(device) # mAP@0.5:0.95 的iou向量
  207. # numel为pytorch预置函数 用来获取张量中的元素个数
  208. niou = iouv.numel()
  209. '''======================4.加载val数据集====================='''
  210. # Dataloader
  211. if not training:
  212. if pt and device.type != 'cpu':
  213. # 创建一张全为0的图片(四维张量)
  214. model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # warmup
  215. pad = 0.0 if task == 'speed' else 0.5
  216. task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
  217. # 调用datasets.py文件中的create_dataloader函数创建dataloader
  218. dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
  219. prefix=colorstr(f'{task}: '))[0]
  220. '''======================5.初始化====================='''
  221. # 初始化已完成测试的图片数量
  222. seen = 0
  223. # 调用matrics中函数 存储混淆矩阵
  224. confusion_matrix = ConfusionMatrix(nc=nc)
  225. # 获取数据集所有类别的类名
  226. names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
  227. # 调用general.py中的函数 获取coco数据集的类别索引
  228. class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
  229. # 设置tqdm进度条的显示信息
  230. s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
  231. # 初始化detection中各个指标的值
  232. dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
  233. # 初始化网络训练的loss
  234. loss = torch.zeros(3, device=device)
  235. # 初始化json文件涉及到的字典、统计信息、AP、每一个类别的AP、图片汇总
  236. jdict, stats, ap, ap_class = [], [], [], []
  237. pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
  238. '''======================6.开始验证====================='''
  239. '''===6.1 开始验证前的预处理==='''
  240. for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
  241. # 获取当前时间
  242. t1 = time_sync()
  243. if pt:
  244. # 将图片数据拷贝到device(GPU)上面
  245. im = im.to(device, non_blocking=True)
  246. #对targets也做同样拷贝的操作
  247. targets = targets.to(device)
  248. # 将图片从64位精度转换为32位精度
  249. im = im.half() if half else im.float() # uint8 to fp16/32
  250. # 将图像像素值0-255的范围归一化到0-1的范围
  251. im /= 255 # 0 - 255 to 0.0 - 1.0
  252. # 四个变量分别代表batchsize、通道数目、图像高度、图像宽度
  253. nb, _, height, width = im.shape # batch size, channels, height, width
  254. # 获取当前时间
  255. t2 = time_sync()
  256. # dt[0]: 累计处理数据时间
  257. dt[0] += t2 - t1
  258. '''===6.2 前向推理==='''
  259. # Inference
  260. out, train_out = model(im) if training else model(im, augment=augment, val=True) # inference, loss outputs
  261. # 累计前向推理时间 dt[1]
  262. dt[1] += time_sync() - t2
  263. '''===6.3 计算损失==='''
  264. # Loss
  265. # compute_loss不为空 说明正在执行train.py 根据传入的compute_loss计算损失值
  266. if compute_loss:
  267. # loss 包含bounding box 回归的GIoU、object和class 三者的损失
  268. loss += compute_loss([x.float() for x in train_out], targets)[1] # box, obj, cls
  269. '''===6.4 NMS获得预测框==='''
  270. # NMS
  271. # 运行NMS 目标检测的后处理模块 用于删除冗余的bounding box
  272. # targets: [num_target, img_index+class_index+xywh] = [31, 6]
  273. targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
  274. # 提取bach中每一张图片的目标的label
  275. # lb: {list: bs} 第一张图片的target[17, 5] 第二张[1, 5] 第三张[7, 5] 第四张[6, 5]
  276. lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
  277. # 计算NMS过程所需要的时间
  278. t3 = time_sync()
  279. # 调用general.py中的函数 进行非极大值抑制操作
  280. out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
  281. # 累计NMS时间
  282. dt[2] += time_sync() - t3
  283. '''===6.5 统计真实框、预测框信息==='''
  284. # Metrics
  285. # 为每张图片做统计,将写预测信息到txt文件,生成json文件字典,统计tp等
  286. # out: list{bs} [300, 6] [42, 6] [300, 6] [300, 6] [:, image_index+class+xywh]
  287. # si代表第si张图片,pred是对应图片预测的label信息
  288. for si, pred in enumerate(out):
  289. # 获取第si张图片的gt标签信息 包括class, x, y, w, h target[:, 0]为标签属于哪张图片的编号
  290. labels = targets[targets[:, 0] == si, 1:]
  291. # nl为图片检测到的目标个数
  292. nl = len(labels)
  293. # tcls为检测到的目标的类别 label矩阵的第一列
  294. tcls = labels[:, 0].tolist() if nl else [] # target class
  295. # 第si张图片对应的文件路径
  296. path, shape = Path(paths[si]), shapes[si][0]
  297. # 统计测试图片数量 +1
  298. seen += 1
  299. # 如果预测为空,则添加空的信息到stats里
  300. if len(pred) == 0:
  301. if nl: # 预测为空但同时有label信息
  302. # stats初始化为一个空列表[] 此处添加一个空信息
  303. # 添加的每一个元素均为tuple 其中第二第三个变量为一个空的tensor
  304. stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
  305. continue
  306. # Predictions
  307. # 预测
  308. if single_cls:
  309. pred[:, 5] = 0
  310. # 对pred进行深复制
  311. predn = pred.clone()
  312. # 调用general.py中的函数 将图片调整为原图大小
  313. scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
  314. # Evaluate
  315. # 预测框评估
  316. if nl:
  317. # 获得xyxy格式的框
  318. tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
  319. # 调用general.py中的函数 将图片调整为原图大小
  320. scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
  321. # 处理完gt的尺寸信息,重新构建成 (cls, xyxy)的格式
  322. labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space label
  323. # 对当前的预测框与gt进行一一匹配,并且在预测框的对应位置上获取iou的评分信息,其余没有匹配上的预测框设置为False
  324. correct = process_batch(predn, labelsn, iouv)
  325. if plots:
  326. # 计算混淆矩阵 confusion_matrix
  327. confusion_matrix.process_batch(predn, labelsn)
  328. else:
  329. # 返回一个形状为为pred.shape[0, 类型为torch.dtype,里面的每一个值都是0的tensor
  330. correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
  331. # 每张图片的结果统计到stats里
  332. stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) # (correct, conf, pcls, tcls)
  333. # Save/log
  334. # 保存预测信息到txt文件
  335. if save_txt:
  336. save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
  337. # 保存预测信息到json字典
  338. if save_json:
  339. save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
  340. callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
  341. '''===6.6 画出前三个batch图片的 gt 和 pred 框==='''
  342. # Plot images
  343. # 画出前三个batch的图片的ground truth和预测框predictions(两个图)一起保存
  344. if plots and batch_i < 3:
  345. f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels
  346. Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
  347. '''
  348. Thread()函数为创建一个新的线程来执行这个函数 函数为plots.py中的plot_images函数
  349. target: 执行的函数 args: 传入的函数参数 daemon: 当主线程结束后, 由他创建的子线程Thread也已经自动结束了
  350. .start(): 启动线程 当thread一启动的时候, 就会运行我们自己定义的这个函数plot_images
  351. 如果在plot_images里面打开断点调试, 可以发现子线程暂停, 但是主线程还是在正常的训练(还是正常的跑)
  352. '''
  353. # 传入plot_images函数之前需要改变pred的格式 target则不需要改
  354. f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions
  355. Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
  356. '''===6.7 计算指标==='''
  357. # Compute metrics
  358. # 将stats列表的信息拼接到一起
  359. stats = [np.concatenate(x, 0) for x in zip(*stats)] # 转换为对应格式numpy
  360. # stats[0].any(): stats[0]是否全部为False, 是则返回 False, 如果有一个为 True, 则返回 True
  361. if len(stats) and stats[0].any():
  362. # 计算上述测试过程中的各种性能指标
  363. p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
  364. '''
  365. 根据上面的统计预测结果计算p, r, ap, f1, ap_class(ap_per_class函数是计算每个类的mAP等指标的)等指标
  366. p: [nc] 最大平均f1时每个类别的precision
  367. r: [nc] 最大平均f1时每个类别的recall
  368. ap: [71, 10] 数据集每个类别在10个iou阈值下的mAP
  369. f1 [nc] 最大平均f1时每个类别的f1
  370. ap_class: [nc] 返回数据集中所有的类别index
  371. '''
  372. ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
  373. '''
  374. ap50: [nc] 所有类别的mAP@0.5
  375. ap: [nc] 所有类别的mAP@0.5:0.95
  376. '''
  377. mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
  378. '''
  379. mp: [1] 所有类别的平均precision(最大f1时)
  380. mr: [1] 所有类别的平均recall(最大f1时)
  381. map50: [1] 所有类别的平均mAP@0.5
  382. map: [1] 所有类别的平均mAP@0.5:0.95
  383. '''
  384. nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
  385. '''
  386. nt: [nc] 统计出整个数据集的gt框中数据集各个类别的个数
  387. '''
  388. else:
  389. nt = torch.zeros(1)
  390. '''===6.8 打印日志==='''
  391. # Print results
  392. # 按照以下格式来打印测试过程的指标
  393. pf = '%20s' + '%11i' * 2 + '%11.3g' * 4 # print format
  394. LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
  395. # Print results per class
  396. # 打印每一个类别对应的性能指标
  397. if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
  398. for i, c in enumerate(ap_class):
  399. LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
  400. # Print speeds
  401. # 打印 推断/NMS过程/总过程 的在每一个batch上面的时间消耗
  402. t = tuple(x / seen * 1E3 for x in dt) # speeds per image
  403. if not training:
  404. shape = (batch_size, 3, imgsz, imgsz)
  405. LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)
  406. '''===6.9 保存验证结果==='''
  407. # Plots
  408. # 绘图
  409. if plots:
  410. # confusion_matrix.plot()函数绘制混淆矩阵
  411. confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
  412. # 调用Loggers中的on_val_end方法,将日志记录并生成一些记录的图片
  413. callbacks.run('on_val_end')
  414. # Save JSON
  415. # 采用之前保存的json文件格式预测结果 通过coco的api评估各个指标
  416. if save_json and len(jdict):
  417. w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
  418. # 注释的json格式
  419. anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
  420. # 预测的json格式
  421. pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
  422. # 在控制台打印coco的api评估各个指标,保存到json文件
  423. LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
  424. # 打开pred_json文件只用于写入
  425. with open(pred_json, 'w') as f: # w:打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
  426. # 测试集的标签也需要转成coco的json格式。将 dict==>json 序列化,用json.dumps()
  427. json.dump(jdict, f)
  428. try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
  429. check_requirements(['pycocotools'])
  430. # 以下过程为利用官方coco工具进行结果的评测
  431. from pycocotools.coco import COCO
  432. from pycocotools.cocoeval import COCOeval
  433. # 获取并初始化测试集标签的json文件
  434. anno = COCO(anno_json) # init annotations api
  435. # 初始化预测框的文件
  436. pred = anno.loadRes(pred_json) # init predictions api
  437. # 创建评估器
  438. eval = COCOeval(anno, pred, 'bbox')
  439. if is_coco:
  440. eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate
  441. # 评估
  442. eval.evaluate()
  443. eval.accumulate()
  444. # 展示结果
  445. eval.summarize()
  446. map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
  447. except Exception as e:
  448. LOGGER.info(f'pycocotools unable to run: {e}')
  449. '''===6.10 返回结果==='''
  450. # Return results
  451. # 返回测试指标结果
  452. model.float() # 将模型转换为适用于训练的状态
  453. if not training:# 如果不是训练过程则将结果保存到对应的路径
  454. s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
  455. # 在控制台中打印保存结果
  456. LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
  457. maps = np.zeros(nc) + map
  458. for i, c in enumerate(ap_class):
  459. maps[c] = ap[i]
  460. # 返回对应的测试结果
  461. return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
  462. '''
  463. (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()): {tuple:7}
  464. 0: mp [1] 所有类别的平均precision(最大f1时)
  465. 1: mr [1] 所有类别的平均recall(最大f1时)
  466. 2: map50 [1] 所有类别的平均mAP@0.5
  467. 3: map [1] 所有类别的平均mAP@0.5:0.95
  468. 4: val_box_loss [1] 验证集回归损失
  469. 5: val_obj_loss [1] 验证集置信度损失
  470. 6: val_cls_loss [1] 验证集分类损失
  471. maps: [80] 所有类别的mAP@0.5:0.95
  472. t: {tuple: 3} 0: 打印前向传播耗费的总时间 1: nms耗费总时间 2: 总时间
  473. '''
  474. '''===============================================五、设置opt参数==================================================='''
  475. def parse_opt():
  476. parser = argparse.ArgumentParser()
  477. # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
  478. parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
  479. # 模型的权重文件地址yolov5s.pt
  480. parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
  481. # 前向传播的批次大小 默认32
  482. parser.add_argument('--batch-size', type=int, default=32, help='batch size')
  483. # 输入网络的图片分辨率 默认640
  484. parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
  485. # object置信度阈值 默认0.001
  486. parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
  487. # 进行NMS时IOU的阈值 默认0.6
  488. parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
  489. # 设置测试的类型 有train, val, test, speed or study几种 默认val
  490. parser.add_argument('--task', default='val', help='train, val, test, speed or study')
  491. # 测试的设备
  492. parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
  493. # 数据集是否只用一个类别 默认False
  494. parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
  495. # 测试是否使用TTA Test Time Augment 默认False
  496. parser.add_argument('--augment', action='store_true', help='augmented inference')
  497. # 是否打印出每个类别的mAP 默认False
  498. parser.add_argument('--verbose', action='store_true', help='report mAP by class')
  499. # 是否以txt文件的形式保存模型预测的框坐标, 默认False
  500. parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
  501. # 保存label+prediction杂交结果到对应.txt,默认False
  502. parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
  503. # 保存置信度
  504. parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
  505. # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签) 默认False
  506. parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
  507. # 测试保存的源文件 默认runs/val
  508. parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
  509. # 测试保存的文件地址 默认exp 保存在runs/val/exp下
  510. parser.add_argument('--name', default='exp', help='save to project/name')
  511. # 是否存在当前文件 默认False 一般是 no exist-ok 连用 所以一般都要重新创建文件夹
  512. parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
  513. # 是否使用半精度推理 默认False
  514. parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
  515. # 是否使用 OpenCV DNN对ONNX 模型推理
  516. parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
  517. # 解析上述参数
  518. opt = parser.parse_args()
  519. opt.data = check_yaml(opt.data)
  520. # |或 左右两个变量有一个为True 左边变量就为True
  521. opt.save_json |= opt.data.endswith('coco.yaml')
  522. opt.save_txt |= opt.save_hybrid
  523. print_args(FILE.stem, opt)
  524. return opt
  525. '''===============================================六、执行main()函数==================================================='''
  526. def main(opt):
  527. # 检测requirements文件中需要的包是否安装好了
  528. check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
  529. # 如果task in ['train', 'val', 'test']就正常测试 训练集/验证集/测试集
  530. if opt.task in ('train', 'val', 'test'): # run normally
  531. if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
  532. LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
  533. run(**vars(opt))
  534. else:
  535. weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
  536. opt.half = True # FP16 for fastest results
  537. # 如果opt.task == 'speed' 就测试yolov5系列和yolov3-spp各个模型的速度评估
  538. if opt.task == 'speed': # speed benchmarks
  539. # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
  540. opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
  541. for opt.weights in weights:
  542. run(**vars(opt), plots=False)
  543. # 如果opt.task = ['study']就评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
  544. elif opt.task == 'study': # speed vs mAP benchmarks
  545. # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
  546. for opt.weights in weights:
  547. # 保存的文件名
  548. f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to
  549. # x坐标轴和y坐标
  550. x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
  551. for opt.imgsz in x: # img-size
  552. LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
  553. r, _, t = run(**vars(opt), plots=False)
  554. # 返回相关结果和时间
  555. y.append(r + t) # results and times
  556. # 将y输出保存
  557. np.savetxt(f, y, fmt='%10.4g') # save
  558. # 命令行执行命令将study文件进行压缩
  559. os.system('zip -r study.zip study_*.txt')
  560. # 调用plots.py中的函数 可视化各个指标
  561. plot_val_study(x=x) # plot
  562. # python val.py --data data/mask_data.yaml --weights runs/train/exp_yolov5s/weights/best.pt --img 640
  563. if __name__ == "__main__":
  564. opt = parse_opt()
  565. main(opt)