非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素。例如在行人检测中,滑动窗口经特征提取,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。 NMS在计算机视觉领域有着非常重要的应用,如视频目标跟踪、数据挖掘、3D重建、目标识别以及纹理分析等 。
在目标检测中,NMS的目的就是要去除冗余的检测框,保留最好的一个,如下图所示:
NMS的原理是对于预测框的集合S及其对应的置信度score(这里的置信度就是softmax得出的概率值,它的含义是多大的把握预测正确,也就是有多大的把握确定检测框中存在真正的目标),选择具有最大score的检测框,记为M,将其从集合S中移除并加入到最终的检测结果集合中.并且将集合S中剩余检测框中与检测框M的IoU大于阈值的框从集合S中移除.重复这个过程,直到集合S为空。
使用流程如下图所示:
首先是检测出一系列的检测框
将检测框按照类别进行分类
对同一类别的检测框应用NMS获取最终的检测结果
通过一个例子看些NMS的使用方法,假设定位车辆,算法就找出了一系列的矩形框,我们需要判别哪些矩形框是没用的,需要使用NMS的方法来实现。
假设集合S中有A、B、C、D、E 5个候选框,每个框旁边的数字是它的置信度,我们设定NMS的iou阈值是0.5,接下来进行迭代计算:
第一轮:因为B是得分最高的(即B的置信度最高),在集合S的其余候选框中,如果与B的IoU>0.5会被删除。A,C,D,E中现在分别与B计算IoU,DE结果>0.5,剔除DE(说明BDE检测的是同一个目标,保留置信度最大的候选框;而AC可能检测的是另一个目标),B作为一个预测结果,从集合S中移除,并放入最终的检测结果集合中。此时新的集合S中只剩下候选框A,C
第二轮:在新的集合S中,A的置信度得分最高,将集合S中剩下的候选框分别与A计算IoU,因为A与C的iou>0.5,所以剔除C,A作为另外一个预测结果从集合S中移除,并放入最终的检测结果集合中,此时集合S为空,所以循环结束。
最终结果为在这个5个中检测出了两个目标为A和B。
单类别的NMS的实现方法如下所示:
import numpy as np
def nms(bboxes, confidence_score, threshold):
"""非极大抑制过程
:param bboxes: 同类别候选框坐标
:param confidence: 同类别候选框分数(即置信度)
:param threshold: iou阈值
:return:
"""
# 1、没有传入候选框则返回空列表
if len(bboxes) == 0:
return [], []
#强制转换为numpy类型的数组,这样才可以进行切片等numpy所支持的操作
bboxes = np.array(bboxes)
score = np.array(confidence_score)
# 取出所有候选框的左上角坐标和右下角坐标
x1 = bboxes[:, 0]
y1 = bboxes[:, 1]
x2 = bboxes[:, 2]
y2 = bboxes[:, 3]
# 2、对候选框进行NMS筛选
# 返回的框坐标和分数
picked_boxes = []
picked_score = []
# 对置信度进行排序, 获取排序后的下标序号, argsort默认从小到大排序
order = np.argsort(score)
#计算所有候选框的面积
areas = (x2 - x1) * (y2 - y1)
while order.size > 0:
# 将当前置信度最大的候选框的索引,加入返回值列表中,因为是从小到大排序,所有最后一个值最大,即 order[-1]表示最后一个元素
index = order[-1]
#将置信度最大的候选框及其置信度值加入返回值列表中
picked_boxes.append(bboxes[index])
picked_score.append(score[index])
# 获取当前置信度最大的候选框与其他任意候选框的相交面积,这里的order[:-1]表示除了最后一个元素之外的所有元素,np.max和np.maximum的实现功能是不同的
#np.maximum的用法:np.maximum([2,4,7],[3,1,5])输出的结果是array([3, 4, 7]);np.maximum([2],[3,1,5])的输出结果是array([3, 2, 5])
#np.max的用法:np.max([2,4,7])输出结果是7
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]])
# 计算相交的面积,不重叠时面积设为0
w = np.maximum(0.0, x22 - x11)
h = np.maximum(0.0, y22 - y11)
inter_area = w * h
# 计算交并比
iou = inter_area / (areas[index] + areas[order[:-1]] - inter_area)
# 获取IoU小于阈值的候选框的索引
keep_boxes = np.where(iou < threshold)
#更新order,以便保留IoU小于阈值的框,
order = order[keep_boxes]
# 返回NMS后的框及分类结果
return picked_boxes, picked_score
假设有检测结果如下:当阈值threshold设置的越大,则保留越多的候选框
当threshold取0.3时:
bounding = [(187, 82, 337, 317), (150, 67, 305, 282), (246, 121, 368, 304)]
confidence_score = [0.9, 0.65, 0.8]
threshold = 0.3
picked_boxes, picked_score = nms(bounding, confidence_score, threshold)
print('阈值threshold为:', threshold)
print('NMS后得到的bbox是:', picked_boxes)
print('NMS后得到的bbox的confidences是:', picked_score)
返回结果:
阈值threshold为: 0.3
NMS后得到的bbox是: [array([187, 82, 337, 317])]
NMS后得到的bbox的confidences是: [0.9]
当threshold取0.5时:
bounding = [(187, 82, 337, 317), (150, 67, 305, 282), (246, 121, 368, 304)]
confidence_score = [0.9, 0.65, 0.8]
threshold = 0.5
picked_boxes, picked_score = nms(bounding, confidence_score, threshold)
print('阈值threshold为:', threshold)
print('NMS后得到的bbox是:', picked_boxes)
print('NMS后得到的bbox的confidences是:', picked_score)
返回结果:
阈值threshold为: 0.5
NMS后得到的bbox是: [array([187, 82, 337, 317]), array([246, 121, 368, 304])]
NMS后得到的bbox的confidences是: [0.9, 0.8]
上述所讲的NMS方法都是先将检测框按照类别进行分类,然后对对同一类别的检测框应用NMS。但是在实际的任务中,如果所预测的类别很多时,那么这种效率非常低。所以有些时候我们会使用新的方法进行NMS:它的大致思想是先将不同类别的预测框在坐标位置上尽可能的区分开,然后就可以一次性对所有预测框进行NMS(此时不用先进行分类,然后分别对每一个类别依次做NMS)
,比如下图所示,蓝色方框的类别索引是1,黄色方框的类别索引是2,这些不同类别的预测框在位置上靠的很近,此时如果直接对所有类别同时做NMS,效果就很差。所以我们会设法将蓝色方框和黄色方框分离开,本例的方法是首先找到所有方框中坐标值最大的数值max_value,比如这里是81,
然后使用类别索引 indxs与val_value相乘,得到不同类别框的偏移量offsets,它的公式是:offsets=indxs*max_value
比如对于类别索引为1的方框,它的偏移量是offsets=indxs*max_value=1*81=81,对于类别索引为1的方框,它的偏移量是offsets=indxs*max_value=2*81=162
计算完每个类别的偏移量后,我们就得到新的预测框的坐标以及其对于的新位置,如下所示。然后就可以一次性对所有预测框进行NMS(此时不用先进行分类,然后分别对每一个类别依次做NMS)