Fast NMS和Matrix NMS解读

Fast NMS

Fast-NMS是YOLACT这篇文章提出的。传统NMS的具体过程是:对于每个类别的所有检测框,按得分排序,保留得分最高的检测框,然后删去得分低于该框且与该框IoU大于设定阈值的其它所有检测框,然后对剩下的检测框再重复执行这一过程。可以看到传统NMS只能按顺序执行,没法并行。

为了可以并行,作者提出了Fast-NMS,在传统NMS中,如果一个框被删除了,它就不再参与后续计算了即对其它框的保留和删除不起作用了。而Fast-NMS允许被删除的框继续对其它框起到抑制作用,这是一种放松限制的过程,因此可能会导致更多的框被删除,但因为可以通过GPU加速的矩阵操作来实现,速度更快了。

我们用一个具体例子来讲解一下具体实现过程:

假设某一类别一共有5个检测框,先按score进行排序,然后两两之间计算IoU,就得到了一个5x5的IoU矩阵。因为AB的IoU和BA的IoU是相同的,并且AA的IoU没有意义,我们把矩阵下三角和对角线上的值都置为0,就得到了如下矩阵

\begin{align*}
\begin{pmatrix}
  0 & 0.6 & 0.1 & 0.3 & 0.8 \\
  0 & 0 & 0.2 & 0.72 & 0.1 \\
  0 & 0 & 0 & 0.45 & 0.12 \\
  0 & 0 & 0 & 0 & 0.28 \\
  0 & 0 & 0 & 0 & 0
\end{pmatrix}
\end{align*}

然后我们按列取最大值,得到 \([\ 0,\ 0.6,\ 0.2, \ 0.72,\ 0.8\ ]\)。提前设定的NMS过滤的IoU阈值假设为0.5,小于该值的保留,大于该值的删去。则得到 \([\ 1,\ 0,\ 1, \ 0,\ 0\ ]\),即保留第一个和第三个检测框,得到最终的结果。

这里按个人理解解释一下这个过程。首先检测框按得分从高到低的顺序进行排序了,所以上述IoU矩阵里从左到右和从上到下对应的检测框的得分是逐渐减小的。首先左上角(0, 0)位置的IoU是0所以该框肯定是保留的,因为这个框是得分最高的检测框,所以保留是合理的。

第二个框和第一个框的IoU为0.6大于阈值0,删去。

第三个框和第一个框的IoU为0.1小于阈值保留。按照传统NMS,第二个框已经删去了,就不参与后续其它框的IoU计算了。而这里Fast-NMS仍然计算了第二个框和第三个框的IoU为0.2,这就是两者有区别的地方。不过这里0.2仍然小于阈值所以还是保留,并没有改变结果。假设这里第二个框和第三个框的IoU大于0.5了就要删去了,而传统NMS则保留这个框,因此Fast-NMS可能会比传统的NMS删除更多的框。

后续的步骤和前面一样,剩下的每个框不只计算和保留框的IoU,还计算和删去框的IoU,只要大于设定阈值就删去。

这里的思想是,第一个得分高的框保留,第二个框和第一个框的IoU大于设定阈值删去,如果第三个框和第二个框的IoU大于设定阈值,而第二个框和第一个框的IoU也大于阈值,则很大概率第三个框和第一个框的IoU大于设定阈值,所以删去。可以看出,这是一种放宽限制的NMS方法,因此可能导致更多的框被删除,造成漏检,当然优点就是可以通过矩阵计算一步到位,速度变快了。

Matrix NMS 

Matrix-NMS是SOLOv2这篇文章提出的。Matrix-NMS受到了Soft-NMS和Fast-NMS的启发,Soft-NMS用一个iou的单调递减函数 \(f(\mathrm{iou})\) 作为得分的衰减因子,这样较高IoU的检测将会被一个较小的得分阈值过滤掉。但Soft-NMS和传统NMS一样,只能迭代地按顺序进行,速度较慢。而Fast-NMS则可以通过并行计算加快推理速度。Matrix-NMS则结合了两者的优点。

Matrix-NMS是针对实例分割任务提出的,它从另一个角度来看待非极大值抑制的过程,它考虑了一个predicted mask \(m_j\) 是如何被抑制的。对于 \(m_j\),它的decay factor受两个因素的影响:(a) 每个预测 \(m_i\) 对 \(m_j\) 的惩罚(\(s_i>s_j\)),\(s_i\) 和 \(s_j\) 是置信度得分。(b) \(m_i\) 被抑制的概率。对于(a),每个预测 \(m_i\) 对 \(m_j\) 的惩罚可以很容易地通过计算 \(f(\mathrm{iou}_{i.j})\) 得到。对于(b),\(m_i\) 被抑制的概率没法直接计算得到。但是该概率通常和IoU呈正相关,所以作者直接用与 \(m_i\) 重叠最大的预测作为近似的概率

最终decay factor如下

然后得分按 \(s_j=s_j\cdot decay_j\) 进行更新,考虑考虑了两个常见的递减函数,\(\mathrm{linear}\ f(\mathrm{iou}_{i,j})=1-\mathrm{iou}_{i,j}\) 和 \(\mathrm{Gaussian}\ f(\mathrm{iou}_{i,j})=\mathrm{exp}(-\frac{\mathrm{iou^2}_{i,j}}{\sigma})\)。

整个计算过程可以一次完成,不用循环迭代。首先对预测按得分从高到低顺序排序,取最高的 \(N\) 个预测,然后计算两两之间的IoU得到一个 \(N\times N\) 的IoU矩阵。对于binary mask,IoU矩阵可以通过矩阵操作高效地实现。然后按列取最大值column-wise max得到每个预测和得分更高预测之间的最大IoU。然后计算所有较高得分预测的decay factor,通过按列取最小值column-wise min得到对每个预测影响最大的值作为decay factor。然后与原始score相乘进行更新。最后按设定的得分阈值取top-k个预测作为最终结果 。

个人理解:decay值是与原始score相乘的,decay值大表示更新后的score值大,对检测的抑制效果是变小的。而 \(f\) 又是递减函数,因此decay值大表示分子的 \(\mathrm{iou}_{i,j}\) 应该小,这和传统的NMS逻辑是一致的,即与前面得分更高的保留下来的检测的iou越小,越不该删去当前检测。

再看分母,decay越大表示分母的 \(\mathrm{iou}_{\cdot,i}\) 应该大,注意式(4)中的分母还有一个min即式(3),和外面的min不是同一个。分母的含义是比当前检测得分更高的检测被抑制的概率或者是程度,考虑一种极端情况就是是否已经被删去。

在传统NMS中,当前得分最高的框保留下来,然后计算剩余检测框和当前保留框的IoU,大于阈值删去,即只考虑与保留框的IoU,或者说只有保留的框对剩余其它框的保留和删去起作用;而在Fast-NMS中,无论当前框是否保留都对后续框起作用,即考虑当前框是否删除是计算与前面得分更高的所有框的IoU,不管它是不是已经被删去的。

可以看到这是两种极端情况,一种是删去的框就完全不考虑了,另一种是删去的框也考虑和保留的框作用相同。而Matrix-NMS是取了个折中,即如果一个框保留的概率大,它对后续框的删除起的作用就大,反之保留的概率小即删去的概率大,对后续框的作用就小。而这里保留的概率就可以用IoU近似,IoU越大保留的概率就越小。

因此decay值越大表示对抑制的效果越小,即分母 \(\mathrm{iou}_{\cdot,i}\) 越大,即框 \(i\) 保留的概率越小,即对框 \(j\) 的抑制作用越小。

Matrix-NMS的伪代码如下所示

接下来我们以mmdetection中实现的matrix nms为例讲解下代码实现,完整代码如下

# Copyright (c) OpenMMLab. All rights reserved.
import torch


def mask_matrix_nms(masks,
                    labels,
                    scores,
                    filter_thr=-1,  # 0.05
                    nms_pre=-1,  # 500
                    max_num=-1,  # 100
                    kernel='gaussian',
                    sigma=2.0,
                    mask_area=None):
    """Matrix NMS for multi-class masks.

    Args:
        masks (Tensor): Has shape (num_instances, h, w)
        labels (Tensor): Labels of corresponding masks,
            has shape (num_instances,).
        scores (Tensor): Mask scores of corresponding masks,
            has shape (num_instances).
        filter_thr (float): Score threshold to filter the masks
            after matrix nms. Default: -1, which means do not
            use filter_thr.
        nms_pre (int): The max number of instances to do the matrix nms.
            Default: -1, which means do not use nms_pre.
        max_num (int, optional): If there are more than max_num masks after
            matrix, only top max_num will be kept. Default: -1, which means
            do not use max_num.
        kernel (str): 'linear' or 'gaussian'.
        sigma (float): std in gaussian method.
        mask_area (Tensor): The sum of seg_masks.

    Returns:
        tuple(Tensor): Processed mask results.

            - scores (Tensor): Updated scores, has shape (n,).
            - labels (Tensor): Remained labels, has shape (n,).
            - masks (Tensor): Remained masks, has shape (n, w, h).
            - keep_inds (Tensor): The indices number of
                the remaining mask in the input mask, has shape (n,).
    """
    assert len(labels) == len(masks) == len(scores)
    if len(labels) == 0:
        return scores.new_zeros(0), labels.new_zeros(0), masks.new_zeros(
            0, *masks.shape[-2:]), labels.new_zeros(0)
    if mask_area is None:
        mask_area = masks.sum((1, 2)).float()
    else:
        assert len(masks) == len(mask_area)

    # sort and keep top nms_pre
    scores, sort_inds = torch.sort(scores, descending=True)

    keep_inds = sort_inds
    if 0 < nms_pre < len(sort_inds):
        sort_inds = sort_inds[:nms_pre]
        keep_inds = keep_inds[:nms_pre]
        scores = scores[:nms_pre]
    masks = masks[sort_inds]
    mask_area = mask_area[sort_inds]
    labels = labels[sort_inds]

    num_masks = len(labels)
    flatten_masks = masks.reshape(num_masks, -1).float()
    # inter.
    inter_matrix = torch.mm(flatten_masks, flatten_masks.transpose(1, 0))
    expanded_mask_area = mask_area.expand(num_masks, num_masks)  # (9,)->(9,9)
    # Upper triangle iou matrix.
    iou_matrix = (inter_matrix /
                  (expanded_mask_area + expanded_mask_area.transpose(1, 0) -
                   inter_matrix)).triu(diagonal=1)  # (9,9)

    # label_specific matrix.
    expanded_labels = labels.expand(num_masks, num_masks)  # (9,)->(9,9)
    # Upper triangle label matrix.
    label_matrix = (expanded_labels == expanded_labels.transpose(
        1, 0)).triu(diagonal=1)  # (9,9)

    # IoU compensation
    compensate_iou, _ = (iou_matrix * label_matrix).max(0)
    compensate_iou = compensate_iou.expand(num_masks,
                                           num_masks).transpose(1, 0)

    # IoU decay
    decay_iou = iou_matrix * label_matrix

    # Calculate the decay_coefficient
    if kernel == 'gaussian':
        decay_matrix = torch.exp(-1 * sigma * (decay_iou**2))
        compensate_matrix = torch.exp(-1 * sigma * (compensate_iou**2))
        decay_coefficient, _ = (decay_matrix / compensate_matrix).min(0)
    elif kernel == 'linear':
        decay_matrix = (1 - decay_iou) / (1 - compensate_iou)
        decay_coefficient, _ = decay_matrix.min(0)
    else:
        raise NotImplementedError(
            f'{kernel} kernel is not supported in matrix nms!')
    # update the score.
    scores = scores * decay_coefficient

    if filter_thr > 0:
        keep = scores >= filter_thr
        keep_inds = keep_inds[keep]
        if not keep.any():
            return scores.new_zeros(0), labels.new_zeros(0), masks.new_zeros(
                0, *masks.shape[-2:]), labels.new_zeros(0)
        masks = masks[keep]
        scores = scores[keep]
        labels = labels[keep]

    # sort and keep top max_num
    scores, sort_inds = torch.sort(scores, descending=True)
    keep_inds = keep_inds[sort_inds]
    if 0 < max_num < len(sort_inds):
        sort_inds = sort_inds[:max_num]
        keep_inds = keep_inds[:max_num]
        scores = scores[:max_num]
    masks = masks[sort_inds]
    labels = labels[sort_inds]

    return scores, labels, masks, keep_inds

这里是推理的一张图片,可视化结果如下,可以看到实际包含两个目标,只有一个类别 

首先第53行排序后的得分如下,可以看到模型一共预测了9个实例,并且得分都很高

tensor([0.9965, 0.9965, 0.9961, 0.9959, 0.9953, 0.9951, 0.9950, 0.8834, 0.8308], 
       device='cuda:0')

然后65-72行计算mask实例的IoU,因为这里mask前景为1背景为0,直接转置后相乘就可以得到相交部分的面积,只有两者都为1结果才为1。得到的iou_matrix的shape为(9, 9),因为一共9个实例,表示两两之间的IoU。代码中和label相乘因为NMS是按类别每一类单独进行的,因为这里只有一类就不用考虑。

77行取.triu和上面的Fast-NMS一样,即将IoU矩阵的下三角和对角线置0,因为IoU的计算和两个目标的顺序无关且每个目标和自己的IoU没有意义。iou_matrix打印结果如下

tensor([[0.0000, 1.0000, 0.0000, 0.9994, 0.0000, 0.0000, 0.9994, 0.9994, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.9994, 0.0000, 0.0000, 0.9994, 0.9994, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.9993, 1.0000, 0.0000, 0.0000, 1.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 1.0000, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.9993, 0.0000, 0.0000, 0.9993],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 1.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]],
       device='cuda:0')

81行按列取最大值,得到每个预测和得分更高预测之间的IoU的最大值。结果如下,可以看到如果按照传统NMS会保留第一个和第三个预测,其它删去。

tensor([0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
       device='cuda:0')

82行拓展维度再转置,结果如下。这里转置是因为本来每一行表示每个预测和其它得分更高预测的最大IoU值,式(4)中计算对 \(j\) 的抑制因子,分子表示计算 \(j\) 和每个得分更高的预测 \(i\) 之间的IoU,对应代码中的iou_matrix,而分母表示对于每个得分更高的预测 \(i\) 再计算比 \(i\) 得分更高的预测之间的IoU的最大值,所以需要转置一下也能对应起来。

tensor([[0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 1.0000, 0.0000, 0.9994, 0.9993, 1.0000, 1.0000, 1.0000, 1.0000]],
       device='cuda:0')
tensor([[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.9994, 0.9994, 0.9994, 0.9994, 0.9994, 0.9994, 0.9994, 0.9994, 0.9994],
        [0.9993, 0.9993, 0.9993, 0.9993, 0.9993, 0.9993, 0.9993, 0.9993, 0.9993],
        [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
        [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
        [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
        [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000]],
       device='cuda:0')

最后按gaussian或linear计算式(4),92行或95行按列取个min就得到了最终的decay factor,然后100行与原始score相乘得到更新后的score,最后103行按阈值得到保留下来的预测index。更新后的score如下,可以看到第1个和第3个预测的得分完全没有被抑制,而其它预测的得分都下降了很多。

tensor([0.9965, 0.1349, 0.9961, 0.1351, 0.1350, 0.1347, 0.1350, 0.1199, 0.1124],
       device='cuda:0')

但是代码中的阈值filter_thr非常小为0.05,因此所有的预测还是都保留下来的。而上面可视化的结果只有两个实例是因为外面还有一个阈值pred_score_thr=0.3又进行了一遍过滤,得分小于0.3的预测都删去了。 

参考

https://zhuanlan.zhihu.com/p/157900024

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

00000cj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值