【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析
文章目录
- [【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析](#【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析)
- 前言
- 所需的库和模块
- [DetectionValidator 类](#DetectionValidator 类)
-
- 完整代码
-
- 总结
前言
代码路径: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_suppression 的 multi_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已正确计算(父类BaseValidator的benchmark方法) |
分布式指标聚合: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) |
关键注意事项
- 分布式验证 :多GPU验证时需确保
dist.init_process_group已初始化,否则gather_stats会报错;
- COCO评估 :需保证数据集标注文件路径正确,且安装
faster-coco-eval,否则无法生成官方mAP;
- 单类别检测 :设置
args.single_cls=True后,_prepare_pred会将预测类别置0,保证指标计算正确;
- 旋转框检测(OBB) :需设置
args.task="obb",postprocess会启用旋转框NMS;
- 小目标检测 :需增大
max_det(如1000),避免小目标框被过滤,影响mAP计算。
总结
详细介绍了 Ultralytics 框架中继承自 BaseValidator 的 YOLO 目标检测专用验证器。