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