YoloV3网络学习

释放双眼,带上耳机,听听看~!
YoloV3的网络学习及主干特征提取网络Residual Block结构解析,以及利用多次的上采样进行高纬度和低纬度的特征融合。

YoloV3网络学习—(供自己查看)

使用的是B站Bubbliiiing的代码,附上自己的理解,供自己目前学习与后续的查看。

YoloV3网络学习

上图为YoloV3的网络结构示意图,左边蓝色部分为主干特征提取网络,使用的是Darknet53。右边橘色部分为加强特征提取部分,利用多次的上采样进行高纬度和低纬度的特征融合。

特征提取部分

Darknet53主干特征提取网络

Resiual Block

可以看到Darknet53主干特征提取网络中基本上都是Resiual Block的堆叠,所以先看一下Resiual Block的结构是怎么样的。

YoloV3网络学习


#input_channels :输入的通道数
#output_channels:输出的通道数
#blocks      :残差结构的个数
def make_layers(input_channels,output_channels,blocks):
    #在resiual_block之前先用一个3*3的卷积步长为2对输入进来的特整层进行下采样,宽高变为原来的一半。然后进行卷积标准化加激活函数)
    layers = []
    layers.append(("ds_conv",nn.Conv2d(input_channels,output_channels,kernel_size=3,stride=2,padding=1,bias=False))) #3*3,步长为2的卷积下采样并提高通道数
    layers.append(("ds_bn",nn.BatchNorm2d(output_channels)))#标准化
    layers.append(("ds_relu",nn.LeakyRelu(0.1))) #激活函数
    #进行残差块的堆叠
    for i in range(0,blocks):
        #这里通过传进来的blocks来确定需要多少个ResiualBlock进行堆叠
        layers.append(('resiual_{}'.format(i),ResiualBlock(input_channels,output_channels)))
    return nn.Sequential(OrderedDict(layers))
    
class ResiualBlock(nn.Module):
    def __init__(self,input_channels,output_channels):
        super(ResiualBlock,self).__init__()
        
        self.conv1 = nn.Conv2d(output_channels,input_channels,kernel_size=1,stride=1,padding=0,bias=False)
        self.bn1   = nn.BatchNorm2d(input_channels)
        self.relu1 = nn.LeakyReLU(0.1)
        
        self.conv2 = nn.Conv2d(input_channels,out_channels,kernel_size=3,stride=2,padding=1,bias=False)
        self.bn2   = nn.BatchNorm2d(output_channels)
        self.relu2 = nn.LeakyReLU(0.1)
     
     def forward(x):
        #将x保留一份不做任何处理 
        resiual = x
        #通过1*1的卷积降低通道数
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)
        #再通过3*3的卷积提取特征并上升通道数
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)
        #最后将不做任何处理的残差边相加
        #两次卷积操作没有改变特征图的大小,最后通道数也与输入相同所以可以直接相加
        out += resiual
    
        return out

Darknet53 详解

最上面的图Darknet53从上往下看,Yolov3的默认输入大小是[416,416],输入的是一张RGB彩色图像所以输入的shape为 (batch,3,416,416)。

(1) 首先进行一个Conv2D Block(卷积标准化+激活函数) 上升通道数。
    shape从[batch,3,416,416] ------>[batch,32,416,416]
    Conv2D Block是步长为1的卷积操作,不会对图像的宽高进行缩小所以宽高不变。
    
 #上面讲过了ResiualBlock之前的卷积操作对特征图进行了下采样并且通道数的上升,之后特征图进入ResiualBlock输出的大小与通道数没有变化。
(2) 进行第一次ResiualBlock(重复一次),输入shape:[batch,32,416,416]
    shape从[batch,32,416,416] ------>[batch,64,208,208]
    
(3) 进行第二次ResiualBlock(重复两次),输入shape:[batch,64,208,208]
    shape从[batch,64,208,208] ------>[batch,128,104,104]
    
(4) 进行第三次ResiualBlock(重复八次),输入shape:[batch,128,104,104]
    shape从[batch,128,104,104] ------>[batch,256,52,52]
    保留当前特征图作为out0
    
(5) 进行第四次ResiualBlock(重复八次),输入shape:[batch,256,52,52]
    shape从[batch,256,52,52] ------>[batch,512,26,26]
    保留当前特征图作为out1
    
(6) 进行第五次ResiualBlock(重复四次),输入shape:[batch,512,26,26]
    shape从[batch,512,26,26] ------>[batch,1024,13,13]
    保留当前特征图作为out3
    
Darknet53通过六个大模块组成,最终输出的是模块4,5,6的输出:
out0[batch,256,52,52]
out1[batch,512,26,26]
out2[batch,1024,13,13]

加强特征提取部分

通过将低维度的语义信息与高纬度的语义信息进行融合,提取到更多的特征。

Conv2D Block

右边橘色框部分为加强特征提取部分(从下往上看)。主要是用到了Conv2D Block,先来看一下Conv2D Block的实现。

#Conv2d_block 存在七层
#前六层是一次1*1的卷积降低通道数接着一次3*3的卷积提取特征并上升通道数的操作(重复三次)。conv2d包含卷积标准化加激活函数。
#第七层通过一个1*1的卷积操作降低通道数至指定的预测通道数值
def Conv2d_block(filters_list,in_filters,out_filter):
    m = nn.Sequential(
        conv2d(in_filters,filters_list[0],1)
        conv2d(filters_list[0],filters_list[1],3)
        conv2d(filters_list[1],filters_list[0],1)
        conv2d(filters_list[0],filters_list[1],3)
        conv2d(filters_list[1],filters_list[0],1)
        conv2d(filters_list[0],filters_list[1],3)
        #out_filter的组成是3*(4+1+num_classes)
        #4是网格点上先验框的中心点与宽高的调整参数
        #1是网格点是否存在物体
        #num_classes是每个类别的概率
        nn.Conv2d(filters_list[1],out_filter,kernel_size=1,stride=1,padding=0,bias=True)
        
     return m
    )

加强特征提取部分详解

#从下往上看 以voc数据集为例,voc数据集一共有20个类
#最后加强特征提取的通道输出为 3*(4+1+20) = 75
第一个Con2d_block Conv2_block([512,1024],1024,75)
(1) 将out2进行Conv2_block的Start 5 前五层
    shape从[bacth,1024,13,13] ----> [batch,512,13,13] ,命名为out2_branch,作为后续的处理
    out2_branch 再进入Conv2d_block的Last 2最后两层
    shape从[batch,512,13,13] -----> [batch,75,13,13] ,命名为out0 作为yolov3的第一个输出
    
(2) out2_branch与out1融合之前的处理
    [1] 通过1*1的卷积进行通道数的下降(卷积标准化加激活函数)
    shape:[batch,512,13,13] -----> [batch,256,13,13]
    [2] 对特征图进行上采样,将宽高扩大到原来的两倍,nn.Upsample(scale_factor=2,mode='nearest')
    shape:[batch,256,13,13] -----> [batch,256,26,26]
    
(3) 与out1进行特征融合cat,并命名为x1_in
    shape:[batch,256,26,26] ----->cat([batch,256,26,26],[batch,512,26,26]) = [batch,768,26,26]
    
 第二个Con2d_block Conv2_block([256,512],768,75)
(4) 将x1_in进行Conv2_block的Start 5 ,前五层
    shape从[bacth,768,26,26] ----> [bacth,256,26,26] ,命名为out1_branch,作为后续的处理
    out1_branch 再进入Conv2d_block的Last 2最后两层
    shape从[bacth,256,26,26] -----> [batch,75,26,26] ,命名为out1 作为yolov3的第二个输出
    
(5) out1_branch与out0融合之前的处理
    [1] 通过1*1的卷积进行通道数的下降(卷积标准化加激活函数)
    shape:[bacth,256,26,26] -----> [bacth,128,26,26]
    [2] 对特征图进行上采样,将宽高扩大到原来的两倍,nn.Upsample(scale_factor=2,mode='nearest')
    shape:[bacth,128,26,26] -----> [bacth,128,52,52]
 
(6) 与out0进行特征融合cat,并命名为x2_in
    shape:[bacth,128,52,52] ----->cat([batch,256,52,52],[bacth,128,52,52]) = [bacth,384,52,52]
    
第三个Con2d_block Conv2_block([128,256],384,75)
(7) 将x2_in进行Conv2_block的Start 5 ,前五层
    shape从[bacth,384,52,52] ----> [bacth,128,52,52] ,命名为out0_branch,作为后续的处理
    out0_branch 再进入Conv2d_block的Last 2最后两层
    shape从[bacth,128,52,52] -----> [batch,75,52,52] ,命名为out2 作为yolov3的第三个输出
    
至此完成了yolov3网络的特征提取部分,提取到了三个不同大小的有效特征图
    out0:shape[batch,75,13,13]
    out1:shape[batch,75,26,26]
    out2:shape[batch,75,52,52]
    
    

YoloV3 Loss 详解

Loss 包括(回归Loss,置信度Loss,物体类别置信度Loss)

  首先要知道先验框的概念
  Yolo中存在9组不同大小的anchors,这些anchors的宽高大小是相对于[416,416][w,h] = [10,13],[16,30],[33,23],[30,61],[62,45],[59.119],[116,90],[156,198],[373,326]
  9组不同的anchors又可以分为三大组
  [10,13],[16,30],[33,23] 分为一组,用于检测52,52最大尺寸的特征图
  [30,61],[62,45],[59,119] 分为一组,用于检测26,26中等尺寸的特征图
  [116,90],[156,198],[373,326] 分为一组,用于检测13,13小尺寸的特征图
 
 对应不同大小的特征图,每个网格点上都会存在三个不同大小的anchors用于预测该网格点上的物体
 

将anchor_size对应到当前特征图大小并且对特征图参数进行处理。

首先需要将相对于[416,416]尺寸的anchors转换成当前特征图尺寸的anchors

#in_h 和 in_w 是特征图的大小(13,13) (26,26) (52,52)
#查看一个网格点是416多少个像素点
stride_h = 416/in_h
stride_w = 416/in_w
#将[416,416]的特征图anchors转换成当前尺寸的anchors
scaled_anchors = [(a_w / stride_w,a_h/stride_h)] for a_w,a_h in anchors]

将原本shape为[batch,75,w,h] ———> [batch,3,w,h,5+num_classes]
,并且将所有网格点的中心点调整参数,置信度,种类的置信度参数进行sigmoid,规定到0-1之间

#input的shape[batch,75,w,h]
#view将inputshape改变为--->[batch,3,5+num_classes,w,h]
#permute改变通道顺序[batch,3,w,h,5+num_classes] -------> [batch,3,w,h,5+num_classes]
#contiguous是对改变完的tensor进行拷贝,后续对prediction进行更改不会改变input。
prediction = input.view(bs,len(self.anchprs_mask[l]),self.bbox_attrs,in_h,in_w).permute(0,1,3,4,2).contiguous()

#先验框的中心位置的调整参数,并且进行sigmoid 
x = torch.sigmoid(prediction[...,0])
y = torch.sigmoid(prediction[...,1])
#先验框的宽高调整参数
w = prediction[...,2]
h = prediction[...,3]
#网格点的是否有物体的置信度
conf = torch.sigmoid(prediction[...,4])
#网格点包含所有种类的置信度
pred_cls = torch.sigmoid(prediction[...,5:])

获取真实框在网格中的结果

设置哪个网格预测真实框的结果

#三个重要的Tensor
#初始化全部为1,默认所有网格点上都不包含物体,(负样本)
noobj_mask = torch.ones(batch,3,h,w,requires_grad = False)
#初始化全部为0,box_loss_scale,包含权重信息,大目标权重小,小目标权重大。让网络更加去关注小目标。
box_loss_scale = torch.zeros(batch,3,h,w,requires_grad = False)
#用于反应真实图片上框的信息。
y_true = torch.zeros(batch,3,h,w,5+classes_num,requires_grad = False)

#这里的targets包含的是图片上的真实框与类别信息
#targets[boxes_num,5],boxes_num 是这张图片上有几个真实框,5包含[x,y,w,h,class_index],除了类别索引,中心点坐标与真实框宽高均以416大小的尺寸做了归一化处理。
batch_target = torch.zeros_like(targets)
#计算出正样本在当前特征图上的位置与宽高
#in_w和in_h为当前网格点的宽高,(13,13),(26,26),(52,52)
batch_target = target[:,[0,2]] * in_w #中心点x与w
batch_target = target[:,[1,3]] * in_h #中心点y与h
batch_target = target[:.4] # class_index

#将真实框转换一个形式[boxes_num,5] ------> [boxes_num,4]
#修改完的参数为[0,0,w,h],后续的iou计算暂时不需要中心点参数,所以将中心点参数设置为0,
gt_box = torch.FloatTensor(torch.cat((torch.zeros((box_num,2)),batch_target[:,2:4]),dim=1))
#将先验框转换一个形式[anchors_num,2]   -------> [anchors_num,4]
#修改完的参数为[0,0,w,h],
anchor_shapes = torch.FloatTensor(torch.cat((torch.zeros(len(anchors),2)),dim=1))
#将anchor_shapes 与 gt_box 进行iou处理

将真实框信息与先验框信息进行IOU

真实框与先验框交集图:

YoloV3网络学习

#box_a target_box [x,y,w,h]
#box_b anchor_box [x,y,w,h]
#首先先将中心点,宽高转换成左上角坐标与右下角坐标的形式。
#真实框
#左上角x与右下角x,公式就是中心点X +- 宽的一半
b1_x1,b1_x2 = box_a[:,0] - box_a[:,2] / 2, box_a[:,0] + box_a[:,2] / 2
#左上角y与右下角y,公式就是中心点Y +- 高的一半
b1_y1,b1_y2 = box_a[:,1] - box_a[:,3] / 2, box_a[:,1] + box_a[:,3] / 2 
#先验框
#与真实框同理,求出b2_x1,b2_x2,b2_y1,b2_y2

#创建两个box与box_a和box_b的shape相同,并将左上角右下角赋值给他们
box_target = torch.zeros_like(box_a)
box_anchor = torch.zeros_like(box_b)
box_target[:,0],box_target[:,1],box_target[:,2],box_target[:,3] = b1_x1,b1_y1,b1_x2,b1_y2
box_anchor[:,0],box_anchor[:,1],box_anchor[:,2],box_anchor[:,3] = b2_x1,b2_y1,b2_x2,b2_y2

#真实框的数量
t_box_num = box_target.size(0)
#先验框的数量
a_box_num = box_anchor.size(0)

#计算真实框与先验框的交面积
#如上图:真实框与先验框交集图
#找出左上角做小的坐标和右下角最大的坐标,输出的shape为[box_num,anchor_num,2]
#在原来的二维tensor索引为1处通过unsqueeze()加上1维变成三维,然后通过expand()对tensor进行复制到指定维度。

#右下角的坐标
max_xy = torch.min(box_target[:,2].unsqueeze(1).expand(A,B,2),box_anchor[:,2].unsqueeze(1).expand(A,B,2))
#左上角坐标
min_xy = torch.max(box_target[:,:2].unsqueeze(1).expand(A,B,2),box_anchor[:,:2].unsqueeze(1).expand(A,B,2))
#交集的宽高
inter = torch.clamp((max_xy - min_xy),min = 0)
#交集面积
inter = inter[:,:,0] * inter[:,:,1]

#计算真实框与先验框各自的面积,在对应的索引上加上维度,使得shape相同
#真实框
area_target = ((box_target[:,2] - box_target[:,0]) * (box_target[:,3] - box_target[:,1])).unsqueeze(1).expand_as(inter)
#先验框
area_anchor = ((box_anchor[:,2] - box_anchor[:,0]) * (box_anchor[:,3] - box_anchor[:,1])).unsqueeze(0).expand_as(inter)

#并集
union = area_target + area_anchor - inter
#iou, shape[box_num,anchor_num] 
iou = inter / union

通过IOU值查看当前真实框利用哪个先验框预测。

#选出真实框与anchors的iou最大值索引
#(13,13) 网格对应anchors[6,7,8]
#(26,26) 网格对应anchors[3,4,5]
#(52,52) 网格对应anchors[0,1,2]
best_ns = torch.argmax(iou,dim=-1)
#查看真实框的anchors索引是否在当前处理的网格对应的anchors索引中
#比如当前处理的网格是(13,13),max_iou 索引是4。不在当前(13,13)anchors网格索引中,所以不用当前的(13,13)网格对这个真实框进行处理直接跳过。
#等处理(26,26)网格的时候再对当前真实框进行处理。

#查看当前先验框是当前特征图的哪一个先验框
#anchors:[6,7,8][3,4,5][0,1,2]
k = anchors_mask[l].index(best_n)
#查看预测真实框的先验框网格的左上角坐标,向下取整
#左上角x
i = torch.floor(batch_target[t,0]).long()
#左上角y
j = torch.floor(batch_target[t,1]).long()
#真实框的种类
c = batch_target[t,4].long()

#将有目标的先验框网格点的noobj_mask设为0
noobj_mask[b,k,j,i] = 0

#两种赋值方法,用于不同的loss计算(一个是框与框计算giou,一个是通过真实与预测的中心点,宽高计算loss)
#进行giou计算loss
#(这里的赋值是直接赋值真实框的参数值,用于后续直接和先验框的参数值进行giou的计算,计算出loss_loc)
y_true[b, k, j, i, 0] = batch_target[t, 0]
y_true[b, k, j, i, 1] = batch_target[t, 1]
y_true[b, k, j, i, 2] = batch_target[t, 2]
y_true[b, k, j, i, 3] = batch_target[t, 3]
y_true[b, k, j, i, 4] = 1
y_true[b, k, j, i, c + 5] = 1
#不进行giou计算loss
#(这里的赋值是中心点与宽高的调整参数,用于后续直接和预测值中心点与宽高的调整参数计算loss_loc)
y_true[b, k, j, i, 0] = batch_target[t, 0] - i.float()
y_true[b, k, j, i, 1] = batch_target[t, 1] - j.float()
y_true[b, k, j, i, 2] = math.log(batch_target[t, 2] / anchors[best_n][0])
y_true[b, k, j, i, 3] = math.log(batch_target[t, 3] / anchors[best_n][1])
y_true[b, k, j, i, 4] = 1
y_true[b, k, j, i, c + 5] = 1


#对通过真实框的宽高进行大小目标权重计算,并给box_loss_scale对应位置赋值
#计算公式 (w * h / 网格点w / 网格点h)
box_loss_scale[b,k,j,i] = batch_target[t,2] * batch_target[t,3] / in_w / in_h 

对网络的预测值进行解码操作

grid_x:
YoloV3网络学习

解码图示:
YoloV3网络学习

#生成网格,先验框的中心,网格的左上角,shape为[batch,3,网格点宽,网格点高]
#grid_x 为上图所示
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 为纵向的grid_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)

# 生成先验框的宽高,每个网格点上对应三个不同大小的先验框,shape为[batch,3,网格点宽,网格点高]
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)


#计算调整后的先验框中心与框高

#x,y,w,h 均为网络的预测值
#x,y 为网格点往右下角的偏移量g
pred_boxes_x = torch.unsqueeze(x + grid_x,-1)
pred_boxes_y = torch.unsqueeze(y + grid_y,-1)
pred_boxes_w = torch.unsqueeze(torch.exp(w) * anchor_w,-1)
pred_boxes_h = torch.unsqueeze(torch.exp(h) * anchor_h,-1)
#将先验框x,y,w,h参数进行堆叠
pred_boxes = torch.cat([pred_boxes_x,pred_boxes_y,pred_boxes_w,pred_boxes_h],dim=-1)

#将调整好的先验框与真实框进行iou的计算,并且判断iou值是否超过指定阈值,如果超过了则让这个网格点进行预测。
#将原本shape[3,13,13,4] -----> [507,4] 便于后续计算
pred_boxes_for_ignore = pred_boxes.view(-1,4)

#计算真实框与先验框的iou,shape[boxes_num,507],图像上每个真实框与每个先验框的iou值
anch_ious = calculate_iou(batch_target,pred_boxes_for_ignore)
#选取出每个先验框最大的iou,shape[1,507] (为每个先验框对应真实框的最大iou值)
anch_ious_max,_ = torch.max(anch_ious,dim = 0)
#查看iou值是否大于指定阈值,并给noobj_mask赋值
#进行reshape,从[507,] ------> [3,13,13]
anch_ious_max       = anch_ious_max.view(pred_boxes[b].size()[:3])
#iou值与指定阈值做对比,小于为False,大于为True,shape[3,13,13]
mask = anch_ious_max > self.ignore_threshold
#为mask为True的网格点设置为0,说明当前网格的预测结果和真实值的重合程度较大,作为负样本不合适。
noobj_mask[b][mask] = 0

计算最后的Loss

#通过判断y_true中的置信度来获取obj_mask网格掩码,网格点上有物体为True,没有物体为False
obj_mask = y_true[...,4] == 1

正样本loss计算(loss_loc 先验框调整参数, loss_cls 物体置信度)

Loss_loc

YoloV3网络学习

YoloV3网络学习

#查看是否是giou计算loss
#如果是giou计算loss,之前预测值的boxes中保存的就是先验框的宽高与中心点坐标
#上图为giou的计算公式:
#先计算出两个矩形的iou值,S2为两个矩形相并的区域
#S3所包含A,B框的最小区域
giou = box_giou(pred_boxes,y_true[...,:4])
#如果通过giou计算loss_loc的话,loss就是1-giou
loss_loc = torch.mean((1-giou)[obj_mask]) # 因为调整参数只针对正样本,所有取obj_mask为True的进行计算。

#如果不是giou计算loss
#则通过BCELoss计算真实的x,y,w,h的调整参数与预测的x,y,w,h的调整参数进行loss计算,同样也是只针对正样本,obj_mask为True进行计算,并且乘上一个大小目标的权重信息,也是只针对正样本。
loss_x      = torch.mean(self.BCELoss(x[obj_mask], y_true[..., 0][obj_mask]) * box_loss_scale[obj_mask])
loss_y      = torch.mean(self.BCELoss(y[obj_mask], y_true[..., 1][obj_mask]) * box_loss_scale[obj_mask])
loss_w      = torch.mean(self.MSELoss(w[obj_mask], y_true[..., 2][obj_mask]) * box_loss_scale[obj_mask])
loss_h      = torch.mean(self.MSELoss(h[obj_mask], y_true[..., 3][obj_mask]) * box_loss_scale[obj_mask])
loss_loc    = (loss_x + loss_y + loss_h + loss_w) * 0.1

Loss_cls

#物体类别loss也是通过BCELoss对预测值与真实值进行计算,pred_cls为预测值,y_true为真实值
#也是只对正样本进行计算
loss_cls = torch.mean(self.BCELoss(pred_cls[obj_mask],y_true[...,5:][obj_mask]))

#loss_loc 与  loss_cls 都乘上一个相对应的权重
#box_ratio = 0.05
#cls_ration = 1 * (num_classes / 80)
loss        += loss_loc * self.box_ratio + loss_cls * self.cls_ratio

负样本与正样本共同的loss计算(loss_conf 存在物体的置信度)

Loss_conf

#网格点上物体的置信度计算,正样本负样本都会进行loss计算
#noobj_mask 为负样本掩码,为True的是负样本
#obj_mask   为正样本掩码,为True的是正样本
#通过BCELoss计算预测值与真实值的Loss
loss_conf   = torch.mean(self.BCELoss(conf, obj_mask.type_as(conf))[noobj_mask.bool() | obj_mask])
#乘上两个权重
#obj_ratio = 5 * (input_shape[0] * input_shape[1]) / (416 ** 2)
#balance = [0.4,1.0,4]
loss        += loss_conf * self.balance[l] * self.obj_ratio

预测解码并且进行NMS

经过网络输出三个不同大小的特征图 [b,75,13,13] [b,75,26,26] [b,75,52,52]

根据预测值对先验框进行调整

#与训练时的解码操作类似
outputs = [] #用于存储三个特征图解码后的信息
for i,input in enumerate(inputs): # 循环处理三个不同大小的特征图
    #判断当前一个网格相当于输入尺寸的多少像素点
    #输入尺寸:[416,416] 网格大小[13,13]  stride_h = stride_w = 416/13 = 32
    stride_h = self.input_shape[0] / input_height
    stride_w = self.input_shape[1] / input_width
    #将anchors的大小设置到相对于当前特整层
    scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors[self.anchors_mask[i]]]
    #特征图的shape分解[batch,75,w,h] ----> [batch,3,w,h,25],便于后续的操作
    prediction = input.view(batch_size, len(self.anchors_mask[i]),
                                    self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous()
    #先验框中心点调整参数,进行sigmoid归一化,因为中心点是网格点的左上角,调整是从左上角向右下角进行偏移调整                                
    x = torch.sigmoid(prediction[..., 0])  
    y = torch.sigmoid(prediction[..., 1])
    #先验框宽高的调整参数
    w = prediction[..., 2]
    h = prediction[..., 3]
    #网格点是否存在物体的置信度,进行sigmoid归一化
    conf        = torch.sigmoid(prediction[..., 4])
    #每个种类的置信度,进行sigmoid归一化
    pred_cls    = torch.sigmoid(prediction[..., 5:])
    #生成网格,和先验框的宽高,与训练时候的一样
    #生成的网格根据当前特征图大小
    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)
    #生成先验框的宽高,根据当前特征图对应的anchors_size
    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 = 网格点左上角x + 预测中心点x偏移量
    #先验框中心点y = 网格点左上角y + 预测中心点y偏移量
    #先验框w = 先验框w + 预测w调整参数取exp
    #先验框h = 先验框h + 预测h调整参数取exp
    
    pred_boxes          = FloatTensor(prediction[..., :4].shape)
    pred_boxes[..., 0]  = x.data + grid_x
    pred_boxes[..., 1]  = y.data + grid_y
    pred_boxes[..., 2]  = torch.exp(w.data) * anchor_w
    pred_boxes[..., 3]  = torch.exp(h.data) * anchor_h
    
    #将输出结果堆叠,并且对先验框的中心点坐标与宽高进行归一化处理。
    #_scale为当前特征图大小的tensor [in_w,in_h,in_w,in_h] = [13,13,13,13]
    _scale = torch.Tensor([input_width, input_height, input_width, input_height]).type(FloatTensor)
    #将pred_boxes 除以_scale 进行归一化处理,output为堆叠后的tensor,shape [batch,先验框数量,25](当前是13*13的网格,先验框的数量= 13*13*3 = 507)
    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)
    #这个output的意思就是有507个先验框,每个先验框包含25个参数
    #25个参数分为4+1+20 ,20是num_classes

经过对先验框的调整,获得了三个新的output:[batch,507,25] [batch,2028,25] [batch,8112,25]b

根据总置信度筛选以及类别相同的先验框进行iou的nms筛选选出最终的预测框

#先将原本先验框的中心点宽高参数调整为左上角右下角
#此时的prediction 为三个output的堆叠,shape为[batch,10647,25]
box_corner          = prediction.new(prediction.shape)
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))] # 创建一个output用于存放筛选过后的框
#对batch进行循环
#image_pred是先验框的信息 shape: [10647,25] 代表有10647个先验框,每个先验框有25个参数
for i, image_pred in enumerate(prediction):
    #对类别参数进行最大值判断,并且获得最大值所在的索引位置,也就是类别索引
    #class_conf 存放的是类别置信度
    #class_pred 存放的是类别索引位置
    class_conf, class_pred = torch.max(image_pred[:, 5:5 + num_classes], 1, keepdim=True)
    #进行第一轮筛选,通过类别置信度*网格存在物体的置信度 = 总置信度
    #总置信度需要大于设定的置信度阈值,conf_mask 为置信度掩码,大于阈值的先验框为True,小于阈值的先验框为False
    conf_mask = (image_pred[:, 4] * class_conf[:, 0] >= conf_thres).squeeze()
    #根据置信度掩码进行筛选保留为True的先验框
    image_pred = image_pred[conf_mask] #先验框所有参数Tensor
    class_conf = class_conf[conf_mask] #先验框类别置信度Tensor
    class_pred = class_pred[conf_mask] #先验框类别索引Tensor
    #对上面参数进行一个堆叠,image_pred只取前四个参数,x1,y1,x2,y2,conf
    #detections shape:[num_anchors,7] = num_anchors为第一轮筛选过后先验框的数量,7为x1,y1,x2,y2,conf,class_conf,class_index
    detections = torch.cat((image_pred[:, :5], class_conf.float(), class_pred.float()), 1)
    #根据class_index进行去重,查看所有先验框包含的类别种类
    unique_labels = detections[:, -1].cpu().unique()
    #进行第二轮去重,相同的类别种类根据进行NMS去重
    #循环每一个种类
    for c in unique_labels:
       #获取种类相同的先验框
       detections_class = detections[detections[:, -1] == c]
       #使用官方自带的nms函数
       #传入三个参数,
       #1.先验框的前四个参数(x1,y1,x2,y2)
       #2.总置信度 = 先验框存在物体的置信度 * 类别置信度
       #3.自己设置的iou阈值
       #keep返回的是经过nms筛选完成的先验框索引
       keep = nms(
               detections_class[:, :4],
               detections_class[:, 4] * detections_class[:, 5],
               nms_thres
           )
       #获取到第二轮筛选,选出最终的预测框max_detections
       max_detections = detections_class[keep]    

将预测框的信息映射到原图

目前为止以及得到了图片最终的预测框信息,现在要做的就是将预测框的前四个参数(y1,x1,y2,x2)信息映射到原本的图片上

YoloV3网络学习

letterbox_image参数: 代表图片在resize的时候是否是保持图片的宽高比。如果为True 在resize的时候会保持宽高比,并且加入灰条。如果为False,直接对图片进行resize。

#将(x1,y1,x2,y2) 转换为 (x,y,w,h)
box_xy, box_wh      = (output[i][:, 0:2] + output[i][:, 2:4])/2, output[i][:, 2:4] - output[i][:, 0:2]
#将xy 位置交换为 yx,将wh 交换为 hw 
box_yx = box_xy[..., ::-1]
box_hw = box_wh[..., ::-1]

#获取到输入进网络的尺寸和原图的尺寸
input_shape = np.array(input_shape)
image_shape = np.array(image_shape)
#判断是否是保持宽高比resize的
if letterbox_image:
    #先求出[416,416]中保持宽高比的图片的尺寸
    new_shape = np.round(image_shape * np.min(input_shape/image_shape))
    #算出一条灰度条在416尺寸中的比例
    offset  = (input_shape - new_shape)/2./input_shape
    #算出416尺寸的图片与内部新图的比例
    scale   = input_shape/new_shape
    #先需要减去一侧的灰度图,让xy坐标不是相对于[416,416],而是相对于内部的保持宽高比的小图,然后再乘上比例,映射到原始大小的图片上
    box_yx  = (box_yx - offset) * scale
    #宽高直接乘上比例即可
    box_hw *= scale
#将(y,x,h,w) -----> (y1,x1,y2,x2)
box_mins    = box_yx - (box_hw / 2.)
box_maxes   = box_yx + (box_hw / 2.)
#将y1,x1,y2,x2进行堆叠
boxes  = np.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]], axis=-1)
#将y1,x1,y2,x2映射到原图像上的尺寸
boxes *= np.concatenate([image_shape, image_shape], axis=-1)
#替换原本的前四个参数
#此时的shape:[anchor_num,7]
#anchor_num为最终筛选完的预测框个数
#7 = y1,x1,y2,x2,conf,class_conf,class_index   
output[:4] = boxes

到此位置YOLOV3的整个流程都结束了。

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

深度学习框架简介及如何学习使用

2023-12-13 19:04:14

AI教程

NVIDIA GPUDirect Storage 技术大幅提升 GPU 数据传输速度和性能收益

2023-12-13 19:18:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索