对Ayoosh Kathuria的YOLOv3实现进行翻译和总结,原文链接如下:

https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-4/

*首先翻译遵循不删不改的原则有一说一,对容易起到歧义的中文采取保留英文的方式。其中对原文没有删减但是略有扩充,其中某些阐释是我一句话的总结,如有错误请大家在留言区指出扶正。

这是从头开始实现YOLO v3检测器的教程的第4部分。 在上一部分中,我们实现了网络的前向传递。 在这一部分中,我们先通过目标置信度然后进行非最大抑制来对检测设立阈值。

本教程的代码在Python 3.5和PyTorch 0.4上运行。在这个Github repo中可以完整地找到它。

Part 1 : Understanding How YOLO works
Part 2 : Creating the layers of the network architecture
Part 3: Implementing the forward pass of the network
Part 4(This one) : Objectness score thresholding and Non-maximum suppression
Part 5 : Designing the input and the output pipelines

1.先决条件

  • 本教程的第1到3部分。
  • PyTorch的基本知识,包括如何使用nn.Module,nn.Sequential和torch.nn.parameter类创建自定义架构。
  • 关于numpy的基础知识

如果你有任何先决知识的储备不足,你可以在下方找到一些相关知识的链接。

在前面的部分中,我们建立了一个模型,该模型在给定输入图像的情况下输出几个目标检测。 确切地说,我们的输出是形状为B x 22743 x 85的张量。B是批处理中的图像数量,22743是每个图像预测的边界框的数量,而85是边界框属性的数量。

但是,如第1部分中所述,我们必须使输出经过目标评分阈值化和非最大抑制,才能获得本文其余部分所说的真实检测结果。 为此,我们将在文件util.py中创建一个称为write_results的函数。

def write_results(prediction, confidence, num_classes, nms_conf = 0.4):

该函数将预测值,置信度(目标分数阈值),num_classes(在我们的情况下为80)和nms_conf(NMS IoU阈值)作为输入。

2.Object Confidence Thresholding

我们的预测张量包含有关B x 22743边界框的信息。 对于目标分数低于阈值的每个边界框,我们将其每个属性(代表边界框的整个行)的值设置为零。

    conf_mask = (prediction[:,:,4] > confidence).float().unsqueeze(2)
    prediction = prediction*conf_mask

3.Performing Non-maximum Suppression

现在,我们通过中心坐标和边界框的高度与宽度来描述边界框的属性。 但是,使用每个框的两个对角线的坐标来计算两个框的IoU更容易。 因此,我们将盒子的(中心x,中心y,高度,宽度)属性转换为(左上角x,左上角y,右下角x,右下角y)。

每个图像中的真实检测次数可能会有所不同。 例如,批大小为3的图像1、2和3分别具有5、2、4个真实检测。 因此,必须一次对每一张图像进行置信度阈值和NMS。 这意味着,我们无法向量化所涉及的操作,而必须在预测的第一维上循环(包含批处理中的图像索引)。

    batch_size = prediction.size(0)

    write = False

    for ind in range(batch_size):
        image_pred = prediction[ind]          #image Tensor
           #confidence threshholding 
           #NMS

如前所述,write标志用于指示我们尚未初始化输出,我们将使用张量来收集整个批次中的真实检测结果。

一旦进入循环,让我们稍微整理一下。 注意,每个边界框行都有85个属性,其中80个是类分数。 在这一点上,我们只关心具有最高分的类分数。 因此,我们从每行中删除80个类分数,并且添加具有最大值的类索引以及该类的类分数。

        max_conf, max_conf_score = torch.max(image_pred[:,5:5 + num_classes], 1)
        max_conf = max_conf.float().unsqueeze(1)
        max_conf_score = max_conf_score.float().unsqueeze(1)
        seq = (image_pred[:,:5], max_conf, max_conf_score)
        image_pred = torch.cat(seq, 1)

还记得我们已经将对象置信度小于阈值的边界框行设置为零吗? 让我们摆脱它们。

        non_zero_ind =  (torch.nonzero(image_pred[:,4]))
        try:
            image_pred_ = image_pred[non_zero_ind.squeeze(),:].view(-1,7)
        except:
            continue
        
        #For PyTorch 0.4 compatibility
        #Since the above code with not raise exception for no detection 
        #as scalars are supported in PyTorch 0.4
        if image_pred_.shape[0] == 0:
            continue         

try-except块用于处理无法检测到的情况。 在这种情况下,我们使用continue跳过该图像的其余循环体。

现在,让我们获取图像中检测到的类。

        #Get the various classes detected in the image
        img_classes = unique(image_pred_[:,-1]) # -1 index holds the class index

由于可以对同一类进行多次真实检测,因此我们使用一个名为unique的函数来获取任何给定图像中存在的类。

def unique(tensor):
    tensor_np = tensor.cpu().numpy()
    unique_np = np.unique(tensor_np)
    unique_tensor = torch.from_numpy(unique_np)
    
    tensor_res = tensor.new(unique_tensor.shape)
    tensor_res.copy_(unique_tensor)
    return tensor_res

之后,我们执行NMS类监测

        for cls in img_classes:
            #perform NMS

一旦进入循环,我们要做的第一件事就是提取特定类的检测值(用变量cls表示)。

            #get the detections with one particular class
            cls_mask = image_pred_*(image_pred_[:,-1] == cls).float().unsqueeze(1)
            class_mask_ind = torch.nonzero(cls_mask[:,-2]).squeeze()
            image_pred_class = image_pred_[class_mask_ind].view(-1,7)
            
            #sort the detections such that the entry with the maximum objectness
            #confidence is at the top
            conf_sort_index = torch.sort(image_pred_class[:,4], descending = True )[1]
            image_pred_class = image_pred_class[conf_sort_index]
            idx = image_pred_class.size(0)   #Number of detections

现在,执行NMS

            for i in range(idx):
                #Get the IOUs of all boxes that come after the one we are looking at 
                #in the loop
                try:
                    ious = bbox_iou(image_pred_class[i].unsqueeze(0), image_pred_class[i+1:])
                except ValueError:
                    break

                except IndexError:
                    break

                #Zero out all the detections that have IoU > treshhold
                iou_mask = (ious < nms_conf).float().unsqueeze(1)
                image_pred_class[i+1:] *= iou_mask       

                #Remove the non-zero entries
                non_zero_ind = torch.nonzero(image_pred_class[:,4]).squeeze()
                image_pred_class = image_pred_class[non_zero_ind].view(-1,7)

在这里,我们使用一个函数bbox_iou。 第一个输入是边界框的行,该行由循环中的变量i索引。

bbox_iou的第二个输入是边界框的多行张量。 函数bbox_iou的输出是一个张量,该张量包含第一个输入边界框和第二个输入中包含每个边界框的IOU。

 

 

 如果我们有两个相同类别的边界框,它们的IoU都大于阈值,那么将消除具有较低类别置信度的边界框。 我们已经筛选出边界框,顶部具有较高的置信度。

在循环的主体中,以下几行给出了盒子的IoU,以i索引,所有边界框的索引都高于i。

ious = bbox_iou(image_pred_class[i].unsqueeze(0), image_pred_class[i+1:])

每次迭代,如果索引大于i的任何边界框的IoU(以i索引的框)都大于阈值nms_thresh,则将消除该特定框。

#Zero out all the detections that have IoU > treshhold
iou_mask = (ious < nms_conf).float().unsqueeze(1)
image_pred_class[i+1:] *= iou_mask       

#Remove the non-zero entries
non_zero_ind = torch.nonzero(image_pred_class[:,4]).squeeze()
image_pred_class = image_pred_class[non_zero_ind]     

还要注意,我们将代码行用于在try-catch块中计算ious。 这是因为循环旨在运行idx迭代(image_pred_class中的行数)。 但是,随着循环的进行,可能会从image_pred_class中删除许多边界框。 这意味着,即使从image_pred_class中删除了一个值,我们也无法进行idx迭代。 因此,我们可能尝试索引超出范围的值(IndexError),或者切片image_pred_class [i + 1:]可能会返回一个空张量,并分配该张量来触发ValueError。 到那时,我们可以确定NMS无法进一步删除任何边界框,所以我们跳出了循环。

4.Calculating the IoU

这是bbox_iou函数

def bbox_iou(box1, box2):
    """
    Returns the IoU of two bounding boxes 
    
    
    """
    #Get the coordinates of bounding boxes
    b1_x1, b1_y1, b1_x2, b1_y2 = box1[:,0], box1[:,1], box1[:,2], box1[:,3]
    b2_x1, b2_y1, b2_x2, b2_y2 = box2[:,0], box2[:,1], box2[:,2], box2[:,3]
    
    #get the corrdinates of the intersection rectangle
    inter_rect_x1 =  torch.max(b1_x1, b2_x1)
    inter_rect_y1 =  torch.max(b1_y1, b2_y1)
    inter_rect_x2 =  torch.min(b1_x2, b2_x2)
    inter_rect_y2 =  torch.min(b1_y2, b2_y2)
    
    #Intersection area
    inter_area = torch.clamp(inter_rect_x2 - inter_rect_x1 + 1, min=0) * torch.clamp(inter_rect_y2 - inter_rect_y1 + 1, min=0)
 
    #Union Area
    b1_area = (b1_x2 - b1_x1 + 1)*(b1_y2 - b1_y1 + 1)
    b2_area = (b2_x2 - b2_x1 + 1)*(b2_y2 - b2_y1 + 1)
    
    iou = inter_area / (b1_area + b2_area - inter_area)
    
    return iou

5.Writing the predictions

函数write_results输出形状为D x 8的张量。这里D是所有图像中的真实检测,每个检测由一行表示。 检测具有8个属性,即该检测所属的批次中的图像索引,4个角坐标,目标分数,具有最大置信度的类别的分数以及该类别的索引。

和之前一样,除非我们有检测要分配给它,否则我们不会初始化输出张量。 初始化完成后,我们会将随后的检测连接起来。 我们使用write标志来指示张量是否已初始化。 在遍历类的循环的最后,我们将结果检测添加到张量来输出。

            batch_ind = image_pred_class.new(image_pred_class.size(0), 1).fill_(ind)      
            #Repeat the batch_id for as many detections of the class cls in the image
            seq = batch_ind, image_pred_class

            if not write:
                output = torch.cat(seq,1)
                write = True
            else:
                out = torch.cat(seq,1)
                output = torch.cat((output,out))

在函数的结尾,我们检查输出是否已全部初始化。 如果尚未检测到,则表示该批次的任何图像中都没有检测到一个。 在这种情况下,我们返回0。

    try:
        return output
    except:
        return 0

在本文的最后,我们终于有了张量形式的预测,该预测列出了每个预测的行。 现在剩下的唯一事情就是创建一个输入管道,以从磁盘读取图像,计算预测,在图像上绘制边界框,然后显示/写入这些图像。 这是我们在下一部分中将要做的。

Further Reading

  1. PyTorch tutorial
  2. IoU
  3. Non maximum suppresion
  4. Non-maximum Suppression
posted on 2020-01-10 14:21  NWNU-LHY  阅读(197)  评论(0编辑  收藏  举报