本篇主要记录学习yolov5+csl旋转目标检测的原理,对前几篇文章作一个总结,添加一些细节。参考知乎 略略略 https://zhuanlan.zhihu.com/p/358441134; yangxue https://zhuanlan.zhihu.com/p/111493759

一、训练部分

1.数据加载

  加载数据的主要过程都在create_dataloader这个方法里。
图片描述
  下面是该方法的返回值:

return loader(dataset,
            batch_size=batch_size,
            shuffle=shuffle and sampler is None,
            num_workers=nw,
            sampler=sampler,
            pin_memory=True,
            collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn), dataset

  需要注意的是,这里返回的dataset是原始数据(与我们的label基本一致,clsid换一下),但是在下面遍历这里所取得的train_loader成员的时候会调用 LoadImagesAndLabels这个类的专有函数getitem__(self, index),这里重写了这个方法,使得返回将原先[x1, y1, x2, y2, x3, y3, x4, y4]格式标签转换成了旋转目标检测的长边表示法,同时作了一些数据增强(中途print一下label会发现标签在变成长边表示法之前改变了),另一方面这里的img输出是经典的[3 , h, w],RGB格式。
  其他有一个小地方要注意一下,在poly2rbox这个方法中,根据四点坐标计算中心点坐标以及长短边角度的函数用的是cv2.minAreaRect(poly) ,这个方法原意是求任意个数的点集所围成的最小矩形(计算量还是有的),在转换poly表示法到长边表示法的过程中似乎与我所理解的有所不同(见下图),我的理解长边很明显是5. 其实这不是个标准矩形,长边表示法是标准矩形。

def __getitem__(self, index):
      '''
      Augment the [clsid poly] labels and trans label format to rbox.
      Returns:
      img (tensor): (3, height, width), RGB
      labels_out (tensor): (n, [None clsid cx cy l s theta gaussian_θ_labels]) θ∈[-pi/2, pi/2)
      img_file (str): img_dir 
      shapes : None or [(h_raw, w_raw), (hw_ratios, wh_paddings)], for COCO mAP rescaling
      '''

图片描述

2.推理pred

  首先train.py这里改变图片尺寸(/255)作归一化,给tensor处理,同时改为浮点类型,shape不变
图片描述
  这里把图片(图片shape[b, 3, height, width], RGB)喂给网络,
图片描述
  然后在forward里迭代网络的各个层(卷积、C3等)
图片描述
  经过第一层卷积:

Conv(
  (conv): Conv2d(3, 48, kernel_size=(6, 6), stride=(2, 2), padding=(2, 2), bias=False)
  (bn): BatchNorm2d(48, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
  (act): SiLU(inplace=True)
)

  之后x的shape变成如图(2, 48, 512,512)
图片描述
  第二层网络结构:经过第二层网络之后的x的shape:(2, 96, 256, 256)

Conv(
  (conv): Conv2d(48, 96, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
  (bn): BatchNorm2d(96, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
  (act): SiLU(inplace=True)
)

图片描述
  依此类推,需要注意的是在第12层concat层,将当前层(即上一层输出的x)和m.f参数指定的之前保存的某一层结合作list
图片描述
  在第16层的时候同样,将当前层和m.f参数指定的之前保存的某一层结合作list,下面还有concat层,不作记录
  共25层(整体网络结构见上一篇,这里不再给出),下面是最后一层detect层之前,可以看到,过程中根据self.save参数保存了部分层的输出,并且根据m.f参数,指定当前层的输入是单一的上一层输出还是和之前层组成的list
图片描述
  比如detect层的m.f参数是如下:
图片描述
  则意味着要把y中这几个层的输出联合成list
图片描述
  detect层内容:

Detect(
  (m): ModuleList(
    (0): Conv2d(192, 603, kernel_size=(1, 1), stride=(1, 1))
    (1): Conv2d(384, 603, kernel_size=(1, 1), stride=(1, 1))
    (2): Conv2d(768, 603, kernel_size=(1, 1), stride=(1, 1))
  )
)

  分别对三个尺度的tensor作卷积,经过detect层之后三个尺度的输出:
图片描述

2.1 detect层细节处理

  下面是detect层处理的一些细节:
  首先进入forward,按lawyer(3层)操作,经过第一次卷积之后x[0]变成如图:
图片描述
  这里把x[i]进行重组(我的理解相当于一个线性层,但不完全一样):
图片描述
  201是self.no参数,在detect类中定义(如下图),代表着[16个类别,cx,cy,l,s,theta,180个gaussian_theta_labels],值得注意的是,这201个参数是相对于一个anchor来说的。这里2是bz,3是每一个grid负责的anchor数量,即一个grid产生3个anchor,然后每一张图片,按3个尺度,分成128×128,64×64,32×32个grid。
图片描述

3.损失计算

  送给loss时pred(三个尺度的输出,yolov5特点)和target的格式:
图片描述
  computer_loss初始化时的一些设置
图片描述
  下面是调用computer_losscall方法一部分,重点是最后的build_targets方法

def __call__(self, p, targets):  # predictions, targets, model
      """
      Args:
      p (list[P3_out,...]): torch.Size(b, self.na, h_i, w_i, self.no), self.na means the number of anchors scales
      targets (tensor): (n_gt_all_batch, [img_index clsid cx cy l s theta gaussian_θ_labels])

      Return:
      total_loss * bs (tensor): [1] 
      torch.cat((lbox, lobj, lcls, ltheta)).detach(): [4]
      """
      device = targets.device
      lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
      ltheta = torch.zeros(1, device=device)
      # tcls, tbox, indices, anchors = self.build_targets(p, targets)  # targets
      tcls, tbox, indices, anchors, tgaussian_theta = self.build_targets(p, targets)  # targets
3.1 build_targets方法
def build_targets(self, p, targets):
      """
      Args:
      p (list[P3_out,...]): torch.Size(b, self.na, h_i, w_i, self.no), self.na means the number of anchors scales
      targets (tensor): (n_gt_all_batch, [img_index clsid cx cy l s theta gaussian_θ_labels]) pixel

      Return:non-normalized data
      tcls (list[P3_out,...]): len=self.na, tensor.size(n_filter2)
      tbox (list[P3_out,...]): len=self.na, tensor.size(n_filter2, 4) featuremap pixel
      indices (list[P3_out,...]): len=self.na, tensor.size(4, n_filter2) [b, a, gj, gi]
      anch (list[P3_out,...]): len=self.na, tensor.size(n_filter2, 2)
      tgaussian_theta (list[P3_out,...]): len=self.na, tensor.size(n_filter2, hyp['cls_theta'])
      # ttheta (list[P3_out,...]): len=self.na, tensor.size(n_filter2)
      """

  这里所做的是先把targets复制3份,并且在每一分的最后一维加上anchor信息,比如第一份最后数字是0,第二份最后数字是2,这也是targets的shape由[95,187]变成[3,95,188]的过程
图片描述
  这里提取pred的尺度信息
图片描述
  把第三维的2:6列作放缩(坐标信息,cx,cy,l,s)
图片描述
  match过滤部分,让某一尺度的anchor预测对应尺度的targets,同时可以避免无限制带来的CIOU计算的梯度爆炸问题。
图片描述
  下面这部分是yolov5的正采样部分,细节可以在之前的博客上看到。总之就是把目标中心点那个grid相邻两个grid都标记为正样本,有助于收敛。
图片描述
  取出targets中预测框中心点以及长短边和180个高斯角度类别信息。需要注意的是,这里只取到了倒数第二列,最后一列是anchor信息(前面提到的加入的0,1,2信息)
图片描述
  tbox中信息是中心点坐标以及长短边,且是偏移量(减去了grid左下角坐标这种)
图片描述
  至此build_targets方法就结束了,已经将targets由原始的数据转换成了分开的目标类别,目标框,索引,目标高斯角度类别等。

3.2 计算损失

  pred同样在三个尺度上迭代
图片描述
  从indices中取出信息,信息包括这765个targets的所属batch,所属anchor,以及在grid中的x,y坐标,然后,把这些信息带入到预测pred中,就能取得这765个anchor的预测信息(长度为201的tensor)。
图片描述
图片描述
  这些gridx,y信息也可以佐证,在128尺度时,这些数据的范围都在128以内,64尺度下的范围也同样在64以内
图片描述
图片描述
  之后就是取出相应列的数据和targets数据作loss处理,没什么需要特别注意的。预测框部分采用的是CIOU,其他是BCEloss

二、检测部分

  需要注意的是,预测detect部分是相较于训练部分多了一些后处理,最主要的是NMS,训练的时候不需要NMS,pred网络出来的是什么就是什么,不需要改动,是要送去损失函数计算损失作反向传播以便下一次更好的推理。也因此,显然检测部分是不需要反向传播的。下面同样从数据的加载代码部分开始看:

1.模型和图片加载

  加载模型和一些参数(权重文件的格式、预定的图片长宽,stridestride相当与网络模型的输出的尺度和真实图片的尺度之间的比例,便于之后标记等,
图片描述
  加载test图片,共28张图片
图片描述
  同训练部分的加载图片(训练部分还有标签)过程的专有函数get_item一样,LoadStreams类的专有函数next:一般都是在这类专有函数中实现对数据的初步处理,然后送给网络。
图片描述
  最开始读取到的图像格式:分辨率+3通道
图片描述
  之后会进行一个resize,具体是到预定的宽高(1024*1024),当然,输入不一定长宽等长,取src_w/des_w,src_h/des_h中较小的比例进行缩小,并且补上一定黑边(yolov5特色)
  这里在推理之前遍历dataset(过程中就是在调用上面的next专有函数),并进行归一化操作,以便送入网络。
图片描述

2.网络推理

  第一张图片推理结果:
图片描述
  下面具体看一下推理过程细节(整体和训练的时候类似,有部分处理不同)
  detect层之前,与预测过程同样的地方输出不同的是bs和最后一个图像宽高(输入不是1024*1024,所以不一样很正常)
图片描述
  第一个尺度下生成128x92(原来是128)个grid,每个grid有201个预测通道
图片描述
  如果shape不一致,把self.grid,anchor等改成对应的
图片描述
  改成原图片尺度(这一部分在yolov5原理的时候有讲过,可以看之前的内容):
图片描述
  把除了最后一维度,其他维度打平,得到第一个尺度下的所有anchor输出
图片描述
  然后以此类推,遍历三个尺度,输出是按第一维把三个尺度的输出相加:
图片描述

3.非极大值抑制

  具体里面就是一系列处理,先通过预定的置信度筛选一部分,然后通过计算IOU,留下较大的。注意cls*obj的操作是在这里面完成的,最后返回符合条件,经过筛选的anchor的index。
图片描述
  经过NMS只剩100多个有效输出
图片描述

4.add poly

  之后就是一些写检测出来的物体的label写到txt里,并且在图像中作标记的过程