yolov5 代码解读 –common.py

  • Post author:
  • Post category:其他


先从common.py学起,接下来的近期时间将会对yolov5的代码进行解析,以及对yolov5的网络结构进行解析。

在common.py文件中主要是封装了不同的通用模块

1:头文件

这是commonpy依赖的头件.可以看到,它包含了一些matplotlib的绘制模块以及xywh转换工具

requests这个库,是yolov5提供的一个很好的技巧,可以让我们直接从http协议拉取视频流放入model中检测

# 引入各种库
import math
from pathlib import Path

import numpy as np
import requests
import torch
import torch.nn as nn
import PIL import Image, ImageDraw

from utils.datasets import letterbox
from utils.general import non_max_suppression, make_divisible, scale_coords, xyxy2xywh
from utils.plots import color_list

2:autopad

# 为same卷积或者same池化自动扩充
# 通过卷积核的大小来计算需要的padding为多少才能把tensor补成原来的形状
def autopad(k,p=None):   # kernel, padding
    # pad to 'same'
    # 如果p是none 则进行下一步
    if p is None:
        # 如果k是int 则进行k//2 若不是则进行x//2
        p = k // 2 if isinstance(k,int) else [x // 2 for x in k]  # auto-pad
    return p



下面定义的类和网络架构中的模块的实现过程一致

3:DWConv

深度分离卷积层,是GCONV的极端情况,

分组数量等于输入通道数量,即每个通道作为一个小组分别进行卷积

结果联结作为输出,Cin=Cout=g,没有bias项,在yolov5中没有真正的使用

具体DWConv的内容可以参考链接

https://www.cnblogs.com/Libo-Master/p/9685451.html

# 这里的深度可分离卷积,主要是将通道按输入输出的最大公约数进行切分,在不同的特征图层上进行特征学习
def DWConv(c1, c2, k=1, s=1, act=True):
    # Depthwise convolution
    '''
    :param c1: 输入channel值
    :param c2: 输出channel值
    :param k: 卷积核kernel,k=1
    :param s: 步长stride,s=1
    :param act:是否使用激活函数
    返回:基本卷积层
    g是一个组 
    math.gcd()返回的是最大公约数
    '''
    # 返回一个类conv在下面定义了
    return Conv(c1, c2, k, s, g=math.gcd(c1,c2), act=act)

4:Conv

标准卷积层

g=1表示从输入通道到输出通道的阻塞连接数为1

autopad(k,p)此处换成自动填充

标准卷积层包括

conv+BN+Leaky relu

Conv实现了将输入特征经过卷积层,激活函数,归一化层,得到输出层

同时可以指定是否使用归一化层

class Conv(nn.module):
    # standard convolution
    # 这个类做了三件事 架构图中的标准卷积: conv+BN+hardswish
    # init初始化构造函数
    def __init__(self,c1,c2,k=1,s=1,p=None,g=1,act=True):  # ch_in ch_out kernel, stride, padding, groups 
        super(Conv, self).__init__()
        '''
        nn.conv2d函数的基本参数是:
        nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, paddinng_mode='zeros')
        参数:nn.Conv 参考链接https://blog.csdn.net/qq_26369907/article/details/88366147
        in_channel:输入数据的通道数,例RGB图片的通道数为3
        out_channel:输出数据的通道数,这个根据模型调整
        kennel_size:卷积核大小,可以是int,或tuple,kennel_size=2,意味着卷积大小(2,2), kennel_size=(2,3),意味着卷积大小(2,3),即非正方形卷积
        stride: 步长,默认为1, 与kennel_size类似, stride=2, 意味着步长上下左右扫描皆为2, stride=(2,3),左右三秒步长为2,上下为3
        padding: 零填充
        groups: 从输入通道到输出通道阻塞连接数,通道分组的参数,输入通道数,输出通道数必须同时满足被groups整除
        groups:如果输出通道为6,输入通道也为6,假设groups为3,卷积核为1*1,;则卷积核的shape为2*1*1,即把输入通道分成了3份;那么卷积核的个数呢?之前是由输出通道
            决定的,这里也一样,输出通道为6,那么就有6个卷积核。这里实际是将卷积核也平分为groups份,在groups份特征图上计算,以输入输出都为6为例,每个2*h*w的特征图子层
            就有且仅有2个卷积核,最后相加恰好是6,这里可以起到的作用是不同通道分别计算特征
        bias:如果为True,则向输出添加可学习的偏置
        '''

        # conv调用nn.Conv2d函数,  p采用autopad, 不使用偏置bias=False 因为下面做融合时,这个卷积的bias会被消掉, 所以不用
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k,p), groups=g, bias=False)
        self.bn = nn.Batch2dNorm2d(c2)
        # 如果act是true则使用nn.SiLU 否则不使用激活函数
        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())

    def forward(self, x):   # 前向计算,网络执行的顺序是根据forward函数来决定的
        return self.act(self.bn(self.conv(x)))      # 先conv卷积然后在bn最后act激活

    def fuseforward(self,x):    # 前向融合计算
        return self.act(self.conv(x))  # 这里只有卷积和激活

Conv2d的具体用法可参考链接

https://blog.csdn.net/qq_26369907/article/details/88366147

5:Bottlenck

标准Bottlenck

网络架构中的bottlenack模块 ,分为True和False

残差快结构

class Bottleneck(nn.module):
    # standard bottleneck
    # bottlenack(True时)
    '''
    参数说明
    c1:bottleneck结果的输入通道维度
    c2:bottleneck结构的输出通道维度
    shortcut:是否给bottleneck结构部添加shortcut连接,添加后即为ResNet模块;
    g:groups,通道分组的参数,输入通道数、输出通道数必须满足被groups整除
    e:expansion:bottleneck结构中的瓶颈部分的通道膨胀率,使用0.5即变为输入的1/2
    这个e有点类似于一种控制瓶颈的参数,e越小瓶颈越窄
    瓶颈指的是:首先经过一个Conv将通道数缩小,然后在通过一个Conv变成原来的通道数,而e就是控制这个窄度的
    有实验表明,这种瓶颈结构可以很好的减少训练参数
    '''
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in ch_out shortcut短接  groups  expansion
        super(Bottleneck,self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)   # 第一个conv模块 1*1的卷积
        self.cv2 = Conv(c_, c2, 3, 1, g=g)      # 第二个conv模块 3*3的卷积
        # 如果shortcut为True就会将输入和输出相加之后在输出
        self.add = shortcut and c1 == c2    # 相加操作
    '''
    详解:
    这里的瓶颈层,瓶颈主要体现在通道数channel上面!一般1*1卷积具有很强的灵活性,这里用于降低通道数,如上面的膨胀率为0.5,
    若输入通道为640,那么经过1*1的卷积层之后变为320;经过3*3之后变为输出的通道数,这样参数量会大量减少
    这里的shortcut即为图中的红色虚线,在实际中,shortcut(捷径)不一定是上面都不操作,也有可能有卷积处理,但此时,另一只一般是多个ReNet模块串联而成。而这里的
    shortcut也成为了identity分支,可以理解为恒等映射,另一个分支被称为残差分支(Residual分支)
    我们经常使用的残差分支实际上是1*1+3*3+1*1结构
    '''
    
    
    # bottlenack(False时)
    def forward(self, x):
        # 根据self.add的值确定是不是有shortcut
        # 如果相加,则进行x+两次卷积的值, 若不想加  则进行两次卷积
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

Bottleneck的模型结构:


穿插一个ResNet模块:

残差模块是深度神经网络中非常重要的模块,在创建模型的过程中经常被使用。

残差模块结构如其名,实际上就是shortcut的直接应用,最出名的残差模块应用这样的:



左边这个结构即Bottleneck结构,也叫




瓶颈残差模块




!右边的图片展示的是基本的残差模块!

6: BottleneckCSP

这部分是几个标准Bottleneck的堆叠+几个标准卷积层

网络架构中的BCSPn模块

基于CSP的残差快

class BottleneckCSP(nn.Module):
    # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):     # ch_in, ch_out, number, shortcut, groups, expansion(膨胀系数)
        # groups: 控制着输入和输出之间的连接关系,,默认为1,相当于全连接
        '''
        参数说明
        c1:BottleneckCSP结构的输入通道维度
        c2:BottleneckCSP结构的输出通道维度
        n:BottleneckCSP结构的个数 即有多少个残差组件Bottleneck
        shortcut:是否给Bottleneck结构天机shortcut连接,添加后即为ResNet模块
        g:groups:通道分组的参数,输入通道数、输出通道数必须同时满足被groups整除
        e:expansion:bottleneck结构中的瓶颈部分的通道膨胀率,使用0.5即为变为输入的1/2
        torch.cat(y1, y2), dim=1: 这里是指定在第一个维度上进行合并,即在channel维度上合并
        c_: bottleneckCSP 结构的中间层的通道数,由膨胀率e决定
        '''
        super(BottleneckCSP, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)   # 第一个Conv模块
        self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
        self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
        self.cv4 = Conv(2 * c_, c2, 1, 1)
        self.bn = nn.BatchNorm2d(2 * c_)  # applied to cat(cv2, cv3)
        self.act = nn.LeakyReLU(0.1, inplace=True)
        # 操作符*可以把一个list拆开成一个个独立的元素
        # m 相当于用n次的Bottleneck操作
        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for_ in rang(n)])
    def forward(self, x):   # 正向传播
        # y1相当于 先做一次cv1操作然后进行m操作最后进行cv3操作, 也就是BCSPn模块中的上面的分支操作
        # 输入x ->Conv模块 ->n个bottleneck模块 ->Conv模块 ->y1
        y1 = self.cv3(self.m(self.cv1(x)))
        # y2就是进行cv2操作,也就是BCSPn模块中的下面的分支操作(直接逆行conv操作的分支, Conv--nXBottleneck--conv)
        # 输入x -> Conv模块 -> 输出y2
        y2 = self.cv2(x)
        # 最后y1和y2做拼接, 然后做bn, 然后做act激活, 最后做cv4
        # 输入y1,y2->按照通道数融合 ->归一化 -> 激活函数 -> Conv输出 -> 输出 
        return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))

CSP的模型结构:

CSP瓶颈层结构在Bottleneck部分存在一个可修改的参数n,标识使用的Bottleneck结构个数!这一条也是我们的主分支,是对残差进行学习的主要结构,右侧分支

nn.Conv2d

实际上是shortcut分支实现不同stage的连接。

7:C3

C3模块和BottleneckCSP模块类似,但是少了一个Conv模块,这里就不做赘述

class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions 
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super(C3, self).__init__()
        c_ = int(c2 * e)   # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # act=FReLU(c2)
        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])
        # self.m = nn.Sequential(*[Bottleneck(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)])
    def forward(self, x):
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))

8:SPP

SPP 是空间金字塔池化的缩写

网络架构中的spp模块

过去的卷积神经网络CNN由卷积层+全连接层组成,其中卷积层对于输入数据的大小并没有要求,唯一对数据大小有要求的则是第一个全连接层,因此基本上所有的

CNN都要求数据固定大小,例如注明的VGG模型则要求输入数据大小是(224×224)

固定输入数据大小有两个问题:

1:很多场景所得到的的数据并不是固定大小的,例如不同相机,不同手机,拍出来的图片其宽高比是不固定的;

2:有人说可以对图片进行切割,但是切割的话很可能丢失重要信息。

综上,SPP的提出就是为了解决CNN输入图像大小必须固定的问题,从而可以使得输入图像可以具有任意尺寸

有效避免了对图像区域的裁剪、缩放操作导致的图像失真问题;

有效解决了卷积神经网络对图像重复特征提取的问题,大大提高了产生候选框的速度,且节省了计算成本

SPP被借鉴,用在最小的特征输出层。在Feature Map经过SPP module池化后的特征图重新cat起来传到下一层侦测网络中。此部分SPP最主要的作用就是融合了局部与全局特征,同时兼顾了避免裁剪图像的作用。

class SPP(nn.Module):
    # Spatial pyramid pooling layer used in yolov3-SPP
    # init 构造模块
    def __init__(self, c1, c2, k=(5, 9, 13)):
        super(SPP, self).__init__()
        c_ = c1 // 2  # hidden channels
        # cv1进行Conv操作
        self.cv1 = Conv(c1, c_, 1, 1)  # 1*1的卷积
        # cv2进行Conv操作
        self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
        # m先进行最大池化操作, 然后通过nn.ModuleList进行构造一个模块 在构造时对每一个k都要进行最大池化
        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])
    def forward(self, x):
        # 先进行cv1的操作
        x = self.cv1(x)
        # 对每一个m进行最大池化 和没有做池化的每一个输入进行叠加  然后做拼接 最后做cv2操作
        return  self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
    # torch.cat()是将两个tensor横着拼接在一起
    # 补充: 或者是list列表中的tensor

PP即为空间金字塔池化模块!可视化为:

三种池化核,padding都是根据核的大小自适应,保证池化后的特征图[H, W]保持一致!

如图所示,任何输入尺寸的图片,都会得到16+4+1=21长度的输出,channel数量仍为256不变。

但是在YOLOV3~V5中,SPP作用不同于前。YOLO中的SPP module由四个并行的分支构成,分别是kernel size为 5×5, 9×9, 13×13的最大池化和一个跳跃连接。如下图所示,以YOLOv3-SPP为例,作者检测头前面的第5和第6卷积层之间集成SPP模块来获得,而在随后的YOLOV4/V5中,SPP都被借鉴,用在最小的特征输出层。在Feature Map经过SPP module池化后的特征图重新cat起来传到下一层侦测网络中。此部分SPP最主要的作用就是融合了局部与全局特征。YOLOV3-SPP相比YOLOV3提升了好几个点。

具体SPP的内容理解 可以参考


https://www.cnblogs.com/marsggbo/p/8572846.html


https://blog.csdn.net/weixin_38145317/article/details/106471322?ops_request_misc=&request_id=&biz_id=102&utm_term=class%20SPP(nn.Module):&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-.first_rank_v2_pc_rank_v29&spm=1018.2226.3001.4187


9:

Focus

把输入x分别从(0,0)、(1,0)、(0,1)、(1,1)开始,按步长为2取值,然后进行一次卷积

将输入(b,c,w,h)的shape变成了输出(b, 4c, w/2, h/2)

他将特征层的长和宽都缩减为原来的一半,然后通道数变成原来的4倍,也可以理解成将一个图片等分切成4个,

然后将这四个小的上下堆叠起来

最后在经过一个conv输出

# 把宽度w和高度h的信息整合到c空间中
class Focus(nn.Module):
    # Focus wh information into c-space
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):      # ch_in ch_out kernel, stride, padding, groups
        super(Focus, self).__init__()
        self.conv = Conv(c1 * 4, c2, k, s, p, g, act)
        # self.contract = Contract(gain=2)
    def forward(self, x):   # x(b,c,w,h)  ->  y(b, 4c, w/2, h/2) 有一个下采样的效果
        # 先进行切分, 然后进行拼接, 最后再做conv操作
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2],x[...,::2, 1::2], x[..., 1::2, 1::2]], 1))
        # return self.conv(self.contract(x))

10:以下模块Contract,Expand,Concat是用来处理输入特征的shape的

class Contract(nn.Module):
    # Contract width-height into channels, i.e.x(1,64,80,80)  to x(1,256,40,40)
    def __init__(self, gain=2):
        super().__init__()
        self.gain = gain

    def forward(self,x):
        N, C, H, W = x.size()    # assert (H / s == 0), 'Indivisible gain'
        s = self.gain
        x = x.view(N, C, H // s, s, W // s, s) # x(1,64,40,2,40,2)
        x = x.permute(0,3,5,1,2,4).contiguous()         # x(1,2,2,64,40,40)
        return x.view(N, C * s * s, H // s, W // s)  # x(1, 256, 40, 40)
class Expand(nn.Module):
    # Expand channels into width_heiht, i.e. x(1,64,80,80) to x(1,16,,160,160)
    def __init__(self, gain=2):
        super().__init__()
        self.gain = gain
    def forward(self, x):
        N,  C, H, W = x.size()  # assert C / s ** 2 == 0,  'Indivisible gain'
        s = self.gain
        x = x.view(N, s, s, C // s ** 2, H, W)      # x(1, 2,2,16,80,80)
        x = x.permute(0, 3, 4, 1, 5, 2).contiguous()    # x(1,16,80,2,80,2)
        return x.view(N, C // s ** 2, H * s, W * s)   # x(1,16,160,160)
# 作拼接的一个类
# 拼接函数,将两个tensor进行拼接
class Concat(nn.Module):
    # Concatenate alist of tensors along dimension 沿着维度连接张量
    def __init__(self, dimension=1):
        super(Concat, self).__init__()
        # 指定dimension 在哪一个维度进行拼接
        self.d = dimension
    def forward(self, x):
        # x是一个列表
        return torch.cat(x, self.d)

11:NMS

nms是一个抑制proposals的东西, 将x输入到non_max_suppression里面后,他会处理得到两个参数一个bbox[n,4],还有一个是scores[n,classes],

首先找出每个bbox的score最大的class,然后判断这个score如果大于conf就保留,

接着把保留的bbox判断哪些之间的叫交并比,如果叫交并比大于iou就只保留score比较高的框

# 非极大值抑制 NMS类
class NMS(NN.Module):
    # Non-Maximum Suppression (NMS) module
    conf = 0.25 # confidence threshold 置信度阈值
    iou = 0.45 # iou 阈值
    classes = None # (optional list) filter by class 类别列表

    def __init__(self):
        super(NMS, self).__init__()
    def forward(self, x):
        # 调用non_max_suppression函数进行非极大值抑制
        return non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes)

12:autoShape

autoshape模块在train中不会被调用,当模型训练结束后,会通过这个模块对图片进行重塑,来方便模型的预测

自动调整shape,我们输入的图像可能不一样,可能来自cv2/np/PIL/torch 对输入进行预处理 调整其shape,调整shape在datasets.py文件中,这个实在预测阶段使用的,model.eval(),模型就已经无法训练进入预测模式了

class autoShape(nn.Module):
    # input-robust model wrapper for passing cv2/np/PIL/torch inputs.Includes preprocessing,inference and NMS
    img_size = 640 # inference size (pixels)
    conf = 0.25  # NMS confidence threshold
    iou = 0.45 # NMS Iou threshold
    classes = None # (optional list) filter by class

    def __init__(self,model):
        super(autoshape, self).__init__()
        self.model = model.eval()
    
    def autoshape(self):
        print('autoshape already enabled, skipping...')   # model already converted to model.autoshape()
        return self
    '''
    这里的imgs针对不同的方法读入,官方也给了具体的方法,size是图片的尺寸,就比如最上面图片里面的输入608*608*3'''
    def forward(self, imgs, size=640, augment=False, profile=False):
        # Inference from varius sources. For height =720, width=1280, RGB images example inputs are:
        '''
        filename:   imgs = 'data/samples/zidane.jpg'
        URI:             = 'https://github.com/ultralytics/yolov5/releases/download/v1.0/zidane.jpg'
        OpenCV:          = cv2.imread('image.jpg')[:,:,::-1]  # HWC BGR to RGB x(720,1280,3)
        PIL:             = Image.open('image.jpg')  # HWC x(720,1280,3)
        numpy:           = np.zeros((720,1280,3))  # HWC
        torch:           = torch.zeros(16,3,720,1280)  # BCHW
        multiple:        = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...]  # list of images
        '''

        p = next(self.model.parameters())   # for device and type
        # 吧图片转化成模型输入的形式
        if isintance(imgs, torch.Tensor):
            return self.model(imgs.to(p.device).type_as(p),augment, profile)  # inference

        # pre-process
        # 输入的可能不是一张图片,因为你输入的形式应该是[batch_size,image style],这里获取到输入的n张图片,然后每张图片存储在列表imgs里面
        n, imgs = (len(imgs),imgs) if isinstance(imgs,list) else (1,[imgs])   # number of images, list oj images
        shape0, shape1, files = [], [], []  # image and inference shapes,filenaames
        # 遍历图片
        for i,im in enumerate(imgs):
            # 这个if条件基本上不会满足,他是针对一些特殊需求的,输入的是uri
            # 这其实也是有效的,比如训练了一个黄图监测网络,然后通过爬虫从地址池里爬取img的uri,储存起来,--输入model,效率比较高
            if isinstance(im, str):     # filename or uri
                im =Image.open(requests.get(im, steam=True).raw if im.starswith('http') else im)  # open
            # 这里存放的是图像的名字
            fiels.append(Path(im.filename).wieh_suffix('.jpg').name if isinstance(im, Image.Image) else f'image{i}.jpg')

            '''
            下面的操作主要是以下功用:
            首先将图片转换成numpy格式
            然后将图片原来的[H,W,C]的shape转换成[C,H,W]
            提取图像的[H,W]保存到shape0列表里面
            用输入图像的size(上面默认的640)/max[H,W],就可以得到缩放比例g
            再将原始的[H,W]x缩放比例g得到模型输入的shape保存到shape1列表里面
            然后更新imgs
            '''
            im = np.array(im)   # to numpy
            if im.shape[0]<5:   # image in chw
                im = im.transpose((1,2,0))  # reverse dataloader .transpose(2,0,1)
            im = im[:, :, :3] if im.ndim == 3 else np.tile(im[:, :, None], 3)    # enforce 3ch input
            s = im.shape[:2] # HWC
            shape0.append(s)        # image shape
            g = (size / max(s))   # gain
            shape1.append([y * g for y in s])
            imgs[i] = im # update
        shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)]   # inference shape
        x = [letterbox(im, new_shape=shape1, auto=False)[0] for im in imgs]   # pad
        x = np.stack(x, 0) if n >1 else x[0][None]   # stack
        x = np.ascontiguousarray(x.transpose((0,3,1,2)))   # BHWC to BCHW
        x = torch.from_numpy(x).to(p.device).type_as(p) / 255.    # uint8 to fp16/32

        # Inference
        with torch.no_grad():
            y = self.model(x, augment, profile)[0] # forward
        # 然后对y进行nms处理,就得等到了最终的preds [batch_size, n, 4]和scores [batch_size,n,1]了
        y = non_max_suppression(y, conf_thres=self.conf, iou_thres=self.iou, classes=self.classes)  # NMS
        
        # Post-process
        for i in range(n):
            scale_coords(shape1, y[i][:, :4], shape0[i])
    # 把处理好的内容丢到Detections类中做最后的处理
        return Detections(imgs, y, files, self.names)

13:detection

class Detections:
    # detections class for yolov5 inference results
    def __init__(self, imgs, pred, files, names=None):
        super(Detections, self).__init__()
        d = pred[0].device  # device
        gn = [troch.tensor([*[im.shape[i] for i in [1, 0, 1, 0], 1., 1.],device=d) for im in imgs]   # normlization
        self.imgs = imgs # list of images as numpy arrays
        self.pred = pred # list of tensors pred[0] = (xyxy, conf, cls)
        self.names = names  # class names
        self.files = files # image filename s
        self.xyxy = pred # xyxy pixels
        self.xywh =[xyxy2xywh(x) for x in pred]   # xywh pixels
        self.xywhn = [x/ g for x, g in zip(self.xywh, gn)]   # xywh normalized
        self.xyxyn = [x/ g for x, g in zip(self.xyxy, gn)]  # xyxy normalized
        self.n = len(self.pred)

    def display(self, pprint=False, show=False, save=False, render=False, save_dir=''):
        colors = color_list()
        for i, (img,pred) in enumerate(zip(self.imgs, self.pred)):
            str = f'image {i + 1}/{len(self.pred): {img.shape[0]}x{img,shape[1]} '
            if pred is not None:
                for c in pred[:, -1].unique():
                    n = (pred[:, -1] == c).sum()  # detections per class
                    str += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, "  # add to string 
                if show or save or render:
                    img = Image.fromarray(img.astype(np.uint8)) if isinstance(img, np.ndarray) else img  # from np
                    for *box, conf, cls in pred: # xyxy, confidence, class
                        # str += '%s %.2f, ' % (names[int(cls)], conf)  # label
                        ImageDraw.Draw(img).retangle(box, width=4, outline=colors[int(cls) % 10])    # plot
            
            if pprint:
                print(str.restrip(', '))
            if show:
                img.show(self.files[i])   # show
            if save:
                f = Path(save_dir) / self.files[i]
                img.save(f)  # save
                print(f"{'Saving' * (i == 0)} {f},", end='' if i < self.n -1 else 'done.\n')
            if render:
                self.imgs[i] = np.asarray(img)
    def print(self):
        self.display(pprint=True)   # print results
    
    def show(self):
        self.display(pprint=True)  # show results
    def save(self, save_dir='results/'):
        Path(save_dir).mkdir(exist_ok=True)
        self.display(save=True, save_dir=save_dir)  # save results
    
    def render(self):
        self.display(render=True)  # render results
        return self.imgs
    
    def __len__(self):
        return self.n
    
    def tolist(self):
        # return a list of Detections objects, i.e. 'for result in results.tolists():' 
        # 返回检测对象的列表
        x = [Detections([self.imgs[i]], [self.pred[i]], self.names) for i in range(self.n)]0
        for d in x:
            for k in ['imgs', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']:
                setattr(d, k, getattr(d,k)[0])  # pop out of list
        return x

14:Flatten

之前的版本没有定义这个类, 但是最近的版本好像删掉了 ,在这里我加上了这个类

# 展平
class Flatten(nn.Moudle):
    # use after nn.AdaptiveAvgPool2d(1) to remove last 2 dimensions
    @staticmethod
    def forward(x):
        return x.view(x,size(0),-1)

flatten函数作用是压扁一个张量,这意味着去除所有的轴,只保留一个,他创造了一个单轴的张量,他包含了张量的所有元素。

flatten操作是当我们从一个卷积层过渡到一个全连接层时必须在神经网络中发生的操作

flatten是一种特殊类型的reshape,其中所有的轴都被平滑或压扁在一起

具体内容可参考:


参考1


参考2


参考3

15:Classify

用于第二季分类 比如做车牌的识别 先识别出车牌 如果想对车牌上的字进行识别 就需要二级分类的方法

如果对模型输出的分类 再进行分类 就可以用这个模块

这个类写的比较简单 若进行复杂的二级分类 可以改写

class Classify(nn.Module):
    # Classification head, i.e x(b, c1, 20, 20) to x(b, c2)
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in ch_out kernel, stride, padding, groups
        super(Classify, self).__init__()
        self.aap = nn.AdaptiveAvgPool2d(1)   # to x(b,c1,1,1)  自适应平均池化操作
        self.conv = nn.Conv2d(c1,c2,k,s,autopad(k, p), groups=g)  # to x(b,c2,1,1)
        self.flat = nn.Flatten() # 展平
    
    def forward(self, x):
        # 先自适应平均池化操作, 然后拼接
        z = torch.cat([self.aap(y) for y in (x if isinstance(x, list) else [x])], 1)   # cat if list
        # 对z进行展平操作
        return self.flat(self.conv(z))  # flatten to x(b,c2)

以上内容的结合了各位大佬博客以及自己的理解以及在白勇老师课上的学习,如若侵权,请联系我删除;如有错误,还请大佬不吝指出。

小白学习中。


参考1


参考2


参考3


参考4


参考5


参考6



版权声明:本文为XiaoGShou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。