一文打尽NMS技术

作者丨仿佛若有光157
来源丨CV技术指南
编辑丨极市平台
前言Non-Maximum Suppression(NMS)非极大值抑制 。从字面意思理解 , 抑制那些非极大值的元素 , 保留极大值元素 。其主要用于目标检测 , 目标跟踪 , 3D重建 , 数据挖掘等 。
目前NMS常用的有标准NMS, Soft NMS, DIOU NMS等 。后续出现了新的Softer NMS , Weighted NMS等改进版 。
原始NMS以目标检测为例 , 目标检测推理过程中会产生很多检测框(A,B,C,D,E,F等) , 其中很多检测框都是检测同一个目标 , 但最终每个目标只需要一个检测框 , NMS选择那个得分最高的检测框(假设是C) , 再将C与剩余框计算相应的IOU值 , 当IOU值超过所设定的阈值(普遍设置为0.5 , 目标检测中常设置为0.7 , 仅供参考) , 即对超过阈值的框进行抑制 , 抑制的做法是将检测框的得分设置为0 , 如此一轮过后 , 在剩下检测框中继续寻找得分最高的 , 再抑制与之IOU超过阈值的框 , 直到最后会保留几乎没有重叠的框 。这样基本可以做到每个目标只剩下一个检测框 。

一文打尽NMS技术

文章插图
 
原始NMS(左图1维 , 右图2维)算法伪代码如下:
一文打尽NMS技术

文章插图
 

一文打尽NMS技术

文章插图
 
实现代码:(以pytorch为例)
def NMS(boxes,scores, thresholds):x1 = boxes[:,0]y1 = boxes[:,1]x2 = boxes[:,2]y2 = boxes[:,3]areas = (x2-x1)*(y2-y1)_,order = scores.sort(0,descending=True)keep = []while order.numel() > 0:i = order[0]keep.Append(i)if order.numel() == 1:breakxx1 = x1[order[1:]].clamp(min=x1[i])yy1 = y1[order[1:]].clamp(min=y1[i])xx2 = x2[order[1:]].clamp(max=x2[i])yy2 = y2[order[1:]].clamp(max=y2[i])w = (xx2-xx1).clamp(min=0)h = (yy2-yy1).clamp(min=0)inter = w*hovr = inter/(areas[i] + areas[order[1:]] - inter)ids = (ovr<=thresholds).nonzero().squeeze()if ids.numel() == 0:breakorder = order[ids+1]return torch.LongTensor(keep)除了自己实现以外 , 也可以直接使用torchvision.ops.nms来实现 。
torchvision.ops.nms(boxes, scores, iou_threshold)上面这种做法是把所有boxes放在一起做NMS , 没有考虑类别 。即某一类的boxes不应该因为它与另一类最大得分boxes的iou值超过阈值而被筛掉 。
对于多类别NMS来说 , 它的思想比较简单:每个类别内部做NMS就可以了 。实现方法:把每个box的坐标添加一个偏移量 , 偏移量由类别索引来决定 。
下面是
torchvision.ops.batched_nms的实现源码以及使用方法
#实现源码max_coordinate = boxes.max()offsets = idxs.to(boxes) * (max_coordinate + torch.tensor(1).to(boxes))boxes_for_nms = boxes + offsets[:, None]keep = nms(boxes_for_nms, scores, iou_threshold)return keep#使用方法torchvision.ops.boxes.batched_nms(boxes, scores, classes, nms_thresh)这里偏移量用boxes中最大的那个作为偏移基准 , 然后每个类别索引乘以这个基准即得到每个类的box对应的偏移量 。这样就把所有的boxes按类别分开了 。
在YOLO_v5中 , 它自己写了个实现的代码 。
c = x[:, 5:6] * (0 if agnostic else max_wh)# classesboxes, scores = x[:, :4] + c, x[:, 4]# boxes (offset by class), scoresi = torchvision.ops.nms(boxes, scores, iou_thres) 这里的max_wh相当于前面的boxes.max() , YOLO_v5中取的定值4096 。这里的agnostic用来控制是否用于多类别NMS还是普通NMS 。
NMS的缺点
  1. 需要手动设置阈值 , 阈值的设置会直接影响重叠目标的检测 , 太大造成误检 , 太小达不到理想情况 。
  2. 低于阈值的直接设置score为0 , 做法太hard 。
  3. 只能在CPU上运行 , 成为影响速度的重要因素 。
  4. 通过IoU来评估 , IoU的做法对目标框尺度和距离的影响不同 。
NMS的改进思路
  1. 根据手动设置阈值的缺陷 , 通过自适应的方法在目标系数时使用小阈值 , 目标稠密时使用大阈值 。例如Adaptive NMS 。
  2. 将低于阈值的直接置为0的做法太hard , 通过将其根据IoU大小来进行惩罚衰减 , 则变得更加soft 。例如Soft NMS , Softer NMS 。


    推荐阅读