参考自:(2条消息) 睿智的目标检测56——Pytorch搭建YoloV5目标检测平台_Bubbliiiing的博客-CSDN博客
YOLOv5s网络模型讲解(一看就会)_wisdom_zhe的博客-CSDN博客
YOLOv5 Focus C3 各模块详解及代码实现_c3模块_创不了浩的博客-CSDN博客
YOLOv5-网络结构_whaosoft143的博客-CSDN博客
Pytorch 搭建自己的YoloV5目标检测平台(Bubbliiiing 源码详解 训练 预测)-主干网络介绍_哔哩哔哩_bilibili
源码下载:GitHub - bubbliiiing/yolov5-pytorch: 这是一个YoloV5-pytorch的源码,可以用于训练自己的模型。
1.YOLOV5网络结构图
2.主干网络Backbone
主干特征提取网络为CSPDarknet,其包含Focus模块、Conv(Conv2D_BN_SiLU)模块、Bottleneck模块、CspLayer(C3)模块、SPP(SPPBottleneck)模块。
2.1 Focus模块
Focus模块在v5中是图片进入backbone前,对图片进行切片操作,具体操作是在一张图片中每隔一个像素拿到一个值,类似于邻近下采样,这样就拿到了四张图片,四张图片互补,长的差不多,但是没有信息丢失,这样一来,将W、H信息就集中到了通道空间,输入通道扩充了4倍,即拼接起来的图片相对于原先的RGB三通道模式变成了12个通道,最后将得到的新图片再经过卷积操作,最终得到了没有信息丢失情况下的二倍下采样特征图。
原始的640 × 640 × 3的图像输入Focus结构,采用切片操作,先变成320 × 320 × 12的特征图,再经过一次卷积操作,最终变成320 × 320 × 64的特征图。切片操作如下:
2.2 Conv(Conv2D_BN_SiLU)模块
对输入的特征图进行卷积,归一化,激活函数操作。激活函数为SiLU激活函数,SiLU是Sigmoid和ReLU的改进版。SiLU具备无上界有下界、平滑、非单调的特性。SiLU在深层模型上的效果优于 ReLU。可以看做是平滑的ReLU激活函数。
f(x)=x⋅sigmoid(x)
2.3 Bottleneck模块
先将channel 数减小再扩大(默认减小到一半),具体做法是先进行1×1卷积将channel减小一半,再通过3×3卷积将通道数加倍,并获取特征(共使用两个标准卷积模块),其输入与输出的通道数是不发生改变的。
shortcut参数控制是否进行残差连接(使用ResNet)。
在backbone中的Bottleneck都默认使shortcut为True,在head中的Bottleneck都不使用shortcut。
与ResNet对应的,使用add而非concat进行特征融合,使得融合后的特征数不变
2.4 CspLayer(C3)模块
将原来的残差块的堆叠进行了一个拆分,拆成左右两部分:主干部分继续进行原来的残差块的堆叠;另一部分则像一个残差边一样,经过少量处理直接连接到最后。因此可以认为CSP中存在一个大的残差边。以第一个C3模块为例,如下图:
2.5 SPP(SPPBottleneck)模块
使用了SPP结构,通过不同池化核大小的最大池化进行特征提取,提高网络的感受野。在YoloV4中,SPP是用在FPN里面的,在YoloV5中,SPP模块被用在了主干特征提取网络中。YOLOV5的6.0版本后Neck部分将SPP换成成了SPPF(Glenn Jocher自己设计的),两者的作用是一样的,但后者效率更高。
SPP先通过一个标准卷积模块将输入通道减半,然后分别做kernel-size为5,9,13的maxpooling(对于不同的核大小,padding是自适应的)。对三次最大池化的结果与未进行池化操作的数据进行concat,最终合并后channel数是原来的2倍,再通过一个标准卷积模块将输入通道减半。
完整代码为:
import torch import torch.nn as nn class SiLU(nn.Module): @staticmethod def forward(x): return x * torch.sigmoid(x) def autopad(k, p=None): if p is None: p = k // 2 if isinstance(k, int) else [x // 2 for x in k] return p # -----------------------------------------------------------------------------------# # 实现了一种特殊的特征提取层,它通过对输入张量进行拼接和卷积操作,将输入通道数扩展为输入通道数的四倍,并输出指定的通道数。 # 这种层可以在一些视觉任务中用于提取多尺度的特征。 # ---------------------------------------------------------------------------------# class Focus(nn.Module): 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) def forward(self, x): # 320, 320, 12 => 320, 320, 64 return self.conv( # 640, 640, 3 => 320, 320, 12 torch.cat( [ x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2] ], 1 ) ) class Conv(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): super(Conv, self).__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2, eps=0.001, momentum=0.03) self.act = SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) # --------------------------------------------------------------------# # 首先将输入张量 x 传递给卷积层进行卷积操作。 # 然后将卷积结果传递给批归一化层进行批归一化处理。 # 最后将经过批归一化的张量作为输入传递给激活函数层,并返回激活函数的输出张量。 # --------------------------------------------------------------------# def forward(self, x): return self.act(self.bn(self.conv(x))) def fuseforward(self, x): return self.act(self.conv(x)) # -------------------------------------------------------------------------# # 由两个卷积层组成,通过残差连接将输入和输出进行相加,然后返回最终的输出张量。 # -------------------------------------------------------------------------# class Bottleneck(nn.Module): # Standard bottleneck 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) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) 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(*[CrossConv(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)) class SPP(nn.Module): # Spatial pyramid pooling layer used in YOLOv3-SPP def __init__(self, c1, c2, k=(5, 9, 13)): super(SPP, self).__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k]) def forward(self, x): x = self.cv1(x) return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) class CSPDarknet(nn.Module): def __init__(self, base_channels, base_depth, phi, pretrained): super().__init__() #-----------------------------------------------# # 输入图片是640, 640, 3 # 初始的基本通道base_channels是64 #-----------------------------------------------# #-----------------------------------------------# # 利用focus网络结构进行特征提取 # 640, 640, 3 -> 320, 320, 12 -> 320, 320, 64 #-----------------------------------------------# self.stem = Focus(3, base_channels, k=3) #-----------------------------------------------# # 完成卷积之后,320, 320, 64 -> 160, 160, 128 # 完成CSPlayer之后,160, 160, 128 -> 160, 160, 128 #-----------------------------------------------# self.dark2 = nn.Sequential( # 320, 320, 64 -> 160, 160, 128 Conv(base_channels, base_channels * 2, 3, 2), # 160, 160, 128 -> 160, 160, 128 C3(base_channels * 2, base_channels * 2, base_depth), ) #-----------------------------------------------# # 完成卷积之后,160, 160, 128 -> 80, 80, 256 # 完成CSPlayer之后,80, 80, 256 -> 80, 80, 256 # 在这里引出有效特征层80, 80, 256 # 进行加强特征提取网络FPN的构建 #-----------------------------------------------# self.dark3 = nn.Sequential( Conv(base_channels * 2, base_channels * 4, 3, 2), C3(base_channels * 4, base_channels * 4, base_depth * 3), ) #-----------------------------------------------# # 完成卷积之后,80, 80, 256 -> 40, 40, 512 # 完成CSPlayer之后,40, 40, 512 -> 40, 40, 512 # 在这里引出有效特征层40, 40, 512 # 进行加强特征提取网络FPN的构建 #-----------------------------------------------# self.dark4 = nn.Sequential( Conv(base_channels * 4, base_channels * 8, 3, 2), C3(base_channels * 8, base_channels * 8, base_depth * 3), ) #-----------------------------------------------# # 完成卷积之后,40, 40, 512 -> 20, 20, 1024 # 完成SPP之后,20, 20, 1024 -> 20, 20, 1024 # 完成CSPlayer之后,20, 20, 1024 -> 20, 20, 1024 #-----------------------------------------------# self.dark5 = nn.Sequential( Conv(base_channels * 8, base_channels * 16, 3, 2), SPP(base_channels * 16, base_channels * 16), C3(base_channels * 16, base_channels * 16, base_depth, shortcut=False), ) if pretrained: url = { 's' : 'https://github.com/bubbliiiing/yolov5-pytorch/releases/download/v1.0/cspdarknet_s_backbone.pth', 'm' : 'https://github.com/bubbliiiing/yolov5-pytorch/releases/download/v1.0/cspdarknet_m_backbone.pth', 'l' : 'https://github.com/bubbliiiing/yolov5-pytorch/releases/download/v1.0/cspdarknet_l_backbone.pth', 'x' : 'https://github.com/bubbliiiing/yolov5-pytorch/releases/download/v1.0/cspdarknet_x_backbone.pth', }[phi] checkpoint = torch.hub.load_state_dict_from_url(url=url, map_location="cpu", model_dir="./model_data") self.load_state_dict(checkpoint, strict=False) print("Load weights from ", url.split('/')[-1]) def forward(self, x): x = self.stem(x) x = self.dark2(x) #-----------------------------------------------# # dark3的输出为80, 80, 256,是一个有效特征层 #-----------------------------------------------# x = self.dark3(x) feat1 = x #-----------------------------------------------# # dark4的输出为40, 40, 512,是一个有效特征层 #-----------------------------------------------# x = self.dark4(x) feat2 = x #-----------------------------------------------# # dark5的输出为20, 20, 1024,是一个有效特征层 #-----------------------------------------------# x = self.dark5(x) feat3 = x return feat1, feat2, feat3
3.FPN特征金字塔进行加强特征提取
YoloV5一共提取三个特征层进行目标检测,三个特征层的shape分别为feat1=(80,80,256)、feat2=(40,40,512)、feat3=(20,20,1024)。
在获得三个有效特征层后,我们利用这三个有效特征层进行FPN层的构建,构建方式为:
feat3=(20,20,1024)的特征层进行1次1X1卷积调整通道后获得P5,P5进行上采样UmSampling2d后与feat2=(40,40,512)特征层进行结合,然后使用CSPLayer进行特征提取获得P5_upsample,此时获得的特征层为(40,40,512)。
P5_upsample=(40,40,512)的特征层进行1次1X1卷积调整通道后获得P4,P4进行上采样UmSampling2d后与feat1=(80,80,256)特征层进行结合,然后使用CSPLayer进行特征提取P3_out,此时获得的特征层为(80,80,256)。
P3_out=(80,80,256)的特征层进行一次3x3卷积进行下采样,下采样后与P4堆叠,然后使用CSPLayer进行特征提取P4_out,此时获得的特征层为(40,40,512)。
P4_out=(40,40,512)的特征层进行一次3x3卷积进行下采样,下采样后与P5堆叠,然后使用CSPLayer进行特征提取P5_out,此时获得的特征层为(20,20,1024)。
特征金字塔可以将不同shape的特征层进行特征融合,有利于提取出更好的特征。
如下图所示:
利用FPN特征金字塔,我们可以获得三个加强特征,这三个加强特征的shape分别为(20,20,1024)、(40,40,512)、(80,80,256),然后我们利用这三个shape的特征层传入Yolo Head获得预测结果。
对于每一个特征层,我们可以获得利用一个1X1卷积调整通道数,最终的通道数和需要区分的种类个数相关,在YoloV5里,每一个特征层上每一个特征点存在3个先验框。
如果使用的是voc训练集,类则为20种,最后的维度应该为75 = 3x25,三个特征层的shape为(20,20,75),(40,40,75),(80,80,75)。
最后的75可以拆分成3个25,对应3个先验框的25个参数,25可以拆分成4+1+20。
前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框;
第5个参数用于判断每一个特征点是否包含物体;
最后20个参数用于判断每一个特征点所包含的物体种类。
完整代码为:
import torch import torch.nn as nn from nets.ConvNext import ConvNeXt_Small, ConvNeXt_Tiny from nets.CSPdarknet import C3, Conv, CSPDarknet from nets.Swin_transformer import Swin_transformer_Tiny # ---------------------------------------------------# # yolo_body # ---------------------------------------------------# class YoloBody(nn.Module): def __init__(self, anchors_mask, num_classes, phi, backbone='cspdarknet', pretrained=False, input_shape=[640, 640]): super(YoloBody, self).__init__() depth_dict = {'s' : 0.33, 'm' : 0.67, 'l' : 1.00, 'x' : 1.33,} width_dict = {'s' : 0.50, 'm' : 0.75, 'l' : 1.00, 'x' : 1.25,} dep_mul, wid_mul = depth_dict[phi], width_dict[phi] base_channels = int(wid_mul * 64) # 64 base_depth = max(round(dep_mul * 3), 1) # 3 # -----------------------------------------------# # 输入图片是640, 640, 3 # 初始的基本通道是64 # -----------------------------------------------# self.backbone_name = backbone if backbone == "cspdarknet": # ---------------------------------------------------# # 生成CSPdarknet53的主干模型 # 获得三个有效特征层,他们的shape分别是: # 80,80,256 # 40,40,512 # 20,20,1024 # ---------------------------------------------------# self.backbone = CSPDarknet(base_channels, base_depth, phi, pretrained) else: #---------------------------------------------------# # 如果输入不为cspdarknet,则调整通道数 # 使其符合YoloV5的格式 #---------------------------------------------------# self.backbone = { 'convnext_tiny' : ConvNeXt_Tiny, 'convnext_small' : ConvNeXt_Small, 'swin_transfomer_tiny' : Swin_transformer_Tiny, }[backbone](pretrained=pretrained, input_shape=input_shape) in_channels = { 'convnext_tiny' : [192, 384, 768], 'convnext_small' : [192, 384, 768], 'swin_transfomer_tiny' : [192, 384, 768], }[backbone] feat1_c, feat2_c, feat3_c = in_channels self.conv_1x1_feat1 = Conv(feat1_c, base_channels * 4, 1, 1) self.conv_1x1_feat2 = Conv(feat2_c, base_channels * 8, 1, 1) self.conv_1x1_feat3 = Conv(feat3_c, base_channels * 16, 1, 1) self.upsample = nn.Upsample(scale_factor=2, mode="nearest") self.conv_for_feat3 = Conv(base_channels * 16, base_channels * 8, 1, 1) self.conv3_for_upsample1 = C3(base_channels * 16, base_channels * 8, base_depth, shortcut=False) self.conv_for_feat2 = Conv(base_channels * 8, base_channels * 4, 1, 1) self.conv3_for_upsample2 = C3(base_channels * 8, base_channels * 4, base_depth, shortcut=False) self.down_sample1 = Conv(base_channels * 4, base_channels * 4, 3, 2) self.conv3_for_downsample1 = C3(base_channels * 8, base_channels * 8, base_depth, shortcut=False) self.down_sample2 = Conv(base_channels * 8, base_channels * 8, 3, 2) self.conv3_for_downsample2 = C3(base_channels * 16, base_channels * 16, base_depth, shortcut=False) # 80, 80, 256 => 80, 80, 3 * (5 + num_classes) => 80, 80, 3 * (4 + 1 + num_classes) self.yolo_head_P3 = nn.Conv2d(base_channels * 4, len(anchors_mask[2]) * (5 + num_classes), 1) # 40, 40, 512 => 40, 40, 3 * (5 + num_classes) => 40, 40, 3 * (4 + 1 + num_classes) self.yolo_head_P4 = nn.Conv2d(base_channels * 8, len(anchors_mask[1]) * (5 + num_classes), 1) # 20, 20, 1024 => 20, 20, 3 * (5 + num_classes) => 20, 20, 3 * (4 + 1 + num_classes) self.yolo_head_P5 = nn.Conv2d(base_channels * 16, len(anchors_mask[0]) * (5 + num_classes), 1) def forward(self, x): # backbone feat1, feat2, feat3 = self.backbone(x) if self.backbone_name != "cspdarknet": feat1 = self.conv_1x1_feat1(feat1) feat2 = self.conv_1x1_feat2(feat2) feat3 = self.conv_1x1_feat3(feat3) # 20, 20, 1024 -> 20, 20, 512 P5 = self.conv_for_feat3(feat3) # 20, 20, 512 -> 40, 40, 512 P5_upsample = self.upsample(P5) # 40, 40, 512 -> 40, 40, 1024 P4 = torch.cat([P5_upsample, feat2], 1) # 40, 40, 1024 -> 40, 40, 512 P4 = self.conv3_for_upsample1(P4) # 40, 40, 512 -> 40, 40, 256 P4 = self.conv_for_feat2(P4) # 40, 40, 256 -> 80, 80, 256 P4_upsample = self.upsample(P4) # 80, 80, 256 cat 80, 80, 256 -> 80, 80, 512 P3 = torch.cat([P4_upsample, feat1], 1) # 80, 80, 512 -> 80, 80, 256 P3 = self.conv3_for_upsample2(P3) # 80, 80, 256 -> 40, 40, 256 P3_downsample = self.down_sample1(P3) # 40, 40, 256 cat 40, 40, 256 -> 40, 40, 512 P4 = torch.cat([P3_downsample, P4], 1) # 40, 40, 512 -> 40, 40, 512 P4 = self.conv3_for_downsample1(P4) # 40, 40, 512 -> 20, 20, 512 P4_downsample = self.down_sample2(P4) # 20, 20, 512 cat 20, 20, 512 -> 20, 20, 1024 P5 = torch.cat([P4_downsample, P5], 1) # 20, 20, 1024 -> 20, 20, 1024 P5 = self.conv3_for_downsample2(P5) # ---------------------------------------------------# # 第三个特征层 # y3=(batch_size,75,80,80) # ---------------------------------------------------# out2 = self.yolo_head_P3(P3) #---------------------------------------------------# # 第二个特征层 # y2=(batch_size,75,40,40) # ---------------------------------------------------# out1 = self.yolo_head_P4(P4) # ---------------------------------------------------# # 第一个特征层 # y1=(batch_size,75,20,20) # ---------------------------------------------------# out0 = self.yolo_head_P5(P5) return out0, out1, out2
4.预测结果解码
获得预测框:
参考自:睿智的目标检测56——Pytorch搭建YoloV5目标检测平台_Bubbliiiing的博客-CSDN博客
(我自己写的总觉得差了点意思,就将大佬写的搬运过来)
由以上我们可以获得三个特征层的预测结果,shape分别为(N,20,20,75),(N,40,40,75),(N,20,20,75)的数据。但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。在YoloV5里,每一个特征层上每一个特征点存在3个先验框。
每个特征层最后的75可以拆分成3个25,对应3个先验框的25个参数,我们先将其reshape一下,其结果为(N,20,20,3,25),(N,40.40,3,25),(N,20,20,3,25)。
其中的25可以拆分成4+1+20。
前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框;
第5个参数用于判断每一个特征点是否包含物体;
最后20个参数用于判断每一个特征点所包含的物体种类。
以(N,20,20,3,25)这个特征层为例,该特征层相当于将图像划分成20x20个特征点,如果某个特征点落在物体的对应框内,就用于预测该物体。
如图所示,蓝色的点为20x20的特征点,此时我们对左图黑色点的三个先验框进行解码操作演示:
1、进行中心预测点的计算,利用Regression预测结果前两个序号的内容对特征点的三个先验框中心坐标进行偏移,偏移后是右图红色的三个点;
2、进行预测框宽高的计算,利用Regression预测结果后两个序号的内容求指数后获得预测框的宽高;
3、此时获得的预测框就可以绘制在图片上了。
代码解读(以(N,20,20,75)为例)
生成网格,先验框中心和网格左上角,按照网格格式生成先验框的宽高(如上图)
grid_x为网格坐标点,x.data * 2. - 0.5为调整因子,调整范围:x :0 ~ 1 => 0 ~ 2 => -0.5, 1.5 => 负责一定范围的目标的预测。(y与x一样)
anchor_w, anchor_h以第一个框为例,即上图(3.625,2.8125),(w.data * 2) ** 2为调整因子,调整范围:w 0 ~ 1 => 0 ~ 2 => 0 ~ 4 => 先验框的宽高调节范围为0~4倍。(h类似)
pred_boxes为[...,(x,y,w,h)],(x,y,w,h为调整后的x,y,w,h)
_scale为[20.0,20.0,20.0,20.0] ,目的是将[x,y,w,h]调整到[0,1]。
outputs为[1,3,20,20,(x,y,w,h,conf,pred_cls)] 。
完整代码为
def decode_box(self, inputs): outputs = [] for i, input in enumerate(inputs): #-----------------------------------------------# # 输入的input一共有三个,他们的shape分别是 # batch_size = 1 # batch_size, 3 * (4 + 1 + 80), 20, 20 # batch_size, 255, 40, 40 # batch_size, 255, 80, 80 #-----------------------------------------------# batch_size = input.size(0) input_height = input.size(2) input_width = input.size(3) #-----------------------------------------------# # 输入为640x640时 # stride_h = stride_w = 32、16、8 #-----------------------------------------------# stride_h = self.input_shape[0] / input_height stride_w = self.input_shape[1] / input_width #-------------------------------------------------# # 此时获得的scaled_anchors大小是相对于特征层的 #-------------------------------------------------# scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors[self.anchors_mask[i]]] #-----------------------------------------------# # 输入的input一共有三个,他们的shape分别是 # batch_size, 3, 20, 20, 85 # batch_size, 3, 40, 40, 85 # batch_size, 3, 80, 80, 85 #-----------------------------------------------# prediction = input.view(batch_size, len(self.anchors_mask[i]), self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous() #-----------------------------------------------# # 先验框的中心位置的调整参数 #-----------------------------------------------# x = torch.sigmoid(prediction[..., 0]) y = torch.sigmoid(prediction[..., 1]) #-----------------------------------------------# # 先验框的宽高调整参数 #-----------------------------------------------# w = torch.sigmoid(prediction[..., 2]) h = torch.sigmoid(prediction[..., 3]) #-----------------------------------------------# # 获得置信度,是否有物体 #-----------------------------------------------# conf = torch.sigmoid(prediction[..., 4]) #-----------------------------------------------# # 种类置信度 #-----------------------------------------------# pred_cls = torch.sigmoid(prediction[..., 5:]) FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor #----------------------------------------------------------# # 生成网格,先验框中心,网格左上角 # batch_size,3,20,20 #----------------------------------------------------------# grid_x = torch.linspace(0, input_width - 1, input_width).repeat(input_height, 1).repeat( batch_size * len(self.anchors_mask[i]), 1, 1).view(x.shape).type(FloatTensor) grid_y = torch.linspace(0, input_height - 1, input_height).repeat(input_width, 1).t().repeat( batch_size * len(self.anchors_mask[i]), 1, 1).view(y.shape).type(FloatTensor) #----------------------------------------------------------# # 按照网格格式生成先验框的宽高 # batch_size,3,20,20 #----------------------------------------------------------# anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0])) anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1])) anchor_w = anchor_w.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(w.shape) anchor_h = anchor_h.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(h.shape) #----------------------------------------------------------# # 利用预测结果对先验框进行调整 # 首先调整先验框的中心,从先验框中心向右下角偏移 # 再调整先验框的宽高。 # x 0 ~ 1 => 0 ~ 2 => -0.5, 1.5 => 负责一定范围的目标的预测 # y 0 ~ 1 => 0 ~ 2 => -0.5, 1.5 => 负责一定范围的目标的预测 # w 0 ~ 1 => 0 ~ 2 => 0 ~ 4 => 先验框的宽高调节范围为0~4倍 # h 0 ~ 1 => 0 ~ 2 => 0 ~ 4 => 先验框的宽高调节范围为0~4倍 #----------------------------------------------------------# pred_boxes = FloatTensor(prediction[..., :4].shape) pred_boxes[..., 0] = x.data * 2. - 0.5 + grid_x pred_boxes[..., 1] = y.data * 2. - 0.5 + grid_y pred_boxes[..., 2] = (w.data * 2) ** 2 * anchor_w pred_boxes[..., 3] = (h.data * 2) ** 2 * anchor_h #----------------------------------------------------------# # 将输出结果归一化成小数的形式 #----------------------------------------------------------# _scale = torch.Tensor([input_width, input_height, input_width, input_height]).type(FloatTensor) output = torch.cat((pred_boxes.view(batch_size, -1, 4) / _scale, conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_classes)), -1) outputs.append(output.data) return outputs
修正预测框:(仅供参考,不一定对)
box_xy:预测框的中心坐标(x, y)。
box_wh:预测框的宽度和高度(w, h)。
input_shape:模型的输入形状(宽度,高度)。
image_shape:图像的原始形状(宽度,高度)。
letterbox_image:一个布尔值,表示是否对图像进行了 letterbox 缩放
接下来,如果letterbox_image为True,表示图像经过了 letterbox 缩放。在这种情况下,需要将预测框的坐标和尺寸进行修正。
首先,计算出缩放后的新形状new_shape,通过图像形状与输入形状比值的最小值乘以原始图像形状得到。
然后,计算缩放的偏移量offset,用于调整预测框的坐标,使其相对于图像左上角的偏移保持不变。
接下来,计算缩放的比例scale,用于调整预测框的尺寸,使其与图像形状相适应。
最后,根据偏移量和缩放比例对预测框的中心坐标和尺寸进行修正,得到缩放后的预测框box_yx和box_hw。
最后,根据修正后的预测框坐标和尺寸,计算预测框的最小坐标box_mins和最大坐标box_maxes。然后,使用np.concatenate将最小坐标和最大坐标进行拼接,得到修正后的预测框boxes。
最终,将修正后的预测框乘以原始图像的形状,并返回结果boxes,表示修正后的预测框在原始图像中的坐标。
完整代码为
def yolo_correct_boxes(self, box_xy, box_wh, input_shape, image_shape, letterbox_image): #-----------------------------------------------------------------# # 把y轴放前面是因为方便预测框和图像的宽高进行相乘 #-----------------------------------------------------------------# box_yx = box_xy[..., ::-1] box_hw = box_wh[..., ::-1] input_shape = np.array(input_shape) image_shape = np.array(image_shape) if letterbox_image: #-----------------------------------------------------------------# # 这里求出来的offset是图像有效区域相对于图像左上角的偏移情况 # new_shape指的是宽高缩放情况 #-----------------------------------------------------------------# new_shape = np.round(image_shape * np.min(input_shape/image_shape)) offset = (input_shape - new_shape)/2./input_shape scale = input_shape/new_shape box_yx = (box_yx - offset) * scale box_hw *= scale box_mins = box_yx - (box_hw / 2.) box_maxes = box_yx + (box_hw / 2.) boxes = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1) boxes *= np.concatenate([image_shape, image_shape], axis=-1) return boxes
得分筛选与非极大抑制
(大佬写的很好,膜拜大佬参考自:睿智的目标检测56——Pytorch搭建YoloV5目标检测平台_Bubbliiiing的博客-CSDN博客)
得到最终的预测结果后还要进行得分排序与非极大抑制筛选。
得分筛选就是筛选出得分满足confidence置信度的预测框。
非极大抑制就是筛选出一定区域内属于同一种类得分最大的框。
得分筛选与非极大抑制的过程可以概括如下:
1、找出该图片中得分大于门限函数的框。在进行重合框筛选前就进行得分的筛选可以大幅度减少框的数量。
2、对种类进行循环,非极大抑制的作用是筛选出一定区域内属于同一种类得分最大的框,对种类进行循环可以帮助我们对每一个类分别进行非极大抑制。
3、根据得分对该种类进行从大到小排序。
4、每次取出得分最大的框,计算其与其它所有预测框的重合程度,重合程度过大的则剔除。
得分筛选与非极大抑制后的结果就可以用于绘制预测框了。
下图是经过非极大抑制的。
下图是未经过非极大抑制的。
完整代码为
def non_max_suppression(self, prediction, num_classes, input_shape, image_shape, letterbox_image, conf_thres=0.5, nms_thres=0.4): #----------------------------------------------------------# # 将预测结果的格式转换成左上角右下角的格式。 # prediction [batch_size, num_anchors, 85] #----------------------------------------------------------# box_corner = np.zeros_like(prediction) box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2 box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2 box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2 box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2 prediction[:, :, :4] = box_corner[:, :, :4] output = [None for _ in range(len(prediction))] for i, image_pred in enumerate(prediction): #----------------------------------------------------------# # 对种类预测部分取max。 # class_conf [num_anchors, 1] 种类置信度 # class_pred [num_anchors, 1] 种类 #----------------------------------------------------------# class_conf = np.max(image_pred[:, 5:5 + num_classes], 1, keepdims=True) class_pred = np.expand_dims(np.argmax(image_pred[:, 5:5 + num_classes], 1), -1) #----------------------------------------------------------# # 利用置信度进行第一轮筛选 #----------------------------------------------------------# conf_mask = np.squeeze((image_pred[:, 4] * class_conf[:, 0] >= conf_thres)) #----------------------------------------------------------# # 根据置信度进行预测结果的筛选 #----------------------------------------------------------# image_pred = image_pred[conf_mask] class_conf = class_conf[conf_mask] class_pred = class_pred[conf_mask] if not np.shape(image_pred)[0]: continue #-------------------------------------------------------------------------# # detections [num_anchors, 7] # 7的内容为:x1, y1, x2, y2, obj_conf, class_conf, class_pred #-------------------------------------------------------------------------# detections = np.concatenate((image_pred[:, :5], class_conf, class_pred), 1) #------------------------------------------# # 获得预测结果中包含的所有种类 #------------------------------------------# unique_labels = np.unique(detections[:, -1]) for c in unique_labels: #------------------------------------------# # 获得某一类得分筛选后全部的预测结果 #------------------------------------------# detections_class = detections[detections[:, -1] == c] # 按照存在物体的置信度排序 conf_sort_index = np.argsort(detections_class[:, 4] * detections_class[:, 5])[::-1] detections_class = detections_class[conf_sort_index] # 进行非极大抑制 max_detections = [] while np.shape(detections_class)[0]: # 取出这一类置信度最高的,一步一步往下判断,判断重合程度是否大于nms_thres,如果是则去除掉 max_detections.append(detections_class[0:1]) if len(detections_class) == 1: break ious = self.bbox_iou(max_detections[-1], detections_class[1:]) detections_class = detections_class[1:][ious < nms_thres] # 堆叠 max_detections = np.concatenate(max_detections, 0) # Add max detections to outputs output[i] = max_detections if output[i] is None else np.concatenate((output[i], max_detections)) if output[i] is not None: output[i] = output[i] box_xy, box_wh = (output[i][:, 0:2] + output[i][:, 2:4])/2, output[i][:, 2:4] - output[i][:, 0:2] output[i][:, :4] = self.yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image) return output
5. 损失函数
嗯,代码是参考大佬的,总感觉大佬的损失函数跟yolov3没多少区别,可能是我刚学,不了解哈。直接将大佬的解释搬过来吧。
睿智的目标检测56——Pytorch搭建YoloV5目标检测平台_Bubbliiiing的博客-CSDN博客
GitHub - bubbliiiing/yolov5-pytorch: 这是一个YoloV5-pytorch的源码,可以用于训练自己的模型。
YoloV5的损失由三个部分组成:
1、Reg部分,由第2部分可知道每个真实框对应的先验框,获取到每个框对应的先验框后,取出该先验框对应的预测框,利用真实框和预测框计算CIOU损失,作为Reg部分的Loss组成。
2、Obj部分,由第2部分可知道每个真实框对应的先验框,所有真实框对应的先验框都是正样本,剩余的先验框均为负样本,根据正负样本和特征点的是否包含物体的预测结果计算交叉熵损失,作为Obj部分的Loss组成。
3、Cls部分,由第三部分可知道每个真实框对应的先验框,获取到每个框对应的先验框后,取出该先验框的种类预测结果,根据真实框的种类和先验框的种类预测结果计算交叉熵损失,作为Cls部分的Loss组成。
完整代码为
import math from copy import deepcopy from functools import partial import numpy as np import torch import torch.nn as nn class YOLOLoss(nn.Module): def __init__(self, anchors, num_classes, input_shape, cuda, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]], label_smoothing = 0): super(YOLOLoss, self).__init__() # -----------------------------------------------------------# # 20x20的特征层对应的anchor是[116,90],[156,198],[373,326] # 40x40的特征层对应的anchor是[30,61],[62,45],[59,119] # 80x80的特征层对应的anchor是[10,13],[16,30],[33,23] # -----------------------------------------------------------# self.anchors = anchors self.num_classes = num_classes self.bbox_attrs = 5 + num_classes self.input_shape = input_shape self.anchors_mask = anchors_mask self.label_smoothing = label_smoothing self.threshold = 4 self.balance = [0.4, 1.0, 4] self.box_ratio = 0.05 self.obj_ratio = 1 * (input_shape[0] * input_shape[1]) / (640 ** 2) self.cls_ratio = 0.5 * (num_classes / 80) self.cuda = cuda def clip_by_tensor(self, t, t_min, t_max): t = t.float() result = (t >= t_min).float() * t + (t < t_min).float() * t_min result = (result <= t_max).float() * result + (result > t_max).float() * t_max return result def MSELoss(self, pred, target): return torch.pow(pred - target, 2) def BCELoss(self, pred, target): epsilon = 1e-7 pred = self.clip_by_tensor(pred, epsilon, 1.0 - epsilon) output = - target * torch.log(pred) - (1.0 - target) * torch.log(1.0 - pred) return output def box_giou(self, b1, b2): """ 输入为: ---------- b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh 返回为: ------- giou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1) """ # ----------------------------------------------------# # 求出预测框左上角右下角 # ----------------------------------------------------# b1_xy = b1[..., :2] b1_wh = b1[..., 2:4] b1_wh_half = b1_wh/2. b1_mins = b1_xy - b1_wh_half b1_maxes = b1_xy + b1_wh_half # ----------------------------------------------------# # 求出真实框左上角右下角 # ----------------------------------------------------# b2_xy = b2[..., :2] b2_wh = b2[..., 2:4] b2_wh_half = b2_wh/2. b2_mins = b2_xy - b2_wh_half b2_maxes = b2_xy + b2_wh_half # ----------------------------------------------------# # 求真实框和预测框所有的iou # ----------------------------------------------------# intersect_mins = torch.max(b1_mins, b2_mins) intersect_maxes = torch.min(b1_maxes, b2_maxes) intersect_wh = torch.max(intersect_maxes - intersect_mins, torch.zeros_like(intersect_maxes)) intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] b1_area = b1_wh[..., 0] * b1_wh[..., 1] b2_area = b2_wh[..., 0] * b2_wh[..., 1] union_area = b1_area + b2_area - intersect_area iou = intersect_area / union_area # ----------------------------------------------------# # 找到包裹两个框的最小框的左上角和右下角 # ----------------------------------------------------# enclose_mins = torch.min(b1_mins, b2_mins) enclose_maxes = torch.max(b1_maxes, b2_maxes) enclose_wh = torch.max(enclose_maxes - enclose_mins, torch.zeros_like(intersect_maxes)) # ----------------------------------------------------# # 计算对角线距离 # ----------------------------------------------------# enclose_area = enclose_wh[..., 0] * enclose_wh[..., 1] giou = iou - (enclose_area - union_area) / enclose_area return giou # ---------------------------------------------------# # 平滑标签 # ---------------------------------------------------# def smooth_labels(self, y_true, label_smoothing, num_classes): return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes def forward(self, l, input, targets=None, y_true=None): # ----------------------------------------------------# # l 代表使用的是第几个有效特征层 # input的shape为 bs, 3*(5+num_classes), 20, 20 # bs, 3*(5+num_classes), 40, 40 # bs, 3*(5+num_classes), 80, 80 # targets 真实框的标签情况 [batch_size, num_gt, 5] # ----------------------------------------------------# # --------------------------------# # 获得图片数量,特征层的高和宽 # 20, 20 # --------------------------------# bs = input.size(0) in_h = input.size(2) in_w = input.size(3) # -----------------------------------------------------------------------# # 计算步长 # 每一个特征点对应原来的图片上多少个像素点 # [640, 640] 高的步长为640 / 20 = 32,宽的步长为640 / 20 = 32 # 如果特征层为20x20的话,一个特征点就对应原来的图片上的32个像素点 # 如果特征层为40x40的话,一个特征点就对应原来的图片上的16个像素点 # 如果特征层为80x80的话,一个特征点就对应原来的图片上的8个像素点 # stride_h = stride_w = 32、16、8 # -----------------------------------------------------------------------# stride_h = self.input_shape[0] / in_h stride_w = self.input_shape[1] / in_w # -------------------------------------------------# # 此时获得的scaled_anchors大小是相对于特征层的 # -------------------------------------------------# scaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in self.anchors] # -----------------------------------------------# # 输入的input一共有三个,他们的shape分别是 # bs, 3 * (5+num_classes), 20, 20 => bs, 3, 5 + num_classes, 20, 20 => batch_size, 3, 20, 20, 5 + num_classes # batch_size, 3, 20, 20, 5 + num_classes # batch_size, 3, 40, 40, 5 + num_classes # batch_size, 3, 80, 80, 5 + num_classes # -----------------------------------------------# prediction = input.view(bs, len(self.anchors_mask[l]), self.bbox_attrs, in_h, in_w).permute(0, 1, 3, 4, 2).contiguous() # -----------------------------------------------# # 先验框的中心位置的调整参数 # -----------------------------------------------# x = torch.sigmoid(prediction[..., 0]) y = torch.sigmoid(prediction[..., 1]) # -----------------------------------------------# # 先验框的宽高调整参数 # -----------------------------------------------# w = torch.sigmoid(prediction[..., 2]) h = torch.sigmoid(prediction[..., 3]) # -----------------------------------------------# # 获得置信度,是否有物体 # -----------------------------------------------# conf = torch.sigmoid(prediction[..., 4]) # -----------------------------------------------# # 种类置信度 # -----------------------------------------------# pred_cls = torch.sigmoid(prediction[..., 5:]) # -----------------------------------------------# # self.get_target已经合并到dataloader中 # 原因是在这里执行过慢,会大大延长训练时间 # -----------------------------------------------# # y_true, noobj_mask = self.get_target(l, targets, scaled_anchors, in_h, in_w) # ---------------------------------------------------------------# # 将预测结果进行解码,判断预测结果和真实值的重合程度 # 如果重合程度过大则忽略,因为这些特征点属于预测比较准确的特征点 # 作为负样本不合适 # ----------------------------------------------------------------# pred_boxes = self.get_pred_boxes(l, x, y, h, w, targets, scaled_anchors, in_h, in_w) if self.cuda: y_true = y_true.type_as(x) loss = 0 n = torch.sum(y_true[..., 4] == 1) if n != 0: # ---------------------------------------------------------------# # 计算预测结果和真实结果的giou,计算对应有真实框的先验框的giou损失 # loss_cls计算对应有真实框的先验框的分类损失 # ----------------------------------------------------------------# giou = self.box_giou(pred_boxes, y_true[..., :4]).type_as(x) loss_loc = torch.mean((1 - giou)[y_true[..., 4] == 1]) loss_cls = torch.mean(self.BCELoss(pred_cls[y_true[..., 4] == 1], self.smooth_labels(y_true[..., 5:][y_true[..., 4] == 1], self.label_smoothing, self.num_classes))) loss += loss_loc * self.box_ratio + loss_cls * self.cls_ratio # -----------------------------------------------------------# # 计算置信度的loss # 也就意味着先验框对应的预测框预测的更准确 # 它才是用来预测这个物体的。 # -----------------------------------------------------------# tobj = torch.where(y_true[..., 4] == 1, giou.detach().clamp(0), torch.zeros_like(y_true[..., 4])) else: tobj = torch.zeros_like(y_true[..., 4]) loss_conf = torch.mean(self.BCELoss(conf, tobj)) loss += loss_conf * self.balance[l] * self.obj_ratio # if n != 0: # print(loss_loc * self.box_ratio, loss_cls * self.cls_ratio, loss_conf * self.balance[l] * self.obj_ratio) return loss def get_near_points(self, x, y, i, j): sub_x = x - i sub_y = y - j if sub_x > 0.5 and sub_y > 0.5: return [[0, 0], [1, 0], [0, 1]] elif sub_x < 0.5 and sub_y > 0.5: return [[0, 0], [-1, 0], [0, 1]] elif sub_x < 0.5 and sub_y < 0.5: return [[0, 0], [-1, 0], [0, -1]] else: return [[0, 0], [1, 0], [0, -1]] def get_target(self, l, targets, anchors, in_h, in_w): # -----------------------------------------------------# # 计算一共有多少张图片 # -----------------------------------------------------# bs = len(targets) # -----------------------------------------------------# # 用于选取哪些先验框不包含物体 # bs, 3, 20, 20 # -----------------------------------------------------# noobj_mask = torch.ones(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) # -----------------------------------------------------# # 帮助找到每一个先验框最对应的真实框 # -----------------------------------------------------# box_best_ratio = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) # -----------------------------------------------------# # batch_size, 3, 20, 20, 5 + num_classes # -----------------------------------------------------# y_true = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, self.bbox_attrs, requires_grad = False) for b in range(bs): if len(targets[b])==0: continue batch_target = torch.zeros_like(targets[b]) # -------------------------------------------------------# # 计算出正样本在特征层上的中心点 # 获得真实框相对于特征层的大小 # -------------------------------------------------------# batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h batch_target[:, 4] = targets[b][:, 4] batch_target = batch_target.cpu() # -----------------------------------------------------------------------------# # batch_target : num_true_box, 5 # batch_target[:, 2:4] : num_true_box, 2 # torch.unsqueeze(batch_target[:, 2:4], 1) : num_true_box, 1, 2 # anchors : 9, 2 # torch.unsqueeze(torch.FloatTensor(anchors), 0) : 1, 9, 2 # ratios_of_gt_anchors : num_true_box, 9, 2 # ratios_of_anchors_gt : num_true_box, 9, 2 # # ratios : num_true_box, 9, 4 # max_ratios : num_true_box, 9 # max_ratios每一个真实框和每一个先验框的最大宽高比! # ------------------------------------------------------------------------------# ratios_of_gt_anchors = torch.unsqueeze(batch_target[:, 2:4], 1) / torch.unsqueeze(torch.FloatTensor(anchors), 0) ratios_of_anchors_gt = torch.unsqueeze(torch.FloatTensor(anchors), 0) / torch.unsqueeze(batch_target[:, 2:4], 1) ratios = torch.cat([ratios_of_gt_anchors, ratios_of_anchors_gt], dim = -1) max_ratios, _ = torch.max(ratios, dim = -1) for t, ratio in enumerate(max_ratios): # -------------------------------------------------------# # ratio : 9 # -------------------------------------------------------# over_threshold = ratio < self.threshold over_threshold[torch.argmin(ratio)] = True for k, mask in enumerate(self.anchors_mask[l]): if not over_threshold[mask]: continue # ----------------------------------------# # 获得真实框属于哪个网格点 # x 1.25 => 1 # y 3.75 => 3 # ----------------------------------------# i = torch.floor(batch_target[t, 0]).long() j = torch.floor(batch_target[t, 1]).long() offsets = self.get_near_points(batch_target[t, 0], batch_target[t, 1], i, j) for offset in offsets: local_i = i + offset[0] local_j = j + offset[1] if local_i >= in_w or local_i < 0 or local_j >= in_h or local_j < 0: continue if box_best_ratio[b, k, local_j, local_i] != 0: if box_best_ratio[b, k, local_j, local_i] > ratio[mask]: y_true[b, k, local_j, local_i, :] = 0 else: continue # ----------------------------------------# # 取出真实框的种类 # ----------------------------------------# c = batch_target[t, 4].long() # ----------------------------------------# # noobj_mask代表无目标的特征点 # ----------------------------------------# noobj_mask[b, k, local_j, local_i] = 0 # ----------------------------------------# # tx、ty代表中心调整参数的真实值 # ----------------------------------------# y_true[b, k, local_j, local_i, 0] = batch_target[t, 0] y_true[b, k, local_j, local_i, 1] = batch_target[t, 1] y_true[b, k, local_j, local_i, 2] = batch_target[t, 2] y_true[b, k, local_j, local_i, 3] = batch_target[t, 3] y_true[b, k, local_j, local_i, 4] = 1 y_true[b, k, local_j, local_i, c + 5] = 1 # ----------------------------------------# # 获得当前先验框最好的比例 # ----------------------------------------# box_best_ratio[b, k, local_j, local_i] = ratio[mask] return y_true, noobj_mask def get_pred_boxes(self, l, x, y, h, w, targets, scaled_anchors, in_h, in_w): # -----------------------------------------------------# # 计算一共有多少张图片 # -----------------------------------------------------# bs = len(targets) # -----------------------------------------------------# # 生成网格,先验框中心,网格左上角 # -----------------------------------------------------# grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_h, 1).repeat( int(bs * len(self.anchors_mask[l])), 1, 1).view(x.shape).type_as(x) grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_w, 1).t().repeat( int(bs * len(self.anchors_mask[l])), 1, 1).view(y.shape).type_as(x) # 生成先验框的宽高 scaled_anchors_l = np.array(scaled_anchors)[self.anchors_mask[l]] anchor_w = torch.Tensor(scaled_anchors_l).index_select(1, torch.LongTensor([0])).type_as(x) anchor_h = torch.Tensor(scaled_anchors_l).index_select(1, torch.LongTensor([1])).type_as(x) anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape) anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape) # -------------------------------------------------------# # 计算调整后的先验框中心与宽高 # -------------------------------------------------------# pred_boxes_x = torch.unsqueeze(x * 2. - 0.5 + grid_x, -1) pred_boxes_y = torch.unsqueeze(y * 2. - 0.5 + grid_y, -1) pred_boxes_w = torch.unsqueeze((w * 2) ** 2 * anchor_w, -1) pred_boxes_h = torch.unsqueeze((h * 2) ** 2 * anchor_h, -1) pred_boxes = torch.cat([pred_boxes_x, pred_boxes_y, pred_boxes_w, pred_boxes_h], dim = -1) return pred_boxes
其他部分与YOLOV3没多少区别了,这里就不赘述了。