非极大值抑制(NMS),采用IOU筛选预测框,进行可视化过程分析【附代码】

在目标检测领域中,对于边界框的回归问题上,都会遇到一个专业名词—–非极大值抑制(NMS)。对于网络的输出,我们会通过设置confidence(置信度)阈值来过滤一部分目标,但此时一个物体上还会存在多个预测框,此刻就需要对这些预测框进一步处理,采用的方法就是非极大值抑制,而常常采用的手段是IOU(交并比)。通过设置IOU阈值来获得我们最终的预测框。

目录

 

我们拿yolo来说,如果网络输入是416*416大小,那么会产生52*52、26*26、13*13三个不同的特征层来预测不同尺度的目标,同时每个特征层又会产生3种不同尺度的锚框(先验框),那么我们就可以获得52*52*3+26*26*3+13*13*3=10647个锚框。

如果你对网络进行输出,会发现在锚框的tensor后又跟着一个维度,比如这样的(1,10647,x),其中1指的是batch_size的维度,10647指的锚框数量,后面的x是包含了bbox(x,y,w,h)4个信息,confidence和预测类别信息,如果你是coco类,那么后面这个维度就是85。你可以理解为每一个锚框预测都会产生这几个值。

下面我将用代码来举个例子,并且为了方便起见,我这里只有1个类。

假设通过网络输出我有这样一个tensor,大小是[1,3,6],3是有3个锚框。在6这个维度上,表示[center_x,center_y,width,height,conf,classes]

pred = torch.tensor([[
         [1.2312e+01, 1.5709e+01, 3.8783e+01, 6.6860e+01, 1.2674e-01, 1.0000e+00],
         [5.4555e+01, 1.7518e+01, 4.0713e+01, 3.3931e+01, 2.2637e-01, 1.0000e+00],
         [7.8304e+01, 1.6306e+01, 4.9968e+01, 3.6646e+01, 2.3244e-01, 1.0000e+00]
                    ]])

坐标系可以这样建立:

'''
            w
    ——————————————————> x
    |                |
    |                |
    |       *        |  h  
    |     (x,y)      |
    |                |
    |-----------------
    y
'''

 然后我们将这几个预测框画出来,代码如下。其中pred.squeeze(0)是将第0个维度去掉,即去除batch_size这个维度,获得[3,6]的shape.由于在matplotlib中,坐标系默认是左下角为原点,y轴向上,x向右,但我们图像中的坐标系是左上角为坐标原点,x轴向右,y轴向下,因此可以用plt.gca().invert_yaxis()进行转换,同时,我们获得预测框信息中前两列为中心点坐标,我们需要对其重新计算,获得左上角坐标。

pred = pred.squeeze(0)
pred = pred.cpu().detach().numpy()

fig = plt.figure()

ax = fig.add_subplot(111)
plt.plot(pred[:, 0], pred[:, 1], 'o')
# plt.Rectangle(xy,w,h) 默认为xy为矩形左下角坐标 左下角x=center_x - w/2;y=center_y + h/2
# 换成左上角为原点,那么默认从左上角画矩形,x=center_x - w/2;y=center_y - h/2
rect1 = plt.Rectangle((pred[0, 0] - pred[0, 2]/2, (pred[0, 1] - pred[0, 3]/2)), pred[0, 2], pred[0, 3], fill=False)
rect2 = plt.Rectangle((pred[1, 0] - pred[1, 2]/2, pred[1, 1] - pred[1, 3]/2), pred[1, 2], pred[1, 3], fill=False)
rect3 = plt.Rectangle((pred[2, 0] - pred[2, 2]/2, pred[2, 1] - pred[2, 3]/2), pred[2, 2], pred[2, 3], fill=False)
ax.add_patch(rect1)
ax.add_patch(rect2)
ax.add_patch(rect3)
plt.xlim(-10, 150)
plt.ylim(-20, 110)
plt.gca().invert_yaxis()  # 将坐标原点设置为左上角
plt.show()

我们将获得如下的图,三个预测框,蓝色的点是中心点坐标。 

非极大值抑制(NMS),采用IOU筛选预测框,进行可视化过程分析【附代码】

 然后我们可以进一步的输出,其中 class_conf, class_pred = pred[:, 5:].max(1) 从第5个维度开始,选择每一行最大class_conf的index,

for image_i, pred in enumerate(prediction):
    class_conf, class_pred = pred[:, 5:].max(1)

输出如下:

class_conf =  tensor([1., 1., 1.]) class_pred =  tensor([0, 0, 0])

这表明第5个维度输出的类别最大概率值,对应的索引都为0这个类,比如在voc数据集中第0这个类指person类。

接下来是获得置信度confidence或者有些代码里也写sorce,该值是类别的概率值和分类的值相乘

pred[:, 4] *= class_conf  # conf = obj_conf * class_conf

我们打印一下此刻新的pred

tensor([[12.3120, 15.7090, 38.7830, 66.8600,  0.1267,  1.0000],
        [54.5550, 17.5180, 40.7130, 33.9310,  0.2264,  1.0000],
        [78.3040, 16.3060, 49.9680, 36.6460,  0.2324,  1.0000]])

通过置信度阈值滤除进行筛选

设置一个置信度阈值,比如是0.2,然后设置一个min_wh=2,表示w和h最小值阈值,在我们预测边界框中进一步筛选(返回的是布尔值) 

i = (pred[:, 4] > 0.2) & (pred[:, 2] > min_wh) & (pred[:, 3] > min_wh)  # 返回三个bool值 是看行的内容,不是看列的内容,把满足条件的两个锚框筛选出来

那么通过上述的我们知道,将会把pred中第一行输出为False,因为该行中第置信度那个维度是0.1267,低于我们的阈值(也就是我们滤除的一个边界框),即输出为:

tensor([[54.5550, 17.5180, 40.7130, 33.9310,  0.2264,  1.0000],
        [78.3040, 16.3060, 49.9680, 36.6460,  0.2324,  1.0000]])

然后我们对class_conf通过i返回的bool值进行筛选,(筛选之前的输出为:tensor([1., 1.,1.]))

筛选之后就变为了:tensor([1., 1.])  把原来中间那个去掉了。

同理,对class_pred也进行筛选

class_pred = class_pred[i].unsqueeze(1).float()

接下来是对边界框信息进行处理,我们刚开始得到的信息是center_x,center_y,w,h,现在我们要得到x1,y1,x2,y2,即边界框的左上角和右下角

def xywh2xyxy(x):
    # Convert bounding box format from [x, y, w, h] to [x1, y1, x2, y2]
    y = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2
    y[:, 1] = x[:, 1] - x[:, 3] / 2
    y[:, 2] = x[:, 0] + x[:, 2] / 2
    y[:, 3] = x[:, 1] + x[:, 3] / 2
    return y

然后我们将刚刚通过置信度筛选的各个信息进行拼接,pred2[:,:5]是前4个维度(box信息),class_conf.unsqueeze(1)是筛选后class_conf再通过unsqueeze在1这个维度上再加一个维度(为了方便拼接),cat中的维度1其实就是进行列的拼接

pred2 = torch.cat((pred2[:, :5], class_conf.unsqueeze(1), class_pred), 1)

得到的结果如下,我们可以看到现在多了一个维度,最后一个就是预测得到类的索引: 

tensor([[ 34.1985,   0.5525,  74.9115,  34.4835,   0.2264,   1.0000,   0.0000],
            [ 53.3200,  -2.0170, 103.2880,  34.6290,   0.2324,   1.0000,   0.0000]])

 然后我们再进行一次可视化

fig = plt.figure()
ax = fig.add_subplot(111)
rect1 = plt.Rectangle((pred2[0,0].cpu().detach().numpy(), pred2[0,1].cpu().detach().numpy()),
                          pred2[0, 2].cpu().detach().numpy()-pred2[0, 0].cpu().detach().numpy(),
                          pred2[0, 3].cpu().detach().numpy() - pred2[0,1].cpu().detach().numpy(),
                          fill=False)
rect2 = plt.Rectangle((pred2[1, 0].cpu().detach().numpy(), pred2[1, 1].cpu().detach().numpy()),
                          pred2[1, 2].cpu().detach().numpy() - pred2[1, 0].cpu().detach().numpy(),
                          pred2[1, 3].cpu().detach().numpy() - pred2[1, 1].cpu().detach().numpy(),
                          fill=False)
ax.add_patch(rect1)
ax.add_patch(rect2)
plt.xlim(-10, 150)
plt.ylim(-20, 110)
plt.gca().invert_yaxis()  # 将坐标原点设置为左上角
plt.show()

可以于之前那三个预测框进行对比,可以很清楚的看到,我们已经对其中的一个边界框进行了滤除,现在只剩下两个预测框。 

非极大值抑制(NMS),采用IOU筛选预测框,进行可视化过程分析【附代码】

 

通过IOU进一步筛选预测框

然后剩余的预测框按置信度从大到小进行排列

pred2 = pred2[(-pred2[:, 4]).argsort()]

tensor([[ 53.3200,  -2.0170, 103.2880,  34.6290,   0.2324,   1.0000,   0.0000],
            [ 34.1985,   0.5525,  74.9115,  34.4835,   0.2264,   1.0000,   0.0000]])

 

    for c in pred2[:, -1].unique():
        dc = pred2[pred2[:, -1] == c]  # 再最后一列找和c一致的类
        dc = dc[:min(len(dc), 100)]  # limit to first 100 boxes  选出不超过100个框

        while len(dc) > 1:
            det_max.append(dc[:1])  # save highest conf detection
            
            iou = bbox_iou(dc[0], dc[1:])  # iou with other boxes
            
            dc = dc[1:][iou < 0.2]  # remove ious > threshold

先附上IOU的完整代码如下:

def bbox_iou(box1, box2, x1y1x2y2=True):
    # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
    # box1是通过置信度阈值得到最大的预测框
    box2 = box2.t()
    # Get the coordinates of bounding boxes
    if x1y1x2y2:
        # x1, y1, x2, y2 = box1
        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]
    else:
        # x, y, w, h = box1
        b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
        b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
        b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
        b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2

    # Intersection area
    # 左上角用max  右下角min; w = 右下角x-左上角x,h=右下角y - 左上角y
    inter_area = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
                 (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)

    # Union Area
    union_area = ((b1_x2 - b1_x1) * (b1_y2 - b1_y1) + 1e-16) + \
                 (b2_x2 - b2_x1) * (b2_y2 - b2_y1) - inter_area

    return inter_area / union_area  # iou

然后我们把上述代码再详细理解一下。

获得最大置信度bbox的坐标和其他bbox的坐标(代码中的box2.t()是进行转置,主要就是为了方便获得坐标,box2.t()得到的为:)

tensor([[34.1985],
        [ 0.5525],
        [74.9115],
        [34.4835],
        [ 0.2264],
        [ 1.0000],
        [ 0.0000]])

# x1, y1, x2, y2 = box1
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]

算出最大置信度框和其他框相交的部分面积,如图像中红色区域面积

# Intersection area
# 左上角用max  右下角min; w = 右下角x-左上角x,h=右下角y - 左上角y
inter_area = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
             (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)

非极大值抑制(NMS),采用IOU筛选预测框,进行可视化过程分析【附代码】

 可以得到相交的面积为:

tensor([732.6212])

然后算并集面积的值,算并集的面积就是两个框的面积之和再减去相交的面积

因此公式为:

(box1_x2-box1_x1)*(box1_y2-box_y1)+(box2_x2′-box2_x1′)*(box2_y2′-box2_y1′)-inter_area

代码中的1e-16是为了防止再求IOU时分母出现0.

union_area = ((b1_x2 - b1_x1) * (b1_y2 - b1_y1) + 1e-16) + \
                 (b2_x2 - b2_x1) * (b2_y2 - b2_y1) - inter_area

因此我们可以得到并集的面积为:

tensor([2479.9390]) 

燃尽计算IOU,IOU就是交并比,意思就是两个框的交集面积与并集面积之比。 

inter_area / union_area

我们可以计算得到我们两个预测框的iou值为: 

tensor([0.2954])

然后我们将小于我们人为设置的IOU阈值的预测框保留,大于的则滤除(通常IOU设置为0.5,但因为我这里为了演示,所以取0.2)【为什么说去除大于阈值的,因为两个预测框如果IOU越大,说明重叠越严重,我们希望的只保留一个】,下图就是我们最终保留下来的预测框

非极大值抑制(NMS),采用IOU筛选预测框,进行可视化过程分析【附代码】

 完整的NMS代码:

def non_max_suppression(prediction, conf_thres=0.5, nms_thres=0.4):
    # prediction shape (1,10647,6) 10647是锚框数,每个锚框有6个值,(x,y,w,h,conf,class)
    """
    Removes detections with lower object confidence score than 'conf_thres'
    Non-Maximum Suppression to further filter detections.
    Returns detections with shape:
        (x1, y1, x2, y2, object_conf, class_conf, class)
    """
    min_wh = 2  # (pixels) minimum box width and height

    output = [None] * len(prediction)
    for image_i, pred in enumerate(prediction):
        # Experiment: Prior class size rejection
        # x, y, w, h = pred[:, 0], pred[:, 1], pred[:, 2], pred[:, 3]
        # a = w * h  # area
        # ar = w / (h + 1e-16)  # aspect ratio
        

        # Filter out confidence scores below threshold
        class_conf, class_pred = pred[:, 5:].max(1)  # max class_conf, index
        pred[:, 4] *= class_conf  # finall conf = obj_conf * class_conf

        i = (pred[:, 4] > conf_thres) & (pred[:, 2] > min_wh) & (pred[:, 3] > min_wh)
        # s2=time.time()
        pred2 = pred[i]
        # print("++++++pred2 = pred[i]",time.time()-s2, pred2)

        # If none are remaining => process next image
        if len(pred2) == 0:
            continue

        # Select predicted classes
        class_conf = class_conf[i]
        class_pred = class_pred[i].unsqueeze(1).float()

        # Box (center x, center y, width, height) to (x1, y1, x2, y2)
        pred2[:, :4] = xywh2xyxy(pred2[:, :4])
        # pred[:, 4] *= class_conf  # improves mAP from 0.549 to 0.551

        # Detections ordered as (x1y1x2y2, obj_conf, class_conf, class_pred)
        pred2 = torch.cat((pred2[:, :5], class_conf.unsqueeze(1), class_pred), 1)

        # Get detections sorted by decreasing confidence scores
        pred2 = pred2[(-pred2[:, 4]).argsort()]

        det_max = []
        
        for c in pred2[:, -1].unique():
            dc = pred2[pred2[:, -1] == c]  # select class c
            dc = dc[:min(len(dc), 100)]  # limit to first 100 boxes

            # Non-maximum suppression
            

             while len(dc) > 1:
                    det_max.append(dc[:1])  # save highest conf detection
                
                    iou = bbox_iou(dc[0], dc[1:])  # iou with other boxes
                    dc = dc[1:][iou < nms_thres]  # remove ious > threshold


        if len(det_max): # 保留下来的置信度最高的那个预测框
            det_max = torch.cat(det_max)  # concatenate
            output[image_i] = det_max[(-det_max[:, 4]).argsort()]  # sort
    return output

 

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2022年6月13日 上午11:08
下一篇 2022年6月13日

相关推荐