ultralytics\utils\metrics.py
目录
metrics.py
1.所需的库和模块
2.def bbox_ioa(box1: np.ndarray, box2: np.ndarray, iou: bool = False, eps: float = 1e-7) -> np.ndarray:
3.def box_iou(box1: torch.Tensor, box2: torch.Tensor, eps: float = 1e-7) -> torch.Tensor:
4.def bbox_iou(box1: torch.Tensor, box2: torch.Tensor, xywh: bool = True, GIoU: bool = False, DIoU: bool = False, CIoU: bool = False, eps: float = 1e-7,) -> torch.Tensor:
5.def _get_covariance_matrix(boxes: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
6.def smooth_bce(eps: float = 0.1) -> Tuple[float, float]:
7.class ConfusionMatrix:
8.def smooth(y: np.ndarray, f: float = 0.05) -> np.ndarray:
9.def plot_pr_curve(px: np.ndarray, py: np.ndarray, ap: np.ndarray, save_dir: Path = Path("pr_curve.png"), names: dict = {}, on_plot=None,):
10.def plot_mc_curve(px: np.ndarray, py: np.ndarray, save_dir: Path = Path("mc_curve.png"), names: dict = {}, xlabel: str = "Confidence", ylabel: str = "Metric", on_plot=None,):
11.def compute_ap(recall: List[float], precision: List[float]) -> Tuple[float, np.ndarray, np.ndarray]:
12.def ap_per_class(tp: np.ndarray, conf: np.ndarray, pred_cls: np.ndarray, target_cls: np.ndarray, plot: bool = False, on_plot=None, save_dir: Path = Path(), names: dict = {}, eps: float = 1e-16, prefix: str = "",) -> Tuple:
13.class Metric(SimpleClass):
14.class DetMetrics(SimpleClass, DataExportMixin):
# Ultralytics AGPL-3.0 License - https://ultralytics.com/license
"""Model validation metrics."""
import math
import warnings
from pathlib import Path
from typing import Dict, List, Tuple, Union
import numpy as np
import torch
from ultralytics.utils import LOGGER, DataExportMixin, SimpleClass, TryExcept, checks, plt_settings
# OKS_SIGMA 是一个 numpy 数组,其中包含 17 个浮点数,这些数是通过将给定的数组中的每个元素除以 10.0 得到的。
OKS_SIGMA = (
np.array([0.26, 0.25, 0.25, 0.35, 0.35, 0.79, 0.79, 0.72, 0.72, 0.62, 0.62, 1.07, 1.07, 0.87, 0.87, 0.89, 0.89])
/ 10.0
)
# 这段代码定义了一个函数 bbox_ioa ,用于计算两个边界框(bounding boxes)之间的交并比(Intersection over Area,简称 IoA)或交并比(Intersection over Union,简称 IoU)。
# 定义了一个函数 bbox_ioa ,接受以下参数:
# 1.box1 和 2.box2 :两个 NumPy 数组,分别表示两组边界框的坐标,每个边界框用四个值表示: (x1, y1, x2, y2) ,其中 (x1, y1) 是左上角坐标, (x2, y2) 是右下角坐标。
# 3.iou :一个布尔值,默认为 False ,用于指定计算的是 IoA 还是 IoU。
# 4.eps :一个浮点数,默认为 1e-7 ,用于避免除以零的情况。
def bbox_ioa(box1: np.ndarray, box2: np.ndarray, iou: bool = False, eps: float = 1e-7) -> np.ndarray:
# 给定 box1 和 box2,计算 box2 区域的交集。
# 参数:
# box1 (np.ndarray):一个形状为 (N, 4) 的 NumPy 数组,表示 N 个 x1y1x2y2 格式的边界框。
# box2 (np.ndarray):一个形状为 (M, 4) 的 NumPy 数组,表示 M 个 x1y1x2y2 格式的边界框。
# iou (bool,可选):如果为 True,则计算标准 IoU,否则返回 inter_area/box2_area。
# eps (float,可选):一个较小的值,用于避免被零除。
# 返回:
# (np.ndarray):一个形状为 (N, M) 的 NumPy 数组,表示 box2 区域的交集。
"""
Calculate the intersection over box2 area given box1 and box2.
Args:
box1 (np.ndarray): A numpy array of shape (N, 4) representing N bounding boxes in x1y1x2y2 format.
box2 (np.ndarray): A numpy array of shape (M, 4) representing M bounding boxes in x1y1x2y2 format.
iou (bool, optional): Calculate the standard IoU if True else return inter_area/box2_area.
eps (float, optional): A small value to avoid division by zero.
Returns:
(np.ndarray): A numpy array of shape (N, M) representing the intersection over box2 area.
"""
# Get the coordinates of bounding boxes
# 将输入的边界框坐标数组 box1 和 box2 转置( .T ),以便分别提取出每个边界框的左上角和右下角的坐标。 box1.T 和 box2.T 的结果是一个二维数组,其中每一列分别表示所有边界框的 x1 、 y1 、 x2 和 y2 坐标。
b1_x1, b1_y1, b1_x2, b1_y2 = box1.T
b2_x1, b2_y1, b2_x2, b2_y2 = box2.T
# Intersection area
# 计算两个边界框的交集面积。具体步骤如下:
# np.minimum(b1_x2[:, None], b2_x2) 和 np.maximum(b1_x1[:, None], b2_x1) 分别计算两个边界框在 x 轴方向上的交集的右边界和左边界。
# np.minimum(b1_y2[:, None], b2_y2) 和 np.maximum(b1_y1[:, None], b2_y1) 分别计算两个边界框在 y 轴方向上的交集的上边界和下边界。
# 使用 .clip(0) 确保计算结果不会出现负值(因为边界框可能不相交)。
# 通过将 x 轴和 y 轴方向上的交集长度相乘,得到交集面积。
inter_area = (np.minimum(b1_x2[:, None], b2_x2) - np.maximum(b1_x1[:, None], b2_x1)).clip(0) * (
np.minimum(b1_y2[:, None], b2_y2) - np.maximum(b1_y1[:, None], b2_y1)
).clip(0)
# Box2 area
# 计算边界框 box2 的面积。通过计算每个边界框的宽度 (b2_x2 - b2_x1) 和高度 (b2_y2 - b2_y1) 的乘积来得到面积。
area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1)
# 如果 iou 参数为 True ,则计算 IoU 而不是 IoA。具体步骤如下:
# 计算边界框 box1 的面积 box1_area 。
# 将 box1 和 box2 的面积相加,然后减去交集面积 inter_area ,得到并集面积。
if iou:
box1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1)
area = area + box1_area[:, None] - inter_area
# Intersection over box2 area
# 计算并返回 IoA 或 IoU 的值。为了避免除以零的情况,加上了一个非常小的值 eps 。
return inter_area / (area + eps)
# 这段代码定义了一个函数 box_iou ,用于计算两个边界框(bounding boxes)之间的交并比(Intersection over Union,简称 IoU)。
# 定义了一个函数 box_iou ,接受以下参数:
# 1.box1 和 2.box2 :两个 PyTorch 张量,分别表示两组边界框的坐标,每个边界框用四个值表示: (x1, y1, x2, y2) ,其中 (x1, y1) 是左上角坐标, (x2, y2) 是右下角坐标。
# 3.eps :一个浮点数,默认为 1e-7 ,用于避免除以零的情况。
def box_iou(box1: torch.Tensor, box2: torch.Tensor, eps: float = 1e-7) -> torch.Tensor:
# 计算边界框的交并比 (IoU)。
# 参数:
# box1 (torch.Tensor):一个形状为 (N, 4) 的张量,表示 N 个 (x1, y1, x2, y2) 格式的边界框。
# box2 (torch.Tensor):一个形状为 (M, 4) 的张量,表示 M 个 (x1, y1, x2, y2) 格式的边界框。
# eps (float,可选):一个较小的值,用于避免除以零。
# 返回:
# (torch.Tensor):一个 NxM 张量,包含 box1 和 box2 中每个元素的成对 IoU 值。
# 参考:
# https://github.com/pytorch/vision/blob/main/torchvision/ops/boxes.py
"""
Calculate intersection-over-union (IoU) of boxes.
Args:
box1 (torch.Tensor): A tensor of shape (N, 4) representing N bounding boxes in (x1, y1, x2, y2) format.
box2 (torch.Tensor): A tensor of shape (M, 4) representing M bounding boxes in (x1, y1, x2, y2) format.
eps (float, optional): A small value to avoid division by zero.
Returns:
(torch.Tensor): An NxM tensor containing the pairwise IoU values for every element in box1 and box2.
References:
https://github.com/pytorch/vision/blob/main/torchvision/ops/boxes.py
"""
# NOTE: Need .float() to get accurate iou values
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
# box1.float() 和 box2.float() :将输入的边界框坐标张量转换为浮点数类型,以确保计算的准确性。
# box1.float().unsqueeze(1) :在 box1 的第 1 维(索引为 1)插入一个大小为 1 的维度,使其形状从 [N, 4] 变为 [N, 1, 4] ,方便后续的广播操作。
# box2.float().unsqueeze(0) :在 box2 的第 0 维(索引为 0)插入一个大小为 1 的维度,使其形状从 [M, 4] 变为 [1, M, 4] ,方便后续的广播操作。
# .chunk(2, 2) :将每个边界框的坐标张量分成两部分,分别表示左上角坐标 (x1, y1) 和右下角坐标 (x2, y2) 。 chunk(2, 2) 表示在第 2 维(索引为 2)将张量分成 2 个部分。
# (a1, a2) 和 (b1, b2) :分别存储 box1 和 box2 的左上角坐标和右下角坐标。
(a1, a2), (b1, b2) = box1.float().unsqueeze(1).chunk(2, 2), box2.float().unsqueeze(0).chunk(2, 2)
# 计算两个边界框的交集面积。具体步骤如下:
# torch.min(a2, b2) :计算两个边界框在右下角坐标上的最小值,得到交集的右下角坐标。
# torch.max(a1, b1) :计算两个边界框在左上角坐标上的最大值,得到交集的左上角坐标。
# (torch.min(a2, b2) - torch.max(a1, b1)) :计算交集的宽度和高度。
# .clamp_(0) :将负值(表示不相交的情况)钳制为 0。
# .prod(2) :计算交集的面积,即宽度和高度的乘积。
inter = (torch.min(a2, b2) - torch.max(a1, b1)).clamp_(0).prod(2)
# IoU = inter / (area1 + area2 - inter)
# 计算并返回 IoU 的值。具体步骤如下:
# (a2 - a1).prod(2) :计算 box1 的面积。
# (b2 - b1).prod(2) :计算 box2 的面积。
# ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter) :计算并集面积。
# + eps :为了避免除以零的情况,加上一个非常小的值 eps 。
# inter / (...) :计算 IoU,即交集面积除以并集面积。
return inter / ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter + eps)
# 这段代码实现了一个函数 box_iou ,用于计算两个边界框之间的 IoU。它通过计算交集面积和边界框的面积来实现这一目标,并通过一个非常小的常数 eps 避免除以零的情况。这个函数在目标检测和计算机视觉任务中非常有用,例如评估检测模型的性能或进行非极大值抑制(NMS)。
# 这段代码定义了一个函数 bbox_iou ,用于计算两个边界框(bounding boxes)之间的不同类型的交并比(IoU),包括标准的 IoU、广义交并比(GIoU)、距离交并比(DIoU)和完全交并比(CIoU)。
# 定义了一个函数 bbox_iou ,接受以下参数:
# 1.box1 和 2.box2 :两个 PyTorch 张量,分别表示两组边界框的坐标。
# 3.xywh :一个布尔值,默认为 True ,表示输入的边界框坐标格式是 (x, y, w, h) (中心点坐标和宽高)。如果为 False ,则表示坐标格式是 (x1, y1, x2, y2) (左上角和右下角坐标)。
# 4.GIoU 、 5.DIoU 、 6.CIoU :布尔值,分别表示是否计算广义交并比(GIoU)、距离交并比(DIoU)和完全交并比(CIoU)。默认都为 False 。
# 7.eps :一个浮点数,默认为 1e-7 ,用于避免除以零的情况。
def bbox_iou(
box1: torch.Tensor,
box2: torch.Tensor,
xywh: bool = True,
GIoU: bool = False,
DIoU: bool = False,
CIoU: bool = False,
eps: float = 1e-7,
) -> torch.Tensor:
# 计算边界框之间的交并比 (IoU)。
# 此函数支持 `box1` 和 `box2` 的各种形状,只要最后一个维度为 4。
# 例如,您可以传递形状为 (4,)、(N, 4)、(B, N, 4) 或 (B, N, 1, 4) 的张量。
# 在内部,如果 `xywh=True`,代码会将最后一个维度拆分为 (x, y, w, h);
# 如果 `xywh=False`,则拆分为 (x1, y1, x2, y2)。
# 参数:
# box1 (torch.Tensor):表示一个或多个边界框的张量,最后一个维度为 4。
# box2 (torch.Tensor):表示一个或多个边界框的张量,最后一个维度为 4。
# xywh (bool,可选):如果为 True,则输入框为 (x, y, w, h) 格式。如果如果为 False,则输入框的格式为 (x1, y1, x2, y2)。
# GIoU(布尔值,可选):如果为 True,则计算广义 IoU。
# DIoU(布尔值,可选):如果为 True,则计算距离 IoU。
# CIoU(布尔值,可选):如果为 True,则计算完整 IoU。
# eps(浮点数,可选):一个较小的值,用于避免被零除。
"""
Calculate the Intersection over Union (IoU) between bounding boxes.
This function supports various shapes for `box1` and `box2` as long as the last dimension is 4.
For instance, you may pass tensors shaped like (4,), (N, 4), (B, N, 4), or (B, N, 1, 4).
Internally, the code will split the last dimension into (x, y, w, h) if `xywh=True`,
or (x1, y1, x2, y2) if `xywh=False`.
Args:
box1 (torch.Tensor): A tensor representing one or more bounding boxes, with the last dimension being 4.
box2 (torch.Tensor): A tensor representing one or more bounding boxes, with the last dimension being 4.
xywh (bool, optional): If True, input boxes are in (x, y, w, h) format. If False, input boxes are in
(x1, y1, x2, y2) format.
GIoU (bool, optional): If True, calculate Generalized IoU.
DIoU (bool, optional): If True, calculate Distance IoU.
CIoU (bool, optional): If True, calculate Complete IoU.
eps (float, optional): A small value to avoid division by zero.
Returns:
(torch.Tensor): IoU, GIoU, DIoU, or CIoU values depending on the specified flags.
"""
# Get the coordinates of bounding boxes
# 如果输入的边界框坐标格式是 (x, y, w, h) ,则需要将其转换为 (x1, y1, x2, y2) 格式。这里的 x 和 y 是边界框中心点的坐标, w 和 h 是边界框的宽度和高度。
if xywh: # transform from xywh to xyxy
# box1.chunk(4, -1) 和 box2.chunk(4, -1) :将每个边界框的坐标张量分成 4 个部分,分别表示 x 、 y 、 w 和 h 。
# (x1, y1, w1, h1) 和 (x2, y2, w2, h2) :分别存储 box1 和 box2 的中心点坐标和宽高。
(x1, y1, w1, h1), (x2, y2, w2, h2) = box1.chunk(4, -1), box2.chunk(4, -1)
# 计算边界框的半宽和半高:
# w1_ 和 h1_ :边界框 box1 的半宽和半高。
# w2_ 和 h2_ :边界框 box2 的半宽和半高。
w1_, h1_, w2_, h2_ = w1 / 2, h1 / 2, w2 / 2, h2 / 2
# 根据中心点坐标和半宽半高,计算边界框的左上角和右下角坐标:
# b1_x1 、 b1_x2 、 b1_y1 、 b1_y2 :边界框 box1 的左上角和右下角坐标。
# b2_x1 、 b2_x2 、 b2_y1 、 b2_y2 :边界框 box2 的左上角和右下角坐标。
b1_x1, b1_x2, b1_y1, b1_y2 = x1 - w1_, x1 + w1_, y1 - h1_, y1 + h1_
b2_x1, b2_x2, b2_y1, b2_y2 = x2 - w2_, x2 + w2_, y2 - h2_, y2 + h2_
# 如果输入的边界框坐标格式已经是 (x1, y1, x2, y2) ,则直接提取坐标值。
else: # x1, y1, x2, y2 = box1
# box1.chunk(4, -1) 和 box2.chunk(4, -1) :将每个边界框的坐标张量分成 4 个部分,分别表示 x1 、 y1 、 x2 和 y2 。
# b1_x1 、 b1_y1 、 b1_x2 、 b1_y2 :分别存储 box1 的左上角和右下角坐标。
# b2_x1 、 b2_y1 、 b2_x2 、 b2_y2 :分别存储 box2 的左上角和右下角坐标。
b1_x1, b1_y1, b1_x2, b1_y2 = box1.chunk(4, -1)
b2_x1, b2_y1, b2_x2, b2_y2 = box2.chunk(4, -1)
# 计算每个边界框的宽度和高度:
# w1 和 h1 :边界框 box1 的宽度和高度。
# w2 和 h2 :边界框 box2 的宽度和高度。
# 加上 eps 避免除零。
w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
# Intersection area
# 计算两个边界框的交集面积:
# b1_x2.minimum(b2_x2) :计算两个边界框在 x 轴方向上的最小右边界。
# b1_x1.maximum(b2_x1) :计算两个边界框在 x 轴方向上的最大左边界。
# (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)) :计算交集的宽度。如果两个边界框不相交,这个值可能是负数。
# .clamp_(0) :将负值(表示不相交的情况)钳制为 0,确保交集宽度不会是负数。
# 同理,计算交集的高度: b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1) 。
# 交集面积是交集宽度和高度的乘积: (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp_(0) * (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp_(0) 。
inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp_(0) * (
b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)
).clamp_(0)
# Union Area
# 计算两个边界框的并集面积:
# w1 * h1 :计算边界框 box1 的面积。
# w2 * h2 :计算边界框 box2 的面积。
# w1 * h1 + w2 * h2 :计算两个边界框的总面积。
# w1 * h1 + w2 * h2 - inter :从总面积中减去交集面积,得到并集面积。
# 加上 eps 避免除零。
union = w1 * h1 + w2 * h2 - inter + eps
# IoU
# 计算标准的 IoU 值,即交集面积除以并集面积。
iou = inter / union
# 如果需要计算 GIoU、DIoU 或 CIoU,则计算最小外接矩形(convex box)的宽度和高度。
if CIoU or DIoU or GIoU:
cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width
ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height
# 如果需要计算 DIoU 或 CIoU,则计算:
# c2 :最小外接矩形的对角线长度的平方。
# rho2 :两个边界框中心点之间的距离的平方。
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
c2 = cw.pow(2) + ch.pow(2) + eps # convex diagonal squared
rho2 = (
(b2_x1 + b2_x2 - b1_x1 - b1_x2).pow(2) + (b2_y1 + b2_y2 - b1_y1 - b1_y2).pow(2)
) / 4 # center dist**2
# 如果需要计算 CIoU,则:
# 计算 v ,表示边界框的宽高比的差异。
# 计算 alpha ,一个调节因子。
# 返回 CIoU 值,即 IoU 减去距离惩罚项和宽高比惩罚项。
if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
v = (4 / math.pi**2) * ((w2 / h2).atan() - (w1 / h1).atan()).pow(2)
with torch.no_grad():
alpha = v / (v - iou + (1 + eps))
return iou - (rho2 / c2 + v * alpha) # CIoU
# 如果需要计算 DIoU,则返回 DIoU 值,即 IoU 减去距离惩罚项。
return iou - rho2 / c2 # DIoU
# 如果需要计算 GIoU,则返回 GIoU 值,即 IoU 减去最小外接矩形面积与并集面积的差值的比例。
c_area = cw * ch + eps # convex area
return iou - (c_area - union) / c_area # GIoU https://arxiv.org/pdf/1902.09630.pdf
# 如果不需要计算 GIoU、DIoU 或 CIoU,则直接返回标准的 IoU 值。
return iou # IoU
# bbox_iou 函数是一个多功能的边界框交并比计算工具,能够根据输入参数灵活计算标准 IoU、广义交并比(GIoU)、距离交并比(DIoU)和完全交并比(CIoU)。它支持两种常见的边界框坐标格式( xywh 和 xyxy ),并自动转换为统一的坐标表示以便进行计算。通过计算交集面积、并集面积以及额外的几何特征(如最小外接矩形的尺寸、中心点距离等),该函数能够精确地评估两个边界框之间的重叠程度,适用于目标检测中的多种场景,如模型评估和非极大值抑制(NMS)等。
# 这段代码定义了一个函数 _get_covariance_matrix ,用于计算一组高斯边界框(Gaussian bounding boxes)的协方差矩阵的对角线元素。
# 定义了一个函数 _get_covariance_matrix ,接受一个 PyTorch 张量 1.boxes 作为输入,返回一个包含三个 PyTorch 张量的元组。每个返回的张量表示协方差矩阵的一个对角线元素。
def _get_covariance_matrix(boxes: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
# 根据定向边界框生成协方差矩阵。
# 参数:
# boxes (torch.Tensor):一个形状为 (N, 5) 的张量,表示旋转后的边界框,xywhr 格式。
# 返回:
# (torch.Tensor):与原始旋转边界框对应的协方差矩阵。
"""
Generate covariance matrix from oriented bounding boxes.
Args:
boxes (torch.Tensor): A tensor of shape (N, 5) representing rotated bounding boxes, with xywhr format.
Returns:
(torch.Tensor): Covariance matrices corresponding to original rotated bounding boxes.
"""
# Gaussian bounding boxes, ignore the center points (the first two columns) because they are not needed here. 高斯边界框,忽略中心点(前两列),因为这里不需要它们。
# boxes[:, 2:4] :提取输入张量 boxes 中的第 3 列和第 4 列,这两列通常表示边界框的宽度和高度。
# .pow(2) / 12 :将宽度和高度的平方除以 12,这是计算高斯分布的方差的公式。
# boxes[:, 4:] :提取输入张量 boxes 中的第 5 列及以后的列,这些列通常表示边界框的旋转角度。
# torch.cat(..., dim=-1) :将计算得到的方差和旋转角度拼接在一起,形成一个新的张量 gbbs 。
gbbs = torch.cat((boxes[:, 2:4].pow(2) / 12, boxes[:, 4:]), dim=-1)
# gbbs.split(1, dim=-1) :将 gbbs 按照最后一个维度( dim=-1 )分成三个部分,每个部分的大小为 1。
# a 、 b 、 c :分别存储 gbbs 的三个部分,其中 a 和 b 表示方差, c 表示旋转角度。
a, b, c = gbbs.split(1, dim=-1)
# 计算旋转角度的余弦值和正弦值。
cos = c.cos()
sin = c.sin()
# 计算余弦值和正弦值的平方。
cos2 = cos.pow(2)
sin2 = sin.pow(2)
# 计算并返回协方差矩阵的对角线元素:
# a * cos2 + b * sin2 :计算协方差矩阵的第一个对角线元素。
# a * sin2 + b * cos2 :计算协方差矩阵的第二个对角线元素。
# (a - b) * cos * sin :计算协方差矩阵的非对角线元素(虽然这里只返回对角线元素,但这个计算可能用于后续的完整协方差矩阵计算)。
return a * cos2 + b * sin2, a * sin2 + b * cos2, (a - b) * cos * sin
# 这段代码定义了一个函数 smooth_bce ,用于计算平滑二元交叉熵(Binary Cross-Entropy,BCE)损失的正样本和负样本的目标值。
# 定义了一个函数 smooth_bce ,接受一个浮点数参数 1.eps ,默认值为 0.1 。该函数返回一个包含两个浮点数的元组,分别表示平滑二元交叉熵损失的正样本和负样本的目标值。
def smooth_bce(eps: float = 0.1) -> Tuple[float, float]:
# 计算平滑后的正负二分类交叉熵目标值。
"""
Compute smoothed positive and negative Binary Cross-Entropy targets.
Args:
eps (float, optional): The epsilon value for label smoothing.
Returns:
pos (float): Positive label smoothing BCE target.
neg (float): Negative label smoothing BCE target.
References:
https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
"""
# 计算并返回平滑二元交叉熵损失的正样本和负样本的目标值:
# 1.0 - 0.5 * eps :计算正样本的目标值。通过从 1.0 中减去 0.5 * eps ,使正样本的目标值稍微小于 1.0,以避免模型过于自信。
# 0.5 * eps :计算负样本的目标值。通过将 eps 乘以 0.5,使负样本的目标值稍微大于 0.0,以避免模型过于自信。
return 1.0 - 0.5 * eps, 0.5 * eps
# 这段代码定义了一个函数 smooth_bce ,用于计算平滑二元交叉熵损失的正样本和负样本的目标值。通过引入一个小的平滑因子 eps ,可以避免模型在训练过程中过于自信,从而提高模型的泛化能力。具体来说,正样本的目标值被设置为 1.0 - 0.5 * eps ,负样本的目标值被设置为 0.5 * eps 。这种平滑技术在处理不平衡数据集时尤其有用,可以防止模型对多数类过于自信,从而提高对少数类的识别能力。
# 这段代码定义了一个名为 ConfusionMatrix 的类,用于计算和更新目标检测和分类任务中的混淆矩阵。
# 定义了一个名为 ConfusionMatrix 的类,用于处理混淆矩阵的计算和更新。
class ConfusionMatrix:
# 用于计算和更新用于对象检测和分类任务的混淆矩阵的类。
"""
A class for calculating and updating a confusion matrix for object detection and classification tasks.
Attributes:
task (str): The type of task, either 'detect' or 'classify'.
matrix (np.ndarray): The confusion matrix, with dimensions depending on the task.
nc (int): The number of classes.
conf (float): The confidence threshold for detections.
iou_thres (float): The Intersection over Union threshold.
"""
# 这段代码定义了 ConfusionMatrix 类的初始化方法 __init__ ,用于设置混淆矩阵的基本参数和初始化矩阵。
# 定义了类的初始化方法 __init__ ,接受以下参数:
# 1.nc :类别数量(number of classes)。
# 2.conf :检测的置信度阈值,默认为 0.25。
# 3.iou_thres :交并比(IoU)阈值,默认为 0.45。
# 4.task :任务类型,可以是 'detect' 或 'classify' ,默认为 'detect' 。
def __init__(self, nc: int, conf: float = 0.25, iou_thres: float = 0.45, task: str = "detect"):
# 初始化 ConfusionMatrix 实例。
"""
Initialize a ConfusionMatrix instance.
Args:
nc (int): Number of classes.
conf (float, optional): Confidence threshold for detections.
iou_thres (float, optional): IoU threshold for matching detections to ground truth.
task (str, optional): Type of task, either 'detect' or 'classify'.
"""
# 将传入的任务类型 task 赋值给类的属性 self.task 。
self.task = task
# 根据任务类型初始化混淆矩阵 self.matrix :
# 如果任务是检测( 'detect' ),则矩阵大小为 (nc + 1, nc + 1) ,多出的一行一列用于处理背景类别。
# 如果任务是分类( 'classify' ),则矩阵大小为 (nc, nc) ,不包含背景类别。
self.matrix = np.zeros((nc + 1, nc + 1)) if self.task == "detect" else np.zeros((nc, nc))
# 将传入的类别数量 nc 赋值给类的属性 self.nc 。
self.nc = nc # number of classes
# 设置置信度阈值 self.conf :
# 如果传入的 conf 是 None 或 0.001 ,则使用默认值 0.25 。
# 否则,使用传入的 conf 值。
self.conf = 0.25 if conf in {None, 0.001} else conf # apply 0.25 if default val conf is passed
# 将传入的交并比阈值 iou_thres 赋值给类的属性 self.iou_thres 。
self.iou_thres = iou_thres
# 这段代码初始化了一个 ConfusionMatrix 对象,设置了任务类型、类别数量、置信度阈值和交并比阈值,并根据任务类型初始化了混淆矩阵。具体来说: 任务类型:可以是 'detect' 或 'classify' ,决定了混淆矩阵的大小和是否包含背景类别。 类别数量:决定了混淆矩阵的维度。 置信度阈值:用于过滤检测结果,只有置信度高于此阈值的检测结果才会被考虑。 交并比阈值:用于匹配检测结果和真实标注,只有 IoU 高于此阈值的检测结果才会被视为正确匹配。这些参数和初始化的矩阵为后续的混淆矩阵更新和计算提供了基础。
# 这段代码定义了 ConfusionMatrix 类中的 process_cls_preds 方法,用于处理分类任务的预测结果并更新混淆矩阵。
# 定义了 process_cls_preds 方法,该方法接受两个参数:
# 1.preds :模型的预测结果,通常是一个列表或张量,包含每个样本的预测类别。
# 2.targets :真实标签,通常是一个列表或张量,包含每个样本的真实类别。
def process_cls_preds(self, preds, targets):
# 更新分类任务的混淆矩阵。
# 参数:
# preds (Array[N, min(nc,5)]):预测的类别标签。
# targets (Array[N, 1]):真实类别标签。
"""
Update confusion matrix for classification task.
Args:
preds (Array[N, min(nc,5)]): Predicted class labels.
targets (Array[N, 1]): Ground truth class labels.
"""
# torch.cat(preds) :将 preds 中的所有张量沿着第 0 维拼接起来,形成一个一维张量。
# [:, 0] :提取拼接后的张量的第一列,假设预测结果的第一列是预测的类别。
# torch.cat(targets) :将 targets 中的所有张量沿着第 0 维拼接起来,形成一个一维张量。
preds, targets = torch.cat(preds)[:, 0], torch.cat(targets)
# preds.cpu().numpy() 和 targets.cpu().numpy() :将预测结果和目标标签从 GPU(如果有的话)移动到 CPU,并转换为 NumPy 数组。
# zip(preds.cpu().numpy(), targets.cpu().numpy()) :将预测结果和目标标签配对,形成一个迭代器。
# for p, t in ... :遍历每一对预测结果和目标标签。
# self.matrix[p][t] += 1 :根据预测类别 p 和真实类别 t ,在混淆矩阵 self.matrix 中对应的单元格加 1。
for p, t in zip(preds.cpu().numpy(), targets.cpu().numpy()):
self.matrix[p][t] += 1
# process_cls_preds 方法的作用是处理分类任务的预测结果并更新混淆矩阵。具体步骤如下: 将预测结果和目标标签分别拼接成一维张量。 遍历每一对预测结果和目标标签。 在混淆矩阵中,根据预测类别和真实类别,将对应的单元格的值加 1。这个方法适用于分类任务,通过更新混淆矩阵,可以方便地计算各种评估指标,如准确率、召回率、F1 分数等。
# 这段代码定义了 ConfusionMatrix 类中的 process_batch 方法,用于处理目标检测任务的一个批次的预测结果和真实标注,并更新混淆矩阵。
# 定义了 process_batch 方法,该方法接受以下参数:
# 1.detections :检测结果,通常是一个张量,包含每个检测的边界框坐标、置信度和类别。
# 2.gt_bboxes :真实边界框,通常是一个张量,包含每个真实标注的边界框坐标。
# 3.gt_cls :真实类别,通常是一个张量,包含每个真实标注的类别。
def process_batch(self, detections, gt_bboxes, gt_cls):
# 更新物体检测任务的混淆矩阵。
# 参数:
# detections (Array[N, 6] | Array[N, 7]):检测到的边界框及其相关信息。每行应包含 (x1, y1, x2, y2, conf, class),如果是 obb,则应包含一个附加元素 `angle`。
# gt_bboxes (Array[M, 4]| Array[N, 5]):xyxy/xyxyr 格式的地面实况边界框。
# gt_cls (Array[M]):类别标签。
"""
Update confusion matrix for object detection task.
Args:
detections (Array[N, 6] | Array[N, 7]): Detected bounding boxes and their associated information.
Each row should contain (x1, y1, x2, y2, conf, class)
or with an additional element `angle` when it's obb.
gt_bboxes (Array[M, 4]| Array[N, 5]): Ground truth bounding boxes with xyxy/xyxyr format.
gt_cls (Array[M]): The class labels.
"""
# 检查真实标注( gt_cls )是否为空。如果 gt_cls 的第0维(即行数)为0,说明没有真实标注。
if gt_cls.shape[0] == 0: # Check if labels is empty
# 如果存在检测结果( detections 不为空),则继续处理。
if detections is not None:
# 过滤掉置信度低于阈值( self.conf )的检测结果。假设检测结果的第4列是置信度。
detections = detections[detections[:, 4] > self.conf]
# 提取过滤后的检测结果的类别。假设检测结果的第5列是类别,并将其转换为整数类型。
detection_classes = detections[:, 5].int()
# 对于每个检测到的类别 dc ,在混淆矩阵中,将对应行(预测类别)和最后一列(背景类别)的值加1。这表示这些检测是假正例(false positives),因为没有真实标注与它们匹配。
for dc in detection_classes:
self.matrix[dc, self.nc] += 1 # false positives
# 处理完没有真实标注的情况后,返回。
return
# 如果检测结果为空( detections 为 None ),则处理没有检测结果的情况。
if detections is None:
# 将真实标注的类别转换为整数类型。
gt_classes = gt_cls.int()
# 对于每个真实标注的类别 gc ,在混淆矩阵中,将最后一行(背景类别)和对应列(真实类别)的值加1。这表示这些真实标注是假负例(false negatives),因为没有检测结果与它们匹配。
for gc in gt_classes:
self.matrix[self.nc, gc] += 1 # background FN
# 处理完没有检测结果的情况后,返回。
return
# 过滤掉置信度低于阈值 self.conf 的检测结果。假设检测结果的第 4 列是置信度。
detections = detections[detections[:, 4] > self.conf]
# 将真实标注的类别转换为整数类型。
gt_classes = gt_cls.int()
# 提取检测结果的类别。假设检测结果的第 5 列是类别,并将其转换为整数类型。
detection_classes = detections[:, 5].int()
# 判断是否是定向边界框(oriented bounding box,obb)。如果检测结果有 7 列( detections.shape[1] == 7 )且真实边界框有 5 列( gt_bboxes.shape[1] == 5 ),则认为是定向边界框。定向边界框通常包含额外的角度信息。
is_obb = detections.shape[1] == 7 and gt_bboxes.shape[1] == 5 # with additional `angle` dimension
# 根据是否是定向边界框,选择合适的 IoU 计算方法:
# 如果是定向边界框( is_obb 为 True ),则使用 batch_probiou 函数计算 IoU。 batch_probiou 函数需要真实边界框和检测结果的前 4 列(边界框坐标)以及最后一列(角度信息)。
# 如果不是定向边界框( is_obb 为 False ),则使用 box_iou 函数计算 IoU。 box_iou 函数只需要真实边界框和检测结果的前 4 列(边界框坐标)。
iou = (
batch_probiou(gt_bboxes, torch.cat([detections[:, :4], detections[:, -1:]], dim=-1))
if is_obb
else box_iou(gt_bboxes, detections[:, :4])
)
# 使用 torch.where 找出 IoU 大于阈值 self.iou_thres 的索引。 x 是一个元组,包含两个张量,分别表示满足条件的行索引和列索引。
x = torch.where(iou > self.iou_thres)
# 检查是否有满足 IoU 阈值的匹配。 x[0].shape[0] 表示满足条件的行索引的数量。
if x[0].shape[0]:
# 如果有匹配,将行索引、列索引和对应的 IoU 值拼接成一个二维数组 matches 。
# torch.stack(x, 1) :将 x 中的两个张量(行索引和列索引)沿着第 1 维堆叠起来,形成一个二维张量。
# iou[x[0], x[1]][:, None] :提取满足条件的 IoU 值,并增加一个维度以便拼接。
# torch.cat(..., 1) :将行索引、列索引和 IoU 值拼接在一起。
# .cpu().numpy() :将结果移动到 CPU 并转换为 NumPy 数组。
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()
# 如果有多个匹配,需要对匹配结果进行排序和去重。
if x[0].shape[0] > 1:
# 按照 IoU 值从高到低对匹配结果进行排序。
matches = matches[matches[:, 2].argsort()[::-1]]
# 去除重复的检测结果(列索引)。 np.unique(matches[:, 1], return_index=True)[1] 返回每个唯一列索引的第一个出现的索引,从而保留每个检测结果的最高 IoU 匹配。
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
# 再次按照 IoU 值从高到低对匹配结果进行排序。
matches = matches[matches[:, 2].argsort()[::-1]]
# 去除重复的真实标注(行索引)。 np.unique(matches[:, 0], return_index=True)[1] 返回每个唯一行索引的第一个出现的索引,从而保留每个真实标注的最高 IoU 匹配。
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
# 如果没有匹配, matches 为一个空的 NumPy 数组,形状为 (0, 3) 。
else:
matches = np.zeros((0, 3))
# 检查是否有匹配结果。 matches.shape[0] 表示匹配结果的数量, n 是一个布尔值,表示是否有匹配结果。
n = matches.shape[0] > 0
# 将匹配结果 matches 转置并转换为整数类型。 m0 和 m1 分别表示匹配的真实标注索引和检测结果索引。 _ 是一个占位符,用于忽略匹配结果中的 IoU 值。
m0, m1, _ = matches.transpose().astype(int)
# 遍历每个真实标注的类别 gc 。
for i, gc in enumerate(gt_classes):
# 找出与当前真实标注索引 i 匹配的检测结果索引。
j = m0 == i
# 如果有匹配结果,并且当前真实标注只与一个检测结果匹配,则将混淆矩阵中对应的单元格(预测类别,真实类别)加 1,表示正确检测(true positives)。
if n and sum(j) == 1:
self.matrix[detection_classes[m1[j]], gc] += 1 # correct
# 如果没有匹配结果,或者当前真实标注与多个检测结果匹配(这种情况在去重后不应该发生),则将混淆矩阵中最后一行(背景类别,真实类别)的值加 1,表示假负例(false negatives)。
else:
self.matrix[self.nc, gc] += 1 # true background
# 遍历每个检测结果的类别 dc 。
for i, dc in enumerate(detection_classes):
# 如果当前检测结果没有与任何真实标注匹配,则将混淆矩阵中对应的单元格(预测类别,背景类别)加 1,表示假正例(false positives)。
if not any(m1 == i):
self.matrix[dc, self.nc] += 1 # predicted background
# process_batch 方法的作用是处理目标检测任务的一个批次的预测结果和真实标注,并更新混淆矩阵。具体步骤如下: 检查是否有真实标注和检测结果。 过滤掉置信度低于阈值的检测结果。 计算检测结果和真实标注之间的交并比(IoU)。 根据 IoU 值匹配检测结果和真实标注。 根据匹配结果更新混淆矩阵,记录正确检测、假正例和假负例。这个方法适用于目标检测任务,通过更新混淆矩阵,可以方便地计算各种评估指标,如准确率、召回率、F1 分数等。
# 这段代码定义了一个名为 matrix 的方法,它是 ConfusionMatrix 类的一部分。这个方法的作用是返回当前的混淆矩阵。
# 定义了一个名为 matrix 的方法,该方法属于 ConfusionMatrix 类。这个方法不接受任何参数,除了隐式的 self 参数,它指向类的实例本身。
def matrix(self):
# 返回混淆矩阵。
"""Return the confusion matrix."""
# self.matrix :访问类实例的 matrix 属性,该属性存储了混淆矩阵。
# return :将混淆矩阵返回给调用者。
return self.matrix
# matrix 方法的作用是提供对 ConfusionMatrix 类实例中混淆矩阵的访问。通过调用这个方法,用户可以获取当前的混淆矩阵,以便进行进一步的分析或计算评估指标。这个方法简单直接,主要功能是作为混淆矩阵的访问接口。
# 这段代码定义了 ConfusionMatrix 类中的 tp_fp 方法,用于计算真正例(true positives)和假正例(false positives)。
# 定义了一个名为 tp_fp 的方法,该方法属于 ConfusionMatrix 类。这个方法不接受任何参数,除了隐式的 self 参数,它指向类的实例本身。方法的返回值是一个包含两个 NumPy 数组的元组,分别表示真正例和假正例。
def tp_fp(self) -> Tuple[np.ndarray, np.ndarray]:
# 返回真阳性和假阳性。
# 返回:
# tp (np.ndarray):真阳性。
# fp (np.ndarray):假阳性。
"""
Return true positives and false positives.
Returns:
tp (np.ndarray): True positives.
fp (np.ndarray): False positives.
"""
# self.matrix.diagonal() :提取混淆矩阵的对角线元素,这些元素表示正确分类的样本数量(即真正例)。
# tp :存储真正例的数量。
tp = self.matrix.diagonal() # true positives
# self.matrix.sum(1) :计算混淆矩阵每一行的和,表示每个类别的预测总数。
# fp :通过从每一行的和中减去对角线元素(真正例),得到假正例的数量。
fp = self.matrix.sum(1) - tp # false positives
# self.matrix.sum(0) :计算混淆矩阵每一列的和,表示每个类别的真实总数。
# fn :通过从每一列的和中减去对角线元素(真正例),得到假负例的数量。
# 这行代码被注释掉了,表示不直接使用这个计算结果。
# fn = self.matrix.sum(0) - tp # false negatives (missed detections)
# 如果任务是检测( self.task == "detect" ),则移除背景类别的结果,即返回 tp 和 fp 的前 nc 个元素(不包括最后一个元素)。
# 如果任务是分类,或者不需要移除背景类别,则直接返回 tp 和 fp 。
return (tp[:-1], fp[:-1]) if self.task == "detect" else (tp, fp) # remove background class if task=detect
# tp_fp 方法的作用是计算真正例和假正例的数量。具体步骤如下: 提取混淆矩阵的对角线元素,得到真正例的数量。 计算混淆矩阵每一行的和,然后减去对角线元素,得到假正例的数量。 根据任务类型(检测或分类),决定是否移除背景类别的结果。这个方法为评估模型的性能提供了重要的指标,如准确率、召回率和 F1 分数等。通过计算真正例和假正例,可以更好地理解模型在不同类别上的表现。
# 这段代码定义了 ConfusionMatrix 类中的 plot 方法,用于绘制混淆矩阵并将其保存为图像文件。
# 使用装饰器 @TryExcept 和 @plt_settings 来处理可能的异常和设置绘图环境。
@TryExcept(msg="ConfusionMatrix plot failure")
@plt_settings()
# 定义了 plot 方法,接受以下参数:
# 1.normalize :布尔值,表示是否对混淆矩阵进行归一化,默认为 True 。
# 2.save_dir :字符串,表示保存图像的目录,默认为空字符串。
# 3.names :元组,表示类别的名称,默认为空元组。
# 4.on_plot :可选的回调函数,用于在图像保存后执行某些操作。
def plot(self, normalize: bool = True, save_dir: str = "", names: tuple = (), on_plot=None):
# 使用 matplotlib 绘制混淆矩阵并将其保存到文件中。
"""
Plot the confusion matrix using matplotlib and save it to a file.
Args:
normalize (bool, optional): Whether to normalize the confusion matrix.
save_dir (str, optional): Directory where the plot will be saved.
names (tuple, optional): Names of classes, used as labels on the plot.
on_plot (callable, optional): An optional callback to pass plots path and data when they are rendered.
"""
# 导入 matplotlib.pyplot ,用于绘制混淆矩阵。
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
# 如果 normalize 为 True ,则对混淆矩阵的每一列进行归一化,避免除以零的情况。
array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1e-9) if normalize else 1) # normalize columns
# 将小于 0.005 的值设置为 np.nan ,避免在图上显示过小的值。
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
# 将类别名称从元组或列表转换为列表,以便后续操作。
names = list(names)
# 创建一个大小为 (12, 9) 的绘图窗口,用于绘制混淆矩阵。
fig, ax = plt.subplots(1, 1, figsize=(12, 9))
# 检查类别数量是否大于或等于 100。如果是,为了防止绘制的图像过于拥挤,需要对类别进行下采样。
if self.nc >= 100: # downsample for large class count
# 计算下采样的步长 k 。步长至少为 2,以确保下采样后至少保留 60 个类别。
k = max(2, self.nc // 60) # step size for downsampling, always > 1
# 创建一个切片对象 keep_idx ,用于选择保留的类别索引。 slice(None, None, k) 表示从 0 开始,每隔 k 个元素选择一个。
keep_idx = slice(None, None, k) # create slice instead of array
# 使用切片对象 keep_idx 选择保留的类别名称。
names = names[keep_idx] # slice class names
# 使用切片对象 keep_idx 选择保留的混淆矩阵的行和列。这一步确保了混淆矩阵的大小与保留的类别数量一致。
array = array[keep_idx, :][:, keep_idx] # slice matrix rows and cols
# 计算保留的类别数量 n 。这是通过将总类别数量 self.nc 除以下采样步长 k 并向上取整得到的。
n = (self.nc + k - 1) // k # number of retained classes
# 根据任务类型(分类或检测)调整类别数量:
# 如果任务是分类( self.task == "classify" ),则保留的类别数量为 n 。
# 如果任务是检测,通常需要额外考虑背景类别,因此保留的类别数量为 n + 1 。
nc = nn = n if self.task == "classify" else n + 1 # adjust for background if needed
# 如果类别数量小于 100,则不需要下采样。根据任务类型(分类或检测)直接设置类别数量:
# 如果任务是分类,类别数量为 self.nc 。
# 如果任务是检测,类别数量为 self.nc + 1 。
else:
nc = nn = self.nc if self.task == "classify" else self.nc + 1
# 如果类别数量 nn 在 1 到 98 之间,并且 nn 等于 nc ,则将类别名称 names 和 "background" 作为坐标轴的标签。 否则,使用默认的标签( "auto" ),这通常会导致 Matplotlib 自动生成标签。
ticklabels = (names + ["background"]) if (0 < nn < 99) and (nn == nc) else "auto"
# 创建一个从 0 到 len(ticklabels) - 1 的数组,用于设置坐标轴的刻度位置。
xy_ticks = np.arange(len(ticklabels))
# 根据类别数量 nc 动态调整字体大小,确保字体大小在不同类别数量下都能保持可读性。
tick_fontsize = max(6, 15 - 0.1 * nc) # Minimum size is 6
label_fontsize = max(6, 12 - 0.1 * nc)
title_fontsize = max(6, 12 - 0.1 * nc)
# 根据类别数量 nc 动态调整图表底部的边距,确保图表布局合理。
btm = max(0.1, 0.25 - 0.001 * nc) # Minimum value is 0.1
# 使用 warnings.catch_warnings 和 warnings.simplefilter 忽略可能的警告,特别是当矩阵中存在全 NaN 切片时的警告。
with warnings.catch_warnings():
warnings.simplefilter("ignore") # suppress empty matrix RuntimeWarning: All-NaN slice encountered
# 使用 ax.imshow 绘制混淆矩阵,选择蓝色调色板( "Blues" ),设置最小值为 0.0,并禁用插值。
im = ax.imshow(array, cmap="Blues", vmin=0.0, interpolation="none")
# 设置 x 轴标签的位置在底部。
ax.xaxis.set_label_position("bottom")
# 检查类别数量 nc 是否小于 30。如果类别数量较少,为了保持图表的清晰度,可以在每个单元格中添加数值标签。
if nc < 30: # Add score for each cell of confusion matrix
# 遍历混淆矩阵的前 nc 行。 array[:nc] 确保只处理保留的类别。
for i, row in enumerate(array[:nc]):
# 遍历当前行的前 nc 个元素。 row[:nc] 确保只处理保留的类别。
for j, val in enumerate(row[:nc]):
# 获取当前单元格的值。
val = array[i, j]
# 如果当前单元格的值是 NaN(表示该单元格的值无效或为空),则跳过当前循环迭代。
if np.isnan(val):
continue
# 使用 ax.text 在当前单元格的位置添加文本标签。
# j 和 i 分别表示 x 和 y 坐标,对应于矩阵的列和行。
# f"{val:.2f}" if normalize else f"{int(val)}" :如果 normalize 为 True ,则显示两位小数的值;否则,显示整数值。
# ha="center" 和 va="center" :将文本水平和垂直居中对齐。
# fontsize=10 :设置字体大小为 10。
# color="white" if val > (0.7 if normalize else 2) else "black" :根据单元格的值选择字体颜色。如果值较大(归一化时大于 0.7,未归一化时大于 2),使用白色字体;否则,使用黑色字体。这有助于确保文本在不同背景颜色下都能清晰可见。
ax.text(
j,
i,
f"{val:.2f}" if normalize else f"{int(val)}",
ha="center",
va="center",
fontsize=10,
color="white" if val > (0.7 if normalize else 2) else "black",
)
# 添加颜色条,用于解释矩阵中颜色的含义。
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.05)
# 设置图表的标题。如果 normalize 为 True ,则在标题中添加 "Normalized"。
title = "Confusion Matrix" + " Normalized" * normalize
# 设置 x 轴和 y 轴的标签,分别表示真实类别和预测类别。 使用之前计算的 label_fontsize 设置字体大小,并设置标签的间距。
ax.set_xlabel("True", fontsize=label_fontsize, labelpad=10)
ax.set_ylabel("Predicted", fontsize=label_fontsize, labelpad=10)
# 设置图表的标题,并使用之前计算的 title_fontsize 设置字体大小。 设置标题的间距。
ax.set_title(title, fontsize=title_fontsize, pad=20)
# 设置 x 轴和 y 轴的刻度位置,使用之前计算的 xy_ticks 。
ax.set_xticks(xy_ticks)
ax.set_yticks(xy_ticks)
# 设置坐标轴的刻度参数,确保刻度标签只在底部和左侧显示。
ax.tick_params(axis="x", bottom=True, top=False, labelbottom=True, labeltop=False)
ax.tick_params(axis="y", left=True, right=False, labelleft=True, labelright=False)
# 如果 ticklabels 不是 "auto" ,则使用自定义的刻度标签。 设置刻度标签的字体大小、旋转角度和水平对齐方式。
if ticklabels != "auto":
ax.set_xticklabels(ticklabels, fontsize=tick_fontsize, rotation=90, ha="center")
ax.set_yticklabels(ticklabels, fontsize=tick_fontsize)
# 遍历图表的边框,将所有边框设置为不可见,以确保图表没有边框。 同样,将颜色条的边框设置为不可见。
for s in ["left", "right", "bottom", "top", "outline"]:
if s != "outline":
ax.spines[s].set_visible(False) # Confusion matrix plot don't have outline
cbar.ax.spines[s].set_visible(False)
# 调整图表的布局,确保边距相等。
fig.subplots_adjust(left=0, right=0.84, top=0.94, bottom=btm) # Adjust layout to ensure equal margins
# 构造保存图表的文件名,文件名基于标题并转换为小写,空格替换为下划线。
plot_fname = Path(save_dir) / f"{title.lower().replace(' ', '_')}.png"
# 保存图表为 PNG 文件,设置分辨率为 250 DPI。
fig.savefig(plot_fname, dpi=250)
# 关闭图表窗口,释放资源。
plt.close(fig)
# 如果提供了回调函数 on_plot ,则调用它并传递保存的文件名。
if on_plot:
on_plot(plot_fname)
# plot 方法是 ConfusionMatrix 类的一个功能,用于可视化混淆矩阵。它接受是否归一化、保存路径、类别名称和回调函数等参数,通过 Matplotlib 绘制混淆矩阵,并可选择性地在每个单元格中标注数值。该方法还支持对大量类别进行下采样以简化可视化,并自动调整字体大小和布局以确保图表的可读性和美观性。最终,它将图表保存为图像文件,并在需要时调用回调函数。
# 这段代码定义了 ConfusionMatrix 类中的 print 方法,用于将混淆矩阵的内容打印到控制台。
# 定义了一个名为 print 的方法,该方法属于 ConfusionMatrix 类。这个方法不接受任何参数,除了隐式的 self 参数,它指向类的实例本身。
def print(self):
# 将混淆矩阵打印到控制台。
"""Print the confusion matrix to the console."""
# 遍历混淆矩阵的每一行。 self.matrix.shape[0] 表示混淆矩阵的行数。
for i in range(self.matrix.shape[0]):
# 使用 map(str, self.matrix[i]) 将当前行的每个元素转换为字符串。
# 使用 " ".join(...) 将这些字符串元素连接成一个以空格分隔的字符串。
# 使用 LOGGER.info(...) 将生成的字符串记录到日志中。 LOGGER 是一个日志记录器,用于输出信息。
LOGGER.info(" ".join(map(str, self.matrix[i])))
# print 方法的作用是将混淆矩阵的内容逐行打印到控制台或日志文件中。通过将矩阵的每一行转换为字符串并记录,用户可以直观地查看混淆矩阵的详细内容。这在调试和分析模型性能时非常有用,尤其是在处理分类或检测任务时。
# ConfusionMatrix 类是一个用于目标检测和分类任务的工具类,它能够计算、更新和可视化混淆矩阵。该类支持多种任务类型(如分类和检测),并提供了灵活的配置选项,如置信度阈值和交并比阈值。它还具备处理大量类别时的下采样功能,以简化可视化,并能动态调整字体大小和布局以确保图表的可读性和美观性。此外,该类还提供了方法来打印混淆矩阵、计算真正例和假正例,以及绘制并保存混淆矩阵为图像文件。
# 这段代码定义了一个名为 smooth 的函数,用于对输入的数组进行平滑处理。
# 定义了一个名为 smooth 的函数,接受以下参数:
# 1.y :一个 NumPy 数组,表示需要平滑的数据。
# 2.f :一个浮点数,默认值为 0.05,表示平滑的程度。
def smooth(y: np.ndarray, f: float = 0.05) -> np.ndarray:
# 分数 f 的箱式滤波器。
"""Box filter of fraction f."""
# 计算平滑滤波器的元素数量 nf 。 len(y) * f * 2 用于确定滤波器的大小, round(...) 将其四舍五入为整数, // 2 确保结果为偶数,最后加 1 确保滤波器的大小为奇数。
nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd)
# 创建一个长度为 nf // 2 的数组 p ,其中所有元素均为 1。这个数组用于在数据的开头和结尾进行填充。
p = np.ones(nf // 2) # ones padding
# 使用 np.concatenate 将填充数组 p 与原始数据 y 连接起来。填充数组的值分别为 y 的第一个元素和最后一个元素,以确保平滑处理时边界数据不会突变。
yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded
# 使用 np.convolve 对填充后的数据 yp 进行卷积操作,实现平滑处理。卷积核是一个长度为 nf 的数组,所有元素均为 1/nf ,表示对数据进行平均平滑。 mode="valid" 表示只返回不超出边界的卷积结果,确保输出数组的长度与原始数据 y 一致。
return np.convolve(yp, np.ones(nf) / nf, mode="valid") # y-smoothed
# smooth 函数的作用是对输入的数组 y 进行平滑处理。它通过以下步骤实现: 计算滤波器大小:根据输入数据的长度和平滑程度 f ,计算滤波器的大小 nf ,并确保其为奇数。 创建填充数组:创建一个长度为 nf // 2 的数组 p ,用于在数据的开头和结尾进行填充。 填充数据:将填充数组与原始数据连接起来,确保平滑处理时边界数据不会突变。 卷积操作:使用卷积操作对填充后的数据进行平滑处理,返回平滑后的结果。这个函数适用于需要对时间序列数据或其他一维数据进行平滑处理的场景,例如在信号处理或数据可视化中。
# 这段代码定义了一个名为 plot_pr_curve 的函数,用于绘制精确率-召回率(Precision-Recall,PR)曲线,并将其保存为图像文件。
@plt_settings()
# 定义了一个名为 plot_pr_curve 的函数,接受以下参数:
# 1.px :一个 NumPy 数组,表示召回率(Recall)的值。
# 2.py :一个 NumPy 数组,表示精确率(Precision)的值。
# 3.ap :一个 NumPy 数组,表示平均精确率(Average Precision,AP)的值。
# 4.save_dir :一个 Path 对象,表示保存图像的路径,默认为 "pr_curve.png" 。
# 5.names :一个字典,表示类别名称,默认为空字典。
# 6.on_plot :一个可选的回调函数,用于在图像保存后执行某些操作。
def plot_pr_curve(
px: np.ndarray,
py: np.ndarray,
ap: np.ndarray,
save_dir: Path = Path("pr_curve.png"),
names: dict = {},
on_plot=None,
):
# 绘制精确率-召回率曲线。
"""
Plot precision-recall curve.
Args:
px (np.ndarray): X values for the PR curve.
py (np.ndarray): Y values for the PR curve.
ap (np.ndarray): Average precision values.
save_dir (Path, optional): Path to save the plot.
names (dict, optional): Dictionary mapping class indices to class names.
on_plot (callable, optional): Function to call after plot is saved.
"""
# 导入 matplotlib.pyplot ,用于绘制 PR 曲线。
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
# 创建一个大小为 (9, 6) 的绘图窗口,启用紧凑布局。
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
# 将 py 的维度重新排列,使其适合后续的绘图操作。
py = np.stack(py, axis=1)
# 如果类别数量在 1 到 20 之间,则为每个类别绘制单独的 PR 曲线并显示图例。
if 0 < len(names) < 21: # display per-class legend if < 21 classes
# 遍历每个类别的精确率值 y 。 使用 ax.plot 绘制 PR 曲线,设置线宽为 1,并添加图例标签,显示类别名称和 AP 值。
for i, y in enumerate(py.T):
ax.plot(px, y, linewidth=1, label=f"{names[i]} {ap[i, 0]:.3f}") # plot(recall, precision)
# 如果类别数量较多(超过 20 个),则绘制所有类别的 PR 曲线,颜色为灰色。
else:
ax.plot(px, py, linewidth=1, color="grey") # plot(recall, precision)
# 绘制所有类别的平均 PR 曲线,线宽为 3,颜色为蓝色,并添加图例标签,显示所有类别的平均 AP 值。
ax.plot(px, py.mean(1), linewidth=3, color="blue", label=f"all classes {ap[:, 0].mean():.3f} [email protected]")
# 设置 x 轴和 y 轴的标签、范围,确保图表的坐标轴在 0 到 1 之间。
ax.set_xlabel("Recall")
ax.set_ylabel("Precision")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
# 设置图例的位置,使其位于图表的外部上方。
ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
# 设置图表的标题。
ax.set_title("Precision-Recall Curve") # 精确召回曲线。
# 将图表保存为图像文件,设置分辨率为 250 DPI。
fig.savefig(save_dir, dpi=250)
# 关闭图表窗口,释放资源。
plt.close(fig)
# 如果提供了回调函数 on_plot ,则调用它并传递保存的文件名。
if on_plot:
on_plot(save_dir)
# plot_pr_curve 函数的作用是绘制精确率-召回率曲线,并将其保存为图像文件。它支持为每个类别绘制单独的曲线,并显示图例。如果类别数量较多,它会绘制所有类别的平均曲线。此外,它还支持自定义保存路径和回调函数,以便在图像保存后执行其他操作。这个函数在评估分类或检测模型的性能时非常有用,能够直观地展示模型在不同召回率下的精确率表现。
# 这段代码定义了一个名为 plot_mc_curve 的函数,用于绘制指标-置信度(Metric-Confidence)曲线,并将其保存为图像文件。
@plt_settings()
# 定义了一个名为 plot_mc_curve 的函数,接受以下参数:
# 1.px :一个 NumPy 数组,表示置信度(Confidence)的值。
# 2.py :一个 NumPy 数组,表示指标(Metric)的值。
# 3.save_dir :一个 Path 对象,表示保存图像的路径,默认为 "mc_curve.png" 。
# 4.names :一个字典,表示类别名称,默认为空字典。
# 5.xlabel :一个字符串,表示 x 轴的标签,默认为 "Confidence" 。
# 6.ylabel :一个字符串,表示 y 轴的标签,默认为 "Metric" 。
# 7.on_plot :一个可选的回调函数,用于在图像保存后执行某些操作。
def plot_mc_curve(
px: np.ndarray,
py: np.ndarray,
save_dir: Path = Path("mc_curve.png"),
names: dict = {},
xlabel: str = "Confidence",
ylabel: str = "Metric",
on_plot=None,
):
# 绘制度量置信度曲线。
"""
Plot metric-confidence curve.
Args:
px (np.ndarray): X values for the metric-confidence curve.
py (np.ndarray): Y values for the metric-confidence curve.
save_dir (Path, optional): Path to save the plot.
names (dict, optional): Dictionary mapping class indices to class names.
xlabel (str, optional): X-axis label.
ylabel (str, optional): Y-axis label.
on_plot (callable, optional): Function to call after plot is saved.
"""
# 导入 matplotlib.pyplot ,用于绘制指标-置信度曲线。
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
# 创建一个大小为 (9, 6) 的绘图窗口,启用紧凑布局。
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
# 如果类别数量在 1 到 20 之间,则为每个类别绘制单独的曲线并显示图例。
if 0 < len(names) < 21: # display per-class legend if < 21 classes
# 遍历每个类别的指标值 y 。
# 使用 ax.plot 绘制指标-置信度曲线,设置线宽为 1,并添加图例标签,显示类别名称。
for i, y in enumerate(py):
ax.plot(px, y, linewidth=1, label=f"{names[i]}") # plot(confidence, metric)
# 如果类别数量较多(超过 20 个),则绘制所有类别的曲线,颜色为灰色。
else:
ax.plot(px, py.T, linewidth=1, color="grey") # plot(confidence, metric)
# 对所有类别的平均指标值进行平滑处理,平滑程度为 0.1。
y = smooth(py.mean(0), 0.1)
# 绘制平滑后的平均指标-置信度曲线,线宽为 3,颜色为蓝色。 添加图例标签,显示所有类别的最大指标值及其对应的置信度。
ax.plot(px, y, linewidth=3, color="blue", label=f"all classes {y.max():.2f} at {px[y.argmax()]:.3f}")
# 设置 x 轴和 y 轴的标签、范围,确保图表的坐标轴在 0 到 1 之间。
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
# 设置图例的位置,使其位于图表的外部上方。
ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
# 设置图表的标题。
ax.set_title(f"{ylabel}-Confidence Curve") # {ylabel}-置信曲线。
# 将图表保存为图像文件,设置分辨率为 250 DPI。
fig.savefig(save_dir, dpi=250)
# 关闭图表窗口,释放资源。
plt.close(fig)
# 如果提供了回调函数 on_plot ,则调用它并传递保存的文件名。
if on_plot:
on_plot(save_dir)
# plot_mc_curve 函数的作用是绘制指标-置信度曲线,并将其保存为图像文件。它支持为每个类别绘制单独的曲线,并显示图例。如果类别数量较多,它会绘制所有类别的曲线。此外,它还支持自定义保存路径、x 轴和 y 轴的标签以及回调函数,以便在图像保存后执行其他操作。这个函数在评估分类或检测模型的性能时非常有用,能够直观地展示模型在不同置信度下的指标表现。
# 这段代码定义了一个名为 compute_ap 的函数,用于计算平均精确率(Average Precision,AP),这是评估分类或目标检测模型性能的一个重要指标。
# 定义了一个名为 compute_ap 的函数,接受两个参数:
# 1.recall :一个浮点数列表,表示召回率(Recall)的值。
# 2.precision :一个浮点数列表,表示精确率(Precision)的值。
# 函数返回一个元组,包含 AP 值以及处理后的召回率和精确率数组。
def compute_ap(recall: List[float], precision: List[float]) -> Tuple[float, np.ndarray, np.ndarray]:
# 根据召回率和准确率曲线计算平均准确率 (AP)。
"""
Compute the average precision (AP) given the recall and precision curves.
Args:
recall (list): The recall curve.
precision (list): The precision curve.
Returns:
ap (float): Average precision.
mpre (np.ndarray): Precision envelope curve.
mrec (np.ndarray): Modified recall curve with sentinel values added at the beginning and end.
"""
# Append sentinel values to beginning and end
# 在召回率列表的开头添加 0.0,结尾添加 1.0,确保曲线从 0 到 1。
mrec = np.concatenate(([0.0], recall, [1.0]))
# 在精确率列表的开头添加 1.0,结尾添加 0.0,确保曲线从 1 到 0。
mpre = np.concatenate(([1.0], precision, [0.0]))
# Compute the precision envelope
# 计算精确率的上包络线(precision envelope)。通过反转精确率数组,计算累积最大值,再反转回来,确保精确率在每个召回率点上都是非递增的。
# 为什么这样做?
# 这样做是为了确保精确率在每个召回率点上都是非递增的。在计算 AP 时,我们需要精确率曲线是单调递减的,这样可以避免在召回率增加时精确率意外上升的情况。通过计算累积最大值并反转,我们确保了精确率曲线的单调性。
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
# Integrate area under curve
# 定义积分方法。这里使用 "interp" ,表示通过插值计算 AP。
method = "interp" # methods: 'continuous', 'interp'
# 如果使用插值方法:
# 创建一个从 0 到 1 的 101 个点的数组 x ,这是 COCO 数据集的标准做法。
# 使用 np.interp 在这些点上插值精确率和召回率。
# 使用梯形法则( np.trapezoid 或 np.trapz )计算曲线下面积,即 AP 值。
if method == "interp":
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
func = np.trapezoid if checks.check_version(np.__version__, ">=2.0") else np.trapz # np.trapz deprecated
ap = func(np.interp(x, mrec, mpre), x) # integrate
# 如果使用连续方法:
# 找出召回率变化的点。
# 计算这些点之间的面积,即 AP 值。
else: # 'continuous'
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x-axis (recall) changes
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
# 返回计算得到的 AP 值以及处理后的精确率和召回率数组。
return ap, mpre, mrec
# compute_ap 函数的作用是计算平均精确率(AP),这是评估分类或目标检测模型性能的一个重要指标。它通过以下步骤实现: 添加哨兵值:在召回率和精确率的开头和结尾添加特定值,确保曲线的完整性。 计算精确率上包络线:确保精确率在每个召回率点上都是非递增的。 积分方法选择:选择积分方法(插值或连续)。 计算 AP:根据选择的方法计算曲线下面积,即 AP 值。这个函数在评估分类或检测模型的性能时非常有用,能够直观地展示模型在不同召回率下的精确率表现。
# 这段代码定义了一个名为 ap_per_class 的函数,用于计算每个类别的平均精确率(AP),并绘制精确率-召回率(PR)曲线、F1 曲线、精确率曲线和召回率曲线。
# 定义了一个名为 ap_per_class 的函数,接受以下参数:
# 1.tp :一个 NumPy 数组,表示每个预测是否为真正例(True Positive)。
# 2.conf :一个 NumPy 数组,表示每个预测的置信度。
# 3.pred_cls :一个 NumPy 数组,表示每个预测的类别。
# 4.target_cls :一个 NumPy 数组,表示每个目标的类别。
# 5.plot :一个布尔值,表示是否绘制曲线,默认为 False 。
# 6.on_plot :一个可选的回调函数,用于在图像保存后执行某些操作。
# 7.save_dir :一个 Path 对象,表示保存图像的路径,默认为空路径。
# 8.names :一个字典,表示类别名称,默认为空字典。
# 9.eps :一个浮点数,用于避免除零,默认为 1e-16 。
# 10.prefix :一个字符串,用于图像文件名的前缀,默认为空字符串。
# 函数返回一个元组,包含多个计算结果。
def ap_per_class(
tp: np.ndarray,
conf: np.ndarray,
pred_cls: np.ndarray,
target_cls: np.ndarray,
plot: bool = False,
on_plot=None,
save_dir: Path = Path(),
names: dict = {},
eps: float = 1e-16,
prefix: str = "",
) -> Tuple:
# 计算每个类别的平均精度,用于评估目标检测。
# 参数:
# tp (np.ndarray):二进制数组,指示检测结果正确 (True) 或错误 (False)。
# conf (np.ndarray):检测结果的置信度分数数组。
# pred_cls (np.ndarray):检测结果的预测类别数组。
# target_cls (np.ndarray):检测结果的真实类别数组。
# plot (bool,可选):是否绘制 PR 曲线。
# on_plot (callable,可选):用于在渲染图表时传递路径和数据的回调函数。
# save_dir (Path,可选):保存 PR 曲线的目录。
# names (dict,可选):用于绘制 PR 曲线的类名字典。
# eps (float,可选):一个较小的值,用于避免被零除。
# prefix (str,可选):用于保存图表文件的前缀字符串。
# 返回:
# tp (np.ndarray):真阳性每个类别在由最大 F1 指标给定的阈值下的计数。
# fp (np.ndarray):每个类别在由最大 F1 指标给定的阈值下的假阳性计数。
# p (np.ndarray):每个类别在由最大 F1 指标给定的阈值下的精确度值。
# r (np.ndarray):每个类别在由最大 F1 指标给定的阈值下的召回率值。
# f1 (np.ndarray):每个类别在由最大 F1 指标给定的阈值下的 F1 分值。
# ap (np.ndarray):每个类别在不同 IoU 阈值下的平均精确度。
# unique_classes (np.ndarray):包含数据的唯一类别数组。
# p_curve (np.ndarray):每个类别的精确度曲线。
# r_curve (np.ndarray):每个类别的召回率曲线。
# f1_curve (np.ndarray):每个类别的 F1 分值曲线。
# x (np.ndarray):曲线的 X 轴值。
# prec_values (np.ndarray):每个类别在 [email protected] 时的精度值。
"""
Compute the average precision per class for object detection evaluation.
Args:
tp (np.ndarray): Binary array indicating whether the detection is correct (True) or not (False).
conf (np.ndarray): Array of confidence scores of the detections.
pred_cls (np.ndarray): Array of predicted classes of the detections.
target_cls (np.ndarray): Array of true classes of the detections.
plot (bool, optional): Whether to plot PR curves or not.
on_plot (callable, optional): A callback to pass plots path and data when they are rendered.
save_dir (Path, optional): Directory to save the PR curves.
names (dict, optional): Dict of class names to plot PR curves.
eps (float, optional): A small value to avoid division by zero.
prefix (str, optional): A prefix string for saving the plot files.
Returns:
tp (np.ndarray): True positive counts at threshold given by max F1 metric for each class.
fp (np.ndarray): False positive counts at threshold given by max F1 metric for each class.
p (np.ndarray): Precision values at threshold given by max F1 metric for each class.
r (np.ndarray): Recall values at threshold given by max F1 metric for each class.
f1 (np.ndarray): F1-score values at threshold given by max F1 metric for each class.
ap (np.ndarray): Average precision for each class at different IoU thresholds.
unique_classes (np.ndarray): An array of unique classes that have data.
p_curve (np.ndarray): Precision curves for each class.
r_curve (np.ndarray): Recall curves for each class.
f1_curve (np.ndarray): F1-score curves for each class.
x (np.ndarray): X-axis values for the curves.
prec_values (np.ndarray): Precision values at [email protected] for each class.
"""
# Sort by objectness
# 使用 np.argsort(-conf) 获取按置信度从高到低的索引 i 。这里对置信度取负值是为了让 np.argsort 按降序排列。
i = np.argsort(-conf)
# 使用索引 i 对 tp (真正例标志)、 conf (置信度)和 pred_cls (预测类别)进行重新排列,确保它们按置信度从高到低排序。
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
# Find unique classes
# 使用 np.unique(target_cls, return_counts=True) 找出目标类别中的唯一类别及其数量。 unique_classes 是唯一类别的数组, nt 是每个类别的数量。
unique_classes, nt = np.unique(target_cls, return_counts=True)
# nc 是唯一类别的数量,即目标类别中的不同类别总数。
nc = unique_classes.shape[0] # number of classes, number of detections
# Create Precision-Recall curve and compute AP for each class
# 使用 np.linspace(0, 1, 1000) 创建一个从 0 到 1 的 1000 个点的数组 x ,用于后续的插值操作。
# 初始化一个空列表 prec_values ,用于存储每个类别的精确率值。
x, prec_values = np.linspace(0, 1, 1000), []
# Average precision, precision and recall curves
# 初始化三个数组:
# ap :用于存储每个类别的 AP 值,形状为 (nc, tp.shape[1]) 。
# p_curve :用于存储每个类别的精确率曲线,形状为 (nc, 1000) 。
# r_curve :用于存储每个类别的召回率曲线,形状为 (nc, 1000) 。
ap, p_curve, r_curve = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))
# 遍历每个唯一类别 c , ci 是类别的索引。
for ci, c in enumerate(unique_classes):
# 找出预测类别为 c 的索引 i 。
i = pred_cls == c
# n_l 是该类别的标签数量。
n_l = nt[ci] # number of labels
# n_p 是该类别的预测数量。
n_p = i.sum() # number of predictions
# 如果该类别的预测数量或标签数量为 0,则跳过当前类别。
if n_p == 0 or n_l == 0:
continue
# Accumulate FPs and TPs
# 计算假正例(FP)和真正例(TP)的累积数量:
# fpc :假正例的累积数量。
# tpc :真正例的累积数量。
fpc = (1 - tp[i]).cumsum(0)
tpc = tp[i].cumsum(0)
# Recall
# 计算召回率曲线:
# recall :召回率的计算公式为 tpc / (n_l + eps) ,其中 eps 用于避免除零。
# 使用 np.interp 对召回率进行插值,插值点为 x ,插值的 x 值为 -conf[i] (因为置信度是降序排列的)。
recall = tpc / (n_l + eps) # recall curve
r_curve[ci] = np.interp(-x, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases
# Precision
# 计算精确率曲线:
# precision :精确率的计算公式为 tpc / (tpc + fpc) 。
# 使用 np.interp 对精确率进行插值,插值点为 x ,插值的 x 值为 -conf[i] 。
precision = tpc / (tpc + fpc) # precision curve
p_curve[ci] = np.interp(-x, -conf[i], precision[:, 0], left=1) # p at pr_score
# AP from recall-precision curve
# 计算每个类别的 AP:
# 遍历每个阈值 j 。
# 使用 compute_ap 函数计算 AP,返回值包括 AP 值、平滑后的精确率和召回率。
# 如果是第一个阈值(通常是 0.5),将精确率值添加到 prec_values 列表中。
for j in range(tp.shape[1]):
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])
if j == 0:
prec_values.append(np.interp(x, mrec, mpre)) # precision at [email protected]
# 将 prec_values 转换为 NumPy 数组,如果为空则初始化为零数组,形状为 (1, 1000) 。
prec_values = np.array(prec_values) if prec_values else np.zeros((1, 1000)) # (nc, 1000)
# Compute F1 (harmonic mean of precision and recall)
# 计算 F1 曲线,F1 是精确率和召回率的调和平均值。
f1_curve = 2 * p_curve * r_curve / (p_curve + r_curve + eps)
# 过滤出有数据的类别名称,确保只包含在 unique_classes 中的类别。
names = [v for k, v in names.items() if k in unique_classes] # list: only classes that have data
# 将类别名称列表转换为字典,以便在绘图时使用。
names = dict(enumerate(names)) # to dict
# 检查是否需要绘制曲线。如果 plot 参数为 True ,则执行以下绘图操作。
if plot:
# 调用 plot_pr_curve 函数绘制精确率-召回率(PR)曲线,并保存为图像文件。文件名由 prefix 和 "PR_curve.png" 组成,保存路径为 save_dir 。
plot_pr_curve(x, prec_values, ap, save_dir / f"{prefix}PR_curve.png", names, on_plot=on_plot)
# 调用 plot_mc_curve 函数绘制 F1 曲线,并保存为图像文件。文件名由 prefix 和 "F1_curve.png" 组成,保存路径为 save_dir 。
plot_mc_curve(x, f1_curve, save_dir / f"{prefix}F1_curve.png", names, ylabel="F1", on_plot=on_plot)
# 调用 plot_mc_curve 函数绘制精确率曲线,并保存为图像文件。文件名由 prefix 和 "P_curve.png" 组成,保存路径为 save_dir 。
plot_mc_curve(x, p_curve, save_dir / f"{prefix}P_curve.png", names, ylabel="Precision", on_plot=on_plot)
# 调用 plot_mc_curve 函数绘制召回率曲线,并保存为图像文件。文件名由 prefix 和 "R_curve.png" 组成,保存路径为 save_dir 。
plot_mc_curve(x, r_curve, save_dir / f"{prefix}R_curve.png", names, ylabel="Recall", on_plot=on_plot)
# 使用 smooth 函数对 F1 曲线的均值进行平滑处理,平滑程度为 0.1。 使用 argmax 找到平滑后的 F1 曲线最大值对应的索引 i 。
i = smooth(f1_curve.mean(0), 0.1).argmax() # max F1 index
# 使用索引 i 从精确率曲线 p_curve 、召回率曲线 r_curve 和 F1 曲线 f1_curve 中提取最大 F1 值对应的精确率、召回率和 F1 值。
p, r, f1 = p_curve[:, i], r_curve[:, i], f1_curve[:, i] # max-F1 precision, recall, F1 values
# 计算真正例(TP)的数量。通过将召回率 r 与每个类别的标签数量 nt 相乘,然后四舍五入得到 TP 的数量。
tp = (r * nt).round() # true positives
# 计算假正例(FP)的数量。通过将 TP 除以精确率 p (加上一个小值 eps 以避免除零),然后减去 TP,最后四舍五入得到 FP 的数量。
fp = (tp / (p + eps) - tp).round() # false positives
# 返回多个计算结果:
# tp :真正例的数量。
# fp :假正例的数量。
# p :最大 F1 值对应的精确率。
# r :最大 F1 值对应的召回率。
# f1 :最大 F1 值。
# ap :每个类别的平均精确率(AP)。
# unique_classes.astype(int) :唯一类别的整数数组。
# p_curve :每个类别的精确率曲线。
# r_curve :每个类别的召回率曲线。
# f1_curve :每个类别的 F1 曲线。
# x :插值点数组。
# prec_values :精确率值数组。
return tp, fp, p, r, f1, ap, unique_classes.astype(int), p_curve, r_curve, f1_curve, x, prec_values
# ap_per_class 函数用于计算每个类别的平均精确率(AP),并生成精确率-召回率(PR)曲线、F1 曲线、精确率曲线和召回率曲线。它首先按置信度对预测结果进行排序,然后找出目标类别中的唯一类别及其数量。接着,它计算每个类别的假正例(FP)和真正例(TP)的累积数量,进而计算召回率和精确率曲线。通过这些曲线,函数计算每个类别的 AP,并找到 F1 曲线的最大值对应的精确率、召回率和 F1 值。此外,它还计算真正例和假正例的数量,并返回多个计算结果,包括 TP、FP、精确率、召回率、F1 值、AP、唯一类别、精确率曲线、召回率曲线、F1 曲线、插值点和精确率值。如果需要,它还会绘制并保存这些曲线为图像文件。
# 这段代码定义了一个名为 Metric 的类,该类继承自 SimpleClass ,用于计算 Ultralytics YOLO 模型的评估指标。
# 定义了一个名为 Metric 的类,继承自 SimpleClass 。这意味着 Metric 类继承了 SimpleClass 的所有方法和属性,包括 __str__ 、 __repr__ 和 __getattr__ 方法。
class Metric(SimpleClass):
# 用于计算 Ultralytics YOLO 模型评估指标的类。
# 属性:
# p(列表):每个类别的准确率。形状:(nc,)。
# r(列表):每个类别的召回率。形状:(nc,)。
# f1(列表):每个类别的 F1 分数。形状:(nc,)。
# all_ap(列表):所有类别和所有 IoU 阈值的 AP 分数。形状:(nc, 10)。
# ap_class_index(列表):每个 AP 分数对应的类别索引。形状:(nc,)。
# nc(整数):类别数量。
# 方法:
# ap50():所有类别在 IoU 阈值为 0.5 时的 AP。返回:AP 分数列表。形状:(nc,) 或 []。
# ap():所有类别在 IoU 阈值为 0.5 至 0.95 时的 AP。返回:AP 分数列表。形状:(nc,) 或 []。
# mp():所有类别的平均准确率。返回:浮点数。
# mr():所有类别的平均召回率。返回:浮点数。
# map50():所有类别在 IoU 阈值为 0.5 时的平均 AP。返回:浮点数。
# map75():所有类别在 IoU 阈值为 0.75 时的平均 AP。返回:浮点数。
# map():所有类别在 IoU 阈值从 0.5 到 0.95 时的平均 AP。返回:浮点数。
# mean_results():结果平均值,返回 mp、mr、map50 和 map。
# class_result(i):类别感知结果,返回 p[i]、r[i]、ap50[i] 和 ap[i]。
# maps():每个类别的 mAP。返回:mAP 分数数组,形状:(nc,)。
# fitness():将适应度建模为指标的加权组合。返回:浮点数。
# update(results):使用新的评估结果更新指标属性。
"""
Class for computing evaluation metrics for Ultralytics YOLO models.
Attributes:
p (list): Precision for each class. Shape: (nc,).
r (list): Recall for each class. Shape: (nc,).
f1 (list): F1 score for each class. Shape: (nc,).
all_ap (list): AP scores for all classes and all IoU thresholds. Shape: (nc, 10).
ap_class_index (list): Index of class for each AP score. Shape: (nc,).
nc (int): Number of classes.
Methods:
ap50(): AP at IoU threshold of 0.5 for all classes. Returns: List of AP scores. Shape: (nc,) or [].
ap(): AP at IoU thresholds from 0.5 to 0.95 for all classes. Returns: List of AP scores. Shape: (nc,) or [].
mp(): Mean precision of all classes. Returns: Float.
mr(): Mean recall of all classes. Returns: Float.
map50(): Mean AP at IoU threshold of 0.5 for all classes. Returns: Float.
map75(): Mean AP at IoU threshold of 0.75 for all classes. Returns: Float.
map(): Mean AP at IoU thresholds from 0.5 to 0.95 for all classes. Returns: Float.
mean_results(): Mean of results, returns mp, mr, map50, map.
class_result(i): Class-aware result, returns p[i], r[i], ap50[i], ap[i].
maps(): mAP of each class. Returns: Array of mAP scores, shape: (nc,).
fitness(): Model fitness as a weighted combination of metrics. Returns: Float.
update(results): Update metric attributes with new evaluation results.
"""
# 这段代码是 Metric 类的构造函数 __init__ 的定义,用于初始化类的实例。
# 定义了 Metric 类的构造函数 __init__ 。构造函数是类的特殊方法,用于在创建类的实例时初始化对象的属性。 -> None 表示该方法没有返回值。
def __init__(self) -> None:
# 初始化一个 Metric 实例,用于计算 YOLOv8 模型的评估指标。
"""Initialize a Metric instance for computing evaluation metrics for the YOLOv8 model."""
# 初始化 self.p 属性,这是一个空列表,用于存储每个类别的精确度(Precision)。注释 # (nc, ) 表示这个列表的形状应该是 (nc,) ,其中 nc 是类别数量。
self.p = [] # (nc, )
# 初始化 self.r 属性,这是一个空列表,用于存储每个类别的召回率(Recall)。注释 # (nc, ) 表示这个列表的形状应该是 (nc,) 。
self.r = [] # (nc, )
# 初始化 self.f1 属性,这是一个空列表,用于存储每个类别的 F1 分数。注释 # (nc, ) 表示这个列表的形状应该是 (nc,) 。
self.f1 = [] # (nc, )
# 初始化 self.all_ap 属性,这是一个空列表,用于存储所有类别在所有 IoU 阈值下的平均精度(AP)。注释 # (nc, 10) 表示这个列表的形状应该是 (nc, 10) ,其中 10 表示有 10 个不同的 IoU 阈值(通常是从 0.5 到 0.95,步长为 0.05)。
self.all_ap = [] # (nc, 10)
# 初始化 self.ap_class_index 属性,这是一个空列表,用于存储每个 AP 分数对应的类别索引。注释 # (nc, ) 表示这个列表的形状应该是 (nc,) 。
self.ap_class_index = [] # (nc, )
# 初始化 self.nc 属性,这是一个整数,表示类别数量。初始值为 0,表示在初始化时没有类别。
self.nc = 0
# 这段代码的作用是初始化 Metric 类的实例,设置了一些用于存储评估指标的属性。这些属性在后续的评估过程中会被更新和使用。具体来说: self.p :存储每个类别的精确度(Precision)。 self.r :存储每个类别的召回率(Recall)。 self.f1 :存储每个类别的 F1 分数。 self.all_ap :存储所有类别在所有 IoU 阈值下的平均精度(AP)。 self.ap_class_index :存储每个 AP 分数对应的类别索引。 self.nc :存储类别数量。这些属性在类的其他方法中会被使用,例如计算平均精确度(AP)、平均召回率(AR)、平均 F1 分数等。
# 定义了一个名为 ap50 的属性,使用 @property 装饰器。这个属性返回在 IoU 阈值为 0.5 时,所有类别的平均精度(AP)。
@property
def ap50(self) -> Union[np.ndarray, List]:
# 返回所有类别在 IoU 阈值为 0.5 时的平均精度 (AP)。
# 返回:
# (np.ndarray | list):形状为 (nc,) 的数组,每个类别包含 AP50 值,若无则返回空列表。
"""
Return the Average Precision (AP) at an IoU threshold of 0.5 for all classes.
Returns:
(np.ndarray | list): Array of shape (nc,) with AP50 values per class, or an empty list if not available.
"""
# 如果 self.all_ap 不为空,则返回 self.all_ap 的第一列(IoU 阈值为 0.5 的 AP 分数)。如果 self.all_ap 为空,则返回一个空列表。返回值类型可以是 np.ndarray 或 List 。
return self.all_ap[:, 0] if len(self.all_ap) else []
# 定义了一个名为 ap 的属性,使用 @property 装饰器。这个属性返回在 IoU 阈值从 0.5 到 0.95 时,所有类别的平均精度(AP)。
@property
def ap(self) -> Union[np.ndarray, List]:
# 返回所有类别在 IoU 阈值为 0.5-0.95 时的平均精度 (AP)。
# 返回:
# (np.ndarray | list):形状为 (nc,) 的数组,每个类别的 AP 值在 50-95 之间,如果不可用,则返回空列表。
"""
Return the Average Precision (AP) at an IoU threshold of 0.5-0.95 for all classes.
Returns:
(np.ndarray | list): Array of shape (nc,) with AP50-95 values per class, or an empty list if not available.
"""
# 如果 self.all_ap 不为空,则返回 self.all_ap 每行的平均值(即每个类别的 AP 分数在 IoU 阈值 0.5-0.95 的平均值)。如果 self.all_ap 为空,则返回一个空列表。返回值类型可以是 np.ndarray 或 List 。
return self.all_ap.mean(1) if len(self.all_ap) else []
# 定义了一个名为 mp 的属性,使用 @property 装饰器。这个属性返回所有类别的平均精确度(Mean Precision)。
@property
def mp(self) -> float:
# 返回所有类别的平均精度。
"""
Return the Mean Precision of all classes.
Returns:
(float): The mean precision of all classes.
"""
# 如果 self.p 不为空,则返回 self.p 的平均值(即所有类别的平均精确度)。如果 self.p 为空,则返回 0.0。
return self.p.mean() if len(self.p) else 0.0
# 定义了一个名为 mr 的属性,使用 @property 装饰器。这个属性返回所有类别的平均召回率(Mean Recall)。
@property
def mr(self) -> float:
# 返回所有类别的平均召回率。
"""
Return the Mean Recall of all classes.
Returns:
(float): The mean recall of all classes.
"""
# 如果 self.r 不为空,则返回 self.r 的平均值(即所有类别的平均召回率)。如果 self.r 为空,则返回 0.0。
return self.r.mean() if len(self.r) else 0.0
# 定义了一个名为 map50 的属性,使用 @property 装饰器。这个属性返回在 IoU 阈值为 0.5 时,所有类别的平均精度(mAP)。
@property
def map50(self) -> float:
# 返回 IoU 阈值为 0.5 时的平均精度 (mAP)。
"""
Return the mean Average Precision (mAP) at an IoU threshold of 0.5.
Returns:
(float): The mAP at an IoU threshold of 0.5.
"""
# 如果 self.all_ap 不为空,则返回 self.all_ap 第一列的平均值(即 IoU 阈值为 0.5 的平均 AP 分数)。如果 self.all_ap 为空,则返回 0.0。
return self.all_ap[:, 0].mean() if len(self.all_ap) else 0.0
# 定义了一个名为 map75 的属性,使用 @property 装饰器。这个属性返回在 IoU 阈值为 0.75 时,所有类别的平均精度(mAP)。
@property
def map75(self) -> float:
# 返回 IoU 阈值为 0.75 时的平均精度 (mAP)。
"""
Return the mean Average Precision (mAP) at an IoU threshold of 0.75.
Returns:
(float): The mAP at an IoU threshold of 0.75.
"""
# 如果 self.all_ap 不为空,则返回 self.all_ap 第六列的平均值(即 IoU 阈值为 0.75 的平均 AP 分数)。如果 self.all_ap 为空,则返回 0.0。
return self.all_ap[:, 5].mean() if len(self.all_ap) else 0.0
# 定义了一个名为 map 的属性,使用 @property 装饰器。这个属性返回在 IoU 阈值从 0.5 到 0.95 时,所有类别的平均精度(mAP)。
@property
def map(self) -> float:
# 返回 IoU 阈值为 0.5 - 0.95 时,步长为 0.05 的平均精度 (mAP)。
"""
Return the mean Average Precision (mAP) over IoU thresholds of 0.5 - 0.95 in steps of 0.05.
Returns:
(float): The mAP over IoU thresholds of 0.5 - 0.95 in steps of 0.05.
"""
# 如果 self.all_ap 不为空,则返回 self.all_ap 的整体平均值(即所有类别在所有 IoU 阈值下的平均 AP 分数)。如果 self.all_ap 为空,则返回 0.0。
return self.all_ap.mean() if len(self.all_ap) else 0.0
# 定义了一个名为 mean_results 的方法,该方法返回一个包含平均精确度(MP)、平均召回率(MR)、在 IoU 阈值为 0.5 时的平均精度(mAP50)和在 IoU 阈值从 0.5 到 0.95 时的平均精度(mAP)的列表。
def mean_results(self) -> List[float]:
# 返回结果的平均值、mp、mr、map50、map。
"""Return mean of results, mp, mr, map50, map."""
# 返回一个列表,包含以下四个指标:
# self.mp :所有类别的平均精确度(Mean Precision)。
# self.mr :所有类别的平均召回率(Mean Recall)。
# self.map50 :在 IoU 阈值为 0.5 时,所有类别的平均精度(mAP50)。
# self.map :在 IoU 阈值从 0.5 到 0.95 时,所有类别的平均精度(mAP)。
return [self.mp, self.mr, self.map50, self.map]
# 定义了一个名为 class_result 的方法,该方法接受一个整数参数 1.i ,表示类别的索引,并返回一个包含特定类别 i 的精确度(Precision)、召回率(Recall)、在 IoU 阈值为 0.5 时的平均精度(AP50)和在 IoU 阈值从 0.5 到 0.95 时的平均精度(AP)的元组。
def class_result(self, i: int) -> Tuple[float, float, float, float]:
# 返回类感知结果,p[i],r[i],ap50[i],ap[i]。
"""Return class-aware result, p[i], r[i], ap50[i], ap[i]."""
# 返回一个元组,包含以下四个指标:
# self.p[i] :类别 i 的精确度(Precision)。
# self.r[i] :类别 i 的召回率(Recall)。
# self.ap50[i] :类别 i 在 IoU 阈值为 0.5 时的平均精度(AP50)。
# self.ap[i] :类别 i 在 IoU 阈值从 0.5 到 0.95 时的平均精度(AP)。
return self.p[i], self.r[i], self.ap50[i], self.ap[i]
# 定义了一个名为 maps 的属性,使用 @property 装饰器。这个属性返回一个 NumPy 数组,包含每个类别的平均精度(mAP)。
@property
def maps(self) -> np.ndarray:
# 返回每个类的 mAP。
"""Return mAP of each class."""
# 初始化一个 NumPy 数组 maps ,其长度为 self.nc (类别数量),并用 self.map (所有类别的平均精度)填充整个数组。这一步确保了每个类别都有一个默认的 mAP 值,即所有类别的平均值。
maps = np.zeros(self.nc) + self.map
# 遍历 self.ap_class_index 列表,该列表包含每个 AP 分数对应的类别索引。对于每个索引 c 和其对应的 AP 分数 self.ap[i] ,将 maps 数组中索引 c 的值更新为 self.ap[i] 。这一步将每个类别的具体 AP 分数赋值到 maps 数组中。
for i, c in enumerate(self.ap_class_index):
maps[c] = self.ap[i]
# 返回 maps 数组,该数组现在包含每个类别的平均精度(mAP)。
return maps
# 定义了一个名为 fitness 的方法,该方法返回一个浮点数,表示模型的适应度。
def fitness(self) -> float:
# 将模型适应度作为指标的加权组合返回。
"""Return model fitness as a weighted combination of metrics."""
# 定义了一个权重列表 w ,用于加权平均四个评估指标:精确度(P)、召回率(R)、在 IoU 阈值为 0.5 时的平均精度(mAP50)和在 IoU 阈值从 0.5 到 0.95 时的平均精度(mAP)。权重分别为 0.0、0.0、0.1 和 0.9,表示 mAP50 和 mAP 对适应度的贡献更大。
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, [email protected], [email protected]:0.95]
# 计算适应度的步骤如下:
# 调用 self.mean_results() 方法,返回一个包含四个评估指标(P、R、mAP50、mAP)的列表。
# 将这个列表转换为 NumPy 数组。
# 使用 np.nan_to_num 函数将数组中的任何 NaN 值转换为 0。
# 将权重列表 w 与这个数组相乘,得到加权后的指标。
# 使用 sum 方法计算加权指标的总和,即模型的适应度。
return (np.nan_to_num(np.array(self.mean_results())) * w).sum()
# 定义了一个名为 update 的方法,该方法接受一个元组 1.results 作为参数,用于更新类的评估指标。
def update(self, results: tuple):
# 使用一组新的结果更新评估指标。
"""
Update the evaluation metrics with a new set of results.
Args:
results (tuple): A tuple containing evaluation metrics:
- p (list): Precision for each class.
- r (list): Recall for each class.
- f1 (list): F1 score for each class.
- all_ap (list): AP scores for all classes and all IoU thresholds.
- ap_class_index (list): Index of class for each AP score.
- p_curve (list): Precision curve for each class.
- r_curve (list): Recall curve for each class.
- f1_curve (list): F1 curve for each class.
- px (list): X values for the curves.
- prec_values (list): Precision values for each class.
"""
# 将 results 元组中的值解包并分别赋值给类的属性:
# self.p :每个类别的精确度(Precision)。
# self.r :每个类别的召回率(Recall)。
# self.f1 :每个类别的 F1 分数。
# self.all_ap :所有类别在所有 IoU 阈值下的平均精度(AP)。
# self.ap_class_index :每个 AP 分数对应的类别索引。
# self.p_curve :精确度曲线。
# self.r_curve :召回率曲线。
# self.f1_curve :F1 分数曲线。
# self.px :曲线的 x 值(通常是置信度)。
# self.prec_values :精确度值。
# 这个方法用于更新类的内部状态,以便后续可以使用这些新的评估指标。
(
self.p,
self.r,
self.f1,
self.all_ap,
self.ap_class_index,
self.p_curve,
self.r_curve,
self.f1_curve,
self.px,
self.prec_values,
) = results
# 定义了一个名为 curves 的属性,使用 @property 装饰器。这个属性返回一个空列表。
@property
def curves(self) -> List:
# 返回用于访问特定指标曲线的曲线列表。
"""Return a list of curves for accessing specific metrics curves."""
# 返回一个空列表。这个属性可能是一个占位符,用于后续扩展功能,例如返回特定的曲线数据。
return []
# 定义了一个名为 curves_results 的属性,使用 @property 装饰器。这个属性返回一个包含多个子列表的列表,每个子列表包含曲线的数据和标签。
@property
def curves_results(self) -> List[List]:
# 返回用于访问特定指标曲线的曲线列表。
"""Return a list of curves for accessing specific metrics curves."""
# 返回一个包含四个子列表的列表,每个子列表包含:
# self.px :曲线的 x 值(通常是置信度)。
# 曲线的 y 值(例如精确度值、F1 分数曲线、精确度曲线、召回率曲线)。
# x 轴标签(例如 "Recall"、"Confidence")。
# y 轴标签(例如 "Precision"、"F1")。
# 这些子列表可以用于绘制不同类型的评估曲线,例如精确度-召回率曲线、F1 分数曲线等。
return [
[self.px, self.prec_values, "Recall", "Precision"],
[self.px, self.f1_curve, "Confidence", "F1"],
[self.px, self.p_curve, "Confidence", "Precision"],
[self.px, self.r_curve, "Confidence", "Recall"],
]
# Metric 类是一个用于计算和管理 Ultralytics YOLO 模型评估指标的工具类。它继承自 SimpleClass ,具备良好的字符串表示和属性访问控制功能。该类通过一系列属性和方法,提供了对模型性能的全面评估,包括精确度(Precision)、召回率(Recall)、F1 分数、平均精度(AP)以及它们的平均值(如 mAP50 和 mAP)。此外, Metric 类还支持更新评估指标、计算模型适应度以及提供用于绘制评估曲线的数据。这些功能使得 Metric 类成为评估和优化 YOLO 模型性能的有力工具。
# 这段代码定义了一个名为 DetMetrics 的类,它继承自 SimpleClass 和 DataExportMixin ,用于计算目标检测任务中的评估指标,如精确度(Precision)、召回率(Recall)和平均精度(mAP)。
# 定义了一个名为 DetMetrics 的类,继承自 SimpleClass 和 DataExportMixin 。 SimpleClass 提供了基本的字符串表示和属性访问控制功能,而 DataExportMixin 提供了将评估指标导出为多种格式的功能。
class DetMetrics(SimpleClass, DataExportMixin):
# 用于计算检测指标(例如精确度、召回率和平均精确度 (mAP))的实用程序类。
"""
Utility class for computing detection metrics such as precision, recall, and mean average precision (mAP).
Attributes:
save_dir (Path): A path to the directory where the output plots will be saved.
plot (bool): A flag that indicates whether to plot precision-recall curves for each class.
names (dict): A dictionary of class names.
box (Metric): An instance of the Metric class for storing detection results.
speed (dict): A dictionary for storing execution times of different parts of the detection process.
task (str): The task type, set to 'detect'.
"""
# 这段代码定义了 DetMetrics 类的构造函数 __init__ ,用于初始化类的实例。
# 定义了 DetMetrics 类的构造函数 __init__ ,该方法接受以下参数:
# 1.save_dir :一个 Path 对象,表示保存输出图表的目录路径,默认为当前目录( Path(".") )。
# 2.plot :一个布尔值,表示是否绘制每个类别的精确度-召回率曲线,默认为 False 。
# 3.names :一个字典,映射类别索引到类别名称,默认为空字典。
# 构造函数的返回类型为 None ,表示该方法没有返回值。
def __init__(self, save_dir: Path = Path("."), plot: bool = False, names: dict = {}) -> None:
# 使用保存目录、绘图标志和类名初始化 DetMetrics 实例。
"""
Initialize a DetMetrics instance with a save directory, plot flag, and class names.
Args:
save_dir (Path, optional): Directory to save plots.
plot (bool, optional): Whether to plot precision-recall curves.
names (dict, optional): Dictionary mapping class indices to names.
"""
# 将传入的 save_dir 参数值赋给实例的 save_dir 属性,用于存储输出图表的目录路径。
self.save_dir = save_dir
# 将传入的 plot 参数值赋给实例的 plot 属性,用于控制是否绘制精确度-召回率曲线。
self.plot = plot
# 将传入的 names 参数值赋给实例的 names 属性,该字典用于存储类别索引与类别名称的映射关系。
self.names = names
# 创建一个 Metric 类的实例,并将其赋给实例的 box 属性。 Metric 类用于存储检测结果并计算评估指标,如精确度、召回率和平均精度(mAP)。
self.box = Metric()
# 初始化一个字典 speed ,用于存储检测过程中不同阶段的执行时间。字典的键包括:
# "preprocess" :预处理阶段的执行时间。
# "inference" :推理阶段的执行时间。
# "loss" :损失计算阶段的执行时间。
# "postprocess" :后处理阶段的执行时间。
# 每个键对应的值初始化为 0.0 ,表示初始时这些阶段的执行时间为零。
self.speed = {"preprocess": 0.0, "inference": 0.0, "loss": 0.0, "postprocess": 0.0}
# 将实例的 task 属性设置为字符串 "detect" ,表示当前任务类型为目标检测。
self.task = "detect"
# 这段代码初始化了 DetMetrics 类的实例,设置了以下属性: save_dir :保存输出图表的目录路径。 plot :控制是否绘制精确度-召回率曲线的布尔值。 names :类别索引与类别名称的映射字典。 box :一个 Metric 类的实例,用于存储检测结果并计算评估指标。 speed :一个字典,用于存储检测过程中不同阶段的执行时间。 task :任务类型,设置为 "detect" ,表示目标检测任务。通过这些初始化操作, DetMetrics 类的实例能够为后续的目标检测任务做好准备,包括存储检测结果、计算评估指标以及记录执行时间等。
# 这段代码定义了 DetMetrics 类中的 process 方法,用于处理目标检测的预测结果并更新评估指标。
# 定义了一个名为 process 的方法,该方法接受以下参数:
# 1.tp :一个 NumPy 数组,表示真正例(True Positives)。
# 2.conf :一个 NumPy 数组,表示预测的置信度。
# 3.pred_cls :一个 NumPy 数组,表示预测的类别索引。
# 4.target_cls :一个 NumPy 数组,表示目标的类别索引。
# 5.on_plot :一个可选的回调函数,在生成图表后调用。
def process(self, tp: np.ndarray, conf: np.ndarray, pred_cls: np.ndarray, target_cls: np.ndarray, on_plot=None):
# 处理目标检测的预测结果并更新指标。
"""
Process predicted results for object detection and update metrics.
Args:
tp (np.ndarray): True positive array.
conf (np.ndarray): Confidence array.
pred_cls (np.ndarray): Predicted class indices array.
target_cls (np.ndarray): Target class indices array.
on_plot (callable, optional): Function to call after plots are generated.
"""
# 调用 ap_per_class 函数,计算每个类别的平均精度(AP)和其他相关指标。 ap_per_class 函数的参数包括:
# tp :真正例数组。
# conf :置信度数组。
# pred_cls :预测的类别索引数组。
# target_cls :目标的类别索引数组。
# plot :一个布尔值,指示是否绘制精确度-召回率曲线。
# save_dir :保存图表的目录路径。
# names :类别名称的字典。
# on_plot :一个可选的回调函数,在生成图表后调用。
# ap_per_class 函数返回多个值,这里取从第三个值开始的所有结果,并将它们赋给 results 。
results = ap_per_class(
tp,
conf,
pred_cls,
target_cls,
plot=self.plot,
save_dir=self.save_dir,
names=self.names,
on_plot=on_plot,
)[2:]
# 将 Metric 实例的 nc 属性设置为类别数量,即 names 字典的长度。 nc 表示类别数量,用于后续的评估指标计算。
self.box.nc = len(self.names)
# 调用 Metric 实例的 update 方法,用 results 更新评估指标。 results 包含了从 ap_per_class 函数返回的评估指标,如精确度、召回率、F1 分数、AP 等。
self.box.update(results)
# 这段代码定义了 DetMetrics 类中的 process 方法,用于处理目标检测的预测结果并更新评估指标。具体步骤如下: 调用 ap_per_class 函数: 计算每个类别的平均精度(AP)和其他相关指标。 返回的值包括精确度、召回率、F1 分数、AP 等。 设置类别数量: 将 Metric 实例的 nc 属性设置为类别数量,即 names 字典的长度。 更新评估指标: 调用 Metric 实例的 update 方法,用 results 更新评估指标。这个方法使得 DetMetrics 类的实例能够方便地处理预测结果,并将评估指标存储在 Metric 实例中,便于后续的分析和导出。
# 定义了一个名为 keys 的属性,使用 @property 装饰器。这个属性返回一个包含特定评估指标键的列表。
@property
def keys(self) -> List[str]:
# 返回用于访问特定指标的键列表。
"""Return a list of keys for accessing specific metrics."""
# 返回一个包含四个评估指标键的列表,分别表示:
# metrics/precision(B) :精确度(Precision)。
# metrics/recall(B) :召回率(Recall)。
# metrics/mAP50(B) :在 IoU 阈值为 0.5 时的平均精度(mAP50)。
# metrics/mAP50-95(B) :在 IoU 阈值从 0.5 到 0.95 时的平均精度(mAP50-95)。
return ["metrics/precision(B)", "metrics/recall(B)", "metrics/mAP50(B)", "metrics/mAP50-95(B)"]
# 定义了一个名为 mean_results 的方法,用于计算检测到的对象的平均评估指标。
def mean_results(self) -> List[float]:
# 计算检测到的物体的平均值并返回精度、召回率、mAP50 和 mAP50-95。
"""Calculate mean of detected objects & return precision, recall, mAP50, and mAP50-95."""
# 调用 Metric 实例的 mean_results 方法,返回精确度、召回率、mAP50 和 mAP50-95 的平均值。
return self.box.mean_results()
# 定义了一个名为 class_result 的方法,用于返回特定类别 i 的评估指标。
def class_result(self, i: int) -> Tuple[float, float, float, float]:
# 返回评估对象检测模型在特定类别上的性能的结果。
"""Return the result of evaluating the performance of an object detection model on a specific class."""
# 调用 Metric 实例的 class_result 方法,返回类别 i 的精确度、召回率、在 IoU 阈值为 0.5 时的平均精度(AP50)和在 IoU 阈值从 0.5 到 0.95 时的平均精度(AP)。
return self.box.class_result(i)
# 定义了一个名为 maps 的属性,使用 @property 装饰器。这个属性返回每个类别的平均精度(mAP)。
@property
def maps(self) -> np.ndarray:
# 返回每个类别的平均精度(mAP)分数。
"""Return mean Average Precision (mAP) scores per class."""
# 返回 Metric 实例的 maps 属性,即每个类别的平均精度(mAP)。
return self.box.maps
# 定义了一个名为 fitness 的属性,使用 @property 装饰器。这个属性返回模型的适应度。
@property
def fitness(self) -> float:
# 返回盒子对象的适应度。
"""Return the fitness of box object."""
# 返回 Metric 实例的 fitness 方法的结果,即模型的适应度。
return self.box.fitness()
# 定义了一个名为 ap_class_index 的属性,使用 @property 装饰器。这个属性返回每个平均精度(AP)分数对应的类别索引。
@property
def ap_class_index(self) -> List:
# 返回每个类别的平均精度指数。
"""Return the average precision index per class."""
# 返回 Metric 实例的 ap_class_index 属性,即每个 AP 分数对应的类别索引。
return self.box.ap_class_index
# 定义了一个名为 results_dict 的属性,使用 @property 装饰器。这个属性返回一个包含评估指标和统计信息的字典。
@property
def results_dict(self) -> Dict[str, float]:
# 返回计算出的性能指标和统计数据的字典。
"""Return dictionary of computed performance metrics and statistics."""
# 将 keys 属性返回的评估指标键和 mean_results 方法返回的平均评估指标值,以及 "fitness" 键和 fitness 属性返回的适应度值,组合成一个字典并返回。
return dict(zip(self.keys + ["fitness"], self.mean_results() + [self.fitness]))
# 定义了一个名为 curves 的属性,使用 @property 装饰器。这个属性返回一个包含特定评估曲线名称的列表。
@property
def curves(self) -> List[str]:
# 返回用于访问特定指标曲线的曲线列表。
"""Return a list of curves for accessing specific metrics curves."""
# 返回一个包含四个评估曲线名称的列表,分别表示:
# Precision-Recall(B) :精确度-召回率曲线。
# F1-Confidence(B) :F1 分数-置信度曲线。
# Precision-Confidence(B) :精确度-置信度曲线。
# Recall-Confidence(B) :召回率-置信度曲线。
return ["Precision-Recall(B)", "F1-Confidence(B)", "Precision-Confidence(B)", "Recall-Confidence(B)"]
# 定义了一个名为 curves_results 的属性,使用 @property 装饰器。这个属性返回一个包含评估曲线数据的列表。
@property
def curves_results(self) -> List[List]:
# 返回计算出的性能指标和统计数据的字典。
"""Return dictionary of computed performance metrics and statistics."""
# 返回 Metric 实例的 curves_results 属性,即评估曲线的数据。
return self.box.curves_results
# 这段代码定义了 DetMetrics 类中的 summary 方法,用于生成一个包含每个类别评估指标的列表,每个类别对应一个字典。
# 定义了一个名为 summary 的方法,该方法接受任意数量的关键字参数 1.**kwargs ,并返回一个列表,其中每个元素是一个字典,字典的键为字符串,值为字符串或浮点数。
def summary(self, **kwargs) -> List[Dict[str, Union[str, float]]]:
# 返回包含共享标量值的每个类检测指标。
"""Return per-class detection metrics with shared scalar values included."""
# 创建一个字典 scalars ,存储整体的评估指标:
# "box-map" :所有类别的平均精度(mAP)。
# "box-map50" :在 IoU 阈值为 0.5 时的平均精度(mAP50)。
# "box-map75" :在 IoU 阈值为 0.75 时的平均精度(mAP75)。
# 这些指标是从 self.box (一个 Metric 类的实例)中获取的。
scalars = {
"box-map": self.box.map,
"box-map50": self.box.map50,
"box-map75": self.box.map75,
}
# 创建一个字典 per_class ,存储每个类别的评估指标:
# "box-p" :每个类别的精确度(Precision)。
# "box-r" :每个类别的召回率(Recall)。
# "box-f1" :每个类别的 F1 分数。
# 这些指标也是从 self.box 中获取的。
per_class = {
"box-p": self.box.p,
"box-r": self.box.r,
"box-f1": self.box.f1,
}
# 返回一个列表,列表中的每个元素是一个字典,表示一个类别的评估指标。
return [
{
# 为每个类别生成一个字典,字典的第一个键是 "class_name" ,其值为: 如果 self.names 属性存在且类别索引 i 在 self.names 字典中,则使用 self.names[i] 作为类别名称。 否则,使用类别索引 i 的字符串表示作为类别名称。
"class_name": self.names[i] if hasattr(self, "names") and i in self.names else str(i),
# 使用字典推导式 {k: v[i] for k, v in per_class.items()} ,将每个类别的评估指标(精确度、召回率、F1 分数)添加到字典中。 per_class 是一个字典,其键为指标名称(如 "box-p" 、 "box-r" 、 "box-f1" ),值为包含每个类别指标值的列表。 v[i] 表示第 i 个类别的指标值。
**{k: v[i] for k, v in per_class.items()},
# 将整体的评估指标(mAP、mAP50、mAP75)添加到字典中。 scalars 是一个字典,包含整体的评估指标。
**scalars,
# 结束每个类别的字典定义。
}
# 使用列表推导式,遍历所有类别索引 i 。 len(next(iter(per_class.values()), [])) 获取 per_class 中任意一个指标列表的长度,即类别数量。
# per_class.values() :
# per_class 是一个字典,其键是评估指标的名称(如 "box-p" 、 "box-r" 、 "box-f1" ),值是包含每个类别评估指标值的列表。
# per_class.values() 返回一个视图对象,包含字典中所有值的列表。
# iter(per_class.values()) :
# iter() 函数将 per_class.values() 转换为一个迭代器,用于逐个访问字典中的值。
# next(iter(per_class.values()), []) :
# next() 函数从迭代器中获取第一个值。如果迭代器为空(即 per_class 字典为空),则返回默认值 [] 。
# 这里的作用是获取 per_class 字典中的第一个值(即第一个评估指标的列表),如果字典为空,则返回一个空列表 [] 。
# len(next(iter(per_class.values()), [])) :
# len() 函数计算从 next(iter(per_class.values()), []) 获取的列表的长度。
# 如果 per_class 字典为空, next(iter(per_class.values()), []) 返回空列表 [] ,其长度为 0 。
# 如果 per_class 字典不为空, len() 返回第一个评估指标列表的长度,即类别的数量。
# range(len(next(iter(per_class.values()), []))) :
# range() 函数生成一个从 0 到 n-1 的整数序列,其中 n 是 len(next(iter(per_class.values()), [])) 的值。
# 这个序列用于遍历所有类别的索引。
for i in range(len(next(iter(per_class.values()), [])))
]
# 这段代码定义了 DetMetrics 类中的 summary 方法,用于生成一个包含每个类别评估指标的列表。每个类别对应一个字典,包含类别名称、每个类别的评估指标(精确度、召回率、F1 分数)以及整体的评估指标(mAP、mAP50、mAP75)。这个方法使得 DetMetrics 类的实例能够方便地生成和使用详细的评估指标报告。
# DetMetrics 类是一个用于计算和管理目标检测任务评估指标的工具类。它继承自 SimpleClass 和 DataExportMixin ,具备良好的字符串表示、属性访问控制以及将评估指标导出为多种格式(如 Pandas DataFrame、CSV、XML、HTML、JSON 和 SQLite)的功能。该类通过一系列方法和属性,提供了对目标检测模型性能的全面评估,包括精确度(Precision)、召回率(Recall)、F1 分数、平均精度(mAP)等指标的计算和管理。此外, DetMetrics 类还支持绘制精确度-召回率曲线,并提供了详细的评估指标报告,便于对模型的性能进行深入分析和优化。