非极大值抑制(nms)算法详解[python]

一、起源

目标检测在使用了基于深度学习的端到端模型后效果斐然。目前,常用的目标检测算法,无论是One-stage的SSD系列算法、YOLO系列算法还是Two-stage的基于RCNN系列的算法,非极大值抑制都是其中必不可少的一个组件。在现有的基于anchor的目标检测算法中,都会产生数量巨大的候选矩形框,这些矩形框有很多是指向同一目标,因此就存在大量冗余的候选矩形框。非极大值抑制算法的目的正在于此,它可以消除多余的框,找到最佳的物体检测位置。

非极大值抑制(Non-Maximum Suppression,以下简称NMS算法)的思想搜索局部极大值,抑制非极大值元素。针对不同的应用场景和检测算法,由于矩形框的表征方式不同,NMS算法具有各种变体,本文针对NMS及其各种变体(主要是本文检测相关)进行介绍。

二、经典NMS

经典NMS最初第一次应用到目标检测中是在RCNN算法中,其实现严格按照搜索局部极大值,抑制非极大值元素的思想来实现的,具体的实现步骤如下:

  1. 设定目标框的置信度阈值,常用的阈值是0.5左右

  2. 根据置信度降序排列候选框列表

  3. 选取置信度最高的框A添加到输出列表,并将其从候选框列表中删除

  4. 计算A与候选框列表中的所有框的IoU值,删除大于阈值的候选框

  5. 重复上述过程,直到候选框列表为空,返回输出列表

其中IoU(Intersection over Union)为交并比,如图1所示,IoU相当于两个区域交叉的部分除以两个区域的并集部分得出的结果。图2是IoU为各个取值时的情况展示,一般来说,这个score > 0.5 就可以被认为一个不错的结果了。

非极大值抑制(nms)算法详解[python]_第1张图片

                                                                                              图1

非极大值抑制(nms)算法详解[python]_第2张图片

                                                                                              图2

下面,通过一个具体例子来说明经典NMS究竟做了什么。图3的左图是包含一个检测目标(王闹海)的实例图片。其中的绿色矩形框代表了经过目标检测算法后,生成的大量的带置信度的Bounding box,矩形框左下角的浮点数即代表该Bounding box的置信度。在这里,使用Python对经典NMS算法实现,并应用到该实例中去。当NMS的阈值设为0.2时,最后的效果如图3中右图所示。

非极大值抑制(nms)算法详解[python]_第3张图片

​                                                                                              图3

Python代码如下:

本文所有代码和数据的github地址:https://github.com/lzneu/letGo/tree/master/nms

def nms(bounding_boxes, Nt):
    if len(bounding_boxes) == 0:
        return [], []
    bboxes = np.array(bounding_boxes)
​
    # 计算 n 个候选框的面积大小
    x1 = bboxes[:, 0]
    y1 = bboxes[:, 1]
    x2 = bboxes[:, 2]
    y2 = bboxes[:, 3]
    scores = bboxes[:, 4]
    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
​
    # 对置信度进行排序, 获取排序后的下标序号, argsort 默认从小到大排序
    order = np.argsort(scores)
​
    picked_boxes = []  # 返回值
    while order.size > 0:
        # 将当前置信度最大的框加入返回值列表中
        index = order[-1]
        picked_boxes.append(bounding_boxes[index])
​
        # 获取当前置信度最大的候选框与其他任意候选框的相交面积
        x11 = np.maximum(x1[index], x1[order[:-1]])
        y11 = np.maximum(y1[index], y1[order[:-1]])
        x22 = np.minimum(x2[index], x2[order[:-1]])
        y22 = np.minimum(y2[index], y2[order[:-1]])
        w = np.maximum(0.0, x22 - x11 + 1)
        h = np.maximum(0.0, y22 - y11 + 1)
        intersection = w * h
​
        # 利用相交的面积和两个框自身的面积计算框的交并比, 将交并比大于阈值的框删除
        ious = intersection / (areas[index] + areas[order[:-1]] - intersection)
        left = np.where(ious < Nt)
        order = order[left]
    return picked_boxes

三、soft-NMS

经典NMS是为了去除重复的预测框,这种算法在图片中只有单个物体被检测的情况下具有很好的效果,如上图所示,“腰子”王闹海被成功的检测出来。然而,经典NMS算法存在着一些问题:对于重叠物体无法很好的检测。当图像中存在两个重叠度很高的物体时,经典NMS会过滤掉其中置信度较低的一个。如下图所示,图中存在两个王闹海,经典NMS过滤后的结果如下图4所示:

非极大值抑制(nms)算法详解[python]_第4张图片

                                                                                              图4

而我们期望的结果是两个腰子都被算法成功检测出来。

为了解决这类问题,Jan Hosang,等人提出了Soft-NMS算法。Soft-NMS的算法伪代码如图5所示。其中红色框为经典NMS的步骤,而绿色框中的内容为Soft-NMS改进的步骤。可以看出,相对于经典NMS算法,Soft-NMS仅仅修改了一行代码。当选取了最大置信度的Bounding box之后,计算其余每个Bounding box与Bounding box的I ou值,经典NMS算法的做法是直接删除Iou大于阈值的Bounding box;而Soft-NMS则是使用一个基于Iou的衰减函数,降低Iou大于阈值Nt的Bounding box的置信度,IoU越大,衰减程度越大。

换一个角度来理解,经典NMS同样可以用一个基于Iou的衰减函数来表示,只是这个衰减函数是个0-1函数,当Iou大于阈值Nt,直接降低该Bounding box的置信度到0。关于经典NMS和Soft-NMS的衰减函数区别,可以通过如下的公式来表示[1]:

  • 经典NMS:                          非极大值抑制(nms)算法详解[python]_第5张图片

  • Soft-NMS-线性:               非极大值抑制(nms)算法详解[python]_第6张图片

  • Soft-NMS-高斯:                非极大值抑制(nms)算法详解[python]_第7张图片

非极大值抑制(nms)算法详解[python]_第8张图片

                                                                                              图5​

接下来, 继续使用图4的实例来验证Soft-NMS的效果,使用Python对Soft-NMS算法的实现如下

Python代码:

def soft_nms(bboxes, Nt=0.3, sigma2=0.5, score_thresh=0.3, method=2):
    # 在 bboxes 之后添加对于的下标[0, 1, 2...], 最终 bboxes 的 shape 为 [n, 5], 前四个为坐标, 后一个为下标
    res_bboxes = deepcopy(bboxes)
    N = bboxes.shape[0]  # 总的 box 的数量
    indexes = np.array([np.arange(N)])  # 下标: 0, 1, 2, ..., n-1
    bboxes = np.concatenate((bboxes, indexes.T), axis=1)  # concatenate 之后, bboxes 的操作不会对外部变量产生影响
    # 计算每个 box 的面积
    x1 = bboxes[:, 0]
    y1 = bboxes[:, 1]
    x2 = bboxes[:, 2]
    y2 = bboxes[:, 3]
    scores = bboxes[:, 4]
    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
​
    for i in range(N):
        # 找出 i 后面的最大 score 及其下标
        pos = i + 1
        if i != N - 1:
            maxscore = np.max(scores[pos:], axis=0)
            maxpos = np.argmax(scores[pos:], axis=0)
        else:
            maxscore = scores[-1]
            maxpos = 0
        # 如果当前 i 的得分小于后面的最大 score, 则与之交换, 确保 i 上的 score 最大
        if scores[i] < maxscore:
            bboxes[[i, maxpos + i + 1]] = bboxes[[maxpos + i + 1, i]]
            scores[[i, maxpos + i + 1]] = scores[[maxpos + i + 1, i]]
            areas[[i, maxpos + i + 1]] = areas[[maxpos + i + 1, i]]
        # IoU calculate
        xx1 = np.maximum(bboxes[i, 0], bboxes[pos:, 0])
        yy1 = np.maximum(bboxes[i, 1], bboxes[pos:, 1])
        xx2 = np.minimum(bboxes[i, 2], bboxes[pos:, 2])
        yy2 = np.minimum(bboxes[i, 3], bboxes[pos:, 3])
        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        intersection = w * h
        iou = intersection / (areas[i] + areas[pos:] - intersection)
        # Three methods: 1.linear 2.gaussian 3.original NMS
        if method == 1:  # linear
            weight = np.ones(iou.shape)
            weight[iou > Nt] = weight[iou > Nt] - iou[iou > Nt]
        elif method == 2:  # gaussian
            weight = np.exp(-(iou * iou) / sigma2)
        else:  # original NMS
            weight = np.ones(iou.shape)
            weight[iou > Nt] = 0
        scores[pos:] = weight * scores[pos:]
    # select the boxes and keep the corresponding indexes
    inds = bboxes[:, 5][scores > score_thresh]
    keep = inds.astype(int)
    return res_bboxes[keep]

                                                                                              图6

四、Locality-Aware NMS

以上的NMS或相关算法均为针对目标检测问题而提出的,而文本检测作为目标检测的一个特殊领域,具有其特殊之处,比如文本检测中面临的成千上万的候选几何体,如果使用计算复杂度为 ​ 的经典NMS类算法,由于n的数量过大,这是不可接受的。为此旷世公司于2017在EAST(一种非常好用的One-stage的文本检测算法,敬请关注交易只能团队下期文章!)论文[2]中提出了Locality-Aware NMS算法。

Locality-Aware NMS算法考虑了文本检测任务中临近像素高度相关的特征,提出了一种基于行的几何体合并方法,它的步骤主要是两个:

  1. 合并同一行区域中的几何体积

  2. 对合并后的Bounding box集合进行标准的NMS操作

Locality-Aware NMS提升了算法的时间复杂度,在最优情况下可以提升至 ​ 。即使是最坏情况,也不会比经典NMS的计算量更大。

Python代码如下:

import numpy as np
from shapely.geometry import Polygon
​
def intersection(g, p):
    # 取g,p中的几何体信息组成多边形
    g = Polygon(g[:8].reshape((4, 2)))
    p = Polygon(p[:8].reshape((4, 2)))
    # 判断g,p是否为有效的多边形几何体
    if not g.is_valid or not p.is_valid:
        return 0
    # 取两个几何体的交集和并集
    inter = Polygon(g).intersection(Polygon(p)).area
    union = g.area + p.area - inter
    if union == 0:
        return 0
    else:
        return inter / union
​
def weighted_merge(g, p):
    # 取g,p两个几何体的加权(权重根据对应的检测得分计算得到)
    g[:8] = (g[8] * g[:8] + p[8] * p[:8]) / (g[8] + p[8])
    # 合并后的几何体的得分为两个几何体得分的总和
    g[8] = (g[8] + p[8])
    return g
​
def standard_nms(S, thres):
    # 标准NMS
    order = np.argsort(S[:, 8])[::-1]
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        ovr = np.array([intersection(S[i], S[t]) for t in order[1:]])
        inds = np.where(ovr <= thres)[0]
        order = order[inds + 1]
    return S[keep]
​
​
def nms_locality(polys, thres=0.3):
    '''
    locality aware nms of EAST
    :param polys: a N*9 numpy array. first 8 coordinates, then prob
    :return: boxes after nms
    '''
    S = []  # 合并后的几何体集合
    p = None  # 合并后的几何体
    for g in polys:
        if p is not None and intersection(g, p) > thres:  # 若两个几何体的相交面积大于指定的阈值,则进行合并
            p = weighted_merge(g, p)
        else:  # 反之,则保留当前的几何体
            if p is not None:
                S.append(p)
            p = g
    if p is not None:
        S.append(p)
    if len(S) == 0:
        return np.array([])
    return standard_nms(np.array(S), thres)

五、Softer-NMS

以上的NMS算法,无论是经典NMS还是Soft-NMS算法,都是在一种假设前提下:置信度最高的Bounding box就是目标的候选位置最精确的物体位置。但是事实上,这个假设可能并不成立,或者说并不那么精确。针对这个问题,来自卡内基梅隆大学与旷视科技的研究人员在文中提出了一种新的非极大抑制算法Softer-NMS,显著改进了目标检测的定位精度,代码已经开源,目前Github上的Star已超100,可谓短短两天已经引起了不小的关注。作者关注了两种目前NMS会出现的问题:

  1. 所有的候选Bounding box都够精确,无法确定该选择哪一个,见图7a

  2. 具有高置信度的Bounding box不够精确,见图7b

非极大值抑制(nms)算法详解[python]_第9张图片

​                                                                                              图7

Softer-NMS正是为了这两个问题而设计的。关于Softer-NMS的相关解析,敬请关注交易智能团队后续分享。

参考文献:

[1] Bodla N , Singh B , Chellappa R , et al. Improving Object Detection With One Line of Code[J]. 2017.

[2] Zhou X , Yao C , Wen H , et al. EAST: An Efficient and Accurate Scene Text Detector[J]. 2017.

[3] He Y , Zhu C , Wang J , et al. Bounding Box Regression with Uncertainty for Accurate Object Detection[J]. 2018.

[4] 非极大值抑制(Non-Maximum Suppression,NMS) - 康行天下 - 博客园

[5] 非极大值抑制(Non-Maximum Suppression) - 云+社区 - 腾讯云

[6] Intersection over Union (IoU) for object detection - PyImageSearch

[7] NMS 算法源码实现 | 从零开始的BLOG

[8] https://www.52cv.net/?p=1434

[9] https://www.jishuwen.com/d/24Tx#tuit

你可能感兴趣的:(python,算法,深度学习)