【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析

【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析


文章目录


前言

代码路径:ultralytics\models\yolo\detect\val.py

这段代码是Ultralytics YOLO框架中目标检测模型专用验证器DetectionValidator的核心实现,继承自基础验证器BaseValidator,专门适配YOLO目标检测的验证特性(如检测指标计算、预测结果NMS后处理、多卡验证指标聚合、COCO/LVIS标准评估适配),封装了从「数据集构建→数据加载→预处理→指标初始化→预测后处理→指标更新→结果可视化→评估报告生成」的全流程验证逻辑,是YOLO检测模型验证的核心组件,负责输出模型性能指标(如mAP、Precision、Recall)以评估训练效果。

YOLOv8-Ultralytics 系列文章目录


所需的库和模块

python 复制代码
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# 引入未来版本的类型注解支持,提升代码类型提示和静态检查能力
from __future__ import annotations

# 导入操作系统路径、路径处理模块(用于结果保存路径拼接)
import os
from pathlib import Path
# 导入类型注解模块(定义字典/列表的类型约束)
from typing import Any

# 导入数值计算、PyTorch核心、PyTorch分布式训练模块(支持多GPU验证指标聚合)
import numpy as np
import torch
import torch.distributed as dist

# 从ultralytics数据模块导入:数据加载器构建、YOLO数据集构建、COCO类别映射工具
from ultralytics.data import build_dataloader, build_yolo_dataset, converter
# 从ultralytics引擎模块导入基础验证器基类(提供通用验证流程)
from ultralytics.engine.validator import BaseValidator
# 从ultralytics工具模块导入:日志器、分布式进程排名、非极大值抑制(NMS)、通用操作函数
from ultralytics.utils import LOGGER, RANK, nms, ops
# 从ultralytics工具检查模块导入依赖检查函数(验证COCO评估依赖)
from ultralytics.utils.checks import check_requirements
# 从ultralytics工具指标模块导入:混淆矩阵、检测指标计算器、IoU计算函数
from ultralytics.utils.metrics import ConfusionMatrix, DetMetrics, box_iou
# 从ultralytics工具绘图模块导入验证样本可视化函数
from ultralytics.utils.plotting import plot_images

DetectionValidator 类

整体概览

项目 详情
类名 DetectionValidator
父类 BaseValidator(Ultralytics 通用验证器,提供验证循环、数据加载、基础指标计算能力)
核心定位 YOLO 目标检测模型专用验证器,负责检测任务的预测后处理、指标计算(mAP/PR)、结果可视化与保存
核心依赖模块 ultralytics.data(数据集构建)、ultralytics.utils(NMS/IOU/指标/绘图)、torch.distributed(分布式聚合)、faster_coco_eval(COCO指标评估)
典型使用流程 初始化→预处理批次→初始化指标→模型推理→预测后处理→更新指标→聚合分布式统计→计算最终指标→可视化/保存结果
关键特性 1. 支持COCO/LVIS数据集自动识别与指标适配;2. 分布式训练下的指标聚合;3. 混淆矩阵计算与可视化;4. 结果导出(JSON/TXT);5. mAP@0.5:0.95多阈值计算

1. 检测验证器属性说明表

属性名 类型 说明
is_coco bool 数据集是否为COCO格式(决定类别映射/JSON输出格式)
is_lvis bool 数据集是否为LVIS格式(适配LVIS专属评估指标)
class_map list[int] 模型类别索引到数据集类别索引的映射(如COCO80→COCO91)
metrics DetMetrics 检测指标计算器(计算P/R/mAP等核心指标)
iouv torch.Tensor mAP计算的IoU阈值向量(0.5~0.95,步长0.05,共10个阈值)
niou int IoU阈值数量(固定为10)
lb list[Any] 混合保存时存储真实标签的列表(预留属性)
jdict list[dict[str, Any]] 存储COCO格式JSON检测结果的列表(用于官方评估)
stats dict[str, list[torch.Tensor]] 验证过程中存储统计信息的字典(TP/FP/置信度等)

2. 检测验证器方法说明表

方法名 功能说明
init 初始化检测验证器,配置IoU阈值、指标计算器等核心属性
preprocess 验证批次数据预处理(设备迁移、归一化、半精度转换)
init_metrics 初始化评估指标(识别数据集类型、类别映射、JSON保存开关)
get_desc 生成格式化的指标打印标题字符串(便于日志输出)
postprocess 对模型原始预测执行NMS后处理(过滤冗余框)
_prepare_batch 预处理单样本真实标签(坐标转换、尺寸对齐)
_prepare_pred 预处理单样本预测结果(单类别任务适配)
update_metrics 用预测结果和真实标签更新指标统计(TP/FP/混淆矩阵)
finalize_metrics 最终化指标(补充速度/混淆矩阵信息、保存指标)
gather_stats 多GPU分布式验证时聚合所有进程的指标/结果
get_stats 计算并返回最终的指标字典(P/R/mAP等)
print_results 打印验证指标(整体+逐类别)
_process_batch 计算预测与真实标签的匹配矩阵(TP矩阵,按IoU阈值)
build_dataset 构建验证数据集(适配YOLO输入要求)
get_dataloader 构建验证数据加载器(禁用打乱、适配编译模式)
plot_val_samples 可视化验证样本的真实标注(检查标注质量)
plot_predictions 可视化验证样本的预测结果(对比标注与预测)
save_one_txt 将预测结果保存为TXT文件(归一化坐标格式)
pred_to_json 将预测结果转换为COCO/LVIS格式的JSON(用于官方评估)
scale_preds 将预测框缩放到原始图像尺寸(消除预处理的缩放/填充影响)
eval_json 调用COCO/LVIS官方评估工具计算指标(补充mAP结果)
coco_evaluate 基于faster-coco-eval库执行COCO/LVIS指标评估

初始化函数:init

python 复制代码
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
    """
    初始化DetectionValidator实例,用于YOLO目标检测模型验证
    核心是继承BaseValidator的通用验证逻辑,初始化检测任务专属的IoU阈值、指标计算器

    参数:
        dataloader (torch.utils.data.DataLoader, 可选): 验证集数据加载器
        save_dir (Path, 可选): 验证结果保存目录(如runs/detect/val)
        args (dict[str, Any], 可选): 验证参数(如conf、iou、max_det、save_json等)
        _callbacks (list[Any], 可选): 验证过程中执行的回调函数列表(如日志打印、结果保存)
    """
    # 调用父类BaseValidator的初始化方法,传入数据加载器、保存目录、参数、回调函数
    super().__init__(dataloader, save_dir, args, _callbacks)
    # 初始化数据集类型标记(默认非COCO/LVIS)
    self.is_coco = False
    self.is_lvis = False
    # 初始化类别映射(模型类别→数据集类别)
    self.class_map = None
    # 标记任务类型为检测(detect)
    self.args.task = "detect"
    # 定义mAP计算的IoU阈值向量:0.5到0.95,步长0.05(共10个阈值)
    self.iouv = torch.linspace(0.5, 0.95, 10)
    # 记录IoU阈值数量(固定为10)
    self.niou = self.iouv.numel()
    # 初始化检测指标计算器(封装P/R/mAP计算逻辑)
    self.metrics = DetMetrics()
项目 详情
函数名 __init__
功能概述 继承父类通用验证器逻辑,初始化检测任务专属的指标、IoU阈值、数据集标识等核心属性
返回值 无(构造函数)
核心逻辑 调用父类初始化,设置检测任务专属参数(IoU阈值、指标计算器、数据集标识)
注意事项 IoU阈值数量(niou=10)固定,若需自定义需修改torch.linspace参数

批次预处理:preprocess

python 复制代码
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
    """
    对验证批次数据做预处理:设备迁移、归一化、半精度转换
    确保输入符合模型推理要求,与训练阶段的预处理逻辑对齐

    参数:
        batch (dict[str, Any]): 批次数据字典,包含img(图像张量)、cls(类别)、bboxes(框坐标)等

    返回:
        (dict[str, Any]): 预处理后的批次数据字典
    """
    # 遍历批次字典,将所有张量移至指定设备(GPU/CPU):
    # - CUDA设备启用non_blocking=True(非阻塞传输,提升数据加载速度)
    for k, v in batch.items():
        if isinstance(v, torch.Tensor):
            batch[k] = v.to(self.device, non_blocking=self.device.type == "cuda")
    # 图像归一化+精度转换:
    # - 半精度(half)/浮点型(float)转换(适配模型推理精度)
    # - 除以255,将像素值从[0,255]缩放到[0,1]
    batch["img"] = (batch["img"].half() if self.args.half else batch["img"].float()) / 255
    return batch
项目 详情
函数名 preprocess
功能概述 验证批次数据的设备迁移、数据类型转换、像素归一化,适配YOLO模型输入要求
返回值 dict[str, Any]:预处理后的批次字典
核心逻辑 张量设备迁移→数据类型转换(半精度/浮点)→像素值归一化(0-255→0-1)
设计亮点 兼容半精度推理,非阻塞设备传输提升验证速度
注意事项 需确保批次中所有张量均迁移至模型设备(CPU/GPU),避免设备不匹配错误

指标初始化:init_metrics

python 复制代码
def init_metrics(self, model: torch.nn.Module) -> None:
    """
    初始化检测评估指标:识别数据集类型、配置类别映射、开启JSON保存开关
    是验证前的核心准备步骤,确保指标计算适配数据集格式

    参数:
        model (torch.nn.Module): 待验证的YOLO检测模型实例
    """
    # 获取验证集路径(从数据配置中提取val字段)
    val = self.data.get(self.args.split, "")
    # 判断是否为COCO数据集:路径包含"coco"且以val2017.txt/test-dev2017.txt结尾
    self.is_coco = (
        isinstance(val, str)
        and "coco" in val
        and (val.endswith(f"{os.sep}val2017.txt") or val.endswith(f"{os.sep}test-dev2017.txt"))
    )
    # 判断是否为LVIS数据集:路径包含"lvis"且非COCO
    self.is_lvis = isinstance(val, str) and "lvis" in val and not self.is_coco
    # 配置类别映射:
    # - COCO数据集:将模型的80类索引映射到COCO官方91类索引
    # - 非COCO:直接使用连续索引(1~类别数)
    self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1, len(model.names) + 1))
    # 开启JSON保存开关:验证模式+COCO/LVIS数据集+非训练阶段时自动开启
    self.args.save_json |= self.args.val and (self.is_coco or self.is_lvis) and not self.training
    # 绑定模型类别名、类别数到验证器
    self.names = model.names
    self.nc = len(model.names)
    # 标记模型是否为端到端模式(预留属性,适配特殊模型结构)
    self.end2end = getattr(model, "end2end", False)
    # 初始化已验证样本数、JSON结果列表
    self.seen = 0
    self.jdict = []
    # 将类别名绑定到指标计算器(便于逐类别指标打印)
    self.metrics.names = model.names
    # 初始化混淆矩阵(用于可视化类别预测错误):
    # - names=model.names:类别名映射
    # - save_matches=plots+visualize:开启匹配样本可视化(错误预测样本)
    self.confusion_matrix = ConfusionMatrix(names=model.names, save_matches=self.args.plots and self.args.visualize)
项目 详情
函数名 init_metrics
功能概述 识别数据集类型(COCO/LVIS)、初始化类别映射、配置指标计算规则
返回值
核心逻辑 1. 识别COCO/LVIS数据集;2. 构建类别映射;3. 配置JSON导出规则;4. 初始化混淆矩阵
设计亮点 自动识别数据集类型,无需手动配置类别映射;动态控制JSON导出开关
注意事项 LVIS数据集需确保标注格式符合要求,否则JSON导出会出错

指标描述生成:get_desc

python 复制代码
def get_desc(self) -> str:
    """
    生成格式化的指标打印标题字符串(用于日志输出,对齐列宽)
    示例输出:
                      Class     Images  Instances       Box(P       R   mAP50  mAP50-95)

    返回:
        (str): 格式化的标题字符串
    """
    return ("%22s" + "%11s" * 6) % (    # 类别名占22字符宽度(适配长类别名);其余指标各占11字符宽度,保证打印对齐
        "Class",          # 类别名(all表示整体)
        "Images",         # 验证样本数
        "Instances",      # 真实目标实例数
        "Box(P",          # 精确率(Precision)
        "R",              # 召回率(Recall)
        "mAP50",          # mAP@0.5
        "mAP50-95)",      # mAP@0.5:0.95
    )
项目 详情
函数名 get_desc
功能概述 生成格式化的指标打印标题字符串,适配检测任务的mAP/PR指标展示
返回值 str:格式化的指标标题(如Class Images Instances Box(P R mAP50 mAP50-95)
核心逻辑 按固定宽度拼接类别、图像数、实例数、Box相关指标(P/R/mAP50/mAP50-95)
设计亮点 动态适配检测指标维度,打印格式统一且易读
注意事项 字符串宽度固定,若类别名过长会导致换行,需按需调整宽度

预测后处理:postprocess

python 复制代码
def postprocess(self, preds: torch.Tensor) -> list[dict[str, torch.Tensor]]:
    """
    对模型原始预测执行非极大值抑制(NMS)后处理,过滤冗余检测框
    是检测任务的核心后处理步骤,确保每个目标仅保留最优预测框

    参数:
        preds (torch.Tensor): 模型原始预测张量(维度:[B, N, 4+1+NC],4=框坐标,1=置信度,NC=类别数)

    返回:
        (list[dict[str, torch.Tensor]]): 后处理后的预测结果列表,每个元素为字典:
            - bboxes: 过滤后的框坐标(xyxy格式)
            - conf: 框的置信度
            - cls: 框的类别索引
            - extra: 额外信息(预留字段),如旋转框角度
    """
    # 执行NMS:
    # - conf: 置信度阈值(过滤低置信度框)
    # - iou: NMS的IoU阈值(合并重叠框)
    # - nc: 类别数(0表示自动识别)
    # - multi_label: 允许一个框预测多个类别
    # - agnostic: 单类别/agnostic_nms模式下跨类别NMS
    # - max_det: 单图最大检测框数量
    # - end2end: 适配端到端模型的输出格式
    # - rotated: 适配旋转框检测(OBB任务)
    outputs = nms.non_max_suppression(
        preds,
        self.args.conf,
        self.args.iou,
        nc=0 if self.args.task == "detect" else self.nc,
        multi_label=True,
        agnostic=self.args.single_cls or self.args.agnostic_nms,
        max_det=self.args.max_det,
        end2end=self.end2end,
        rotated=self.args.task == "obb",
    )
    # 将NMS输出转换为结构化字典列表(便于后续指标计算)
    return [{"bboxes": x[:, :4], "conf": x[:, 4], "cls": x[:, 5], "extra": x[:, 6:]} for x in outputs]
项目 详情
函数名 postprocess
功能概述 对模型原始预测结果执行NMS(非极大值抑制),过滤冗余检测框
返回值 list[dict[str, torch.Tensor]]:每个元素为单张图的预测结果(bboxes/conf/cls/extra)
核心逻辑 调用NMS过滤低置信/重叠框→格式化预测结果为字典结构
设计亮点 兼容普通检测/旋转框检测(OBB)、单类别/多类别NMS,参数全可配置
注意事项 max_det需根据数据集调整(小目标多的场景需增大,避免漏检)
概念 含义 对应代码控制逻辑
单类别检测任务 整个检测任务仅需识别一种目标(如只检测"人"、只检测"汽车")。 self.args.single_cls = True
单标签预测(每框一类) 每个检测框仅预测一个类别(vs 多标签:一个框预测多个类别,如"人+背包") nms.non_max_suppressionmulti_label 参数

根据配置自动切换 NMS 的类别处理逻辑,单类别任务强制开启类别无关 NMS 提升效率,多类别任务可手动开启只保留置信度最高的一个以减少重复检测,适配不同检测场景的需求。

模式 逻辑 适用场景
常规NMS(agnostic=False) 按类别分组,对每个类别单独执行NMS;不同类别间的框即使重叠度极高,也不会互相过滤 多类别目标边界清晰的场景(如人、自行车、汽车无重叠),避免误过滤不同类别的合法框
类别无关NMS(agnostic=True) 忽略类别信息,将所有框合并后执行一次NMS;只要框的IoU超过阈值,无论类别是否相同,只保留置信度最高的框 单类别检测、或多类别目标易混淆/重叠的场景(如汽车/卡车、猫/狗),减少重复检测

批次预处理(单样本):_prepare_batch

python 复制代码
def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
    """
    预处理单样本的真实标签:提取当前样本的类别/框坐标,转换坐标格式并对齐尺寸
    为后续指标计算做准备,确保真实标签与预测结果维度匹配

    参数:
        si (int): 批次内样本索引(如0~7,对应批次样本数 8)
        batch (dict[str, Any]): 批次数据字典(包含cls、bboxes、ori_shape等)

    返回:
        (dict[str, Any]): 预处理后的单样本真实标签字典:
            - cls: 类别索引(张量)
            - bboxes: 框坐标(xyxy格式,适配模型输入尺寸)
            - ori_shape: 原始图像尺寸(H,W)
            - imgsz: 模型输入尺寸(H,W)
            - ratio_pad: 缩放/填充比例(用于后续框缩放)
            - im_file: 图像文件路径
    """
    # 提取当前样本的索引掩码(batch_idx标记每个框所属的样本)
    idx = batch["batch_idx"] == si
    # 提取当前样本的类别(去除冗余维度)
    cls = batch["cls"][idx].squeeze(-1)
    # 提取当前样本的框坐标(xywh格式)
    bbox = batch["bboxes"][idx]
    # 提取原始图像尺寸、模型输入尺寸、缩放/填充比例
    ori_shape = batch["ori_shape"][si]
    imgsz = batch["img"].shape[2:]
    ratio_pad = batch["ratio_pad"][si]
    # 若存在真实框(真实标注>0),将xywh转换为xyxy(适配IOU计算),并缩放到模型输入尺寸(对应imgsz为[h,w]框的xyxy维度)
    if cls.shape[0]:
        #  图像格式[[1, 0, 1, 0]]作用:[480,640] => [640,480,640,480]
        bbox = ops.xywh2xyxy(bbox) * torch.tensor(imgsz, device=self.device)[[1, 0, 1, 0]]
    # 返回结构化的真实标签字典
    return {
        "cls": cls,
        "bboxes": bbox,
        "ori_shape": ori_shape,
        "imgsz": imgsz,
        "ratio_pad": ratio_pad,
        "im_file": batch["im_file"][si],
    }
项目 详情
函数名 _prepare_batch
功能概述 提取单样本的真实标注,转换框格式并缩放至模型输入尺寸
返回值 dict[str, Any]:单样本真实标注(cls/bboxes/ori_shape/imgsz/ratio_pad/im_file)
核心逻辑 筛选单样本标注→框格式转换(xywh→xyxy)→缩放至模型输入尺寸
设计亮点 自动适配批次索引,框缩放维度对齐避免尺寸错误
注意事项 需确保batch["batch_idx"]正确标记每个标注所属的样本索引

batch字段存储维度对照表

存储维度 字段名
以框为单位 例如:batch_idx、cls、bboxes、im_file
以图像为单位 例如:ori_shape、img、ratio_pad

以图像为单位的字段按batch内单张图像粒度 存储,每个元素对应一张完整图像的属性/数据;以框为单位的字段按单个检测框粒度 存储,每个元素对应一个框的坐标/类别/所属图像等信息,需通过batch_idx关联到对应图像。

预测预处理(单样本):_prepare_pred

python 复制代码
def _prepare_pred(self, pred: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
    """
    预处理单样本的预测结果:单类别任务时强制类别索引为0
    确保单类别验证时指标计算逻辑统一

    参数:
        pred (dict[str, torch.Tensor]): 后处理后的预测结果字典

    返回:
        (dict[str, torch.Tensor]): 预处理后的预测结果字典
    """
    # 单类别任务:将所有预测框的类别索引置为0(统一指标计算逻辑)
    if self.args.single_cls:
        pred["cls"] *= 0
    return pred
项目 详情
函数名 _prepare_pred
功能概述 单样本预测结果的预处理(单类别检测时重置类别索引)
返回值 dict[str, torch.Tensor]:预处理后的预测结果
核心逻辑 单类别检测时将所有预测类别索引置0,保证指标计算一致性
设计亮点 兼容单/多类别检测,无需修改指标计算核心逻辑
注意事项 仅在self.args.single_cls=True时生效,多类别场景无操作

将所有预测框的类别索引统一置为 0,让指标计算逻辑适配 "单类别任务":让单类别场景的预测类别索引与真实标签(单类别时默认标注为 0)对齐,避免类别索引不匹配导致 P/R/mAP 等指标计算错误;复用多类别下的指标统计逻辑,无需为单类别单独编写适配代码,保证单 / 多类别验证流程统一,降低代码维护成本。

指标更新:update_metrics

python 复制代码
def update_metrics(self, preds: list[dict[str, torch.Tensor]], batch: dict[str, Any]) -> None:
    """
    用预测结果和真实标签更新指标统计:计算TP/FP、更新混淆矩阵、保存预测结果
    是验证过程的核心步骤,逐样本累积指标数据

    参数:
        preds (list[dict[str, torch.Tensor]]): 批次预测结果列表(每个元素为单样本预测字典)
        batch (dict[str, Any]): 批次真实标签字典
    """
    # 遍历批次内每个样本的预测结果
    for si, pred in enumerate(preds):
        # 累计已验证样本数
        self.seen += 1
        # 预处理当前样本的真实标签
        pbatch = self._prepare_batch(si, batch)
        # 预处理当前样本的预测结果
        predn = self._prepare_pred(pred)

        # 转换真实类别为NumPy数组(适配指标计算器输入)
        cls = pbatch["cls"].cpu().numpy()
        # 判断是否无预测框
        no_pred = predn["cls"].shape[0] == 0
        # 更新指标计算器统计信息:
        # - _process_batch: 计算TP矩阵(按IoU阈值)
        # - target_cls: 真实类别
        # - target_img: 样本包含的类别
        # - conf: 预测置信度
        # - pred_cls: 预测类别
        self.metrics.update_stats(
            {
                **self._process_batch(predn, pbatch),
                "target_cls": cls,
                "target_img": np.unique(cls),
                "conf": np.zeros(0) if no_pred else predn["conf"].cpu().numpy(),
                "pred_cls": np.zeros(0) if no_pred else predn["cls"].cpu().numpy(),
            }
        )
        # 可视化相关:
        if self.args.plots:
            # 更新混淆矩阵(统计类别预测错误)
            self.confusion_matrix.process_batch(predn, pbatch, conf=self.args.conf)
            # 可视化匹配样本(错误预测的样本)
            if self.args.visualize:
                self.confusion_matrix.plot_matches(batch["img"][si], pbatch["im_file"], self.save_dir)

        # 无预测框时跳过结果保存
        if no_pred:
            continue

        # 结果保存:缩放预测框到原始图像尺寸(消除预处理的缩放/填充影响)
        if self.args.save_json or self.args.save_txt:
            predn_scaled = self.scale_preds(predn, pbatch)
        # 保存为COCO/LVIS格式JSON
        if self.args.save_json:
            self.pred_to_json(predn_scaled, pbatch)
        # 保存为TXT文件(归一化坐标)
        if self.args.save_txt:
            self.save_one_txt(
                predn_scaled,
                self.args.save_conf,
                pbatch["ori_shape"],
                self.save_dir / "labels" / f"{Path(pbatch['im_file']).stem}.txt",
            )
项目 详情
函数名 update_metrics
功能概述 逐样本计算预测与真实标注的匹配关系,更新指标统计、混淆矩阵、结果保存
返回值
核心逻辑 1. 逐样本提取真实标注/预测结果;2. 计算TP/FP矩阵;3. 更新指标统计;4. 混淆矩阵更新;5. 结果导出(JSON/TXT)
设计亮点 逐样本精细化更新指标,支持可视化匹配结果、多格式结果导出
注意事项 无预测框时需跳过结果导出,避免空文件/空JSON条目

指标最终化:finalize_metrics

python 复制代码
def finalize_metrics(self) -> None:
    """
    最终化指标:补充推理速度、混淆矩阵信息,保存指标到指定目录
    验证结束前的收尾步骤,确保指标信息完整
    """
    # 绘制混淆矩阵:
    # - normalize=True: 归一化(百分比)
    # - normalize=False: 原始数量
    if self.args.plots:
        for normalize in True, False:
            # 生成归一化/非归一化两种混淆矩阵图
            self.confusion_matrix.plot(save_dir=self.save_dir, normalize=normalize, on_plot=self.on_plot)
    # 补充推理速度到指标,将验证过程的推理速度(img/s)绑定到指标对象
    self.metrics.speed = self.speed
    # 补充混淆矩阵到指标
    self.metrics.confusion_matrix = self.confusion_matrix
    # 绑定保存目录到指标(便于结果保存)
    self.metrics.save_dir = self.save_dir
项目 详情
函数名 finalize_metrics
功能概述 补充指标的速度信息、保存混淆矩阵,生成最终指标结果
返回值
核心逻辑 混淆矩阵可视化→绑定推理速度→设置指标保存目录
设计亮点 两种混淆矩阵可视化方式,便于分析类别误检/漏检情况
注意事项 需确保self.speed已正确计算(父类BaseValidatorbenchmark方法)

分布式指标聚合:gather_stats

python 复制代码
def gather_stats(self) -> None:
    """
    多GPU分布式验证时聚合所有进程的指标和JSON结果:
    - 主进程(RANK=0)收集所有进程的stats和jdict,合并后更新
    - 非主进程仅发送数据,清空本地统计
    确保分布式验证的指标结果准确
    """
    if RANK == 0:
        # 初始化收集容器(数量=获取全局进程总数)
        gathered_stats = [None] * dist.get_world_size()
        # 收集所有进程的指标统计
        dist.gather_object(self.metrics.stats, gathered_stats, dst=0)
        # 合并统计信息(按key聚合)
        merged_stats = {key: [] for key in self.metrics.stats.keys()}
        for stats_dict in gathered_stats:
            for key in merged_stats:
                merged_stats[key].extend(stats_dict[key])
        gathered_jdict = [None] * dist.get_world_size()
        # 收集所有进程的JSON结果
        dist.gather_object(self.jdict, gathered_jdict, dst=0)
        # 合并JSON结果
        self.jdict = []
        for jdict in gathered_jdict:
            self.jdict.extend(jdict)
        # 更新指标统计和已验证样本数
        self.metrics.stats = merged_stats
        self.seen = len(self.dataloader.dataset)
    elif RANK > 0:
        # 非主进程发送数据后清空本地统计
        dist.gather_object(self.metrics.stats, None, dst=0)
        dist.gather_object(self.jdict, None, dst=0)
        self.jdict = []
        self.metrics.clear_stats()
项目 详情
函数名 gather_stats
功能概述 多GPU分布式验证时,聚合所有进程的指标统计和JSON结果
返回值
核心逻辑 主进程(rank=0)收集所有进程的stats/jdict→合并;子进程仅发送数据
设计亮点 兼容分布式/单机验证,自动处理进程间数据聚合
注意事项 需确保分布式环境初始化完成(dist.init_process_group),否则会报错

指标计算:get_stats

python 复制代码
def get_stats(self) -> dict[str, Any]:
    """
    计算并返回最终的检测指标字典(P/R/mAP等)
    是验证的核心输出,包含所有关键评估指标

    返回:
        (dict[str, Any]): 指标字典,示例:
            {
                "metrics/precision(B)": 0.85,
                "metrics/recall(B)": 0.80,
                "metrics/mAP50(B)": 0.88,
                "metrics/mAP50-95(B)": 0.65,
                "fitness": 0.72
            }
    """
    # 处理指标(计算P/R/mAP、绘制指标曲线)
    # - plot=self.args.plots:启用指标曲线可视化(PR曲线/mAP曲线);
    self.metrics.process(save_dir=self.save_dir, plot=self.args.plots, on_plot=self.on_plot)
    # 清空临时统计(释放内存)
    self.metrics.clear_stats()
    # 返回最终指标字典
    return self.metrics.results_dict
项目 详情
函数名 get_stats
功能概述 处理聚合后的统计数据,计算最终的mAP/PR/F1等指标
返回值 dict[str, Any]:包含所有检测指标(mAP50/mAP50-95/P/R/F1等)
核心逻辑 调用指标计算器处理统计数据→清空临时统计→返回最终指标
设计亮点 自动生成指标可视化曲线,结果字典包含所有关键指标便于解析
注意事项 需先完成gather_stats聚合,否则指标仅包含单进程数据

指标打印:print_results

python 复制代码
def print_results(self) -> None:
    """
    打印验证指标:先打印整体指标,再打印逐类别指标(verbose模式)
    便于用户快速查看模型性能
    """
    # 定义打印格式:22字符类别名 + 2个整数(图像数/实例数) + 4个浮点型指标
    pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys)
    # 打印整体指标(all行)
    LOGGER.info(pf % ("all", self.seen, self.metrics.nt_per_class.sum(), *self.metrics.mean_results()))
    # 无真实标签时打印警告(无法计算有效指标)
    if self.metrics.nt_per_class.sum() == 0:
        LOGGER.warning(f"no labels found in {self.args.task} set, can not compute metrics without labels")

    # 逐类别打印指标(verbose模式+非训练+多类别+有统计数据时)
    if self.args.verbose and not self.training and self.nc > 1 and len(self.metrics.stats):
        for i, c in enumerate(self.metrics.ap_class_index):
            LOGGER.info(
                pf
                % (
                    self.names[c],                  # 类别名
                    self.metrics.nt_per_image[c],   # 该类别出现的图像数
                    self.metrics.nt_per_class[c],   # 该类别的实例数
                    *self.metrics.class_result(i),  # 该类别的P/R/mAP50/mAP50-95
                )
            )
项目 详情
函数名 print_results
功能概述 格式化打印整体指标和逐类别指标,便于终端查看验证结果
返回值
核心逻辑 打印整体指标→打印逐类别指标(verbose模式)→无标注时告警
设计亮点 按AP排序打印类别指标,便于快速识别低性能类别;无标注时友好告警
注意事项 仅在self.args.verbose=True且非训练模式下打印逐类别指标

verbose 模式是 DetectionValidator 验证器中控制指标打印粒度的配置项,开启后(self.args.verbose=True)会在验证完成时额外打印每个类别的详细检测指标(如精确率、召回率、mAP 等),而非仅打印整体汇总指标。

批次匹配计算:_process_batch

python 复制代码
def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, Any]) -> dict[str, np.ndarray]:
    """
    计算预测框与真实框的匹配矩阵(TP矩阵):
    - 按IoU阈值(0.5~0.95)判断预测框是否为真阳性(TP)
    是mAP计算的核心步骤

    参数:
        preds (dict[str, torch.Tensor]): 预处理后的预测结果字典(bboxes/cls)
        batch (dict[str, Any]): 预处理后的真实标签字典(bboxes/cls)

    返回:
        (dict[str, np.ndarray]): 包含TP矩阵的字典,TP矩阵维度:[预测框数, 10](10个IoU阈值)
    """
    # 无真实框或无预测框时,返回空TP矩阵
    if batch["cls"].shape[0] == 0 or preds["cls"].shape[0] == 0:
        return {"tp": np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)}
    # 计算真实框与预测框的IoU矩阵(两两IoU,维度:[真实框数, 预测框数])
    iou = box_iou(batch["bboxes"], preds["bboxes"])
    # 匹配预测框与真实框,基于类别和IoU匹配,生成N×10的生成TP矩阵(True=真阳性,False=假阳性)
    return {"tp": self.match_predictions(preds["cls"], batch["cls"], iou).cpu().numpy()}
项目 详情
函数名 _process_batch
功能概述 计算预测框与真实框的IoU,生成TP矩阵(判断每个预测框在各IoU阈值下是否为真阳性)
返回值 dict[str, np.ndarray]:包含TP矩阵(shape: [N, 10],10个IoU阈值)
核心逻辑 计算IoU→匹配预测框与真实框→生成TP矩阵
设计亮点 多IoU阈值并行计算TP,提升指标计算效率
注意事项 IoU计算需保证真实框和预测框均为xyxy格式,否则结果错误

数据集构建:build_dataset

python 复制代码
def build_dataset(self, img_path: str, mode: str = "val", batch: int | None = None) -> torch.utils.data.Dataset:
    """
    构建YOLO验证数据集(适配YOLO的输入要求:stride对齐、矩形推理)
    与训练数据集构建逻辑一致,但验证模式禁用训练增强

    参数:
        img_path (str): 验证图像文件夹路径
        mode (str): 数据集模式,固定为"val"(验证)
        batch (int, 可选): 批次大小,仅用于矩形推理的尺寸计算

    返回:
        (Dataset): 配置好的YOLO验证数据集实例
    """
    # 调用build_yolo_dataset构建数据集:
    # 验证模式启用矩形推理,禁用数据增强
    # - stride=self.stride:模型下采样步长,保证图像尺寸为stride整数倍
    return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, stride=self.stride)
项目 详情
函数名 build_dataset
功能概述 构建YOLO检测验证数据集,适配验证模式的矩形推理、stride对齐
返回值 Dataset:YOLO验证数据集实例(YOLODataset)
核心逻辑 调用build_yolo_dataset,适配验证模式的rect推理和stride对齐
设计亮点 复用通用数据集构建逻辑,仅适配验证模式的专属配置
注意事项 验证数据集禁用shuffle,避免影响指标计算的一致性

数据加载器构建:get_dataloader

python 复制代码
def get_dataloader(self, dataset_path: str, batch_size: int) -> torch.utils.data.DataLoader:
    """
    构建验证数据加载器:禁用打乱、适配编译模式、设置worker数
    确保验证过程的稳定性和可重复性

    参数:
        dataset_path (str): 验证数据集路径
        batch_size (int): 验证批次大小

    返回:
        (torch.utils.data.DataLoader): 配置好的验证数据加载器
    """
    # 构建验证数据集
    dataset = self.build_dataset(dataset_path, batch=batch_size, mode="val")
    # 构建数据加载器:
    # - workers: 使用指定的worker数
    # - shuffle=False: 验证禁用打乱(确保结果可复现)
    # - rank=-1: 非分布式模式
    # - drop_last=compile: 编译模式下丢弃最后不完整批次
    # - pin_memory=training: 训练模式启用内存锁定(提升数据传输速度)
    return build_dataloader(
        dataset,
        batch_size,
        self.args.workers,
        shuffle=False,
        rank=-1,
        drop_last=self.args.compile,
        pin_memory=self.training,
    )
项目 详情
函数名 get_dataloader
功能概述 构建验证集DataLoader,适配验证模式的无shuffle、多线程加载
返回值 torch.utils.data.DataLoader:验证集数据加载器
核心逻辑 构建验证数据集→创建无shuffle的DataLoader→配置多线程/内存锁定
设计亮点 验证模式专属配置,保证结果稳定且加载效率高
注意事项 验证批次大小可大于训练批次(无梯度计算,显存占用低)

验证样本可视化:plot_val_samples

python 复制代码
def plot_val_samples(self, batch: dict[str, Any], ni: int) -> None:
    """
    可视化验证样本的真实标注,并保存为图片(检查标注质量)
    保存路径:save_dir/val_batch{ni}_labels.jpg

    参数:
        batch (dict[str, Any]): 批次数据字典
        ni (int): 批次索引(用于命名图片文件)
    """
    plot_images(
        labels=batch,               # 真实标注信息
        paths=batch["im_file"],     # 图像文件路径
        fname=self.save_dir / f"val_batch{ni}_labels.jpg",  # 保存路径
        names=self.names,           # 类别名映射,标注框旁显示类别名
        on_plot=self.on_plot,       # 绘图回调函数
    )
项目 详情
函数名 plot_val_samples
功能概述 可视化验证样本的真实标注,保存为图片便于检查标注质量
返回值
核心逻辑 调用plot_images绘制带真实标注的样本→保存至验证目录
设计亮点 直观展示验证样本的真实标注,快速定位标注错误(如框偏移/类别错误)
注意事项 仅在self.args.plots=True时生效,默认启用

预测结果可视化:plot_predictions

python 复制代码
def plot_predictions(
    self, batch: dict[str, Any], preds: list[dict[str, torch.Tensor]], ni: int, max_det: int | None = None
) -> None:
    """
    可视化验证样本的预测结果(对比真实标注),保存为图片
    保存路径:save_dir/val_batch{ni}_pred.jpg

    参数:
        batch (dict[str, Any]): 批次数据字典
        preds (list[dict[str, torch.Tensor]]): 批次预测结果列表
        ni (int): 批次索引
        max_det (int, 可选): 单图最大可视化检测框数(默认使用args.max_det)
    """
    # 预留优化标记
    # TODO: optimize this
    # 为每个预测结果添加批次索引(适配plot_images的批量可视化)
    for i, pred in enumerate(preds):
        pred["batch_idx"] = torch.ones_like(pred["conf"]) * i   # 长度 = N(该样本的检测框数量),值全为i的张量,标记该样本所有框的归属
    # 提取预测结果的键(bboxes/conf/cls等)
    keys = preds[0].keys()
    # 确定最大可视化框数
    max_det = max_det or self.args.max_det
    # 拼接批次内所有预测结果(基于已经按置信度降序排序后的预测框限制最大框数)
    batched_preds = {k: torch.cat([x[k][:max_det] for x in preds], dim=0) for k in keys}
    # 预留修复标记
    # TODO: fix this
    # 将预测框从xyxy转换为xywh(适配plot_images的输入格式)
    batched_preds["bboxes"][:, :4] = ops.xyxy2xywh(batched_preds["bboxes"][:, :4])
    # 绘制预测结果并保存
    plot_images(
        images=batch["img"],        # 批次图像
        labels=batched_preds,       # 拼接后的预测结果
        paths=batch["im_file"],     # 图像文件路径
        fname=self.save_dir / f"val_batch{ni}_pred.jpg",     # 保存路径
        names=self.names,           # 类别名
        on_plot=self.on_plot,       # 绘图回调函数
    )  # pred
项目 详情
函数名 plot_predictions
功能概述 可视化验证样本的预测结果,对比真实标注展示检测效果
返回值
核心逻辑 拼接批次预测结果→转换框格式→绘制预测框→保存至验证目录
设计亮点 限制最大展示框数,避免因检测框过多导致画面杂乱、无法判断模型真实性能;自动匹配类别名,可视化清晰
注意事项 预测框需先缩放至原始图像尺寸,否则位置偏移

TXT结果保存:save_one_txt

python 复制代码
def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
    """
    将预测结果保存为TXT文件(归一化坐标格式),每行对应一个检测框:
    格式:<类别索引> <x_center> <y_center> <width> <height> [置信度](可选)

    参数:
        predn (dict[str, torch.Tensor]): 缩放后的预测结果字典
        save_conf (bool): 是否保存置信度
        shape (tuple[int, int]): 原始图像尺寸(H,W)
        file (Path): TXT文件保存路径
    """
    # 导入Results类(封装检测结果的保存逻辑)
    from ultralytics.engine.results import Results
    # 构建Results实例并保存为TXT
    Results(
        np.zeros((shape[0], shape[1]), dtype=np.uint8),     # 空图像(仅用于初始化)
        path=None,
        names=self.names,
        # 拼接框坐标、置信度、类别索引(维度:[N, 6])
        boxes=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
    ).save_txt(file, save_conf=save_conf)
项目 详情
函数名 save_one_txt
功能概述 将单样本预测结果保存为TXT文件,格式为归一化xywh+类别+置信度
返回值
核心逻辑 封装预测结果为Results对象→调用save_txt保存为归一化格式
设计亮点 复用Results类的保存逻辑,保证格式统一且易于解析
注意事项 TXT文件中坐标为归一化值,需乘以原始图像尺寸得到像素坐标

JSON结果保存:pred_to_json

python 复制代码
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
    """
    将预测结果转换为COCO/LVIS格式的JSON(用于官方评估工具)
    JSON条目格式:
    {
        "image_id": 图像ID,
        "file_name": 图像文件名,
        "category_id": 数据集类别索引,
        "bbox": [x, y, width, height](左上角坐标+宽高),
        "score": 置信度
    }

    参数:
        predn (dict[str, torch.Tensor]): 缩放后的预测结果字典
        pbatch (dict[str, Any]): 预处理后的单样本真实标签字典
    """
    # 提取图像文件路径和文件名前缀
    path = Path(pbatch["im_file"])
    stem = path.stem
    # 确定图像ID(数字前缀则转整数,否则用字符串)
    image_id = int(stem) if stem.isnumeric() else stem
    # 将预测框从xyxy转换为xywh(COCO格式要求)
    box = ops.xyxy2xywh(predn["bboxes"])
    # 将xy中心坐标转换为左上角坐标(COCO格式要求)
    box[:, :2] -= box[:, 2:] / 2
    # 遍历每个预测框,构建JSON条目
    for b, s, c in zip(box.tolist(), predn["conf"].tolist(), predn["cls"].tolist()):
        self.jdict.append(
            {
                "image_id": image_id,
                "file_name": path.name,
                "category_id": self.class_map[int(c)],  # 映射到数据集类别索引
                "bbox": [round(x, 3) for x in b],       # 保留3位小数
                "score": round(s, 5),                   # 保留5位小数
            }
        )
项目 详情
函数名 pred_to_json
功能概述 将单样本预测结果转换为COCO JSON格式,用于官方工具评估
返回值
核心逻辑 提取图像ID→转换框格式(xyxy→xywh)→拼接JSON条目→添加到jdict列表
设计亮点 严格遵循COCO JSON格式,支持非数字图像ID,兼容官方评估工具
注意事项 类别映射需正确(COCO数据集需80→91类映射),否则评估结果错误

预测框缩放:scale_preds

python 复制代码
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
    """
    将预测框从模型输入尺寸缩放到原始图像尺寸(消除预处理的缩放/填充影响)
    确保保存的框坐标与原始图像匹配

    参数:
        predn (dict[str, torch.Tensor]): 预处理后的预测结果字典
        pbatch (dict[str, Any]): 预处理后的单样本真实标签字典

    返回:
        (dict[str, torch.Tensor]): 缩放后的预测结果字典(bboxes适配原始尺寸)
    """
   
    return {
        **predn,
        # 缩放框坐标:模型输入尺寸 → 原始图像尺寸(根据模型输入尺寸、原始尺寸、缩放比例和填充值,将框缩放至原始图像)
        "bboxes": ops.scale_boxes(
            pbatch["imgsz"],
            predn["bboxes"].clone(),    # 避免修改原预测框张量
            pbatch["ori_shape"],
            ratio_pad=pbatch["ratio_pad"],
        ),
    }
项目 详情
函数名 scale_preds
功能概述 将模型输入尺寸的预测框缩放至原始图像尺寸,适配结果保存/可视化
返回值 dict[str, torch.Tensor]:原始尺寸的预测结果
核心逻辑 调用scale_boxes缩放框→返回包含缩放后框的预测字典
设计亮点 自动适配矩形推理的缩放/填充,保证框位置精准
注意事项 需传入正确的ratio_pad(由数据集构建时生成),否则缩放结果错误

JSON指标评估:eval_json

python 复制代码
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
    """
    调用COCO/LVIS官方评估工具计算指标,补充到stats字典中
    是第三方工具评估的入口,提升指标的权威性

    参数:
        stats (dict[str, Any]): 当前指标字典

    返回:
        (dict[str, Any]): 更新后的指标字典(包含COCO/LVIS评估结果)
    """
    # 定义预测JSON和标注JSON路径
    pred_json = self.save_dir / "predictions.json"  # 预测结果JSON
    anno_json = (
        self.data["path"]
        / "annotations"
        # COCO用instances_val2017.json,LVIS用lvis_v1_<split>.json
        / ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
    )  # 真实标注JSON
    # 调用COCO评估函数
    return self.coco_evaluate(stats, pred_json, anno_json)
项目 详情
函数名 eval_json
功能概述 调用COCO/LVIS官方评估工具,基于JSON结果计算精准指标
返回值 dict[str, Any]:更新后的指标字典(包含COCO/LVIS官方mAP)
设计亮点 自动识别数据集类型,加载对应标注文件,无需手动指定
注意事项 需确保标注文件路径正确,否则评估工具无法找到标注

COCO指标评估:coco_evaluate

python 复制代码
def coco_evaluate(
    self,
    stats: dict[str, Any],
    pred_json: str,
    anno_json: str,
    iou_types: str | list[str] = "bbox",
    suffix: str | list[str] = "Box",
) -> dict[str, Any]:
    """
    基于faster-coco-eval库执行COCO/LVIS指标评估(比官方pycocotools更快)
    计算mAP50、mAP50-95,LVIS额外计算APr/APc/APf(稀有/常见/频繁类别)

    参数:
        stats (dict[str, Any]): 待更新的指标字典
        pred_json (str | Path): 预测结果JSON路径
        anno_json (str | Path): 真实标注JSON路径
        iou_types (str | list[str]): IoU评估类型 bbox/segm(默认"bbox",检测任务)
        suffix (str | list[str]): 指标名后缀,用于区分bbox/segm(默认"Box",区分框/分割/关键点)

    返回:
        (dict[str, Any]): 更新后的指标字典(包含COCO/LVIS评估结果)
    """
    # 仅在保存JSON+COCO/LVIS数据集+有预测结果时执行评估
    if self.args.save_json and (self.is_coco or self.is_lvis) and len(self.jdict):
        LOGGER.info(f"\nEvaluating faster-coco-eval mAP using {pred_json} and {anno_json}...")
        try:
            # 检查JSON文件是否存在
            for x in pred_json, anno_json:
                assert x.is_file(), f"{x} file not found"
            # 统一iou_types和suffix为列表格式
            iou_types = [iou_types] if isinstance(iou_types, str) else iou_types
            suffix = [suffix] if isinstance(suffix, str) else suffix
            # 检查并安装faster-coco-eval依赖(版本≥1.6.7)
            check_requirements("faster-coco-eval>=1.6.7")
            # 导入faster-coco-eval库
            from faster_coco_eval import COCO, COCOeval_faster

            # 加载真实标注和预测结果
            anno = COCO(anno_json)
            pred = anno.loadRes(pred_json)
            # 遍历IoU类型执行评估
            for i, iou_type in enumerate(iou_types):
                val = COCOeval_faster(
                    anno, pred, iouType=iou_type, lvis_style=self.is_lvis, print_function=LOGGER.info
                )
                # 指定待评估的图像ID(仅验证集图像)
                val.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files]
                # 执行评估、累积结果、打印总结
                val.evaluate()
                val.accumulate()
                val.summarize()

                # 更新指标字典(mAP50和mAP50-95)
                stats[f"metrics/mAP50({suffix[i][0]})"] = val.stats_as_dict["AP_50"]    # [0] 是取 suffix[i] 字符串的首字母
                stats[f"metrics/mAP50-95({suffix[i][0]})"] = val.stats_as_dict["AP_all"]

                # LVIS额外指标(稀有/常见/频繁类别AP)
                if self.is_lvis:
                    stats[f"metrics/APr({suffix[i][0]})"] = val.stats_as_dict["APr"]
                    stats[f"metrics/APc({suffix[i][0]})"] = val.stats_as_dict["APc"]
                    stats[f"metrics/APf({suffix[i][0]})"] = val.stats_as_dict["APf"]

            # LVIS数据集的fitness用框的mAP50-95计算
            if self.is_lvis:
                stats["fitness"] = stats["metrics/mAP50-95(B)"]
        except Exception as e:
            # 评估失败时打印警告(不中断验证流程)
            LOGGER.warning(f"faster-coco-eval unable to run: {e}")
    return stats
项目 详情
函数名 coco_evaluate
功能概述 使用faster_coco_eval库计算COCO/LVIS官方mAP指标,更新到stats字典
返回值 dict[str, Any]:包含官方mAP的指标字典
核心逻辑 加载预测/标注JSON→初始化COCOeval→计算指标→更新stats字典
设计亮点 使用更快的faster_coco_eval替代官方pycocotools,评估速度提升数倍
注意事项 需安装faster-coco-eval>=1.6.7,否则会降级为警告并跳过评估

完整代码

python 复制代码
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# 引入未来版本的类型注解支持,提升代码类型提示和静态检查能力
from __future__ import annotations

# 导入操作系统路径、路径处理模块(用于结果保存路径拼接)
import os
from pathlib import Path
# 导入类型注解模块(定义字典/列表的类型约束)
from typing import Any

# 导入数值计算、PyTorch核心、PyTorch分布式训练模块(支持多GPU验证指标聚合)
import numpy as np
import torch
import torch.distributed as dist

# 从ultralytics数据模块导入:数据加载器构建、YOLO数据集构建、COCO类别映射工具
from ultralytics.data import build_dataloader, build_yolo_dataset, converter
# 从ultralytics引擎模块导入基础验证器基类(提供通用验证流程)
from ultralytics.engine.validator import BaseValidator
# 从ultralytics工具模块导入:日志器、分布式进程排名、非极大值抑制(NMS)、通用操作函数
from ultralytics.utils import LOGGER, RANK, nms, ops
# 从ultralytics工具检查模块导入依赖检查函数(验证COCO评估依赖)
from ultralytics.utils.checks import check_requirements
# 从ultralytics工具指标模块导入:混淆矩阵、检测指标计算器、IoU计算函数
from ultralytics.utils.metrics import ConfusionMatrix, DetMetrics, box_iou
# 从ultralytics工具绘图模块导入验证样本可视化函数
from ultralytics.utils.plotting import plot_images


class DetectionValidator(BaseValidator):
    """
    基于BaseValidator扩展的YOLO目标检测专用验证器类
    该验证器针对目标检测任务定制,处理YOLO模型验证的专属需求:
    包括检测指标计算(mAP@0.5:0.95、Precision、Recall)、预测后处理(NMS)、
    结果可视化(混淆矩阵、验证样本标注/预测对比)、COCO/LVIS格式评估等核心流程

    属性:
        is_coco (bool): 数据集是否为COCO格式(决定类别映射/JSON输出格式)
        is_lvis (bool): 数据集是否为LVIS格式(适配LVIS专属评估指标)
        class_map (list[int]): 模型类别索引到数据集类别索引的映射(如COCO80→COCO91)
        metrics (DetMetrics): 检测指标计算器(计算P/R/mAP等核心指标)
        iouv (torch.Tensor): mAP计算的IoU阈值向量(0.5~0.95,步长0.05,共10个阈值)
        niou (int): IoU阈值数量(固定为10)
        lb (list[Any]): 混合保存时存储真实标签的列表(预留属性)
        jdict (list[dict[str, Any]]): 存储COCO格式JSON检测结果的列表(用于官方评估)
        stats (dict[str, list[torch.Tensor]]): 验证过程中存储统计信息的字典(TP/FP/置信度等)

    方法:
        __init__: 初始化检测验证器,配置IoU阈值、指标计算器等核心属性
        preprocess: 验证批次数据预处理(设备迁移、归一化、半精度转换)
        init_metrics: 初始化评估指标(识别数据集类型、类别映射、JSON保存开关)
        get_desc: 生成格式化的指标打印标题字符串(便于日志输出)
        postprocess: 对模型原始预测执行NMS后处理(过滤冗余框)
        _prepare_batch: 预处理单样本真实标签(坐标转换、尺寸对齐)
        _prepare_pred: 预处理单样本预测结果(单类别任务适配)
        update_metrics: 用预测结果和真实标签更新指标统计(TP/FP/混淆矩阵)
        finalize_metrics: 最终化指标(补充速度/混淆矩阵信息、保存指标)
        gather_stats: 多GPU分布式验证时聚合所有进程的指标/结果
        get_stats: 计算并返回最终的指标字典(P/R/mAP等)
        print_results: 打印验证指标(整体+逐类别)
        _process_batch: 计算预测与真实标签的匹配矩阵(TP矩阵,按IoU阈值)
        build_dataset: 构建验证数据集(适配YOLO输入要求)
        get_dataloader: 构建验证数据加载器(禁用打乱、适配编译模式)
        plot_val_samples: 可视化验证样本的真实标注(检查标注质量)
        plot_predictions: 可视化验证样本的预测结果(对比标注与预测)
        save_one_txt: 将预测结果保存为TXT文件(归一化坐标格式)
        pred_to_json: 将预测结果转换为COCO/LVIS格式的JSON(用于官方评估)
        scale_preds: 将预测框缩放到原始图像尺寸(消除预处理的缩放/填充影响)
        eval_json: 调用COCO/LVIS官方评估工具计算指标(补充mAP结果)
        coco_evaluate: 基于faster-coco-eval库执行COCO/LVIS指标评估

    示例:
        # >>> from ultralytics.models.yolo.detect import DetectionValidator
        # >>> args = dict(model="yolo11n.pt", data="coco8.yaml")
        # >>> validator = DetectionValidator(args=args)
        # >>> validator()
    """

    def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
        """
        初始化DetectionValidator实例,用于YOLO目标检测模型验证
        核心是继承BaseValidator的通用验证逻辑,初始化检测任务专属的IoU阈值、指标计算器

        参数:
            dataloader (torch.utils.data.DataLoader, 可选): 验证集数据加载器
            save_dir (Path, 可选): 验证结果保存目录(如runs/detect/val)
            args (dict[str, Any], 可选): 验证参数(如conf、iou、max_det、save_json等)
            _callbacks (list[Any], 可选): 验证过程中执行的回调函数列表(如日志打印、结果保存)
        """
        # 调用父类BaseValidator的初始化方法,传入数据加载器、保存目录、参数、回调函数
        super().__init__(dataloader, save_dir, args, _callbacks)
        # 初始化数据集类型标记(默认非COCO/LVIS)
        self.is_coco = False
        self.is_lvis = False
        # 初始化类别映射(模型类别→数据集类别)
        self.class_map = None
        # 标记任务类型为检测(detect)
        self.args.task = "detect"
        # 定义mAP计算的IoU阈值向量:0.5到0.95,步长0.05(共10个阈值)
        self.iouv = torch.linspace(0.5, 0.95, 10)
        # 记录IoU阈值数量(固定为10)
        self.niou = self.iouv.numel()
        # 初始化检测指标计算器(封装P/R/mAP计算逻辑)
        self.metrics = DetMetrics()

    def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
        """
        对验证批次数据做预处理:设备迁移、归一化、半精度转换
        确保输入符合模型推理要求,与训练阶段的预处理逻辑对齐

        参数:
            batch (dict[str, Any]): 批次数据字典,包含img(图像张量)、cls(类别)、bboxes(框坐标)等

        返回:
            (dict[str, Any]): 预处理后的批次数据字典
        """
        # 遍历批次字典,将所有张量移至指定设备(GPU/CPU):
        # - CUDA设备启用non_blocking=True(非阻塞传输,提升数据加载速度)
        for k, v in batch.items():
            if isinstance(v, torch.Tensor):
                batch[k] = v.to(self.device, non_blocking=self.device.type == "cuda")
        # 图像归一化+精度转换:
        # - 半精度(half)/浮点型(float)转换(适配模型推理精度)
        # - 除以255,将像素值从[0,255]缩放到[0,1]
        batch["img"] = (batch["img"].half() if self.args.half else batch["img"].float()) / 255
        return batch

    def init_metrics(self, model: torch.nn.Module) -> None:
        """
        初始化检测评估指标:识别数据集类型、配置类别映射、开启JSON保存开关
        是验证前的核心准备步骤,确保指标计算适配数据集格式

        参数:
            model (torch.nn.Module): 待验证的YOLO检测模型实例
        """
        # 获取验证集路径(从数据配置中提取val字段)
        val = self.data.get(self.args.split, "")
        # 判断是否为COCO数据集:路径包含"coco"且以val2017.txt/test-dev2017.txt结尾
        self.is_coco = (
            isinstance(val, str)
            and "coco" in val
            and (val.endswith(f"{os.sep}val2017.txt") or val.endswith(f"{os.sep}test-dev2017.txt"))
        )
        # 判断是否为LVIS数据集:路径包含"lvis"且非COCO
        self.is_lvis = isinstance(val, str) and "lvis" in val and not self.is_coco
        # 配置类别映射:
        # - COCO数据集:将模型的80类索引映射到COCO官方91类索引
        # - 非COCO:直接使用连续索引(1~类别数)
        self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1, len(model.names) + 1))
        # 开启JSON保存开关:验证模式+COCO/LVIS数据集+非训练阶段时自动开启
        self.args.save_json |= self.args.val and (self.is_coco or self.is_lvis) and not self.training
        # 绑定模型类别名、类别数到验证器
        self.names = model.names
        self.nc = len(model.names)
        # 标记模型是否为端到端模式(预留属性,适配特殊模型结构)
        self.end2end = getattr(model, "end2end", False)
        # 初始化已验证样本数、JSON结果列表
        self.seen = 0
        self.jdict = []
        # 将类别名绑定到指标计算器(便于逐类别指标打印)
        self.metrics.names = model.names
        # 初始化混淆矩阵(用于可视化类别预测错误):
        # - names=model.names:类别名映射
        # - save_matches=plots+visualize:开启匹配样本可视化(错误预测样本)
        self.confusion_matrix = ConfusionMatrix(names=model.names, save_matches=self.args.plots and self.args.visualize)

    def get_desc(self) -> str:
        """
        生成格式化的指标打印标题字符串(用于日志输出,对齐列宽)
        示例输出:
                          Class     Images  Instances       Box(P       R   mAP50  mAP50-95)

        返回:
            (str): 格式化的标题字符串
        """
        return ("%22s" + "%11s" * 6) % (    # 类别名占22字符宽度(适配长类别名);其余指标各占11字符宽度,保证打印对齐
            "Class",          # 类别名(all表示整体)
            "Images",         # 验证样本数
            "Instances",      # 真实目标实例数
            "Box(P",          # 精确率(Precision)
            "R",              # 召回率(Recall)
            "mAP50",          # mAP@0.5
            "mAP50-95)",      # mAP@0.5:0.95
        )

    def postprocess(self, preds: torch.Tensor) -> list[dict[str, torch.Tensor]]:
        """
        对模型原始预测执行非极大值抑制(NMS)后处理,过滤冗余检测框
        是检测任务的核心后处理步骤,确保每个目标仅保留最优预测框

        参数:
            preds (torch.Tensor): 模型原始预测张量(维度:[B, N, 4+1+NC],4=框坐标,1=置信度,NC=类别数)

        返回:
            (list[dict[str, torch.Tensor]]): 后处理后的预测结果列表,每个元素为字典:
                - bboxes: 过滤后的框坐标(xyxy格式)
                - conf: 框的置信度
                - cls: 框的类别索引
                - extra: 额外信息(预留字段),如旋转框角度
        """
        # 执行NMS:
        # - conf: 置信度阈值(过滤低置信度框)
        # - iou: NMS的IoU阈值(合并重叠框)
        # - nc: 类别数(0表示自动识别)
        # - multi_label: 允许一个框预测多个类别
        # - agnostic: 单类别/agnostic_nms模式下跨类别NMS
        # - max_det: 单图最大检测框数量
        # - end2end: 适配端到端模型的输出格式
        # - rotated: 适配旋转框检测(OBB任务)
        outputs = nms.non_max_suppression(
            preds,
            self.args.conf,
            self.args.iou,
            nc=0 if self.args.task == "detect" else self.nc,
            multi_label=True,
            agnostic=self.args.single_cls or self.args.agnostic_nms,
            max_det=self.args.max_det,
            end2end=self.end2end,
            rotated=self.args.task == "obb",
        )
        # 将NMS输出转换为结构化字典列表(便于后续指标计算)
        return [{"bboxes": x[:, :4], "conf": x[:, 4], "cls": x[:, 5], "extra": x[:, 6:]} for x in outputs]

    def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
        """
        预处理单样本的真实标签:提取当前样本的类别/框坐标,转换坐标格式并对齐尺寸
        为后续指标计算做准备,确保真实标签与预测结果维度匹配

        参数:
            si (int): 批次内样本索引(如0~7,对应批次样本数 8)
            batch (dict[str, Any]): 批次数据字典(包含cls、bboxes、ori_shape等)

        返回:
            (dict[str, Any]): 预处理后的单样本真实标签字典:
                - cls: 类别索引(张量)
                - bboxes: 框坐标(xyxy格式,适配模型输入尺寸)
                - ori_shape: 原始图像尺寸(H,W)
                - imgsz: 模型输入尺寸(H,W)
                - ratio_pad: 缩放/填充比例(用于后续框缩放)
                - im_file: 图像文件路径
        """
        # 提取当前样本的索引掩码(batch_idx标记每个框所属的样本)
        idx = batch["batch_idx"] == si
        # 提取当前样本的类别(去除冗余维度)
        cls = batch["cls"][idx].squeeze(-1)
        # 提取当前样本的框坐标(xywh格式)
        bbox = batch["bboxes"][idx]
        # 提取原始图像尺寸、模型输入尺寸、缩放/填充比例
        ori_shape = batch["ori_shape"][si]
        imgsz = batch["img"].shape[2:]
        ratio_pad = batch["ratio_pad"][si]
        # 若存在真实框(真实标注>0),将xywh转换为xyxy(适配IOU计算),并缩放到模型输入尺寸(对应imgsz为[h,w]框的xyxy维度)
        if cls.shape[0]:
            #  图像格式[[1, 0, 1, 0]]作用:[480,640] => [640,480,640,480]
            bbox = ops.xywh2xyxy(bbox) * torch.tensor(imgsz, device=self.device)[[1, 0, 1, 0]]
        # 返回结构化的真实标签字典
        return {
            "cls": cls,
            "bboxes": bbox,
            "ori_shape": ori_shape,
            "imgsz": imgsz,
            "ratio_pad": ratio_pad,
            "im_file": batch["im_file"][si],
        }

    def _prepare_pred(self, pred: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
        """
        预处理单样本的预测结果:单类别任务时强制类别索引为0
        确保单类别验证时指标计算逻辑统一

        参数:
            pred (dict[str, torch.Tensor]): 后处理后的预测结果字典

        返回:
            (dict[str, torch.Tensor]): 预处理后的预测结果字典
        """
        # 单类别任务:将所有预测框的类别索引置为0(统一指标计算逻辑)
        if self.args.single_cls:
            pred["cls"] *= 0
        return pred

    def update_metrics(self, preds: list[dict[str, torch.Tensor]], batch: dict[str, Any]) -> None:
        """
        用预测结果和真实标签更新指标统计:计算TP/FP、更新混淆矩阵、保存预测结果
        是验证过程的核心步骤,逐样本累积指标数据

        参数:
            preds (list[dict[str, torch.Tensor]]): 批次预测结果列表(每个元素为单样本预测字典)
            batch (dict[str, Any]): 批次真实标签字典
        """
        # 遍历批次内每个样本的预测结果
        for si, pred in enumerate(preds):
            # 累计已验证样本数
            self.seen += 1
            # 预处理当前样本的真实标签
            pbatch = self._prepare_batch(si, batch)
            # 预处理当前样本的预测结果
            predn = self._prepare_pred(pred)

            # 转换真实类别为NumPy数组(适配指标计算器输入)
            cls = pbatch["cls"].cpu().numpy()
            # 判断是否无预测框
            no_pred = predn["cls"].shape[0] == 0
            # 更新指标计算器统计信息:
            # - _process_batch: 计算TP矩阵(按IoU阈值)
            # - target_cls: 真实类别
            # - target_img: 样本包含的类别
            # - conf: 预测置信度
            # - pred_cls: 预测类别
            self.metrics.update_stats(
                {
                    **self._process_batch(predn, pbatch),
                    "target_cls": cls,
                    "target_img": np.unique(cls),
                    "conf": np.zeros(0) if no_pred else predn["conf"].cpu().numpy(),
                    "pred_cls": np.zeros(0) if no_pred else predn["cls"].cpu().numpy(),
                }
            )
            # 可视化相关:
            if self.args.plots:
                # 更新混淆矩阵(统计类别预测错误)
                self.confusion_matrix.process_batch(predn, pbatch, conf=self.args.conf)
                # 可视化匹配样本(错误预测的样本)
                if self.args.visualize:
                    self.confusion_matrix.plot_matches(batch["img"][si], pbatch["im_file"], self.save_dir)

            # 无预测框时跳过结果保存
            if no_pred:
                continue

            # 结果保存:缩放预测框到原始图像尺寸(消除预处理的缩放/填充影响)
            if self.args.save_json or self.args.save_txt:
                predn_scaled = self.scale_preds(predn, pbatch)
            # 保存为COCO/LVIS格式JSON
            if self.args.save_json:
                self.pred_to_json(predn_scaled, pbatch)
            # 保存为TXT文件(归一化坐标)
            if self.args.save_txt:
                self.save_one_txt(
                    predn_scaled,
                    self.args.save_conf,
                    pbatch["ori_shape"],
                    self.save_dir / "labels" / f"{Path(pbatch['im_file']).stem}.txt",
                )

    def finalize_metrics(self) -> None:
        """
        最终化指标:补充推理速度、混淆矩阵信息,保存指标到指定目录
        验证结束前的收尾步骤,确保指标信息完整
        """
        # 绘制混淆矩阵:
        # - normalize=True: 归一化(百分比)
        # - normalize=False: 原始数量
        if self.args.plots:
            for normalize in True, False:
                # 生成归一化/非归一化两种混淆矩阵图
                self.confusion_matrix.plot(save_dir=self.save_dir, normalize=normalize, on_plot=self.on_plot)
        # 补充推理速度到指标,将验证过程的推理速度(img/s)绑定到指标对象
        self.metrics.speed = self.speed
        # 补充混淆矩阵到指标
        self.metrics.confusion_matrix = self.confusion_matrix
        # 绑定保存目录到指标(便于结果保存)
        self.metrics.save_dir = self.save_dir

    def gather_stats(self) -> None:
        """
        多GPU分布式验证时聚合所有进程的指标和JSON结果:
        - 主进程(RANK=0)收集所有进程的stats和jdict,合并后更新
        - 非主进程仅发送数据,清空本地统计
        确保分布式验证的指标结果准确
        """
        if RANK == 0:
            # 初始化收集容器(数量=获取全局进程总数)
            gathered_stats = [None] * dist.get_world_size()
            # 收集所有进程的指标统计
            dist.gather_object(self.metrics.stats, gathered_stats, dst=0)
            # 合并统计信息(按key聚合)
            merged_stats = {key: [] for key in self.metrics.stats.keys()}
            for stats_dict in gathered_stats:
                for key in merged_stats:
                    merged_stats[key].extend(stats_dict[key])
            gathered_jdict = [None] * dist.get_world_size()
            # 收集所有进程的JSON结果
            dist.gather_object(self.jdict, gathered_jdict, dst=0)
            # 合并JSON结果
            self.jdict = []
            for jdict in gathered_jdict:
                self.jdict.extend(jdict)
            # 更新指标统计和已验证样本数
            self.metrics.stats = merged_stats
            self.seen = len(self.dataloader.dataset)
        elif RANK > 0:
            # 非主进程发送数据后清空本地统计
            dist.gather_object(self.metrics.stats, None, dst=0)
            dist.gather_object(self.jdict, None, dst=0)
            self.jdict = []
            self.metrics.clear_stats()

    def get_stats(self) -> dict[str, Any]:
        """
        计算并返回最终的检测指标字典(P/R/mAP等)
        是验证的核心输出,包含所有关键评估指标

        返回:
            (dict[str, Any]): 指标字典,示例:
                {
                    "metrics/precision(B)": 0.85,
                    "metrics/recall(B)": 0.80,
                    "metrics/mAP50(B)": 0.88,
                    "metrics/mAP50-95(B)": 0.65,
                    "fitness": 0.72
                }
        """
        # 处理指标(计算P/R/mAP、绘制指标曲线)
        # - plot = self.args.plots:启用指标曲线可视化(PR曲线 / mAP曲线);
        self.metrics.process(save_dir=self.save_dir, plot=self.args.plots, on_plot=self.on_plot)
        # 清空临时统计(释放内存)
        self.metrics.clear_stats()
        # 返回最终指标字典
        return self.metrics.results_dict

    def print_results(self) -> None:
        """
        打印验证指标:先打印整体指标,再打印逐类别指标(verbose模式)
        便于用户快速查看模型性能
        """
        # 定义打印格式:22字符类别名 + 2个整数(图像数/实例数) + 4个浮点型指标
        pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys)
        # 打印整体指标(all行)
        LOGGER.info(pf % ("all", self.seen, self.metrics.nt_per_class.sum(), *self.metrics.mean_results()))
        # 无真实标签时打印警告(无法计算有效指标)
        if self.metrics.nt_per_class.sum() == 0:
            LOGGER.warning(f"no labels found in {self.args.task} set, can not compute metrics without labels")

        # 逐类别打印指标(verbose模式+非训练+多类别+有统计数据时)
        if self.args.verbose and not self.training and self.nc > 1 and len(self.metrics.stats):
            for i, c in enumerate(self.metrics.ap_class_index):
                LOGGER.info(
                    pf
                    % (
                        self.names[c],                  # 类别名
                        self.metrics.nt_per_image[c],   # 该类别出现的图像数
                        self.metrics.nt_per_class[c],   # 该类别的实例数
                        *self.metrics.class_result(i),  # 该类别的P/R/mAP50/mAP50-95
                    )
                )

    def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, Any]) -> dict[str, np.ndarray]:
        """
        计算预测框与真实框的匹配矩阵(TP矩阵):
        - 按IoU阈值(0.5~0.95)判断预测框是否为真阳性(TP)
        是mAP计算的核心步骤

        参数:
            preds (dict[str, torch.Tensor]): 预处理后的预测结果字典(bboxes/cls)
            batch (dict[str, Any]): 预处理后的真实标签字典(bboxes/cls)

        返回:
            (dict[str, np.ndarray]): 包含TP矩阵的字典,TP矩阵维度:[预测框数, 10](10个IoU阈值)
        """
        # 无真实框或无预测框时,返回空TP矩阵
        if batch["cls"].shape[0] == 0 or preds["cls"].shape[0] == 0:
            return {"tp": np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)}
        # 计算真实框与预测框的IoU矩阵(两两IoU,维度:[真实框数, 预测框数])
        iou = box_iou(batch["bboxes"], preds["bboxes"])
        # 匹配预测框与真实框,基于类别和IoU匹配,生成N×10的生成TP矩阵(True=真阳性,False=假阳性)
        return {"tp": self.match_predictions(preds["cls"], batch["cls"], iou).cpu().numpy()}

    def build_dataset(self, img_path: str, mode: str = "val", batch: int | None = None) -> torch.utils.data.Dataset:
        """
        构建YOLO验证数据集(适配YOLO的输入要求:stride对齐、矩形推理)
        与训练数据集构建逻辑一致,但验证模式禁用训练增强

        参数:
            img_path (str): 验证图像文件夹路径
            mode (str): 数据集模式,固定为"val"(验证)
            batch (int, 可选): 批次大小,仅用于矩形推理的尺寸计算

        返回:
            (Dataset): 配置好的YOLO验证数据集实例
        """
        # 调用build_yolo_dataset构建数据集:
        # 验证模式启用矩形推理,禁用数据增强
        # - stride=self.stride:模型下采样步长,保证图像尺寸为stride整数倍
        return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, stride=self.stride)

    def get_dataloader(self, dataset_path: str, batch_size: int) -> torch.utils.data.DataLoader:
        """
        构建验证数据加载器:禁用打乱、适配编译模式、设置worker数
        确保验证过程的稳定性和可重复性

        参数:
            dataset_path (str): 验证数据集路径
            batch_size (int): 验证批次大小

        返回:
            (torch.utils.data.DataLoader): 配置好的验证数据加载器
        """
        # 构建验证数据集
        dataset = self.build_dataset(dataset_path, batch=batch_size, mode="val")
        # 构建数据加载器:
        # - workers: 使用指定的worker数
        # - shuffle=False: 验证禁用打乱(确保结果可复现)
        # - rank=-1: 非分布式模式
        # - drop_last=compile: 编译模式下丢弃最后不完整批次
        # - pin_memory=training: 训练模式启用内存锁定(提升数据传输速度)
        return build_dataloader(
            dataset,
            batch_size,
            self.args.workers,
            shuffle=False,
            rank=-1,
            drop_last=self.args.compile,
            pin_memory=self.training,
        )

    def plot_val_samples(self, batch: dict[str, Any], ni: int) -> None:
        """
        可视化验证样本的真实标注,并保存为图片(检查标注质量)
        保存路径:save_dir/val_batch{ni}_labels.jpg

        参数:
            batch (dict[str, Any]): 批次数据字典
            ni (int): 批次索引(用于命名图片文件)
        """
        plot_images(
            labels=batch,               # 真实标注信息
            paths=batch["im_file"],     # 图像文件路径
            fname=self.save_dir / f"val_batch{ni}_labels.jpg",  # 保存路径
            names=self.names,           # 类别名映射,标注框旁显示类别名)
            on_plot=self.on_plot,       # 绘图回调函数
        )

    def plot_predictions(
        self, batch: dict[str, Any], preds: list[dict[str, torch.Tensor]], ni: int, max_det: int | None = None
    ) -> None:
        """
        可视化验证样本的预测结果(对比真实标注),保存为图片
        保存路径:save_dir/val_batch{ni}_pred.jpg

        参数:
            batch (dict[str, Any]): 批次数据字典
            preds (list[dict[str, torch.Tensor]]): 批次预测结果列表
            ni (int): 批次索引
            max_det (int, 可选): 单图最大可视化检测框数(默认使用args.max_det)
        """
        # 预留优化标记
        # TODO: optimize this
        # 为每个预测结果添加批次索引(适配plot_images的批量可视化)
        for i, pred in enumerate(preds):
            pred["batch_idx"] = torch.ones_like(pred["conf"]) * i   # 长度 = N(该样本的检测框数量),值全为i的张量,标记该样本所有框的归属
        # 提取预测结果的键(bboxes/conf/cls等)
        keys = preds[0].keys()
        # 确定最大可视化框数
        max_det = max_det or self.args.max_det
        # 拼接批次内所有预测结果(基于已经按置信度降序排序后的预测框限制最大框数)
        batched_preds = {k: torch.cat([x[k][:max_det] for x in preds], dim=0) for k in keys}
        # 预留修复标记
        # TODO: fix this
        # 将预测框从xyxy转换为xywh(适配plot_images的输入格式)
        batched_preds["bboxes"][:, :4] = ops.xyxy2xywh(batched_preds["bboxes"][:, :4])
        # 绘制预测结果并保存
        plot_images(
            images=batch["img"],        # 批次图像
            labels=batched_preds,       # 拼接后的预测结果
            paths=batch["im_file"],     # 图像文件路径
            fname=self.save_dir / f"val_batch{ni}_pred.jpg",     # 保存路径
            names=self.names,           # 类别名
            on_plot=self.on_plot,       # 绘图回调函数
        )  # pred

    def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
        """
        将预测结果保存为TXT文件(归一化坐标格式),每行对应一个检测框:
        格式:<类别索引> <x_center> <y_center> <width> <height> [置信度](可选)

        参数:
            predn (dict[str, torch.Tensor]): 缩放后的预测结果字典
            save_conf (bool): 是否保存置信度
            shape (tuple[int, int]): 原始图像尺寸(H,W)
            file (Path): TXT文件保存路径
        """
        # 导入Results类(封装检测结果的保存逻辑)
        from ultralytics.engine.results import Results
        # 构建Results实例并保存为TXT
        Results(
            np.zeros((shape[0], shape[1]), dtype=np.uint8),     # 空图像(仅用于初始化)
            path=None,
            names=self.names,
            # 拼接框坐标、置信度、类别索引(维度:[N, 6])
            boxes=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
        ).save_txt(file, save_conf=save_conf)

    def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
        """
        将预测结果转换为COCO/LVIS格式的JSON(用于官方评估工具)
        JSON条目格式:
        {
            "image_id": 图像ID,
            "file_name": 图像文件名,
            "category_id": 数据集类别索引,
            "bbox": [x, y, width, height](左上角坐标+宽高),
            "score": 置信度
        }

        参数:
            predn (dict[str, torch.Tensor]): 缩放后的预测结果字典
            pbatch (dict[str, Any]): 预处理后的单样本真实标签字典
        """
        # 提取图像文件路径和文件名前缀
        path = Path(pbatch["im_file"])
        stem = path.stem
        # 确定图像ID(数字前缀则转整数,否则用字符串)
        image_id = int(stem) if stem.isnumeric() else stem
        # 将预测框从xyxy转换为xywh(COCO格式要求)
        box = ops.xyxy2xywh(predn["bboxes"])
        # 将xy中心坐标转换为左上角坐标(COCO格式要求)
        box[:, :2] -= box[:, 2:] / 2
        # 遍历每个预测框,构建JSON条目
        for b, s, c in zip(box.tolist(), predn["conf"].tolist(), predn["cls"].tolist()):
            self.jdict.append(
                {
                    "image_id": image_id,
                    "file_name": path.name,
                    "category_id": self.class_map[int(c)],  # 映射到数据集类别索引
                    "bbox": [round(x, 3) for x in b],       # 保留3位小数
                    "score": round(s, 5),                   # 保留5位小数
                }
            )

    def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
        """
        将预测框从模型输入尺寸缩放到原始图像尺寸(消除预处理的缩放/填充影响)
        确保保存的框坐标与原始图像匹配

        参数:
            predn (dict[str, torch.Tensor]): 预处理后的预测结果字典
            pbatch (dict[str, Any]): 预处理后的单样本真实标签字典

        返回:
            (dict[str, torch.Tensor]): 缩放后的预测结果字典(bboxes适配原始尺寸)
        """

        return {
            **predn,
            # 缩放框坐标:模型输入尺寸 → 原始图像尺寸(根据模型输入尺寸、原始尺寸、缩放比例和填充值,将框缩放至原始图像)
            "bboxes": ops.scale_boxes(
                pbatch["imgsz"],
                predn["bboxes"].clone(),    # 避免修改原预测框张量
                pbatch["ori_shape"],
                ratio_pad=pbatch["ratio_pad"],
            ),
        }

    def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
        """
        调用COCO/LVIS官方评估工具计算指标,补充到stats字典中
        是第三方工具评估的入口,提升指标的权威性

        参数:
            stats (dict[str, Any]): 当前指标字典

        返回:
            (dict[str, Any]): 更新后的指标字典(包含COCO/LVIS评估结果)
        """
        # 定义预测JSON和标注JSON路径
        pred_json = self.save_dir / "predictions.json"  # 预测结果JSON
        anno_json = (
            self.data["path"]
            / "annotations"
            # COCO用instances_val2017.json,LVIS用lvis_v1_<split>.json
            / ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
        )  # 真实标注JSON
        # 调用COCO评估函数
        return self.coco_evaluate(stats, pred_json, anno_json)

    def coco_evaluate(
        self,
        stats: dict[str, Any],
        pred_json: str,
        anno_json: str,
        iou_types: str | list[str] = "bbox",
        suffix: str | list[str] = "Box",
    ) -> dict[str, Any]:
        """
        基于faster-coco-eval库执行COCO/LVIS指标评估(比官方pycocotools更快)
        计算mAP50、mAP50-95,LVIS额外计算APr/APc/APf(稀有/常见/频繁类别)

        参数:
            stats (dict[str, Any]): 待更新的指标字典
            pred_json (str | Path): 预测结果JSON路径
            anno_json (str | Path): 真实标注JSON路径
            iou_types (str | list[str]): IoU评估类型 bbox/segm(默认"bbox",检测任务)
            suffix (str | list[str]): 指标名后缀,用于区分bbox/segm(默认"Box",区分框/分割/关键点)

        返回:
            (dict[str, Any]): 更新后的指标字典(包含COCO/LVIS评估结果)
        """
        # 仅在保存JSON+COCO/LVIS数据集+有预测结果时执行评估
        if self.args.save_json and (self.is_coco or self.is_lvis) and len(self.jdict):
            LOGGER.info(f"\nEvaluating faster-coco-eval mAP using {pred_json} and {anno_json}...")
            try:
                # 检查JSON文件是否存在
                for x in pred_json, anno_json:
                    assert x.is_file(), f"{x} file not found"
                # 统一iou_types和suffix为列表格式
                iou_types = [iou_types] if isinstance(iou_types, str) else iou_types
                suffix = [suffix] if isinstance(suffix, str) else suffix
                # 检查并安装faster-coco-eval依赖(版本≥1.6.7)
                check_requirements("faster-coco-eval>=1.6.7")
                # 导入faster-coco-eval库
                from faster_coco_eval import COCO, COCOeval_faster

                # 加载真实标注和预测结果
                anno = COCO(anno_json)
                pred = anno.loadRes(pred_json)
                # 遍历IoU类型执行评估
                for i, iou_type in enumerate(iou_types):
                    val = COCOeval_faster(
                        anno, pred, iouType=iou_type, lvis_style=self.is_lvis, print_function=LOGGER.info
                    )
                    # 指定待评估的图像ID(仅验证集图像)
                    val.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files]
                    # 执行评估、累积结果、打印总结
                    val.evaluate()
                    val.accumulate()
                    val.summarize()

                    # 更新指标字典(mAP50和mAP50-95)
                    stats[f"metrics/mAP50({suffix[i][0]})"] = val.stats_as_dict["AP_50"]    # [0] 是取 suffix[i] 字符串的首字母
                    stats[f"metrics/mAP50-95({suffix[i][0]})"] = val.stats_as_dict["AP_all"]

                    # LVIS额外指标(稀有/常见/频繁类别AP)
                    if self.is_lvis:
                        stats[f"metrics/APr({suffix[i][0]})"] = val.stats_as_dict["APr"]
                        stats[f"metrics/APc({suffix[i][0]})"] = val.stats_as_dict["APc"]
                        stats[f"metrics/APf({suffix[i][0]})"] = val.stats_as_dict["APf"]

                # LVIS数据集的fitness用框的mAP50-95计算
                if self.is_lvis:
                    stats["fitness"] = stats["metrics/mAP50-95(B)"]
            except Exception as e:
                # 评估失败时打印警告(不中断验证流程)
                LOGGER.warning(f"faster-coco-eval unable to run: {e}")
        return stats

适配YOLO检测的核心特性

特性 实现方式
多IoU阈值mAP计算 初始化iouv=0.5~0.95_process_batch生成N×10的TP矩阵
COCO/LVIS适配 init_metrics自动识别数据集类型,class_map处理类别映射
NMS后处理 postprocess兼容普通/旋转框、单/多类别NMS,参数全可配置
结果多格式导出 save_one_txt(归一化TXT)、pred_to_json(COCO JSON)
混淆矩阵可视化 finalize_metrics生成归一化/非归一化两种混淆矩阵图

工程化核心优化

优化点 实现方式
分布式指标聚合 gather_stats通过dist.gather_object收集所有进程的stats/jdict
验证效率提升 使用faster_coco_eval替代官方工具,评估速度提升数倍
内存/速度优化 非阻塞设备传输、半精度推理、验证批次增大、内存锁定(pin_memory)
兼容性处理 单/多类别检测、普通/旋转框检测、COCO/LVIS数据集无缝适配

调试与可视化能力

可视化项 用途
验证样本标注可视化 检查标注质量(val_batch{ni}_labels.jpg)
预测结果可视化 直观展示检测效果(val_batch{ni}_pred.jpg)
混淆矩阵可视化 分析类别误检/漏检情况(confusion_matrix.png)
指标曲线可视化 查看PR/mAP曲线,分析模型性能(PR_curve.png/mAP_curve.png)

关键注意事项

  1. 分布式验证 :多GPU验证时需确保dist.init_process_group已初始化,否则gather_stats会报错;
  2. COCO评估 :需保证数据集标注文件路径正确,且安装faster-coco-eval,否则无法生成官方mAP;
  3. 单类别检测 :设置args.single_cls=True后,_prepare_pred会将预测类别置0,保证指标计算正确;
  4. 旋转框检测(OBB) :需设置args.task="obb"postprocess会启用旋转框NMS;
  5. 小目标检测 :需增大max_det(如1000),避免小目标框被过滤,影响mAP计算。

总结

详细介绍了 Ultralytics 框架中继承自 BaseValidator 的 YOLO 目标检测专用验证器。

相关推荐
谷睿同学2 小时前
华为HCIA-AI认证是什么级别?考试内容与适用人群有哪些?
人工智能·华为·hcie·考证
1024小神2 小时前
xcode也有了自己独有的Ai本地大语言模型支持了
人工智能·语言模型·自然语言处理
无妄无望2 小时前
思维链:Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
人工智能·语言模型·自然语言处理
岁月蹉跎的一杯酒2 小时前
Clion opencv C++无法直接读取本地图像
c++·人工智能·opencv
阿杰学AI2 小时前
AI核心知识49——大语言模型之Model Collapse(简洁且通俗易懂版)
人工智能·ai·语言模型·aigc·ai训练·模型崩溃·model collapse
虹科网络安全2 小时前
艾体宝洞察 | “顶会”看安全(三):Black hat-从底层突破AI安全 :利用 NVIDIA 漏洞实现容器逃逸
人工智能·安全
●VON2 小时前
AI辅助学习如何避免依赖陷阱?
人工智能·学习
XiaoMu_0012 小时前
基于深度学习的文物图像修复系统
人工智能·深度学习
点云SLAM2 小时前
Incisive英文单词学习
人工智能·学习·英文单词学习·雅思备考·incisive·犀利的、有洞察力的·直击核心、犀利有力、分析深刻