ops.py
ultralytics\utils\ops.py
目录
ops.py
1.所需的库和模块
2.class Profile(contextlib.ContextDecorator):
3.def segment2box(segment, width=640, height=640):
4.def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None, padding=True, xywh=False):
5.def make_divisible(x, divisor):
6.def nms_rotated(boxes, scores, threshold=0.45):
7.def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False, labels=(), max_det=300, nc=0, max_time_img=0.05, max_nms=30000, max_wh=7680, in_place=True, rotated=False,):
8.def clip_boxes(boxes, shape):
9.def clip_coords(coords, shape):
10.def scale_image(masks, im0_shape, ratio_pad=None):
11.def xyxy2xywh(x):
12.def xywh2xyxy(x):
13.def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
14.def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0):
15.def xywh2ltwh(x):
16.def xyxy2ltwh(x):
17.def ltwh2xywh(x):
18.def xyxyxyxy2xywhr(corners):
19.def xywhr2xyxyxyxy(rboxes):
20.def ltwh2xyxy(x):
21.def segments2boxes(segments):
22.def resample_segments(segments, n=1000):
23.def crop_mask(masks, boxes):
24.def process_mask_upsample(protos, masks_in, bboxes, shape):
25.def process_mask(protos, masks_in, bboxes, shape, upsample=False):
26.def process_mask_native(protos, masks_in, bboxes, shape):
27.def scale_masks(masks, shape, padding=True):
28.def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None, normalize=False, padding=True):
29.def regularize_rboxes(rboxes):
30.def masks2segments(masks, strategy="largest"):
31.def convert_torch2numpy_batch(batch: torch.Tensor) -> np.ndarray:
32.def clean_str(s):
33.def v10postprocess(preds, max_det, nc=80):
1.所需的库和模块
# Ultralytics YOLO 🚀, AGPL-3.0 licenseimport contextlib
import math
import re
import timeimport cv2
import numpy as np
import torch
import torch.nn.functional as F
import torchvisionfrom ultralytics.utils import LOGGER
from ultralytics.utils.metrics import batch_probiou
2.class Profile(contextlib.ContextDecorator):
# 这段代码定义了一个名为 Profile 的类,它继承自 contextlib.ContextDecorator ,用于测量代码块的执行时间,支持CPU和CUDA设备。
# 这行代码定义了 Profile 类,继承自 contextlib.ContextDecorator 。 ContextDecorator 是一个上下文管理器,它允许类通过装饰器的方式使用,使得在函数或代码块中可以方便地添加上下文管理逻辑,如时间测量等。
class Profile(contextlib.ContextDecorator):# YOLOv8 Profile 类。使用 @Profile() 作为装饰器,或使用 'with Profile():' 作为上下文管理器。"""YOLOv8 Profile class. Use as a decorator with @Profile() or as a context manager with 'with Profile():'.Example:```pythonfrom ultralytics.utils.ops import Profilewith Profile(device=device) as dt:pass # slow operation hereprint(dt) # prints "Elapsed time is 9.5367431640625e-07 s"```"""# 这是 Profile 类的初始化方法。它接受两个参数。这个方法初始化了实例变量 t 、 device 和 cuda ,其中 cuda 是一个布尔值,表示是否使用CUDA设备,通过判断 device 是否为 None 且其字符串表示以“cuda”开头来确定。# 1.t :一个浮点数,默认值为0.0,用于累计时间。# 2.device :一个 torch.device 对象,默认为 None ,用于指定设备(CPU或CUDA设备)。def __init__(self, t=0.0, device: torch.device = None):# 初始化 Profile 类。"""Initialize the Profile class.Args:t (float): Initial time. Defaults to 0.0.device (torch.device): Devices used for model inference. Defaults to None (cpu)."""# 将传入的 t 参数赋值给实例变量 t ,用于累计时间。self.t = t# 这行代码将传入的 device 参数赋值给实例变量 device ,用于存储设备信息。self.device = device# 根据 device 是否为 None 且其字符串表示是否以“cuda”开头来判断是否使用CUDA设备,并将结果赋值给布尔类型的实例变量 cuda 。self.cuda = bool(device and str(device).startswith("cuda"))# 这是上下文管理器的 __enter__ 方法,当进入 with 语句块时会被调用。def __enter__(self):# 开始计时。"""Start timing."""# 调用 time 方法获取当前时间,并将其赋值给实例变量 start ,作为时间测量的起始点。self.start = self.time()# 返回当前实例对象,使得可以在 with 语句块中使用该对象。return self# 这是上下文管理器的 __exit__ 方法,当退出 with 语句块时会被调用。它接受三个参数。这里的 # noqa 是一个注释,通常用于告诉代码检查工具忽略该行代码的某些检查。# 1.type :异常类型。# 2.value :异常值。# 3.traceback :异常的回溯信息。def __exit__(self, type, value, traceback): # noqa# 停止计时。"""Stop timing."""# 调用 time 方法获取当前时间,然后减去 start 时间,得到时间差 dt ,并将其赋值给实例变量 dt 。self.dt = self.time() - self.start # delta-time# 将时间差 dt 累加到实例变量 t 中,用于累计总时间。self.t += self.dt # accumulate dt# 这是 Profile 类的 __str__ 方法,用于返回对象的字符串表示。def __str__(self):# 返回一个人类可读的字符串,表示分析器中累计的运行时间。"""Returns a human-readable string representing the accumulated elapsed time in the profiler."""# 返回一个格式化的字符串,表示累计的时间 t ,单位为秒。return f"Elapsed time is {self.t} s" # 已用时间为 {self.t} 秒。# 这是 Profile 类的一个方法,用于获取当前时间。def time(self):# 获取当前时间。"""Get current time."""# 判断是否使用CUDA设备,如果是,则调用 torch.cuda.synchronize(self.device) 来同步CUDA设备,确保所有CUDA操作都已完成,以便准确测量时间。if self.cuda:# torch.cuda.synchronize(device=None)# torch.cuda.synchronize() 是 PyTorch 库中的一个函数,用于同步 CPU 和 GPU 之间的计算。# 参数 :# device :可选参数,指定要同步的设备。可以是 torch.device 对象、整数(表示 GPU 编号)或字符串(如 'cuda:0' )。如果省略此参数或设置为 None ,则默认同步当前设备。# 功能 :# torch.cuda.synchronize() 函数用于确保当前设备上所有先前提交的 CUDA 核心执行完成。这个函数会阻塞调用它的 CPU 线程,直到所有 CUDA 核心完成执行为止。# 作用 :# 由于 GPU 计算是异步的,提交到 GPU 的计算任务会被放入队列中,程序不会等待 GPU 完成计算就继续执行后续代码。这可能会导致一个问题 :在 GPU 计算尚未完成时,CPU 就开始访问 GPU 的计算结果,此时可能会得到错误的结果。# 为了避免这种情况,可以使用 torch.cuda.synchronize() 函数来同步 CPU 和 GPU 之间的计算,确保在继续执行后续代码之前 GPU 的计算已经完成。# 典型使用场景 :# 性能测试 :在测量 GPU 上运行的操作(如模型推理)所需时间时,需要在操作前后分别调用 torch.cuda.synchronize() 来确保测量时间包括了 GPU 上所有操作的执行时间。# 确保数据一致性 :在需要确保 GPU 计算结果已经准备好被 CPU 使用的场景下,比如在训练深度神经网络时,在每个 epoch 结束后计算验证集的误差,此时需要使用 torch.cuda.synchronize() 来同步 CPU 和 GPU 之间的计算,以确保得到正确的结果。# torch.cuda.synchronize() 是一个重要的函数,用于确保在继续执行后续代码之前 GPU 的计算已经完成,从而避免因异步计算导致的数据不一致问题。torch.cuda.synchronize(self.device)# 返回当前的时间戳,单位为秒。return time.time()
# Profile 类通过上下文管理器的方式,提供了一个方便的接口来测量代码块的执行时间。它支持CPU和CUDA设备,能够准确地测量并累计时间。在使用时,只需将需要测量时间的代码块放在 with Profile() as p: 语句块中,即可在 p 对象中获取累计时间等信息。
3.def segment2box(segment, width=640, height=640):
# 这段代码定义了一个名为 segment2box 的函数,用于将多边形顶点坐标(segment)转换为边界框(bounding box)坐标。
# 定义了 segment2box 函数,它接受三个参数。这个函数的目的是将多边形顶点坐标转换为边界框坐标。
# 1.segment :一个二维数组,表示多边形的顶点坐标。
# 2.width 和 3.height :分别表示图像的宽度和高度,默认值为640。
def segment2box(segment, width=640, height=640):# 将 1 个片段标签转换为 1 个框标签,应用图像内部约束,即 (xy1, xy2, ...) 到 (xyxy)。"""Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy).Args:segment (torch.Tensor): the segment labelwidth (int): the width of the image. Defaults to 640height (int): The height of the image. Defaults to 640Returns:(np.ndarray): the minimum and maximum x and y values of the segment."""# 将 segment 数组转置后解包为两个一维数组 x 和 y ,分别表示多边形顶点的x坐标和y坐标。 segment.T 表示对 segment 进行转置操作,使得原本按顶点顺序排列的坐标变为按坐标轴顺序排列。x, y = segment.T # segment xy# 码创建了一个布尔数组 inside ,用于标记哪些顶点坐标位于图像内部。通过逻辑与操作 & ,判断每个顶点的x坐标和y坐标是否都在图像的边界内(即x坐标在0到 width 之间,y坐标在0到 height 之间)。inside = (x >= 0) & (y >= 0) & (x <= width) & (y <= height)# 使用布尔索引 inside 筛选出位于图像内部的x坐标。x = x[inside]# 使用布尔索引 inside 筛选出位于图像内部的y坐标。y = y[inside]# 返回一个表示边界框坐标的数组。# 如果 x 数组中有元素(即 any(x) 为 True ),则计算边界框的左上角和右下角坐标( x.min(), y.min(), x.max(), y.max() ),并使用 segment 的dtype创建一个NumPy数组。# 如果没有顶点位于图像内部(即 any(x) 为 False ),则返回一个全零的数组,表示无效的边界框。边界框坐标格式为 xyxy ,即左上角x坐标、左上角y坐标、右下角x坐标、右下角y坐标。return (np.array([x.min(), y.min(), x.max(), y.max()], dtype=segment.dtype)if any(x)else np.zeros(4, dtype=segment.dtype)) # xyxy
# segment2box 函数将多边形顶点坐标转换为边界框坐标,同时考虑了顶点是否位于图像内部。它返回一个表示边界框坐标的数组,如果多边形完全位于图像外部,则返回一个全零数组。这个函数在处理图像中的多边形对象时非常有用,例如在目标检测任务中将多边形标注转换为边界框标注。
4.def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None, padding=True, xywh=False):
# 这段代码定义了一个名为 scale_boxes 的函数,用于将边界框从一个图像尺寸缩放到另一个图像尺寸,并考虑了填充(padding)和坐标格式(xyxy 或 xywh)。
# 定义了 scale_boxes 函数,它接受以下参数 :
# 1.img1_shape :目标图像的形状,格式为 (height, width) 。
# 2.boxes :边界框的坐标,格式为 [x1, y1, x2, y2] (xyxy格式)或 [x, y, w, h] (xywh格式)。
# 3.img0_shape :原始图像的形状,格式为 (height, width) 。
# 4.ratio_pad :可选参数,包含缩放比例和填充的元组,格式为 ((gain, ...), (padh, padw)) 。
# 5.padding :布尔值,表示是否考虑填充。
# 6.xywh :布尔值,表示边界框是否为xywh格式。
def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None, padding=True, xywh=False):# 将边界框(默认格式为 xyxy)从最初指定的图像形状(img1_shape)重新缩放为不同图像的形状(img0_shape)。"""Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originallyspecified in (img1_shape) to the shape of a different image (img0_shape).Args:img1_shape (tuple): The shape of the image that the bounding boxes are for, in the format of (height, width).boxes (torch.Tensor): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2)img0_shape (tuple): the shape of the target image, in the format of (height, width).ratio_pad (tuple): a tuple of (ratio, pad) for scaling the boxes. If not provided, the ratio and pad will becalculated based on the size difference between the two images.padding (bool): If True, assuming the boxes is based on image augmented by yolo style. If False then do regularrescaling.xywh (bool): The box format is xywh or not, default=False.Returns:boxes (torch.Tensor): The scaled bounding boxes, in the format of (x1, y1, x2, y2)"""# 检查是否提供了 ratio_pad 。如果没有提供,则根据 img0_shape 计算缩放比例和填充。if ratio_pad is None: # calculate from img0_shape# 计算缩放比例 gain ,取 img1_shape 和 img0_shape 的高度和宽度比值的最小值。gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new# 计算填充的宽度和高度,确保图像在缩放后居中。pad = (round((img1_shape[1] - img0_shape[1] * gain) / 2 - 0.1),round((img1_shape[0] - img0_shape[0] * gain) / 2 - 0.1),) # wh padding# 如果提供了 ratio_pad ,则直接使用其中的 缩放比例 gain 和 填充 pad 。else:gain = ratio_pad[0][0]pad = ratio_pad[1]# 如果 padding 为 True ,则从边界框坐标中减去填充值。if padding:boxes[..., 0] -= pad[0] # x paddingboxes[..., 1] -= pad[1] # y padding# 如果边界框格式为xyxy,则右下角坐标也需要减去填充值。if not xywh:boxes[..., 2] -= pad[0] # x paddingboxes[..., 3] -= pad[1] # y padding# 将边界框坐标除以缩放比例 gain ,以调整到原始图像尺寸。boxes[..., :4] /= gain# 调用 clip_boxes 函数,将调整后的边界框限制在原始图像的边界内,并返回结果。return clip_boxes(boxes, img0_shape)
# scale_boxes 函数用于将边界框从一个图像尺寸缩放到另一个图像尺寸,并考虑了填充和坐标格式。它首先计算或使用提供的缩放比例和填充值,然后调整边界框坐标,并将其限制在原始图像的边界内。这个函数在图像处理和目标检测任务中非常有用,特别是在处理不同尺寸的图像时。
5.def make_divisible(x, divisor):
# 这段代码定义了一个名为 make_divisible 的函数,用于将一个数 x 调整为能够被 divisor 整除的最小整数。这个函数在深度学习中特别有用,特别是在设计卷积神经网络(CNN)时,确保某些层的输出尺寸能够被特定的数整除,以满足后续层的要求。
# 定义了 make_divisible 函数,它接受两个参数。
# 1.x :一个需要被调整的数。
# 2.divisor :一个除数。
def make_divisible(x, divisor):# 返回能被给定除数整除的最接近的数。"""Returns the nearest number that is divisible by the given divisor.Args:x (int): The number to make divisible.divisor (int | torch.Tensor): The divisor.Returns:(int): The nearest number divisible by the divisor."""# 检查 divisor 是否是一个PyTorch张量。如果是,需要将其转换为一个整数。if isinstance(divisor, torch.Tensor):# 如果 divisor 是一个张量,取张量中的最大值,并将其转换为整数。这样做的目的是确保 divisor 是一个单一的整数值,因为后续操作需要一个整数除数。divisor = int(divisor.max()) # to int# 这行代码执行核心操作 :# x / divisor :计算 x 除以 divisor 的商。# math.ceil(x / divisor) :使用 math.ceil 函数对商进行向上取整,确保结果是一个整数。# math.ceil(x / divisor) * divisor :将取整后的商乘以 divisor ,得到能够被 divisor 整除的最小整数。return math.ceil(x / divisor) * divisor
# make_divisible 函数将一个数 x 调整为能够被 divisor 整除的最小整数。这个函数在深度学习中特别有用,特别是在设计卷积神经网络(CNN)时,确保某些层的输出尺寸能够被特定的数整除,以满足后续层的要求。例如,在使用某些预训练模型时,输入图像的尺寸需要能够被特定的数(如32)整除,以确保网络的兼容性。
6.def nms_rotated(boxes, scores, threshold=0.45):
# 这段代码定义了一个名为 nms_rotated 的函数,用于执行旋转边界框的非极大值抑制(Non-Maximum Suppression, NMS)。NMS是一种常用的算法,用于去除重叠的边界框,只保留置信度最高的边界框。
# 定义了 nms_rotated 函数,它接受三个参数。
# 1.boxes :一个二维数组,表示旋转边界框的坐标,格式为 [x, y, w, h, angle] ,其中 x, y 是边界框中心的坐标, w, h 是边界框的宽度和高度, angle 是边界框的旋转角度。
# 2.scores :一个一维数组,表示每个边界框的置信度分数。
# 3.threshold :一个浮点数,表示NMS的阈值,默认值为0.45。
def nms_rotated(boxes, scores, threshold=0.45):# obbs 的 NMS,由 probiou 和 fast-nms 提供支持。"""NMS for obbs, powered by probiou and fast-nms.Args:boxes (torch.Tensor): (N, 5), xywhr.scores (torch.Tensor): (N, ).threshold (float): IoU threshold.Returns:"""# 检查 boxes 是否为空。if len(boxes) == 0:# 如果为空,则返回一个空的NumPy数组,表示没有边界框被保留。return np.empty((0,), dtype=np.int8)# 使用 torch.argsort 函数对 scores 进行排序,返回一个索引数组 sorted_idx ,表示按置信度分数从高到低排序的边界框索引。sorted_idx = torch.argsort(scores, descending=True)# 根据 sorted_idx 对 boxes 进行重新排序,确保置信度最高的边界框排在前面。boxes = boxes[sorted_idx]# torch.triu_(input, diagonal=0, *, out=None) -> Tensor# triu_() 是 PyTorch 中的一个函数,它用于返回输入张量的上三角部分,并将其他元素设置为0。这个函数就地修改输入张量,因此在使用时需要注意,它会改变原始数据。# 参数 :# input (Tensor): 输入张量。# diagonal (int, 可选): 要考虑的对角线。 diagonal=0 表示主对角线,正值表示主对角线上方的对角线,负值表示主对角线下方的对角线。# out (Tensor, 可选): 输出张量。# 功能 :# 返回输入张量的上三角部分,其余位置为0。# diagonal 参数控制要考虑的对角线。如果 diagonal = 0 ,则保留主对角线及以上的所有元素。正值排除主对角线上方同样多的对角线,负值包括主对角线下方同样多的对角线。# 在非极大值抑制(NMS)算法中,使用 triu_ 方法将矩阵的下三角部分设置为0的原因是为了避免重复计算和比较边界框之间的交并比(IoU)。具体来说,NMS算法的目的是保留置信度最高的边界框,同时去除与之重叠较大的其他边界框。通过将IoU矩阵的下三角部分设置为0,可以确保每个边界框只与它之后的边界框进行比较,而不是与所有其他边界框进行比较。这样可以减少计算量,并且避免重复计算。# 详细解释 :# IoU矩阵的对称性 :# IoU矩阵是对称的,即 IoU(i, j) = IoU(j, i) 。这意味着每个边界框与另一个边界框的IoU值在矩阵中出现两次(一次在上三角部分,一次在下三角部分)。# 例如,边界框1与边界框2的IoU值在矩阵中既出现在 (1, 2) 位置,也出现在 (2, 1) 位置。# 避免重复计算 :# 通过将下三角部分设置为0,我们只需要考虑上三角部分的IoU值。这样可以减少计算量,因为每个边界框只与它之后的边界框进行比较。# 例如,对于边界框1,我们只需要计算它与边界框2、3、4等的IoU值,而不需要再计算它与边界框0的IoU值(因为边界框0已经在之前被处理过)。# 简化逻辑 :# 使用 triu_ 方法可以简化NMS算法的逻辑。我们只需要遍历上三角部分的IoU值,找到与当前边界框重叠较大的其他边界框,并将它们去除。# 例如,对于排序后的边界框,我们从置信度最高的边界框开始,依次检查它与之后的边界框的IoU值,如果IoU值大于阈值,则将这些边界框去除。# 调用 batch_probiou 函数计算所有边界框之间的交并比(IoU),并使用 triu_ 方法将矩阵的下三角部分设置为0,只保留上三角部分的IoU值。 diagonal=1 表示从对角线上的第一个元素开始设置为0,这样可以避免边界框与自身的IoU值影响结果。# def batch_probiou(obb1, obb2, eps=1e-7): -> 用于计算两个定向边界框(OBB)集合之间的概率交并比(ProbIoU)。返回最终的ProbIoU,结果形状为 [N, M] 。 -> return 1 - hdious = batch_probiou(boxes, boxes).triu_(diagonal=1)# torch.Tensor.squeeze([self, dim=None])# .squeeze() 是 PyTorch 中的一个方法,用于去除张量(tensor)中所有长度为1的维度。这个方法返回一个新的张量,它与原始张量共享数据,但是没有长度为1的维度。# 参数说明 :# self :要进行操作的张量。# dim=None :可选参数,指定要去除的维度。如果未指定或为 None ,则去除张量中所有长度为1的维度。# 返回值 :# 返回一个新的张量,其中所有长度为1的维度都被去除。# 示例 :# 原始张量 tensor 的形状是 (1, 3, 1, 5) ,使用 squeeze() 方法后,形状变为 (3, 5) ,因为长度为1的维度被去除了。# 我们指定了 dim=0 ,所以只有第0维(长度为1)被去除,而其他长度为1的维度保持不变。# .squeeze() 方法不会修改原始张量,而是返回一个新的张量。如果原始张量中没有长度为1的维度,或者指定的维度不是长度为1,那么返回的张量将与原始张量相同。# 找到所有最大IoU值小于阈值 threshold 的边界框索引。 ious.max(dim=0)[0] 计算每一列的最大IoU值, torch.nonzero 找到这些值中小于阈值的索引, squeeze_(-1) 去除最后一个维度的冗余。pick = torch.nonzero(ious.max(dim=0)[0] < threshold).squeeze_(-1)# 返回最终保留的边界框索引,这些索引对应于置信度最高且与其他边界框的IoU值小于阈值的边界框。return sorted_idx[pick]
# nms_rotated 函数用于对旋转边界框执行非极大值抑制,去除重叠的边界框,只保留置信度最高的边界框。这个函数在目标检测任务中非常有用,特别是在处理旋转目标时。通过调整 threshold 参数,可以控制保留边界框的严格程度。
7.def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False, labels=(), max_det=300, nc=0, max_time_img=0.05, max_nms=30000, max_wh=7680, in_place=True, rotated=False,):
# 这段代码定义了一个名为 non_max_suppression 的函数,用于执行非极大值抑制(Non-Maximum Suppression, NMS),这是目标检测任务中常用的一种技术,用于去除重叠的边界框,只保留置信度最高的边界框。这个函数支持多种配置和选项,适用于不同的检测模型和场景。
# 定义了一个名为 non_max_suppression 的方法,接受以下参数 :
# 1.prediction :模型的预测输出,通常是一个张量。
# 2.conf_thres :置信度阈值,用于过滤掉低置信度的边界框,默认值为0.25。
# 3.iou_thres :IoU阈值,用于NMS中去除重叠的边界框,默认值为0.45。
# 4.classes :可选的类别列表,用于只保留特定类别的边界框。
# 5.agnostic :是否类别不可知,即在NMS中不考虑类别信息,默认为 False 。
# 6.multi_label :是否允许多标签,即一个边界框可以属于多个类别,默认为 False 。
# 7.labels :可选的先验标签,用于自动标注。
# 8.max_det :每张图像的最大检测数,默认为300。
# 9.nc :类别数,默认为0,如果为0则从预测中自动推断。
# 10.max_time_img :每张图像的最大处理时间,默认为0.05秒。
# 11.max_nms :NMS的最大边界框数,默认为30000。
# 12.max_wh :最大边界框宽度和高度,默认为7680。
# 13.in_place :是否原地修改预测张量,默认为 True 。
# 14.rotated :是否处理旋转边界框,默认为 False 。
def non_max_suppression(prediction,conf_thres=0.25,iou_thres=0.45,classes=None,agnostic=False,multi_label=False,labels=(),max_det=300,nc=0, # number of classes (optional)max_time_img=0.05,max_nms=30000,max_wh=7680,in_place=True,rotated=False,
):# 在目标检测任务中, prediction 张量通常包含模型对每个边界框的预测结果。对于形状为 (batch_size, num_classes + 4 + num_masks, num_boxes) 的张量,各个维度的具体含义如下 :# 形状解释 :# batch_size : 这是批量大小,表示一次前向传播中处理的图像数量。例如,如果 batch_size 为4,表示一次处理4张图像。# num_classes + 4 + num_masks :这个维度包含了每个边界框的预测信息,具体分为三部分 :# 4 :表示边界框的坐标,通常格式为 [x, y, w, h] 或 [x1, y1, x2, y2] 。具体格式取决于模型的输出格式。# num_classes :表示每个边界框属于每个类别的置信度分数。例如,如果有80个类别,这个部分将包含80个分数。# num_masks :表示每个边界框的掩码信息,用于实例分割任务。如果模型不进行实例分割,这个部分可以为空。# num_boxes :这是每个图像中预测的边界框数量。例如,如果 num_boxes 为1000,表示每个图像中预测了1000个边界框。# 具体示例 :# 假设我们有一个模型,其输出张量 prediction 的形状为 (4, 84, 1000) ,具体解释如下 :# batch_size = 4 :表示一次处理4张图像。# num_classes + 4 + num_masks = 84 : 4 :边界框坐标 [x, y, w, h] 。 80 :80个类别的置信度分数。 0 :没有掩码信息(假设不进行实例分割)。# num_boxes = 1000 :每个图像中预测了1000个边界框。# 详细解释 :# 边界框坐标 : [x, y, w, h] 表示边界框的中心坐标 (x, y) 和宽度 w 、高度 h 。 [x1, y1, x2, y2] 表示边界框的左上角坐标 (x1, y1) 和右下角坐标 (x2, y2) 。# 类别置信度分数 :每个边界框有 num_classes 个分数,表示该边界框属于每个类别的置信度。例如,如果有80个类别,每个边界框将有80个分数,这些分数通常经过softmax或sigmoid激活函数处理。# 掩码信息 :如果模型进行实例分割,每个边界框还会有一个或多个掩码,用于表示边界框内的像素属于该实例。 num_masks 表示每个边界框的掩码数量。# 对一组框执行非最大抑制 (NMS),支持每个框的掩码和多个标签。# 参数:# prediction (torch.Tensor):形状为 (batch_size, num_classes + 4 + num_masks, num_boxes) 的张量,包含预测的框、类和掩码。张量应采用模型(例如 YOLO)输出的格式。# 返回:# (List[torch.Tensor]):长度为 batch_size 的列表,其中每个元素都是一个形状为 (num_boxes, 6 + num_masks) 的张量,包含保留的框,列为 (x1, y1, x2, y2, confidence, class, mask1, mask2, ...)。"""Perform non-maximum suppression (NMS) on a set of boxes, with support for masks and multiple labels per box.Args:prediction (torch.Tensor): A tensor of shape (batch_size, num_classes + 4 + num_masks, num_boxes)containing the predicted boxes, classes, and masks. The tensor should be in the formatoutput by a model, such as YOLO.conf_thres (float): The confidence threshold below which boxes will be filtered out.Valid values are between 0.0 and 1.0.iou_thres (float): The IoU threshold below which boxes will be filtered out during NMS.Valid values are between 0.0 and 1.0.classes (List[int]): A list of class indices to consider. If None, all classes will be considered.agnostic (bool): If True, the model is agnostic to the number of classes, and allclasses will be considered as one.multi_label (bool): If True, each box may have multiple labels.labels (List[List[Union[int, float, torch.Tensor]]]): A list of lists, where each innerlist contains the apriori labels for a given image. The list should be in the formatoutput by a dataloader, with each label being a tuple of (class_index, x1, y1, x2, y2).max_det (int): The maximum number of boxes to keep after NMS.nc (int, optional): The number of classes output by the model. Any indices after this will be considered masks.max_time_img (float): The maximum time (seconds) for processing one image.max_nms (int): The maximum number of boxes into torchvision.ops.nms().max_wh (int): The maximum box width and height in pixels.in_place (bool): If True, the input prediction tensor will be modified in place.Returns:(List[torch.Tensor]): A list of length batch_size, where each element is a tensor ofshape (num_boxes, 6 + num_masks) containing the kept boxes, with columns(x1, y1, x2, y2, confidence, class, mask1, mask2, ...)."""# 这段代码是 non_max_suppression 函数的一部分,主要进行一些初始的检查和设置,为后续的非极大值抑制(NMS)操作做准备。# Checks# 检查 置信度阈值 conf_thres 是否在有效范围内(0.0到1.0之间)。如果 conf_thres 不在这个范围内,会抛出一个 AssertionError ,并显示错误信息。assert 0 <= conf_thres <= 1, f"Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0" # 置信度阈值 {conf_thres} 无效,有效值介于 0.0 和 1.0 之间。# 检查 IoU阈值 iou_thres 是否在有效范围内(0.0到1.0之间)。如果 iou_thres 不在这个范围内,会抛出一个 AssertionError ,并显示错误信息。assert 0 <= iou_thres <= 1, f"Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0" # IoU {iou_thres} 无效,有效值介于 0.0 至 1.0 之间。# 检查 prediction 是否是一个列表或元组。if isinstance(prediction, (list, tuple)): # YOLOv8 model in validation model, output = (inference_out, loss_out)# 如果是,假设它是YOLOv8模型在验证模式下的输出,其中 第一个元素是推理输出 , 第二个元素是损失输出 。这里只取第一个元素作为预测输出。prediction = prediction[0] # select only inference output# 获取 批量大小 bs ,即 prediction 张量的第一个维度的大小,表示一次前向传播中处理的图像数量。bs = prediction.shape[0] # batch size# 计算 类别数 nc 。如果 nc 参数已经提供,则使用提供的值;否则,从 prediction 张量的形状中推断。假设 prediction 的形状为 (batch_size, num_classes + 4 + num_masks, num_boxes) ,则 nc 为 prediction.shape[1] - 4 。nc = nc or (prediction.shape[1] - 4) # number of classes# 计算 掩码数 nm 。假设 prediction 的形状为 (batch_size, num_classes + 4 + num_masks, num_boxes) ,则 nm 为 prediction.shape[1] - nc - 4 。nm = prediction.shape[1] - nc - 4# 计算 掩码起始索引 mi 。 mi 表示从 prediction 张量中提取掩码信息的起始索引,值为 4 + nc 。mi = 4 + nc # mask start index# 计算 候选边界框的布尔掩码 xc 。 prediction[:, 4:mi] 提取每个边界框的类别置信度分数, amax(1) 计算每个边界框的最高类别置信度分数, > conf_thres 生成一个布尔数组,表示哪些边界框的最高置信度分数大于置信度阈值 conf_thres 。xc = prediction[:, 4:mi].amax(1) > conf_thres # candidates# 这段代码的主要作用是进行一些初始的检查和设置,确保输入参数有效,并从预测张量中提取必要的信息,为后续的非极大值抑制(NMS)操作做准备。检查置信度阈值和IoU阈值是否在有效范围内。处理预测输出,确保其为张量形式。获取批量大小、类别数、掩码数和掩码起始索引。计算候选边界框的布尔掩码,用于后续的过滤操作。# 这段代码是 non_max_suppression 函数的一部分,主要进行一些设置和预测张量的预处理,为后续的非极大值抑制(NMS)操作做准备。# Settings# 这行代码是一个注释,表示可以设置最小边界框宽度和高度的阈值(以像素为单位),但在这个实现中没有使用这个设置。# min_wh = 2 # (pixels) minimum box width and height# 计算 处理每张图像的最大时间限制 time_limit 。 max_time_img 是每张图像的最大处理时间(秒), bs 是批量大小。 time_limit 的值为 2.0 + max_time_img * bs ,表示在处理所有图像时,如果总时间超过这个值,将提前终止处理。time_limit = 2.0 + max_time_img * bs # seconds to quit after# 检查是否允许多标签,并且只有当类别数 nc 大于1时才生效。 multi_label 是一个布尔值,表示是否允许多标签。如果 nc 大于1, multi_label 保持不变;否则, multi_label 将被设置为 False 。multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)# 将 预测张量 prediction 的最后两个维度进行转置。假设 prediction 的形状为 (1, 84, 6300) ,转置后形状变为 (1, 6300, 84) 。这样做的目的是将 边界框的数量 ( num_boxes )放在第二维,将每个边界框的特征(坐标、类别置信度、掩码等)放在第三维,便于后续处理。prediction = prediction.transpose(-1, -2) # shape(1,84,6300) to shape(1,6300,84)# 处理边界框坐标的格式转换。# 如果 rotated 为 False ,表示边界框不是旋转的,需要将坐标格式从 xywh (中心点坐标和宽度高度)转换为 xyxy (左上角和右下角坐标)。if not rotated:# in_place 为 True 。if in_place:# 直接在原张量上修改,将 prediction[..., :4] (即每个边界框的前4个值,表示 xywh 坐标)转换为 xyxy 格式。prediction[..., :4] = xywh2xyxy(prediction[..., :4]) # xywh to xyxy# in_place 为 False 。else:# 创建一个新的张量,将转换后的 xyxy 坐标与其余部分(类别置信度、掩码等)拼接在一起。prediction = torch.cat((xywh2xyxy(prediction[..., :4]), prediction[..., 4:]), dim=-1) # xywh to xyxy# 这段代码的主要作用是进行一些设置和预测张量的预处理,确保预测张量的格式适合后续的非极大值抑制(NMS)操作。计算处理每张图像的最大时间限制。检查是否允许多标签。转置预测张量,将边界框数量放在第二维。将边界框坐标格式从 xywh 转换为 xyxy ,以便进行NMS操作。# 这段代码是 non_max_suppression 函数的一部分,主要处理每个图像的预测结果,应用一些约束条件,并进行自动标注(如果适用)。# 记录当前时间,用于后续的时间限制检查。t = time.time()# 初始化一个列表 output ,长度为 批量大小 bs 。每个元素是一个形状为 (0, 6 + nm) 的张量,表示每个图像的最终检测结果。 6 + nm 表示每个检测结果包含6个基本字段( x1, y1, x2, y2, conf, cls )和 nm 个掩码字段。output = [torch.zeros((0, 6 + nm), device=prediction.device)] * bs# 遍历 每个图像的预测结果 。 xi 是图像索引, x 是该图像的预测张量。for xi, x in enumerate(prediction): # image index, image inference# Apply constraints# x[((x[:, 2:4] < min_wh) | (x[:, 2:4] > max_wh)).any(1), 4] = 0 # width-height# 应用 置信度阈值 conf_thres ,过滤掉低置信度的边界框。 xc[xi] 是一个布尔数组,表示哪些边界框的最高类别置信度分数大于 conf_thres 。 x[xc[xi]] 筛选出这些高置信度的边界框。x = x[xc[xi]] # confidence# Cat apriori labels if autolabelling# 检查 是否有先验标签 labels ,并且当前图像有先验标签,且边界框不是旋转的。如果这些条件都满足, 将先验标签添加到预测结果中 。if labels and len(labels[xi]) and not rotated:# 获取当前图像的先验标签。lb = labels[xi]# 创建一个形状为 (len(lb), nc + nm + 4) 的零张量 v ,用于 存储先验标签的边界框和类别信息 。v = torch.zeros((len(lb), nc + nm + 4), device=x.device)# 将 先验标签的边界框坐标 从 xywh 格式转换为 xyxy 格式,并存储在 v 的前4列中。v[:, :4] = xywh2xyxy(lb[:, 1:5]) # box# 将 先验标签的类别信息 存储在 v 的相应位置。 lb[:, 0].long() + 4 计算每个先验标签的类别索引, v[range(len(lb)), lb[:, 0].long() + 4] = 1.0 将这些位置的值设置为1.0。v[range(len(lb)), lb[:, 0].long() + 4] = 1.0 # cls# 将 先验标签的边界框和类别信息 v 与 预测结果 x 拼接在一起。x = torch.cat((x, v), 0)# If none remain process next image# 检查 当前图像的预测结果 x 是否为空。如果为空,跳过当前图像,处理下一个图像。if not x.shape[0]:continue# 这段代码的主要作用是处理每个图像的预测结果,应用置信度阈值过滤低置信度的边界框,并在需要时添加先验标签。记录当前时间,用于后续的时间限制检查。初始化输出列表 output ,每个元素是一个形状为 (0, 6 + nm) 的张量。遍历每个图像的预测结果。应用置信度阈值,过滤掉低置信度的边界框。如果有先验标签且边界框不是旋转的,将先验标签添加到预测结果中。如果当前图像的预测结果为空,跳过当前图像,处理下一个图像。# 这段代码是 non_max_suppression 函数的一部分,主要处理每个图像的预测结果,提取边界框、类别置信度和掩码信息,并根据多标签设置、类别过滤和最大NMS限制进行进一步处理。# Detections matrix nx6 (xyxy, conf, cls)# 将预 测张量 x 分割为三个部分。# box :包含 边界框的坐标 ,形状为 (num_boxes, 4) ,格式为 [x1, y1, x2, y2] 。# cls :包含 类别置信度分数 ,形状为 (num_boxes, nc) 。# mask :包含 掩码信息 ,形状为 (num_boxes, nm) 。如果 nm 为0, mask 将为空。box, cls, mask = x.split((4, nc, nm), 1)# 如果允许多标签( multi_label 为 True ),则执行以下操作if multi_label:# 找到 所有类别置信度分数 大于 conf_thres 的索引 (i, j) ,其中 i 是边界框索引, j 是类别索引。i, j = torch.where(cls > conf_thres)# x[i, 4 + j, None] :提取这些边界框的类别置信度分数。# j[:, None].float() :将类别索引转换为浮点数,并增加一个维度。# torch.cat((box[i], x[i, 4 + j, None], j[:, None].float(), mask[i]), 1) :将边 界框坐标 、 类别置信度分数 、 类别索引 和 掩码信息 拼接在一起,形成 新的预测张量 x 。x = torch.cat((box[i], x[i, 4 + j, None], j[:, None].float(), mask[i]), 1)# 如果不允许多标签( multi_label 为 False ),则只保留每个边界框的最高类别置信度分数。else: # best class only# 找到每个边界框的 最高类别置信度分数 conf 和 对应的类别索引 j 。conf, j = cls.max(1, keepdim=True)# torch.cat((box, conf, j.float(), mask), 1) :将 边界框坐标 、 最高类别置信度分数 、 类别索引 和 掩码信息 拼接在一起。# [conf.view(-1) > conf_thres] :过滤掉置信度分数小于 conf_thres 的边界框。x = torch.cat((box, conf, j.float(), mask), 1)[conf.view(-1) > conf_thres]# Filter by class# 如果指定了特定类别 classes ,则只保留这些类别的边界框。if classes is not None:# x[:, 5:6] == torch.tensor(classes, device=x.device) :生成一个布尔张量,表示每个边界框的类别索引是否在指定的类别列表中。# .any(1) :对每个边界框,检查是否有任何类别索引匹配,返回一个布尔数组。# x[...] :根据布尔数组过滤边界框。x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]# Check shape# 检查 当前图像的边界框数量 n 。如果没有边界框,跳过当前图像,处理下一个图像。n = x.shape[0] # number of boxesif not n: # no boxescontinue# 如果边界框数量超过 max_nms ,则按置信度分数降序排序,并保留前 max_nms 个边界框。if n > max_nms: # excess boxes# x[:, 4].argsort(descending=True) :按置信度分数降序排序,返回索引。# [:max_nms] :取前 max_nms 个索引。# x[...] :根据索引过滤边界框。x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence and remove excess boxes# 这段代码的主要作用是处理每个图像的预测结果,提取边界框、类别置信度和掩码信息,并根据多标签设置、类别过滤和最大NMS限制进行进一步处理。分割预测张量,提取边界框、类别置信度和掩码信息。根据多标签设置,处理多标签或单标签情况。如果指定了特定类别,过滤掉其他类别的边界框。检查边界框数量,如果没有边界框,跳过当前图像。如果边界框数量超过 max_nms ,按置信度分数降序排序并保留前 max_nms 个边界框。# 这段代码是 non_max_suppression 函数的一部分,主要执行批量非极大值抑制(Batched NMS)操作,用于去除重叠的边界框,只保留置信度最高的边界框。# Batched NMS# 计算 类别偏移量 c 。如果 agnostic 为 True ,表示类别不可知, c 为0;否则, c 为类别索引乘以 max_wh ,用于在NMS中区分不同类别的边界框。c = x[:, 5:6] * (0 if agnostic else max_wh) # classes# 提取 每个边界框的置信度分数 scores ,位于预测张量的第5列(索引为4)。scores = x[:, 4] # scores# 如果边界框是旋转的( rotated 为 True ),则执行以下操作。if rotated:# x[:, :2] + c :将 类别偏移量 c 加到 边界框的中心坐标上 。# x[:, 2:4] :提取边界框的宽度和高度。# x[:, -1:] :提取边界框的旋转角度。# torch.cat((x[:, :2] + c, x[:, 2:4], x[:, -1:]), dim=-1) :将这些信息拼接在一起,形成旋转边界框的格式 [x, y, w, h, angle] 。boxes = torch.cat((x[:, :2] + c, x[:, 2:4], x[:, -1:]), dim=-1) # xywhr# 调用 nms_rotated 函数执行旋转边界框的NMS操作。i = nms_rotated(boxes, scores, iou_thres)# 如果边界框不是旋转的( rotated 为 False ),则执行以下操作。else:# 将类别偏移量 c 加到 边界框的坐标上 ,形成 偏移后的边界框坐标 [x1, y1, x2, y2] 。boxes = x[:, :4] + c # boxes (offset by class)# 调用 torchvision.ops.nms 函数执行标准的NMS操作。i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS# 限制 最终保留的检测数 不超过 max_det 。 i 是 NMS操作返回的索引数组 ,表示 保留的边界框索引 。通过切片 i[:max_det] ,确保最多保留 max_det 个边界框。i = i[:max_det] # limit detections# 这段代码的主要作用是执行批量非极大值抑制(Batched NMS)操作,去除重叠的边界框,只保留置信度最高的边界框。计算类别偏移量 c ,用于在NMS中区分不同类别的边界框。提取每个边界框的置信度分数 scores 。根据是否处理旋转边界框,选择合适的NMS函数:如果是旋转边界框,调用 nms_rotated 函数。如果不是旋转边界框,调用 torchvision.ops.nms 函数。限制最终保留的检测数不超过 max_det 。# # Experimental# merge = False # use merge-NMS# if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)# # Update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)# from .metrics import box_iou# iou = box_iou(boxes[i], boxes) > iou_thres # IoU matrix# weights = iou * scores[None] # box weights# x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes# redundant = True # require redundant detections# if redundant:# i = i[iou.sum(1) > 1] # require redundancy# 这段代码是 non_max_suppression 函数的最后一部分,主要负责将处理后的检测结果存储到输出列表中,并检查是否超过了预设的时间限制。如果超过时间限制,则发出警告并终止处理。# 将 经过NMS处理后保留的边界框 x[i] 存储到 输出列表 output 中,对应于第 xi 个图像。 x[i] 是一个张量,包含该图像中所有保留的检测结果,每个检测结果的格式为 [x1, y1, x2, y2, conf, cls] 。output[xi] = x[i]# 检查从开始处理到当前的时间是否超过了预设的时间限制 time_limit 。 time.time() - t 计算当前时间与开始时间的差值。if (time.time() - t) > time_limit:# 如果这个差值超过了 time_limit ,则使用 LOGGER.warning 发出警告,提示NMS处理时间超过了预设限制。LOGGER.warning(f"WARNING ⚠️ NMS time limit {time_limit:.3f}s exceeded") # 警告 ⚠️ 已超出 NMS 时间限制 {time_limit:.3f}s 。# 语句用于终止循环,不再处理后续的图像。break # time limit exceeded# 这段代码将经过NMS处理后的检测结果存储到输出列表中。检查处理时间是否超过了预设的时间限制,如果超过,则发出警告并终止处理。# 返回处理后的边界框列表,每张图像一个列表。 输出是一个列表,每张图像一个列表,形状为 (num_det, 6) 的张量,其中 num_det 是检测到的边界框数量,每个列表包含保留的边界框,每个边界框的格式为 [x1, y1, x2, y2, conf, cls] ,其中 x1, y1, x2, y2 是边界框的坐标, conf 是置信度, cls 是类别索引。return output
# non_max_suppression 函数是一个功能强大的NMS实现,支持多种配置和选项,适用于不同的目标检测模型和场景。通过调整参数,可以灵活地控制NMS的行为,确保检测结果的准确性和效率。
8.def clip_boxes(boxes, shape):
# 这段代码定义了一个名为 clip_boxes 的函数,用于将边界框坐标限制在指定的图像形状内。它支持PyTorch张量和NumPy数组作为输入。
# 定义了 clip_boxes 函数,它接受两个参数。
# 1.boxes :一个二维数组,表示边界框的坐标,格式为 [x1, y1, x2, y2] ,其中 (x1, y1) 是左上角坐标, (x2, y2) 是右下角坐标。
# 2.shape :一个表示图像形状的元组,格式为 (height, width) 。
def clip_boxes(boxes, shape):# 获取边界框列表和形状(高度、宽度),并将边界框剪裁为该形状。"""Takes a list of bounding boxes and a shape (height, width) and clips the bounding boxes to the shape.Args:boxes (torch.Tensor): the bounding boxes to clipshape (tuple): the shape of the imageReturns:(torch.Tensor | numpy.ndarray): Clipped boxes"""# 检查 boxes 是否是一个PyTorch张量。如果是,将分别对每个坐标进行限制。这里提到了一个警告,即在Apple MPS(Metal Performance Shaders)上使用 .clamp_() 方法可能会有bug,但通常情况下这种方法是更快的。if isinstance(boxes, torch.Tensor): # faster individually (WARNING: inplace .clamp_() Apple MPS bug)# 将所有边界框的左上角x坐标( x1 )限制在0到 shape[1] (图像宽度)之间。boxes[..., 0] = boxes[..., 0].clamp(0, shape[1]) # x1# 将所有边界框的左上角y坐标( y1 )限制在0到 shape[0] (图像高度)之间。boxes[..., 1] = boxes[..., 1].clamp(0, shape[0]) # y1# 将所有边界框的右下角x坐标( x2 )限制在0到 shape[1] (图像宽度)之间。boxes[..., 2] = boxes[..., 2].clamp(0, shape[1]) # x2# 将所有边界框的右下角y坐标( y2 )限制在0到 shape[0] (图像高度)之间。boxes[..., 3] = boxes[..., 3].clamp(0, shape[0]) # y2# 如果 boxes 不是PyTorch张量,则假设它是NumPy数组,并使用NumPy的 clip 方法进行批量限制,这通常比逐个限制更快。else: # np.array (faster grouped)# 将所有边界框的左上角和右下角x坐标( x1 和 x2 )限制在0到 shape[1] (图像宽度)之间。boxes[..., [0, 2]] = boxes[..., [0, 2]].clip(0, shape[1]) # x1, x2# 将所有边界框的左上角和右下角y坐标( y1 和 y2 )限制在0到 shape[0] (图像高度)之间。boxes[..., [1, 3]] = boxes[..., [1, 3]].clip(0, shape[0]) # y1, y2# 返回限制后的边界框坐标。return boxes
# clip_boxes 函数将边界框坐标限制在指定的图像形状内,确保边界框不会超出图像边界。它支持PyTorch张量和NumPy数组作为输入,并根据输入类型选择不同的限制方法。这个函数在处理图像中的边界框时非常有用,例如在目标检测任务中将预测的边界框限制在图像范围内。
9.def clip_coords(coords, shape):
# 这段代码定义了一个名为 clip_coords 的函数,用于将坐标限制在指定的形状范围内。
# 定义函数 clip_coords ,接受两个参数。
# 1.coords :坐标。
# 2.shape :形状,通常是一个包含高度和宽度的元组。
def clip_coords(coords, shape):# 将线坐标剪切到图像边界。"""Clip line coordinates to the image boundaries.Args:coords (torch.Tensor | numpy.ndarray): A list of line coordinates.shape (tuple): A tuple of integers representing the size of the image in the format (height, width).Returns:(torch.Tensor | numpy.ndarray): Clipped coordinates"""# 判断 coords 是否为 torch.Tensor 类型,即PyTorch张量类型。if isinstance(coords, torch.Tensor): # faster individually (WARNING: inplace .clamp_() Apple MPS bug)# 如果 coords 是张量,对张量中所有坐标的x轴分量(即索引为0的分量)进行限制,使其在0到 shape[1] (宽度)的范围内。这里使用了 clamp_() 方法,它是一个原地操作,直接修改原张量的值。但需要注意,这里提到存在Apple MPS(Metal Performance Shaders)的bug,可能会影响其在某些设备上的使用。coords[..., 0] = coords[..., 0].clamp(0, shape[1]) # x# 对张量中所有坐标的y轴分量(即索引为1的分量)进行限制,使其在0到 shape[0] (高度)的范围内。coords[..., 1] = coords[..., 1].clamp(0, shape[0]) # y# 如果 coords 不是张量,则执行以下操作。else: # np.array (faster grouped)# 假设 coords 是NumPy数组,对数组中所有坐标的x轴分量进行限制,使其在0到 shape[1] 的范围内。这里使用了 clip() 方法,它会返回一个新的数组,原数组不会被修改。coords[..., 0] = coords[..., 0].clip(0, shape[1]) # x# 对数组中所有坐标的y轴分量进行限制,使其在0到 shape[0] 的范围内。coords[..., 1] = coords[..., 1].clip(0, shape[0]) # y# 返回处理后的坐标。return coords
# 这段代码的主要作用是将输入的坐标限制在给定形状的边界内。它根据坐标的数据类型(PyTorch张量或NumPy数组)选择不同的方法进行处理,确保坐标不会超出形状所定义的范围。这对于图像处理、计算机视觉等领域中处理坐标数据时非常有用,可以避免坐标越界导致的错误。
10.def scale_image(masks, im0_shape, ratio_pad=None):
# 这段代码定义了一个名为 scale_image 的函数,用于将图像掩码(masks)从一个形状( im1_shape )缩放到另一个形状( im0_shape )。
# 定义函数 scale_image ,接受三个参数。
# 1.masks :待缩放的图像掩码。
# 2.im0_shape :目标形状。
# 3.ratio_pad :一个可选参数,用于提供缩放比例和填充信息。
def scale_image(masks, im0_shape, ratio_pad=None):# 获取掩膜,并将其调整为原始图像大小。# 参数:# mask (np.ndarray):调整大小并填充的掩膜/图像,[h, w, num]/[h, w, 3]。"""Takes a mask, and resizes it to the original image size.Args:masks (np.ndarray): resized and padded masks/images, [h, w, num]/[h, w, 3].im0_shape (tuple): the original image shaperatio_pad (tuple): the ratio of the padding to the original image.Returns:masks (torch.Tensor): The masks that are being returned."""# Rescale coordinates (xyxy) from im1_shape to im0_shape# 获取 masks 的当前形状,即源形状。im1_shape = masks.shape# 如果源形状和目标形状的前两个维度(高度和宽度)相同,则直接返回 masks ,无需缩放。if im1_shape[:2] == im0_shape[:2]:return masks# 如果未提供 ratio_pad ,则从 im0_shape 计算缩放比例和填充。if ratio_pad is None: # calculate from im0_shape# 计算缩放比例,取高度和宽度缩放比例的最小值,确保图像不会被拉伸变形。gain = min(im1_shape[0] / im0_shape[0], im1_shape[1] / im0_shape[1]) # gain = old / new# 计算填充的宽度和高度,使得缩放后的图像居中。pad = (im1_shape[1] - im0_shape[1] * gain) / 2, (im1_shape[0] - im0_shape[0] * gain) / 2 # wh padding# 如果提供了 ratio_pad ,则直接使用其中的填充信息。else:# gain = ratio_pad[0][0]# 获取填充信息。pad = ratio_pad[1]# 将填充的上下和左右值转换为整数,分别赋值给 top 和 left 。top, left = int(pad[1]), int(pad[0]) # y, x# 计算底部和右侧的边界位置。bottom, right = int(im1_shape[0] - pad[1]), int(im1_shape[1] - pad[0])# 检查 masks 的维度,如果小于2,则抛出异常。if len(masks.shape) < 2:# 抛出异常,提示 masks 的维度应为2或3。raise ValueError(f'"len of masks shape" should be 2 or 3, but got {len(masks.shape)}') # 遮罩形状的长度”应为 2 或 3,但结果为 {len(masks.shape)} 。# 根据计算出的边界,裁剪 masks ,去除填充部分。masks = masks[top:bottom, left:right]# 使用OpenCV的 resize 函数将裁剪后的 masks 缩放到目标形状。masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]))# 如果缩放后的 masks 是二维的,增加一个维度。if len(masks.shape) == 2:# 增加一个维度,使其变为三维。masks = masks[:, :, None]# 返回缩放后的 masks 。return masks
# 这段代码的主要作用是将图像掩码从一个形状缩放到另一个形状,同时处理填充和裁剪。它首先检查是否需要缩放,然后根据提供的缩放比例和填充信息或自动计算的值进行裁剪和缩放。最后,确保返回的掩码是三维的,以便于后续处理。这对于图像处理和计算机视觉任务中处理不同尺寸的图像掩码非常有用。
11.def xyxy2xywh(x):
# 这段代码定义了一个名为 xyxy2xywh 的函数,其作用是将边界框的坐标格式从 xyxy (左上角坐标和右下角坐标)转换为 xywh (中心点坐标、宽度和高度)。
# 定义函数 xyxy2xywh ,接收一个参数。
# 1.x :一个张量或数组,表示边界框的坐标,其最后一维的长度应为4,分别表示左上角的横坐标、纵坐标以及右下角的横坐标、纵坐标。
def xyxy2xywh(x):# 将边界框坐标从 (x1, y1, x2, y2) 格式转换为 (x, y, width, height) 格式,其中 (x1, y1) 是左上角,(x2, y2) 是右下角。"""Convert bounding box coordinates from (x1, y1, x2, y2) format to (x, y, width, height) format where (x1, y1) is thetop-left corner and (x2, y2) is the bottom-right corner.Args:x (np.ndarray | torch.Tensor): The input bounding box coordinates in (x1, y1, x2, y2) format.Returns:y (np.ndarray | torch.Tensor): The bounding box coordinates in (x, y, width, height) format."""# 断言输入 x 的最后一维长度为4,如果不是,则抛出异常,提示输入的形状不符合要求。assert x.shape[-1] == 4, f"input shape last dimension expected 4 but input shape is {x.shape}" # 输入形状的最后一个维度应为 4,但输入形状为 {x.shape} 。# numpy.empty_like(a, dtype=None, order='K', subok=True, shape=None)# np.empty_like() 是NumPy库中的一个函数,用于创建一个与给定数组形状相同但未初始化的数组。# 参数说明 :# a :输入数组,新数组的形状和数据类型将基于此数组。# dtype :可选参数,指定新数组的数据类型。如果未指定,则默认使用输入数组 a 的数据类型。# order :可选参数,指定新数组的内存布局。可选值有 : 'C' :C顺序(行优先)。 'F' :Fortran顺序(列优先)。 'A' :如果输入数组 a 是Fortran连续的,则使用Fortran顺序;否则使用C顺序。 'K' :尽可能保持输入数组 a 的布局。# subok :可选参数,如果为 True ,则新数组将是一个与输入数组 a 相同类型的子类;如果为 False ,则新数组总是返回一个基类数组。默认为 True 。# shape :可选参数,指定新数组的形状。如果未指定,则默认使用输入数组 a 的形状。# 返回值 :# 返回一个与输入数组 a 形状相同但未初始化的数组。由于未初始化,数组中的值是随机的,取决于内存中的内容。# 需要注意的是, np.empty_like() 创建的数组未进行初始化,因此其内容是未定义的,这使得它比 np.zeros_like() 或 np.ones_like() 等函数更快,但在使用前需要手动初始化。# 根据输入 x 的类型( torch.Tensor 或 numpy 数组),创建一个与 x 形状相同但未初始化的张量或数组 y ,用于存储转换后的坐标。使用 empty_like 比 clone 或 copy 更快,因为不需要复制数据。y = torch.empty_like(x) if isinstance(x, torch.Tensor) else np.empty_like(x) # faster than clone/copy# 计算边界框中心点的横坐标,即左上角横坐标和右下角横坐标的平均值,并将结果赋值给 y 的最后一维的第一个元素。y[..., 0] = (x[..., 0] + x[..., 2]) / 2 # x center# 计算边界框中心点的纵坐标,即左上角纵坐标和右下角纵坐标的平均值,并将结果赋值给 y 的最后一维的第二个元素。y[..., 1] = (x[..., 1] + x[..., 3]) / 2 # y center# 计算边界框的宽度,即右下角横坐标减去左上角横坐标,并将结果赋值给 y 的最后一维的第三个元素。y[..., 2] = x[..., 2] - x[..., 0] # width# 计算边界框的高度,即右下角纵坐标减去左上角纵坐标,并将结果赋值给 y 的最后一维的第四个元素。y[..., 3] = x[..., 3] - x[..., 1] # height# 返回转换后的坐标张量或数组 y 。return y
# 这个函数通过简单的数学运算,将边界框的坐标从一种常见的格式( xyxy )转换为另一种常见格式( xywh ),这种转换在目标检测等计算机视觉任务中非常有用,因为它可以简化后续的计算和处理。函数还通过断言确保输入数据的格式正确,提高了代码的健壮性。
12.def xywh2xyxy(x):
# 这段代码定义了一个名为 xywh2xyxy 的函数,用于将坐标格式从中心点坐标加宽高(xywh)转换为左上角和右下角坐标(xyxy)。
# 定义函数 xywh2xyxy ,接受一个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的中心点坐标(x, y)和宽高(w, h)。
def xywh2xyxy(x):# 将边界框坐标从 (x, y, width, height) 格式转换为 (x1, y1, x2, y2) 格式,其中 (x1, y1) 是左上角,(x2, y2) 是右下角。"""Convert bounding box coordinates from (x, y, width, height) format to (x1, y1, x2, y2) format where (x1, y1) is thetop-left corner and (x2, y2) is the bottom-right corner.Args:x (np.ndarray | torch.Tensor): The input bounding box coordinates in (x, y, width, height) format.Returns:y (np.ndarray | torch.Tensor): The bounding box coordinates in (x1, y1, x2, y2) format."""# 断言输入数组的最后一维长度为4,即每个矩形的描述由4个值组成(x, y, w, h)。如果不符合,抛出异常并显示输入数组的形状。assert x.shape[-1] == 4, f"input shape last dimension expected 4 but input shape is {x.shape}" # 输入形状的最后一个维度应为 4,但输入形状为 {x.shape} 。# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 形状相同但内容为空的新数组 y ,用于存储转换后的坐标。使用 empty_like 而不是 clone 或 copy 可以更快地创建数组。y = torch.empty_like(x) if isinstance(x, torch.Tensor) else np.empty_like(x) # faster than clone/copy# 计算每个矩形宽度的一半,即 dw 。dw = x[..., 2] / 2 # half-width# 计算每个矩形高度的一半,即 dh 。dh = x[..., 3] / 2 # half-height# 计算左上角的x坐标,即中心点x坐标减去宽度的一半。y[..., 0] = x[..., 0] - dw # top left x# 计算左上角的y坐标,即中心点y坐标减去高度的一半。y[..., 1] = x[..., 1] - dh # top left y# 计算右下角的x坐标,即中心点x坐标加上宽度的一半。y[..., 2] = x[..., 0] + dw # bottom right x# 计算右下角的y坐标,即中心点y坐标加上高度的一半。y[..., 3] = x[..., 1] + dh # bottom right y# 返回转换后的坐标数组 y 。return y
# 这段代码的主要作用是将矩形的中心点坐标加宽高(xywh)格式转换为左上角和右下角坐标(xyxy)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。
13.def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
# 这段代码定义了一个名为 xywhn2xyxy 的函数,用于将归一化的中心点坐标加宽高(xywhn)转换为左上角和右下角坐标(xyxy),同时考虑了图像的尺寸和可能的填充。
# 定义函数 xywhn2xyxy ,接受五个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的归一化中心点坐标(x, y)和归一化宽高(w, h)。
# 2.w 和 3.h :分别是图像的宽度和高度,默认值为640。
# 4.padw 和 5.padh :分别是水平和垂直方向的填充,默认值为0。
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):# 将规范化的边界框坐标转换为像素坐标。"""Convert normalized bounding box coordinates to pixel coordinates.Args:x (np.ndarray | torch.Tensor): The bounding box coordinates.w (int): Width of the image. Defaults to 640h (int): Height of the image. Defaults to 640padw (int): Padding width. Defaults to 0padh (int): Padding height. Defaults to 0Returns:y (np.ndarray | torch.Tensor): The coordinates of the bounding box in the format [x1, y1, x2, y2] wherex1,y1 is the top-left corner, x2,y2 is the bottom-right corner of the bounding box."""# 断言输入数组的最后一维长度为4,即每个矩形的描述由4个值组成(x, y, w, h)。如果不符合,抛出异常并显示输入数组的形状。assert x.shape[-1] == 4, f"input shape last dimension expected 4 but input shape is {x.shape}" # 输入形状的最后一个维度应为 4,但输入形状为 {x.shape} 。# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 形状相同但内容为空的新数组 y ,用于存储转换后的坐标。使用 empty_like 而不是 clone 或 copy 可以更快地创建数组。y = torch.empty_like(x) if isinstance(x, torch.Tensor) else np.empty_like(x) # faster than clone/copy# 计算左上角的x坐标,即归一化中心点x坐标减去归一化宽度的一半,再乘以图像宽度 w ,最后加上水平填充 padw 。y[..., 0] = w * (x[..., 0] - x[..., 2] / 2) + padw # top left x# 计算左上角的y坐标,即归一化中心点y坐标减去归一化高度的一半,再乘以图像高度 h ,最后加上垂直填充 padh 。y[..., 1] = h * (x[..., 1] - x[..., 3] / 2) + padh # top left y# 计算右下角的x坐标,即归一化中心点x坐标加上归一化宽度的一半,再乘以图像宽度 w ,最后加上水平填充 padw 。y[..., 2] = w * (x[..., 0] + x[..., 2] / 2) + padw # bottom right x# 计算右下角的y坐标,即归一化中心点y坐标加上归一化高度的一半,再乘以图像高度 h ,最后加上垂直填充 padh 。y[..., 3] = h * (x[..., 1] + x[..., 3] / 2) + padh # bottom right y# 返回转换后的坐标数组 y 。return y
# 这段代码的主要作用是将归一化的中心点坐标加宽高(xywhn)格式转换为左上角和右下角坐标(xyxy)格式,并考虑了图像的实际尺寸和可能的填充。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。此外,考虑填充可以确保在图像边界附近的矩形也能正确转换。
14.def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0):
# 这段代码定义了一个名为 xyxy2xywhn 的函数,用于将左上角和右下角坐标(xyxy)转换为归一化的中心点坐标加宽高(xywhn),同时考虑了图像的尺寸和是否进行坐标裁剪。
# 定义函数 xyxy2xywhn ,接受五个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的左上角和右下角坐标(x1, y1, x2, y2)。
# 2.w 和 3.h :分别是图像的宽度和高度,默认值为640。
# 4.clip :一个布尔值,表示是否对坐标进行裁剪。
# 5.eps :一个小的浮点数,用于在裁剪时避免边界问题,默认值为0.0。
def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0):# 将边界框坐标从 (x1, y1, x2, y2) 格式转换为 (x, y, width, height, normalized) 格式。x、y、width 和 height 已标准化为图像尺寸。"""Convert bounding box coordinates from (x1, y1, x2, y2) format to (x, y, width, height, normalized) format. x, y,width and height are normalized to image dimensions.Args:x (np.ndarray | torch.Tensor): The input bounding box coordinates in (x1, y1, x2, y2) format.w (int): The width of the image. Defaults to 640h (int): The height of the image. Defaults to 640clip (bool): If True, the boxes will be clipped to the image boundaries. Defaults to Falseeps (float): The minimum value of the box's width and height. Defaults to 0.0Returns:y (np.ndarray | torch.Tensor): The bounding box coordinates in (x, y, width, height, normalized) format"""# 如果 clip 为 True ,则对坐标进行裁剪。if clip:# 调用 clip_boxes 函数对坐标进行裁剪,确保坐标不会超出图像边界。 clip_boxes 函数作用是将坐标限制在 (0, 0, h - eps, w - eps) 的范围内。x = clip_boxes(x, (h - eps, w - eps))# 断言输入数组的最后一维长度为4,即每个矩形的描述由4个值组成(x1, y1, x2, y2)。如果不符合,抛出异常并显示输入数组的形状。assert x.shape[-1] == 4, f"input shape last dimension expected 4 but input shape is {x.shape}" # 输入形状的最后一个维度应为 4,但输入形状为 {x.shape} 。# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 形状相同但内容为空的新数组 y ,用于存储转换后的坐标。使用 empty_like 而不是 clone 或 copy 可以更快地创建数组。y = torch.empty_like(x) if isinstance(x, torch.Tensor) else np.empty_like(x) # faster than clone/copy# 计算中心点的x坐标,即左上角和右下角x坐标的平均值,再除以图像宽度 w 进行归一化。y[..., 0] = ((x[..., 0] + x[..., 2]) / 2) / w # x center# 计算中心点的y坐标,即左上角和右下角y坐标的平均值,再除以图像高度 h 进行归一化。y[..., 1] = ((x[..., 1] + x[..., 3]) / 2) / h # y center# 计算矩形的宽度,即右下角x坐标减去左上角x坐标,再除以图像宽度 w 进行归一化。y[..., 2] = (x[..., 2] - x[..., 0]) / w # width# 计算矩形的高度,即右下角y坐标减去左上角y坐标,再除以图像高度 h 进行归一化。y[..., 3] = (x[..., 3] - x[..., 1]) / h # height# 返回转换后的坐标数组 y 。return y
# 这段代码的主要作用是将左上角和右下角坐标(xyxy)格式转换为归一化的中心点坐标加宽高(xywhn)格式,并考虑了图像的实际尺寸和是否进行坐标裁剪。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。此外,坐标裁剪可以确保在图像边界附近的矩形也能正确转换,避免坐标越界的问题。
15.def xywh2ltwh(x):
# 这段代码定义了一个名为 xywh2ltwh 的函数,用于将中心点坐标加宽高(xywh)格式转换为左上角坐标加宽高(ltwh)格式。
# 定义函数 xywh2ltwh ,接受一个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的中心点坐标(x, y)和宽高(w, h)。
def xywh2ltwh(x):# 将边界框格式从 [x, y, w, h] 转换为 [x1, y1, w, h],其中 x1、y1 为左上角坐标。"""Convert the bounding box format from [x, y, w, h] to [x1, y1, w, h], where x1, y1 are the top-left coordinates.Args:x (np.ndarray | torch.Tensor): The input tensor with the bounding box coordinates in the xywh formatReturns:y (np.ndarray | torch.Tensor): The bounding box coordinates in the xyltwh format"""# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 内容相同的副本 y 。如果 x 是PyTorch张量,使用 clone() 方法;如果是NumPy数组,使用 np.copy() 方法。这样可以避免直接修改输入数组 x 。y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)# 计算左上角的x坐标,即中心点x坐标减去宽度的一半。y[..., 0] = x[..., 0] - x[..., 2] / 2 # top left x# 计算左上角的y坐标,即中心点y坐标减去高度的一半。y[..., 1] = x[..., 1] - x[..., 3] / 2 # top left y# 返回转换后的坐标数组 y ,其中每一行包含一个矩形的左上角坐标(x, y)和宽高(w, h)。return y
# 这段代码的主要作用是将矩形的中心点坐标加宽高(xywh)格式转换为左上角坐标加宽高(ltwh)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。这种转换特别适用于需要将中心点坐标转换为左上角坐标的情况,例如在某些目标检测模型的输入预处理中。
16.def xyxy2ltwh(x):
# 这段代码定义了一个名为 xyxy2ltwh 的函数,用于将左上角和右下角坐标(xyxy)格式转换为左上角坐标加宽高(ltwh)格式。
# 定义函数 xyxy2ltwh ,接受一个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的左上角和右下角坐标(x1, y1, x2, y2)。
def xyxy2ltwh(x):# 将 nx4 个边界框从 [x1, y1, x2, y2] 转换为 [x1, y1, w, h],其中 xy1=左上角,xy2=右下角。"""Convert nx4 bounding boxes from [x1, y1, x2, y2] to [x1, y1, w, h], where xy1=top-left, xy2=bottom-right.Args:x (np.ndarray | torch.Tensor): The input tensor with the bounding boxes coordinates in the xyxy formatReturns:y (np.ndarray | torch.Tensor): The bounding box coordinates in the xyltwh format."""# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 内容相同的副本 y 。如果 x 是PyTorch张量,使用 clone() 方法;如果是NumPy数组,使用 np.copy() 方法。这样可以避免直接修改输入数组 x 。y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)# 计算矩形的宽度,即右下角x坐标减去左上角x坐标。y[..., 2] = x[..., 2] - x[..., 0] # width# 计算矩形的高度,即右下角y坐标减去左上角y坐标。y[..., 3] = x[..., 3] - x[..., 1] # height# 返回转换后的坐标数组 y ,其中每一行包含一个矩形的左上角坐标(x1, y1)和宽高(w, h)。return y
# 这段代码的主要作用是将矩形的左上角和右下角坐标(xyxy)格式转换为左上角坐标加宽高(ltwh)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。这种转换特别适用于需要将右下角坐标转换为宽高的情况,例如在某些目标检测模型的输入预处理中。
17.def ltwh2xywh(x):
# 这段代码定义了一个名为 ltwh2xywh 的函数,用于将左上角坐标加宽高(ltwh)格式转换为中心点坐标加宽高(xywh)格式。
# 定义函数 ltwh2xywh ,接受一个参数。
# 1.x :一个二维数组,其中每一行包含一个矩形的左上角坐标(x, y)和宽高(w, h)。
def ltwh2xywh(x):# 将 nx4 个框从 [x1, y1, w, h] 转换为 [x, y, w, h],其中 xy1=左上角,xy=中心。"""Convert nx4 boxes from [x1, y1, w, h] to [x, y, w, h] where xy1=top-left, xy=center.Args:x (torch.Tensor): the input tensorReturns:y (np.ndarray | torch.Tensor): The bounding box coordinates in the xywh format."""# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 内容相同的副本 y 。如果 x 是PyTorch张量,使用 clone() 方法;如果是NumPy数组,使用 np.copy() 方法。这样可以避免直接修改输入数组 x 。y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)# 计算中心点的x坐标,即左上角x坐标加上宽度的一半。y[..., 0] = x[..., 0] + x[..., 2] / 2 # center x# 计算中心点的y坐标,即左上角y坐标加上高度的一半。y[..., 1] = x[..., 1] + x[..., 3] / 2 # center y# 返回转换后的坐标数组 y ,其中每一行包含一个矩形的中心点坐标(x, y)和宽高(w, h)。return y
# 这段代码的主要作用是将矩形的左上角坐标加宽高(ltwh)格式转换为中心点坐标加宽高(xywh)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。这种转换特别适用于需要将左上角坐标转换为中心点坐标的情况,例如在某些目标检测模型的输入预处理中。
18.def xyxyxyxy2xywhr(corners):
# 这段代码定义了一个名为 xyxyxyxy2xywhr 的函数,用于将八个顶点坐标(xyxyxyxy)格式转换为旋转矩形的中心点坐标、宽高和旋转角度(xywhr)格式。
# 定义函数 xyxyxyxy2xywhr ,接受一个参数。
# 1.corners :一个二维数组,其中每一行包含一个矩形的八个顶点坐标(x1, y1, x2, y2, x3, y3, x4, y4)。
def xyxyxyxy2xywhr(corners):# 将批量定向边界框 (OBB) 从 [xy1, xy2, xy3, xy4] 转换为 [xywh, rotation]。旋转值应为 0 到 90 度。# 参数:# corners (numpy.ndarray | torch.Tensor):输入形状为 (n, 8) 的角。# 返回:# (numpy.ndarray | torch.Tensor):转换后的形状为 (n, 5) 的 [cx, cy, w, h, rotation] 格式的数据。"""Convert batched Oriented Bounding Boxes (OBB) from [xy1, xy2, xy3, xy4] to [xywh, rotation]. Rotation values areexpected in degrees from 0 to 90.Args:corners (numpy.ndarray | torch.Tensor): Input corners of shape (n, 8).Returns:(numpy.ndarray | torch.Tensor): Converted data in [cx, cy, w, h, rotation] format of shape (n, 5)."""# 检查 corners 是否为PyTorch张量。is_torch = isinstance(corners, torch.Tensor)# 如果 corners 是PyTorch张量,将其移动到CPU并转换为NumPy数组;否则直接使用 corners 。points = corners.cpu().numpy() if is_torch else corners# 将 points 重塑为形状为 (n, 4, 2) 的数组,其中 n 是矩形的数量,每个矩形有4个顶点,每个顶点有2个坐标(x, y)。points = points.reshape(len(corners), -1, 2)# 初始化一个空列表 rboxes ,用于存储转换后的旋转矩形参数。rboxes = []# 遍历每个矩形的顶点坐标。for pts in points:# 注释说明使用 cv2.minAreaRect 可以获取准确的旋转矩形参数,特别是当一些对象被数据加载器中的增强操作截断时。# NOTE: Use cv2.minAreaRect to get accurate xywhr,# especially some objects are cut off by augmentations in dataloader.# cv2.minAreaRect(points)# cv2.minAreaRect() 是 OpenCV 库中的一个函数,它用于计算给定点集的最小外接旋转矩形(也称为最小面积外接矩形)。这个函数返回一个 RotatedRect 对象,其中包含了矩形的中心点坐标、宽度和高度以及旋转角度。# 参数 :# points :一个点集,可以是一个 numpy 数组,形状为 (n, 1, 2) 或 (n, 2) ,其中 n 是点的数量,每个点由 (x, y) 坐标组成。# 返回值 :# 返回一个 RotatedRect 对象,包含以下属性。center :矩形的中心点坐标 (x, y) 。 size :矩形的宽度和高度 (width, height) 。 angle :矩形的旋转角度,以度为单位,表示矩形相对于水平轴的旋转角度。# 功能 :# cv2.minAreaRect() 函数计算并返回一个旋转矩形,该矩形能够完全包含输入的点集,并且具有最小的面积。这个矩形可能是倾斜的,其角度由 angle 属性给出。# 使用OpenCV的 minAreaRect 函数计算最小面积矩形的 中心点坐标 (x, y)、 宽高 (w, h)和 旋转角度 (angle,单位为度)。(x, y), (w, h), angle = cv2.minAreaRect(pts)# 将旋转矩形的参数(中心点坐标、宽高、旋转角度,角度转换为弧度)添加到 rboxes 列表中。rboxes.append([x, y, w, h, angle / 180 * np.pi])# 根据 corners 的数据类型,将 rboxes 转换为相应的张量或数组并返回。如果 corners 是PyTorch张量,将 rboxes 转换为PyTorch张量,并将其移动到与 corners 相同的设备上,数据类型也与 corners 相同;否则,将 rboxes 转换为NumPy数组。return (torch.tensor(rboxes, device=corners.device, dtype=corners.dtype)if is_torchelse np.asarray(rboxes, dtype=points.dtype)) # rboxes
# 这段代码的主要作用是将八个顶点坐标(xyxyxyxy)格式转换为旋转矩形的中心点坐标、宽高和旋转角度(xywhr)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在处理旋转矩形时,如目标检测中的旋转目标检测。通过使用OpenCV的 minAreaRect 函数,可以准确地计算最小面积矩形的参数,即使在一些对象被数据加载器中的增强操作截断的情况下也能保持准确性。这种转换特别适用于需要将顶点坐标转换为旋转矩形参数的情况,例如在某些目标检测模型的输入预处理中。
19.def xywhr2xyxyxyxy(rboxes):
# 这段代码定义了一个名为 xywhr2xyxyxyxy 的函数,用于将旋转边界框的表示形式从 (x, y, w, h, r) 转换为四个角点的坐标 (x1, y1, x2, y2, x3, y3, x4, y4) 。
# 定义 xywhr2xyxyxyxy 函数,它接受一个参数.
# 1.rboxes :表示旋转边界框的坐标,形状为 (b, n_boxes, 5) ,其中每个边界框用 (x, y, w, h, r) 表示。
def xywhr2xyxyxyxy(rboxes):# 将分批定向边界框 (OBB) 从 [xywh, rotation] 转换为 [xy1, xy2, xy3, xy4]。旋转值应为 0 到 90 度之间的度数。"""Convert batched Oriented Bounding Boxes (OBB) from [xywh, rotation] to [xy1, xy2, xy3, xy4]. Rotation values shouldbe in degrees from 0 to 90.Args:rboxes (numpy.ndarray | torch.Tensor): Boxes in [cx, cy, w, h, rotation] format of shape (n, 5) or (b, n, 5).Returns:(numpy.ndarray | torch.Tensor): Converted corner points of shape (n, 4, 2) or (b, n, 4, 2)."""# 检查 rboxes 是否为 NumPy 数组。如果是,则 is_numpy 为 True ,否则为 False 。is_numpy = isinstance(rboxes, np.ndarray)# 根据 rboxes 的类型选择合适的 cos 和 sin 函数。如果 rboxes 是 NumPy 数组,则使用 NumPy 的 cos 和 sin 函数;否则,使用 PyTorch 的 cos 和 sin 函数。cos, sin = (np.cos, np.sin) if is_numpy else (torch.cos, torch.sin)# 提取 旋转边界框的中心点坐标 (x, y) 。ctr = rboxes[..., :2]# 提取 旋转边界框的 宽度 w 、 高度 h 和 旋转角度 angle 。w, h, angle = (rboxes[..., i : i + 1] for i in range(2, 5))# 计算旋转角度的余弦值和正弦值。cos_value, sin_value = cos(angle), sin(angle)# 计算向量 vec1 ,表示 从中心点到右上角点的向量 。vec1 = [w / 2 * cos_value, w / 2 * sin_value]# 计算向量 vec2 ,表示 从中心点到左上角点的向量 。vec2 = [-h / 2 * sin_value, h / 2 * cos_value]# 将 vec1 的两个分量拼接成一个向量。vec1 = np.concatenate(vec1, axis=-1) if is_numpy else torch.cat(vec1, dim=-1)# 将 vec2 的两个分量拼接成一个向量。vec2 = np.concatenate(vec2, axis=-1) if is_numpy else torch.cat(vec2, dim=-1)# 计算右上角点的坐标。pt1 = ctr + vec1 + vec2# 计算右下角点的坐标。pt2 = ctr + vec1 - vec2# 计算左下角点的坐标。pt3 = ctr - vec1 - vec2# 计算左上角点的坐标。pt4 = ctr - vec1 + vec2# 将四个角点的坐标堆叠成一个形状为 (b, n_boxes, 4, 2) 的张量。如果 rboxes 是 NumPy 数组,则使用 np.stack ;否则,使用 torch.stack 。return np.stack([pt1, pt2, pt3, pt4], axis=-2) if is_numpy else torch.stack([pt1, pt2, pt3, pt4], dim=-2)
# xywhr2xyxyxyxy 函数将旋转边界框的表示形式从 (x, y, w, h, r) 转换为四个角点的坐标 (x1, y1, x2, y2, x3, y3, x4, y4) 。 这种转换在处理旋转边界框时非常有用,特别是在计算IoU、绘制边界框或进行其他几何操作时。
# xywhr2xyxyxyxy 函数提供了一种灵活的方式来处理旋转边界框,支持 NumPy 和 PyTorch 两种数据类型。通过计算四个角点的坐标,可以更方便地进行后续的几何计算和可视化。
20.def ltwh2xyxy(x):
# 这段代码定义了一个名为 ltwh2xyxy 的函数,用于将左上角坐标加宽高(ltwh)格式转换为左上角和右下角坐标(xyxy)格式。
# 定义函数 ltwh2xyxy ,接受一个参数。
# x :一个二维数组,其中每一行包含一个矩形的左上角坐标(x, y)和宽高(w, h)。
def ltwh2xyxy(x):# 它将边界框从 [x1, y1, w, h] 转换为 [x1, y1, x2, y2],其中 xy1=左上角,xy2=右下角。"""It converts the bounding box from [x1, y1, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right.Args:x (np.ndarray | torch.Tensor): the input imageReturns:y (np.ndarray | torch.Tensor): the xyxy coordinates of the bounding boxes."""# 根据 x 的数据类型(PyTorch张量或NumPy数组),创建一个与 x 内容相同的副本 y 。如果 x 是PyTorch张量,使用 clone() 方法;如果是NumPy数组,使用 np.copy() 方法。这样可以避免直接修改输入数组 x 。y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)# 计算右下角的x坐标,即左上角x坐标加上宽度。y[..., 2] = x[..., 2] + x[..., 0] # width# 计算右下角的y坐标,即左上角y坐标加上高度。y[..., 3] = x[..., 3] + x[..., 1] # height# 返回转换后的坐标数组 y ,其中每一行包含一个矩形的左上角和右下角坐标(x1, y1, x2, y2)。return y
# 这段代码的主要作用是将矩形的左上角坐标加宽高(ltwh)格式转换为左上角和右下角坐标(xyxy)格式。这种转换在图像处理和计算机视觉任务中非常常见,特别是在目标检测任务中,不同的模型和算法可能使用不同的坐标表示方式。通过这种方式,可以方便地在不同的表示方式之间进行转换,以便于数据的处理和模型的输入。这种转换特别适用于需要将宽高转换为右下角坐标的情况,例如在某些目标检测模型的输入预处理中。
21.def segments2boxes(segments):
# 这段代码定义了一个名为 segments2boxes 的函数,用于将多边形线段(segments)转换为矩形框(boxes),并将矩形框的格式从左上角和右下角坐标(xyxy)转换为中心点坐标加宽高(xywh)。
# 定义函数 segments2boxes ,接受一个参数。
# 1.segments :一个列表,其中每个元素是一个二维数组,表示一个线段的多个点的坐标(x, y)。
def segments2boxes(segments):# 它将线段标签转换为框标签,即 (cls, xy1, xy2, ...) 转换为 (cls, xywh)"""It converts segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh)Args:segments (list): list of segments, each segment is a list of points, each point is a list of x, y coordinatesReturns:(np.ndarray): the xywh coordinates of the bounding boxes."""# 初始化一个空列表 boxes ,用于存储转换后的矩形框。boxes = []# 遍历每个线段。for s in segments:# 将线段的坐标数组转置,得到两个一维数组 x 和 y ,分别表示所有点的x坐标和y坐标。x, y = s.T # segment xy# 计算线段的最小外接矩形框的左上角和右下角坐标(xyxy),并将其添加到 boxes 列表中。boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy# 将 boxes 列表转换为NumPy数组,并调用 xyxy2xywh 函数将矩形框的格式从左上角和右下角坐标(xyxy)转换为中心点坐标加宽高(xywh),然后返回转换后的矩形框数组。return xyxy2xywh(np.array(boxes)) # cls, xywh
# 这段代码的主要作用是将多边形线段转换为矩形框,并将矩形框的格式从左上角和右下角坐标(xyxy)转换为中心点坐标加宽高(xywh)。这种转换在图像处理和计算机视觉任务中非常常见,特别是在处理多边形标注数据时,需要将多边形转换为矩形框以便于后续处理。通过这种方式,可以方便地将多边形线段转换为矩形框,以便于数据的处理和模型的输入。这种转换特别适用于需要将多边形标注数据转换为矩形框标注数据的情况,例如在目标检测任务中。
22.def resample_segments(segments, n=1000):
# 这段代码定义了一个名为 resample_segments 的函数,用于对多边形线段(segments)进行重采样,使其具有指定数量的点(n=1000)。
# 定义函数 resample_segments ,接受两个参数。
# 1.segments :一个列表,其中每个元素是一个二维数组,表示一个线段的多个点的坐标(x, y)。
# 2.n :一个整数,表示重采样后每个线段的点数,默认值为1000。
def resample_segments(segments, n=1000):# 输入一个片段列表 (n,2),并返回一个片段列表 (n,2),每个片段上采样到 n 个点。"""Inputs a list of segments (n,2) and returns a list of segments (n,2) up-sampled to n points each.Args:segments (list): a list of (n,2) arrays, where n is the number of points in the segment.n (int): number of points to resample the segment to. Defaults to 1000Returns:segments (list): the resampled segments."""# 遍历每个线段。for i, s in enumerate(segments):# np.concatenate((a1, a2, ...), axis=0, out=None, dtype=None, casting='unsafe')# np.concatenate 是 NumPy 库中的一个函数,用于沿指定轴将一序列数组连接起来。# 参数 :# a1, a2, ... :一系列数组,这些数组必须有相同的形状,除了连接的轴以外。# axis :指定连接的轴。默认为 0,表示沿着第一个轴(通常是行)连接。# out :如果提供,将结果存储在这个数组中。# dtype :如果提供,将连接的数组转换为这个数据类型。# casting :指定如何处理不同数据类型的数组。默认为 'unsafe',表示即使可能导致数据丢失也允许转换。# 返回值 :# 返回一个新的数组,它是输入数组沿指定轴连接的结果。# 将线段的最后一个点与第一个点连接,形成一个闭合的多边形。s = np.concatenate((s, s[0:1, :]), axis=0)# 生成一个长度为 n 的等间距数组 x ,用于插值。这个数组的值从0到 len(s) - 1 ,表示线段上点的索引。x = np.linspace(0, len(s) - 1, n)# 生成一个与线段点数相同的索引数组 xp ,用于插值。xp = np.arange(len(s))# 对每个坐标维度(x和y)进行线性插值,生成新的点坐标。segments[i] = (# 这行代码中的操作是为了将两个一维数组(分别表示x坐标和y坐标)合并成一个二维数组,其中每一行表示一个点的坐标(x, y)。# np.interp(x, xp, s[:, i]) :对每个坐标维度(i=0表示x,i=1表示y)进行线性插值,生成新的点坐标。这会生成两个一维数组,分别表示插值后的x坐标和y坐标。# [np.interp(x, xp, s[:, i]) for i in range(2)] :生成一个包含两个一维数组的列表,第一个数组是插值后的x坐标,第二个数组是插值后的y坐标。# np.concatenate([...], dtype=np.float32) :将这个列表中的两个一维数组合并成一个二维数组。此时,合并后的数组形状为 (2, n) ,其中第一行是x坐标,第二行是y坐标。# .reshape(2, -1) :将合并后的数组重塑为 (2, n) 的形状。这一步是必要的,因为 np.concatenate 默认是按行合并,所以合并后的数组形状为 (2, n) 。# .T :将 (2, n) 形状的数组转置为 (n, 2) 形状。转置操作将每一列的两个值(x, y)组合成一个点的坐标,最终得到一个形状为 (n, 2) 的数组,其中每一行表示一个点的坐标(x, y)。# 为什么不能直接重塑为 (n, 2) 的形状?# 直接重塑为 (n, 2) 的形状在 np.concatenate 按行合并的情况下是不适用的,因为 np.concatenate 会将两个一维数组按行合并,生成一个 (2, n) 的数组。如果直接尝试重塑为 (n, 2) ,会引发错误,因为原始数组的形状和目标形状不匹配。# 假设我们有以下插值后的x和y坐标 :# x_coords = [1.0, 2.0, 3.0, 4.0, 5.0]# y_coords = [1.5, 2.5, 3.5, 4.5, 5.5]# np.concatenate([x_coords, y_coords], axis=0) 会生成一个形状为 (2, 5) 的数组:# [[1.0, 2.0, 3.0, 4.0, 5.0],# [1.5, 2.5, 3.5, 4.5, 5.5]]# .reshape(2, -1) 保持形状为 (2, 5) 。# .T 将数组转置为 (5, 2) :# [[1.0, 1.5],# [2.0, 2.5],# [3.0, 3.5],# [4.0, 4.5],# [5.0, 5.5]]# 最终得到的数组每一行表示一个点的坐标(x, y),这正是我们想要的结果。如果直接尝试重塑为 (5, 2) ,会引发错误,因为原始数组的形状是 (2, 5) ,而不是 (10, 1) 或 (5, 2) 。因此,先重塑为 (2, n) 再转置是必要的步骤。np.concatenate([np.interp(x, xp, s[:, i]) for i in range(2)], dtype=np.float32).reshape(2, -1).T # ⚠️ np.concatenate 按行合并后的形状已经是 (2, n) ,所以 .reshape(2, -1) 这一步实际上是多余的。直接使用 np.concatenate 后的结果即可,然后进行转置操作 .T 将其转换为 (n, 2) 的形状。) # segment xy# 返回重采样后的线段列表。return segments
# 这段代码的主要作用是对多边形线段进行重采样,使其具有指定数量的点。这种重采样在图像处理和计算机视觉任务中非常常见,特别是在处理多边形标注数据时,需要将多边形线段标准化为具有固定点数的线段,以便于后续处理。通过这种方式,可以方便地将多边形线段重采样为具有固定点数的线段,以便于数据的处理和模型的输入。这种转换特别适用于需要将多边形标注数据标准化的情况,例如在目标检测任务中。
23.def crop_mask(masks, boxes):
# 这段代码定义了一个名为 crop_mask 的函数,其作用是将掩码根据给定的边界框进行裁剪。
# 定义函数 crop_mask ,接收两个参数。
# 1.masks :形状为 [n, h, w] 的掩码张量。
# 2.boxes :形状为 [n, 4] 的边界框坐标张量,坐标以相对点形式给出。
def crop_mask(masks, boxes):# 它需要一个掩膜和一个边界框,并返回裁剪到边界框的掩膜。"""It takes a mask and a bounding box, and returns a mask that is cropped to the bounding box.Args:masks (torch.Tensor): [n, h, w] tensor of masksboxes (torch.Tensor): [n, 4] tensor of bbox coordinates in relative point formReturns:(torch.Tensor): The masks are being cropped to the bounding box."""# 获取掩码张量的形状,其中 h 和 w 分别表示掩码的高度和宽度。_, h, w = masks.shape# 将边界框坐标张量在第1维上分成4份,分别得到边界框的 左上角横坐标 x1 、 左上角纵坐标 y1 、 右下角横坐标 x2 、 右下角纵坐标 y2 ,它们的形状均为 (n,1,1) 。x1, y1, x2, y2 = torch.chunk(boxes[:, :, None], 4, 1) # x1 shape(n,1,1)# 生成一个从0到 w-1 的序列,表示 掩码的列索引 ,将其形状扩展为 (1,1,w) ,便于后续广播运算。r = torch.arange(w, device=masks.device, dtype=x1.dtype)[None, None, :] # rows shape(1,1,w)# 生成一个从0到 h-1 的序列,表示 掩码的行索引 ,将其形状扩展为 (1,h,1) ,便于后续广播运算。c = torch.arange(h, device=masks.device, dtype=x1.dtype)[None, :, None] # cols shape(1,h,1)# 通过广播运算,将掩码中在边界框范围内的部分保留,范围外的部分置为0,从而实现掩码的裁剪。# 这行代码是函数 crop_mask 的核心部分,其作用是根据边界框坐标对掩码进行裁剪。# r >= x1 :比较掩码的列索引 r 与边界框的左上角横坐标 x1 ,生成一个布尔张量,其形状与 r 相同,即 (n,1,w) 。当 r 中的元素大于等于 x1 中的对应元素时,结果为 True ,否则为 False 。这一步的目的是确定掩码中哪些列在边界框的左侧范围内。# r < x2 :比较掩码的列索引 r 与边界框的右下角横坐标 x2 ,生成一个布尔张量,其形状与 r 相同,即 (n,1,w) 。当 r 中的元素小于 x2 中的对应元素时,结果为 True ,否则为 False 。这一步的目的是确定掩码中哪些列在边界框的右侧范围内。# c >= y1 :比较掩码的行索引 c 与边界框的左上角纵坐标 y1 ,生成一个布尔张量,其形状与 c 相同,即 (n,h,1) 。当 c 中的元素大于等于 y1 中的对应元素时,结果为 True ,否则为 False 。这一步的目的是确定掩码中哪些行在边界框的上侧范围内。# c < y2 :比较掩码的行索引 c 与边界框的右下角纵坐标 y2 ,生成一个布尔张量,其形状与 c 相同,即 (n,h,1) 。当 c 中的元素小于 y2 中的对应元素时,结果为 True ,否则为 False 。这一步的目的是确定掩码中哪些行在边界框的下侧范围内。# (r >= x1) * (r < x2) * (c >= y1) * (c < y2) :将上述四个布尔张量进行逐元素相乘。由于布尔值 True 和 False 在相乘时分别相当于数值 1 和 0 ,所以只有当一个元素同时满足在边界框的左右上下范围内时,结果才为 True (即 1 ),否则为 False (即 0 )。最终得到一个形状为 (n,h,w) 的布尔张量,表示掩码中哪些元素在边界框内。# masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2)) :将原掩码 masks 与上述得到的布尔张量进行逐元素相乘。这样,掩码中在边界框范围内的元素保持不变,范围外的元素被置为 0 ,从而实现了掩码的裁剪。# 这行代码通过一系列的比较和相乘操作,巧妙地利用广播机制,实现了对掩码的高效裁剪,使得掩码仅保留边界框范围内的部分,而范围外的部分被清零。return masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2))
# 这段代码通过巧妙的广播运算,实现了对掩码的快速裁剪,其核心思想是利用掩码的行、列索引与边界框坐标进行比较,生成一个掩码裁剪的掩码,再与原掩码相乘得到裁剪后的掩码。这种方法避免了复杂的循环操作,提高了计算效率。
24.def process_mask_upsample(protos, masks_in, bboxes, shape):
# 这段代码定义了一个名为 process_mask_upsample 的函数,用于处理和上采样掩码(masks),并将其裁剪到指定的边界框(bboxes)内。最终返回二值化的掩码。
# 定义函数 process_mask_upsample ,接受四个参数。
# 1.protos :原型掩码,形状为 (c, mh, mw) ,其中 c 是通道数, mh 和 mw 分别是原型掩码的高度和宽度。
# 2.masks_in :输入掩码,形状为 (n, c) ,其中 n 是掩码的数量。
# 3.bboxes :边界框,形状为 (n, 4) ,每个边界框包含左上角和右下角坐标(x1, y1, x2, y2)。
# 4.shape :目标形状,一个包含高度和宽度的元组 (H, W) 。
def process_mask_upsample(protos, masks_in, bboxes, shape):# 获取掩码头的输出,并将掩码应用于边界框。这样可以生成更高质量的掩码,但速度较慢。# 参数:# protos (torch.Tensor):[mask_dim、mask_h、mask_w]# mask_in (torch.Tensor):[n、mask_dim],n 是 nms 之后的掩码数量# bboxes (torch.Tensor):[n, 4],n 是 nms 之后的掩码数量# shape (tuple):输入图像的大小 (h,w)# 返回:# (torch.Tensor):上采样的掩码。"""Takes the output of the mask head, and applies the mask to the bounding boxes. This produces masks of higher qualitybut is slower.Args:protos (torch.Tensor): [mask_dim, mask_h, mask_w]masks_in (torch.Tensor): [n, mask_dim], n is number of masks after nmsbboxes (torch.Tensor): [n, 4], n is number of masks after nmsshape (tuple): the size of the input image (h,w)Returns:(torch.Tensor): The upsampled masks."""# 获取原型掩码的形状,分别赋值给 c (通道数)、 mh (高度)和 mw (宽度)。c, mh, mw = protos.shape # CHW# protos.float().view(c, -1) :将原型掩码转换为浮点数,并重塑为 (c, mh * mw) 的形状。# masks_in @ protos.float().view(c, -1) :使用矩阵乘法将输入掩码 masks_in 与重塑后的原型掩码相乘,得到形状为 (n, mh * mw) 的掩码。# .sigmoid() :对结果应用sigmoid函数,将值映射到 (0, 1) 范围内。# .view(-1, mh, mw) :将结果重塑为 (n, mh, mw) 的形状。masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw)# masks[None] :在第一个维度添加一个维度,将形状从 (n, mh, mw) 变为 (1, n, mh, mw) ,以便使用PyTorch的 interpolate 函数。# F.interpolate(masks[None], shape, mode="bilinear", align_corners=False) :使用双线性插值将掩码上采样到目标形状 (H, W) 。# [0] :移除第一个维度,将形状从 (1, n, H, W) 变回 (n, H, W) 。masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[0] # CHW# 调用 crop_mask 函数,将上采样后的掩码裁剪到指定的边界框内。 crop_mask 函数的具体实现未在代码中给出,但其作用是根据边界框裁剪掩码。masks = crop_mask(masks, bboxes) # CHW# 使用 gt_ 方法将掩码二值化,阈值为0.5。 gt_ 方法是原地操作,直接修改 masks 数组,将大于0.5的值设为 True ,其余设为 False ,返回二值化的掩码。return masks.gt_(0.5)
# 这段代码的主要作用是处理和上采样掩码,并将其裁剪到指定的边界框内。矩阵乘法:将输入掩码与原型掩码相乘,生成初始掩码。Sigmoid激活:将初始掩码的值映射到 (0, 1) 范围内。上采样:使用双线性插值将掩码上采样到目标形状。裁剪:将上采样后的掩码裁剪到指定的边界框内。二值化:将裁剪后的掩码二值化,阈值为0.5。这种处理流程在目标检测和实例分割任务中非常常见,特别是在处理掩码数据时,需要将低分辨率的掩码上采样到高分辨率的图像尺寸,并根据边界框进行裁剪,以便于后续的处理和评估。
25.def process_mask(protos, masks_in, bboxes, shape, upsample=False):
# 这段代码定义了一个名为 process_mask 的函数,用于处理掩码(masks),包括将掩码裁剪到指定的边界框(bboxes)内,并根据需要进行上采样。这个函数比之前的 process_mask_upsample 函数更加通用,因为它提供了一个参数 upsample 来控制是否进行上采样。
# 定义函数 process_mask ,接受五个参数。
# 1.protos :原型掩码,形状为 (c, mh, mw) ,其中 c 是通道数, mh 和 mw 分别是原型掩码的高度和宽度。
# 2.masks_in :输入掩码,形状为 (n, c) ,其中 n 是掩码的数量。
# 3.bboxes :边界框,形状为 (n, 4) ,每个边界框包含左上角和右下角坐标(x1, y1, x2, y2)。
# 4.shape :目标形状,一个包含高度和宽度的元组 (H, W) 。
# 5.upsample :一个布尔值,表示是否进行上采样,默认值为 False 。
def process_mask(protos, masks_in, bboxes, shape, upsample=False):# 使用掩码头的输出将掩码应用于边界框。# 参数:# protos (torch.Tensor):形状为 [mask_dim, mask_h, mask_w] 的张量。# mask_in (torch.Tensor):形状为 [n, mask_dim] 的张量,其中 n 是 NMS 后的掩码数量。# bboxes (torch.Tensor):形状为 [n, 4] 的张量,其中 n 是 NMS 后的掩码数量。# shape (tuple):表示输入图像大小的整数元组,格式为 (h, w)。# upsample (bool):指示是否将掩码上采样为原始图像大小的标志。默认值为 False。# 返回:# (torch.Tensor):形状为 [n, h, w] 的二进制掩码张量,其中 n 是 NMS 后的掩码数量,h 和 w 是输入图像的高度和宽度。掩码应用于边界框。"""Apply masks to bounding boxes using the output of the mask head.Args:protos (torch.Tensor): A tensor of shape [mask_dim, mask_h, mask_w].masks_in (torch.Tensor): A tensor of shape [n, mask_dim], where n is the number of masks after NMS.bboxes (torch.Tensor): A tensor of shape [n, 4], where n is the number of masks after NMS.shape (tuple): A tuple of integers representing the size of the input image in the format (h, w).upsample (bool): A flag to indicate whether to upsample the mask to the original image size. Default is False.Returns:(torch.Tensor): A binary mask tensor of shape [n, h, w], where n is the number of masks after NMS, and h and ware the height and width of the input image. The mask is applied to the bounding boxes."""# 获取原型掩码的形状,分别赋值给 c (通道数)、 mh (高度)和 mw (宽度)。c, mh, mw = protos.shape # CHW# 获取目标形状的高度和宽度,分别赋值给 ih 和 iw 。ih, iw = shape# protos.float().view(c, -1) :将原型掩码转换为浮点数,并重塑为 (c, mh * mw) 的形状。# masks_in @ protos.float().view(c, -1) :使用矩阵乘法将输入掩码 masks_in 与重塑后的原型掩码相乘,得到形状为 (n, mh * mw) 的掩码。# .sigmoid() :对结果应用sigmoid函数,将值映射到 (0, 1) 范围内。# .view(-1, mh, mw) :将结果重塑为 (n, mh, mw) 的形状。masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw) # CHW# 计算宽度比例,用于将边界框坐标从目标形状转换为原型掩码的形状。width_ratio = mw / iw# 计算高度比例,用于将边界框坐标从目标形状转换为原型掩码的形状。height_ratio = mh / ih# 创建边界框的副本。downsampled_bboxes = bboxes.clone()# 将边界框的左上角x坐标乘以宽度比例。downsampled_bboxes[:, 0] *= width_ratio# 将边界框的右下角x坐标乘以宽度比例。downsampled_bboxes[:, 2] *= width_ratio# 将边界框的右下角y坐标乘以高度比例。downsampled_bboxes[:, 3] *= height_ratio# 将边界框的左上角y坐标乘以高度比例。downsampled_bboxes[:, 1] *= height_ratio# 调用 crop_mask 函数,将掩码裁剪到指定的边界框内。 crop_mask 函数作用是根据边界框裁剪掩码。masks = crop_mask(masks, downsampled_bboxes) # CHW# 如果 upsample 为 True ,则进行上采样。if upsample:# masks[None] :在第一个维度添加一个维度,将形状从 (n, mh, mw) 变为 (1, n, mh, mw) ,以便使用PyTorch的 interpolate 函数。# F.interpolate(masks[None], shape, mode="bilinear", align_corners=False) :使用双线性插值将掩码上采样到目标形状 (H, W) 。# [0] :移除第一个维度,将形状从 (1, n, H, W) 变回 (n, H, W) 。masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[0] # CHW# 使用 gt_ 方法将掩码二值化,阈值为0.5。 gt_ 方法是原地操作,直接修改 masks 数组,将大于0.5的值设为 True ,其余设为 False ,返回二值化的掩码。return masks.gt_(0.5)
# 这段代码的主要作用是处理掩码,包括将掩码裁剪到指定的边界框内,并根据需要进行上采样。矩阵乘法:将输入掩码与原型掩码相乘,生成初始掩码。Sigmoid激活:将初始掩码的值映射到 (0, 1) 范围内。边界框转换:将边界框坐标从目标形状转换为原型掩码的形状。裁剪:将初始掩码裁剪到指定的边界框内。上采样:如果需要,使用双线性插值将裁剪后的掩码上采样到目标形状。二值化:将上采样后的掩码二值化,阈值为0.5。这种处理流程在目标检测和实例分割任务中非常常见,特别是在处理掩码数据时,需要将低分辨率的掩码裁剪到高分辨率的图像尺寸,并根据边界框进行裁剪,以便于后续的处理和评估。通过提供 upsample 参数,这个函数可以灵活地控制是否进行上采样,增加了代码的通用性和灵活性。
26.def process_mask_native(protos, masks_in, bboxes, shape):
# 这段代码定义了一个名为 process_mask_native 的函数,用于处理掩码(masks),包括将掩码上采样到目标形状(shape),裁剪到指定的边界框(bboxes)内,并进行二值化。这个函数与之前的 process_mask 和 process_mask_upsample 函数类似,但使用了一个不同的上采样方法 scale_masks 。
# 定义函数 process_mask_native ,接受四个参数。
# 1.protos :原型掩码,形状为 (c, mh, mw) ,其中 c 是通道数, mh 和 mw 分别是原型掩码的高度和宽度。
# 2.masks_in :输入掩码,形状为 (n, c) ,其中 n 是掩码的数量。
# 3.bboxes :边界框,形状为 (n, 4) ,每个边界框包含左上角和右下角坐标(x1, y1, x2, y2)。
# 4.shape :目标形状,一个包含高度和宽度的元组 (H, W) 。
def process_mask_native(protos, masks_in, bboxes, shape):# 它获取 mask head 的输出,并在上采样到边界框后将其裁剪。# 参数:# protos (torch.Tensor): [mask_dim, mask_h, mask_w]# mask_in (torch.Tensor): [n, mask_dim],n 是 nms 之后的 mask 数量# bboxes (torch.Tensor): [n, 4],n 是 nms 之后的 mask 数量# shape (tuple): 输入图像的大小 (h,w)# 返回:# mask (torch.Tensor): 返回的 mask,尺寸为 [h, w, n]"""It takes the output of the mask head, and crops it after upsampling to the bounding boxes.Args:protos (torch.Tensor): [mask_dim, mask_h, mask_w]masks_in (torch.Tensor): [n, mask_dim], n is number of masks after nmsbboxes (torch.Tensor): [n, 4], n is number of masks after nmsshape (tuple): the size of the input image (h,w)Returns:masks (torch.Tensor): The returned masks with dimensions [h, w, n]"""# 获取原型掩码的形状,分别赋值给 c (通道数)、 mh (高度)和 mw (宽度)。c, mh, mw = protos.shape # CHW# protos.float().view(c, -1) :将原型掩码转换为浮点数,并重塑为 (c, mh * mw) 的形状。# masks_in @ protos.float().view(c, -1) :使用矩阵乘法将输入掩码 masks_in 与重塑后的原型掩码相乘,得到形状为 (n, mh * mw) 的掩码。# .sigmoid() :对结果应用sigmoid函数,将值映射到 (0, 1) 范围内。# .view(-1, mh, mw) :将结果重塑为 (n, mh, mw) 的形状。masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw)# masks[None] :在第一个维度添加一个维度,将形状从 (n, mh, mw) 变为 (1, n, mh, mw) ,以便使用 scale_masks 函数。# scale_masks(masks[None], shape) :调用 scale_masks 函数,将掩码上采样到目标形状 (H, W) 。 scale_masks 函数的作用是将掩码上采样到指定的形状。# [0] :移除第一个维度,将形状从 (1, n, H, W) 变回 (n, H, W) 。masks = scale_masks(masks[None], shape)[0] # CHW# 调用 crop_mask 函数,将上采样后的掩码裁剪到指定的边界框内。 crop_mask 函数的作用是根据边界框裁剪掩码。masks = crop_mask(masks, bboxes) # CHW# 使用 gt_ 方法将掩码二值化,阈值为0.5。 gt_ 方法是原地操作,直接修改 masks 数组,将大于0.5的值设为 True ,其余设为 False ,返回二值化的掩码。return masks.gt_(0.5)
# 这段代码的主要作用是处理掩码,包括将掩码上采样到目标形状,裁剪到指定的边界框内,并进行二值化。矩阵乘法:将输入掩码与原型掩码相乘,生成初始掩码。Sigmoid激活:将初始掩码的值映射到 (0, 1) 范围内。上采样:使用 scale_masks 函数将掩码上采样到目标形状 (H, W) 。裁剪:将上采样后的掩码裁剪到指定的边界框内。二值化:将裁剪后的掩码二值化,阈值为0.5。这种处理流程在目标检测和实例分割任务中非常常见,特别是在处理掩码数据时,需要将低分辨率的掩码上采样到高分辨率的图像尺寸,并根据边界框进行裁剪,以便于后续的处理和评估。通过使用 scale_masks 函数,这个函数提供了一种灵活的上采样方法,可以适应不同的上采样需求。
27.def scale_masks(masks, shape, padding=True):
# 这段代码定义了一个名为 scale_masks 的函数,用于将掩码(masks)从一个形状( mh, mw )缩放到另一个形状( shape ),同时考虑了是否进行填充(padding)。这个函数的主要作用是将掩码上采样到目标形状,同时保持掩码的中心对齐。
# 定义函数 scale_masks ,接受三个参数。
# 1.masks :输入掩码,形状为 (N, C, mh, mw) ,其中 N 是批量大小, C 是通道数, mh 和 mw 分别是掩码的高度和宽度。
# 2.shape :目标形状,一个包含高度和宽度的元组 (H, W) 。
# 3.padding :一个布尔值,表示是否进行填充,默认值为 True 。
def scale_masks(masks, shape, padding=True):# 将片段掩码重新缩放为形状。# 参数:# 掩码 (torch.Tensor):(N、C、H、W)。"""Rescale segment masks to shape.Args:masks (torch.Tensor): (N, C, H, W).shape (tuple): Height and width.padding (bool): If True, assuming the boxes is based on image augmented by yolo style. If False then do regularrescaling."""# 获取 输入掩码 的高度和宽度,分别赋值给 mh 和 mw 。mh, mw = masks.shape[2:]# 计算缩放比例,取高度和宽度缩放比例的最小值,确保掩码不会被拉伸变形。 gain 表示原始尺寸与目标尺寸的比例。gain = min(mh / shape[0], mw / shape[1]) # gain = old / new# 计算填充的宽度和高度,使得缩放后的掩码居中。 pad[0] 是宽度方向的填充, pad[1] 是高度方向的填充。pad = [mw - shape[1] * gain, mh - shape[0] * gain] # wh padding# 如果 padding 为 True ,则将填充值除以2,以便在两侧均匀填充。if padding:pad[0] /= 2pad[1] /= 2# 根据是否进行填充,计算顶部和左侧的填充值。如果 padding 为 True ,则使用计算的填充值;否则,不进行填充。top, left = (int(pad[1]), int(pad[0])) if padding else (0, 0) # y, x# 计算底部和右侧的边界位置。bottom, right = (int(mh - pad[1]), int(mw - pad[0]))# 根据计算出的边界,裁剪掩码,去除填充部分。masks = masks[..., top:bottom, left:right]# 使用双线性插值将裁剪后的掩码上采样到目标形状 (H, W) 。 F.interpolate 是PyTorch的插值函数, mode="bilinear" 表示使用双线性插值, align_corners=False 表示不对角点进行对齐。masks = F.interpolate(masks, shape, mode="bilinear", align_corners=False) # NCHW# 返回上采样后的掩码。return masks
# 这段代码的主要作用是将掩码从一个形状缩放到另一个形状,同时考虑了是否进行填充。计算缩放比例:计算高度和宽度的缩放比例,取最小值以保持掩码的长宽比。计算填充值:计算宽度和高度方向的填充值,确保缩放后的掩码居中。裁剪掩码:根据是否进行填充,裁剪掩码以去除多余的填充部分。上采样:使用双线性插值将裁剪后的掩码上采样到目标形状。这种处理流程在目标检测和实例分割任务中非常常见,特别是在处理掩码数据时,需要将低分辨率的掩码上采样到高分辨率的图像尺寸,并保持掩码的中心对齐,以便于后续的处理和评估。通过提供 padding 参数,这个函数可以灵活地控制是否进行填充,增加了代码的通用性和灵活性。
28.def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None, normalize=False, padding=True):
# 这段代码定义了一个名为 scale_coords 的函数,用于将坐标从一个图像形状( img1_shape )缩放到另一个图像形状( img0_shape ),同时考虑了是否进行填充(padding)和是否进行归一化(normalize)。这个函数的主要作用是将坐标从处理后的图像形状转换回原始图像形状。
# 定义函数 scale_coords ,接受六个参数。
# 1.img1_shape :处理后的图像形状,一个包含高度和宽度的元组 (H1, W1) 。
# 2.coords :需要转换的坐标,形状为 (n, 2) ,其中 n 是坐标点的数量,每个坐标点包含 (x, y) 。
# 3.img0_shape :原始图像形状,一个包含高度和宽度的元组 (H0, W0) 。
# 4.ratio_pad :一个可选参数,包含缩放比例和填充信息。如果为 None ,则从 img0_shape 计算。
# 5.normalize :一个布尔值,表示是否将坐标归一化到 [0, 1] 范围内,默认值为 False 。
# 6.padding :一个布尔值,表示是否考虑填充,默认值为 True 。
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None, normalize=False, padding=True):# 将片段坐标 (xy) 从 img1_shape 重新缩放为 img0_shape。"""Rescale segment coordinates (xy) from img1_shape to img0_shape.Args:img1_shape (tuple): The shape of the image that the coords are from.coords (torch.Tensor): the coords to be scaled of shape n,2.img0_shape (tuple): the shape of the image that the segmentation is being applied to.ratio_pad (tuple): the ratio of the image size to the padded image size.normalize (bool): If True, the coordinates will be normalized to the range [0, 1]. Defaults to False.padding (bool): If True, assuming the boxes is based on image augmented by yolo style. If False then do regularrescaling.Returns:coords (torch.Tensor): The scaled coordinates."""# 如果未提供 ratio_pad ,则从 img0_shape 计算缩放比例和填充。if ratio_pad is None: # calculate from img0_shape# 计算缩放比例,取高度和宽度缩放比例的最小值,确保图像不会被拉伸变形。gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new# 计算宽度和高度方向的填充,使得缩放后的图像居中。pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding# 如果提供了 ratio_pad ,则直接使用其中的缩放比例和填充信息。else:# 获取缩放比例。gain = ratio_pad[0][0]# 获取填充信息。pad = ratio_pad[1]# 如果 padding 为 True ,则从坐标中减去填充值。if padding:# 从x坐标中减去宽度方向的填充。coords[..., 0] -= pad[0] # x padding# 从y坐标中减去高度方向的填充。coords[..., 1] -= pad[1] # y padding# 将x坐标除以缩放比例。coords[..., 0] /= gain# 将y坐标除以缩放比例。coords[..., 1] /= gain# 调用 clip_coords 函数,将坐标限制在原始图像的边界内。 clip_coords 函数的作用是确保坐标不会超出图像的边界。coords = clip_coords(coords, img0_shape)# 如果 normalize 为 True ,则将坐标归一化到 [0, 1] 范围内。if normalize:# 将x坐标归一化到 [0, 1] 范围内。coords[..., 0] /= img0_shape[1] # width# 将y坐标归一化到 [0, 1] 范围内。coords[..., 1] /= img0_shape[0] # height# 返回转换后的坐标。return coords
# 这段代码的主要作用是将坐标从一个图像形状转换到另一个图像形状,同时考虑了是否进行填充和是否进行归一化。计算缩放比例和填充:如果未提供 ratio_pad ,则从 img0_shape 计算缩放比例和填充;否则,直接使用提供的 ratio_pad 。去除填充:如果 padding 为 True ,则从坐标中减去填充值。缩放坐标:将坐标除以缩放比例,转换到原始图像的尺寸。裁剪坐标:将坐标限制在原始图像的边界内。归一化坐标:如果 normalize 为 True ,则将坐标归一化到 [0, 1] 范围内。这种处理流程在图像处理和计算机视觉任务中非常常见,特别是在处理图像缩放和坐标转换时,需要将处理后的坐标转换回原始图像的尺寸,以便于后续的处理和评估。通过提供 padding 和 normalize 参数,这个函数可以灵活地控制是否进行填充和归一化,增加了代码的通用性和灵活性。
29.def regularize_rboxes(rboxes):
# 这段代码定义了一个名为 regularize_rboxes 的函数,用于规范化旋转矩形框(rboxes),确保矩形框的宽度总是大于或等于高度,并且角度在 [0, π) 范围内。这种规范化有助于统一表示旋转矩形框,便于后续处理。
# 定义函数 regularize_rboxes ,接受一个参数。
# 1.rboxes :一个二维数组,其中每一行包含一个旋转矩形框的参数(x, y, w, h, t),分别表示中心点坐标(x, y)、宽度(w)、高度(h)和旋转角度(t,单位为弧度)。
def regularize_rboxes(rboxes):# 在 [0, pi/2] 范围内对旋转框进行正则化。# 参数:# rboxes (torch.Tensor):(N, 5),xywhr。"""Regularize rotated boxes in range [0, pi/2].Args:rboxes (torch.Tensor): (N, 5), xywhr.Returns:(torch.Tensor): The regularized boxes."""# torch.unbind(input, dim=None) -> Sequence[Tensor]# torch.unbind 是 PyTorch 中的一个函数,用于将一个多维张量(tensor)分解为多个张量。这个函数通常用于处理由 torch.cat (张量拼接)产生的结果,或者当你有一个多维张量并希望将其分解为多个子张量时。# 参数 :# input :要解绑的多维张量。# dim :要解绑的维度。默认为 None ,如果不指定, torch.unbind 会将输入张量分解为一维张量。# 返回值 :# 返回一个张量的序列(sequence),这些张量是输入张量沿 dim 维度解绑后的结果。# 功能 :# torch.unbind 函数沿着指定的维度将输入张量分解为多个张量。如果输入张量是一维的,那么 dim 参数可以省略, unbind 会将其分解为单个元素的张量。# 使用 unbind 方法将 rboxes 分解为五个一维数组,分别表示中心点坐标(x, y)、宽度(w)、高度(h)和旋转角度(t)。x, y, w, h, t = rboxes.unbind(dim=-1)# Swap edge and angle if h >= w 如果高度大于或等于宽度,则交换宽度和高度,并调整角度。# 如果宽度大于高度,则保持宽度不变;否则,将高度赋值给宽度。w_ = torch.where(w > h, w, h)# 如果宽度大于高度,则保持高度不变;否则,将宽度赋值给高度。h_ = torch.where(w > h, h, w)# 如果宽度大于高度,则保持角度不变;否则,将角度增加 π/2 并取模 π ,确保角度在 [0, π) 范围内。t = torch.where(w > h, t, t + math.pi / 2) % math.pi# 使用 torch.stack 方法将规范化后的参数重新组合成一个二维数组,每一行表示一个规范化后的旋转矩形框的参数(x, y, w*, h*, t)。return torch.stack([x, y, w_, h_, t], dim=-1) # regularized boxes
# 这段代码的主要作用是规范化旋转矩形框,确保宽度总是大于或等于高度,并且角度在 [0, π) 范围内。分解参数:将输入的旋转矩形框参数分解为中心点坐标(x, y)、宽度(w)、高度(h)和旋转角度(t)。交换宽度和高度:如果高度大于或等于宽度,则交换宽度和高度。调整角度:如果高度大于或等于宽度,则将角度增加 π/2 并取模 π ,确保角度在 [0, π) 范围内。重新组合参数:将规范化后的参数重新组合成一个二维数组,每一行表示一个规范化后的旋转矩形框的参数。这种规范化处理在目标检测和实例分割任务中非常常见,特别是在处理旋转矩形框时,需要统一表示形式,以便于后续的处理和评估。通过这种规范化,可以确保旋转矩形框的表示形式一致,便于模型的训练和推理。
30.def masks2segments(masks, strategy="largest"):
# 这段代码定义了一个名为 masks2segments 的函数,用于将二值掩码(masks)转换为多边形线段(segments)。这个函数支持两种策略 : concat (将所有线段连接起来)和 largest (选择最大的线段)。
# 定义函数 masks2segments ,接受两个参数。
# 1.masks :输入的二值掩码,形状为 (n, H, W) ,其中 n 是掩码的数量, H 和 W 分别是掩码的高度和宽度。
# 2.strategy :一个字符串,表示处理多边形线段的策略,默认值为 "largest" ,支持 "concat" 和 "largest" 两种策略。
def masks2segments(masks, strategy="largest"):# 它接受一个 mask(n,h,w) 列表并返回一个 fragment(n,xy) 列表# 参数:# mask (torch.Tensor):模型的输出,形状为 (batch_size, 160, 160) 的张量"""It takes a list of masks(n,h,w) and returns a list of segments(n,xy)Args:masks (torch.Tensor): the output of the model, which is a tensor of shape (batch_size, 160, 160)strategy (str): 'concat' or 'largest'. Defaults to largestReturns:segments (List): list of segment masks"""# 初始化一个空列表 segments ,用于存储转 换后的多边形线段 。segments = []# 遍历每个掩码。将掩码转换为整数类型,移动到CPU,转换为NumPy数组,并确保数据类型为 uint8 。for x in masks.int().cpu().numpy().astype("uint8"):# cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])# cv2.findContours 是 OpenCV 中的一个函数,用于在二值图像中查找轮廓。# 参数:# image :输入图像,通常是一个二值图像。# mode :轮廓检索模式。它可以是以下值之一:# cv2.RETR_EXTERNAL :只检索最外层的轮廓。# cv2.RETR_LIST :检索所有轮廓,并以列表形式返回。# cv2.RETR_CCOMP :检索所有轮廓,并以树状结构(contour hierarchy)形式返回。此时,轮廓被分为不同的层级。# cv2.RETR_TREE :检索所有轮廓,并以完整的树状结构形式返回。# method :轮廓近似方法。它可以是以下值之一:# cv2.CHAIN_APPROX_NONE :存储轮廓上的所有点。# cv2.CHAIN_APPROX_SIMPLE :压缩水平、垂直和对角方向的轮廓点。# cv2.CHAIN_APPROX_TC89_L1 和 cv2.CHAIN_APPROX_TC89_KCOS :使用L1和KOS链逼近算法。# contours (可选) :输出参数,返回检测到的轮廓。# hierarchy (可选) :输出参数,返回轮廓的层次结构。它是一个多通道多维数组,其中每个轮廓由三个数组组成:[next, previous, first_contour]。其中,“ next ”是下一个轮廓的索引,“ previous ”是上一个轮廓的索引,“ first_contour ”是起始轮廓的索引。# offset (可选) :偏移量,指定从哪里开始搜索轮廓。例如,如果指定了(10, 10),则从图像的(10, 10)位置开始搜索轮廓。# 返回值 :# 如果指定了 contours 参数,则此函数返回被检测到的第一个轮廓的索引;否则,不返回任何内容。# 使用OpenCV的 findContours 函数查找掩码中的轮廓。 cv2.RETR_EXTERNAL 表示只检测最外层的轮廓, cv2.CHAIN_APPROX_SIMPLE 表示使用简单的轮廓近似方法。 [0] 表示获取轮廓列表。c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]# 如果找到了轮廓。if c:# 如果策略是 "concat" ,则将所有轮廓连接起来。if strategy == "concat": # concatenate all segments# 将每个轮廓重塑为 (n, 2) 的形状,并连接成一个大的数组。c = np.concatenate([x.reshape(-1, 2) for x in c])# 如果策略是 "largest" ,则选择最大的轮廓。elif strategy == "largest": # select largest segment# 计算每个轮廓的长度,选择最长的轮廓,并将其重塑为 (n, 2) 的形状。c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2)# 如果没有找到轮廓。else:# 创建一个空的多边形线段,形状为 (0, 2) 。c = np.zeros((0, 2)) # no segments found# 将处理后的多边形线段添加到 segments 列表中,并确保数据类型为 float32 。segments.append(c.astype("float32"))# 返回转换后的多边形线段列表。return segments
# 这段代码的主要作用是将二值掩码转换为多边形线段,支持两种策略: concat (将所有线段连接起来)和 largest (选择最大的线段)。遍历每个掩码:将每个掩码转换为 uint8 类型,使用OpenCV的 findContours 函数查找轮廓。处理轮廓:如果策略是 "concat" ,则将所有轮廓连接起来。如果策略是 "largest" ,则选择最大的轮廓。处理未找到轮廓的情况:如果没有找到轮廓,创建一个空的多边形线段。存储结果:将处理后的多边形线段添加到列表中,并确保数据类型为 float32 。返回结果:返回转换后的多边形线段列表。这种处理流程在目标检测和实例分割任务中非常常见,特别是在处理二值掩码时,需要将掩码转换为多边形线段,以便于后续的处理和评估。通过提供不同的策略,这个函数可以灵活地处理不同的情况,增加了代码的通用性和灵活性。
31.def convert_torch2numpy_batch(batch: torch.Tensor) -> np.ndarray:
# 这段代码定义了一个名为 convert_torch2numpy_batch 的函数,用于将PyTorch张量批量转换为NumPy数组。这个函数特别适用于将图像数据从PyTorch张量格式转换为NumPy数组格式,通常用于图像处理和可视化。
# 定义函数 convert_torch2numpy_batch ,接受一个参数。返回一个NumPy数组。
# 1.batch :这是一个PyTorch张量。
def convert_torch2numpy_batch(batch: torch.Tensor) -> np.ndarray:# 将一批 FP32 torch 张量 (0.0-1.0) 转换为 NumPy uint8 数组 (0-255),从 BCHW 更改为 BHWC 布局。# 参数:# batch (torch.Tensor):输入张量批次,形状为 (Batch、Channels、Height、Width),数据类型为 torch.float32。# 返回:# (np.ndarray):输出 NumPy 数组批次,形状为 (Batch、Height、Width、Channels),数据类型为 uint8。"""Convert a batch of FP32 torch tensors (0.0-1.0) to a NumPy uint8 array (0-255), changing from BCHW to BHWC layout.Args:batch (torch.Tensor): Input tensor batch of shape (Batch, Channels, Height, Width) and dtype torch.float32.Returns:(np.ndarray): Output NumPy array batch of shape (Batch, Height, Width, Channels) and dtype uint8."""# 这一行代码完成了多个操作,具体如下 :# batch.permute(0, 2, 3, 1) :将张量的维度从 (N, C, H, W) 重新排列为 (N, H, W, C) ,其中 N 是批量大小, C 是通道数, H 是高度, W 是宽度。这一步是为了将通道维度移动到最后一维,以便于转换为NumPy数组。# .contiguous() :确保张量在内存中是连续的,这是 permute 操作后可能需要的,因为 permute 操作可能会导致张量在内存中不连续。# * 255 :将张量的值乘以255,将归一化后的浮点数转换为0到255范围内的整数,以便于转换为8位无符号整数。# .clamp(0, 255) :将张量的值限制在0到255范围内,确保值不会超出8位无符号整数的范围。# .to(torch.uint8) :将张量的数据类型转换为8位无符号整数( uint8 )。# .cpu() :将张量移动到CPU内存中,因为NumPy数组只能在CPU上操作。# .numpy() :将张量转换为NumPy数组。return (batch.permute(0, 2, 3, 1).contiguous() * 255).clamp(0, 255).to(torch.uint8).cpu().numpy()
# 这段代码的主要作用是将PyTorch张量批量转换为NumPy数组,特别适用于图像数据。维度重排:将张量的维度从 (N, C, H, W) 重新排列为 (N, H, W, C) 。确保连续性:确保张量在内存中是连续的。值转换:将归一化后的浮点数转换为0到255范围内的整数。值限制:将值限制在0到255范围内。数据类型转换:将张量的数据类型转换为8位无符号整数( uint8 )。移动到CPU:将张量移动到CPU内存中。转换为NumPy数组:将张量转换为NumPy数组。这种转换在图像处理和可视化任务中非常常见,特别是在将PyTorch模型的输出转换为可以用于显示或进一步处理的格式时。通过这些步骤,可以确保转换后的数组格式正确,值范围合适,便于后续处理。
32.def clean_str(s):
# 这段代码定义了一个名为 clean_str 的函数,用于清理字符串中的特殊字符,将这些特殊字符替换为下划线( _ )。这个函数特别适用于处理文件名、标签或其他需要去除特殊字符的字符串。
# 定义函数 clean_str ,接受一个参数。
# 1.s :这是一个字符串。
def clean_str(s):# 通过使用下划线 _ 替换特殊字符来清理字符串"""Cleans a string by replacing special characters with underscore _Args:s (str): a string needing special characters replacedReturns:(str): a string with special characters replaced by an underscore _"""# 使用 re.sub 函数进行字符串替换。# pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]" :定义一个正则表达式模式,匹配字符串中的一系列特殊字符。这些字符包括: | 、 @ 、 # 、 ! 、 ¡ 、 · 、 $ 、 € 、 % 、 & 、 ( 、 ) 、 = 、 ? 、 ¿ 、 ^ 、 * 、 ; 、 : 、 , 、 ¨ 、 ´ 、 > 、 < 、 + 。# repl="_" :将匹配到的特殊字符替换为下划线( _ )。# string=s :指定要处理的字符串 s 。return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
# 这段代码的主要作用是清理字符串中的特殊字符,将这些特殊字符替换为下划线( _ )。定义正则表达式模式:匹配字符串中的一系列特殊字符。使用 re.sub 进行替换:将匹配到的特殊字符替换为下划线( _ )。返回处理后的字符串:返回清理后的字符串。这种处理在处理文件名、标签或其他需要去除特殊字符的字符串时非常常见,特别是在处理用户输入或从外部源获取的字符串时。通过这种清理,可以确保字符串格式正确,避免在文件系统或数据库中出现错误。例如,文件名中通常不允许包含某些特殊字符,通过 clean_str 函数可以将这些字符替换为下划线,生成合法的文件名。
33.def v10postprocess(preds, max_det, nc=80):
# 这段代码定义了一个名为 v10postprocess 的函数,用于对目标检测模型的预测结果进行后处理。这个函数的主要作用是提取预测框(boxes)、置信度(scores)和类别标签(labels),并根据最大检测数(max_det)进行筛选。
# 定义函数 v10postprocess ,接受三个参数。
# 1.preds :模型的预测结果,形状为 (N, M, 4 + nc) ,其中 N 是批量大小, M 是预测框的数量, 4 是边界框的坐标(x, y, w, h), nc 是类别数。
# 2.max_det :最大检测数,即每个图像中保留的预测框的最大数量。
# 3.nc :类别数,默认值为80。
def v10postprocess(preds, max_det, nc=80):# 断言预测结果的最后一个维度的长度为 4 + nc ,确保输入格式正确。assert(4 + nc == preds.shape[-1])# 将预测结果分解为 边界框 (boxes)和 置信度 (scores)。# boxes :形状为 (N, M, 4) ,表示 预测框的坐标 。# scores :形状为 (N, M, nc) ,表示 每个预测框的类别置信度 。boxes, scores = preds.split([4, nc], dim=-1)# 计算 每个预测框的最大置信度 ,形状为 (N, M) 。max_scores = scores.amax(dim=-1)# 获取每个图像中置信度最高的 max_det 个 预测框的置信度 和 索引 。# max_scores :形状为 (N, max_det) ,表示 每个图像中置信度最高的 max_det 个预测框的置信度 。# index :形状为 (N, max_det) ,表示 这些预测框的索引 。max_scores, index = torch.topk(max_scores, max_det, axis=-1)# 在索引的最后一个维度添加一个维度,形状变为 (N, max_det, 1) 。index = index.unsqueeze(-1)# 根据索引提取 对应的预测框 。# index.repeat(1, 1, boxes.shape[-1]) :将 索引 重复扩展到与 boxes 的最后一个维度相同,形状变为 (N, max_det, 4) 。# torch.gather :根据索引 提取对应的预测框 ,形状为 (N, max_det, 4) 。boxes = torch.gather(boxes, dim=1, index=index.repeat(1, 1, boxes.shape[-1]))# 根据索引提取 对应的置信度 。# index.repeat(1, 1, scores.shape[-1]) :将 索引 重复扩展到与 scores 的最后一个维度相同,形状变为 (N, max_det, nc) 。# torch.gather :根据索引 提取对应的置信度 ,形状为 (N, max_det, nc) 。scores = torch.gather(scores, dim=1, index=index.repeat(1, 1, scores.shape[-1]))# 将置信度展平并获取每个图像中置信度最高的 max_det 个预测框的置信度和索引。# scores.flatten(1) :将 scores 从 (N, max_det, nc) 展平为 (N, max_det * nc) 。# torch.topk :获取每个图像中置信度最高的 max_det 个预测框的置信度和索引。# scores :形状为 (N, max_det) ,表示 每个图像中置信度最高的 max_det 个预测框的置信度。# index :形状为 (N, max_det) ,表示这些预测框的索引。scores, index = torch.topk(scores.flatten(1), max_det, axis=-1)# 计算 类别标签 ,形状为 (N, max_det) 。 index % nc :通过取模操作获取 类别标签 。labels = index % nc# 计算 预测框的索引 ,形状为 (N, max_det) 。 index // nc :通过整除操作获取 预测框的索引 。index = index // nc# 根据索引提取 最终的预测框 。# index.unsqueeze(-1).repeat(1, 1, boxes.shape[-1]) :将索引重复扩展到与 boxes 的最后一个维度相同,形状变为 (N, max_det, 4) 。# boxes.gather :根据索引提取最终的预测框,形状为 (N, max_det, 4) 。boxes = boxes.gather(dim=1, index=index.unsqueeze(-1).repeat(1, 1, boxes.shape[-1]))# 返回处理后的 预测框 、 置信度 和 类别标签 。return boxes, scores, labels
# 这段代码的主要作用是对目标检测模型的预测结果进行后处理,提取预测框、置信度和类别标签,并根据最大检测数进行筛选。分解预测结果:将预测结果分解为边界框和置信度。计算最大置信度:计算每个预测框的最大置信度。筛选置信度最高的预测框:根据最大检测数筛选置信度最高的预测框。提取预测框和置信度:根据索引提取对应的预测框和置信度。展平并再次筛选:将置信度展平并再次筛选置信度最高的预测框。计算类别标签:通过取模操作计算类别标签。提取最终预测框:根据索引提取最终的预测框。返回结果:返回处理后的预测框、置信度和类别标签。这种后处理流程在目标检测任务中非常常见,特别是在处理模型输出时,需要将预测结果转换为易于理解和使用的格式。通过这些步骤,可以确保最终的预测结果格式正确,便于后续的处理和评估。# 在 v10postprocess 函数中,两次 torch.topk 操作分别起到了不同的作用。下面详细解释这两次操作的具体作用 :
# 第一次 torch.topk 操作 : max_scores, index = torch.topk(max_scores, max_det, axis=-1)
# 作用 :
# 筛选置信度最高的预测框:
# max_scores :计算每个预测框的最大置信度,形状为 (N, M) 。
# torch.topk(max_scores, max_det, axis=-1) :从每个图像的预测框中选择置信度最高的 max_det 个预测框的置信度和索引。
# max_scores :形状为 (N, max_det) ,表示每个图像中置信度最高的 max_det 个预测框的置信度。
# index :形状为 (N, max_det) ,表示这些预测框的索引。
# 第二次 torch.topk 操作 : scores, index = torch.topk(scores.flatten(1), max_det, axis=-1)
# 作用 :
# 进一步筛选置信度最高的预测框 :
# scores :经过第一次筛选后的预测框的置信度,形状为 (N, max_det, nc) 。
# scores.flatten(1) :将 scores 从 (N, max_det, nc) 展平为 (N, max_det * nc) 。
# torch.topk(scores.flatten(1), max_det, axis=-1) :从展平后的置信度中选择每个图像中置信度最高的 max_det 个预测框的置信度和索引。
# scores :形状为 (N, max_det) ,表示每个图像中置信度最高的 max_det 个预测框的置信度。
# index :形状为 (N, max_det) ,表示这些预测框的索引。
# 总结 :
# 第一次 torch.topk 操作 :
# 从每个图像的预测框中选择置信度最高的 max_det 个预测框。
# 作用是初步筛选出每个图像中置信度最高的预测框,减少后续处理的计算量。
# 第二次 torch.topk 操作 :
# 从初步筛选后的预测框中进一步选择置信度最高的 max_det 个预测框。
# 作用是确保最终保留的预测框是置信度最高的,进一步提高结果的准确性。
# 通过这两次 torch.topk 操作,函数能够有效地筛选出每个图像中置信度最高的预测框,确保最终的预测结果既准确又高效。# 在 v10postprocess 函数中,将 scores 从形状 (N, max_det, nc) 展平为 (N, max_det * nc) 是为了在所有类别中选择置信度最高的 max_det 个预测框。具体来说,这一步操作的目的是将每个预测框的所有类别置信度展平到一个一维数组中,以便可以一次性选择出置信度最高的 max_det 个预测框,而不仅仅是每个预测框的最高置信度类别。
# 展平操作 :
# scores.flatten(1) 将 scores 从形状 (N, max_det, nc) 展平为 (N, max_det * nc) 。这一步操作的目的是将每个预测框的所有类别置信度合并到一个一维数组中,以便可以一次性选择出置信度最高的 max_det 个预测框。
# 例如,假设 max_det 为10, nc 为80,那么每个图像的 scores 形状为 (10, 80) ,展平后变为 (800) ,这样可以一次性选择出800个置信度中的前 max_det 个最高值。
# 选择最高置信度 :
# torch.topk(scores.flatten(1), max_det, axis=-1) 从展平后的 scores 中选择每个图像中置信度最高的 max_det 个预测框的置信度和索引。 这样可以确保最终保留的预测框是所有类别中置信度最高的,而不仅仅是每个预测框的最高置信度类别。
# 为什么需要展平?
# 多类别处理 :如果不展平, torch.topk 只能在每个预测框的 nc 个类别中选择最高置信度,而无法在所有预测框的所有类别中选择最高置信度。
# 全局选择 :展平后, torch.topk 可以在所有预测框的所有类别中选择最高置信度的 max_det 个预测框,这样可以确保最终结果的全局最优性。