YOLOv7-0.1部分代码阅读笔记-test.py
test.py
test.py
目录
test.py
1.所需的库和模块
2.def test(data, weights=None, batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, save_json=False, single_cls=False, augment=False, verbose=False, model=None, dataloader=None, save_dir=Path(''), save_txt=False, save_hybrid=False, save_conf=False, plots=True, wandb_logger=None, compute_loss=None, half_precision=True, trace=False, is_coco=False):
3.if __name__ == '__main__':
1.所需的库和模块
import argparse
import json
import os
from pathlib import Path
from threading import Thread
import numpy as np
import torch
import yaml
from tqdm import tqdm
from models.experimental import attempt_load
from utils.datasets import create_dataloader
from utils.general import coco80_to_coco91_class, check_dataset, check_file, check_img_size, check_requirements, \
box_iou, non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, set_logging, increment_path, colorstr
from utils.metrics import ap_per_class, ConfusionMatrix
from utils.plots import plot_images, output_to_target, plot_study_txt
from utils.torch_utils import select_device, time_synchronized, TracedModel
2.def test(data, weights=None, batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, save_json=False, single_cls=False, augment=False, verbose=False, model=None, dataloader=None, save_dir=Path(''), save_txt=False, save_hybrid=False, save_conf=False, plots=True, wandb_logger=None, compute_loss=None, half_precision=True, trace=False, is_coco=False):
# 这段代码是一个 Python 函数 test ,它用于评估一个目标检测模型的性能。函数接受多个参数,包括数据集、模型权重、批处理大小、图像尺寸、置信度阈值、IOU 阈值等。它还包含了一些用于保存结果、日志记录和可视化的选项。
# 参数说明。
# 1.data : 数据集的配置文件路径或数据字典。
# 2.weights : 模型权重文件的路径。
# 3.batch_size : 批处理大小。
# 4.imgsz : 用于推理的图像尺寸。
# 5.conf_thres : 目标置信度阈值。
# 6.iou_thres : 用于非最大抑制(NMS)的交并比(IOU)阈值。
# 7.save_json : 是否保存预测结果为 JSON 文件。
# 8.single_cls : 是否只处理单个类别。
# 9.augment : 是否进行增强推理。
# 10.verbose : 是否输出详细信息。
# 11.model : 预加载的模型对象。
# 12.dataloader : 预创建的数据加载器对象。
# 13.save_dir : 保存结果的目录。
# 14.save_txt : 是否保存结果到文本文件。
# 15.save_hybrid : 是否保存混合自动标签。
# 16.save_conf : 是否保存自动标签的置信度。
# 17.plots : 是否绘制图像。
# 18.wandb_logger : 用于 Weights & Biases 记录的日志器。
# 19.compute_loss : 用于计算损失的函数。
# 20.half_precision : 是否使用半精度(FP16)。
# 21.trace : 是否追踪模型。
# 22.is_coco : 数据集是否为 COCO 数据集。
def test(data,
weights=None,
batch_size=32,
imgsz=640,
conf_thres=0.001,
iou_thres=0.6, # for NMS
save_json=False,
single_cls=False,
augment=False,
verbose=False,
model=None,
dataloader=None,
save_dir=Path(''), # for saving images
save_txt=False, # for auto-labelling
save_hybrid=False, # for hybrid auto-labelling
save_conf=False, # save auto-label confidences
plots=True,
wandb_logger=None,
compute_loss=None,
half_precision=True,
trace=False,
is_coco=False):
# 这段代码是 test 函数中的一部分,负责初始化或加载目标检测模型并设置设备。
# Initialize/load model and set device 初始化/加载模型并设置设备。
# 判断训练模式。这行代码检查 model 是否为 None 。如果 model 不是 None ,则表示函数是由训练脚本调用的,此时设置为训练模式。
training = model is not None
# 设置设备。如果是训练模式,获取模型参数的设备(CPU或GPU)。 next(model.parameters()).device 返回模型参数所在的设备。
if training: # called by train.py 由 train.py 调用。
device = next(model.parameters()).device # get model device 获取模型设备。
# 直接调用的情况。
else: # called directly 直接调用。
# 如果不是训练模式(即直接调用此函数),则调用 set_logging() 设置日志记录,并使用 select_device() 函数选择设备。 opt.device 是通过命令行参数指定的设备(例如 '0' 表示第一个 GPU,'cpu' 表示使用 CPU)。
# def set_logging(rank=-1): -> 用于配置 Python 的日志记录系统。函数的目的是在程序开始时设置合适的日志记录级别。在分布式训练环境中, rank 参数可以用来区分主进程和其他进程,以便在非主进程中减少日志输出,避免日志信息的冗余。
set_logging()
# def select_device(device='', batch_size=None): -> 用于选择并配置 PyTorch 模型将使用的计算设备,可以是 CPU 或者一个或多个 GPU。返回一个 torch.device 对象,表示选择的设备。 -> return torch.device('cuda:0' if cuda else 'cpu')
device = select_device(opt.device, batch_size=batch_size)
# Directories 目录。
# 设置保存目录。
# 这段代码创建一个保存目录。 increment_path() 函数用于确保目录名称唯一(如果同名目录已存在,则在名称后添加数字)。如果 save_txt 为 True ,则在保存目录下创建一个 labels 子目录。
# exist_ok=opt.exist_ok 参数控制如果路径已存在时的行为 :如果 opt.exist_ok 为 True ,则不增加后缀,允许覆盖;如果为 False ,则增加后缀以避免覆盖。
# def increment_path(path, exist_ok=True, sep=''):
# -> 用于生成一个唯一的文件路径。如果指定的路径已经存在,函数会通过添加一个数字后缀来创建一个新的路径。返回一个新的路径,它是原始路径加上一个更新的数字后缀。
# -> return f"{path}{sep}{n}" # update path
save_dir = Path(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model 加载模型。
# 使用 attempt_load() 函数加载指定的模型权重。 map_location=device 确保模型加载到正确的设备上(CPU或GPU)。
# def attempt_load(weights, map_location=None): -> 负责加载一个或多个模型权重,并将它们组合成一个集成模型(Ensemble)。返回包含多个模型的模型集成对象。 -> return model # return ensemble
model = attempt_load(weights, map_location=device) # load FP32 model
# 获取网格大小。这行代码获取模型的最大步幅(stride),并确保网格大小至少为 32。步幅用于定义模型在推理时的网格划分。
gs = max(int(model.stride.max()), 32) # grid size (max stride)
# 检查图像大小。使用 check_img_size() 函数验证输入图像大小是否符合模型要求。这个函数会根据模型的步幅调整图像尺寸,以确保它是步幅的倍数。
# def check_img_size(img_size, s=32): -> 目的是验证给定的图像尺寸 img_size 是否是步长 s 的倍数。如果不是,函数将调整 img_size 到最接近的、大于或等于 img_size 的 s 的倍数,并打印一条警告信息。返回调整后的图像尺寸 new_size 。 -> return new_size
imgsz = check_img_size(imgsz, s=gs) # check img_size
# 模型追踪。
if trace:
# 如果 trace 为 True ,则使用 TracedModel 对模型进行追踪,以优化推理性能。这个步骤通常用于提高模型在推理时的速度。
# class TracedModel(nn.Module):
# -> TracedModel 的目的是将一个给定的 PyTorch 模型转换为一个追踪(traced)模型,也称为 TorchScript 模型。这种模型被优化为在推理时更快,因为它消除了 PyTorch 动态计算图的开销。
# -> def __init__(self, model=None, device=None, img_size=(640,640)):
# -> def forward(self, x, augment=False, profile=False):
# -> 返回输出。最后,方法返回处理后的输出 out ,这个输出可以是分类结果、检测框、分割图等,具体取决于模型的类型和任务。
# -> return out
model = TracedModel(model, device, opt.img_size)
# 这段代码的主要目的是初始化和加载目标检测模型,并根据输入参数设置设备和保存目录。这是模型评估过程中的重要一步,确保后续的推理和评估能够在正确的环境中进行。
# Half
# 这段代码的目的是在目标检测模型的推理过程中启用半精度(FP16)计算,以提高性能和减少内存使用。
# 判断是否使用半精度。 device.type != 'cpu' :检查当前设备是否是 GPU。如果是 CPU,则不支持半精度计算。 half_precision :这是一个布尔值参数,通常由用户在调用函数时指定,表示是否希望启用半精度。
half = device.type != 'cpu' and half_precision # half precision only supported on CUDA
# 将模型转换为半精度。
if half:
# 如果 half 为 True ,则调用 model.half() 将模型的参数和缓冲区转换为半精度(FP16)。
# 使用半精度可以显著减少模型在 GPU 上的内存占用,同时在某些情况下可以提高计算速度,尤其是在支持 FP16 的 GPU 上(如 NVIDIA 的 Volta、Turing 和 Ampere 架构)。
model.half()
# 这段代码通过判断设备类型和用户设置,决定是否将模型转换为半精度,以优化推理性能。启用半精度计算是深度学习模型在推理阶段常用的优化策略,特别是在处理大型模型和数据集时。
# Configure
# 这段代码是 test 函数中用于配置模型和数据集的步骤。
# 设置模型为评估模式。这行代码将模型设置为评估模式,这是进行推理的必要步骤。在评估模式下,模型的某些层(如 dropout 和 batch normalization)会以评估方式运行,而不是训练方式。
model.eval()
# 处理数据集配置。
if isinstance(data, str):
# 如果 data 是一个字符串,假设它是一个文件路径。检查文件是否以 coco.yaml 结尾,以确定是否是 COCO 数据集。
is_coco = data.endswith('coco.yaml')
with open(data) as f:
# 使用 yaml.load 从文件中加载数据集配置, Loader=yaml.SafeLoader 确保加载过程安全,避免执行文件中的任何代码。
data = yaml.load(f, Loader=yaml.SafeLoader)
# 检查数据集。调用 check_dataset() 函数来验证加载的数据集配置是否有效。这个函数的具体实现可能包括检查必要的字段是否存在,以及字段值是否合理。
# def check_dataset(dict): -> 检查本地是否存在指定的数据集,如果不存在,则尝试下载。
check_dataset(data) # check
# 确定类别数量。如果 single_cls 为 True ,则设置类别数量 nc 为 1。 否则,从数据集配置中获取类别数量,并转换为整数。
nc = 1 if single_cls else int(data['nc']) # number of classes
# 创建 IOU 向量。创建一个从 0.5 到 0.95 的线性间隔的张量,包含 10 个元素,代表不同的 IOU 阈值。这个向量用于计算不同 IOU 阈值下的平均精度(mAP)。
iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95
# torch.Tensor.numel()
# numel() 函数是 PyTorch 中的一个方法,用于返回一个张量(tensor)中元素的总数。这个方法是 torch.Tensor 类的一个实例方法,意味着它可以在任何 torch.Tensor 对象上调用。
# 参数 :没有参数
# 返回值 :
# 返回一个整数,表示张量中的元素总数。
# numel() 方法在处理张量时非常有用,尤其是当你需要知道张量的大小而不需要知道具体的维度大小时。这个函数返回的是张量中所有元素的总数,不考虑它的维度结构。
# 计算 IOU 向量的长度。使用 numel() 方法计算 iouv 张量中的元素数量,即不同 IOU 阈值的数量。
niou = iouv.numel()
# Logging
# 这段代码是用于设置日志记录的,特别是在使用 Weights & Biases(W&B)这个工具时,用于确定在测试过程中记录多少张图像。
# 初始化日志图像计数器。这行代码初始化 log_imgs 变量,它将用于跟踪要记录的图像数量。
log_imgs = 0
# 检查 W&B 日志记录器。这个条件检查 wandb_logger 对象是否存在,并且 wandb 属性为真。这通常意味着 W&B 日志记录已经被启用。
if wandb_logger and wandb_logger.wandb:
# 设置要记录的图像数量。
# 如果 W&B 日志记录器存在,那么 log_imgs 被设置为 wandb_logger.log_imgs 和 100 中的较小值。这意味着日志记录器将记录的图像数量不会超过 100 张,即使 wandb_logger.log_imgs 设置了一个更大的值。
# 这个限制是为了防止记录过多的图像,可能会导致性能下降或超出 W&B 的限制。
log_imgs = min(wandb_logger.log_imgs, 100)
# 这段代码的目的是配置 W&B 日志记录器以记录一定数量的图像,这对于可视化模型在测试集上的表现非常有用。通过限制记录的图像数量,可以确保日志记录过程既有效又不过于繁重。
# 如果 wandb_logger 没有被设置或者 W&B 没有被启用, log_imgs 将保持为 0,意味着不会记录任何图像。
# Dataloader
# 这段代码负责数据加载器的创建和一些初始化操作,以便进行目标检测模型的评估。
# 检查是否在训练模式。这行代码检查当前是否处于训练模式。如果 training 为 False ,则执行数据加载和模型推理的相关操作。
if not training:
# 模型推理预热。
if device.type != 'cpu':
# 如果设备不是 CPU,则通过传入一个全零的张量(形状为 [1, 3, imgsz, imgsz] )来运行模型一次。这是为了预热模型,确保模型在 GPU 上的计算图已构建,从而提高后续推理的速度。
# type_as(next(model.parameters())) 确保输入张量的类型与模型参数一致(例如,FP32 或 FP16)。
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once
# 确定任务类型。 根据 opt.task 的值确定当前的任务类型。如果 opt.task 是 'train'、'val' 或 'test' 之一,则使用该值;否则,默认设置为 'val'。
task = opt.task if opt.task in ('train', 'val', 'test') else 'val' # path to train/val/test images
# 创建数据加载器。
# 调用 create_dataloader() 函数创建数据加载器。该函数使用指定的任务数据(例如训练、验证或测试数据)、图像大小、批处理大小、网格大小等参数。
# pad=0.5 和 rect=True 是数据加载时的额外参数,可能用于处理图像的填充和长宽比调整。
# prefix=colorstr(f'{task}: ') 用于在输出日志时添加任务前缀。
# def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False, rank=-1, world_size=1, workers=8, image_weights=False, quad=False, prefix=''):
# -> 它用于创建并返回一个 PyTorch 数据加载器(dataloader),这个加载器可以用于深度学习模型的训练过程中加载数据。函数接受多个参数,用于配置数据加载器的行为。返回数据加载器和数据集。函数返回创建的数据加载器和数据集对象。
# -> return dataloader, dataset
dataloader = create_dataloader(data[task], imgsz, batch_size, gs, opt, pad=0.5, rect=True,
prefix=colorstr(f'{task}: '))[0]
# 初始化统计变量。
# seen 用于跟踪已处理的图像数量,初始值为 0。
seen = 0
# confusion_matrix 是一个混淆矩阵对象,用于记录每个类别的预测情况, nc 是类别数量。
# class ConfusionMatrix:
# -> ConfusionMatrix 的类,用于在目标检测任务中生成混淆矩阵。混淆矩阵是一个表格,用于描述分类模型的性能,特别是模型预测的类别与真实类别之间的关系。
# -> def __init__(self, nc, conf=0.25, iou_thres=0.45):
confusion_matrix = ConfusionMatrix(nc=nc)
# 获取类别名称。通过检查模型是否具有 names 属性来获取类别名称。如果模型是通过 DataParallel 或 DistributedDataParallel 训练的,则需要访问 model.module.names 。
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
# COCO 类别映射。调用 coco80_to_coco91_class() 函数获取 COCO 数据集中 80 个类别到 91 个类别的映射。这在处理 COCO 数据集时非常有用。
coco91class = coco80_to_coco91_class()
# 初始化统计信息。
# s 是一个格式化字符串,用于输出统计信息的标题。
s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
# 初始化多个变量(如 p 、 r 、 f1 、 mp 、 mr 、 map50 、 map 、 t0 、 t1 )为 0,用于后续的性能统计。
p, r, f1, mp, mr, map50, map, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0.
# loss 是一个张量,初始化为零,用于存储损失值。
loss = torch.zeros(3, device=device)
# jdict 、 stats 、 ap 、 ap_class 和 wandb_images 是空列表,用于存储后续计算的结果和图像。
jdict, stats, ap, ap_class, wandb_images = [], [], [], [], []
# 这段代码的主要目的是在评估过程中创建数据加载器,并初始化相关的统计变量和对象。通过预热模型和创建数据加载器,确保后续的推理和评估过程能够顺利进行。
# 这段代码是 test 函数中处理数据加载和图像预处理的部分。它使用 tqdm 库来显示进度条,并对每个批次的数据进行处理。
# 遍历数据加载器。
# 使用 enumerate 遍历 dataloader batch_i 是当前批次的索引, (img, targets, paths, shapes) 是从数据加载器中获取的当前批次的数据。
# tqdm(dataloader, desc=s) 用于显示一个进度条, desc=s 是进度条的描述,通常包含当前任务的信息。
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
# to()
# 在 PyTorch 中, to 函数是一个非常重要的方法,用于将张量或模型移动到指定的设备(如 CPU 或 GPU),并可以选择性地更改数据类型。以下是 to 函数的定义和用法的详细说明。
# to 函数的定义
# to 函数可以用于以下几种情况 :
# 1. 移动张量或模型到指定设备 : 例如,将张量从 CPU 移动到 GPU。
# 语法 : tensor.to(device) 或 model.to(device) 。
# 2. 更改数据类型 : 例如,将张量的数据类型从 float32 更改为 float16 。
# 语法 : tensor.to(torch.float16) 。
# 3. 同时移动设备和更改数据类型 : 例如,将张量从 CPU 移动到 GPU,并将数据类型更改为 float16 。
# 语法: tensor.to(device).to(torch.float16) 。
# 总结 :
# to 函数在 PyTorch 中是一个非常灵活和强大的工具,它允许用户方便地将张量或模型移动到不同的设备,并且可以在移动的同时更改数据类型。这在训练和推理过程中非常有用,尤其是在使用 GPU 加速时。
# 将图像移动到设备。将当前批次的图像数据 img 移动到指定的设备(CPU 或 GPU)。 non_blocking=True 允许数据在后台异步传输,以提高效率。
img = img.to(device, non_blocking=True)
# 转换图像数据类型。
# 根据之前的设置决定图像数据的类型。如果启用了半精度(FP16),则将图像转换为半精度;否则,将其转换为全精度(FP32)。
# 这一步是将图像数据从无符号 8 位整数(uint8)转换为浮点数格式,以便进行模型推理。
img = img.half() if half else img.float() # uint8 to fp16/32
# 归一化图像数据。将图像数据从范围 [0, 255] 归一化到 [0.0, 1.0]。这是深度学习模型常用的预处理步骤,有助于提高模型的收敛速度和性能。
img /= 255.0 # 0 - 255 to 0.0 - 1.0
# 将目标数据移动到设备。将目标数据 targets (通常包含真实标签和边界框信息)移动到与图像相同的设备。
targets = targets.to(device)
# 获取图像的形状信息。获取当前批次图像的形状信息 : nb :批次大小(当前批次中的图像数量)。 _ :通道数(通常为 3,表示 RGB 图像),但在这里不需要使用。 height 和 width :图像的高度和宽度。
nb, _, height, width = img.shape # batch size, channels, height, width
# 这段代码的主要目的是从数据加载器中获取当前批次的数据,并对图像进行必要的预处理,以便后续的模型推理。通过将数据移动到正确的设备、转换数据类型、归一化图像和获取形状信息,为模型的推理过程做好准备。
# 这段代码是 test 函数中用于执行模型推理、计算损失和运行非最大抑制(NMS)的部分。
# 开启无需梯度的上下文。这个上下文管理器指示 PyTorch 在代码块执行期间不要跟踪梯度。这通常用于推理阶段,因为在推理时不需要进行反向传播,从而可以减少内存消耗并加快计算速度。
with torch.no_grad():
# Run model
# 记录推理开始时间。调用 time_synchronized() 函数记录当前时间。这个函数用于确保在多线程或多进程环境中获取的时间是一致的。
# def time_synchronized(): -> 它用于获取一个与CUDA时间同步的准确时间戳。 -> return time.time()
t = time_synchronized()
# 运行模型。
# 将预处理后的图像 img 传递给模型进行推理。 augment 参数用于指示是否应用增强推理。
# out 是模型推理的输出,通常包含预测的边界框、置信度和类别概率。
# train_out 是模型训练输出,如果不需要可以忽略。在推理阶段,通常只关注 out 。
out, train_out = model(img, augment=augment) # inference and training outputs
# 记录推理结束时间并计算推理时间。再次调用 time_synchronized() 记录推理结束后的时间。• 计算推理的总时间并将其累加到 t0 。 t0 用于跟踪整个推理过程的时间消耗。
t0 += time_synchronized() - t
# Compute loss
# 计算损失(如果需要)。
if compute_loss:
# 如果提供了 compute_loss 函数,则使用它来计算损失。这通常用于训练阶段,但在某些情况下,也可以用于推理阶段以评估模型的性能。
# train_out 被转换为浮点数,然后传递给 compute_loss 函数,连同目标数据 targets 。
# 计算的损失被累加到 loss 变量中,用于后续的性能分析。
loss += compute_loss([x.float() for x in train_out], targets)[1][:3] # box, obj, cls
# Run NMS
# 将目标数据转换为像素坐标。将目标数据(边界框)从归一化坐标转换为像素坐标。这是通过将目标数据乘以图像的宽度和高度来实现的。
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
# 为自动标签准备标签(如果需要)。如果启用了混合自动标签( save_hybrid ),则为每个图像准备标签。这通常用于在推理过程中同时保存预测结果和真实标签。
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
# 记录 NMS 开始时间。
t = time_synchronized()
# 运行非最大抑制(NMS)。
# 对模型输出的预测结果应用 NMS,以消除重叠的边界框并保留最佳的预测结果。
# conf_thres 是置信度阈值, iou_thres 是交并比阈值,用于 NMS。 labels 参数用于指定每个图像的标签,如果启用了混合自动标签。 multi_label=True 表示每个边界框可以有多个标签。
out = non_max_suppression(out, conf_thres=conf_thres, iou_thres=iou_thres, labels=lb, multi_label=True)
# 记录 NMS 结束时间并计算 NMS 时间。再次调用 time_synchronized() 记录 NMS 结束后的时间。• 计算 NMS 的总时间并将其累加到 t1 。 t1 用于跟踪整个 NMS 过程的时间消耗。
t1 += time_synchronized() - t
# 这段代码的主要目的是在无需计算梯度的上下文中运行模型推理,计算损失(如果需要),并应用非最大抑制(NMS)来处理模型的输出。通过记录推理和 NMS 的时间,可以评估模型的推理效率。
# Statistics per image 每幅图像的统计信息。
# 这段代码是 test 函数中用于处理每个图像的统计信息的部分。它遍历模型输出的预测结果 out ,并为每个图像计算统计数据。
# 遍历每个图像的预测结果。使用 enumerate 遍历模型输出的预测结果 out 。 si 是索引,表示当前图像在批次中的位置; pred 是当前图像的预测结果,通常包含边界框、置信度和类别概率。
for si, pred in enumerate(out):
# 获取当前图像的真实标签。从 targets 中获取当前图像(索引 si )的真实标签。 targets 是一个包含所有图像目标信息的张量,其中第一列是图像索引,后续列包含目标的类别和边界框坐标等信息。
labels = targets[targets[:, 0] == si, 1:]
# 计算真实标签的数量。 nl 表示当前图像中真实标签的数量。
nl = len(labels)
# 获取目标类别。如果存在真实标签( nl 大于 0),则提取这些标签的类别信息,并将其转换为列表 tcls ;如果没有真实标签,则 tcls 为空列表。
tcls = labels[:, 0].tolist() if nl else [] # target class
# 获取图像路径。从 paths 列表中获取当前图像的路径,并使用 Path 类创建一个路径对象。
path = Path(paths[si])
# 更新已处理图像的数量。每次处理一个图像时,更新已处理图像的数量 seen 。
seen += 1
# 处理无预测结果的情况。
# 如果当前图像没有预测结果( len(pred) == 0 ),则检查是否存在真实标签。
if len(pred) == 0:
if nl:
# 如果存在真实标签( nl 大于 0),则向 stats 列表追加一个元组,包含 :
# 一个形状为 (0, niou) 的零张量,表示没有预测结果。
# 两个空的张量,分别用于存储置信度和类别信息。
# tcls ,即真实标签的类别信息。
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
# 使用 continue 跳过当前循环的剩余部分,继续处理下一个图像。
continue
# 这段代码的主要目的是为每个图像收集统计信息,包括真实标签和预测结果。如果一个图像没有预测结果,它会创建一个特殊的统计记录,表示该图像没有检测到任何目标。这些统计信息后续用于计算性能指标,如精确度、召回率和平均精度(AP)。
# Predictions
# 这段代码是 test 函数中用于处理模型对每个图像的预测结果的部分。
# 克隆预测结果。 pred 是模型输出的预测结果,包含了边界框、置信度和类别概率等信息。 使用 clone() 方法创建 pred 的一个副本 predn 。这样做是为了避免在后续处理中直接修改原始的预测结果。
predn = pred.clone()
# 调整坐标到原始图像空间。
# img[si].shape[1:] 获取当前图像的高度和宽度。 si 是当前图像在批次中的索引。
# predn[:, :4] 获取预测结果中的边界框坐标。
# shapes[si][0] 和 shapes[si][1] 分别代表当前图像在原始图像空间中的高度和宽度。
# scale_coords 函数用于将边界框坐标从模型输入的归一化空间(通常是模型预期的输入尺寸)调整回原始图像空间。这对于后续的评估和可视化非常重要,因为我们需要在原始图像上绘制边界框。
# def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None): -> 它用于将坐标从一张图片的形状 ( img1_shape ) 缩放到另一张图片的形状 ( img0_shape )。返回调整后的坐标。 -> return coords
scale_coords(img[si].shape[1:], predn[:, :4], shapes[si][0], shapes[si][1]) # native-space pred
# 这段代码的主要目的是将模型的预测结果从模型的输入空间转换到原始图像空间。这样,预测的边界框坐标就可以直接应用于原始图像上,用于后续的评估、绘制边界框或保存结果等操作。
# 这是一个常见的步骤,因为模型通常在不同于原始图像尺寸的输入上进行训练和推理。通过调整坐标,确保预测结果与原始图像对应。
# 这段代码处理将模型的预测结果保存到文本文件,并使用 Weights & Biases (W&B) 进行日志记录。
# Append to text file
# 保存到文本文件。
# 检查是否需要保存文本文件。 如果 save_txt 为 True ,则执行以下操作。
if save_txt:
# 计算归一化增益。 shapes[si][0] 包含原始图像的宽度和高度。 gn 是用于将归一化的坐标转换回原始图像尺寸的增益因子。
gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]] # normalization gain whwh
# 遍历预测结果并转换坐标。
# predn.tolist() 将预测结果转换为 Python 列表,每个元素包含 边界框坐标 xyxy 、 置信度 conf 和 类别 cls 。
for *xyxy, conf, cls in predn.tolist():
# xyxy2xywh 函数将边界框的坐标从 xyxy 格式(左上角和右下角的坐标)转换为 xywh 格式(中心点坐标和宽高)。
# 将 xywh 坐标除以 gn 进行归一化,然后转换为列表。
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
# 格式化标签行。如果 save_conf 为 True ,则将置信度 conf 包含在内;否则,只包含类别和 xywh 坐标。
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
# 写入文本文件。
# 打开保存目录下的 labels 文件夹中的对应图像的文本文件,以追加模式写入。
with open(save_dir / 'labels' / (path.stem + '.txt'), 'a') as f:
# 格式化字符串并写入每个预测结果。
f.write(('%g ' * len(line)).rstrip() % line + '\n')
# W&B logging - Media Panel Plots
# Weights & Biases 日志记录。
# 检查是否需要记录 W&B 图像。如果 wandb_images 列表的长度小于 log_imgs 并且当前是测试阶段( wandb_logger.current_epoch > 0 ),则执行以下操作。
if len(wandb_images) < log_imgs and wandb_logger.current_epoch > 0: # Check for test operation
# 检查是否达到记录间隔。如果当前 epoch 数是 bbox_interval 的倍数,则记录边界框信息。
if wandb_logger.current_epoch % wandb_logger.bbox_interval == 0:
# 准备 W&B 的边界框数据。 从 pred.tolist() 中提取每个预测结果的边界框坐标、类别和置信度。 格式化为 W&B 所需的边界框数据结构。
box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
"class_id": int(cls),
"box_caption": "%s %.3f" % (names[cls], conf),
"scores": {"class_score": conf},
"domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
# 记录 W&B 图像。
# 创建一个包含边界框数据和类别标签的字典。
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
# 使用 wandb_logger.wandb.Image 创建一个图像对象,并将其添加到 wandb_images 列表中。
wandb_images.append(wandb_logger.wandb.Image(img[si], boxes=boxes, caption=path.name))
# 记录训练进度。如果 wandb_logger 和 wandb_logger.wandb_run 存在,则记录训练进度,包括预测结果、图像路径和类别名称。
wandb_logger.log_training_progress(predn, path, names) if wandb_logger and wandb_logger.wandb_run else None
# 这段代码负责将模型的预测结果保存到文本文件,并使用 Weights & Biases 进行可视化日志记录。
# 这对于模型的评估、调试和展示结果非常有用。通过保存文本文件,可以方便地查看和验证预测结果;而通过 W&B 日志记录,可以直观地在 W&B 平台上查看模型的性能和预测结果。
# Append to pycocotools JSON dictionary
# 这段代码是 test 函数中用于将模型的预测结果格式化并添加到一个 JSON 字典中的部分,这个 JSON 字典随后可以被用来与 COCO 工具包( pycocotools )一起计算模型的性能指标,如平均精度(mAP)。
# 检查是否需要保存 JSON 。如果 save_json 为 True ,则执行以下操作,这通常用于后续的评估和分析。
if save_json:
# p.stem :返回文件名不包括后缀的部分。
# [{"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}, ...
# 获取图像 ID 。 path 是当前图像的路径, path.stem 是不带扩展名的文件名。 如果文件名是数字,则直接将文件名转换为整数作为图像 ID;否则,使用文件名作为图像 ID 。
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
# 将预测边界框从 xyxy 格式转换为 xywh 格式。 predn[:, :4] 提取预测结果中的边界框坐标( xyxy 格式)。 xyxy2xywh 函数将坐标从 xyxy 格式转换为 xywh 格式(中心点坐标和宽高)。
box = xyxy2xywh(predn[:, :4]) # xywh
# 调整边界框坐标。这一步将 xywh 格式中的中心点坐标转换回左上角坐标。
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
# numpy.ndarray.tolist() -> list
# 在 Python 中, np.ndarray.tolist() 是 NumPy 库中的一个方法,它用于将 NumPy 数组( np.ndarray )转换为一个嵌套列表(list)。这个方法非常适用于将多维数组转换为等价的列表结构,其中每个子列表对应于数组的一个维度。
# 输入 :这个方法不接受任何额外的参数,它直接作用于调用它的 NumPy 数组对象。
# 输出 :
# 返回一个列表,该列表是原始 NumPy 数组的精确复制品,但是所有的数据都被转换成了 Python 内置的数据类型(例如,整数、浮点数等)。
# 注意事项 :
# np.ndarray.tolist() 方法返回的列表是原始数组数据的一个副本,因此修改列表不会影响原始的 NumPy 数组。
# 这个方法适用于任何维度的数组,但当处理非常大的数组时,可能会消耗较多的内存,因为列表和数组在内存中是分开存储的。
# 通过使用 tolist() 方法,你可以轻松地将 NumPy 数组转换为列表,这在需要与不使用 NumPy 的代码库交互或者需要使用列表数据结构时非常有用。
# 遍历预测结果并构建 JSON 字典。
# zip(pred.tolist(), box.tolist()) 将预测结果和转换后的边界框坐标配对。 p 是预测结果的列表形式,包含边界框坐标、置信度和类别。 b 是对应的边界框坐标列表。
for p, b in zip(pred.tolist(), box.tolist()):
# jdict.append 将每个预测结果添加到 jdict 列表中,每个结果都是一个字典,包含以下键值对 :
# 'image_id' :图像 ID。
# 'category_id' :类别 ID。如果是 COCO 数据集,使用 coco91class 映射类别 ID;否则直接使用预测结果中的类别 ID。
# 'bbox' :边界框坐标,四舍五入到三位小数。
# 'score' :预测置信度,四舍五入到五位小数。
jdict.append({'image_id': image_id,
'category_id': coco91class[int(p[5])] if is_coco else int(p[5]),
'bbox': [round(x, 3) for x in b],
'score': round(p[4], 5)})
# 这段代码的主要目的是将模型的预测结果格式化为 COCO 工具包所需的 JSON 格式,以便后续可以计算模型的性能指标。通过这种方式,可以方便地将模型的预测结果与 COCO 基准或其他使用 COCO 格式的工具进行比较和评估。
# Assign all predictions as incorrect
# 这段代码是 test 函数中用于评估模型预测准确性的部分。它通过比较模型预测的边界框和真实标签的边界框来确定预测是否正确。
# 初始化正确预测的张量。创建一个形状为 (pred.shape[0], niou) 的张量,初始化为零,表示每个预测是否正确( True 表示正确)。 pred.shape[0] 是预测结果的数量, niou 是不同 IOU 阈值的数量。
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device)
# 检查是否有真实标签。 nl 是真实标签的数量。如果存在真实标签,则执行以下操作。
if nl:
# 初始化检测到的目标列表。
detected = [] # target indices
# 获取目标类别的张量。
tcls_tensor = labels[:, 0]
# target boxes
# 将目标边界框从 xywh 格式转换为 xyxy 格式。
tbox = xywh2xyxy(labels[:, 1:5])
# 调整目标边界框坐标到原始图像空间。
# def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None): -> 它用于将坐标从一张图片的形状 ( img1_shape ) 缩放到另一张图片的形状 ( img0_shape )。返回调整后的坐标。 -> return coords
scale_coords(img[si].shape[1:], tbox, shapes[si][0], shapes[si][1]) # native-space labels
# 处理混淆矩阵。
if plots:
# 如果 plots 为 True ,则更新混淆矩阵,用于后续的可视化。
# def process_batch(self, detections, labels): -> ConfusionMatrix 类的一个方法,名为 process_batch ,它用于处理一批目标检测的结果,并更新混淆矩阵。
confusion_matrix.process_batch(predn, torch.cat((labels[:, 0:1], tbox), 1))
# Per target class
# 遍历每个目标类别。获取所有唯一的目标类别,并遍历它们。
for cls in torch.unique(tcls_tensor):
# torch.nonzero(input, as_tuple=False, **kwargs) → LongTensor
# torch.nonzero 是 PyTorch 中的一个函数,它返回输入张量(tensor)中非零元素的索引。这个函数对于查找满足特定条件的元素位置非常有用,类似于 NumPy 中的 np.nonzero 函数。
# 参数 :
# input (Tensor) :输入的张量。
# as_tuple (bool,可选) :如果设置为 True ,则返回一个元组,其中每个元素是一个包含非零元素索引的张量。每个张量对应输入张量的一个维度。默认值为 False 。
# **kwargs :其他关键字参数,用于控制输出的设备和dtype等。
# 返回值 :
# 返回一个包含非零元素索引的 LongTensor。如果 as_tuple=True ,则返回一个元组。
# 注意事项 :
# 返回的索引是基于 0 的,即第一个元素的索引是 0。
# 如果输入张量中没有非零元素, torch.nonzero 将返回一个空的张量或元组。
# torch.nonzero 可以用于任何维度的张量。
# torch.nonzero 是 PyTorch 中处理张量时常用的函数之一,它在索引、筛选和条件操作中非常有用。
# 提取目标索引( ti )。
# cls == tcls_tensor :这是一个元素级别的比较操作,用于检查 tcls_tensor 中的每个元素是否等于当前类别 cls 。结果是一个布尔张量,其中 True 表示匹配的类别。
# .nonzero(as_tuple=False) :这个操作返回一个张量,其中包含原始布尔张量中 True 值的索引。 as_tuple= 参数指定返回的不是一个元组,而是一个普通的张量。
# .view(-1) :这个操作将索引张量重新塑形为一维张量。 -1 表示自动计算维度,使得结果张量中的元素总数与原始张量中的 True 值数量相同。
ti = (cls == tcls_tensor).nonzero(as_tuple=False).view(-1) # prediction indices
# 提取预测索引( pi )。
# cls == pred[:, 5] :这里比较的是当前类别 cls 是否与预测结果张量 pred 的第六列(索引为 5,因为索引从 0 开始)相等。这列通常包含了预测的类别标签。结果是一个布尔张量,其中 True 表示预测的类别与目标类别 cls 匹配。
pi = (cls == pred[:, 5]).nonzero(as_tuple=False).view(-1) # target indices
# Search for detections
# 搜索检测到的目标。如果存在预测结果,则执行以下操作。
if pi.shape[0]:
# Prediction to target ious
# 计算预测和目标之间的 IOU。使用 box_iou 函数计算预测边界框和目标边界框之间的 IOU,并找到最大的 IOU 值和对应的索引。
# def box_iou(box1, box2): -> 用于计算两个边界框集合之间的交并比(Intersection over Union, IoU)。 -> return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter)
ious, i = box_iou(predn[pi, :4], tbox[ti]).max(1) # best ious, indices
# Append detections
# 追加检测到的目标。
# 初始化已检测到的目标集合。创建一个空集合 detected_set ,用于存储已经检测到的目标的索引。使用集合可以快速检查一个元素是否已经存在,避免重复计数。
detected_set = set()
# 遍历超过 IOU 阈值的预测结果。
# ious > iouv[0] 创建一个布尔张量,其中 True 表示对应的预测结果的 IOU 值大于最低的 IOU 阈值( iouv[0] )。
# .nonzero(as_tuple=False) 返回一个包含 True 值索引的张量。
# for j in ... 遍历这些索引, j 是满足条件的预测结果的索引。
for j in (ious > iouv[0]).nonzero(as_tuple=False):
# 获取对应的目标索引。 ti 是包含所有目标索引的张量。 i[j] 是当前遍历到的索引, ti[i[j]] 获取对应的目标索引 d 。
d = ti[i[j]] # detected target
# 检查目标是否已经被检测过。 d.item() 获取 d 的值(因为 d 是一个包含单个元素的张量)。 检查这个目标是否已经被添加到 detected_set 集合中,以避免重复计数。
if d.item() not in detected_set:
# 添加检测到的目标。
# 将目标索引 d 添加到 detected_set 集合中。
detected_set.add(d.item())
# 将目标索引 d 添加到 detected 列表中,这个列表用于记录所有检测到的目标索引。
detected.append(d)
# 更新预测结果的正确性。
# pi 是包含所有预测索引的张量。 pi[j] 是当前遍历到的预测索引。 ious[j] 是对应预测结果的 IOU 值。如果这个值大于 IOU 阈值向量 iouv 中的任何值,则将 correct 张量中对应的位置设置为 True ,表示这个预测结果是正确的。
correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn
# 检查是否所有目标都已检测到。 nl 是图像中目标的总数。 如果检测到的目标数量等于图像中目标的总数,则跳出循环,因为所有目标都已检测到。
if len(detected) == nl: # all targets already located in image
break
# 这段代码的主要目的是评估模型预测的准确性,通过比较预测边界框和真实边界框之间的 IOU 来确定预测是否正确。这些信息可以用来计算精确度、召回率和其他性能指标,对于模型的评估和优化非常重要。
# Append statistics (correct, conf, pcls, tcls)
# 这段代码是在目标检测模型评估过程中,用于收集和记录每个图像的统计信息。这些统计信息通常包括预测的正确性、置信度、预测类别和真实类别。
# 收集正确性信息。
# correct 是一个布尔张量,表示每个预测是否正确。如果预测的边界框与真实边界框的交并比(IoU)超过了某个阈值,则认为该预测是正确的。 .cpu() 是一个上下文方法,用于确保数据被移动到 CPU。这是为了确保统计信息可以在不同设备之间移植,尤其是在使用 GPU 进行计算时。
# 收集预测置信度。
# pred 是模型输出的预测结果张量,其中每一行包含一个预测的结果,通常是边界框坐标、置信度和类别概率。 pred[:, 4] 提取 pred 张量中第 5 列的数据,这通常是预测的置信度。 .cpu() 确保置信度数据被移动到 CPU。
# 收集预测类别。
# pred[:, 5] 提取 pred 张量中第 6 列的数据,这通常是预测的类别。 .cpu() 确保类别数据被移动到 CPU。
# 收集真实类别。
# tcls 是一个列表,包含当前图像中所有真实目标的类别。
# 追加统计信息。 stats 是一个列表,用于存储所有图像的统计信息。 append 方法将当前图像的统计信息作为一个元组追加到 stats 列表中。这个元组包含四个元素: 预测的正确性 、 置信度 、 预测类别 和 真实类别 。
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
# Plot images
# 这段代码是在目标检测模型评估过程中,用于绘制图像及其对应的标签和预测结果的图像。
# 检查是否需要绘制图像。 plots 是一个布尔值,表示是否需要绘制图像。 batch_i < 3 限制了仅对前3个批次的图像进行绘制,以避免生成过多的图像。
if plots and batch_i < 3:
# 设置保存标签图像的文件名。 save_dir 是保存图像的目录。 f'test_batch{batch_i}_labels.jpg' 是一个格式化字符串,用于生成包含批次索引 batch_i 的文件名,用于保存带有真实标签的图像。
f = save_dir / f'test_batch{batch_i}_labels.jpg' # labels
# Thread(group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None)
# 在 Python 中, Thread 是一个类,它属于 threading 模块,用于创建和管理线程。
# 参数 :
# group :这个参数已经被废弃,不需要使用。
# target :一个可调用的对象,它将在这个线程中执行。 target 函数必须接受一个参数,即 Thread 实例本身。
# name :线程的名称。如果未提供,则线程将没有名称。
# args :一个元组,包含传递给 target 函数的参数。
# kwargs :一个字典,包含传递给 target 函数的关键字参数。
# daemon :一个布尔值,表示线程是否作为守护线程运行。如果设置为 True ,则当主程序退出时,线程也会自动退出。
# 方法 :
# start() :启动线程。线程将在 target 函数中执行。
# run() :这是一个在 Thread 类中定义的方法,可以被重写。如果提供了 target 参数,则 run() 方法不会被直接调用,而是调用 target 。
# join(timeout=None) :等待线程终止。 timeout 参数是可选的,表示等待的秒数。
# is_alive() :返回线程是否仍然活跃。
# Thread 类是 Python 中实现多线程编程的基础,允许程序同时执行多个任务。
# 在新线程中绘制标签图像。
# Thread 是 Python 的线程类,用于并发执行任务。
# target=plot_images 指定线程执行的函数,这里是 plot_images 函数,用于绘制图像。 args=(img, targets, paths, f, names) 是传递给 plot_images 函数的参数 :
# img 是当前批次的图像。 targets 是图像的真实标签。 paths 是图像的路径。 f 是保存图像的文件路径。 names 是类别名称。 daemon=True 表示该线程是守护线程,当主程序退出时,守护线程也会自动退出。
# def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
# -> 用于将一批图像及其对应的目标(边界框)绘制成一张图片,并保存。返回马赛克图像。函数返回马赛克图像的 NumPy 数组。
# -> return mosaic
Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start()
# 设置保存预测图像的文件名。类似于标签图像,这里设置保存预测结果图像的文件名。
f = save_dir / f'test_batch{batch_i}_pred.jpg' # predictions
# 这个线程用于绘制包含模型预测结果的图像。 output_to_target(out) 是一个函数,用于将模型输出 out 转换为适合绘制的目标格式。
# def output_to_target(output): -> 它将模型输出转换为目标格式 [batch_id, class_id, x, y, w, h, conf] 。将 targets 列表转换为NumPy数组并返回。 -> return np.array(targets)
Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start()
# 这段代码的主要作用是绘制图像及其对应的标签和预测结果,并将这些图像保存到指定的目录。通过在新线程中执行绘制任务,可以提高程序的效率,特别是在处理大量图像时。这种方法允许主程序继续执行其他任务,而图像绘制操作在后台进行。
# Compute statistics
# 这段代码是目标检测模型评估过程中用于计算统计数据的部分,包括精确度(precision)、召回率(recall)、平均精度(average precision, AP)等关键指标。
# 将统计信息转换为 NumPy 数组。
# stats 是一个列表,其中包含了每个图像的统计信息,如正确性、置信度、预测类别和真实类别。
# zip(*stats) 将 stats 列表中的元组重新组合,使得相同位置的元素被组合在一起,形成新的元组。
# np.concatenate(x, 0) 将每个新元组中的数组沿着第一个维度(0)连接起来,形成一个更长的数组。
# 最终, stats 变成了一个列表,其中每个元素都是一个 NumPy 数组,包含了所有图像的相应统计数据。
stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy
# 检查统计信息是否有效。检查 stats 列表是否不为空,并且第一个数组中至少有一个元素是 True (即至少有一个预测是正确的)。
if len(stats) and stats[0].any():
# 计算每个类别的 AP。
# ap_per_class 函数计算每个类别的精确度、召回率和平均精度。
# *stats 将 stats 列表中的数组作为参数传递给 ap_per_class 函数。 plot=plots 指示是否绘制相关图表。 save_dir=save_dir 指定保存图表的目录。 names=names 提供类别名称。
# def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=()):
# -> 用于计算每个类别的平均精度(Average Precision, AP)的函数,它是目标检测任务中常用的性能评估指标之一。这个函数还包含了绘制精确度-召回率曲线(Precision-Recall curve)和F1分数曲线的功能。
# -> 返回在索引 i 处的 精确率 、 召回率 、 平均精度 和 F1 分数 ,以及 转换为整数类型的类别索引 。
# -> return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32')
p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
# 提取 AP@0.5 和 AP@0.5:0.95。 ap[:, 0] 提取每个类别在 IOU 阈值为 0.5 时的平均精度(AP@0.5)。 ap.mean(1) 计算每个类别在 IOU 阈值从 0.5 到 0.95 范围内的平均精度(AP@0.5:0.95)。
ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
# 计算整体性能指标。 p.mean() 计算所有类别的精确度的平均值。 r.mean() 计算所有类别的召回率的平均值。 ap50.mean() 计算所有类别的 AP@0.5 的平均值。 ap.mean() 计算所有类别的 AP@0.5:0.95 的平均值。
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
# 计算每个类别的目标数量。 stats[3] 是包含所有图像中每个真实类别的数组。 np.bincount 计算每个类别出现的次数。 minlength=nc 确保输出数组的长度至少与类别数量 nc 相同。
nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
# 处理无效统计信息的情况。
else:
# 如果 stats 列表为空或没有任何预测是正确的,则设置 nt 为一个长度为 1 的零张量。
nt = torch.zeros(1)
# 这段代码的主要作用是计算目标检测模型的性能指标,包括精确度、召回率和平均精度。这些指标对于评估模型的检测能力至关重要。通过计算每个类别的指标,可以了解模型在不同类别上的表现,并计算整体的性能。
# 这段代码是目标检测模型评估过程中用于打印结果的部分。它输出整体的性能指标,以及每个类别的详细结果和推理速度。
# Print results
# 打印整体结果。
# 定义打印格式。定义一个格式化字符串 pf ,用于打印结果。其中 %20s 表示字符串占位符,宽度为20; %12i 表示整数占位符,宽度为12; %12.3g 表示浮点数占位符,宽度为12,保留3位有效数字。
pf = '%20s' + '%12i' * 2 + '%12.3g' * 4 # print format
# 打印整体结果。使用 pf 格式化字符串打印整体结果,包括:
# 'all' :表示这是整体结果。 seen :已处理的图像数量。 nt.sum() :所有类别的目标总数。 mp :所有类别的精确度平均值。 mr :所有类别的召回率平均值。 map50 :所有类别的 AP@0.5 平均值。 map :所有类别的 AP@0.5:0.95 平均值。
print(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
# Print results per class
# 打印每个类别的结果。
# 检查是否需要打印每个类别的结果。 verbose :如果为 True ,则打印每个类别的结果。 nc < 50 and not training :如果类别数量少于50且不在训练模式,也打印每个类别的结果。 nc > 1 :确保至少有两个类别。 len(stats) :确保有统计数据。
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
# 遍历每个类别并打印结果。 enumerate(ap_class) :遍历每个类别的 AP 数组, i 是索引, c 是类别索引。
for i, c in enumerate(ap_class):
# names[c] :获取类别名称。 nt[c] :获取类别 c 的目标数量。 p[i] :获取类别 c 的精确度。 r[i] :获取类别 c 的召回率。 ap50[i] :获取类别 c 的 AP@0.5。 ap[i] :获取类别 c 的 AP@0.5:0.95。
print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
# Print speeds
# 打印推理速度。
# 计算推理速度。 t0 、 t1 :分别表示推理和非最大抑制(NMS)的时间。 (x / seen * 1E3 for x in (t0, t1, t0 + t1)) :计算每张图像的平均推理时间、NMS时间和总时间,并将它们转换为毫秒。 + (imgsz, imgsz, batch_size) :将图像尺寸和批处理大小添加到元组中。
t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + (imgsz, imgsz, batch_size) # tuple
if not training:
# 打印推理速度。如果不在训练模式,打印推理速度。 %t 格式化元组 t ,输出推理速度、NMS速度和总速度,以及图像尺寸和批处理大小。
print('Speed: %.1f/%.1f/%.1f ms inference/NMS/total per %gx%g image at batch-size %g' % t)
# 这段代码的主要作用是输出目标检测模型的性能指标和推理速度,包括整体结果和每个类别的详细结果。这些信息对于评估模型的性能和优化模型参数非常有用。通过打印这些结果,可以直观地了解模型在不同类别上的表现和推理效率。
# Plots
# 这段代码是用于在目标检测模型评估过程中绘制混淆矩阵和记录图像到 Weights & Biases (W&B) 平台的部分。
# 检查是否需要绘制图像。 plots 是一个布尔值,表示是否需要绘制图像和混淆矩阵。
if plots:
# 绘制混淆矩阵。
# confusion_matrix 是一个对象,用于跟踪模型预测的混淆矩阵。 plot 方法绘制混淆矩阵,并将其保存到 save_dir 指定的目录。 names 是一个字典,包含类别的名称。 list(names.values()) 获取所有类别名称的列表,用于混淆矩阵的标签。
# def plot(self, save_dir='', names=()): -> ConfusionMatrix 类的一个方法,名为 plot ,它用于绘制并保存归一化的混淆矩阵的热图。
confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
# 记录图像到 W&B 。
# 检查 W&B 日志记录器是否存在。 wandb_logger 是一个对象,用于与 W&B 平台交互。 wandb_logger.wandb 是一个属性,指示是否使用 W&B 进行日志记录。
if wandb_logger and wandb_logger.wandb:
# 收集验证批次的图像。
val_batches = [wandb_logger.wandb.Image(str(f), caption=f.name) for f in sorted(save_dir.glob('test*.jpg'))]
# 记录验证批次的图像到 W&B。
wandb_logger.log({"Validation": val_batches})
# 检查是否有边界框调试图像。
if wandb_images:
# 记录边界框调试图像到 W&B。
wandb_logger.log({"Bounding Box Debugger/Images": wandb_images})
# Save JSON
# 保存预测结果到 JSON。
# 这段代码是目标检测模型评估过程中用于保存预测结果到 JSON 文件,并使用 pycocotools 计算模型性能指标(如 mAP)的部分。
# 检查是否需要保存 JSON。 save_json 是一个布尔值,表示是否需要保存预测结果到 JSON 文件。 jdict 是一个列表,包含所有预测结果的字典。 这个条件确保只有在需要保存 JSON 且有预测结果时才执行以下操作。
if save_json and len(jdict):
# 获取权重文件的名称。 weights 是模型权重的路径或路径列表。 如果 weights 是列表,取第一个路径;否则直接使用 weights 。 Path(...).stem 获取路径的文件名(不包含扩展名)。
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
# 设置注释和预测的 JSON 文件路径。
# anno_json 是 COCO 数据集的注释 JSON 文件路径。
anno_json = '../coco/annotations/instances_val2017.json' # annotations json
# pred_json 是保存预测结果的 JSON 文件路径,文件名基于模型权重文件名。
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
# 打印保存信息。
print('\nEvaluating pycocotools mAP... saving %s...' % pred_json)
# 将预测结果保存到 JSON 文件。
with open(pred_json, 'w') as f:
# 使用 json.dump 将 jdict 中的预测结果保存到 pred_json 指定的文件。
json.dump(jdict, f)
# 使用 pycocotools 计算 mAP。
# 尝试导入 pycocotools。
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
# 尝试导入 COCO API 和 COCOeval 工具。
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
# 初始化 COCO API 和 COCOeval。
# COCO(anno_json) 初始化 COCO API,加载注释数据。
anno = COCO(anno_json) # init annotations api
# anno.loadRes(pred_json) 加载预测结果。
pred = anno.loadRes(pred_json) # init predictions api
# COCOeval(anno, pred, 'bbox') 初始化 COCOeval,用于评估边界框任务。
eval = COCOeval(anno, pred, 'bbox')
# 设置评估的图像 ID。
if is_coco:
# 如果是 COCO 数据集,设置要评估的图像 ID。
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate
# 执行评估。
# evaluate 执行评估。
eval.evaluate()
# accumulate 累积评估结果。
eval.accumulate()
# summarize 总结评估结果。
eval.summarize()
# eval.stats[:2] 获取 mAP@0.5 和 mAP@0.5:0.95 的结果。
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
# 处理异常。
except Exception as e:
# 如果在执行 pycocotools 时出现异常,打印错误信息。
print(f'pycocotools unable to run: {e}')
# 这段代码的主要作用是将目标检测模型的预测结果保存到 JSON 文件,并使用 COCO API 和 COCOeval 工具计算模型的性能指标,如 mAP。这些步骤对于评估模型在 COCO 数据集上的性能至关重要。通过保存预测结果和计算 mAP,可以量化模型的检测能力,并与其他模型或基线进行比较。
# Return results
# 这段代码是目标检测模型评估过程的最后部分,用于返回模型的性能指标和结果。
# 返回结果。
# 将模型设置为浮点精度。这行代码将模型的所有参数转换为浮点数(32位)。如果模型之前被转换为半精度(16位),这一步是必要的,以确保模型在训练时使用正确的精度。
model.float() # for training
# 检查是否在训练模式外。如果不在训练模式(即在评估模式),则执行以下操作。
if not training:
# 打印保存标签的信息。
# 如果 save_txt 为 True ,则计算并打印保存到 save_dir/labels 目录下的标签文件数量。 save_dir.glob('labels/*.txt') 获取 save_dir/labels 目录下所有的 .txt 文件。 len(list(...)) 计算这些文件的数量。
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
# print 语句输出结果保存的位置和保存的标签文件数量。
print(f"Results saved to {save_dir}{s}")
# 初始化 mAP 数组。 np.zeros(nc) 创建一个长度为 nc (类别数量)的零数组。 + map 将每个元素初始化为 map 值, map 是所有类别的 mAP 平均值。
maps = np.zeros(nc) + map
# 更新每个类别的 mAP 值。 enumerate(ap_class) 遍历 ap_class 数组, i 是索引, c 是类别索引。
for i, c in enumerate(ap_class):
# maps[c] = ap[i] 更新 maps 数组中对应类别的 mAP 值。
maps[c] = ap[i]
# 返回性能指标和结果。返回一个元组,包含以下内容 :
# mp :所有类别的精确度平均值。
# mr :所有类别的召回率平均值。
# map50 :所有类别的 AP@0.5 平均值。
# map :所有类别的 mAP 平均值。
# loss.cpu() / len(dataloader) :计算每个批次的平均损失值,并转换为列表。
# maps :每个类别的 mAP 值数组。
# t :包含推理速度的元组。
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
# 这段代码的主要作用是整理和返回目标检测模型的性能指标和结果。通过返回这些信息,可以评估模型的整体性能和每个类别的检测能力。这些结果对于模型的进一步优化和调整至关重要。
# 这个函数是一个完整的评估流程,适用于目标检测任务,特别是当使用 COCO 数据集时。它提供了灵活的参数设置,以适应不同的测试需求和配置。
3.if __name__ == '__main__':
# 这段代码是一个 Python 脚本的主体部分,它使用 argparse 库来解析命令行参数。这些参数用于配置和运行目标检测模型的测试过程。
if __name__ == '__main__':
# 创建参数解析器。创建一个 ArgumentParser 对象, prog='test.py' 指定了程序的名称。
parser = argparse.ArgumentParser(prog='test.py')
# 添加参数。 parser.add_argument 方法用于添加命令行参数。 每个参数都有其名称、类型、默认值和帮助描述。
# --weights 参数用于指定模型权重文件的路径,可以指定多个文件。
parser.add_argument('--weights', nargs='+', type=str, default='yolov7.pt', help='model.pt path(s)')
# --data 参数用于指定数据集配置文件的路径。
parser.add_argument('--data', type=str, default='data/coco.yaml', help='*.data path')
# --batch-size 参数用于指定每个图像批次的大小。
parser.add_argument('--batch-size', type=int, default=32, help='size of each image batch')
# --img-size 参数用于指定推理时的图像尺寸。
parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)')
# --conf-thres 参数用于指定对象置信度的阈值。
parser.add_argument('--conf-thres', type=float, default=0.001, help='object confidence threshold')
# --iou-thres 参数用于指定非最大抑制(NMS)的 IOU 阈值。
parser.add_argument('--iou-thres', type=float, default=0.65, help='IOU threshold for NMS')
# --task 参数用于指定任务类型。
parser.add_argument('--task', default='val', help='train, val, test, speed or study')
# --device 参数用于指定使用的设备,如 GPU 或 CPU。
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
# --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 API 兼容的 JSON 结果文件。
parser.add_argument('--save-json', action='store_true', help='save a cocoapi-compatible JSON results file')
# --project 参数用于指定保存结果的项目目录。
parser.add_argument('--project', default='runs/test', help='save to project/name')
# --name 参数用于指定实验的名称。
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')
# --trace 参数用于指示是否追踪模型。
parser.add_argument('--trace', action='store_true', help='trace model')
# 解析参数。解析命令行参数,并将结果存储在 opt 对象中。
opt = parser.parse_args()
# 这行代码是 Python 中的一个位运算表达式,用于更新 opt.save_json 的值。
# 具体来说,它检查 opt.data 是否以字符串 'coco.yaml' 结尾,如果是,则将 opt.save_json 设置为 True 。
# 这个表达式利用了位或赋值运算符 |= ,它具有短路功能,只在左侧表达式的结果为 False 时才计算右侧的表达式。
# |= :位或赋值运算符。如果左侧的值为 False (即 opt.save_json 为 False ),则将右侧的表达式结果赋值给左侧变量;如果左侧的值为 True ,则保持不变。
# 逻辑解释 :
# 如果 opt.data 的值以 'coco.yaml' 结尾,说明使用的是 COCO 数据集的配置文件,因此可能需要将结果保存为 COCO API 兼容的 JSON 文件,所以 opt.save_json 被设置为 True 。
# 如果 opt.data 不以 'coco.yaml' 结尾,或者 opt.save_json 已经为 True ,则 opt.save_json 的值保持不变。
opt.save_json |= opt.data.endswith('coco.yaml')
# 检查数据文件。使用 check_file 函数检查数据配置文件是否存在。
# ef check_file(file): -> 在给定文件路径不存在时,在当前目录及其子目录中搜索文件。如果找到了文件,函数将返回文件的路径;如果未找到或有多个匹配的文件,函数将抛出异常。返回文件路径。如果文件存在且唯一,返回找到的文件路径。 -> return files[0] # return file
opt.data = check_file(opt.data) # check file
# 打印参数。打印解析后的参数,以便查看所有配置。
print(opt)
#check_requirements()
# 这段代码的主要作用是解析命令行参数,这些参数用于配置目标检测模型的测试过程。通过这些参数,用户可以灵活地指定模型权重、数据集、推理设置、结果保存选项等。这些配置信息对于模型的评估和分析至关重要。
# 这段代码是 Python 脚本中根据不同任务类型执行不同测试流程的条件语句。
# 正常运行测试。
# 如果 opt.task 是 'train' 、 'val' 或 'test' 中的一个,执行正常的测试流程。
if opt.task in ('train', 'val', 'test'): # run normally
# test 函数被调用,传入一系列参数,包括数据集配置、模型权重、批处理大小、图像尺寸、置信度阈值、IOU 阈值等。
# save_txt 参数通过 opt.save_txt | opt.save_hybrid 计算得出,如果需要保存文本结果或混合结果,则为 True 。
# save_hybrid 和 save_conf 参数直接从 opt 对象获取。
# trace 参数用于模型追踪。
test(opt.data,
opt.weights,
opt.batch_size,
opt.img_size,
opt.conf_thres,
opt.iou_thres,
opt.save_json,
opt.single_cls,
opt.augment,
opt.verbose,
save_txt=opt.save_txt | opt.save_hybrid,
save_hybrid=opt.save_hybrid,
save_conf=opt.save_conf,
trace=opt.trace,
)
# 速度基准测试。
# 如果 opt.task 是 'speed' ,执行速度基准测试。
elif opt.task == 'speed': # speed benchmarks
# 遍历 opt.weights 列表中的每个权重文件。
for w in opt.weights:
# 对每个权重文件,调用 test 函数,但关闭 JSON 保存和绘图功能。
test(opt.data, w, opt.batch_size, opt.img_size, 0.25, 0.45, save_json=False, plots=False)
# 研究测试。
# 如果 opt.task 是 'study' ,执行一系列测试以研究不同设置下的性能。
elif opt.task == 'study': # run over a range of settings and save/plot
# python test.py --task study --data coco.yaml --iou 0.65 --weights yolov7.pt
# x 是一个列表,包含一系列图像尺寸,用于测试。
x = list(range(256, 1536 + 128, 128)) # x axis (image sizes)
# 遍历 opt.weights 列表中的每个权重文件。
for w in opt.weights:
# 对每个权重文件,创建一个文件名 f 用于保存结果。
f = f'study_{Path(opt.data).stem}_{Path(w).stem}.txt' # filename to save to
y = [] # y axis
# 遍历 x 中的每个图像尺寸,调用 test 函数,关闭绘图功能,并收集结果和时间。
for i in x: # img-size
print(f'\nRunning {f} point {i}...')
r, _, t = test(opt.data, w, opt.batch_size, i, opt.conf_thres, opt.iou_thres, opt.save_json,
plots=False)
y.append(r + t) # results and times
# 使用 np.savetxt 将结果保存到文件。
np.savetxt(f, y, fmt='%10.4g') # save
# 使用 os.system 命令将所有结果文件压缩成一个 ZIP 文件。
os.system('zip -r study.zip study_*.txt')
# 调用 plot_study_txt 函数绘制结果图表。
# def plot_study_txt(path='', x=None): -> 用于绘制由 test.py 生成的 study.txt 文件中的性能指标数据。这个函数可以帮助用户可视化不同模型配置下的性能对比。
plot_study_txt(x=x) # plot
# 这段代码根据不同的任务类型( train 、 val 、 test 、 speed 、 study )执行不同的测试流程。它允许用户灵活地配置和运行模型测试,以及分析模型在不同设置下的性能。通过这种方式,用户可以深入理解模型的行为,并优化模型参数。