YOLOv5源码解读1.4-测试test.py

往期回顾:

YOLOv5源码解读1.1-整体架构_汉卿HanQ的博客-CSDN博客

YOLOv5源码解读1.2-推理detect.py_汉卿HanQ的博客-CSDN博客

YOLOv5源码解读1.3-训练train.py_汉卿HanQ的博客-CSDN博客

        学习完train.py后,就可以训练自己的数据集了,通常我们训练完数据后需要通过val(test).py取进行测试验证当前模型的mAP等超参数是否最佳,如果不是最佳则调整train直至最佳,最终通过detect.py取泛化使用。


目录

1.导入Python库

2.获取文件路径

3.自定义模块

4.save_one_txt函数

5.save_one_json函数

6.process_batch函数

7.run函数

7.1初始化/加载模型

7.2加载yaml配置

7.3加载val数据集

7.4初始化参数

7.5验证前预处理图片和target

7.6前向推理

7.7计算损失函数

7.8NMS获取anchor

7.9统计真实框和预测框

7.10画出前三个batch图片的gt和pre框

7.11计算分类效果的指标

7.12打印日志

7.13保存验证结果

7.14返回测试结果

8.设置OPT

9.main函数

10.整体代码


1.导入Python库

# ----------------------------------1.导入Python库----------------------------------
import argparse  # 解析命令行参数的库
import json  # 实现字典列表和JSON字符串之间的相互解析
import os  # 与操作系统进行交互的文件库 包含文件路径操作与解析
import sys  # sys系统模块 包含了与Python解释器和它的环境有关的函数
from pathlib import Path  # Path将str转换为Path对象 使字符串路径易于操作的模块
from threading import Thread  # python中处理多线程的库

import numpy as np  # 矩阵计算基础库
import torch  # pytorch 深度学习库
from tqdm import tqdm  # 用于直观显示进度条的一个库

2.获取文件路径

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

3.自定义模块

# ----------------------------------3.自定义模块----------------------------------

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

4.save_one_txt函数

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

5.save_one_json函数

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

6.process_batch函数

# ----------------------------------6.process_batch函数----------------------------------
"""
    计算coorect,来获取匹配预测框的IOU信息
    因为gt可能是一个类别,需要获取置信度高的进行匹配,但可能多个gt和一个anchor匹配,因此需要筛选
"""
def process_batch(detections, labels, iouv):
    """
    Return correct predictions matrix.
    返回每个预测框在10个IoU阈值上是TP还是FP
    Both sets of boxes are in (x1, y1, x2, y2) format.
    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 = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device) # 构建一个[pred_nums, 10]全为False的矩阵
    iou = box_iou(labels[:, 1:], detections[:, :4]) # 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]

    # iou超过阈值而且类别正确,则为True,返回索引
    x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5]))  # iou超过阈值而且类别正确,则为True,返回索引
    # 如果存在符合条件的anchor
    if x[0].shape[0]:  # 至少有一个TP
        # 将符合条件的位置构建成一个新的矩阵,第一列是行索引(表示gt索引),第二列是列索引(表示预测框索引),第三列是iou值
        matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()  # [label, detection, iou]
        if x[0].shape[0] > 1:
            # argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
            matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
            """
                参数return_index=True:表示会返回唯一值的索引,[0]返回的是唯一值,[1]返回的是索引
                matches[:, 1]:这里的是获取iou矩阵每个预测框的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
                这个操作的含义:每个预测框最多只能出现一次,如果有一个预测框同时和多个gt匹配,只取其最大iou的一个
            """
            # matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
            """
                matches[:, 0]:这里的是获取iou矩阵gt的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
                这个操作的含义: 每个gt也最多只能出现一次,如果一个gt同时匹配多个预测框,只取其匹配最大的那一个预测框
            """
            # 以上操作实现了为每一个gt分配一个iou最高的类别的预测框,实现一一对应

        matches = torch.Tensor(matches).to(iouv.device)
        correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv # 在correct中,只有与gt匹配的预测框才有对应的iou评价指标,其他大多数没有匹配的预测框都是全部为False
        """
            当前获得了gt与预测框的一一对应,其对于的iou可以作为评价指标,构建一个评价矩阵
            需要注意,这里的matches[:, 1]表示的是为对应的预测框来赋予其iou所能达到的程度,也就是iouv的评价指标
        """

    return correct

7.run函数

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

        half &= device.type != 'cpu'  # 如果不是cpu则精度减半
        model.half() if half else model.float()
    else:  # 通过val调用run
        device = select_device(device, batch_size=batch_size)# 调用torch_utils中的select_device选择执行程序时的设备

        # Directories
        save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)  # 调用genera.py的increment_path生成save_dir文件路径
        (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)  # mkdir创建路径中的最后一级目录

        # Load model
        model = DetectMultiBackend(weights, device=device, dnn=dnn)
        stride, pt = model.stride, model.pt
        imgsz = check_img_size(imgsz, s=stride)  # 检查图像分辨率能否被32整除
        half &= pt and device.type != 'cpu'  # 如果不是cpu 图片和模型半精度 整体半进度
        if pt:
            model.model.half() if half else model.model.float()
        else:
            half = False
            batch_size = 1  # export.py models default to batch-size 1
            device = torch.device('cpu')
            # 打印消耗时间
            LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')

        # Data
        data = check_dataset(data)  # check
7.2加载yaml配置
    # ----------------------------------7.2 加载yaml配置---------------------------------
    # Configure
    model.eval() # 将默许下转换为测试模型 固定dropout和BN
    is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt')  # 判断是否是coco数据集
    nc = 1 if single_cls else int(data['nc'])  # 确定检测的类别数目
    iouv = torch.linspace(0.5, 0.95, 10).to(device)  # 计算mAP相关参数
    niou = iouv.numel() # numel为pytorch预制函数 获取张量中的元素个数
7.3加载val数据集
   # ----------------------------------7.3 加载val数据集---------------------------------
    # Dataloader
    if not training:
        if pt and device.type != 'cpu':
            model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters())))  # 创建一张全为0的图片(四维张量)
        pad = 0.0 if task == 'speed' else 0.5
        task = task if task in ('train', 'val', 'test') else 'val'  # path to train/val/test images
        dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
                                       prefix=colorstr(f'{task}: '))[0] #调用dataset.py中的create_dataloader创建dataloader
7.4初始化参数
    # ----------------------------------7.4 初始化参数---------------------------------
    seen = 0 # 初始化以及完成测试的图片数量
    confusion_matrix = ConfusionMatrix(nc=nc) # matrics函数存储混淆矩阵
    names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)} # 获取数据集中所以类别的类名
    class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) # 获取coco数据集的类别索引
    s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95') # 设置tqdm进度条显示信息
    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 # 初始化detection中各个指标值
    loss = torch.zeros(3, device=device) # 初始网络训练的loss
    jdict, stats, ap, ap_class = [], [], [], [] # 初始化json文件涉及到的字典 统计信息 AP classicAP 图片
    pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # progress bar
7.5验证前预处理图片和target
    # ----------------------------------7.5 验证前预处理图片和target---------------------------------
    for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
        t1 = time_sync() # 获取当前使劲啊
        if pt:
            im = im.to(device, non_blocking=True) # 将图片拷贝到device(GPU)上
            targets = targets.to(device)# 对targets同样操作
        im = im.half() if half else im.float()  # 将精度从64转化为32
        im /= 255  # 图片归一化
        nb, _, height, width = im.shape  # 四个比哪里分布表示 batch_size 通道数 图像高度 图像宽度
        t2 = time_sync() # 获取当前时间
        dt[0] += t2 - t1 # 累计处理消耗时间
7.6前向推理
# ----------------------------------7.6 前向推理 ---------------------------------
        """
            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]
        """
        # Inference
        out, train_out = model(im) if training else model(im, augment=augment, val=True)  # inference, loss outputs
        dt[1] += time_sync() - t2 # 累计前向推理时间
7.7计算损失函数
# ----------------------------------7.7 计算损失函数---------------------------------
        # Loss
        if compute_loss:
            loss += compute_loss([x.float() for x in train_out], targets)[1]  # 对 box, obj, cls的损失计算
7.8NMS获取anchor
# ----------------------------------7.8 NMS获取anchor---------------------------------
        # NMS
        targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device)  # 将真实框target的xywh映射到真实的图像尺寸
        lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []  # 提取bach中的每一张图片的label
        t3 = time_sync() # 计算NMS所需时间
        out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls) # 非极大值抑制操作
        dt[2] += time_sync() - t3 # 计算累计NMS时间
7.9统计真实框和预测框
 ----------------------------------7.9 统计真实框和预测框---------------------------------
        # Metrics
        for si, pred in enumerate(out):# si代表第i张图片 pred是对图片预测的label信息
            labels = targets[targets[:, 0] == si, 1:] # 获取第si张图片的gt标签信息 包括 class x y w h target[:,0]为标签术语那张图片编号
            nl = len(labels) # nl为图片检测到的目标个数
            tcls = labels[:, 0].tolist() if nl else []  # tcls为检测到的目标的类别 label矩阵的第一列
            path, shape = Path(paths[si]), shapes[si][0] # 第si张图片的文件路径
            seen += 1 # 统计图片数量

            # 如果预测为空 则添加空的信息到stats中
            if len(pred) == 0:
                if nl: # 预测为空的同时有label信息
                    # stats初始化为一个空列表[] 此处廷加一个空信息
                    # 添加的每一个元素均为tuple 第二个第三个为一个空的张量tensor
                    stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
                continue

            # Predictions 预测
            if single_cls:
                pred[:, 5] = 0
            predn = pred.clone() # 对pred进行深复制
            scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1])  # 调整图片大小为原大小

            # Evaluate anchor评估
            if nl:
                tbox = xywh2xyxy(labels[:, 1:5])  # 获取xyxy格式的框
                scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1])  # 将图片调整为原图大小
                labelsn = torch.cat((labels[:, 0:1], tbox), 1)  # 处理完gt的尺寸 信息 重新构建成(cls xyxy)的格式
                correct = process_batch(predn, labelsn, iouv) # 对当前预测框与gt进行意义匹配,并在预测框的位子上获取iou的信息评分,其余没有匹配的为False
                if plots:
                    confusion_matrix.process_batch(predn, labelsn) # 计算混淆矩阵
            else:
                correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool) # 返回一个形状为pred.shape[0] 类型为torch.dtype 值为0的张量anchor
            stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))  # 每张图片结果统计到stats里

            # Save/log
            if save_txt:
                save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt')) # 保存预测信息到txt文件中
            if save_json:
                save_one_json(predn, jdict, path, class_map)  # 保存预测信息到json字典
            callbacks.run('on_val_image_end', pred, predn, path, names, im[si
7.10画出前三个batch图片的gt和pre框
# ----------------------------------7.10 画出前三个batch图片的gt和pre框---------------------------------
        """
            画出前三个batch的ground truch 和 预测框 prediction 两个图一起保存
            gt 真实框 人工标注
            pred 预测框 模型计算
        """
        # Plot images
        if plots and batch_i < 3:
            f = save_dir / f'val_batch{batch_i}_labels.jpg'  # labels
            Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
            """
                Thread()函数为创建一个新的线程来执行这个函数 函数为plots.py中的plot_images函数
                target: 执行的函数  args: 传入的函数参数  daemon: 当主线程结束后, 由他创建的子线程Thread也已经自动结束了
                start(): 启动线程  当thread一启动的时候, 就会运行我们自己定义的这个函数plot_images
                如果在plot_images里面打开断点调试, 可以发现子线程暂停, 但是主线程还是在正常的训练(还是正常的跑)
            """
            f = save_dir / f'val_batch{batch_i}_pred.jpg'  #  传入plot_images之前需要改变pred格式 target则不需要更改
            Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
7.11计算分类效果的指标
# ----------------------------------7.11 计算分类效果的指标---------------------------------
    # Compute metrics
    stats = [np.concatenate(x, 0) for x in zip(*stats)]  # 将stats列表信息拼接
    if len(stats) and stats[0].any():
        p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
        """
            p 最大平均f1时每个类别的precision
            r 最大平均f1时每个类别的recall
            ap 数据集每个类别在10个iou阈值下的mAP
            f1 最大平均f1时每个类别的f1
            ap_class 数据集中所有类别的index
        """
        ap50, ap = ap[:, 0], ap.mean(1)  # AP@0.5, AP@0.5:0.95
        """
            ap50 mAP@0.5
            ap mAP@0.5:0.95
        """
        mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
        """
            mp 所有类别的平均precision
            mr 所有类别的平均recall
            map50 所有类别的平均mAP@0.5
            map 所有类别的平均mAP@0.5:0.95
        """
        nt = np.bincount(stats[3].astype(np.int64), minlength=nc)  # 统计整个数据集的gt框中数据集各个类别的个数
    else:
        nt = torch.zeros(1)
7.12打印日志
# ----------------------------------7.12打印日志 ---------------------------------
    # Print results 按照这个格式打印测试过程
    pf = '%20s' + '%11i' * 2 + '%11.3g' * 4  # print format
    LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))

    # Print results per class 打印美俄类别对应的类别指标
    if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
        for i, c in enumerate(ap_class):
            LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))

    # Print speeds 打印 推断/NMS/总过程 在每个batch上的消耗时间
    t = tuple(x / seen * 1E3 for x in dt)  # speeds per image
    if not training:
        shape = (batch_size, 3, imgsz, imgsz)
        LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)
7.13保存验证结果
# ----------------------------------7.13 保存验证结果---------------------------------
    # Plots 画图
    if plots:
        confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) # 混淆矩阵绘制
        callbacks.run('on_val_end') # 日志记录 和 记录一些图片

    # Save JSON 采用之前保存的json文件预测结果 通过coco的api评估指标
    if save_json and len(jdict):
        w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else ''  # weights
        anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json')  # 注释的json格式
        pred_json = str(save_dir / f"{w}_predictions.json")  # 预测的json格式
        LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') # 打印coco的api评估各个指标并保存
        with open(pred_json, 'w') as f: # 只写打开pre_json
            json.dump(jdict, f) # 将dict序列化为json

        try:  # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
            check_requirements(['pycocotools'])
            # 利用coco官方工具评测库
            from pycocotools.coco import COCO
            from pycocotools.cocoeval import COCOeval

            anno = COCO(anno_json)  # 获取并初始化测试集标签的json文件
            pred = anno.loadRes(pred_json)  # 初始化预测框文件
            eval = COCOeval(anno, pred, 'bbox') #创建评估器
            if is_coco:
                eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files]  # image IDs to evaluate
            eval.evaluate() # 评估
            eval.accumulate()
            eval.summarize() # 展示结果
            map, map50 = eval.stats[:2]  # update results (mAP@0.5:0.95, mAP@0.5)
        except Exception as e:
            LOGGER.info(f'pycocotools unable to run: {e}')
7.14返回测试结果
 # ----------------------------------7.14 返回测试结果---------------------------------
    # Return results
    model.float()  # 将模型转化为适用于训练状态
    if not training: # 如果不是训练过程则将结果保存到对应的路径
        s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
        LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") # 打印保存结果
    maps = np.zeros(nc) + map
    for i, c in enumerate(ap_class):
        maps[c] = ap[i]
    return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t # 返回测试结果

8.设置OPT

# ----------------------------------8.设置OPT----------------------------------
def parse_opt():
    parser = argparse.ArgumentParser()
    # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
    # 模型的权重文件地址yolov5s.pt
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
    # 前向传播的批次大小 默认32
    parser.add_argument('--batch-size', type=int, default=32, help='batch size')
    # 输入网络的图片分辨率 默认640
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
    # object置信度阈值 默认0.001
    parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
    # 进行NMS时IOU的阈值 默认0.6
    parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
    # 设置测试的类型 有train, val, test, speed or study几种 默认val
    parser.add_argument('--task', default='val', help='train, val, test, speed or study')
    # 测试的设备
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    # 数据集是否只用一个类别 默认False
    parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
    # 测试是否使用TTA Test Time Augment 默认False
    parser.add_argument('--augment', action='store_true', help='augmented inference')
    # 是否打印出每个类别的mAP 默认False
    parser.add_argument('--verbose', action='store_true', help='report mAP by class')
    # 是否以txt文件的形式保存模型预测的框坐标, 默认False
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
    # 保存label+prediction杂交结果到对应.txt,默认False
    parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
    # 保存置信度
    parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
    # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签) 默认False
    parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
    # 测试保存的源文件 默认runs/val
    parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
    # 测试保存的文件地址 默认exp  保存在runs/val/exp下
    parser.add_argument('--name', default='exp', help='save to project/name')
    # 是否存在当前文件 默认False 一般是 no exist-ok 连用  所以一般都要重新创建文件夹
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    # 是否使用半精度推理 默认False
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
    # 是否使用 OpenCV DNN对ONNX 模型推理
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')

    opt = parser.parse_args() # 解析上述参数
    opt.data = check_yaml(opt.data)
    # |或 左右两个变量有一个为True 左边变量就为True
    opt.save_json |= opt.data.endswith('coco.yaml')
    opt.save_txt |= opt.save_hybrid
    print_args(FILE.stem, opt)
    return opt

9.main函数

# ----------------------------------9.main函数----------------------------------
def main(opt):
    check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) # 检测requirements文件中需要的包是否安装好了

    # 如果task in ['train', 'val', 'test']就正常测试 训练集/验证集/测试集
    if opt.task in ('train', 'val', 'test'):  # run normally
        if opt.conf_thres > 0.001:  # https://github.com/ultralytics/yolov5/issues/1466
            LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
        run(**vars(opt))

    else:
        weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
        opt.half = True  # FP16 for fastest results
        # 如果opt.task == 'speed' 就测试yolov5系列和yolov3-spp各个模型的速度评估
        if opt.task == 'speed':  # speed benchmarks
            # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
            opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
            for opt.weights in weights:
                run(**vars(opt), plots=False)

        # 如果opt.task = ['study']就评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
        elif opt.task == 'study':  # speed vs mAP benchmarks
            # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
            for opt.weights in weights:
                f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt'  # 保存的文件名
                x, y = list(range(256, 1536 + 128, 128)), []  # x坐标轴和y坐标
                for opt.imgsz in x:  # img-size
                    LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
                    r, _, t = run(**vars(opt), plots=False)
                    y.append(r + t)  # 返回相关结果和时间
                np.savetxt(f, y, fmt='%10.4g')  # 将y输出保存
            os.system('zip -r study.zip study_*.txt') # 命令行执行命令将study文件进行压缩
            plot_val_study(x=x)  # 调用plots.py中的函数 可视化各个指标


10.整体代码

# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Validate a trained YOLOv5 model accuracy on a custom dataset

Usage:
    $ python path/to/val.py --data coco128.yaml --weights yolov5s.pt --img 640
"""
# ----------------------------------1.导入Python库----------------------------------
import argparse  # 解析命令行参数的库
import json  # 实现字典列表和JSON字符串之间的相互解析
import os  # 与操作系统进行交互的文件库 包含文件路径操作与解析
import sys  # sys系统模块 包含了与Python解释器和它的环境有关的函数
from pathlib import Path  # Path将str转换为Path对象 使字符串路径易于操作的模块
from threading import Thread  # python中处理多线程的库

import numpy as np  # 矩阵计算基础库
import torch  # pytorch 深度学习库
from tqdm import tqdm  # 用于直观显示进度条的一个库
# ----------------------------------2.获取文件路径----------------------------------
FILE = Path(__file__).resolve()# __file__指的是当前文件(即val.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/val.py
ROOT = FILE.parents[0]  # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
    sys.path.append(str(ROOT))  # add ROOT to PATH 把ROOT添加到运行路径上
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative ROOT设置为相对路径

# ----------------------------------3.自定义模块----------------------------------

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

# ----------------------------------4.save_one_txt函数----------------------------------
"""
    保存预测信息到txt文件
"""
def save_one_txt(predn, save_conf, shape, file):
    gn = torch.tensor(shape)[[1, 0, 1, 0]]   # gn = [w, h, w, h] 对应图片的宽高  用于后面归一化
    # 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
    for *xyxy, conf, cls in predn.tolist():
        xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # 将xyxy(左上角+右下角)格式转为xywh(中心点+宽高)格式,并归一化,转化为列表再保存
        line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # line的形式是: "类别 xywh",若save_conf为true,则line的形式是:"类别 xywh 置信度"
        # 将上述test得到的信息输出保存 输出为xywh格式 coco数据格式也为xywh格式
        with open(file, 'a') as f:
            f.write(('%g ' * len(line)).rstrip() % line + '\n') # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
# ----------------------------------5.save_one_json函数----------------------------------
"""
    保存预测信息到coco格式的json字典
    image_id 图片id
    category_id 类别
    bbox anchor坐标
    score 预测得分
"""
def save_one_json(predn, jdict, path, class_map):
    # 储存格式 {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
    image_id = int(path.stem) if path.stem.isnumeric() else path.stem # 获取图片id
    box = xyxy2xywh(predn[:, :4])  # 获取预测框 并将xyxy转为xywh格式
    box[:, :2] -= box[:, 2:] / 2  # xy center to top-left corner
    # 序列解包
    for p, b in zip(predn.tolist(), box.tolist()):
        jdict.append({'image_id': image_id,  # 图片id 即属于哪张图片
                      'category_id': class_map[int(p[5])],  # 类别 coco91class()从索引0~79映射到索引0~90
                      'bbox': [round(x, 3) for x in b],  # 预测框坐标
                      'score': round(p[4], 5)})  # 预测得分

# ----------------------------------6.process_batch函数----------------------------------
"""
    计算coorect,来获取匹配预测框的IOU信息
    因为gt可能是一个类别,需要获取置信度高的进行匹配,但可能多个gt和一个anchor匹配,因此需要筛选
"""
def process_batch(detections, labels, iouv):
    """
    Return correct predictions matrix.
    返回每个预测框在10个IoU阈值上是TP还是FP
    Both sets of boxes are in (x1, y1, x2, y2) format.
    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 = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device) # 构建一个[pred_nums, 10]全为False的矩阵
    iou = box_iou(labels[:, 1:], detections[:, :4]) # 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]

    # iou超过阈值而且类别正确,则为True,返回索引
    x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5]))  # iou超过阈值而且类别正确,则为True,返回索引
    # 如果存在符合条件的anchor
    if x[0].shape[0]:  # 至少有一个TP
        # 将符合条件的位置构建成一个新的矩阵,第一列是行索引(表示gt索引),第二列是列索引(表示预测框索引),第三列是iou值
        matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()  # [label, detection, iou]
        if x[0].shape[0] > 1:
            # argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
            matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
            """
                参数return_index=True:表示会返回唯一值的索引,[0]返回的是唯一值,[1]返回的是索引
                matches[:, 1]:这里的是获取iou矩阵每个预测框的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
                这个操作的含义:每个预测框最多只能出现一次,如果有一个预测框同时和多个gt匹配,只取其最大iou的一个
            """
            # matches = matches[matches[:, 2].argsort()[::-1]]
            matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
            """
                matches[:, 0]:这里的是获取iou矩阵gt的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
                这个操作的含义: 每个gt也最多只能出现一次,如果一个gt同时匹配多个预测框,只取其匹配最大的那一个预测框
            """
            # 以上操作实现了为每一个gt分配一个iou最高的类别的预测框,实现一一对应

        matches = torch.Tensor(matches).to(iouv.device)
        correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv # 在correct中,只有与gt匹配的预测框才有对应的iou评价指标,其他大多数没有匹配的预测框都是全部为False
        """
            当前获得了gt与预测框的一一对应,其对于的iou可以作为评价指标,构建一个评价矩阵
            需要注意,这里的matches[:, 1]表示的是为对应的预测框来赋予其iou所能达到的程度,也就是iouv的评价指标
        """

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

    # ----------------------------------7.1 初始化/加载模型---------------------------------
    training = model is not None
    if training:  # called by train.py 通过train.py调用run函数
        device, pt = next(model.parameters()).device, True  # get model device, PyTorch model

        half &= device.type != 'cpu'  # 如果不是cpu则精度减半
        model.half() if half else model.float()
    else:  # 通过val调用run
        device = select_device(device, batch_size=batch_size)# 调用torch_utils中的select_device选择执行程序时的设备

        # Directories
        save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)  # 调用genera.py的increment_path生成save_dir文件路径
        (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)  # mkdir创建路径中的最后一级目录

        # Load model
        model = DetectMultiBackend(weights, device=device, dnn=dnn)
        stride, pt = model.stride, model.pt
        imgsz = check_img_size(imgsz, s=stride)  # 检查图像分辨率能否被32整除
        half &= pt and device.type != 'cpu'  # 如果不是cpu 图片和模型半精度 整体半进度
        if pt:
            model.model.half() if half else model.model.float()
        else:
            half = False
            batch_size = 1  # export.py models default to batch-size 1
            device = torch.device('cpu')
            # 打印消耗时间
            LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')

        # Data
        data = check_dataset(data)  # check
    # ----------------------------------7.2 加载yaml配置---------------------------------
    # Configure
    model.eval() # 将默许下转换为测试模型 固定dropout和BN
    is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt')  # 判断是否是coco数据集
    nc = 1 if single_cls else int(data['nc'])  # 确定检测的类别数目
    iouv = torch.linspace(0.5, 0.95, 10).to(device)  # 计算mAP相关参数
    niou = iouv.numel() # numel为pytorch预制函数 获取张量中的元素个数

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

    # ----------------------------------7.4 初始化参数---------------------------------
    seen = 0 # 初始化以及完成测试的图片数量
    confusion_matrix = ConfusionMatrix(nc=nc) # matrics函数存储混淆矩阵
    names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)} # 获取数据集中所以类别的类名
    class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) # 获取coco数据集的类别索引
    s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95') # 设置tqdm进度条显示信息
    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 # 初始化detection中各个指标值
    loss = torch.zeros(3, device=device) # 初始网络训练的loss
    jdict, stats, ap, ap_class = [], [], [], [] # 初始化json文件涉及到的字典 统计信息 AP classicAP 图片
    pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # progress bar

    # ----------------------------------7.5 验证前预处理图片和target---------------------------------
    for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
        t1 = time_sync() # 获取当前使劲啊
        if pt:
            im = im.to(device, non_blocking=True) # 将图片拷贝到device(GPU)上
            targets = targets.to(device)# 对targets同样操作
        im = im.half() if half else im.float()  # 将精度从64转化为32
        im /= 255  # 图片归一化
        nb, _, height, width = im.shape  # 四个比哪里分布表示 batch_size 通道数 图像高度 图像宽度
        t2 = time_sync() # 获取当前时间
        dt[0] += t2 - t1 # 累计处理消耗时间

        # ----------------------------------7.6 前向推理 ---------------------------------
        """
            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]
        """
        # Inference
        out, train_out = model(im) if training else model(im, augment=augment, val=True)  # inference, loss outputs
        dt[1] += time_sync() - t2 # 累计前向推理时间

        # ----------------------------------7.7 计算损失函数---------------------------------
        # Loss
        if compute_loss:
            loss += compute_loss([x.float() for x in train_out], targets)[1]  # 对 box, obj, cls的损失计算

        # ----------------------------------7.8 NMS获取anchor---------------------------------
        # NMS
        targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device)  # 将真实框target的xywh映射到真实的图像尺寸
        lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []  # 提取bach中的每一张图片的label
        t3 = time_sync() # 计算NMS所需时间
        out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls) # 非极大值抑制操作
        dt[2] += time_sync() - t3 # 计算累计NMS时间

        # ----------------------------------7.9 统计真实框和预测框---------------------------------
        # Metrics
        for si, pred in enumerate(out):# si代表第i张图片 pred是对图片预测的label信息
            labels = targets[targets[:, 0] == si, 1:] # 获取第si张图片的gt标签信息 包括 class x y w h target[:,0]为标签术语那张图片编号
            nl = len(labels) # nl为图片检测到的目标个数
            tcls = labels[:, 0].tolist() if nl else []  # tcls为检测到的目标的类别 label矩阵的第一列
            path, shape = Path(paths[si]), shapes[si][0] # 第si张图片的文件路径
            seen += 1 # 统计图片数量

            # 如果预测为空 则添加空的信息到stats中
            if len(pred) == 0:
                if nl: # 预测为空的同时有label信息
                    # stats初始化为一个空列表[] 此处廷加一个空信息
                    # 添加的每一个元素均为tuple 第二个第三个为一个空的张量tensor
                    stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
                continue

            # Predictions 预测
            if single_cls:
                pred[:, 5] = 0
            predn = pred.clone() # 对pred进行深复制
            scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1])  # 调整图片大小为原大小

            # Evaluate anchor评估
            if nl:
                tbox = xywh2xyxy(labels[:, 1:5])  # 获取xyxy格式的框
                scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1])  # 将图片调整为原图大小
                labelsn = torch.cat((labels[:, 0:1], tbox), 1)  # 处理完gt的尺寸 信息 重新构建成(cls xyxy)的格式
                correct = process_batch(predn, labelsn, iouv) # 对当前预测框与gt进行意义匹配,并在预测框的位子上获取iou的信息评分,其余没有匹配的为False
                if plots:
                    confusion_matrix.process_batch(predn, labelsn) # 计算混淆矩阵
            else:
                correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool) # 返回一个形状为pred.shape[0] 类型为torch.dtype 值为0的张量anchor
            stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))  # 每张图片结果统计到stats里

            # Save/log
            if save_txt:
                save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt')) # 保存预测信息到txt文件中
            if save_json:
                save_one_json(predn, jdict, path, class_map)  # 保存预测信息到json字典
            callbacks.run('on_val_image_end', pred, predn, path, names, im[si])

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

    # ----------------------------------7.11 计算分类效果的指标---------------------------------
    # Compute metrics
    stats = [np.concatenate(x, 0) for x in zip(*stats)]  # 将stats列表信息拼接
    if len(stats) and stats[0].any():
        p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
        """
            p 最大平均f1时每个类别的precision
            r 最大平均f1时每个类别的recall
            ap 数据集每个类别在10个iou阈值下的mAP
            f1 最大平均f1时每个类别的f1
            ap_class 数据集中所有类别的index
        """
        ap50, ap = ap[:, 0], ap.mean(1)  # AP@0.5, AP@0.5:0.95
        """
            ap50 mAP@0.5
            ap mAP@0.5:0.95
        """
        mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
        """
            mp 所有类别的平均precision
            mr 所有类别的平均recall
            map50 所有类别的平均mAP@0.5
            map 所有类别的平均mAP@0.5:0.95
        """
        nt = np.bincount(stats[3].astype(np.int64), minlength=nc)  # 统计整个数据集的gt框中数据集各个类别的个数
    else:
        nt = torch.zeros(1)

    # ----------------------------------7.12打印日志 ---------------------------------
    # Print results 按照这个格式打印测试过程
    pf = '%20s' + '%11i' * 2 + '%11.3g' * 4  # print format
    LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))

    # Print results per class 打印美俄类别对应的类别指标
    if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
        for i, c in enumerate(ap_class):
            LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))

    # Print speeds 打印 推断/NMS/总过程 在每个batch上的消耗时间
    t = tuple(x / seen * 1E3 for x in dt)  # speeds per image
    if not training:
        shape = (batch_size, 3, imgsz, imgsz)
        LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)

    # ----------------------------------7.13 保存验证结果---------------------------------
    # Plots 画图
    if plots:
        confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) # 混淆矩阵绘制
        callbacks.run('on_val_end') # 日志记录 和 记录一些图片

    # Save JSON 采用之前保存的json文件预测结果 通过coco的api评估指标
    if save_json and len(jdict):
        w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else ''  # weights
        anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json')  # 注释的json格式
        pred_json = str(save_dir / f"{w}_predictions.json")  # 预测的json格式
        LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') # 打印coco的api评估各个指标并保存
        with open(pred_json, 'w') as f: # 只写打开pre_json
            json.dump(jdict, f) # 将dict序列化为json

        try:  # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
            check_requirements(['pycocotools'])
            # 利用coco官方工具评测库
            from pycocotools.coco import COCO
            from pycocotools.cocoeval import COCOeval

            anno = COCO(anno_json)  # 获取并初始化测试集标签的json文件
            pred = anno.loadRes(pred_json)  # 初始化预测框文件
            eval = COCOeval(anno, pred, 'bbox') #创建评估器
            if is_coco:
                eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files]  # image IDs to evaluate
            eval.evaluate() # 评估
            eval.accumulate()
            eval.summarize() # 展示结果
            map, map50 = eval.stats[:2]  # update results (mAP@0.5:0.95, mAP@0.5)
        except Exception as e:
            LOGGER.info(f'pycocotools unable to run: {e}')

    # ----------------------------------7.14 返回测试结果---------------------------------
    # Return results
    model.float()  # 将模型转化为适用于训练状态
    if not training: # 如果不是训练过程则将结果保存到对应的路径
        s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
        LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") # 打印保存结果
    maps = np.zeros(nc) + map
    for i, c in enumerate(ap_class):
        maps[c] = ap[i]
    return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t # 返回测试结果

# ----------------------------------8.设置OPT----------------------------------
def parse_opt():
    parser = argparse.ArgumentParser()
    # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
    # 模型的权重文件地址yolov5s.pt
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
    # 前向传播的批次大小 默认32
    parser.add_argument('--batch-size', type=int, default=32, help='batch size')
    # 输入网络的图片分辨率 默认640
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
    # object置信度阈值 默认0.001
    parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
    # 进行NMS时IOU的阈值 默认0.6
    parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
    # 设置测试的类型 有train, val, test, speed or study几种 默认val
    parser.add_argument('--task', default='val', help='train, val, test, speed or study')
    # 测试的设备
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    # 数据集是否只用一个类别 默认False
    parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
    # 测试是否使用TTA Test Time Augment 默认False
    parser.add_argument('--augment', action='store_true', help='augmented inference')
    # 是否打印出每个类别的mAP 默认False
    parser.add_argument('--verbose', action='store_true', help='report mAP by class')
    # 是否以txt文件的形式保存模型预测的框坐标, 默认False
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
    # 保存label+prediction杂交结果到对应.txt,默认False
    parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
    # 保存置信度
    parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
    # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签) 默认False
    parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
    # 测试保存的源文件 默认runs/val
    parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
    # 测试保存的文件地址 默认exp  保存在runs/val/exp下
    parser.add_argument('--name', default='exp', help='save to project/name')
    # 是否存在当前文件 默认False 一般是 no exist-ok 连用  所以一般都要重新创建文件夹
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    # 是否使用半精度推理 默认False
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
    # 是否使用 OpenCV DNN对ONNX 模型推理
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')

    opt = parser.parse_args() # 解析上述参数
    opt.data = check_yaml(opt.data)
    # |或 左右两个变量有一个为True 左边变量就为True
    opt.save_json |= opt.data.endswith('coco.yaml')
    opt.save_txt |= opt.save_hybrid
    print_args(FILE.stem, opt)
    return opt

# ----------------------------------9.main函数----------------------------------
def main(opt):
    check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) # 检测requirements文件中需要的包是否安装好了

    # 如果task in ['train', 'val', 'test']就正常测试 训练集/验证集/测试集
    if opt.task in ('train', 'val', 'test'):  # run normally
        if opt.conf_thres > 0.001:  # https://github.com/ultralytics/yolov5/issues/1466
            LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
        run(**vars(opt))

    else:
        weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
        opt.half = True  # FP16 for fastest results
        # 如果opt.task == 'speed' 就测试yolov5系列和yolov3-spp各个模型的速度评估
        if opt.task == 'speed':  # speed benchmarks
            # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
            opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
            for opt.weights in weights:
                run(**vars(opt), plots=False)

        # 如果opt.task = ['study']就评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
        elif opt.task == 'study':  # speed vs mAP benchmarks
            # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
            for opt.weights in weights:
                f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt'  # 保存的文件名
                x, y = list(range(256, 1536 + 128, 128)), []  # x坐标轴和y坐标
                for opt.imgsz in x:  # img-size
                    LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
                    r, _, t = run(**vars(opt), plots=False)
                    y.append(r + t)  # 返回相关结果和时间
                np.savetxt(f, y, fmt='%10.4g')  # 将y输出保存
            os.system('zip -r study.zip study_*.txt') # 命令行执行命令将study文件进行压缩
            plot_val_study(x=x)  # 调用plots.py中的函数 可视化各个指标


# python val.py --data data/mask_data.yaml --weights runs/train/exp_yolov5s/weights/best.pt --img 640
if __name__ == "__main__":
    opt = parse_opt()
    main(opt)

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值