非极大值抑制(Non-Maximum Suppression,NMS)
概述
非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素,可以理解为局部最大搜索。这个局部代表的是一个邻域,邻域有两个参数可变,一是邻域的维数,二是邻域的大小。这里不讨论通用的NMS算法(参考论文《Efficient Non-Maximum Suppression》对1维和2维数据的NMS实现),而是用于目标检测中提取分数最高的窗口的。例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。
NMS在计算机视觉领域有着非常重要的应用,如视频目标跟踪、数据挖掘、3D重建、目标识别以及纹理分析等。Faster-RCNN中有两处使用NMS,第一处是训练+预测的时候,利用ProposalCreator来生成proposal的时候,因为只需要一部分proposal,所以利用NMS进行筛选。第二处使用是预测的时候,当得到300个分类与坐标偏移结果的时候,需要对每个类别逐一进行非极大值抑制。也许有人问为什么对于每个类别不直接取置信度最高的那一个?因为一张图中某个类别可能不止一个,例如一张图中有多个人,直接取最高置信度的只能预测其中的一个人,而通过NMS理想情况下可以使得每个人(每类中的每个个体)都会有且仅有一个bbox框。
原理
NMS(Non Maximum Suppression),又名非极大值抑制,是目标检测框架中的后处理模块,主要用于删除高度冗余的bbox,先用图示直观看看NMS的工作机制。从下图处理的结果可以看出,在目标检测过程中,对于每个obj在检测的时候会产生多个bbox,NMS本质就是对每个obj的多个bbox去冗余,得到最终的检测结果.

对于Bounding Box的列表B及其对应的置信度S,采用下面的计算方式.选择具有最大score的检测框M,将其从B集合中移除并加入到最终的检测结果D中.通常将B中剩余检测框中与M的IoU大于阈值Nt的框从B中移除.重复这个过程,直到B为空.
重叠率(重叠区域面积比例IOU)阈值
常用的阈值是 0.3 ~ 0.5.
其中用到排序,可以按照右下角的坐标排序或者面积排序,也可以是通过SVM等分类器得到的得分或概率,R-CNN中就是按得分进行的排序.

就像上面的图片一样,定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的方法是:先假设有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是我们保留下来的第二个矩形框。
就这样一直重复,找到所有被保留下来的矩形框。
原文: https://www.cnblogs.com/makefile/p/nms.html
NMS 在目标检测中的应用
人脸检测框重叠例子

face box
我们的目的就是要去除冗余的检测框,保留最好的一个.有多种方式可以解决这个问题,Triggs et al. 建议使用Mean-Shift 算法,利用bbox的坐标和当前图片尺度的对数来检测bbox的多种模式.但效果可能并不如使用强分类器结合NMS的效果好.
目标检测 pipline

产生proposal后使用分类网络给出每个框的每类置信度,使用回归网络修正位置,最终应用NMS.
实现步骤
人脸检测的一些概念
(1) 绝大部分人脸检测器的核心是分类器,即给定一个尺寸固定图片,分类器判断是或者不是人脸;
(2)将分类器进化为检测器的关键是:在原始图像上从多个尺度产生窗口,并resize到固定尺寸,然后送给分类器做判断。最常用的方法是滑动窗口。
以下图为例,由于滑动窗口,同一个人可能有好几个框(每一个框都带有一个分类器得分)

而我们的目标是一个人只保留一个最优的框:于是我们就要用到非极大值抑制,来抑制那些冗余的框: 抑制的过程是一个迭代-遍历-消除的过程。
(1)将所有框的得分排序,选中最高分及其对应的框:

(2)遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值,我们就将框删除。

(3)从未处理的框中继续选一个得分最高的,重复上述过程。

测试

可以看到:置信度为0.99的框保留下来是因为其置信度最高。第一次迭代时就已经存入了pick数组中。
置信度为0.88的框保留下来是因为其与0.99的框交并比IOU < overlap = 0.8
置信度为0.82的框保留下来是因为其与任何框都无重叠。
注意此时经过NMS后,每类别还可能会剩余不止一个的候选框。而对于图像中的每个类别我们只需一个候选框即可。所以还有后续处理。
后续:R-CNN分别用20个回归器对上述20个类别中剩余的候选框进行回归操作。最终得到每个类别修正后的得分最高的bounding box。
代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon May 7 21:45:37 2018
@author: lps
"""
import numpy as np
boxes=np.array([[100,100,210,210,0.72],
[250,250,420,420,0.8],
[220,220,320,330,0.92],
[100,100,210,210,0.72],
[230,240,325,330,0.81],
[220,230,315,340,0.9]])
def py_cpu_nms(dets, thresh):
# dets:(m,5) thresh:scaler
x1 = dets[:,0]
y1 = dets[:,1]
x2 = dets[:,2]
y2 = dets[:,3]
areas = (y2-y1+1) * (x2-x1+1)
scores = dets[:,4]
keep = []
index = scores.argsort()[::-1]
while index.size >0:
i = index[0] # every time the first is the biggst, and add it directly
keep.append(i)
x11 = np.maximum(x1[i], x1[index[1:]]) # calculate the points of overlap
y11 = np.maximum(y1[i], y1[index[1:]])
x22 = np.minimum(x2[i], x2[index[1:]])
y22 = np.minimum(y2[i], y2[index[1:]])
w = np.maximum(0, x22-x11+1) # the weights of overlap
h = np.maximum(0, y22-y11+1) # the height of overlap
overlaps = w*h
ious = overlaps / (areas[i]+areas[index[1:]] - overlaps)
idx = np.where(ious<=thresh)[0]
index = index[idx+1] # because index start from 1
return keep
import matplotlib.pyplot as plt
def plot_bbox(dets, c='k'):
x1 = dets[:,0]
y1 = dets[:,1]
x2 = dets[:,2]
y2 = dets[:,3]
plt.plot([x1,x2], [y1,y1], c)
plt.plot([x1,x1], [y1,y2], c)
plt.plot([x1,x2], [y2,y2], c)
plt.plot([x2,x2], [y1,y2], c)
plt.title("after nms")
plot_bbox(boxes,'k') # before nms
keep = py_cpu_nms(boxes, thresh=0.7)
plot_bbox(boxes[keep], 'r')# after nms
结果大致这样:

新建nms文件夹,将nms_py.py 和__init__.py(空)文件放在其内成为包,可以调用。然后在nms文件夹外新建测试运行时间脚本 test_num.py:
import numpy as np
import time
from nms.nums_py import py_cpu_nms # for cpu
#from nms.gpu_nms import gpu_nms # for gpu
np.random.seed( 1 ) # keep fixed
num_rois = 6000
minxy = np.random.randint(50,145,size=(num_rois ,2))
maxxy = np.random.randint(150,200,size=(num_rois ,2))
score = 0.8*np.random.random_sample((num_rois ,1))+0.2
boxes_new = np.concatenate((minxy,maxxy,score), axis=1).astype(np.float32)
def nms_test_time(boxes_new):
thresh = [0.7,0.8,0.9]
T = 50
for i in range(len(thresh)):
since = time.time()
for t in range(T):
keep = py_cpu_nms(boxes_new, thresh=thresh[i]) # for cpu
# keep = gpu_nms(boxes_new, thresh=thresh[i]) # for gpu
print("thresh={:.1f}, time wastes:{:.4f}".format(thresh[i], (time.time()-since)/T))
return keep
if __name__ =="__main__":
nms_test_time(boxes_new)
测试数据为6000个初始的rois,并设置nms阈值为0.7~0.9。阈值越大越慢,因为满足小于阈值的roi越多,需要循环的次数也越多。对每个阈值循环执行NMS 50次求平均:
直接运行得到运行时间:
thresh=0.7, time wastes:0.0287
thresh=0.8, time wastes:0.1057
thresh=0.9, time wastes:0.4204
参考: