深入浅出之非极大值抑制(NMS)

一. 提出背景、作者及时间

提出背景
非极大值抑制(Non-Maximum Suppression, NMS)首次在论文“Efficient non-maximum suppression”中被提出,主要是为了解决在目标检测任务中,同一目标被多个检测器多次检测到,导致出现多个相交或重叠的候选框的问题。这些冗余的候选框不仅增加了计算负担,还降低了目标检测的精度。

Non-Maximum Suppression的翻译是非“极大值”抑制,而不是非“最大值”抑制。这就说明了这个算法的用处:找到局部极大值,并筛除(抑制)邻域内其余的值。 

作者及时间
NMS的具体作者和首次提出的确切时间可能因不同文献和资料而异,但普遍认为它是在目标检测领域广泛应用后逐渐形成的一种后处理技术,并非由某一具体作者单独提出。不过,随着深度学习在目标检测中的广泛应用,NMS成为了该领域不可或缺的一部分。

二、. 主要贡献

NMS的主要贡献在于通过筛选局部极大值来去除冗余和重叠的候选框,从而提高了目标检测的精度和效率。它确保了每个目标只被一个最优的边界框表示,避免了因多个候选框引起的混淆和误判。

三. 算法原理

NMS的实现过程通常包括以下几个步骤:

  1. 输入数据:包括一组候选边界框及其对应的置信度得分。

  2. 过滤低分边界框:根据设定的置信度阈值(confidence_threshold),过滤掉得分低于该阈值的边界框。

  3. 排序:对剩余的边界框按照置信度从高到低进行排序。

  4. 迭代筛选

    • 从排序后的列表中选择置信度最高的边界框作为基准框。
    • 计算基准框与其他边界框的交并比(Intersection over Union,IoU)。
    • 如果某个边界框与基准框的IoU大于设定的IoU阈值(iou_threshold),则认为该边界框与基准框重叠过多,将其从列表中删除。
    • 重复上述过程,直到处理完所有边界框。
  5. 输出结果:保留下来的边界框即为NMS筛选后的结果。

举例说明:

RCNN会从一张图片中找出n个可能是物体的矩形框,然后为每个矩形框为做类别分类概率:


 

就像上面的图片一样,定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的方法是:先假设有6个矩形框,根据分类器的类别分类概率做排序,假设从小到大属于车辆的概率 分别为A、B、C、D、E、F。

(1)从最大概率矩形框F开始,分别判断A~E与F的重叠度IOU是否大于某个设定的阈值;

(2)假设B、D与F的重叠度超过阈值,那么就扔掉B、D;并标记第一个矩形框F,是我们保留下来的。

(3)从剩下的矩形框A、C、E中,选择概率最大的E,然后判断E与A、C的重叠度,重叠度大于一定的阈值,那么就扔掉;并标记E是我们保留下来的第二个矩形框。

就这样一直重复,找到所有被保留下来的矩形框。

非极大值抑制(NMS)顾名思义就是抑制不是极大值的元素,搜索局部的极大值。这个局部代表的是一个邻域,邻域有两个参数可变,一是邻域的维数,二是邻域的大小。这里不讨论通用的NMS算法,而是用于在目标检测中用于提取分数最高的窗口的。例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。

四. 特点

  • 高效性:NMS能够快速地去除大量冗余和重叠的候选框,提高目标检测的效率。
  • 准确性:通过保留置信度最高且重叠度较低的候选框,NMS能够提高目标检测的准确性。
  • 灵活性:NMS的阈值可以根据具体任务进行调整,以适应不同的检测需求。

五. 优缺点

优点

  • 能够有效去除冗余和重叠的候选框,提高目标检测的精度和效率。
  • 算法实现简单,易于集成到现有的目标检测框架中。

缺点

  • 对于密集场景中的目标检测,NMS可能会误删一些有效的候选框,导致漏检。
  • NMS的阈值设置对检测结果有较大影响,需要根据具体任务进行精细调整。

六. 网络架构

NMS本身并不属于网络架构的一部分,而是一种后处理技术,可以应用于各种基于深度学习的目标检测模型中,如R-CNN系列、YOLO系列、SSD等。这些模型在输出候选框后,通常会使用NMS来去除冗余和重叠的候选框,从而得到最终的检测结果。

七、NMS程序实现 

1. 交并比

交并比(Intersection over Union)是目标检测NMS的依据,因此首先要搞懂交并比及其实现。

衡量边界框位置,常用交并比指标,交并比(Injection Over Union,IOU)发展于集合论的雅卡尔指数(Jaccard Index)[3],被用于计算真实边界框Bgt(数据集的标注)以及预测边界框Bp(模型预测结果)的重叠程度。

具体来说,它是两边界框相交部分面积与相并部分面积之比,如下所示:

Python(numpy)代码实现

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

2. NMS的Python实现

从R-CNN开始,到fast R-CNN,faster R-CNN……都不难看到NMS的身影,且因为实现功能类似,基本的程序都是定型的,这里就分析Faster RCNN的NMS实现:

Python(numpy)代码实现

注意,这里的NMS是单类别的!多类别则只需要在外加一个for循环遍历每个种类即可

def py_cpu_nms(dets, thresh): 
"""Pure Python NMS baseline.""" 
    #dets某个类的框,x1、y1、x2、y2、以及置信度score
    #eg:dets为[[x1,y1,x2,y2,score],[x1,y1,y2,score]……]]
    # thresh是IoU的阈值     
    x1 = dets[:, 0] 
    y1 = dets[:, 1]
    x2 = dets[:, 2] 
    y2 = dets[:, 3] 
    scores = dets[:, 4] 
    #每一个检测框的面积 
    areas = (x2 - x1 + 1) * (y2 - y1 + 1) 
    #按照score置信度降序排序 
    order = scores.argsort()[::-1] 
    keep = [] #保留的结果框集合 
    while order.size > 0: 
        i = order[0] 
        keep.append(i) #保留该类剩余box中得分最高的一个 
        #得到相交区域,左上及右下 
        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:]]) 
        #计算相交的面积,不重叠时面积为0 
        w = np.maximum(0.0, xx2 - xx1 + 1) 
       h = np.maximum(0.0, yy2 - yy1 + 1) 
       inter = w * h 
        #计算IoU:重叠面积 /(面积1+面积2-重叠面积) 
        ovr = inter / (areas[i] + areas[order[1:]] - inter) 
       #保留IoU小于阈值的box 
        inds = np.where(ovr <= thresh)[0] 
        order = order[inds + 1] #因为ovr数组的长度比order数组少一个,所以这里要将所有下标后移一位 
    return keep

Faster R-CNN的MATLAB实现与python版实现一致,代码在这里:nms.m.另外,nms_multiclass.m是多类别nms,加了一层for循环对每类进行nms而已.

3. NMS的Pytorch实现

在Pytorch中,数据类型从numpy的数组变成了pytorch的tensor,因此具体的实现需要改变写法,但核心思路是不变的。

这里的实现参照了知乎大佬TeddyZhang的专栏

IoU计算的Pytorch源码为:(注意矩阵维度的变化)

# IOU计算
    # 假设box1维度为[N,4]   box2维度为[M,4]
 def iou(self, box1, box2):
        N = box1.size(0)
        M = box2.size(0)
 
        lt = torch.max(  # 左上角的点
            box1[:, :2].unsqueeze(1).expand(N, M, 2),   # [N,2]->[N,1,2]->[N,M,2]
            box2[:, :2].unsqueeze(0).expand(N, M, 2),   # [M,2]->[1,M,2]->[N,M,2]
 )
 
        rb = torch.min(
            box1[:, 2:].unsqueeze(1).expand(N, M, 2),
            box2[:, 2:].unsqueeze(0).expand(N, M, 2),
 )
 
        wh = rb - lt  # [N,M,2]
        wh[wh < 0] = 0   # 两个box没有重叠区域
        inter = wh[:,:,0] * wh[:,:,1]   # [N,M]
 
        area1 = (box1[:,2]-box1[:,0]) * (box1[:,3]-box1[:,1])  # (N,)
        area2 = (box2[:,2]-box2[:,0]) * (box2[:,3]-box2[:,1])  # (M,)
        area1 = area1.unsqueeze(1).expand(N,M)  # (N,M)
        area2 = area2.unsqueeze(0).expand(N,M)  # (N,M)
 
        iou = inter / (area1+area2-inter)
 return iou

其中:

  • torch.unsqueeze(1) 表示增加一个维度,增加位置为维度1
  • torch.squeeze(1) 表示减少一个维度
# 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

其中:

  • torch.numel() 表示一个张量总元素的个数
  • torch.clamp(min, max) 设置上下限
  • tensor.item() 把tensor元素取出作为numpy数字

4. C++实现NMS

C++代码来自这个博客,真希望我也能有大佬们的码力233……毕竟搞工程早晚会掣肘于Python的

NMS和soft-nms算法 - outthinker - 博客园​www.cnblogs.com/zf-blog/p/8532228.html​编辑

程序整体思路

先将box中的数据分别存入x1,y1,x2,y2,s中,分别为坐标和置信度,算出每个框的面积,存入area,基于置信度s,从小到达进行排序,做一个while循环,取出置信度最高的,即排序后的最后一个,然后将该框进行保留,存入pick中,然后和其他所有的框进行比对,大于规定阈值就将别的框去掉,并将该置信度最高的框和所有比对过程,大于阈值的框存入suppress,for循环后,将I中满足suppress条件的置为空。直到I为空退出while。

static void sort(int n, const float* x, int* indices) 
{ 
// 排序函数(降序排序),排序后进行交换的是indices中的数据  
// n:排序总数// x:带排序数// indices:初始为0~n-1数目   
 
    int i, j; 
 for (i = 0; i < n; i++) 
 for (j = i + 1; j < n; j++) 
 { 
 if (x[indices[j]] > x[indices[i]]) 
 { 
                //float x_tmp = x[i];  
                int index_tmp = indices[i]; 
                //x[i] = x[j];  
                indices[i] = indices[j]; 
                //x[j] = x_tmp;  
                indices[j] = index_tmp; 
 } 
 } 
}

 int nonMaximumSuppression(int numBoxes, const CvPoint *points, 
                          const CvPoint *oppositePoints, const float *score, 
                          float overlapThreshold, 
                          int *numBoxesOut, CvPoint **pointsOut, 
                          CvPoint **oppositePointsOut, float **scoreOut) 
{ 
 
// numBoxes:窗口数目// points:窗口左上角坐标点// oppositePoints:窗口右下角坐标点  
// score:窗口得分// overlapThreshold:重叠阈值控制// numBoxesOut:输出窗口数目  
// pointsOut:输出窗口左上角坐标点// oppositePoints:输出窗口右下角坐标点  
// scoreOut:输出窗口得分  
    int i, j, index; 
    float* box_area = (float*)malloc(numBoxes * sizeof(float));    // 定义窗口面积变量并分配空间   
    int* indices = (int*)malloc(numBoxes * sizeof(int));          // 定义窗口索引并分配空间   
    int* is_suppressed = (int*)malloc(numBoxes * sizeof(int));    // 定义是否抑制表标志并分配空间   
    // 初始化indices、is_supperssed、box_area信息   
 for (i = 0; i < numBoxes; i++) 
 { 
        indices[i] = i; 
        is_suppressed[i] = 0; 
        box_area[i] = (float)( (oppositePoints[i].x - points[i].x + 1) * 
 (oppositePoints[i].y - points[i].y + 1)); 
 } 
    // 对输入窗口按照分数比值进行排序,排序后的编号放在indices中   
    sort(numBoxes, score, indices); 
 for (i = 0; i < numBoxes; i++)                // 循环所有窗口   
 { 
 if (!is_suppressed[indices[i]])           // 判断窗口是否被抑制   
 { 
 for (j = i + 1; j < numBoxes; j++)    // 循环当前窗口之后的窗口   
 { 
 if (!is_suppressed[indices[j]])   // 判断窗口是否被抑制   
 { 
                    int x1max = max(points[indices[i]].x, points[indices[j]].x);                     // 求两个窗口左上角x坐标最大值   
                    int x2min = min(oppositePoints[indices[i]].x, oppositePoints[indices[j]].x);     // 求两个窗口右下角x坐标最小值   
                    int y1max = max(points[indices[i]].y, points[indices[j]].y);                     // 求两个窗口左上角y坐标最大值   
                    int y2min = min(oppositePoints[indices[i]].y, oppositePoints[indices[j]].y);     // 求两个窗口右下角y坐标最小值   
                    int overlapWidth = x2min - x1max + 1;            // 计算两矩形重叠的宽度   
                    int overlapHeight = y2min - y1max + 1;           // 计算两矩形重叠的高度   
 if (overlapWidth > 0 && overlapHeight > 0) 
 { 
                        float overlapPart = (overlapWidth * overlapHeight) / box_area[indices[j]];    // 计算重叠的比率   
 if (overlapPart > overlapThreshold)          // 判断重叠比率是否超过重叠阈值   
 { 
                            is_suppressed[indices[j]] = 1;           // 将窗口j标记为抑制   
 } 
 } 
 } 
 } 
 } 
 } 
 
 *numBoxesOut = 0;    // 初始化输出窗口数目0   
 for (i = 0; i < numBoxes; i++) 
 { 
 if (!is_suppressed[i]) (*numBoxesOut)++;    // 统计输出窗口数目   
 } 
 
 *pointsOut = (CvPoint *)malloc((*numBoxesOut) * sizeof(CvPoint));           // 分配输出窗口左上角坐标空间   
 *oppositePointsOut = (CvPoint *)malloc((*numBoxesOut) * sizeof(CvPoint));   // 分配输出窗口右下角坐标空间   
 *scoreOut = (float *)malloc((*numBoxesOut) * sizeof(float));                // 分配输出窗口得分空间   
    index = 0; 
 for (i = 0; i < numBoxes; i++)                  // 遍历所有输入窗口   
 { 
 if (!is_suppressed[indices[i]])             // 将未发生抑制的窗口信息保存到输出信息中   
 { 
 (*pointsOut)[index].x = points[indices[i]].x; 
 (*pointsOut)[index].y = points[indices[i]].y; 
 (*oppositePointsOut)[index].x = oppositePoints[indices[i]].x; 
 (*oppositePointsOut)[index].y = oppositePoints[indices[i]].y; 
 (*scoreOut)[index] = score[indices[i]]; 
            index++; 
 } 
 
 } 
 
    free(indices);          // 释放indices空间   
    free(box_area);         // 释放box_area空间   
    free(is_suppressed);    // 释放is_suppressed空间   
 
 return LATENT_SVM_OK; 
} 

八、改进方法

非极大值抑制(NMS)在目标检测中用于去除冗余和高度重叠的检测框,只保留最佳的候选框。然而,传统的NMS方法存在一些缺点,因此研究人员提出了多种改进方法。以下是几种主要的NMS改进方法及其详细原理、优缺点:

1. Soft NMS

原理
Soft NMS改进了传统NMS中直接将IoU超过阈值的检测框得分置为0的做法。它引入了一个与IoU相关的惩罚函数,对IoU超过阈值的检测框得分进行衰减,而不是完全抑制。这样做可以保留部分重叠但可能仍然有价值的检测框。

优点

  • 提高了检测的召回率,因为重叠但得分较高的检测框不会被完全抑制。
  • 更容易集成到现有的目标检测算法中,不需要重新训练模型。

缺点

  • 惩罚函数的选择和参数设置可能对结果有较大影响,需要仔细调整。
  • 在某些情况下,可能仍然会抑制掉一些有价值的检测框。

2. Softer NMS

原理
Softer NMS在Soft NMS的基础上进一步考虑了检测框位置的不确定性。它通过对检测框的位置和得分进行联合建模,实现了对检测框的更精细调整。这种方法能够更准确地反映检测框的质量,并进一步提高检测的准确性。

优点

  • 提高了检测的准确性,因为考虑了检测框位置的不确定性。
  • 对重叠检测框的处理更加灵活和精细。

缺点

  • 实现较为复杂,需要额外的计算资源。
  • 参数设置和模型训练可能更加困难。

3. Weighted NMS

原理
Weighted NMS根据检测框的某些属性(如大小、形状等)对检测框的得分进行加权处理。这样做可以使得检测算法更加关注于那些对检测结果影响较大的检测框。

优点

  • 提高了检测的鲁棒性,因为考虑了检测框的多种属性。
  • 可以根据具体任务的需求进行灵活调整。

缺点

  • 加权函数的选择和参数设置可能对结果有较大影响。
  • 在某些情况下,可能仍然会抑制掉一些有价值的检测框。

4. Adaptive NMS

原理
Adaptive NMS是一种自适应的NMS方法,它根据目标检测场景的不同自动调整NMS的阈值或其他参数。这种方法能够更好地适应复杂多变的检测场景。

优点

  • 提高了检测的泛化能力,因为可以自动适应不同的检测场景。
  • 不需要手动设置NMS的阈值或其他参数。

缺点

  • 自适应算法的设计和实现可能较为复杂。
  • 在某些极端情况下,可能无法准确地调整参数。

5. 基于深度学习的NMS

原理
基于深度学习的NMS方法通常是将NMS集成到深度学习模型中,通过端到端的训练来优化NMS的参数和模型的其他部分。这种方法可以充分利用深度学习模型的强大学习能力。

优点

  • 提高了检测的精度和效率,因为可以通过训练来优化NMS的参数和模型。
  • 可以更好地处理复杂的检测场景和目标。

缺点

  • 需要大量的训练数据和计算资源。
  • 模型的训练可能较为复杂和耗时。

综上所述,不同的NMS改进方法各有优缺点,在实际应用中可以根据具体需求选择合适的方法。

九. 应用场景

NMS在目标检测领域有着广泛的应用,包括但不限于以下几个方面:

  • 自动驾驶:在自动驾驶系统中,NMS用于去除车辆、行人等目标的冗余检测框,提高检测的准确性和可靠性。
  • 安防监控:在安防监控系统中,NMS用于去除人脸、行人等目标的重复检测框,提高监控效率。
  • 医学影像分析:在医学影像分析中,NMS用于去除病灶、器官等目标的重叠检测框,辅助医生进行更准确的诊断。
  • 机器人视觉:在机器人视觉系统中,NMS用于去除机器人视野中物体的冗余检测框,提高机器人的环境感知能力。

参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值