val.py
val.py
目录
2.def save_one_txt(predn, save_conf, shape, file):
3.def process_batch(detections, labels, iouv):
1.所需的库和模块
import argparse
import json
import os
import sys
from pathlib import Path
import numpy as np
import torch
from tqdm import tqdm
# 这段代码它的作用是将YOLO项目的根目录添加到系统路径中,以便可以正确地导入项目中的模块。
# 获取当前执行文件的路径,并将其解析为绝对路径,存储在变量 FILE 中。
FILE = Path(__file__).resolve()
# 获取 FILE 的父目录,即YOLO项目的根目录,并将其存储在变量 ROOT 中。
ROOT = FILE.parents[0] # YOLO root directory
# 检查 ROOT 路径是否已经在系统路径 sys.path 中。
if str(ROOT) not in sys.path:
# 如果 ROOT 不在系统路径中,则将其添加到 sys.path 中,这样就可以在任何地方导入YOLO项目中的模块。
sys.path.append(str(ROOT)) # add ROOT to PATH
# 将 ROOT 转换为相对于当前工作目录的相对路径,并更新 ROOT 变量。
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
# 这段代码的主要目的是确定项目的根目录,并确保这个目录被添加到Python的模块搜索路径中,以便可以方便地导入项目中的其他模块。此外,它还将根目录的路径转换为相对于当前工作目录的相对路径。
from models.common import DetectMultiBackend
from utils.callbacks import Callbacks
from utils.dataloaders import create_dataloader
from utils.general import (LOGGER, TQDM_BAR_FORMAT, Profile, check_dataset, check_img_size, check_requirements,
check_yaml, coco80_to_coco91_class, colorstr, increment_path, non_max_suppression,
print_args, scale_boxes, xywh2xyxy, xyxy2xywh)
from utils.metrics import ConfusionMatrix, ap_per_class, box_iou
from utils.plots import output_to_target, plot_images, plot_val_study
from utils.torch_utils import select_device, smart_inference_mode
2.def save_one_txt(predn, save_conf, shape, file):
# 这段代码定义了一个名为 save_one_txt 的函数,它用于将检测结果保存到一个文本文件中。
# 这行代码定义了一个函数 save_one_txt ,它接受四个参数。
# 1.predn :预测结果,通常是一个包含检测框坐标、置信度和类别的张量。
# 2.save_conf :一个布尔值,指示是否保存置信度信息。
# 3.shape :图像的形状,通常是一个包含图像宽度和高度的元组。
# 4.file :要保存结果的文件路径。
def save_one_txt(predn, save_conf, shape, file):
# Save one txt result
# 创建一个张量 gn ,它包含图像的高度和宽度( shape 参数),用于后续的归一化计算。 [[1, 0, 1, 0]] 索引意味着我们取 shape 的第二个元素(高度)和第一个元素(宽度)两次,形成一个 [height, width, height, width] 的张量。
gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
# 遍历 predn 张量中的每个元素。 xyxy 是一个包含四个坐标值的元组,代表检测框的左上角和右下角坐标; conf 是置信度; cls 是类别。 tolist() 方法将张量转换为列表。
for *xyxy, conf, cls in predn.tolist():
# 将检测框的坐标从 xyxy 格式(左上角和右下角坐标)转换为 xywh 格式(中心点坐标和宽高)。然后,将转换后的坐标除以 gn 进行归一化,并转换为列表。
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
# 根据 save_conf 的值构建要写入文件的行内容。如果 save_conf 为 True ,则包括置信度 conf ;否则,不包括。
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
# 以追加模式打开文件 file ,准备写入数据。
with open(file, 'a') as f:
# 将 line 中的数据格式化为字符串,并写入文件。 '%g ' * len(line) 创建一个格式化字符串,其中 %g 是一个占位符,用于每个数值, len(line) 确保有足够的占位符。 rstrip() 方法移除字符串末尾的空格, % line 将 line 中的值插入到格式化字符串中,最后添加一个换行符。
f.write(('%g ' * len(line)).rstrip() % line + '\n')
# 这个函数将检测结果(包括坐标、置信度和类别)保存到文本文件中。坐标首先从 xyxy 格式转换为 xywh 格式,然后进行归一化处理。根据 save_conf 参数的值,可以选择是否保存置信度。最后,结果被写入指定的文件中。
# 这段代码定义了一个名为 save_one_json 的函数,它用于将检测结果保存到一个 JSON 格式的数据结构中。
# 这行代码定义了一个函数 save_one_json ,它接受四个参数。
# 1.predn :预测结果,通常是一个包含检测框坐标、置信度和类别的张量。
# 2.jdict :一个列表,用于存储 JSON 结果。
# 3.path :图像文件的路径,用于提取图像 ID。
# 4.class_map :一个映射,用于将类别索引转换为类别 ID。
def save_one_json(predn, jdict, path, class_map):
# Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} 保存一个JSON结果 {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236} 。
# 从 path 参数中提取文件名( path.stem ),如果文件名是数字,则将其转换为整数类型,否则保持原样。这个值将用作 JSON 结果中的 image_id 。
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
# 将预测结果中的坐标从 xyxy 格式(左上角和右下角坐标)转换为 xywh 格式(中心点坐标和宽高)。
box = xyxy2xywh(predn[:, :4]) # xywh
# 将 xywh 格式中的中心点坐标转换回左上角坐标。这是通过从中心点坐标减去宽高的一半来实现的。
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
# 遍历 predn 和 box 的列表形式, p 代表预测结果中的一行, b 代表对应的转换后的坐标。
for p, b in zip(predn.tolist(), box.tolist()):
# 将每个预测结果添加到 jdict 列表中。每个结果都是一个字典,包含以下键值对。
jdict.append({
# 图像 ID,从 path 参数中提取。
'image_id': image_id,
# 类别 ID,通过 class_map 映射从类别索引转换而来。
'category_id': class_map[int(p[5])],
# 边界框坐标,四舍五入到三位小数。
'bbox': [round(x, 3) for x in b],
# 置信度,四舍五入到五位小数。
'score': round(p[4], 5)})
# 这个函数将检测结果转换为 JSON 格式,每个结果包含图像 ID、类别 ID、边界框坐标和置信度。边界框坐标从 xyxy 格式转换为 xywh 格式,然后转换为左上角坐标。结果被添加到一个列表中,该列表可以被进一步处理或保存为 JSON 文件。
3.def process_batch(detections, labels, iouv):
# 这段代码定义了一个名为 process_batch 的函数,它用于处理一批检测结果,并根据交并比(IoU)和类别匹配来确定检测是否正确。
# 这行代码定义了一个函数 process_batch ,它接受三个参数。
# 1.detections :检测结果,通常是一个包含检测框坐标、置信度和类别的张量。
# 2.labels :真实标签,包含真实框的坐标和类别。
# 3.iouv :IoU阈值,一个张量,包含不同的IoU匹配阈值。
def process_batch(detections, labels, iouv):
# 返回正确的预测矩阵。
"""
Return correct prediction matrix
Arguments:
detections (array[N, 6]), x1, y1, x2, y2, conf, class
labels (array[M, 5]), class, x1, y1, x2, y2
Returns:
correct (array[N, 10]), for 10 IoU levels
"""
# 初始化一个布尔类型的数组 correct ,其形状由检测结果的数量和IoU阈值的数量决定。 correct 数组用于标记每个检测是否正确。
correct = np.zeros((detections.shape[0], iouv.shape[0])).astype(bool)
# 计算真实框和检测框之间的交并比(IoU)。 box_iou 函数(未在代码中给出)应该执行这个计算。 labels[:, 1:] 表示真实框的坐标, detections[:, :4] 表示检测框的坐标。
# def box_iou(box1, box2, eps=1e-7): -> 用于计算两组边界框之间的交并比(IoU)。 -> return inter / ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter + eps)
iou = box_iou(labels[:, 1:], detections[:, :4])
# 检查检测结果的类别是否与真实标签的类别匹配。
correct_class = labels[:, 0:1] == detections[:, 5]
# 遍历每个IoU阈值。
for i in range(len(iouv)):
# 找到满足IoU大于等于当前阈值且类别匹配的索引。
# torch.where 函数接受一个条件(一个布尔张量),并返回一个新的张量,其中条件为 True 的位置被设置为输入张量的对应值,条件为 False 的位置被设置为另一个值(默认为0)。但是,当使用 torch.where 时,如果没有指定第二个和第三个参数,它将返回满足条件的元素的索引。
# (iou >= iouv[i]) :这是一个元素级别的比较操作,返回一个布尔张量,表示 iou 中的每个元素是否大于或等于 iouv[i] 。
# & correct_class :这是对上述布尔张量与 correct_class 张量进行逻辑与操作,结果是一个布尔张量,表示同时满足 IoU 大于等于阈值和类别匹配的条件。
# torch.where((iou >= iouv[i]) & correct_class) :这里 torch.where 将返回一个元组,包含满足条件的元素的索引。 对于二维张量,它会返回两个张量 :第一个张量包含行索引,第二个张量包含列索引。
# 因此, x 实际上是一个元组,其中包含两个张量 : x[0] 是满足条件的行索引(对应于 labels ), x[1] 是满足条件的列索引(对应于 detections )。这样,你就可以使用这些索引来访问原始 labels 和 detections 张量中满足条件的元素。
x = torch.where((iou >= iouv[i]) & correct_class) # IoU > threshold and classes match
# 检查是否有满足条件的检测结果。
if x[0].shape[0]:
# 将满足条件的索引和对应的IoU值合并成一个数组 matches ,然后转换为NumPy数组。
# torch.stack(x, 1) : x 是一个包含两个元素的元组, x[0] 和 x[1] 分别代表 满足条件的标签 和 检测结果的索引 。 torch.stack(x, 1) 将这两个索引堆叠成一个新维度,得到的张量形状为 (N, 2) ,其中 N 是满足条件的索引数量。
# iou[x[0], x[1]][:, None] :这部分代码从 iou 张量中选取满足条件的交并比(IoU)值, iou[x[0], x[1]] 得到的是一个一维张量,包含所有匹配的IoU值。 [:, None] 将这个一维张量增加一个新维度,使其形状变为 (N, 1) 。
# torch.cat(..., 1) : torch.cat 函数用于在指定维度上连接张量。在这里,它将上述两个张量沿着第二个维度(索引为1)连接起来,形成一个形状为 (N, 3) 的张量,其中每一行包含一个标签索引、一个检测结果索引和一个对应的IoU值。
# .cpu().numpy() :由于 iou 张量可能在GPU上(如果使用了CUDA), .cpu() 方法将其移动到CPU上。 .numpy() 方法将PyTorch张量转换为NumPy数组,以便于后续处理。
# matches 变量包含了一个形状为 (N, 3) 的NumPy数组,其中 N 是满足条件的匹配数量,每一行包含一个标签索引、一个检测结果索引和一个对应的IoU值。这个数组用于后续的匹配处理,以确定哪些检测结果是正确的。
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detect, iou]
# 如果满足条件的检测结果超过一个,执行以下操作。
if x[0].shape[0] > 1:
# 按IoU值对 matches 数组进行降序排序。
matches = matches[matches[:, 2].argsort()[::-1]]
# 去除重复的检测结果,只保留第一个出现的。
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
# matches = matches[matches[:, 2].argsort()[::-1]]
# 去除重复的真实标签,只保留第一个出现的。
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
# 在 correct 数组中标记满足条件的检测结果为正确。
correct[matches[:, 1].astype(int), i] = True
# 将 correct 数组转换为PyTorch张量,并返回。
return torch.tensor(correct, dtype=torch.bool, device=iouv.device)
# 这个函数处理一批检测结果,对于每个IoU阈值,它检查检测结果是否与真实标签匹配,并且IoU是否满足阈值。如果满足条件,该检测结果在 correct 数组中被标记为正确。最终,函数返回一个布尔张量,表示每个检测结果是否正确。
4.def run(data, weights=None, batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.7, max_det=300, task='val', device='', workers=8, single_cls=False, augment=False, verbose=False, save_txt=False, save_hybrid=False, save_conf=False, save_json=False, project=ROOT / 'runs/val', name='exp', exist_ok=False, half=True, dnn=False, min_items=0, model=None, dataloader=None, save_dir=Path(''), plots=True, callbacks=Callbacks(), compute_loss=None,):
# 这段代码定义了一个名为 run 的函数,它用于执行目标检测模型的评估过程。这个函数包含了从数据加载、模型推理、非极大值抑制(NMS)、评估指标计算到结果保存的完整流程。
# 这是一个装饰器,用于设置推理模式。
# def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
# -> 它是一个装饰器工厂,用于根据 PyTorch 版本应用不同的装饰器。根据 torch_1_9 的值,选择 torch.inference_mode 或 torch.no_grad 装饰器,并将其应用于函数 fn 。
# -> return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn)
# -> return decorate
@smart_inference_mode()
# 定义了一个名为 run 的函数,它接受多个参数以配置推理和评估过程。
# 1.data :数据集的路径或数据集对象。
# 2.weights :模型权重文件的路径。
# 3.batch_size :每批处理的图像数量。
# 4.imgsz :推理时的图像尺寸(像素)。
# 5.conf_thres :置信度阈值,低于此值的检测结果将被忽略。
# 6.iou_thres :非最大抑制(NMS)中使用的IoU阈值。
# 7.max_det :每张图像最大检测数量。
# 8.task :任务类型,可以是训练( train )、验证( val )、测试( test )、速度测试( speed )或研究( study )。
# 9.device :指定使用的CUDA设备,例如 0 或 0,1,2,3 ,或者 cpu 。
# 10.workers :数据加载器的最大工作线程数。
# 11.single_cls :是否将数据集视为单类别数据集。
# 12.augment :是否使用增强推理。
# 13.verbose :是否输出详细信息。
# 14.save_txt :是否保存结果到文本文件。
# 15.save_hybrid :是否保存标签和预测结果的混合文本文件。
# 16.save_conf :是否在保存的文本文件中包含置信度。
# 17.save_json :是否保存COCO格式的JSON结果文件。
# 18.project :保存结果的项目路径。
# 19.name :保存结果的名称。
# 20.exist_ok :如果项目/名称已存在,是否允许不增加后缀。
# 21.half :是否使用FP16半精度推理。
# 22.dnn :是否使用OpenCV DNN进行ONNX模型推理。
# 23.min_items :实验性参数,用于控制数据加载的最小项数。
# 24.model :可选的模型对象,如果提供,则不会加载模型权重。
# 25.dataloader :可选的数据加载器对象,如果提供,则不会创建新的数据加载器。
# 26.save_dir :保存结果的目录。
# 27.plots :是否绘制图像。
# 28.callbacks :回调函数对象,用于在推理过程中执行额外的操作。
# 29.compute_loss :可选的损失计算函数,如果提供,则会在推理过程中计算损失。
def run(
data,
weights=None, # model.pt path(s)
batch_size=32, # batch size
imgsz=640, # inference size (pixels)
conf_thres=0.001, # confidence threshold
iou_thres=0.7, # NMS IoU threshold
max_det=300, # maximum detections per image
task='val', # train, val, test, speed or study
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
workers=8, # max dataloader workers (per RANK in DDP mode)
single_cls=False, # treat as single-class dataset
augment=False, # augmented inference
verbose=False, # verbose output
save_txt=False, # save results to *.txt
save_hybrid=False, # save label+prediction hybrid results to *.txt
save_conf=False, # save confidences in --save-txt labels
save_json=False, # save a COCO-JSON results file
project=ROOT / 'runs/val', # save to project/name
name='exp', # save to project/name
exist_ok=False, # existing project/name ok, do not increment
half=True, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference
min_items=0, # Experimental
model=None,
dataloader=None,
save_dir=Path(''),
plots=True,
callbacks=Callbacks(),
compute_loss=None,
):
# 这段代码是函数 run 中的一部分,它负责初始化或加载模型,并设置设备(CPU或GPU)。
# Initialize/load model and set device
# 检查 model 参数是否为 None 。如果 model 参数被提供(即不是 None ),则认为这是一个训练过程, training 变量被设置为 True 。否则, training 为 False ,表示直接调用评估或推理过程。
training = model is not None
# 条件判断,如果 training 为 True ,则执行冒号后面的代码块。这意味着代码被 train.py 调用,即训练过程中。
if training: # called by train.py
# 这一行代码做了几件事情 :
# next(model.parameters()).device 获取模型的第一个参数所在的设备(CPU或GPU),并将其赋值给 device 变量。
# pt 被设置为 True ,表示这是一个PyTorch模型。
# jit 和 engine 被设置为 False ,分别表示不使用TorchScript的JIT编译和不使用TorchScript的AOT编译。
device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
# 一个位运算, &= 用于更新 half 变量的值。如果 device.type 不是 'cpu' (即在CUDA环境下),则 half 保持 True ,表示可以使用半精度(FP16)进行推理。如果在CPU上,则 half 被设置为 False ,因为半精度仅在CUDA环境下支持。
half &= device.type != 'cpu' # half precision only supported on CUDA
# 根据 half 变量的值决定模型的精度。如果 half 为 True ,则调用 model.half() 将模型转换为半精度(FP16)。如果 half 为 False ,则调用 model.float() 将模型转换为单精度(FP32)。
model.half() if half else model.float()
# 这是 if training 条件判断的 else 部分,如果 training 为 False ,则执行这里的代码块。这意味着函数被直接调用,而不是由 train.py 调用。
else: # called directly
# 如果不是训练过程,调用 select_device 函数来选择设备。这个函数根据提供的 device 参数和 batch_size 来决定最终使用的设备。如果 device 参数为空,它将自动选择一个可用的GPU或CPU。
# def select_device(device='', batch_size=0, newline=True):
# -> 根据用户提供的参数选择使用 CPU、单个 GPU 或多个 GPU,并返回一个对应的 PyTorch 设备对象。返回一个 PyTorch 设备对象,用于指定后续计算应该在哪个设备上执行。
# -> return torch.device(arg)
device = select_device(device, batch_size=batch_size)
# 这段代码负责根据是否是训练过程来设置模型和设备。如果是训练过程,它会从模型中获取设备信息,并根据设备类型决定是否使用半精度。如果不是训练过程,它会调用 select_device 函数来自动选择设备。这样的设计使得函数可以在训练和推理/评估之间灵活切换,同时确保模型的精度设置与硬件环境相匹配。
# 这段代码是函数 run 中负责设置和创建目录的部分,用于保存推理或评估过程中生成的文件。
# Directories
# 使用 increment_path 函数来创建或获取一个唯一的 save_dir 路径。 Path(project) / name 组合了项目路径和实验名称,形成保存结果的基础路径。
# increment_path 函数会在基础路径后添加一个数字后缀,以确保路径是唯一的,避免覆盖已有的实验结果。例如,如果 save_dir 是 runs/val/exp ,并且该目录已存在,那么新的目录可能会被命名为 runs/val/exp_1 。
# exist_ok 参数控制如果路径已存在时的行为。如果 exist_ok 为 True ,则不会抛出异常,即使目录已存在也会被认为是正常的。
# def increment_path(path, exist_ok=False, sep='', mkdir=False): -> 为文件或目录生成一个新的路径名,如果原始路径已经存在,则通过在路径后面添加一个数字(默认从2开始递增)来创建一个新的路径。函数返回最终的路径 path 。 -> return path
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
# 负责创建目录。它首先根据 save_txt 参数的值决定是创建 save_dir 下的 labels 子目录,还是直接创建 save_dir 目录。
# 如果 save_txt 为 True ,则创建 save_dir 下的 labels 子目录,用于保存文本格式的结果文件。如果 save_txt 为 False ,则直接创建 save_dir 目录。
# mkdir 函数的 parents=True 参数表示如果父目录不存在,则一并创建。 exist_ok=True 参数表示如果目录已存在,不会抛出异常。
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# 这段代码负责设置和创建保存结果的目录。它通过 increment_path 函数确保目录的唯一性,避免覆盖旧的实验结果。同时,根据 save_txt 参数决定是否创建 labels 子目录,以便于组织和管理不同格式的结果文件。这样的设计使得结果文件的保存更加灵活和有序。
# 这段代码是函数 run 中负责加载模型和设置相关参数的部分。
# Load model
# 使用 DetectMultiBackend 类来加载模型。这个类支持多种后端,可以根据提供的权重文件 weights 和设备 device 来加载模型。
# dnn 参数用于指定是否使用OpenCV的DNN模块进行ONNX模型的推理。 data 参数提供了数据集的信息,可能用于模型的特定配置。 fp16=half 参数指示是否使用半精度(FP16)进行推理,这在支持的CUDA后端上可以提高性能。
# class DetectMultiBackend(nn.Module):
# -> DetectMultiBackend 类实现了一个多后端检测模型,能够支持多种不同的模型格式和推理引擎。
# -> def __init__(self, weights='yolo.pt', device=torch.device('cpu'), dnn=False, data=None, fp16=False, fuse=True):
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
# 从加载的模型中提取几个属性 : stride 模型的步长,用于调整图像尺寸。 pt 指示模型是否是PyTorch模型。 jit 指示模型是否是TorchScript的JIT编译模型。 engine 指示模型是否是TorchScript的AOT编译模型。
stride, pt, jit, engine = model.stride, model.pt, model.jit, model.engine
# 调用 check_img_size 函数来检查和调整 imgsz (推理时的图像尺寸)。函数会确保图像尺寸是模型步长的倍数。
# def check_img_size(imgsz, s=32, floor=0): -> 验证图像尺寸是否是某个步长的倍数。返回新尺寸。函数返回新计算的尺寸。 -> return new_size
imgsz = check_img_size(imgsz, s=stride) # check image size
# 更新 half 变量的值,以反映模型实际支持的精度。如果模型支持FP16并且运行在CUDA上,则 half 保持 True ;否则,设置为 False 。
half = model.fp16 # FP16 supported on limited backends with CUDA
# 条件判断,如果模型是TorchScript的AOT编译模型( engine 为 True ),则执行冒号后面的代码块。
if engine:
# 如果模型是TorchScript的AOT编译模型,使用模型自带的 batch_size 属性。
batch_size = model.batch_size
# 这是 if engine 条件判断的 else 部分,如果模型不是TorchScript的AOT编译模型,则执行这里的代码块。
else:
# 如果模型不是AOT编译模型,更新 device 变量为模型实际使用的设备。
device = model.device
# 条件判断,如果模型既不是PyTorch模型也不是JIT编译模型,则执行冒号后面的代码块。
if not (pt or jit):
# 如果模型既不是PyTorch模型也不是JIT编译模型,设置 batch_size 为1,因为这些模型默认使用单张图像推理。
batch_size = 1 # export.py models default to batch-size 1
# 记录一条信息,表明对于非PyTorch模型,强制使用单张图像的推理模式。
LOGGER.info(f'Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models') # 强制对非 PyTorch 模型进行 --batch-size 1 平方推理 (1,3,{imgsz},{imgsz}) 。
# Data
# 调用 check_dataset 函数来验证和可能地调整 data 参数,确保数据集配置正确。
# def check_dataset(data, autodownload=True): -> 检查、下载(如果需要)并解压数据集,确保数据集在本地可用。返回包含数据集信息的字典。 -> return data # dictionary
data = check_dataset(data) # check
# 这段代码负责加载模型,并根据模型的类型和属性设置相关的参数,如图像尺寸、精度和批量大小。它还确保数据集配置正确,并在必要时调整批量大小和设备设置。这样的设计使得函数能够灵活地处理不同类型的模型和数据集。
# 这段代码是函数 run 中负责配置模型和评估参数的部分。
# Configure
# 将模型设置为评估模式。在PyTorch中, .eval() 方法用于将模型的某些层(如Dropout层和BatchNorm层)设置为评估模式,这对于获得准确的推理结果非常重要。
model.eval()
# 检查设备类型是否不是CPU。如果设备是CUDA(GPU),则 cuda 变量被设置为 True ,否则为 False 。这个变量通常用于确定是否需要将数据传输到GPU。
cuda = device.type != 'cpu'
#is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'coco{os.sep}val2017.txt') # COCO dataset
# 检查当前数据集是否是COCO数据集。它通过检查 data 字典中 'val' 键对应的值是否为字符串,并且该字符串是否以 'val2017.txt' 结尾来判断。如果是,说明数据集是COCO数据集的验证集。
is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'val2017.txt') # COCO dataset
# 设置类别数 nc 。如果 single_cls 参数为 True ,则设置类别数为1,表示单类别数据集。否则,从 data 字典中获取 'nc' 键对应的值,并将其转换为整数,作为类别数。
nc = 1 if single_cls else int(data['nc']) # number of classes
# 创建一个从0.5到0.95的等差数列,包含10个元素,并将这些值存储在 iouv 变量中。这些值用于计算不同IoU阈值下的mAP(平均精度均值)。 device 参数确保这些值被创建在正确的设备上(CPU或GPU)。
iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for [email protected]:0.95
# 计算 iouv 向量中的元素数量,并将其存储在 niou 变量中。 numel() 方法是PyTorch中的一个函数,用于返回张量中的元素总数。
niou = iouv.numel()
# 这段代码负责将模型设置为评估模式,并根据数据集和任务配置相关的评估参数。它检查数据集是否为COCO数据集,并设置类别数和IoU阈值向量,这些参数对于后续的评估和性能计算非常重要。通过这些配置,函数能够适应不同的数据集和评估需求。
# 这段代码是函数 run 中负责创建数据加载器(Dataloader)的部分。
# Dataloader
# 条件判断,如果当前不是训练过程( training 为 False ),则执行冒号后面的代码块。
if not training:
# 嵌套的条件判断,如果模型是PyTorch模型( pt 为 True )且不是单类别数据集( single_cls 为 False ),则执行冒号后面的代码块。
if pt and not single_cls: # check --weights are trained on --data
# 从模型中获取类别数,并将其存储在 ncm 变量中。
ncm = model.model.nc
# 使用 assert 语句来确保 模型训练时使用的类别数 ( ncm )与 当前数据集的类别数 ( nc )相匹配。如果不相等,会抛出异常,并提示用户传递的权重文件和数据集不匹配。
assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \
f'classes). Pass correct combination of --weights and --data that are trained together.' # {weights} ({ncm} 类) 训练的 --data 与您传递的 ({nc} 类) 不同。传递一起训练的 --weights 和 --data 的正确组合。
# 调用 model.warmup 方法来预热模型。 imgsz 参数指定了预热时使用的图像尺寸。如果是PyTorch模型( pt 为 True ),则使用单张图像的尺寸;否则,使用批量大小。
model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup
# 根据任务类型设置填充( pad )和矩形推理( rect )参数。如果是速度测试( task 为 'speed' ),则不进行填充且不使用矩形推理;否则,使用0.5的填充和模型是否为PyTorch模型来决定是否使用矩形推理。
pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks
# 确保 task 变量的值是 'train' 、 'val' 或 'test' 之一。如果不是,将 task 设置为 'val' 。
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
# 开始创建数据加载器, data[task] 指定了任务类型对应的数据集。
dataloader = create_dataloader(data[task],
# 指定了推理时的图像尺寸。
imgsz,
# 指定了批量大小。
batch_size,
# 指定了模型的步长。
stride,
# 指定是否是单类别数据集。
single_cls,
# 指定了填充参数。
pad=pad,
# 指定了矩形推理参数。
rect=rect,
# 指定了数据加载器的工作线程数。
workers=workers,
# 指定了实验性参数 min_items ,用于控制数据加载的最小项数。
min_items=opt.min_items,
# 为数据加载器设置了前缀,用于显示任务类型。
# [0] :从 create_dataloader 函数返回的结果中获取第一个元素,即数据加载器。
prefix=colorstr(f'{task}: '))[0]
# 这段代码负责在非训练过程中创建数据加载器,确保模型预热,并根据任务类型和数据集配置相关的参数。它还检查权重文件和数据集的匹配性,并根据需要调整填充和矩形推理参数。通过这些配置,函数能够为不同的任务类型和数据集提供适当的数据加载器。
# 这段代码是函数 run 中负责初始化评估过程中使用的变量和设置进度条的部分。
# 初始化一个变量 seen ,用于记录处理的图像数量。
seen = 0
# 创建一个 ConfusionMatrix 对象,用于跟踪分类任务中的混淆矩阵, nc 参数指定了类别数。
# class ConfusionMatrix:
# -> 用于在目标检测任务中计算和处理混淆矩阵。
# -> def __init__(self, nc, conf=0.25, iou_thres=0.45):
confusion_matrix = ConfusionMatrix(nc=nc)
# 尝试从模型中获取类别名称。如果模型有 names 属性,则使用它;如果没有,则尝试从 model.module.names 获取。
names = model.names if hasattr(model, 'names') else model.module.names # get class names
# 检查 names 是否是列表或元组(旧格式)。
if isinstance(names, (list, tuple)): # old format
# 如果 names 是列表或元组,将其转换为字典,其中索引作为键,类别名称作为值。
names = dict(enumerate(names))
# 如果数据集是COCO数据集,创建一个从COCO 2014年的80类映射到2017年的91类的映射表;否则,创建一个包含1000个类的列表。
# def coco80_to_coco91_class():
# -> 将 COCO 数据集中的 80 个类别索引(用于验证 2014 数据集)转换为论文中使用的 91 个类别索引。返回一个列表,包含从 1 到 90 的整数,其中每个整数代表 COCO 数据集中的一个类别索引。
# -> return [
# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34,
# 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
# 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90]
class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
# 格式化一个字符串 s ,用于进度条的描述,包含 类别 、 图像数 、 实例数 、 精确度(P) 、 召回率(R) 、 50% IoU下的mAP 和 50-95% IoU下的mAP 。
s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP50', 'mAP50-95')
# 初始化一系列变量,用于存储评估指标,包括 真正例(tp) 、 假正例(fp) 、 精确度(p) 、 召回率(r) 、 F1分数(f1) 、 平均精确度(mp) 、 平均召回率(mr) 、 50% IoU下的mAP(map50) 、 50% IoU下的AP(ap50) 和 50-95% IoU下的mAP(map) 。
tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
# 创建三个 Profile 对象,用于性能分析,记录 不同阶段的时间 。
# class Profile(contextlib.ContextDecorator):
# -> 这个类可以用来测量代码块的执行时间,既可以作为一个装饰器使用,也可以作为一个上下文管理器使用。
# -> def __init__(self, t=0.0):
dt = Profile(), Profile(), Profile() # profiling times
# 初始化一个长度为3的零张量,用于存储损 失值 , device 参数确保张量在正确的设备上创建。
loss = torch.zeros(3, device=device)
# 初始化四个空列表,用于存储 JSON字典 、 统计数据 、 AP值 和 每个类别的AP值 。
jdict, stats, ap, ap_class = [], [], [], []
# 调用回调函数,通知开始验证过程。
callbacks.run('on_val_start')
# 创建一个 tqdm 进度条,用于显示评估过程的进度。 dataloader 是数据加载器, desc 是进度条的描述, bar_format 是进度条的格式。
pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar
# 这段代码负责初始化评估过程中需要的各种变量和进度条,确保评估指标和性能分析工具准备就绪,并开始评估过程的回调。这样的设计使得评估过程更加结构化和易于跟踪。
# 这段代码是函数 run 中负责处理数据加载器中的每个批次数据的部分。
# 开始一个循环,遍历由 pbar (即 tqdm 进度条包装的数据加载器)提供的每个批次数据。 batch_i 是批次的索引, im 是图像数据, targets 是对应的目标(标签), paths 是图像的路径, shapes 是图像的原始尺寸。
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
# 在处理每个批次之前,调用回调函数 on_val_batch_start ,这可以用于执行一些在每个批次开始时需要的操作,比如记录时间或者打印日志。
callbacks.run('on_val_batch_start')
# 使用 with 语句来测量代码块的执行时间。 dt[0] 是之前创建的 Profile 对象之一,用于性能分析。
with dt[0]:
# 条件判断,如果 cuda 变量为 True (即正在使用GPU),则执行冒号后面的代码块。
if cuda:
# 将图像数据 im 传输到指定的设备(GPU或CPU)上, non_blocking=True 参数表示如果可能,操作会是非阻塞的。
im = im.to(device, non_blocking=True)
# 将目标(标签)数据 targets 也传输到指定的设备上。
targets = targets.to(device)
# 如果 half 变量为 True (即使用半精度FP16),则将图像数据 im 转换为半精度浮点数;否则,转换为单精度浮点数(FP32)。这也将图像数据从原始的无符号8位整数(uint8)转换为浮点数。
im = im.half() if half else im.float() # uint8 to fp16/32
# 将图像数据 im 的像素值从[0, 255]范围归一化到[0.0, 1.0]范围。
im /= 255 # 0 - 255 to 0.0 - 1.0
# 提取图像数据 im 的形状信息, nb 是批次大小, _ 是通道数(通常为3), height 是图像高度, width 是图像宽度。
nb, _, height, width = im.shape # batch size, channels, height, width
# 这段代码负责在每个批次开始时执行一系列操作,包括调用回调函数、将数据传输到正确的设备、数据类型转换和归一化,以及提取图像的形状信息。这些步骤是模型推理前的标准预处理步骤,确保数据以正确的格式和范围输入到模型中。
# 这段代码是函数 run 中负责模型推理和(如果需要)计算损失的部分。
# Inference
# 使用 with 语句来测量代码块的执行时间。 dt[1] 是之前创建的 Profile 对象之一,用于性能分析,这里特别用于记录推理过程的时间。
with dt[1]:
# 执行模型推理。
# 如果 compute_loss 参数为 True ,意味着我们需要计算损失,所以直接调用 model(im) 获取预测结果 preds 和用于损失计算的输出 train_out 。
# 如果 compute_loss 为 False ,不需要计算损失,调用 model(im, augment=augment) 进行推理,并将 augment 参数传递给模型,以便进行增强推理(如果 augment 为 True )。在这种情况下, train_out 被设置为 None ,因为我们不需要计算损失。
preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
# Loss
# 条件判断,如果 compute_loss 参数为 True ,则执行冒号后面的代码块。
if compute_loss:
# 如果需要计算损失,调用 compute_loss 函数,传入 模型输出 train_out 和 目标 targets ,计算损失值。 compute_loss 函数返回一个包含多个损失组成部分的元组,这里使用索引 [1] 来获取特定的损失值(例如,可能是分类损失、目标损失和框损失)。然后,将计算出的损失值累加到 loss 变量中。
loss += compute_loss(train_out, targets)[1] # box, obj, cls
# 这段代码负责执行模型的推理过程,并在需要时计算损失。通过使用 Profile 对象,它还测量并记录推理过程的时间,这对于性能分析非常有用。如果 compute_loss 为 True ,它还会计算和累加损失值,这对于训练和评估模型的性能至关重要。
# 这段代码是函数 run 中负责执行非最大抑制(Non-Maximum Suppression, NMS)的部分。
# NMS
# 将目标(标签)中的边界框坐标从归一化值转换为 像素值 。 targets[:, 2:] 表示目标张量中每个边界框的坐标(x, y, x, y)部分。这些坐标被乘以图像的宽度和高度( width , height ),以将它们从相对于图像尺寸的比例转换为 实际的像素坐标 。 torch.tensor((width, height, width, height), device=device) 创建一个包含这些尺寸的张量,并确保它位于正确的设备(GPU或CPU)上。
targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels
# 创建一个列表 lb ,其中包含每个图像的目标标签,用于混合标签和预测结果的保存(如果 save_hybrid 为 True )。对于每个图像(由 range(nb) 遍历),它从 targets 中选择对应图像的标签,并排除第一个元素(通常是类别ID),只保留边界框和其他相关信息。
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
# 使用 with 语句来测量代码块的执行时间。 dt[2] 是之前创建的 Profile 对象之一,用于性能分析,这里特别用于记录NMS过程的时间。
with dt[2]:
# 调用 non_max_suppression 函数,对模型的预测结果执行NMS。NMS是一种用于去除重叠边界框的技术,以保留最佳的边界框并去除不必要的预测。
# def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False, labels=(), max_det=300, nm=0,):
# -> 用于在目标检测任务中执行非极大值抑制(Non-Maximum Suppression, NMS),以去除多余的边界框,只保留最佳的检测结果。函数返回最终的输出列表 output ,其中包含了批量中每个图像经过NMS处理后的检测结果。
# -> return output
preds = non_max_suppression(preds,
# 指定了置信度阈值 conf_thres ,只有高于这个阈值的预测才会被考虑。
conf_thres,
# 指定了IoU(交并比)阈值 iou_thres ,用于确定何时认为两个边界框重叠过多。
iou_thres,
# 如果 save_hybrid 为 True ,将 lb (包含真实标签的列表)传递给NMS函数,用于后续的混合标签和预测结果的保存。
labels=lb,
# 指示NMS函数,模型预测的是多标签问题,即一个边界框可以属于多个类别。
multi_label=True,
# 指示NMS函数,如果模型是单类别模型( single_cls 为 True ),则在NMS中不考虑类别ID。
agnostic=single_cls,
# 指定了每个图像最大检测数量 max_det ,超过这个数量的预测将被忽略。
max_det=max_det)
# 这段代码负责将模型的预测结果通过NMS处理,以去除不必要的重叠边界框,并保留最佳的预测。它还考虑了将真实标签与预测结果一起保存的情况,并测量了NMS过程的时间。这些步骤对于提高模型输出的准确性和减少冗余预测至关重要。
# 这段代码是函数 run 中负责计算评估指标的部分,特别是在非最大抑制(NMS)之后计算精确度、召回率等指标。
# Metrics
# 开始一个循环,遍历 preds 中的每个预测结果。 si 是索引,表示当前处理的是第几张图像的预测结果; pred 是该图像的预测结果。
for si, pred in enumerate(preds):
# 从 targets 中提取当前图像 si 的真实标签。 targets[:, 0] == si 选出所有属于当前图像的标签, 1: 表示排除第一个元素(通常是类别ID),只保留边界框和其他相关信息。
labels = targets[targets[:, 0] == si, 1:]
# 获取当前图像的 真实标签数量 nl 和 预测结果数量 npr 。
nl, npr = labels.shape[0], pred.shape[0] # number of labels, predictions
# 获取 当前图像的路径 path 和 原始尺寸 shape 。
path, shape = Path(paths[si]), shapes[si][0]
# 初始化一个布尔张量 correct ,用于记录每个预测是否正确。 npr 是预测结果的数量, niou 是IoU阈值的数量。
correct = torch.zeros(npr, niou, dtype=torch.bool, device=device) # init
# 增加 seen 计数器,表示已经处理了一个图像。
seen += 1
# 条件判断,如果当前图像没有预测结果( npr 为0),则执行冒号后面的代码块。
if npr == 0:
# 条件判断,如果当前图像有真实标签( nl 为0),则执行冒号后面的代码块。
if nl:
# 如果没有预测结果但有真实标签,将 correct 张量和其他一些零张量(用于占位)以及真实标签的类别ID添加到 stats 列表中。
stats.append((correct, *torch.zeros((2, 0), device=device), labels[:, 0]))
# 条件判断,如果 plots 为 True ,表示需要绘制混淆矩阵或其他可视化图表。
if plots:
# 如果需要绘制图表,调用 confusion_matrix.process_batch 方法,传入真实标签的类别ID,用于更新混淆矩阵。由于没有检测结果,所以 detections 参数为 None 。
# def process_batch(self, detections, labels): -> 用于处理一批检测结果和对应的标签,更新混淆矩阵。
confusion_matrix.process_batch(detections=None, labels=labels[:, 0])
# 如果当前图像没有预测结果,这一行代码将跳过当前循环的剩余部分,继续处理下一个图像。
continue
# 这段代码负责在NMS之后计算评估指标。它遍历每个图像的预测结果,提取真实标签,并初始化用于记录正确预测的张量。如果某个图像没有预测结果,它会将相关信息添加到统计列表中,并更新混淆矩阵。这些步骤对于评估模型的性能至关重要。
# 这段代码是函数 run 中负责处理模型预测结果的部分,特别是在计算评估指标之前对预测的边界框进行调整。
# Predictions
# 条件判断,如果 single_cls 为 True ,表示数据集是单类别的。
if single_cls:
# 如果是单类别数据集,将预测结果 pred 中第六列的所有值设置为0。通常,预测结果中的第六列代表类别ID,如果是单类别数据集,这个值可以被忽略或设置为0。
pred[:, 5] = 0
# 创建了预测结果 pred 的一个副本,并将其存储在 predn 中。这样做是为了在不修改原始预测结果的情况下,对预测的边界框进行调整。
predn = pred.clone()
# 调用 scale_boxes 函数,将 预测的边界框 从模型输入的归一化空间转换回 原始图像空间 。
# im[si].shape[1:] 获取第 si 张图像的高度和宽度。 predn[:, :4] 获取 predn 中每个边界框的四个坐标。 shape 原始图像的尺寸。 shapes[si][1] 原始图像的尺寸比例因子,用于将边界框坐标从归一化空间转换回原始空间。
# def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None): -> 用于调整边界框(boxes)的大小,使其适应新的图像尺寸。返回重新缩放后的边界框数组。 -> return boxes
scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
# 这段代码负责在评估指标计算之前,对模型的预测结果进行必要的调整。如果是单类别数据集,它会将类别ID列设置为0。然后,它会克隆预测结果并使用 scale_boxes 函数将边界框坐标从归一化空间转换回原始图像空间,以便与真实标签进行比较。这些步骤对于确保评估指标的准确性至关重要。
# 这段代码是函数 run 中负责评估模型预测性能的部分,其中包括将预测结果与真实标签进行比较,并更新混淆矩阵。
# Evaluate
# 条件判断,如果 nl (当前图像的真实标签数量)大于0,则执行冒号后面的代码块。
if nl:
# 将真实标签中的边界框从 xywh 格式(x中心点,y中心点,宽度,高度)转换为 xyxy 格式(左上角x,左上角y,右下角x,右下角y)。
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
# 调用 scale_boxes 函数,将 真实标签的边界框 从模型输入的归一化空间转换回原始图像空间。
scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
# 将转换后的边界框 tbox 与类别ID( labels[:, 0:1] )合并,形成完整的真实标签信息 labelsn 。
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
# 调用 process_batch 函数,将预测结果 predn 与真实标签 labelsn 进行比较,计算每个预测是否正确,并返回一个布尔张量 correct ,其中每个元素表示对应预测的准确性。
# def process_batch(detections, labels, iouv): -> 用于处理一批检测结果,并根据交并比(IoU)和类别匹配来确定检测是否正确。将 correct 数组转换为PyTorch张量,并返回。 -> return torch.tensor(correct, dtype=torch.bool, device=iouv.device)
correct = process_batch(predn, labelsn, iouv)
# 条件判断,如果 plots 为 True ,表示需要绘制混淆矩阵或其他可视化图表。
if plots:
# 如果需要绘制图表,调用 confusion_matrix.process_batch 方法,传入预测结果 predn 和真实标签 labelsn ,用于更新混淆矩阵。
# def process_batch(self, detections, labels): -> 用于处理一批检测结果和对应的标签,更新混淆矩阵。
confusion_matrix.process_batch(predn, labelsn)
# 将评估结果添加到 stats 列表中。 correct 是预测正确的布尔张量, pred[:, 4] 是预测的置信度, pred[:, 5] 是预测的类别ID, labels[:, 0] 是真实标签的类别ID。
stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0])) # (correct, conf, pcls, tcls)
# 这段代码负责在NMS之后评估模型的预测性能。它将预测结果与真实标签进行比较,计算每个预测的准确性,并更新混淆矩阵。这些步骤对于评估模型的性能至关重要,因为它们提供了精确度、召回率和mAP等关键指标的基础数据。
# 这段代码是函数 run 中负责将模型的预测结果保存到文件和调用回调函数的部分。
# Save/log
# 条件判断,如果 save_txt 为 True ,表示需要将预测结果保存为文本文件。
if save_txt:
# 如果需要保存文本文件,调用 save_one_txt 函数,将 预测结果 predn 、 置信度 save_conf 、 图像原始尺寸 shape 以及 文件路径(由 save_dir 、 'labels' 子目录和文件名组成)作为参数传递。文件名由图像路径 path 的文件名( stem )组成。
# def save_one_txt(predn, save_conf, shape, file): -> 用于将检测结果保存到一个文本文件中。
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / f'{path.stem}.txt')
# 条件判断,如果 save_json 为 True ,表示需要将预测结果保存为COCO格式的JSON文件。
if save_json:
# 如果需要保存JSON文件,调用 save_one_json 函数,将 预测结果 predn 、 用于存储COCO-JSON字典的 jdict 、 图像路径 path 和 类别映射 class_map 作为参数传递。这个函数将预测结果追加到COCO-JSON字典中,以便后续保存为JSON文件。
# def save_one_json(predn, jdict, path, class_map): -> 用于将检测结果保存到一个 JSON 格式的数据结构中。
save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
# 调用回调函数 on_val_image_end ,通知每个图像的评估结束。传递给回调函数的参数包括原始预测结果 pred 、调整后的预测结果 predn 、图像路径 path 、类别名称 names 和图像数据 im[si] 。
callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
# 这段代码负责在模型评估过程中将预测结果保存到文本文件和COCO-JSON文件,并在每个图像评估结束时调用回调函数。这些操作有助于记录预测结果,便于后续分析和验证,同时也允许用户通过回调函数执行自定义操作,如记录日志、执行额外的评估或触发某些事件。
# 这段代码是函数 run 中负责绘制图像和调用回调函数的部分,用于可视化标签和预测结果。
# Plot images
# 条件判断,如果 plots 为 True 且当前批次索引 batch_i 小于3,即仅对前3个批次的图像进行可视化。
if plots and batch_i < 3:
# 如果条件满足,调用 plot_images 函数,绘制包含真实标签的图像。参数包括 图像数据 im 、 真实标签 targets 、 图像路径 paths 、 保存路径(由 save_dir 和文件名组成)以及 类别名称 names 。文件名格式为 val_batchX_labels.jpg ,其中 X 是批次索引。
# def plot_images(images, targets, paths=None, fname='images.jpg', names=None): -> 用于将图像和目标(例如检测框和标签)绘制到一个网格中,并保存为一个图片文件。
plot_images(im, targets, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names) # labels
# 再次调用 plot_images 函数,绘制包含模型预测结果的图像。参数与上一行类似,但真实标签 targets 被替换为预测结果 preds 。文件名格式为 val_batchX_pred.jpg ,其中 X 是批次索引。
# def output_to_target(output, max_det=300): -> 将模型的输出转换为绘图所需的目标格式。将 targets 列表中的所有张量连接起来,并转换为 NumPy 数组,然后返回。 -> return torch.cat(targets, 0).numpy()
plot_images(im, output_to_target(preds), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred
# 调用回调函数 on_val_batch_end ,通知每个批次的评估结束。传递给回调函数的参数包括当前批次索引 batch_i 、图像数据 im 、真实标签 targets 、图像路径 paths 、图像原始尺寸 shapes 和预测结果 preds 。
callbacks.run('on_val_batch_end', batch_i, im, targets, paths, shapes, preds)
# 这段代码负责在模型评估过程中对前3个批次的图像进行可视化,将包含真实标签和预测结果的图像保存到指定目录。同时,在每个批次评估结束时调用回调函数,允许用户执行自定义操作,如记录日志、执行额外的评估或触发某些事件。这些步骤有助于用户直观地理解模型的预测性能,并提供了灵活的回调机制以适应不同的评估需求。
# 这段代码是函数 run 中负责计算和汇总评估指标的部分。
# Compute metrics
# 将 stats 列表中的所有子列表(每个子列表包含一个批次的评估结果)合并,并转换为NumPy数组。 zip(*stats) 将 stats 列表“转置”,使得相同位置的元素被组合在一起,形成一个元组列表。 torch.cat(x, 0) 将这些元组中的张量沿着第一个维度(0)连接起来, .cpu() 确保数据在CPU上, .numpy() 将PyTorch张量转换为NumPy数组。
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
# 条件判断,检查 stats 列表不为空,并且第一个元素(代表正确预测的布尔张量)中至少有一个 True 值。这确保了在没有预测结果的情况下不会执行评估计算。
if len(stats) and stats[0].any():
# 如果条件满足,调用 ap_per_class 函数,计算每个类别的 平均精度(AP) 和其他评估指标。 *stats 将 stats 列表中的元素作为参数传递给函数。 plot 、 save_dir 和 names 也是传递给函数的参数,用于控制是否绘制图表、保存图表的位置和类别名称。
# def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16, prefix=""):
# -> 用于计算每个类别的平均精度(AP)并绘制相关的曲线图。返回计算结果,包括 真阳性tp 、 假阳性fp 、 精确度p 、 召回率r 、 F1 分数f1 、 平均精度ap 和 类别索引unique_classes.astype(int) 。
# -> return tp, fp, p, r, f1, ap, unique_classes.astype(int)
tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
# 从 ap_per_class 函数返回的AP值中提取[email protected]( ap50 )和[email protected]:0.95( ap )。 ap[:, 0] 获取每个类别在0.5 IoU阈值下的AP值, ap.mean(1) 计算每个类别在0.5到0.95 IoU阈值范围内的AP值的平均值。
ap50, ap = ap[:, 0], ap.mean(1) # [email protected], [email protected]:0.95
# 计算整体的精确度( mp )、召回率( mr )、[email protected]( map50 )和[email protected]:0.95( map )。 p.mean() 和 r.mean() 分别计算所有类别的精确度和召回率的平均值, ap50.mean() 和 ap.mean() 分别计算[email protected]和[email protected]:0.95的平均值。
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
# np.bincount(x, minlength=None)
# np.bincount 是 NumPy 库中的一个函数,它用于计算非负整数数组中每个值的出现次数。
# 参数 :
# x :输入数组,其中的元素必须是非负整数。
# minlength (可选) :输出数组的最小长度。如果提供,数组 x 中小于 minlength 的值将被忽略,而 x 中等于或大于 minlength 的值将导致数组被扩展以包含这些值。如果未提供或为 None ,则输出数组的长度将与 x 中的最大值加一相匹配。
# 返回值 :
# 返回一个数组,其中第 i 个元素代表输入数组 x 中值 i 出现的次数。
# 功能 :
# np.bincount 函数对输入数组 x 中的每个值进行计数,返回一个一维数组,其长度至少与 x 中的最大值一样大。
# 如果 x 中的某个值没有出现,那么在返回的数组中对应的位置将为 0。
# 例:
# x = np.array([1, 2, 3, 3, 0, 1, 4])
# np.bincount(x)
# '''array([1, 2, 1, 2, 1], dtype=int64)'''
# 输出 : [1 2 1 2 1]。统计索引出现次数:索引0出现1次,1出现2次,2出现1次,3出现2次,4出现1次。
# 使用 np.bincount 函数统计 每个类别的目标数量 。 stats[3] 包含每个图像的真实标签类别ID, .astype(int) 确保这些ID是整数类型。 minlength=nc 参数确保结果数组的长度至少与类别数 nc 相同,这样可以为所有类别提供计数,即使某些类别没有目标。
nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class
# 这段代码负责将评估结果从PyTorch张量转换为NumPy数组,计算每个类别的评估指标,如精确度、召回率和平均精度,并汇总这些指标以获得整体性能。此外,它还统计了每个类别的目标数量,这对于评估模型在不同类别上的性能分布非常有用。
# 这段代码是函数 run 中负责打印评估结果和性能指标的部分。
# Print results
# 定义了一个格式化字符串 pf ,用于打印评估结果。 %22s 表示一个宽度为22的字符串字段, %11i 表示一个宽度为11的整数字段, %11.3g 表示一个宽度为11并保留3位小数的浮点数字段。
pf = '%22s' + '%11i' * 2 + '%11.3g' * 4 # print format
# 使用之前定义的格式化字符串 pf 打印整体评估结果。 'all' 表示这是整体结果, seen 是处理的图像数量, nt.sum() 是所有类别目标数量的总和, mp 、 mr 、 map50 、 map 分别是平均精确度、平均召回率、[email protected]和[email protected]:0.95。
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
# 条件判断,如果所有类别的目标数量总和为0,即没有检测到任何目标。
if nt.sum() == 0:
# 如果没有检测到任何目标,打印一条警告信息,指出在当前任务的数据集中没有找到标签,因此无法计算评估指标。
LOGGER.warning(f'WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels') # 警告 ⚠️ 在 {task} 集中未找到标签,无法计算没有标签的指标。
# Print results per class
# 条件判断,如果 verbose 为 True 或者类别数 nc 小于50且不是训练过程,并且类别数大于1且 stats 列表不为空,则执行冒号后面的代码块。
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
# 开始一个循环,遍历 ap_class 列表中的每个类别的AP值。
for i, c in enumerate(ap_class):
# 在循环中,使用之前定义的格式化字符串 pf 打印每个类别的评估结果。 names[c] 是类别名称, nt[c] 是该类别的目标数量, p[i] 、 r[i] 、 ap50[i] 、 ap[i] 分别是该类别的精确度、召回率、[email protected]和[email protected]:0.95。
LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
# Print speeds
# 计算每个阶段(预处理、推理、NMS)的速度,单位为毫秒。 x.t / seen 计算每个阶段的平均时间,乘以1E3转换为毫秒。
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image
# 条件判断,如果不是训练过程,则执行冒号后面的代码块。
if not training:
# 定义了一个元组 shape ,表示图像的批量大小和尺寸。
shape = (batch_size, 3, imgsz, imgsz)
# 打印每个阶段的速度,使用之前计算的 t 元组中的值,并格式化为毫秒。
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t) # 速度:%.1fms 预处理,%.1fms 推理,形状为 {shape} 的每幅图像 %.1fms NMS 。
# 这段代码负责打印整体和每个类别的评估结果,包括精确度、召回率和平均精度等指标。它还打印模型运行的速度,包括预处理、推理和NMS阶段。这些信息对于理解模型的性能和调整模型参数非常有用。
# 这段代码是函数 run 中负责绘制混淆矩阵和调用回调函数的部分,用于可视化模型性能和通知评估结束。
# Plots
# 条件判断,如果 plots 为 True ,表示需要绘制混淆矩阵和其他可能的可视化图表。
if plots:
# 如果需要绘制图表,调用 confusion_matrix.plot 方法,绘制混淆矩阵并保存到指定目录。 save_dir 参数指定了保存图表的目录, names 参数提供了类别名称列表,用于图表的标签。
# def plot(self, normalize=True, save_dir='', names=()): -> 用于绘制混淆矩阵的热力图,并提供了一个 print 方法来打印混淆矩阵。
confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
# 调用回调函数 on_val_end ,通知整个验证过程结束。传递给回调函数的参数包括 :
# nt 每个类别的目标数量。 tp 真正例(True Positives)的数量。 fp 假正例(False Positives)的数量。 p 精确度(Precision)的值。 r 召回率(Recall)的值。 f1 F1分数。 ap 平均精度(Average Precision)的值。 ap50 在0.5 IoU阈值下的平均精度。 ap_class 每个类别的AP值。 confusion_matrix 混淆矩阵对象,可能用于回调函数中的进一步分析或可视化。
callbacks.run('on_val_end', nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
# 这段代码负责在评估结束后绘制混淆矩阵,这是一种常用的性能评估工具,可以帮助识别模型在哪些类别上表现良好,在哪些类别上存在问题。同时,通过回调函数,它允许用户在评估结束时执行自定义操作,如记录最终结果、触发后续处理流程或进行其他形式的分析。这些步骤有助于用户全面了解模型的性能,并根据需要进行调整。
# 这段代码是函数 run 中负责保存预测结果为JSON格式,并使用 pycocotools 库来评估模型性能的部分。
# Save JSON
# 条件判断,如果 save_json 为 True 且 jdict (预测结果的字典)不为空,则执行冒号后面的代码块。
if save_json and len(jdict):
# 获取模型权重文件的名称(不包括路径)。如果 weights 是列表,则取第一个元素;如果不是列表,则直接使用 weights 。然后,使用 Path 对象的 stem 属性获取文件名(不包括扩展名)。如果 weights 为 None ,则 w 设置为空字符串。
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
# 码构造 注释JSON文件 的路径。 data.get('path', '../coco') 尝试从 data 字典中获取 'path' 键的值,如果不存在,则使用 '../coco' 作为默认值。然后,构造完整的注释JSON文件路径。
anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
# 构造 预测结果JSON文件 的路径。使用 save_dir 和权重文件名 w (加上 '_predictions.json' 后缀)来创建文件名。
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
# 记录一条信息,表明正在使用 pycocotools 评估mAP并将预测结果保存到 pred_json 指定的文件。
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') # 评估 pycocotools mAP...保存 {pred_json}...
# 打开 pred_json 指定的文件用于写入。
with open(pred_json, 'w') as f:
# 将 jdict 字典转换为JSON格式并写入到文件中。
json.dump(jdict, f)
# 一个尝试块,用于捕获在评估过程中可能发生的任何异常。
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
# 检查是否安装了 pycocotools 库。
# def check_requirements(requirements=ROOT / 'requirements.txt', exclude=(), install=True, cmds=''): -> 用于检查是否安装了满足YOLO要求的依赖项。如果某些依赖项未安装或版本不兼容,函数会尝试自动安装它们。
check_requirements('pycocotools')
# 导入 COCO 类。
from pycocotools.coco import COCO
# 导入 COCOeval 类。
from pycocotools.cocoeval import COCOeval
# 初始化注释API。
anno = COCO(anno_json) # init annotations api
# 加载预测结果并初始化预测API。
pred = anno.loadRes(pred_json) # init predictions api
# 创建一个 COCOeval 对象,用于评估边界框。
eval = COCOeval(anno, pred, 'bbox')
# 条件判断,如果数据集是COCO数据集,则执行冒号后面的代码块。
if is_coco:
# 如果是COCO数据集,设置要评估的图像ID列表。
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # image IDs to evaluate
# 执行评估。
eval.evaluate()
# 累积评估结果。
eval.accumulate()
# 总结评估结果。
eval.summarize()
# 获取评估结果,包括[email protected]和[email protected]:0.95。
map, map50 = eval.stats[:2] # update results ([email protected]:0.95, [email protected])
# 异常处理块,如果评估过程中发生任何异常,将捕获异常并记录错误信息。
except Exception as e:
# 如果发生异常,记录一条错误信息,指出 pycocotools 无法运行,并显示异常信息。
LOGGER.info(f'pycocotools unable to run: {e}') # pycocotools 无法运行:{e} 。
# 这段代码负责将预测结果保存为JSON文件,并使用 pycocotools 库来评估模型的性能,特别是计算mAP指标。它还处理了可能发生的异常,并记录了错误信息。这些步骤对于评估目标检测模型的性能至关重要,因为它们提供了一个标准化的评估指标。
# 这段代码是函数 run 中负责返回评估结果的部分。
# Return results
# 将模型转换为单精度浮点数(FP32)。这是为了确保模型在训练时使用的是标准的数值精度。
model.float() # for training
# 条件判断,如果不是训练过程,则执行冒号后面的代码块。
if not training:
# 如果 save_txt 为 True ,计算保存在 save_dir / 'labels' 目录下的文本文件数量,并构造一条消息 s ,表示有多少标签被保存。如果 save_txt 为 False ,则 s 为空字符串。
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
# 记录一条信息,指出结果已经被保存到 save_dir 目录,并且如果 s 不为空,还会显示额外的信息 s 。
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") # 结果保存至 {colorstr('bold', save_dir)}{s} 。
# 创建一个长度为类别数 nc 的数组 maps ,并用整体的平均精度 map 初始化。
maps = np.zeros(nc) + map
# 开始一个循环,遍历 ap_class 列表中的每个类别的索引和类别ID。
for i, c in enumerate(ap_class):
# 在循环中,将每个类别的AP值赋值给 maps 数组对应的类别ID位置。
maps[c] = ap[i]
# 返回评估结果,包括 :
# mp 平均精确度。 mr 平均召回率。 map50 在0.5 IoU阈值下的平均精度。 map 在0.5:0.95 IoU阈值下的平均精度。 loss 平均损失值,通过将损失张量转换到CPU并除以数据加载器的长度来计算,然后转换为列表。 maps 每个类别的AP值数组。 t 每个阶段的速度(预处理、推理、NMS)。
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
# 这段代码负责将评估结果整理并返回,包括模型性能指标、每个类别的AP值和速度信息。这些结果对于评估模型的整体性能和每个类别的性能非常有用,并且可以用于进一步的分析和模型优化。
# 这个函数是一个完整的评估流程,可以用于在COCO数据集上评估目标检测模型的性能。它支持多种输出格式,包括文本文件、JSON文件,并且可以计算和输出多种评估指标。
5.def parse_opt():
# 这段代码定义了一个名为 parse_opt 的函数,它使用 argparse 库来解析命令行参数。这个函数用于设置和获取运行模型所需的配置选项。
def parse_opt():
# 创建一个新的参数解析器对象,用于处理命令行参数。
parser = argparse.ArgumentParser()
# 添加 --data 命令行参数,用于指定数据集配置文件的路径。默认值为 ROOT / 'data/coco.yaml' 。
parser.add_argument('--data', type=str, default=ROOT / 'data/coco.yaml', help='dataset.yaml path')
# 添加 --weights 命令行参数,用于指定模型权重文件的路径。 nargs='+' 表示参数可以有多个值。默认值为 ROOT / 'yolo.pt' 。
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolo.pt', help='model path(s)')
# 添加 --batch-size 命令行参数,用于指定批处理大小。默认值为32。
parser.add_argument('--batch-size', type=int, default=32, help='batch size')
# 添加 --imgsz 、 --img 和 --img-size 三个等效的命令行参数,用于指定推理时的图像尺寸。默认值为640像素。
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
# 添加 --conf-thres 命令行参数,用于指定置信度阈值。默认值为0.001。
parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
# 添加 --iou-thres 命令行参数,用于指定非最大抑制(NMS)的IoU阈值。默认值为0.7。
parser.add_argument('--iou-thres', type=float, default=0.7, help='NMS IoU threshold')
# 添加 --max-det 命令行参数,用于指定每张图像的最大检测数量。默认值为300。
parser.add_argument('--max-det', type=int, default=300, help='maximum detections per image')
# 添加 --task 命令行参数,用于指定任务类型。默认值为 val 。
parser.add_argument('--task', default='val', help='train, val, test, speed or study')
# 添加 --device 命令行参数,用于指定CUDA设备。默认值为空字符串,意味着自动选择设备。
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
# 添加 --workers 命令行参数,用于指定数据加载器的最大工作线程数。默认值为8。
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
# 添加 --single-cls 命令行参数,用于指示数据集是否为单类别数据集。
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
# 添加 --augment 命令行参数,用于指示是否进行增强推理。
parser.add_argument('--augment', action='store_true', help='augmented inference')
# 添加 --verbose 命令行参数,用于指示是否按类别报告mAP。
parser.add_argument('--verbose', action='store_true', help='report mAP by class')
# 添加 --save-txt 命令行参数,用于指示是否将结果保存为文本文件。
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
# 添加 --save-hybrid 命令行参数,用于指示是否将标签和预测结果的混合保存为文本文件。
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
# 添加 --save-conf 命令行参数,用于指示是否在保存的文本文件中包含置信度。
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
# 添加 --save-json 命令行参数,用于指示是否保存COCO格式的JSON结果文件。
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
# 添加 --project 命令行参数,用于指定保存结果的项目路径。默认值为 ROOT / 'runs/val' 。
parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
# 添加 --name 命令行参数,用于指定保存结果的实验名称。默认值为 exp 。
parser.add_argument('--name', default='exp', help='save to project/name')
# 添加 --exist-ok 命令行参数,用于指示如果项目/名称已存在,是否允许不增加后缀。
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
# 添加 --half 命令行参数,用于指示是否使用FP16半精度推理。
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
# 添加 --dnn 命令行参数,用于指示是否使用OpenCV DNN进行ONNX模型推理。
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
# 添加 --min-items 命令行参数,用于实验性功能。默认值为0。
parser.add_argument('--min-items', type=int, default=0, help='Experimental')
# 解析命令行参数,并将结果存储在 opt 变量中。
opt = parser.parse_args()
# 验证 opt.data 指定的YAML文件是否有效。
opt.data = check_yaml(opt.data) # check YAML
# 如果 opt.data 以 coco.yaml 结尾,则设置 opt.save_json 为 True 。
opt.save_json |= opt.data.endswith('coco.yaml')
# 如果 opt.save_hybrid 为 True ,则设置 opt.save_txt 为 True 。
opt.save_txt |= opt.save_hybrid
# 打印解析后的命令行参数。
print_args(vars(opt))
# 返回解析后的命令行参数对象。
return opt
# parse_opt 函数用于解析命令行参数,这些参数控制着模型推理和评估的各种设置。函数首先定义了各种参数和它们的默认值,然后解析命令行输入,并对某些参数进行后处理,如检查YAML文件的有效性和更新保存标志。最后,函数打印并返回这些参数,以便在程序的其他部分使用。这些参数对于自定义模型的推理和评估过程至关重要。
6.def main(opt):
# 这段代码定义了一个名为 main 的函数,它根据传入的参数 opt 来执行不同的任务。
# 定义一个名为 main 的函数,它接受一个参数。
# 1.opt :这个参数是一个包含命令行选项的命名空间。
def main(opt):
#check_requirements(exclude=('tensorboard', 'thop'))
# 这段代码是Python函数 main 中的一部分,用于处理当 opt.task 属性值为'train'、'val'或'test'时的情况。
# 检查 opt 对象中的 task 属性是否为'train'、'val'或'test'中的一个。如果是,那么执行冒号后面的代码块。
if opt.task in ('train', 'val', 'test'): # run normally
# 检查 opt 对象中的 conf_thres (置信度阈值)属性是否大于0.001。
# 如果置信度阈值大于0.001,使用 LOGGER 记录一条警告信息。这条警告信息指出,置信度阈值大于0.001可能会导致结果无效。这是因为在YOLOv5的mAP计算中,过高的置信度阈值可能会导致在PR曲线的右侧缺乏数据,从而导致外推误差增大。这个问题在GitHub issue #1466中有讨论。
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
LOGGER.info(f'WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results') # 警告⚠️ 置信度阈值 {opt.conf_thres} > 0.001 会产生无效结果。
# 检查 opt 对象中的 save_hybrid 属性是否为True。
if opt.save_hybrid:
# 如果 save_hybrid 为True,使用 LOGGER 记录一条警告信息。这条警告信息指出,使用 --save-hybrid 选项会导致mAP值偏高,因为它是从混合标签中返回的,而不仅仅是从预测结果中得出的。
LOGGER.info('WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone') # 警告⚠️ --save-hybrid 将从混合标签返回高 mAP,而不仅仅是从预测返回。
# 调用 run 函数,并传入 opt 对象中的所有属性作为参数。 vars(opt) 将 opt 对象转换为字典, ** 操作符用于将字典解包为关键字参数。
run(**vars(opt))
# 这段代码的主要目的是在执行训练、验证或测试任务时,检查并警告用户关于置信度阈值和混合标签保存选项可能对结果的影响。如果置信度阈值设置得过高,或者启用了混合标签保存,都会记录相应的警告信息,以确保用户意识到这些设置可能会影响模型评估的准确性。
# 这段代码是 main 函数中的 else 部分,处理当 opt.task 不是'train'、'val'或'test'时的情况,特别是处理速度测试('speed')任务。
# 如果 opt.task 不是'train'、'val'或'test'中的任何一个,执行这个 else 代码块。
else:
# 确保 opt.weights 是一个列表。如果 opt.weights 已经是一个列表,就直接使用它;如果不是列表,就将其转换为一个只包含该权重的列表。
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
# 设置 opt.half 属性,如果CUDA可用且设备不是CPU,则设置为True,表示可以使用FP16(半精度浮点数)来加速计算,以获得最快的结果。
opt.half = torch.cuda.is_available() and opt.device != 'cpu' # FP16 for fastest results
# 检查 opt.task 是否为'speed',如果是,则执行速度测试。
if opt.task == 'speed': # speed benchmarks
# python val.py --task speed --data coco.yaml --batch 1 --weights yolo.pt...
# 为速度测试设置特定的参数值 :置信度阈值( conf_thres )为0.25,交并比阈值( iou_thres )为0.45,不保存JSON格式的结果( save_json )。
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
# 遍历 weights 列表中的每个权重文件。
for opt.weights in weights:
# 对每个权重文件调用 run 函数,并传入 opt 对象中的所有属性作为参数。同时,设置 plots 参数为False,表示在速度测试中不生成图表。
run(**vars(opt), plots=False)
# 这段代码的主要目的是处理速度测试任务。它首先确保 opt.weights 是一个列表,然后根据是否使用CUDA和设备类型设置FP16加速。接着,为速度测试设置了特定的参数,并遍历所有权重文件,对每个权重文件调用 run 函数来执行速度测试,同时不生成图表。这样的设置可以帮助用户评估模型在不同权重下的性能,特别是在推理速度方面。
# 这段代码是 main 函数中的 elif 部分,处理当 opt.task 为'study'时的情况,即进行速度与mAP(平均精度均值)基准测试。
# 如果 opt.task 属性值为'study',执行这个 elif 代码块。
elif opt.task == 'study': # speed vs mAP benchmarks
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolo.pt...
# 遍历 weights 列表中的每个权重文件。
for opt.weights in weights:
# 为每个权重文件生成一个文件名,用于保存基准测试的结果。文件名包含数据集名称和权重文件名称。
f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to
# 初始化两个列表 : x 用于存储图像大小(从256到1536,每次增加128), y 用于存储结果。
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
# 遍历不同的图像大小。
for opt.imgsz in x: # img-size
# 记录正在运行的基准测试信息,包括文件名和当前的图像大小。
LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...') # 运行 {f} --imgsz {opt.imgsz}...
# 调用 run 函数,传入 opt 对象中的所有属性作为参数,并设置 plots 参数为False,表示不生成图表。 run 函数返回结果和时间,分别存储在 r 和 t 中。
r, _, t = run(**vars(opt), plots=False)
# 将结果和时间添加到 y 列表中。
y.append(r + t) # results and times
# 使用 numpy.savetxt 函数将 y 列表中的数据保存到之前生成的文件名 f 中。
np.savetxt(f, y, fmt='%10.4g') # save
# 使用操作系统命令将所有以 study_ 开头的文本文件压缩成一个名为 study.zip 的zip文件。
os.system('zip -r study.zip study_*.txt')
# 调用 plot_val_study 函数,传入 x 参数,用于绘制速度与mAP的对比图表。
# def plot_val_study(file='', dir='', x=None): -> 用于绘制由 val.py 生成的 study.txt 文件中的性能研究数据(或在指定目录中绘制所有 study*.txt 文件的数据)。
plot_val_study(x=x) # plot
# 这段代码的主要目的是进行速度与mAP的基准测试。它遍历不同的权重文件和图像大小,记录每个组合的结果和时间,并将这些数据保存到文件中。最后,它将所有结果文件压缩,并绘制一个图表来直观地展示速度与mAP之间的关系。这样的测试可以帮助用户理解不同图像大小和模型权重对模型性能的影响。
# main 函数根据传入的参数执行不同的任务,包括正常的推理和评估、速度基准测试以及速度与mAP的基准测试。它还负责记录警告信息、设置参数以及调用 run 函数。这个函数是程序的入口点,根据用户的需求执行相应的操作。
7.if __name__ == "__main__":
# 这段代码是Python脚本中常见的模式,用于在直接运行该脚本时执行特定的操作。
# 这是一个特殊的Python变量 __name__ ,当Python文件被直接运行时, __name__ 的值会被设置为 "__main__" 。因此,这行代码检查该文件是否作为脚本直接运行,而不是作为模块导入到另一个文件中。
if __name__ == "__main__":
# 如果文件是作为脚本直接运行的,调用 parse_opt 函数,该函数的目的是解析命令行参数,并将这些参数转换为一个对象(或者字典),这个对象(或字典)包含了所有的配置选项。这些选项通常用于控制脚本的行为。
opt = parse_opt()
# 最后,将 parse_opt 函数返回的配置选项对象(或字典)传递给 main 函数,以执行脚本的主要逻辑。
main(opt)
# 这段代码的作用是确定当前脚本是被直接运行还是作为模块导入。如果是直接运行,它会解析命令行参数,并调用 main 函数来执行程序的主要功能。这是一种常见的实践,使得Python脚本既可以作为独立的程序运行,也可以作为模块被其他Python代码导入和使用。