【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用验证器代码val.py解析
前言
代码路径:ultralytics\models\yolo\detect\val.py
这段代码是 Ultralytics YOLO 框架中目标检测模型专用验证器 DetectionValidator 的核心实现,继承自基础验证器 BaseValidator,专门适配 YOLO 目标检测的验证特性(如 COCO/LVIS 指标评估、NMS 后处理、混淆矩阵计算、结果可视化),封装了从「数据预处理→模型推理→预测后处理→指标计算→结果保存 / 可视化→分布式指标聚合」的全流程验证逻辑,是 YOLO 检测模型验证 / 评估的核心入口。
【YOLOv8-Ultralytics 系列文章目录】
所需的库和模块
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import torch
import torch.distributed as dist
from ultralytics.data import build_dataloader, build_yolo_dataset, converter
from ultralytics.engine.validator import BaseValidator
from ultralytics.utils import LOGGER, RANK, nms, ops
from ultralytics.utils.checks import check_requirements
from ultralytics.utils.metrics import ConfusionMatrix, DetMetrics, box_iou
from ultralytics.utils.plotting import plot_images
DetectionValidator 类
整体概览
| 项目 |
详情 |
| 类名 |
DetectionValidator |
| 父类 |
BaseValidator(YOLO通用验证器基类,封装通用验证流程) |
| 核心定位 |
YOLO目标检测任务专用验证器,继承BaseValidator通用逻辑,适配检测任务专属评估需求:多格式数据集兼容、核心指标(mAP/P/R)计算、NMS后处理、标签匹配、结果可视化/多格式保存、分布式验证、第三方COCO/LVIS工具评估 |
| 核心依赖模块 |
ultralytics.data(数据集/加载器/COCO格式转换)、ultralytics.engine.validator(BaseValidator)、ultralytics.utils(日志/NMS/ops/依赖检查)、ultralytics.utils.metrics(ConfusionMatrix/DetMetrics/box_iou)、ultralytics.utils.plotting(plot_images)、torch(张量/分布式)、numpy(数值计算)、pathlib/os(路径处理)、faster-coco-eval(COCO/LVIS快速评估) |
| 关键特性 |
1. 多格式数据集兼容(COCO/LVIS/自定义,自动识别格式并适配类别映射);2. 核心检测指标计算(mAP@0.5:0.95、Precision、Recall、Fitness);3. NMS后处理过滤冗余检测框;4. 按IoU阈值(0.5~0.95)匹配预测-真实标签生成TP矩阵;5. 多维度可视化(混淆矩阵/验证样本标注/预测对比/PR曲线);6. 多格式结果保存(TXT归一化坐标、COCO/LVIS JSON);7. 分布式验证指标聚合(多GPU数据同步);8. 第三方工具评估(faster-coco-eval,支持LVIS稀有/常见/频繁类别AP);9. 单类别任务适配(强制类别索引为0);10. 端到端模型兼容(适配特殊输出格式) |
| 典型使用流程 |
初始化验证器→配置参数(模型/数据集/保存规则等)→初始化指标(识别数据集类型/类别映射)→加载验证数据集/加载器→逐批次预处理(设备迁移/归一化)→模型推理→NMS后处理→更新指标(TP/FP/混淆矩阵)→分布式聚合指标→最终化指标(混淆矩阵绘图/速度补充)→计算最终指标→打印整体/逐类别结果→保存TXT/JSON→调用第三方COCO/LVIS评估工具 |
1. DetectionValidator 类属性说明表
| 属性名 |
类型 |
说明 |
| is_coco |
bool |
数据集是否为COCO格式(决定类别映射和JSON输出规则) |
| is_lvis |
bool |
数据集是否为LVIS格式(适配稀有/常见/频繁类别AP指标) |
| class_map |
list[int] |
模型类别索引到数据集类别索引的映射表(如COCO80→COCO91) |
| metrics |
DetMetrics |
检测指标计算器,封装P/R/mAP/Fitness的计算逻辑 |
| 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/LVIS格式的预测结果列表,用于官方评估工具 |
| stats |
dict[str, list[torch.Tensor]] |
验证过程的统计缓存字典,存储TP/FP/置信度等中间数据 |
| end2end |
bool |
模型是否为端到端结构(适配特殊模型输出格式) |
| confusion_matrix |
ConfusionMatrix |
混淆矩阵对象,统计类别预测错误并可视化 |
2. DetectionValidator 类方法说明表
| 方法名 |
功能说明 |
| init |
初始化检测验证器,配置IoU阈值、指标计算器、任务类型等核心属性 |
| preprocess |
验证批次数据预处理(设备迁移、像素归一化、半精度/浮点型转换) |
| init_metrics |
初始化评估环境(识别COCO/LVIS数据集、配置类别映射、开启JSON保存) |
| get_desc |
生成格式化的指标打印标题字符串,保证日志输出列对齐 |
| postprocess |
对模型原始预测执行NMS后处理,过滤冗余检测框 |
| _prepare_batch |
预处理单样本真实标签(坐标格式转换、尺寸对齐模型输入) |
| _prepare_pred |
预处理单样本预测结果(单类别任务强制类别索引为0) |
| update_metrics |
核心指标更新:计算TP矩阵、更新混淆矩阵、保存TXT/JSON预测结果 |
| finalize_metrics |
验证收尾:绘制混淆矩阵、补充推理速度、绑定保存目录到指标对象 |
| gather_stats |
分布式验证聚合多GPU的指标/JSON结果,保证主进程数据完整 |
| get_stats |
计算并返回最终指标字典(P/R/mAP/Fitness等) |
| print_results |
打印验证结果(整体指标+verbose模式下逐类别指标) |
| _process_batch |
计算预测框与真实框的IoU矩阵,生成N×10的TP判断张量 |
| build_dataset |
构建YOLO验证数据集(矩形推理、禁用增强、对齐模型stride) |
| get_dataloader |
构建验证数据加载器(禁用打乱、适配编译模式、设置worker数) |
| plot_val_samples |
可视化验证样本真实标注,保存为标签检查图 |
| plot_predictions |
可视化验证样本预测结果,对比真实标注生成预测图 |
| save_one_txt |
将预测结果保存为TXT(归一化xywh坐标,可选保存置信度) |
| pred_to_json |
将预测结果转换为COCO/LVIS格式JSON,用于官方评估工具 |
| scale_preds |
将预测框从模型输入尺寸缩放到原始图像尺寸,消除预处理影响 |
| eval_json |
调用COCO/LVIS官方评估工具,补充第三方验证指标 |
| coco_evaluate |
基于faster-coco-eval执行快速评估,支持LVIS专属APr/APc/APf指标 |
初始化函数:__init__
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], 可选): 验证过程中执行的回调函数列表(如日志打印、结果保存)
"""
super().__init__(dataloader, save_dir, args, _callbacks)
self.is_coco = False
self.is_lvis = False
self.class_map = None
self.args.task = "detect"
self.iouv = torch.linspace(0.5, 0.95, 10)
self.niou = self.iouv.numel()
self.metrics = DetMetrics()
| 项目 |
详情 |
| 函数名 |
__init__ |
| 功能概述 |
继承父类通用验证器逻辑,初始化检测任务专属的指标、IoU阈值、数据集标识等核心属性 |
| 返回值 |
无(构造函数) |
| 核心逻辑 |
调用父类初始化,设置检测任务专属参数(IoU阈值、指标计算器、数据集标识) |
| 注意事项 |
IoU阈值数量(niou=10)固定,若需自定义需修改torch.linspace参数 |
批次预处理:preprocess
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
"""
对验证批次数据做预处理:设备迁移、归一化、半精度转换
确保输入符合模型推理要求,与训练阶段的预处理逻辑对齐
参数:
batch (dict[str, Any]): 批次数据字典,包含img(图像张量)、cls(类别)、bboxes(框坐标)等
返回:
(dict[str, Any]): 预处理后的批次数据字典
"""
for k, v in batch.items():
if isinstance(v, torch.Tensor):
batch[k] = v.to(self.device, non_blocking=self.device.type == "cuda")
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
def init_metrics(self, model: torch.nn.Module) -> None:
"""
初始化检测评估指标:识别数据集类型、配置类别映射、开启JSON保存开关
是验证前的核心准备步骤,确保指标计算适配数据集格式
参数:
model (torch.nn.Module): 待验证的YOLO检测模型实例
"""
val = self.data.get(self.args.split, "")
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"))
)
self.is_lvis = isinstance(val, str) and "lvis" in val and not self.is_coco
self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1, len(model.names) + 1))
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)
self.seen = 0
self.jdict = []
self.metrics.names = model.names
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
def get_desc(self) -> str:
"""
生成格式化的指标打印标题字符串(用于日志输出,对齐列宽)
示例输出:
Class Images Instances Box(P R mAP50 mAP50-95)
返回:
(str): 格式化的标题字符串
"""
return ("%22s" + "%11s" * 6) % (
"Class",
"Images",
"Instances",
"Box(P",
"R",
"mAP50",
"mAP50-95)",
)
| 项目 |
详情 |
| 函数名 |
get_desc |
| 功能概述 |
生成格式化的指标打印标题字符串,适配检测任务的mAP/PR指标展示 |
| 返回值 |
str:格式化的指标标题(如Class Images Instances Box(P R mAP50 mAP50-95)) |
| 核心逻辑 |
按固定宽度拼接类别、图像数、实例数、Box相关指标(P/R/mAP50/mAP50-95) |
| 设计亮点 |
动态适配检测指标维度,打印格式统一且易读 |
| 注意事项 |
字符串宽度固定,若类别名过长会导致换行,需按需调整宽度 |
预测后处理:postprocess
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: 额外信息(预留字段),如旋转框角度
"""
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",
)
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超过阈值,无论类别是否相同,只保留置信度最高的框 |
单类别检测、或多类别目标易混淆/重叠的场景(如汽车/卡车、猫/狗),减少重复检测 |
预测张量形状为 [B, N, 4 + 1 + nc],当 nc=0 时NMS 函数内部会自动从预测张量的维度推断类别数nc。
批次预处理(单样本):_prepare_batch
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: 图像文件路径
"""
idx = batch["batch_idx"] == si
cls = batch["cls"][idx].squeeze(-1)
bbox = batch["bboxes"][idx]
ori_shape = batch["ori_shape"][si]
imgsz = batch["img"].shape[2:]
ratio_pad = batch["ratio_pad"][si]
if cls.shape[0]:
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
def _prepare_pred(self, pred: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
"""
预处理单样本的预测结果:单类别任务时强制类别索引为0
确保单类别验证时指标计算逻辑统一
参数:
pred (dict[str, torch.Tensor]): 后处理后的预测结果字典
返回:
(dict[str, torch.Tensor]): 预处理后的预测结果字典
"""
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
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)
cls = pbatch["cls"].cpu().numpy()
no_pred = predn["cls"].shape[0] == 0
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)
if self.args.save_json:
self.pred_to_json(predn_scaled, pbatch)
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
def finalize_metrics(self) -> None:
"""
最终化指标:补充推理速度、混淆矩阵信息,保存指标到指定目录
验证结束前的收尾步骤,确保指标信息完整
"""
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)
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
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)
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()
dist.gather_object(self.jdict, gathered_jdict, dst=0)
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
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
}
"""
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
def print_results(self) -> None:
"""
打印验证指标:先打印整体指标,再打印逐类别指标(verbose模式)
便于用户快速查看模型性能
"""
pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys)
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")
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),
)
)
| 项目 |
详情 |
| 函数名 |
print_results |
| 功能概述 |
格式化打印整体指标和逐类别指标,便于终端查看验证结果 |
| 返回值 |
无 |
| 核心逻辑 |
打印整体指标→打印逐类别指标(verbose模式)→无标注时告警 |
| 设计亮点 |
按AP排序打印类别指标,便于快速识别低性能类别;无标注时友好告警 |
| 注意事项 |
仅在self.args.verbose=True且非训练模式下打印逐类别指标 |
verbose 模式是 DetectionValidator 验证器中控制指标打印粒度的配置项,开启后(self.args.verbose=True)会在验证完成时额外打印每个类别的详细检测指标(如精确率、召回率、mAP 等),而非仅打印整体汇总指标。
批次匹配计算:_process_batch
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阈值)
"""
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 = box_iou(batch["bboxes"], preds["bboxes"])
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
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验证数据集实例
"""
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
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")
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
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
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)
"""
for i, pred in enumerate(preds):
pred["batch_idx"] = torch.ones_like(pred["conf"]) * i
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}
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,
)
| 项目 |
详情 |
| 函数名 |
plot_predictions |
| 功能概述 |
可视化验证样本的预测结果,对比真实标注展示检测效果 |
| 返回值 |
无 |
| 核心逻辑 |
拼接批次预测结果→转换框格式→绘制预测框→保存至验证目录 |
| 设计亮点 |
限制最大展示框数,避免因检测框过多导致画面杂乱、无法判断模型真实性能;自动匹配类别名,可视化清晰 |
| 注意事项 |
预测框需先缩放至原始图像尺寸,否则位置偏移 |
TXT结果保存:save_one_txt
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文件保存路径
"""
from ultralytics.engine.results import Results
Results(
np.zeros((shape[0], shape[1]), dtype=np.uint8),
path=None,
names=self.names,
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
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
image_id = int(stem) if stem.isnumeric() else stem
box = ops.xyxy2xywh(predn["bboxes"])
box[:, :2] -= box[:, 2:] / 2
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],
"score": round(s, 5),
}
)
| 项目 |
详情 |
| 函数名 |
pred_to_json |
| 功能概述 |
将单样本预测结果转换为COCO JSON格式,用于官方工具评估 |
| 返回值 |
无 |
| 核心逻辑 |
提取图像ID→转换框格式(xyxy→xywh)→拼接JSON条目→添加到jdict列表 |
| 设计亮点 |
严格遵循COCO JSON格式,支持非数字图像ID,兼容官方评估工具 |
| 注意事项 |
类别映射需正确(COCO数据集需80→91类映射),否则评估结果错误 |
预测框缩放:scale_preds
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
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
"""
调用COCO/LVIS官方评估工具计算指标,补充到stats字典中
是第三方工具评估的入口,提升指标的权威性
参数:
stats (dict[str, Any]): 当前指标字典
返回:
(dict[str, Any]): 更新后的指标字典(包含COCO/LVIS评估结果)
"""
pred_json = self.save_dir / "predictions.json"
anno_json = (
self.data["path"]
/ "annotations"
/ ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
)
return self.coco_evaluate(stats, pred_json, anno_json)
| 项目 |
详情 |
| 函数名 |
eval_json |
| 功能概述 |
调用COCO/LVIS官方评估工具,基于JSON结果计算精准指标 |
| 返回值 |
dict[str, Any]:更新后的指标字典(包含COCO/LVIS官方mAP) |
| 设计亮点 |
自动识别数据集类型,加载对应标注文件,无需手动指定 |
| 注意事项 |
需确保标注文件路径正确,否则评估工具无法找到标注 |
COCO指标评估:coco_evaluate
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评估结果)
"""
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:
for x in pred_json, anno_json:
assert x.is_file(), f"{x} file not found"
iou_types = [iou_types] if isinstance(iou_types, str) else iou_types
suffix = [suffix] if isinstance(suffix, str) else suffix
check_requirements("faster-coco-eval>=1.6.7")
from faster_coco_eval import COCO, COCOeval_faster
anno = COCO(anno_json)
pred = anno.loadRes(pred_json)
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
)
val.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files]
val.evaluate()
val.accumulate()
val.summarize()
stats[f"metrics/mAP50({suffix[i][0]})"] = val.stats_as_dict["AP_50"]
stats[f"metrics/mAP50-95({suffix[i][0]})"] = val.stats_as_dict["AP_all"]
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"]
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,否则会降级为警告并跳过评估 |
完整代码
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import torch
import torch.distributed as dist
from ultralytics.data import build_dataloader, build_yolo_dataset, converter
from ultralytics.engine.validator import BaseValidator
from ultralytics.utils import LOGGER, RANK, nms, ops
from ultralytics.utils.checks import check_requirements
from ultralytics.utils.metrics import ConfusionMatrix, DetMetrics, box_iou
from ultralytics.utils.plotting import plot_images
class DetectionValidator(BaseValidator):
"""
YOLO目标检测任务专用验证器,继承自通用验证器基类BaseValidator
核心作用:为YOLO检测模型提供端到端的验证流程,适配检测任务的专属评估需求,包含:
- 多格式数据集兼容(COCO/LVIS/自定义数据集,自动识别格式)
- 检测核心指标计算(mAP@0.5:0.95、Precision、Recall、Fitness)
- 预测结果后处理(非极大值抑制NMS过滤冗余框)
- 真实标签与预测结果匹配(按IoU阈值计算TP/FP矩阵)
- 结果可视化(混淆矩阵、验证样本标注/预测对比图、错误匹配样本)
- 多格式结果保存(TXT归一化坐标、COCO/LVIS格式JSON)
- 分布式验证支持(多GPU指标聚合,保证结果一致性)
- 第三方工具评估(调用faster-coco-eval执行COCO/LVIS官方指标评估)
属性:
is_coco (bool): 数据集是否为COCO格式(决定类别映射和JSON输出规则)
is_lvis (bool): 数据集是否为LVIS格式(适配稀有/常见/频繁类别AP指标)
class_map (list[int]): 模型类别索引到数据集类别索引的映射表(如COCO80→COCO91)
metrics (DetMetrics): 检测指标计算器,封装P/R/mAP/Fitness的计算逻辑
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/LVIS格式的预测结果列表,用于官方工具评估
stats (dict[str, list[torch.Tensor]]): 验证过程的统计缓存字典,存储TP/FP/置信度等中间数据
end2end (bool): 模型是否为端到端结构(适配特殊模型输出格式)
confusion_matrix (ConfusionMatrix): 混淆矩阵对象,统计类别预测错误并可视化
方法:
__init__: 初始化检测验证器,配置IoU阈值、指标计算器、任务类型等核心属性
preprocess: 验证批次数据预处理(设备迁移、像素归一化、精度转换)
init_metrics: 初始化评估环境(识别数据集类型、配置类别映射、开启JSON保存)
get_desc: 生成格式化的指标打印标题字符串,用于日志输出的列对齐
postprocess: 对模型原始预测执行NMS后处理,过滤冗余检测框
_prepare_batch: 预处理单样本真实标签(坐标格式转换、尺寸对齐)
_prepare_pred: 预处理单样本预测结果(单类别任务强制类别索引为0)
update_metrics: 核心指标更新逻辑(计算TP矩阵、更新混淆矩阵、保存预测结果)
finalize_metrics: 验证收尾(绘制混淆矩阵、补充推理速度、保存指标数据)
gather_stats: 分布式验证时聚合多GPU的指标和JSON结果,保证主进程数据完整
get_stats: 计算并返回最终指标字典(包含P/R/mAP/Fitness等核心指标)
print_results: 打印验证结果(整体指标+逐类别指标,支持verbose模式)
_process_batch: 计算预测框与真实框的匹配矩阵,生成N×10的TP判断张量
build_dataset: 构建YOLO验证数据集(启用矩形推理、禁用训练增强、对齐模型步长)
get_dataloader: 构建验证数据加载器(禁用打乱、适配编译模式、设置worker数)
plot_val_samples: 可视化验证样本的真实标注,保存为标签检查图
plot_predictions: 可视化验证样本的预测结果,对比真实标注生成预测图
save_one_txt: 将预测结果保存为TXT文件(归一化xywh坐标,可选保存置信度)
pred_to_json: 将预测结果转换为COCO/LVIS格式JSON,用于官方评估工具
scale_preds: 将预测框从模型输入尺寸缩放到原始图像尺寸,消除预处理影响
eval_json: 调用COCO/LVIS官方评估工具,补充第三方验证指标
coco_evaluate: 基于faster-coco-eval库执行快速评估,支持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], 可选): 验证过程中执行的回调函数列表(如日志打印、结果保存)
"""
super().__init__(dataloader, save_dir, args, _callbacks)
self.is_coco = False
self.is_lvis = False
self.class_map = None
self.args.task = "detect"
self.iouv = torch.linspace(0.5, 0.95, 10)
self.niou = self.iouv.numel()
self.metrics = DetMetrics()
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
"""
对验证批次数据做预处理:设备迁移、归一化、半精度转换
确保输入符合模型推理要求,与训练阶段的预处理逻辑对齐
参数:
batch (dict[str, Any]): 批次数据字典,包含img(图像张量)、cls(类别)、bboxes(框坐标)等
返回:
(dict[str, Any]): 预处理后的批次数据字典
"""
for k, v in batch.items():
if isinstance(v, torch.Tensor):
batch[k] = v.to(self.device, non_blocking=self.device.type == "cuda")
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 = self.data.get(self.args.split, "")
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"))
)
self.is_lvis = isinstance(val, str) and "lvis" in val and not self.is_coco
self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1, len(model.names) + 1))
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)
self.seen = 0
self.jdict = []
self.metrics.names = model.names
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) % (
"Class",
"Images",
"Instances",
"Box(P",
"R",
"mAP50",
"mAP50-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: 额外信息(预留字段),如旋转框角度
"""
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",
)
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: 图像文件路径
"""
idx = batch["batch_idx"] == si
cls = batch["cls"][idx].squeeze(-1)
bbox = batch["bboxes"][idx]
ori_shape = batch["ori_shape"][si]
imgsz = batch["img"].shape[2:]
ratio_pad = batch["ratio_pad"][si]
if cls.shape[0]:
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]): 预处理后的预测结果字典
"""
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)
cls = pbatch["cls"].cpu().numpy()
no_pred = predn["cls"].shape[0] == 0
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)
if self.args.save_json:
self.pred_to_json(predn_scaled, pbatch)
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:
"""
最终化指标:补充推理速度、混淆矩阵信息,保存指标到指定目录
验证结束前的收尾步骤,确保指标信息完整
"""
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)
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)
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()
dist.gather_object(self.jdict, gathered_jdict, dst=0)
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
}
"""
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模式)
便于用户快速查看模型性能
"""
pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys)
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")
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),
)
)
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阈值)
"""
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 = box_iou(batch["bboxes"], preds["bboxes"])
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验证数据集实例
"""
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")
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)
"""
for i, pred in enumerate(preds):
pred["batch_idx"] = torch.ones_like(pred["conf"]) * i
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}
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,
)
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文件保存路径
"""
from ultralytics.engine.results import Results
Results(
np.zeros((shape[0], shape[1]), dtype=np.uint8),
path=None,
names=self.names,
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
image_id = int(stem) if stem.isnumeric() else stem
box = ops.xyxy2xywh(predn["bboxes"])
box[:, :2] -= box[:, 2:] / 2
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],
"score": round(s, 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评估结果)
"""
pred_json = self.save_dir / "predictions.json"
anno_json = (
self.data["path"]
/ "annotations"
/ ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
)
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评估结果)
"""
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:
for x in pred_json, anno_json:
assert x.is_file(), f"{x} file not found"
iou_types = [iou_types] if isinstance(iou_types, str) else iou_types
suffix = [suffix] if isinstance(suffix, str) else suffix
check_requirements("faster-coco-eval>=1.6.7")
from faster_coco_eval import COCO, COCOeval_faster
anno = COCO(anno_json)
pred = anno.loadRes(pred_json)
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
)
val.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files]
val.evaluate()
val.accumulate()
val.summarize()
stats[f"metrics/mAP50({suffix[i][0]})"] = val.stats_as_dict["AP_50"]
stats[f"metrics/mAP50-95({suffix[i][0]})"] = val.stats_as_dict["AP_all"]
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"]
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 目标检测专用验证器。
所有评论(0)