一文汇总 NMS,持续更新~~

转载请注明作者和出处: http://blog.csdn.net/john_bh/

1. NMS

1.1 什么是NMS

非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素,可以理解为局部最大搜索,主要用于删除高度冗余的bounding box,如下如图所示:
在这里插入图片描述
NMS在计算机视觉领域有着非常重要的应用,如视频目标跟踪、数据挖掘、3D重建、目标识别,图像分割以及纹理分析等。

1.2 为什么要用 NMS

以目标检测为例:目标检测的过程中在同一目标的位置上会产生大量的候选框,这些候选框相互之间可能会有重叠,此时我们需要利用非极大值抑制找到最佳的目标边界框,消除冗余的边界框。Demo如下图:
在这里插入图片描述
左图是人脸检测的候选框结果,每个边界框有一个置信度得分(confidence score),如果不使用非极大值抑制,就会有多个候选框出现。右图是使用非极大值抑制之后的结果,符合我们人脸检测的预期结果。

1.3 如何使用 NMS

NMS 使用在 回归边框之后,即所有的框已经被分类且精修了位置。且所有区域提议的预测结果已经由置信度与阈值初步筛选之后。

非极大值抑制的流程如下:

  1. 将所有的框按类别划分,并剔除背景类,因为无需NMS;
  2. 对每个物体类中的边界框(Bounding Box),按照分类置信度降序排列;
  3. 在某一类中,选择置信度最高的边界框Bounding_Box_1,将Bounding_Box_1从输入列表中去除,并加入输出列表;
  4. 逐个计算Bounding_Box_1与其余Bounding_Box的交并比IoU,若IoU(Bounding_Box_1,Bounding_Box_2) 阈值TH,则在输入去除Bounding_Box_2;
  5. 重复步骤 3 ~ 4,直到输入列表为空,完成一个物体类的遍历;
  6. 重复 2 ~ 5,直到所有物体类的 NMS 处理完成;
  7. 输出列表,算法结束

可以利用下面这个例子来体会算法的具体流程:

在这里插入图片描述
我们可以看到图中每一只狗都有各自的边界框,每个框都有各自的参数(x,y,h,w,s)。首先按照置信度S对边界框进行排序,找出置信度最大的边界框,这里找到的是置信度为0.9的红色框,将红色框从原集合B中剔除,加入到集合D中。然后将置信度为0.9的红色框与剩余的边界框做IOU,得到的结果分别是0、0.6、0.05、0,并且假设阈值为0.5,可以看到此时与粉色框的IOU > = 0.6,大于阈值,此时直接剔除粉色框,即将其置信度设置为0。再从剩余的框中继续选择置信度最大的边界框,这里找到是置信度为0.89的黄色框,将黄色框从集合B中剔除,加入到保留框集合D,再将其与剩余的框做IOU,结果分别为0.75和0.7,均大于阈值,直接剔除。此时集合B为空,计算结束,可以看到我们最终得到两个边界框。

一般将阈值设置为0-0.5,阈值太大将不会有效的剔除多余的边界框,达不到过滤的效果,如下下面的这个例子:
在这里插入图片描述

IoU:intersection-over-union,即两个边界框的交集部分除以它们的并集。
在这里插入图片描述

  • Numpy 实现 IOU:
import numpy as np
def compute_iou(box1, box2, wh=False):
        """
        compute the iou of two boxes.
        Args:
                box1, box2: [xmin, ymin, xmax, ymax] (wh=False) or [xcenter, ycenter, w, h] (wh=True)
                wh: the format of coordinate.
        Return:
                iou: iou of box1 and box2.
        """
        if wh == False:
                xmin1, ymin1, xmax1, ymax1 = box1
                xmin2, ymin2, xmax2, ymax2 = box2
        else:
                xmin1, ymin1 = int(box1[0]-box1[2]/2.0), int(box1[1]-box1[3]/2.0)
                xmax1, ymax1 = int(box1[0]+box1[2]/2.0), int(box1[1]+box1[3]/2.0)
                xmin2, ymin2 = int(box2[0]-box2[2]/2.0), int(box2[1]-box2[3]/2.0)
                xmax2, ymax2 = int(box2[0]+box2[2]/2.0), int(box2[1]+box2[3]/2.0)
 
        ## 获取矩形框交集对应的左上角和右下角的坐标(intersection)
        xx1 = np.max([xmin1, xmin2])
        yy1 = np.max([ymin1, ymin2])
        xx2 = np.min([xmax1, xmax2])
        yy2 = np.min([ymax1, ymax2])
 
        ## 计算两个矩形框面积
        area1 = (xmax1-xmin1) * (ymax1-ymin1) 
        area2 = (xmax2-xmin2) * (ymax2-ymin2)
 
        inter_area = (np.max([0, xx2-xx1])) * (np.max([0, yy2-yy1]))#计算交集面积
        iou = inter_area / (area1+area2-inter_area+1e-6)#计算交并比
return iou
  • Faster R-CNN 中 NMS实现代码:
# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------

import numpy as np

def py_cpu_nms(dets, thresh):
    """Pure Python NMS baseline."""
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    x2 = dets[:, 2]
    y2 = dets[:, 3]
    scores = dets[:, 4]

    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)

        inds = np.where(ovr <= thresh)[0]
        order = order[inds + 1]

    return keep

PyTorch 实现 NMS (参考链接:NMS算法详解(附Pytorch实现代码)):

# NMS算法
    # bboxes维度为[N,4],scores维度为[N,], 均为tensor
    def nms(self, bboxes, scores, threshold=0.5):
        x1 = bboxes[:,0]
        y1 = bboxes[:,1]
        x2 = bboxes[:,2]
        y2 = bboxes[:,3]
        areas = (x2-x1)*(y2-y1)   # [N,] 每个bbox的面积
        _, order = scores.sort(0, descending=True)    # 降序排列

        keep = []
        while order.numel() > 0:       # torch.numel()返回张量元素个数
            if order.numel() == 1:     # 保留框只剩一个
                i = order.item()
                keep.append(i)
                break
            else:
                i = order[0].item()    # 保留scores最大的那个框box[i]
                keep.append(i)

            # 计算box[i]与其余各框的IOU(思路很好)
            xx1 = x1[order[1:]].clamp(min=x1[i])   # [N-1,]
            yy1 = y1[order[1:]].clamp(min=y1[i])
            xx2 = x2[order[1:]].clamp(max=x2[i])
            yy2 = y2[order[1:]].clamp(max=y2[i])
            inter = (xx2-xx1).clamp(min=0) * (yy2-yy1).clamp(min=0)   # [N-1,]

            iou = inter / (areas[i]+areas[order[1:]]-inter)  # [N-1,]
            idx = (iou <= threshold).nonzero().squeeze() # 注意此时idx为[N-1,] 而order为[N,]
            if idx.numel() == 0:
                break
            order = order[idx+1]  # 修补索引之间的差值
        return torch.LongTensor(keep)   # Pytorch的索引值为LongTensor

2. NMS 提升精度

2.1 分类优先

  1. 传统 NMS
    传统NMS有多个名称,据不完全统计可以被称为:Traditional / Original / Standard / Greedy NMS,为统一起见,下称Traditional NMS。
    Traditional NMS算法是最为经典的版本,伪代码如下:在这里插入图片描述
    作为最为人所知晓的版本,Traditional NMS不仅有CPU版本,并且现已有GPU版本,GPU版本为Ross Girshick大神编写,俗称CUDA NMS。多个深度学习框架也已将CUDA NMS纳入,作为基本函数使用,如Pytorch在Torchvision 0.3中正式集成了CUDA NMS。

    缺点:

    1. 顺序处理的模式,计算IoU拖累了运算效率;
    2. 剔除机制太严格,依据NMS阈值暴力剔除;
    3. 阈值是经验选取的;
    4. 评判标准是IoU,即只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面。
  2. Soft-NMS
    Soft-NMS 算法伪代码如下(参考:Soft-NMS – Improving Object Detection With One Line of Code):在这里插入图片描述
    Soft-NMS是Traditional NMS的推广,主要旨在缓解Traditional NMS的第二条缺点。数学上看,Traditional NMS的剔除机制可视为:
    在这里插入图片描述
    显然,对于IoU≥NMS阈值的相邻框,Traditional NMS的做法是将其得分暴力置0。这对于有遮挡的案例较不友好。因此Soft-NMS的做法是采取得分惩罚机制,使用一个与IoU正相关的惩罚函数对得分 s s s 进行惩罚。线性乘法:
    在这里插入图片描述
    其中 M M M 代表当前的最大得分框。
    线性惩罚有不光滑的地方,因而还有一种高斯惩罚:
    在这里插入图片描述
    在迭代终止之后,Soft-NMS依据预先设定的得分阈值来保留幸存的检测框,通常设为0.0001
    该文对两种惩罚方法的超参数也进行了实验,结果验证了超参数的不敏感性。经本人实测,Soft-NMS在Faster R-CNN中的提升约有0.5-0.8个点的AP提升。

    缺点:

    1. 仍然是顺序处理的模式,运算效率比Traditional NMS更低;
    2. 对双阶段算法友好,而在一些单阶段算法上可能失效;
    3. 如果存在定位与得分不一致的情况,则可能导致定位好而得分低的框比定位差得分高的框惩罚更多(遮挡情况下);
    4. 评判标准是IoU,即只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面。

2.2 定位优先

IoU-Guided NMS出现于 IoU-Net(Acquisition of Localization Confidence for Accurate Object Detection 一文中,研究者认为框的定位与分类得分可能出现不一致的情况,特别是框的边界有模棱两可的情形时。因而该文提出了IoU预测分支,来学习定位置信度,进而使用定位置信度来引导NMS。
在这里插入图片描述
在这里插入图片描述
具体来说,就是使用定位置信度作为NMS的筛选依据,每次迭代挑选出最大定位置信度的框 M M M ,然后将 IoU≥NMS 阈值的相邻框剔除,但把冗余框及其自身的最大分类得分直接赋予 M M M ,这样一来,最终输出的框必定是同时具有最大分类得分与最大定位置信度的框。

优点:

  1. IoU-Guided NMS有助于提高严格指标下的精度,如AP75, AP90。

缺点:

  1. 顺序处理的模式,运算效率与Traditional NMS相同;
  2. 需要额外添加IoU预测分支,造成计算开销;
  3. 评判标准是IoU,即只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面。

2.3 加权平均

Weighted NMS出现于Inception Single Shot MultiBox Detector for object detection一文中。论文认为Traditional NMS每次迭代所选出的最大得分框未必是精确定位的,冗余框也有可能是定位良好的。那么与直接剔除机制不同,Weighted NMS顾名思义是对坐标加权平均,加权平均的对象包括 M M M 自身以及 IoU≥NMS 阈值的相邻框。
在这里插入图片描述
加权的权重为 w i = s i I o U ( M , B i ) w_i=s_iIoU(M,B_i) wi=siIoU(M,Bi) ,表示得分与IoU的乘积。

优点:

  1. Weighted NMS通常能够获得更高的Precision和Recall,以本人的使用情况来看,只要NMS阈值选取得当,Weighted
    NMS均能稳定提高AP与AR,无论是AP50还是AP75,也不论所使用的检测模型是什么。

缺点:

  1. 顺序处理模式,且运算效率比Traditional NMS更低;
  2. 加权因子是IoU与得分,前者只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面;而后者受到定位与得分不一致问题的限制。

2.4 方差加权平均

Softer-NMS (Bounding Box Regression with Uncertainty for Accurate Object Detection) 同样是坐标加权平均的思想,不同在于权重 w i w_i wi发生变化,以及引入了box边界的不确定度。
在这里插入图片描述
加权公式如下:
在这里插入图片描述
其中权重 w i = e − ( 1 − I o U ( M , B i ) ) 2 σ t w_i=e^{-\frac {(1-IoU(M,B_i))^2}{\sigma_{t}}} wi=eσt(1IoU(M,Bi))2 抛弃了得分 s i s_i si,而只与IoU有关。

在加权平均的过程中,权重越大有两种情形:

  1. M M M 的IoU越大;
  2. 方差越小,代表定位不确定度越低。

在这里插入图片描述
var voting表示方差加权平均

优点:

  1. 可以与Traditional NMS或Soft-NMS结合使用。
  2. 通常可以稳定提升AP与AR。

缺点:

  1. 顺序处理模式,且运算效率比Traditional NMS更低。
  2. 需要修改模型来预测方差。
  3. 加权因子是IoU与方差,前者依然只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面。

2.5 自适应阈值

以上这些NMS都基于这样的假设:与当前最高得分框重叠越大,越有可能是冗余框。

Adaptive NMS (Adaptive NMS: Refining Pedestrian Detection in a Crowd)的研究者认为这在物体之间有严重遮挡时可能带来不好的结果。我们期望当物体分布稀疏时,NMS大可选用小阈值以剔除更多冗余框;而在物体分布密集时,NMS选用大阈值,以获得更高的召回。既然如此,该文提出了密度预测模块,来学习一个框的密度。

Adaptive NMS 伪代码:
在这里插入图片描述
在这里插入图片描述
一个GT框 B i B_i Bi 的密度标签定义如下,
在这里插入图片描述

模型的输出将变为 ( x , y , w , h , s , d ) (x,y,w,h,s,d) (x,y,w,h,s,d) ,分别代表box坐标,宽高,分类得分,密度,其中密度 d d d 越大,代表该框所处的位置的物体分布越密集,越有可能是遮挡严重的地方;反之密度 d d d 越小,代表该框所处的位置的物体分布越稀疏,不太可能有遮挡。

论文以Traditionnal NMS和Soft-NMS的线性惩罚为基础,将每次迭代的NMS阈值更改如下:
[公式]
其中 t h r e s h thresh thresh 代表最小的NMS阈值。

优点:

  1. 可以与前面所述的各种NMS结合使用;
  2. 对遮挡案例更加友好。

缺点:

  1. 与Soft-NMS结合使用,效果可能倒退 (受低分检测框的影响);
  2. 顺序处理模式,运算效率低;
  3. 需要额外添加密度预测模块,造成计算开销;
  4. 评判标准是IoU,即只考虑两个框的重叠面积,这对描述box重叠关系或许不够全面。

2.6 +中心点距离

DIoU-NMS出现于Distance-IoU (Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression)一文,研究者认为若相邻框的中心点越靠近当前最大得分框 M M M 的中心点,则其更有可能是冗余框。也就是说,考虑IoU相同的情况,如下所示:
在这里插入图片描述
第一种相比于第三种越不太可能是冗余框。基于该观点,研究者使用所提出的DIoU替代IoU作为NMS的评判准则,公式如下:
在这里插入图片描述
DIoU的定义为:
在这里插入图片描述
而在实际操作中,研究者还引入了参数 β \beta β ,用于控制 d 2 c 2 \frac{d^2}{c^2} c2d2 的惩罚幅度。即
在这里插入图片描述
由公式可以看出:

  1. β → ∞ \beta \rightarrow \infty β时,DIoU退化为IoU,此时的DIoU-NMS与Traditional NMS效果相当;
  2. β → 0 \beta \rightarrow 0 β0 时,此时几乎所有中心点不与 M M M 重合的框都被保留了。
    在这里插入图片描述

研究者进一步比较了Traditional NMS和DIoU-NMS的性能,在YOLOv3和SSD上,选取NMS阈值为[0.43,0.48]。可以看到DIoU-NMS在每个阈值上都优于Traditional NMS,此外还值得一提的是,即便是性能最差的DIoU-NMS也比性能最好的Traditional NMS相当或更优,说明即便不仔细调整NMS阈值,DIoU-NMS也通常能够表现更好。
在这里插入图片描述
这里顺便一提,既然都比了[0.43, 0.48]的阈值,就让人比较好奇更宽的阈值范围会怎样?Traditional NMS会不会有反超DIoU-NMS的情况?当然我个人比较认同DIoU-NMS更优的范围会大一些,也就是NMS阈值不必精调也可放心使用DIoU-NMS。

优点:

  1. 从几何直观的角度,将中心点考虑进来有助于缓解遮挡案例;
  2. 可以与前述NMS变体结合使用;
  3. 保持NMS阈值不变的情况下,必然能够获得更高recall (因为保留的框增多了),至于precision就需要调 β \beta β 来平衡了;
  4. 个人认为+中心点距离的后处理可以与DIoU/CIoU损失结合使用,这两个损失一方面优化IoU,一方面指引中心点的学习,而中心点距离学得越好,应该对这种后处理思想的执行越有利。

缺点:

  1. 依然是顺序处理模式,运算效率低;
  2. DIoU的计算比IoU更复杂一些,这会降低运算效率;
  3. 在保持NMS阈值不变的情况下,使用DIoU-NMS会导致每次迭代剩余更多的框,这会增加迭代轮数,进一步降低运算效率。(经本人实测,DIoU-NMS是Traditional NMS 起码1.5倍耗时)

2.7 总结

  1. 加权平均法通常能够稳定获得精度与召回的提升。
  2. 定位优先法,方差加权平均法与自适应阈值法需要修改模型,不够灵活。
  3. 中心点距离法可作为额外惩罚因子与其他NMS变体结合。
  4. 得分惩罚法会改变box的得分,打破了模型校准机制。
  5. 运算效率的低下可能会限制它们的实时应用性。

3. NMS 提升效率

所参考的代码库列举如下:

  1. Faster RCNN pytorch (rbg大神) 的 CUDA NMS https://github.com/rbgirshick/py-faster-rcnn

  2. YOLACT团队提出的Fast NMS https://github.com/dbolya/yolact

  3. CIoU团队提出的Cluster NMS https://github.com/Zzh-tju/CIoU

  4. SOLOv2团队提出的Matrix NMS https://github.com/WXinlong/SOLO

  5. Torchvision封装的免编译CUDA NMS

NMS 计算效率主要瓶颈在于IoU计算及顺序迭代抑制。

3.1 IoU矩阵

假如一张图片中有 n n n 个检测框,由于顺序处理的原因,某一个框与其他框计算IoU,最少一次,最多有 n − 1 n-1 n1 次。再加上顺序迭代抑制,NMS算法在计算IoU方面,共需要计算IoU至少 n − 1 n-1 n1 次,最多计算次数:
( n − 1 ) + ( n − 2 ) + . . . + 1 = 1 2 n 2 − n 2 (n-1) +( n-2) + ...+1=\frac{1}{2}n^2-\frac{n}{2} (n1)+(n2)+...+1=21n22n

因此,要想加速NMS,首当其冲应该要将IoU的计算并行化。这个操作在使用IoU-based loss的时候就有,只需计算检测框集合 B = { B i } i = 1   t o   n B=\{B_i\}_{i=1\ to\ n} B={Bi}i=1 to n 与自身的 IoU 即可。检测框集合 B B B 事先会按照得分降序排列,也就是说 B 1 B_1 B1 是最高得分框, B n B_n Bn 是最低得分框。得到如下这个IoU矩阵:
在这里插入图片描述
得益于GPU的并行计算,可以一次性得到IoU的全部计算结果。这一步就已经极大地解决了IoU计算繁琐又耗时的问题。代码如下:

def box_iou(boxes1, boxes2):
    # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
    """
    Return intersection-over-union (Jaccard index) of boxes.
    Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
    Arguments:
        boxes1 (Tensor[N, 4])
        boxes2 (Tensor[M, 4])
    Returns:
        iou (Tensor[N, M]): the NxM matrix containing the pairwise
            IoU values for every element in boxes1 and boxes2
    """

    def box_area(box):
        # box = 4xn
        return (box[2] - box[0]) * (box[3] - box[1])

    area1 = box_area(boxes1.t())
    area2 = box_area(boxes2.t())

    lt = torch.max(boxes1[:, None, :2], boxes2[:, :2])  # [N,M,2]
    rb = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])  # [N,M,2]

    inter = (rb - lt).clamp(min=0).prod(2)  # [N,M]
    return inter / (area1[:, None] + area2 - inter)  # iou = inter / (area1 + area2 - inter)

到这里为止,以上列出的5种NMS都可以做到,从速度上来说CUDA NMS和torchvision NMS相对底层,编译后使用,速度稍快,但必然损失了一些灵活度。(关于CUDA NMS的教程,有兴趣的小伙伴可以参考 faster-rcnn源码阅读:nms的CUDA编程,非常详实。

在有了IoU矩阵之后,接下来就是应该要如何利用它来抑制冗余框。

3.2 Fast NMS

Fast NMS是 YOLACT: Real-time Instance Segmentation 一文的其中一个创新点。由于IoU的对称性,即 I o U ( B i , B j ) = I o U ( B j , B i ) IoU(B_i,B_j)=IoU(B_j,B_i) IoU(Bi,Bj)=IoU(Bj,Bi),看出 X X X 是一个对称矩阵。再加上一个框自己与自己算 IoU 也是无意义的,因此Fast NMS首先对 X X X 使用 pytorch 的triu函数进行上三角化,得到了一个对角线元素及下三角元素都为0的IoU矩阵 X X X
在这里插入图片描述
接着对 X X X 执行按列取最大值操作,得到一维张量 b = [ b 1 , b 2 , . . . , b n ] b=[b_1,b_2,...,b_n] b=[b1,b2,...,bn] b i b_i bi 代表了 X X X 的第 i i i 列上元素的最大值。最后使用NMS阈值,论文取0.5,对 b b b 二值化。 b b b 中元素小于NMS阈值的是保留的框,≥NMS阈值的是冗余框。例如
在这里插入图片描述
1代表保留,0代表抑制。

def fast_nms(self, boxes, scores, NMS_threshold:float=0.5):
    '''
    Arguments:
        boxes (Tensor[N, 4])
        scores (Tensor[N, 1])
    Returns:
        Fast NMS results
    '''
    scores, idx = scores.sort(1, descending=True)
    boxes = boxes[idx]   # 对框按得分降序排列
    iou = box_iou(boxes, boxes)  # IoU矩阵
    iou.triu_(diagonal=1)  # 上三角化
    keep = iou.max(dim=0)[0] < NMS_threshold  # 列最大值向量,二值化

    return boxes[keep], scores[keep]

在这里插入图片描述

优点:

  1. 速度比cython编译加速的Traditional NMS快。(https://github.com/Zzh-tju/CIoU)
  2. 可支持与其他提升精度的NMS方法结合。


缺点:

  1. Fast NMS会比Traditional NMS抑制更多的框,性能略微下降。
  2. 比CUDA NMS慢,约0.2ms。

这里有必要解释一下,为什么Fast NMS会抑制更多的框?

NMS的思想是:当一个框是冗余框,被抑制后,将不会对其他框产生任何影响。但在Fast NMS中,如果一个框 B i B_i Bi 的得分比 B j B_j Bj 高,且 B i B_i Bi 被抑制了,矩阵 X X X 的第 i i i 行正是 B i B_i Bi 与得分低于它的所有框的IoU,如果 x i j x_{ij} xij 这个元素 ≥NMS 阈值的话,那么在取列最大值这个操作时, b b b 的第 j j j 个元素必然 ≥NMS 阈值,于是很不幸地 B j B_j Bj 就被抑制掉了。

也就是在刚刚的例子中,由于第二个框 B 2 B_2 B2 被抑制,那么第二行第四列的0.72就不应该存在,可是Fast NMS允许冗余框去抑制其他框,导致了第四个框 B 4 B_4 B4 被错误地抑制了。
在这里插入图片描述
不过呢,YOLACT主要针对的是实例分割,mask是从box中裁剪出来的,Fast NMS对mask AP的下降比较轻微,约0.1~0.3的AP,但似乎对目标检测的box AP会下降更多。

3.3 Cluster NMS

3.3.1 Cluster NMS

Cluster NMS出自Enhancing Geometric Factors in Model Learning and Inference for Object Detection and Instance Segmentation一文。研究者主要旨在弥补Fast NMS的性能下降,期望也利用pytorch的GPU矩阵运算进行NMS,但同时又使得性能保持与Traditional NMS相同。
在这里插入图片描述
在这里插入图片描述
以上是论文的原话,意思是说,cluster是一个框集合,若某一个框A属于这个cluster,则必有cluster中的框与A的IoU≥NMS阈值,且A不会与除这个cluster之外的框有IoU≥NMS阈值。举个简单的例子:
在这里插入图片描述
上图中的黑红蓝橙四个框构成一个cluster,而绿色的两个框构成一个cluster,虽然两个cluster之间有相交,但都没有超过NMS阈值,于是这两个框集合不能合并成一个cluster。

其实就是一个迭代式的Fast NMS。前面的过程与Fast NMS一模一样,都是 B B B 按得分降序排列,计算IoU矩阵,上三角化。然后按列取最大值,经过NMS阈值二值化得到一维张量 b b b 。但不同于Fast NMS直接输出了 b b b ,Cluster NMS而是利用 b b b,将它展开成一个对角矩阵 E E E 。也就是这个对角矩阵 E E E 的对角线元素与 b b b 相同。然后用 E E E 去左乘IoU矩阵 X X X 。然后再按列取最大值,NMS阈值二值化得到一个新的一维张量 b b b,再展开成一个新的对角矩阵 E E E,继续左乘IoU矩阵 X X X 。直到出现某两次迭代后, b b b 保持不变了,那么谁该保留谁该抑制也就确定了。

这里借用一下作者在github的流程图。
在这里插入图片描述
在这里插入图片描述

这种利用矩阵左乘的方式,其实就是在省略上一次Fast NMS迭代中被抑制的框对其他框的影响。

因为一个对角矩阵左乘一个矩阵,就是在做行变换啊,对应行是1,乘完的结果这一行保持不变,如果对应行是0,乘完的结果就是把这一行全部变成0。而 b b b 中某个元素若是0,就代表这个位置是冗余框,于是乘完之后,这个冗余框在下一次的迭代中就不再对其他框产生影响。为什么这里要强调下一次迭代?是因为这个迭代过程 b b b 会不断发生变动!可能某个框这会儿是冗余框,到了下一次迭代又变成了保留框。但是但是到了最终 (其实未必是迭代满n次), b b b 就保持不变了!你说奇怪不?这里论文还给出了一个命题,说Cluster NMS的输出结果与Traditional NMS的结果一模一样

论文里还给出了对这个命题的证明,又是数学数学数学!大致一看是利用数学归纳法证的,感兴趣的可以去看论文。

def cluster_nms(self, boxes, scores, NMS_threshold:float=0.5):
    '''
    Arguments:
        boxes (Tensor[N, 4])
        scores (Tensor[N, 1])
    Returns:
        Fast NMS results
    '''
    scores, idx = scores.sort(1, descending=True)
    boxes = boxes[idx]   # 对框按得分降序排列
    iou = box_iou(boxes, boxes).triu_(diagonal=1)  # IoU矩阵,上三角化
    C = iou
    for i in range(200):    
        A=C
        maxA = A.max(dim=0)[0]   # 列最大值向量
        E = (maxA < NMS_threshold).float().unsqueeze(1).expand_as(A)   # 对角矩阵E的替代
        C = iou.mul(E)     # 按元素相乘
        if A.equal(C)==True:     # 终止条件
            break
    keep = maxA < NMS_threshold  # 列最大值向量,二值化

    return boxes[keep], scores[keep]

好了,至此Cluster NMS算是完成了对Fast NMS性能下降的弥补。直接看结果好了:
在这里插入图片描述
嗯,效果还是可以的,保持了AP与AR一样,运算效率比Fast NMS下降了一些,毕竟是迭代Fast NMS的操作,但也比Traditional NMS快多了。

3.3.2 Cluster NMS 变体

  1. 得分惩罚机制SPM(Score Penalty Mechanism)
    在这里插入图片描述
    也就是对刚刚Cluster NMS终止后的矩阵 C = E × X C=E\times X C=E×X ,先做一个Soft-NMS里gaussian版的变换,然后将每一列上的元素连乘作为惩罚得分的系数。不过论文提到,虽然是叫得分惩罚机制,但又与Soft-NMS不同,因为这里的SPM不改变框的次序,再加上矩阵 C C C 是一个上三角矩阵的缘故,所以每个框都只会被得分高于它的框惩罚,并且这里已经排除了高得分的冗余框对它的惩罚。而Soft-NMS每次迭代惩罚得分后,需要重新按得分降序排列,所以框的次序会不断变动。
    def SPM_cluster_nms(self, boxes, scores, NMS_threshold:float=0.5):
        '''
        Arguments:
            boxes (Tensor[N, 4])
            scores (Tensor[N, 1])
        Returns:
            Fast NMS results
        '''
        scores, idx = scores.sort(1, descending=True)
        boxes = boxes[idx]   # 对框按得分降序排列
        iou = box_iou(boxes, boxes).triu_(diagonal=1)  # IoU矩阵,上三角化
        C = iou
        for i in range(200):    
            A=C
            maxA = A.max(dim=0)[0]   # 列最大值向量
            E = (maxA < NMS_threshold).float().unsqueeze(1).expand_as(A)   # 对角矩阵E的替代
            C = iou.mul(E)     # 按元素相乘
            if A.equal(C)==True:     # 终止条件
                break
        scores = torch.prod(torch.exp(-C**2/0.2),0)*scores    #惩罚得分
        keep = scores > 0.01    #得分阈值筛选
        return boxes[keep], scores[keep]
    
  2. +中心点距离
    说到底DIoU就是该团队提出来的,那怎能不用上中心点距离呢?于是论文在两个方面添加了中心点距离。一个是IoU矩阵直接变成DIoU矩阵,由于DIoU也是满足尺度不变性的,所以完全没问题,相距很远的框之间的DIoU会变成负数,不过不影响过程。第二个是基于上面的SPM,公式如下:
    在这里插入图片描述
    这里多添了一个 D D D 也就是DIoU loss的惩罚项,两框中心点距离²/最小包围矩形对角线长度²。 β \beta β 用于控制中心点距离惩罚的幅度。然后又与1相比较,取最小值,以避免惩罚因子大于1。
    在这里插入图片描述
    加了这两个变体之后,AP与AR得到了明显的改善,速度也是略微下降。
  3. 加权平均法Weighted NMS
    也是利用矩阵 C C C,先与得分张量 S = [ s 1 , s 2 , . . . , s n ] S=[s_1,s_2,...,s_n] S=[s1,s2,...,sn] 按列相乘得到 C ′ C' C,随后 B = C ′ × B B=C'\times B B=C×B 就可以更新框的坐标了。
    在这里插入图片描述在YOLOv3上的效果有了不错的改进,虽然速度不及torchvision NMS,增加了5ms的运算成本,但结合Weighted NMS与DIoU,可以提升精度 (最后一行)。
    def Weighted_cluster_nms(self, boxes, scores, NMS_threshold:float=0.5):
        '''
        Arguments:
            boxes (Tensor[N, 4])
            scores (Tensor[N, 1])
        Returns:
            Fast NMS results
        '''
        scores, idx = scores.sort(1, descending=True)
        boxes = boxes[idx]   # 对框按得分降序排列
        iou = box_iou(boxes, boxes).triu_(diagonal=1)  # IoU矩阵,上三角化
        C = iou
        for i in range(200):    
            A=C
            maxA = A.max(dim=0)[0]   # 列最大值向量
            E = (maxA < NMS_threshold).float().unsqueeze(1).expand_as(A)   # 对角矩阵E的替代
            C = iou.mul(E)     # 按元素相乘
            if A.equal(C)==True:     # 终止条件
                break
        keep = maxA < NMS_threshold  # 列最大值向量,二值化
        weights = (C*(C>NMS_threshold).float() + torch.eye(n).cuda()) * (scores.reshape((1,n)))
        xx1 = boxes[:,0].expand(n,n)
        yy1 = boxes[:,1].expand(n,n)
        xx2 = boxes[:,2].expand(n,n)
        yy2 = boxes[:,3].expand(n,n)
    
        weightsum=weights.sum(dim=1)         # 坐标加权平均
        xx1 = (xx1*weights).sum(dim=1)/(weightsum)
        yy1 = (yy1*weights).sum(dim=1)/(weightsum)
        xx2 = (xx2*weights).sum(dim=1)/(weightsum)
        yy2 = (yy2*weights).sum(dim=1)/(weightsum)
        boxes = torch.stack([xx1, yy1, xx2, yy2], 1)
        return boxes[keep], scores[keep]
    

3.3.3 Cluster NMS理论上的好处

Cluster NMS的迭代次数通常少于Traditional NMS的迭代次数。

这一优点,从理论上给了它使用CUDA编程更进一步加速的可能。大致意思是说,平常我们在做NMS时,迭代都是顺序处理每一个cluster的。在Traditional NMS中,虽然不同的cluster之间本应毫无关系,但计算IoU重复计算了属于不同cluster之间的框,顺序迭代抑制的迭代次数也仍然保持不变。

但在Cluster NMS中,使用这种行变换的方式,就可以将本应在所有cluster上迭代,化简为只需在一个拥有框数量最多的cluster上迭代就够了(妙啊)。不同的cluster间享有相同的矩阵操作,且它们互不影响。这导致迭代次数最多不超过一张图中最大cluster所拥有的框的个数。也就是下面这种情形
在这里插入图片描述
上图中共有10个检测框,分成了3个cluster,它的IoU矩阵(被NMS阈值二值化了)在右边。Cluster NMS做迭代的次数最多不超过4次,因为上图中框数量最多的那个cluster(红色)一共只有4个框。而实际上这张图使用Cluster NMS,只需迭代2轮便结束了。

因此这带来的好处是时间复杂度的下降。特别是对于一张图中有很多个cluster时,效果更为显著。比如这种情形,密密麻麻的狗狗
在这里插入图片描述
物体数量越多,到时候的检测框也就越多,形成的cluster必然也会增多,于是乎Cluster NMS这种对所有cluster并行处理的算法必然迭代次数非常少,不会随着物体的增多而过分地增加迭代轮数。最极端的情形是 n n n 个物体形成 n n n 个cluster,Traditional NMS需要迭代 ≥ n \ge n n 次,而Cluster NMS不与cluster数量有关,只与需要迭代次数最多的那一个cluster有关。这也是为什么文中说,有可能可以被进一步使用工程技巧加速的原因,比如底层CUDA实现。

3.3.4 Cluster NMS 小结

优点:

  1. Cluster NMS可以保持与Traditional NMS一样的精度,速度快于Cython编译的Traditional NMS。
  2. 可以结合一些提升精度的方法,比如得分惩罚法,加权平均法,+中心点距离法。
  3. 并行处理cluster,给了算法迭代次数一个上界 (最大cluster的框数量)。

缺点:

  1. 速度上,各种变体Cluster NMS < Cluster NMS < Fast NMS < CUDA NMS

3.4 Matrix NMS

Matrix NMS出自 SOLOv2: Dynamic, Faster and Stronger,也是利用排序过后的上三角化的IoU矩阵。不同在于它针对的是mask IoU。我们知道mask IoU的计算会比box IoU计算更耗时一些,特别是在Traditional NMS中,会加剧运算开销。因此Matrix NMS将mask IoU并行化是它最大的一个贡献。然后论文同样采取了得分惩罚机制来抑制冗余mask。具体代码如下:
在这里插入图片描述

不过笔者在实现这个代码时遇到了问题,惩罚因子decay通常会≥1,在考虑一个mask B i B_i Bi 的惩罚因子 decay 时,与两个东西有关,一个是所有得分高于它的mask与 B j B_j Bj的IoU,该IoU越大,惩罚应该更多(也就是上图中decay的分子);另一个是所有得分高于 B j B_j Bj 的mask B i B_i Bi 被抑制的概率(也就是上图中decay的分母),这是由 B i B_i Bi 所处的这一列的最大IoU决定,越大则 B i B_i Bi 越可能是冗余mask,就应该降低对 B j B_j Bj的惩罚力度。

随后decay取列最小值。但依据如上代码,decay通常会≥1,这会增大mask的得分,而代码又以0.05作为score筛选的阈值,说明应该是减少得分的思想没有错,这个矛盾导致得出的结果奇差无比。如有任何人知道Matrix NMS出现这种问题的原因,还请告知我问题在哪。

这里直接贴出论文结果,从这里似乎也看出了相比于Soft-NMS每次迭代重排序的做法,直接得分惩罚也是可取的。
在这里插入图片描述

优点:

  1. 实现了mask IoU的并行计算,对于box-free的密集预测实例分割模型很有使用价值。
  2. 与Fast NMS一样,只需一次迭代。


缺点:

  1. 与Fast NMS一样,直接从上三角IoU矩阵出发,可能造成过多抑制。

3.5 总结

  1. CUDA NMS与Torchvision NMS稍快于以上三种基于pytorch实现的NMS,但比较死板,改动不易。
  2. Cluster NMS基本可以取代Fast NMS,且与其他提升精度的方法的有机结合,使它又快又好。(目前作者团队的代码库只提供了得分惩罚法,+中心点距离,加权平均法三种。)
  3. Matrix NMS也可以参考Cluster NMS的方式先得到一个与Traditional NMS一样的结果后,再进行后续处理。
  4. 三种基于pytorch的NMS方法,只依赖于矩阵操作,无需编译。
  5. 将2D检测的NMS加速方法推广至3D检测,应该也是很有价值的。

参考链接:

  1. NMS算法详解(附Pytorch实现代码)
  2. 非极大值抑制算法原理
  3. 非极大值抑制(Non-Maximum Suppression,NMS)
  4. 一文打尽目标检测NMS——精度提升篇
  5. Soft-NMS – Improving Object Detection With One Line of Code
  6. IoU-Net:Acquisition of Localization Confidence for Accurate Object Detection
  7. Inception Single Shot MultiBox Detector for object detection ICME Workshop 2017
  8. Bounding Box Regression with Uncertainty for Accurate Object Detection
  9. Adaptive NMS: Refining Pedestrian Detection in a Crowd
  10. Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression AAAI 2020
  11. 一文打尽目标检测NMS——效率提升篇
  12. YOLACT: Real-time Instance Segmentation
  13. SOLOv2: Dynamic, Faster and Stronger
  14. Enhancing Geometric Factors in Model Learning and Inference for Object Detection and Instance Segmentation
  15. https://github.com/Zzh-tju/CIoU
  • 11
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值