【YOLOv3】 源码(common.py)

news/2024/12/24 13:17:00/

概述

该文件中提供了构建yolov3模型的各种基础模块,其中包含了常用的功能模块,如标准卷积层、瓶颈层、空间金字塔池化层、图像预处理和后处理工具等,这些都是构建高效和模块化模型的基本

该文件的作用类似于一栋建筑的建筑材料和工具,首先对其主要模块整体认识和理解,然后再针对每个函数进行详细分析

  • 自动填充(autopad函数)
    • 施工团队根据建筑设计自动计算需要的材料填充量,确保结构稳定(主要就是保证输入和输出保持一致,放在详细分析中)
  • 标准卷积层(Conv类)
    • 提供标准的建筑构件(如梁、柱),确保一致性和模块化
  • 瓶颈层(Bottleneck类)
    • 提供用于增强建筑结构的模块,提升整体稳定性和强度
  • 空间金字塔池化层(SPP类)
    • 通过不同尺寸的池化操作,增强建筑的空间结构和功能性
  • Focus 层
    • 将建筑的不同部分信息压缩到一个统一的空间,便于进一步处理
  • Contract 层
    • 将建筑的宽高信息紧缩到通道空间,提升建筑的密度和稳定性
  • Expand 层
    • 将建筑的通道信息扩展到宽高空间,提升建筑的扩展性
  • Concat 层
    • 将多个建筑模块合并到一个统一的结构中,确保整体协调性
  • DetectMultiBackend 类
    • 支持在不同施工后端(如不同设备或框架)上进行模型推理,确保建筑适应不同的施工环境
  • AutoShape 类
    • 处理各种输入格式,进行预处理、推理和后处理,确保建筑模块能够适应不同的输入和输出需求
  • Detections 类
    • 管理和存储检测结果,包括图像、预测框、文件名等信息,并提供结果的可视化和保存功能
  • Classify 类
    • 用于将输入特征图分类到不同的类别,类似于建筑中的分类和分区功能

详细分析

自动填充(autopad函数)

根据卷积核大小K自动计算需要填充的步幅p,最终的目的就是保证输入和输出尺寸一致;主要服务于Conv类和Classify类中

  • 如果k是整形,那么p直接k//2取整k即可
  • 如果k不是整形,那么k就会被设置成一个列表,该列表的每个元素都是k中对应元素的一半
def autopad(k, p=None):  # kernel, padding"""用于 Conv 函数和 Classify 函数中,为 'same' 卷积或 'same' 池化作自动扩充(0 填充)。该函数根据卷积核的大小 k 自动计算所需的填充量(padding),从而实现 'same' 卷积或池化操作,保持输出特征图的大小与输入特征图一致。具体来说:- 'same' 卷积或池化意味着输出的空间维度(高度和宽度)与输入相同。- 该函数自动计算填充大小,以确保卷积操作在输入图像的边缘不会丢失太多信息。:params k: 卷积核的大小(kernel_size),可以是单一整数(例如 3),也可以是列表或元组(例如 [3, 3])。:params p: 手动指定的填充大小。如果为 `None`,则由函数自动计算。:return p: 返回自动计算的需要填充的数量(0 填充),如果 `p` 为 `None`,则返回填充值。"""# 如果没有手动指定填充(p 为 None),则根据卷积核大小 k 自动计算填充大小。if p is None:# 如果卷积核是一个整数,则计算填充大小为卷积核大小的半数(向下取整)。# 例如,对于 3x3 的卷积,填充大小应为 1,以保持输出特征图的大小与输入一致。p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad# 如果 k 是一个列表或元组(例如 [3, 3]),则分别计算每个维度的填充大小。# 对于每个维度(高度和宽度),填充大小是对应卷积核尺寸的一半。return p  # 返回计算得到的填充大小

yolov3中主要就有两种卷积

  • 下采样卷积
    • 该卷积的主要目的就是减少输入的空间维度,也就是减少宽度和高度,一般常用的就是3*3卷积和步长为2的配合
    • 在该情况下如果想要保持输出大小和输入大小相同,那么卷积操作就要加上适当的填充,也就是p = k//2 = 1
  • 保持特征图不变的卷积
    • 这类卷积不会改变输入特征图的空间维度,比如,使用一个 1x1 的卷积核并设置步长为 1,输出的空间维度与输入保持一致
    • 同样,为了确保输出尺寸不变,卷积核也需要合适的填充量,p = k//2=1,这里的K也就是卷积核的大小

标准卷积层(Conv类)

该类类似于提供了建筑中所需要的标准化构件,从而确保建筑物的一致性和模块化

Conv类实现了一个标准的卷积层,包括卷积操作、批归一化(BN)和激活函数(默认为SiLU/Swish)。通过参数调整,可以灵活控制卷积核大小、步幅、填充、分组数以及激活函数类型,从而满足不同层次和需求的特征提取

  • 3*3卷积:Conv(c1,c2,k=3)
class Conv(nn.Module):# Standard convolutiondef __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups"""Standard convolution  conv+BN+act:params c1: 输入的channel值:params c2: 输出的channel值:params k: 卷积的kernel_size:params s: 卷积的stride:params p: 卷积的padding  一般是None  可以通过autopad自行计算需要pad的padding数:params g: 卷积的groups数  =1就是普通的卷积  >1就是深度可分离卷积,也就是分组卷积:params act: 激活函数类型   True就是SiLU()/Swish   False就是不使用激活函数类型是nn.Module就使用传进来的激活函数类型"""super().__init__()self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)  # 定义卷积层self.bn = nn.BatchNorm2d(c2)  # 定义批归一化层# Todo 修改激活函数# self.act = nn.Identity() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())# self.act = nn.Tanh() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())# self.act = nn.Sigmoid() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())# self.act = nn.ReLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())# self.act = nn.LeakyReLU(0.1) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())# self.act = nn.Hardswish() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())  # 定义激活函数def forward(self, x):# 模型的前向传播return self.act(self.bn(self.conv(x)))  # 卷积 -> 批归一化 -> 激活函数def forward_fuse(self, x):"""用于Model类的fuse函数前向融合conv+bn计算 加速推理 一般用于测试/验证阶段"""return self.act(self.conv(x))  # 卷积 -> 激活函数(已融合BN)

注意两种前向传播

forward在训练过程中使用,整体流程则是标准卷积、批量归一化、激活函数

forward_fuse则是在加速推理阶段操作,主要融合的卷积和归一化的操作,此时在推理的时候不需要单独计算归一化,而是将其与卷积层融合(即合并权重),减少计算量。通常这用于推理阶段,提升效率

分析:此处为什么不像搭建深度学习网络中有后向传播?

后向传播是自动的,yTorch 使用 Autograd 自动生成计算图,在调用.backwark()的时候,框架会自动计算梯度,所以论是卷积操作,还是批归一化、激活函数,都会自动计算其梯度

只有在自己有特殊需求的时候才需要手动实现后向传播

瓶颈层(Bottleneck类)

该类包括两个卷积层和一个可选的shortcut连接,如果shortcut为 True 且输入和输出通道数相同,则在输出中添加输入,实现残差连接。这样可以帮助模型在深层网络中缓解梯度消失问题,提高训练稳定性和模型性能

class Bottleneck(nn.Module):# Standard bottleneck block used in YOLOv3def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  """YOLOv3 模型中的标准瓶颈模块,由两个卷积层和一个可选的 shortcut(跳跃连接)组成。该模块的结构是 Conv + Conv + shortcut,常用于提高网络的深度和计算效率。参数:c1: 第一个卷积层的输入通道数,通常为前一层输出的通道数。c2: 第二个卷积层的输出通道数,通常是 Bottleneck 输出的通道数。shortcut: 是否使用 shortcut 连接,默认为 True。如果启用,则输入与输出会进行相加。g: 卷积分组数,用于分组卷积,默认为 1。如果 `g > 1`,则使用深度可分离卷积。e: 扩展比例,决定第一个卷积的输出通道数。扩展比例为 `e`,第一个卷积的输出通道数为 `e * c2`。"""super().__init__()# 根据扩展比例计算第一个卷积的输出通道数c_ = int(c2 * e)  # hidden channels, 第一个卷积的输出通道数# 第一个卷积层:1x1卷积,将输入通道数 `c1` 缩减到 `c_`,减少计算量self.cv1 = Conv(c1, c_, 1, 1)  # 卷积核大小 1x1,步幅 1,通道数从 c1 压缩到 c_# 第二个卷积层:3x3卷积,将通道数从 `c_` 恢复到 `c2`,并可选进行分组卷积(深度可分离卷积)self.cv2 = Conv(c_, c2, 3, 1, g=g)  # 卷积核大小 3x3,步幅 1,使用分组卷积(g > 1 表示深度可分离卷积)# 如果启用 shortcut 连接,且输入和输出通道数相同(c1 == c2),则创建 shortcut 连接# shortcut 连接有助于缓解深层网络中的梯度消失问题self.add = shortcut and c1 == c2  # 如果启用 shortcut 且输入输出通道数相同,shortcut 为 Truedef forward(self, x):"""前向传播函数。根据是否启用 shortcut 连接,将输入 x 与卷积后的特征图相加。参数:x: 输入张量,形状为 (batch_size, c1, height, width)输出:y: 输出张量,形状为 (batch_size, c2, height, width)"""# 如果启用 shortcut 连接,并且输入和输出通道数相同,则返回输入与卷积后结果的相加# 即:y = x + Conv(Conv(x)),这类似于残差连接,有助于梯度流动return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

分析:跳跃连接与Bottleneck类之间的关系

Bottleneck不是简单的类似于ResNet中的跳跃连接机制,而是其增强版本,其中涉及了压缩和拓展提高相应效率

跳跃连接类似于一个项目管理团队需要完成一个复杂的任务,团队中有人已经有了基本的解决方案(输入信息)。跳跃连接就像保留了这个基本解决方案,同时允许团队成员对其进行优化。如果最终的优化方案有问题,原始的解决方案仍然可以用作备选

Bottleneck 进一步优化了这个过程。团队会先对已有的解决方案进行 压缩处理(1x1 卷积),提取最重要的部分(删除冗余)。然后团队成员进行深入的讨论(3x3 卷积),生成更详细的解决方案。最终,这个优化过的方案会与原始方案一起汇总,形成最终输出

这种流程既节省了资源(减少计算量),又提高了效率(保留原始信息,提取更深层特征)

总结基本逻辑

  • 压缩和筛选(1×1 卷积)
    • 将输入的通道数(信息量)减少到更小的通道数(隐藏通道数)
  • 扩展和加工(3×3 卷积)
    • 将前面筛选出的信息重新加工和扩展,提取出更加深入的特征
  • Shortcut 连接(保留原始输入)
    • 如果 输入和输出的通道数相同,Bottleneck 会将原始输入直接与处理后的信息相加

空间金字塔池化层(SPP类)

通过多个不同大小的最大池化操作(如5x5、9x9、13x13)来提取不同尺度的特征,然后将这些特征拼接并通过一个卷积层进行融合。这种多尺度特征提取方法增强了模型对不同尺度目标的检测能力,提高了模型的泛化性能

分析:此处与传统yolov3结构并不相同

在普通的 YOLOv3 中,主干网络直接输出特征图,进入 Neck 部分进行特征融合

YOLOv3-SPP 中,增加了 SPP 模块,使得输出特征图在进入 Neck 之前已经融合了多尺度上下文信息。这对检测大目标和小目标都有帮助,因为SPP通过了多层池化操作,可以实现提取大范围的上下文信息

该过程就类似于我们在不同视角和范围内观察物体,通过将下述不同范围内的观察结果拼接在一起,那么就可以了解局部信息,也可以获取全局信息,从而更加清晰的认识目标

  • 小池化核(5x5):就像我们近距离观察物体的细节,关注某些局部的特征,例如树叶的纹理
  • 中等池化核(9x9):类似稍微拉远一些观察,看到物体的一部分,例如一棵树的整体轮廓
  • 大池化核(13x13):相当于站得更远,关注场景的全局信息,例如整片森林

实现分析

  • 生成3个最大池化层(5*5 9*9 13*13)然后存储在self.m中
class SPP(nn.Module):"""空间金字塔池化(Spatial Pyramid Pooling)层,用于 YOLOv3 中的 SPP 模块。SPP模块通过多尺度池化来增强模型的感知能力,它利用不同尺寸的池化核在不同的空间尺度上提取特征,从而增强网络对不同尺寸目标的检测能力。参数:c1: 输入通道数,即输入特征图的通道数。c2: 输出通道数,即通过池化操作和卷积处理后的输出特征图的通道数。k: 一个包含多个池化核大小的元组,默认是 (5, 9, 13),表示使用三个不同尺寸的最大池化操作。"""def __init__(self, c1, c2, k=(5, 9, 13)):"""初始化 SPP 层。参数:c1: 输入特征图的通道数。c2: 输出特征图的通道数。k: 一个包含多个池化核大小的元组,默认为 (5, 9, 13)。"""super().__init__()# 隐藏层通道数,c1//2 将输入通道数减少一半c_ = c1 // 2# 第一个卷积层:1x1卷积,将输入的通道数 c1 压缩到 c_# 目的是减少计算量,同时提取输入特征的低级信息self.cv1 = Conv(c1, c_, 1, 1)# 最后一层卷积:1x1卷积,将多个池化后的特征图的通道数恢复到 c2# 这里通过 concat 操作合并了多个不同尺度池化后的特征图# 因为池化后会增加特征图的通道数,所以需要用 1x1 卷积恢复输出通道数self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)# 创建多个最大池化层,使用不同尺寸的卷积核# 这些池化层的尺寸是由 k 中的元素指定的# 例如 k=(5, 9, 13) 表示使用 3 种不同的池化尺寸:5x5、9x9 和 13x13self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])def forward(self, x):"""前向传播函数,将输入的特征图通过 SPP 层进行处理。参数:x: 输入张量,形状为 (batch_size, c1, height, width)输出:y: 输出张量,形状为 (batch_size, c2, height/2, width/2),通过多个池化操作和卷积操作后的特征图。"""# 通过第一个卷积层,将输入特征图的通道数从 c1 减少到 c_x = self.cv1(x)  # (batch_size, c_, height, width)# 使用警告过滤器来避免在 PyTorch 1.9.0 版本中 `max_pool2d` 产生的警告with warnings.catch_warnings():warnings.simplefilter('ignore')  # 抑制 PyTorch 1.9.0 中的 max_pool2d 警告# 执行多个池化操作,并将池化后的结果与原始输入特征图拼接# `x` 是第一个卷积后的特征图# `[x]` 表示将原始特征图作为池化的基准图# `[m(x) for m in self.m]` 将输入通过每个池化层(具有不同尺寸的卷积核)进行池化# 然后将原始特征图和池化后的特征图在通道维度上拼接# 最后通过最后一个卷积层将拼接后的特征图映射到 c2 个通道return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))  

Focus 层

通过将输入特征图的四个象限拼接在一起,增加通道数,然后通过一个卷积层提取特征。这样可以在保持空间信息的同时,增加通道数,提升特征提取效率。该层有助于在早期阶段减少特征图的空间尺寸,同时增加通道维度,提升模型的计算效率

类似于将建筑的不同部分信息压缩到一个统一的空间,便于进一步处理,通过该方式实现了重新排列和压缩特征图,减少了空间维度的计算量,同时保留了完整的信息

简单来说类似于将原本一张一张的照片,一起放入四方格中,然后再提取关键信息,这样一次性就可以提取很多关键信息

class Focus(nn.Module):r""" 将宽高信息压缩到通道空间中。Focus 层通过将输入图像的四个象限拼接在一起,然后通过一个卷积层来提取特征。该层的主要目的是将空间信息压缩到通道维度,从而减少计算量并增强模型对图像细节的捕捉能力。"""def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  """初始化 Focus 层,使用卷积将拼接后的四个象限特征图映射到目标通道数 c2。参数:c1: 输入图像的通道数c2: 输出图像的通道数k: 卷积核大小 (默认为 1)s: 步幅 (默认为 1)p: 填充 (默认为 None,自动根据卷积核大小计算)g: 分组卷积数 (默认为 1)act: 是否使用激活函数 (默认为 True)"""super().__init__()# 初始化卷积层,将四个象限拼接后的特征图映射到目标通道数 c2# Conv 是一个封装了卷积操作和激活函数的模块self.conv = Conv(c1 * 4, c2, k, s, p, g, act)# 如果需要将图像尺寸减半,可以使用 Contract 层,这里注释掉了# self.contract = Contract(gain=2)def forward(self, x):  """前向传播函数,将输入图像的四个象限拼接到一起,并通过卷积层进行处理。参数:x: 输入张量,形状为 (batch_size, c1, width, height)输出:y: 输出张量,形状为 (batch_size, c2, width/2, height/2)"""# 将输入特征图的四个象限拼接起来# `x[..., ::2, ::2]`:获取输入图像的左上象限 (步长为2,选择偶数索引的行列)# `x[..., 1::2, ::2]`:获取输入图像的右上象限 (步长为2,选择奇数索引的行,偶数列)# `x[..., ::2, 1::2]`:获取输入图像的左下象限 (步长为2,偶数行,选择奇数列)# `x[..., 1::2, 1::2]`:获取输入图像的右下象限 (步长为2,奇数行,奇数列)# `torch.cat(..., 1)`:将这四个象限沿通道维度(dim=1)进行拼接# 拼接后,特征图的通道数是原来的 4 倍,空间维度保持不变return self.conv(torch.cat([x[..., ::2, ::2],  # 左上象限:选择偶数行和偶数列x[..., 1::2, ::2],  # 右上象限:选择奇数行和偶数列x[..., ::2, 1::2],  # 左下象限:选择偶数行和奇数列x[..., 1::2, 1::2]   # 右下象限:选择奇数行和奇数列], 1))  # 沿通道维度拼接四个象限# 如果启用 Contract 层,图像尺寸将被减半# return self.conv(self.contract(x))

Contract 层

主要作用是将输入特征图的宽度和高度的信息压缩到通道维度上,从而实现宽高的缩小和通道数的放大。它可以理解为一种特征重新排列操作,通过对特征图的宽高维度进行收缩,腾出更多的计算资源给通道维度

分析:Contract类到底在做什么

简单来说目Contract类就是在将一个大蛋糕切割成小块,然后重新排列这些小块以节省空间,但是仍然需要保留所有的信息,那么按照这个思路,我们可以将步骤进行拆解

  • 假设这大蛋糕的尺寸为80*80,蛋糕是一个64层的蛋糕(这个也就对应着64个通道),目标是将蛋糕进行打包从而方便运输
  • 切割蛋糕
    • 将蛋糕的每一层都切成小块,例如每一块都是2*2
    • 那么原本80*80被切割成40*40的小块,每块对应4块子区域
  • 重新排列
    • 将每个切割下来的2*2小块重新排列到新的一层中
    • 也就是说原本一层中包含的信息被分散到新的多层中了
  • 结果
    • 宽高从80*80变为40*40
    • 但是此时蛋糕的层数从64层拓展到了256层

分析:通道拓展操作发生在yolov3中的哪些阶段

Backbone特征提取阶段,在该主干网络中,Contract主要用于缩小特征图的空间尺寸,同时拓展通道数,这也就更好的提取深层特征,并将空间信息编码到通道中

假设Backbone的输出特征图尺寸为80*80*64,那么通过Contract操作后特征图就变为了40*40*256,那么也就实现了通道数增加,宽高减小,有助于后续层提取更丰富的特征

Neck特征融合阶段,该类可以用于多尺度特征的融合。通过将空间信息压缩到通道中,可以更高效地整合不同尺度的特征图

因为Neck的输入特征图是来自多个层,而通过Contract可以将较大的特征图压缩到较小的尺寸中,从而实现和其他特征图融合

class Contract(nn.Module):"""用于 YOLOv3 模型的 parse_model 模块将输入特征的宽度和高度维度缩小,同时将缩小的空间信息扩展到通道维度。该操作将特征图的空间维度(宽度和高度)压缩为通道维度,以增强模型的表示能力。例如:输入特征图形状:x(1, 64, 80, 80),输出形状:x(1, 256, 40, 40)即将空间维度缩小一半(宽度和高度各缩小为原来的一半),而通道数则增大为原来的 4 倍。参数:gain: 缩放因子,用于控制空间维度缩小的比例,默认值为 2。"""def __init__(self, gain=2):super().__init__()self.gain = gain  # 缩放因子,默认为 2,表示将宽度和高度缩小一半def forward(self, x):"""前向传播函数,将输入特征图的空间维度(宽度和高度)缩小,并将缩小的信息扩展到通道维度。参数:x: 输入张量,形状为 (batch_size, channels, height, width),即输入特征图。输出:x: 输出张量,形状为 (batch_size, channels * gain * gain, height / gain, width / gain),即经过空间维度缩小并将其扩展到通道维度后的特征图。"""# 获取输入张量的大小:batch_size、通道数、图像高度、图像宽度b, c, h, w = x.size()# 缩放因子,这里 gain = 2,表示将输入特征图的宽度和高度缩小为原来的一半。s = self.gain# 重塑张量形状:# 使用 .view() 方法将输入张量的空间维度分解,并将它们扩展到通道维度。# 这一步将形状从 (b, c, h, w) 转换为 (b, c, h // s, s, w // s, s),# 其中 `h // s` 和 `w // s` 是缩小后的空间维度,`s` 是缩小后的每个空间单元分配的通道数。# 例如,如果 gain = 2,则输入的高宽分别会被缩小为原来的一半。x = x.view(b, c, h // s, s, w // s, s)# 使用 .permute() 方法重新排列张量的维度顺序。# permute(0, 3, 5, 1, 2, 4) 将原来的形状 (b, c, h // s, s, w // s, s) 调整为:# (b, s, s, c, h // s, w // s),即交换通道维度和缩小后的空间维度。x = x.permute(0, 3, 5, 1, 2, 4).contiguous()# 最后,通过 .view() 将张量重新塑形,得到最终的输出张量。# 输出的形状为 (b, c * s * s, h // s, w // s),即通道数由原来的 c 增加为 c * s * s,# 同时宽度和高度都缩小了 s 倍。# 例如,如果 gain = 2,原来 c = 64,则输出 c = 256,宽度和高度都缩小为原来的 1/2。return x.view(b, c * s * s, h // s, w // s)

Expand 层

Expand类的主要作用就是与Contract类相反,通过将通道维度缩小,并将这些维度的信息扩展到宽高维度,实现形状的转换

如果延续使用上述切蛋糕的例子进行理解,那么此时的目标则是减少蛋糕的层数、保留所有蛋糕的内容,同时将每层信息重新分布到宽度和高度上, 扩大单层的面积(四方格照片改为了列表排列)

class Expand(nn.Module):"""用于 YOLOv3 模型的 parse_model 模块。Expand 函数改变输入特征的形状,将通道维度(channel)缩小的数据扩展到空间维度(宽度和高度)。具体来说,`Expand` 模块的作用是将输入特征图的通道信息“扩展”到更大的空间维度,即增加图像的宽度和高度,同时减小通道数。该操作的目标是将压缩的通道信息“还原”到空间维度。例如:输入特征图形状:`x(1, 64, 80, 80)`,输出特征图形状:`x(1, 16, 160, 160)`即将通道数从 64 降到 16,同时将空间维度(宽度和高度)扩大到原来的两倍。参数:gain: 缩放因子,默认为 2,表示空间维度将扩展为原来的两倍。"""def __init__(self, gain=2):super().__init__()self.gain = gain  # 缩放因子,默认值为 2,表示将输入特征图的宽度和高度扩展一倍def forward(self, x):"""前向传播函数,扩展输入特征图的空间维度,并缩小通道数。参数:x: 输入张量,形状为 (batch_size, channels, height, width),即输入特征图。输出:x: 输出张量,形状为 (batch_size, channels // gain^2, height * gain, width * gain),即通过扩展空间维度并缩小通道数后的特征图。"""# 获取输入张量的形状:batch_size、通道数、图像高度、图像宽度b, c, h, w = x.size()# 缩放因子,这里 gain = 2,表示将空间维度扩展为原来的两倍,同时通道数缩小为原来的四分之一s = self.gain# 重塑张量形状:# 使用 .view() 方法将输入张量的形状从 (b, c, h, w) 转换为 (b, s, s, c // s^2, h, w)。# 这一步将通道数分解,并分配到空间维度上。`c // s^2` 是每个空间单元分配的通道数。# 例如,如果 gain=2,通道数会被缩小为原来的 1/4,同时空间维度将扩展为原来的两倍。x = x.view(b, s, s, c // s ** 2, h, w)# 使用 .permute() 方法重新排列张量的维度顺序:# 将 (b, s, s, c // s^2, h, w) 转换为 (b, c // s^2, h, s, w, s),# 使得空间维度(height 和 width)被展开,而通道数(c // s^2)保持不变。x = x.permute(0, 3, 4, 1, 5, 2).contiguous()# 最后,通过 .view() 将张量重塑为新的形状:# 输出形状为 (b, c // s^2, h * s, w * s),# 即通道数为 `c // s^2`,而高度和宽度分别扩展了 `s` 倍。return x.view(b, c // s ** 2, h * s, w * s)

Concat 层

这个类主要作用就是将不同层的特征图进行合并,增强特征表示,从而进一步提高模型的预测能力

如果将其整体比喻成盖房子的话,那么这个类就是将多个建筑模块合并到一个统一的结构中,确保整体协调性

分析:这个类的核心作用理解

 主要就是用于拼接底层和高层的特征图,此时模型可以同时关注细节和大范围的物体,从而更加准确的识别和定位不同尺度的物体

类似于拍摄一张照片的时候,照片中既有近处的球,又有远处的建筑

  • 低层特征:像是拍摄细节的小球,它们可以通过高分辨率的图片细节来捕捉到
  • 高层特征:捕捉到大楼的整体轮廓,这时你需要网络从更大的视野中提取出这些大物体的信息

分析Concat发生在哪个阶段

注意下图中的黄色区域 

Concat主要发生在Neck阶段

  • 使用上采样将低分辨率的特征图放大到与高分辨率特征图相同的大小。
  • 使用 Concat 操作将上采样后的低分辨率特征图与相应的高分辨率特征图拼接(在通道维度上),形成更强的特征表示
  • 最终拼接后的特征图经过进一步的卷积层(CBL)处理,最终输送到预测头阶段
import torch
import torch.nn as nnclass Concat(nn.Module):# 这个类用于在指定的维度上拼接多个张量,通常用于合并特征图(feature map)。# 例如,在一些目标检测架构(如YOLO)中,可能需要将前后不同尺度的特征图合并, # 以便在后续层中进行处理。# 此类使用 `torch.cat` 函数实现张量的拼接操作。def __init__(self, dimension=1):"""初始化Concat类。参数:dimension (int): 指定拼接的维度。默认值为1,表示在第一个维度(通常是通道维度)上进行拼接。"""super().__init__()self.d = dimension  # 将指定的拼接维度保存在实例变量self.d中。def forward(self, x):"""定义前向传播方法,执行拼接操作。参数:x (list of tensors): 输入的张量列表,通常为多个特征图张量,维度需要匹配除拼接维度外的其他维度。返回:Tensor: 沿指定维度拼接后的张量。"""# 使用torch.cat在指定维度上拼接张量,self.d是拼接的维度。return torch.cat(x, self.d)

DetectMultiBackend 类

主要用于支持在不同设备或者框架上进行模型推理,从而让模型可以在不同的硬件和软件中部署和使用

import torch
import torch.nn as nn
import json
import numpy as np
import cv2
import tensorflow as tf
from pathlib import Path
from PIL import Image
import platform
from utils import check_suffix, check_requirements, LOGGER, xywh2xyxy  # 假设这些方法在其他地方定义class DetectMultiBackend(nn.Module):# MultiBackend 类用于在各种后端上进行推断,支持多种模型格式(PyTorch, ONNX, TensorFlow, CoreML 等)。def __init__(self, weights='yolov3.pt', device=None, dnn=True):"""初始化 DetectMultiBackend 类,加载不同后端模型。参数:weights (str): 模型文件路径,可以是各种格式(如:PyTorch `.pt`, ONNX `.onnx`, TensorFlow `.pb`, etc.)。device (str): 设备设置,通常为 'cuda' 或 'cpu',用于指定模型推断的硬件。dnn (bool): 是否使用 OpenCV DNN 后端进行 ONNX 模型推理,默认使用 True。"""super().__init__()# 处理模型路径,如果是一个列表,则取第一个权重路径w = str(weights[0] if isinstance(weights, list) else weights)  suffix, suffixes = Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '', '.mlmodel']check_suffix(w, suffixes)  # 检查权重后缀是否在允许的列表中# 根据文件后缀确定模型类型pt, onnx, tflite, pb, saved_model, coreml = (suffix == x for x in suffixes)# 判断是否为 TorchScript 模型jit = pt and 'torchscript' in w.lower()# 默认步幅和类别名称stride, names = 64, [f'class{i}' for i in range(1000)]  if jit:  # 如果是 TorchScript 模型LOGGER.info(f'Loading {w} for TorchScript inference...')extra_files = {'config.txt': ''}  # 存储附加文件(如模型元数据)model = torch.jit.load(w, _extra_files=extra_files)  # 加载 TorchScript 模型if extra_files['config.txt']:  # 如果有配置文件,解析配置d = json.loads(extra_files['config.txt'])stride, names = int(d['stride']), d['names']  # 设置步幅和类别名称elif pt:  # 如果是 PyTorch 模型from models.experimental import attempt_load  # 导入尝试加载模型的方法model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device)stride = int(model.stride.max())  # 获取模型的最大步幅names = model.module.names if hasattr(model, 'module') else model.names  # 获取类别名称elif coreml:  # 如果是 CoreML 模型 (*.mlmodel)import coremltools as ctmodel = ct.models.MLModel(w)  # 加载 CoreML 模型elif dnn:  # 使用 OpenCV DNN 加载 ONNX 模型LOGGER.info(f'Loading {w} for ONNX OpenCV DNN inference...')check_requirements(('opencv-python>=4.5.4',))  # 确保安装了 OpenCVnet = cv2.dnn.readNetFromONNX(w)  # 使用 OpenCV 读取 ONNX 模型elif onnx:  # 使用 ONNX Runtime 加载 ONNX 模型LOGGER.info(f'Loading {w} for ONNX Runtime inference...')check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime'))  # 检查依赖项import onnxruntime  # 导入 ONNX Runtimesession = onnxruntime.InferenceSession(w, None)  # 创建推理会话else:  # 使用 TensorFlow 模型(TFLite, pb, saved_model)import tensorflow as tfif pb:  # TensorFlow Frozen Graphdef wrap_frozen_graph(gd, inputs, outputs):# 包装函数,用于处理 TensorFlow frozen graph 格式x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), [])return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs),tf.nest.map_structure(x.graph.as_graph_element, outputs))LOGGER.info(f'Loading {w} for TensorFlow *.pb inference...')graph_def = tf.Graph().as_graph_def()graph_def.ParseFromString(open(w, 'rb').read())frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0")elif saved_model:  # TensorFlow SavedModelLOGGER.info(f'Loading {w} for TensorFlow saved_model inference...')model = tf.keras.models.load_model(w)  # 加载 SavedModelelif tflite:  # TensorFlow Liteif 'edgetpu' in w.lower():  # 使用 Edge TPU 加速LOGGER.info(f'Loading {w} for TensorFlow Edge TPU inference...')import tflite_runtime.interpreter as tflidelegate = {'Linux': 'libedgetpu.so.1','Darwin': 'libedgetpu.1.dylib','Windows': 'edgetpu.dll'}[platform.system()]interpreter = tfli.Interpreter(model_path=w, experimental_delegates=[tfli.load_delegate(delegate)])else:LOGGER.info(f'Loading {w} for TensorFlow Lite inference...')interpreter = tf.lite.Interpreter(model_path=w)  # 加载 TFLite 模型interpreter.allocate_tensors()  # 分配张量input_details = interpreter.get_input_details()  # 获取输入张量信息output_details = interpreter.get_output_details()  # 获取输出张量信息# 将所有局部变量存储为实例属性self.__dict__.update(locals())def forward(self, im, augment=False, visualize=False, val=False):"""执行模型推理。参数:im (Tensor): 输入图像,格式为 BCHW。augment (bool): 是否使用数据增强。visualize (bool): 是否进行可视化。val (bool): 是否用于验证模式。返回:Tensor: 推理结果。"""b, ch, h, w = im.shape  # 批量大小、通道数、高度、宽度if self.pt:  # 如果使用 PyTorch 模型y = self.model(im) if self.jit else self.model(im, augment=augment, visualize=visualize)return y if val else y[0]elif self.coreml:  # 如果使用 CoreML 模型im = im.permute(0, 2, 3, 1).cpu().numpy()  # 将 BCHW 转为 BHWC 格式im = Image.fromarray((im[0] * 255).astype('uint8'))  # 转换为 PIL 图像y = self.model.predict({'image': im})  # 使用 CoreML 进行预测box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]])  # 转换为 xyxy 格式的坐标conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float)  # 获取置信度和类别y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)  # 合并结果elif self.onnx:  # 如果使用 ONNX 模型im = im.cpu().numpy()  # 将输入转换为 numpy 数组if self.dnn:  # 如果使用 OpenCV DNNself.net.setInput(im)  # 设置输入y = self.net.forward()  # 执行推理else:  # 使用 ONNX Runtimey = self.session.run([self.session.get_outputs()[0].name], {self.session.get_inputs()[0].name: im})[0]else:  # 如果使用 TensorFlow 模型(TFLite, pb, saved_model)im = im.permute(0, 2, 3, 1).cpu().numpy()  # 将 BCHW 转为 BHWC 格式if self.pb:  # TensorFlow Frozen Graphy = self.frozen_func(x=self.tf.constant(im)).numpy()  # 执行推理elif self.saved_model:  # TensorFlow SavedModely = self.model(im, training=False).numpy()  # 执行推理elif self.tflite:  # TensorFlow Liteinput, output = self.input_details[0], self.output_details[0]int8 = input['dtype'] == np.uint8  # 检查是否是量化模型if int8:  # 如果是量化模型,进行反量化scale, zero_point = input['quantization']im = (im / scale + zero_point).astype(np.uint8)self.interpreter.set_tensor(input['index'], im)  # 设置输入张量self.interpreter.invoke()  # 执行推理y = self.interpreter.get_tensor(output['index'])  # 获取输出张量if int8:  # 如果是量化模型,进行反量化scale, zero_point = output['quantization']y = (y.astype(np.float32) - zero_point) * scaley[..., 0] *= w  # 转换 x 方向坐标y[..., 1] *= h  # 转换 y 方向坐标y[..., 2] *= w  # 转换宽度y[..., 3] *= h  # 转换高度y = torch.tensor(y)  # 转换为 torch 张量return (y, []) if val else y  # 如果 val 为 True,返回 (y, []),否则返回 y

AutoShape 类

该类实现了一个AutoShape模型包装器,用于目标检测任务中处理各种类型的输入,并执行推理和后处理(包括非极大值抑制 NMS)。代码将原始输入进行预处理后送入模型,随后进行推断,并通过后处理输出检测结果

分析1:这个类的应用场景分析

类似于一个智能摄像头的应用,其主要目标就是用来实时检测自家狗和猫的位置,那么我们就会出现以下情况

  • 用户拍摄图片的格式和来源各异,比如直接拍照(numpy 数组)、网络图片(URL 地址)或视频帧(OpenCV 图像)
  • 你需要将这些输入统一预处理,然后送入深度学习模型
  • 模型推断完成后,你需要根据用户需求对输出结果进行处理,比如过滤类别为“猫”和“狗”的框

分析2:这个类主要服务于yolov3整体流程的哪些流程

主要发生在两个阶段,分别是输入预处理阶段和推理、后处理阶段

  • 输入预处理阶段
    • 位于流程图的输入端,将输入图片转为 PyTorch 张量格式,统一大小、填充、归一化等
    • 一般是通过letterbox()或者torch.from_numpy()实现
  • 推理和后处理阶段 
    • 位于流程图的检测头和后处理阶段
    • 自动执行推理结果的非极大值抑制(NMS
    • 将检测框坐标从网络输入大小映射回原始图像大小
    • 最终生成用户友好的检测结果格式(包含检测框、类别、置信度等)
import torch
import torch.nn as nn
import numpy as np
import time
from pathlib import Path
from PIL import Image
import requests
from utils import time_sync, make_divisible, letterbox, exif_transpose, non_max_suppression, scale_coords, LOGGER  # 假设这些方法在其他地方定义class AutoShape(nn.Module):# 模型包装器,处理不同格式的输入图像,进行预处理、推理和后处理(包括NMS)。conf = 0.25  # NMS 置信度阈值,低于该值的框将被抑制iou = 0.45  # NMS 中的 IoU 阈值,高于该值的重叠框将被抑制classes = None  # 可选:指定要检测的类别(例如 COCO 中的人、猫和狗 = [0, 15, 16])multi_label = False  # 是否允许每个检测框有多个标签max_det = 1000  # 每张图片最多返回的检测框数量def __init__(self, model):"""初始化 AutoShape 类,包装传入的模型。参数:model (nn.Module): 传入的目标检测模型(例如 YOLO 模型)。"""super().__init__()self.model = model.eval()  # 将模型设置为评估模式,禁用 dropout 和 batch normalizationdef autoshape(self):"""如果 AutoShape 已经启用,直接跳过,避免重复启用。"""LOGGER.info('AutoShape already enabled, skipping... ')  # 记录日志,跳过重复启用return selfdef _apply(self, fn):"""对模型的非参数部分(如 stride、grid、anchor_grid)应用指定的函数。参数:fn (function): 应用于模型的函数,例如将数据移动到 GPU。返回:self: 返回更新后的模型。"""self = super()._apply(fn)  # 对父类调用 _apply 方法m = self.model.model[-1]  # 获取模型的最后一层(假设为 Detect 层)m.stride = fn(m.stride)  # 应用函数修改 stridem.grid = list(map(fn, m.grid))  # 修改 gridif isinstance(m.anchor_grid, list):  # 如果 anchor_grid 是列表格式m.anchor_grid = list(map(fn, m.anchor_grid))  # 修改 anchor_gridreturn self@torch.no_grad()def forward(self, imgs, size=640, augment=False, profile=False):"""输入图像进行推理,支持多种输入格式(如文件路径、PIL、Numpy、Torch 张量)。参数:imgs (list/str/Path/Tensor): 输入图像,可以是文件路径、URL、PIL 图像、Numpy 数组或 Torch 张量。size (int): 图像调整到的目标大小,默认为 640。augment (bool): 是否进行数据增强,默认为 False。profile (bool): 是否开启性能分析,默认为 False。返回:Detections: 返回检测结果对象,包含检测框、类别、分数等信息。"""t = [time_sync()]  # 记录推理开始的时间p = next(self.model.parameters())  # 获取模型的设备(如 GPU 或 CPU)和数据类型if isinstance(imgs, torch.Tensor):  # 如果输入是 torch.Tensor 格式with torch.cuda.amp.autocast(enabled=p.device.type != 'cpu'):  # 启用自动混合精度(仅在非 CPU 上启用)return self.model(imgs.to(p.device).type_as(p), augment, profile)  # 执行推理# 预处理:将输入图像转为统一格式(例如 PIL 图像转为 Numpy 数组)n, imgs = (len(imgs), imgs) if isinstance(imgs, list) else (1, [imgs])  # 如果是单图像输入,转为列表shape0, shape1, files = [], [], []  # 分别保存原始图像尺寸、目标尺寸、文件名for i, im in enumerate(imgs):f = f'image{i}'  # 默认文件名if isinstance(im, (str, Path)):  # 如果输入是文件路径或 URL# 如果是 URL,通过 requests 下载图像;否则直接读取图像im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith('http') else im), imim = np.asarray(exif_transpose(im))  # 读取图像并处理 EXIF 旋转信息elif isinstance(im, Image.Image):  # 如果输入是 PIL Imageim, f = np.asarray(exif_transpose(im)), getattr(im, 'filename', f) or f  # 转换为 Numpy 数组files.append(Path(f).with_suffix('.jpg').name)  # 保存文件名(以 .jpg 为后缀)if im.shape[0] < 5:  # 如果图像是 CHW 格式(通道数在前)im = im.transpose((1, 2, 0))  # 转换为 HWC 格式(通道数在最后)im = im[..., :3] if im.ndim == 3 else np.tile(im[..., None], 3)  # 强制图像为 3 通道(灰度图转伪彩色)s = im.shape[:2]  # 获取图像的高度和宽度shape0.append(s)  # 保存原始图像的尺寸g = (size / max(s))  # 计算缩放因子shape1.append([y * g for y in s])  # 计算目标尺寸imgs[i] = im if im.data.contiguous else np.ascontiguousarray(im)  # 确保图像是连续内存存储# 计算推理时的目标尺寸,确保是 stride 的倍数shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)]  x = [letterbox(im, new_shape=shape1, auto=False)[0] for im in imgs]  # 使用 letterbox 填充图像(保持纵横比)x = np.stack(x, 0) if n > 1 else x[0][None]  # 如果输入有多张图像,堆叠成批次x = np.ascontiguousarray(x.transpose((0, 3, 1, 2)))  # 转换为 BCHW 格式x = torch.from_numpy(x).to(p.device).type_as(p) / 255  # 将 Numpy 数组转为 Tensor 并归一化(0-1)t.append(time_sync())  # 记录推理的时间with torch.cuda.amp.autocast(enabled=p.device.type != 'cpu'):  # 启用自动混合精度y = self.model(x, augment, profile)[0]  # 执行推理t.append(time_sync())  # 记录推理结束的时间# 后处理:执行非极大值抑制(NMS)去除冗余的框y = non_max_suppression(y, self.conf, iou_thres=self.iou, classes=self.classes,multi_label=self.multi_label, max_det=self.max_det)  # 执行 NMSfor i in range(n):scale_coords(shape1, y[i][:, :4], shape0[i])  # 将预测框坐标缩放回原始图像的尺寸t.append(time_sync())  # 记录 NMS 完成后的时间return Detections(imgs, y, files, t, self.names, x.shape)  # 返回检测结果对象,包含图像、框、类别、分数等信息

Detections 类

用于管理和存储检测结果,其中包括图像、预测框、文件名等,同时提供结果的可视化和保存功能

分析1:主要发生在哪个阶段

主要发生在后处理阶段,如果基于上述家庭摄像头应用的例子,那么该类的主要作用就是

  • 解析模型检测到的“猫”和“狗”的位置
  • 显示边界框和类别标签
  • 保存裁剪出的目标区域供进一步使用(如分类分析)
class Detections:r""" 用于推理结果的检测类。该类用于处理模型的推理输出,包括图像、预测框、文件名等信息,并提供归一化后的框坐标。"""def __init__(self, imgs, pred, files, times=None, names=None, shape=None):"""初始化 Detections 类,存储模型的推理结果并进行必要的处理(如坐标归一化)。参数:imgs (list): 输入图像列表。pred (list): 预测结果,包含每个图像的检测框信息。files (list): 图像的文件名列表。times (list, optional): 推理过程中的时间信息,用于评估性能。names (list, optional): 类别名称列表。shape (tuple, optional): 输入图像的形状,通常是 BCHW 格式。"""super().__init__()d = pred[0].device  # 获取预测结果所在的设备(CPU 或 GPU)# 计算每张图像的归一化因子,这些因子用于将检测框的坐标归一化到 0-1 区间gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) for im in imgs]  # 归一化因子# 初始化类属性self.imgs = imgs  # 存储输入的图像self.pred = pred  # 存储预测结果,pred[0] 包含 (xyxy, conf, cls) 信息self.names = names  # 类别名称列表self.files = files  # 图像文件名列表self.xyxy = pred  # 存储预测框的 xyxy 坐标self.xywh = [xyxy2xywh(x) for x in pred]  # 将预测框的 xyxy 坐标转换为 xywh 格式self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)]  # 将 xyxy 坐标归一化self.xywhn = [x / g for x, g in zip(self.xywh, gn)]  # 将 xywh 坐标归一化self.n = len(self.pred)  # 图像数量(即批次大小)self.t = tuple((times[i + 1] - times[i]) * 1000 / self.n for i in range(3)) if times else (0, 0, 0)  # 计算每个步骤的平均时间self.s = shape  # 存储图像的形状,通常为 BCHW 格式def display(self, pprint=False, show=False, save=False, crop=False, render=False, save_dir=Path('')):r""" 显示、保存或裁剪检测结果。根据设置,执行以下操作:- `pprint`: 打印检测信息到日志。- `show`: 显示检测结果图像。- `save`: 保存检测结果图像到指定目录。- `crop`: 裁剪检测区域并保存。- `render`: 渲染检测结果到图像列表中。参数:- `save_dir`: 保存图像的目录路径。"""crops = []  # 用于存储裁剪后的检测区域for i, (im, pred) in enumerate(zip(self.imgs, self.pred)):# 创建包含图像信息的字符串:图像编号和图像尺寸s = f'image {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} 'if pred.shape[0]:  # 如果有检测结果for c in pred[:, -1].unique():  # 遍历每个类别n = (pred[:, -1] == c).sum()  # 当前类别的检测数量s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, "  # 添加类别名称和数量到信息字符串# 如果需要显示、保存、渲染或裁剪结果if show or save or render or crop:annotator = Annotator(im, example=str(self.names))  # 初始化注释工具for *box, conf, cls in reversed(pred):  # 逆序遍历预测框label = f'{self.names[int(cls)]} {conf:.2f}'  # 标签文本if crop:  # 如果需要裁剪# 创建裁剪后的图像并保存file = save_dir / 'crops' / self.names[int(cls)] / self.files[i] if save else Nonecrops.append({'box': box, 'conf': conf, 'cls': cls, 'label': label,'im': save_one_box(box, im, file=file, save=save)})else:  # 否则直接在图像上绘制框annotator.box_label(box, label, color=colors(cls))im = annotator.im  # 更新图像,包含检测框和标签else:s += '(no detections)'  # 如果没有检测到目标im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im  # 如果图像是 numpy 数组,则转换为 PIL 图像if pprint:LOGGER.info(s.rstrip(', '))  # 打印信息到日志if show:im.show(self.files[i])  # 显示图像if save:f = self.files[i]im.save(save_dir / f)  # 保存图像if i == self.n - 1:LOGGER.info(f"Saved {self.n} image{'s' * (self.n > 1)} to {colorstr('bold', save_dir)}")  # 保存完成后的日志if render:self.imgs[i] = np.asarray(im)  # 渲染图像if crop:if save:LOGGER.info(f'Saved results to {save_dir}\n')  # 保存裁剪结果的日志return crops  # 返回裁剪的检测区域def print(self):r""" 打印检测结果和处理速度信息。"""self.display(pprint=True)  # 打印检测结果LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {tuple(self.s)}' %self.t)  # 打印每张图像的处理速度(预处理、推理、非极大值抑制)以及图像的形状def show(self):r""" 显示检测结果。"""self.display(show=True)  # 显示检测结果def save(self, save_dir='runs/detect/exp'):r""" 保存检测结果到指定目录。"""save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True)  # 递增目录名称(避免覆盖)self.display(save=True, save_dir=save_dir)  # 保存检测结果def crop(self, save=True, save_dir='runs/detect/exp'):r""" 裁剪检测结果并保存。"""save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True) if save else Nonereturn self.display(crop=True, save=save, save_dir=save_dir)  # 裁剪并保存结果def render(self):r""" 渲染检测结果。"""self.display(render=True)  # 渲染检测框到图像return self.imgs  # 返回渲染后的图像def pandas(self):r""" 将检测结果转换为 pandas DataFrame 格式。"""new = copy(self)  # 创建对象的副本ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name'  # xyxy 格式的列名cb = 'xcenter', 'ycenter', 'width', 'height', 'confidence', 'class', 'name'  # xywh 格式的列名# 遍历 'xyxy', 'xyxyn', 'xywh', 'xywhn' 字段及其对应的列名for k, c in zip(['xyxy', 'xyxyn', 'xywh', 'xywhn'], [ca, ca, cb, cb]):# 将检测结果转换为 DataFrame,并更新列名a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] for x in x.tolist()] for x in getattr(self, k)]setattr(new, k, [pd.DataFrame(x, columns=c) for x in a])  # 设置 DataFrame 属性return new  # 返回包含 DataFrame 的副本对象def tolist(self):r""" 返回一个 Detections 对象的列表。例如,可以用 'for result in results.tolist():' 遍历。"""# 创建一个 Detections 对象的列表,每个对象包含一个图像和对应的预测结果x = [Detections([self.imgs[i]], [self.pred[i]], self.names, self.s) for i in range(self.n)]# 对每个 Detections 对象,移除其内部列表,使其属性为单一元素for d in x:for k in ['imgs', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']:setattr(d, k, getattr(d, k)[0])  # 从列表中弹出return x  # 返回包含 Detections 对象的列表def __len__(self):r""" 返回 Detections 对象中图像的数量。"""return self.n  # 返回图像的数量

Classify 类

主要用于将输入特征图分类到不同的类别中,也就类似于简单的图片分类

分析:该类使用的阶段以及具体作用

该类主要发生在后处理阶段,假如yolov3检测到了这个图片是动物,此时Classify类的作用就是将这个动物进行细分,是狗还是猫或者拉布拉多等特定品种,所以说它是对yolov3输出作为输出,然后对检测目标的进一步分类

class Classify(nn.Module):# 分类头,将输入 x(b,c1,20,20) 转换为 x(b,c2)def __init__(self, c1, c2, k=1, s=1, p=None, g=1):"""初始化 Classify 类。参数:c1 (int): 输入通道数。c2 (int): 输出通道数。k (int, optional): 卷积核大小,默认为 1。s (int, optional): 步幅,默认为 1。p (int, optional): 填充,默认为 None,表示自动填充。g (int, optional): 分组卷积的分组数,默认为 1,表示常规卷积。"""super().__init__()# 创建自适应平均池化层,将输入特征图大小缩放为 (b, c1, 1, 1)self.aap = nn.AdaptiveAvgPool2d(1)  # 自适应平均池化,输出尺寸为 (b, c1, 1, 1)# 创建卷积层,将输入特征图 (b, c1, 1, 1) 转换为 (b, c2, 1, 1)# autopad 是自定义的填充方式,根据卷积核的大小来自动计算填充self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g)  # 卷积层# 展平层,将卷积的输出展平为一维向量self.flat = nn.Flatten()  # 展平层,输出形状为 (b, c2)def forward(self, x):"""前向传播函数,将输入 x 通过自适应池化、卷积和展平进行处理。参数:x (Tensor or list): 输入数据,形状为 (b, c1, h, w),其中 b 为批次大小,c1 为输入通道数,h 和 w 为图像高和宽。返回:Tensor: 经处理后的张量,形状为 (b, c2),即经过卷积和展平后的输出。"""# 如果 x 是一个列表,则对列表中的每个元素分别进行自适应池化,并将它们按通道维度进行拼接# 如果 x 不是列表,直接对 x 进行池化z = torch.cat([self.aap(y) for y in (x if isinstance(x, list) else [x])], 1)# 对池化后的结果进行卷积,然后展平为 (b, c2) 的向量return self.flat(self.conv(z))  # 先卷积,再展平
// 使用举例# 初始化分类头
c1, c2 = 256, 10  # 输入通道 256,输出类别数 10
classifier = Classify(c1, c2)# 输入特征图 (batch_size=2, channels=256, height=20, width=20)
x = torch.randn(2, 256, 20, 20)  # 模拟特征图# 前向传播
output = classifier(x)  # 输出形状为 (2, 10)print(output.shape)  # torch.Size([2, 10])


http://www.ppmy.cn/news/1557744.html

相关文章

算法—回文链表

题目链接&#xff1a;https://leetcode.cn/problems/palindrome-linked-list/description/ 题目 给你一个单链表的头节点 head&#xff0c;请你判断该链表是否为回文链表。如果是&#xff0c;返回 true&#xff1b;否则&#xff0c;返回 false。 示例1&#xff1a; 输入&…

富格林:曝光交易良方阻挠损失

富格林悉知&#xff0c;投资者在出金环节受到阻挠时&#xff0c;要注意多留几个心眼避免损失。因为据曝光黄金市场的活跃表现可以为投资者创造了许多获利机会&#xff0c;但是想要通过炒黄金赚钱&#xff0c;就必须掌握一些有效的交易技巧。以下富格林总结曝光几点做单的技巧&a…

Java 实现日志文件大小限制及管理——以 Python Logging 为启示

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云/阿里云/华为云/51CTO&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互…

LiteFlow决策系统的策略模式,顺序、最坏、投票、权重

个人博客&#xff1a;无奈何杨&#xff08;wnhyang&#xff09; 个人语雀&#xff1a;wnhyang 共享语雀&#xff1a;在线知识共享 Github&#xff1a;wnhyang - Overview 想必大家都有听过或做过职业和性格测试吧&#xff0c;尤其是现在的毕业生&#xff0c;在投了简历之后经…

C# OpenCV机器视觉:角度和方向检测

又是一个无聊的周末&#xff0c;阿强正准备享受他期待已久的休闲时光。他打算去公园散步&#xff0c;拍几张美丽的风景照&#xff0c;顺便享受一下大自然的气息。正当他兴致勃勃地走出家门&#xff0c;脑海中幻想着与阳光、花朵和微风的亲密接触时&#xff0c;手机突然响了起来…

如何在电脑上控制手机?

在现代生活中&#xff0c;通过电脑控制手机已经成为一种高效的工作和娱乐方式。Total Control 是一款实用的电脑端软件&#xff0c;通过USB或Wi-Fi连接&#xff0c;用户可以在电脑上直接操作多台手机,通过电脑键盘输入文字&#xff0c;提高操作效率。特别适合需要大屏操作的用户…

如何判断产品需不需要做ATT认证?ATT测试内容和要求分享

随着经济全球化的发展&#xff0c;国内越来越多产品厂商选择将自家产品出口到北美市场&#xff0c;而这时候各位厂商都会面临产品需不需要做AT&T的问题。今天英利检测针对这一问题整理了一些关于AT&T认证中的测试内容与测试要求&#xff0c;供大家参考。 AT&T认证的…

使用 Vite 和 Redux Toolkit 创建 React 项目

文章目录 1. 创建 React 项目2. 安装依赖3. 创建状态仓库user.js创建 shopSlice 4. 在状态仓库中合并切片5. 在入口文件中导入并使用 store6. 获取切片中的数据7. 修改数据结尾 在本教程中&#xff0c;我们将通过使用 Vite 创建一个 React 项目&#xff0c;并结合 Redux Toolki…