非极大值抑制(NMS)算法详解

Posted VipSoft

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了非极大值抑制(NMS)算法详解相关的知识,希望对你有一定的参考价值。

NMS(non maximum suppression)即非极大值抑制,广泛应用于传统的特征提取和深度学习的目标检测算法中。
NMS原理是通过筛选出局部极大值得到最优解。
在2维边缘提取中体现在提取边缘轮廓后将一些梯度方向变化率较小的点筛选掉,避免造成干扰。
在三维关键点检测中也起到重要作用,筛选掉特征中非局部极值。
在目标检测方面,无论是One-stage的SSD系列算法、YOLO系列算法还是Two-stage的基于RCNN系列的算法,非极大值抑制都是其中必不可少的一个组件,可以将较小分数的输出框过滤掉,同样,在三维基于点云的目标检测模型中亦有使用。

在现有的基于anchor的目标检测算法中,都会产生数量巨大的候选矩形框,这些矩形框有很多是指向同一目标,因此就存在大量冗余的候选矩形框。非极大值抑制算法的目的正在于此,它可以消除多余的框,找到最佳的物体检测位置。

IoU(Intersection over Union) :定位精度评价公式。
相当于两个区域交叉的部分除以两个区域的并集部分得出的结果。

IoU各个取值时的情况展示,一般来说,这个 Score > 0.5 就可以被认为一个不错的结果了。

IOU计算:

如何计算IoU(交并比)

选取两个矩形框左顶角的横,纵坐标的最大值,x21,y21;选取两个矩形框右下边角的横纵坐标的最小值,x12,y12;

  • 交集面积计算:

\\[Area(A \\cap B) = |x12 - x21| * |y12 - y21| \\]

  • 并集面积计算:

\\[Area(A \\cup B) = |x11 - x12| * |y11 - y12| + |x21 - x22| * |y21 - y22| - Area(A \\cap B) \\]

  • 计算IOU公式

\\[IoU = \\frac Area(A \\cap B) Area(A \\cup B) \\]

算法流程如下:

  • 将所有框的得分排序,选中最高分及其对应的框
  • 遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值(常用的值为0.5左右),我们就将框删除。(为什么要删除,是因为超过设定阈值,认为两个框的里面的物体属于同一个类别,比如都属于狗这个类别。我们只需要留下一个类别的可能性框图即可。)
  • 从未处理的框中继续选一个得分最高的,重复上述过程。


代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NMS function(Non-Maximum Suppression,  抑制不是极大值的元素)
        psedocode:
            1. choose the highest score element  a_1  in set B, add a_1 to the keep set C
            2. compute the IOU between the chosen element(such as a_1) and others elements in set B
            3. only keep the nums  at set B whose IOU value is less than thresholds (can be set as >=0.5), delete the nums similiar
                to a_1(the higher IOU it is , the more interseciton between a_1 and it will have)
            4. choose the highest score value a_2 left at set B  and add a_2 to set C
            5. repeat the 2-4 until  there is nothing in set B, while set C is the NMS value set

"""
import numpy as np

# boxes表示人脸框的xywh4点坐标+相关置信度
boxes = np.array([[100, 100, 210, 210, 0.72],
                  [250, 250, 420, 420, 0.8],
                  [220, 220, 320, 330, 0.92],
                  [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]  # [100. 250. 220. 230. 220.]
    y1 = dets[:, 1]  # [100. 250. 220. 240. 230.]
    x2 = dets[:, 2]  # [210. 420. 320. 325. 315.]
    y2 = dets[:, 3]  # [210. 420. 330. 330. 340.]

    areas = (y2 - y1 + 1) * (x2 - x1 + 1)
    scores = dets[:, 4]  # [0 1 3 4 2]
    keep = []
    # index表示按照scores从高到底的相关box的序列号
    index = scores.argsort()[::-1]  # [2 4 3 1 0]

    while index.size > 0:
        print("sorted index of boxes according to scores", index)
        # 选择得分最高的score直接加入keep列表中
        i = index[0]
        keep.append(i)
        # 计算score最高的box和其他box分别的相关交集坐标
        x11 = np.maximum(x1[i], x1[index[1:]])  # [220. 230. 250. 220.]  最高的被提走了,所以要从1开始取后 4位
        y11 = np.maximum(y1[i], y1[index[1:]])  # [230. 240. 250. 220.]
        x22 = np.minimum(x2[i], x2[index[1:]])  # [315. 320. 320. 210.]
        y22 = np.minimum(y2[i], y2[index[1:]])  # [330. 330. 330. 210.]

        print("x1 values by original order:", x1)
        print("x1 value by scores:", x1[index[:]])  #  [220. 220. 230. 250. 100.]
        print("x11 value means  replacing the less value compared" \\
              " with the value by the largest score :", x11)
        # 计算交集面积
        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
        # 计算相关IOU值(交集面积/并集面积,表示边框重合程度,越大表示越相似,越该删除)
        # 重叠面积 /(面积1+面积2-重叠面积)
        ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)
        # 只保留iou小于阈值的索引号,重复上步
        idx = np.where(ious <= thresh)[0]
        # 因为第一步index[0]已经被划走,所以需要原来的索引号需要多加一
        index = index[idx + 1]

    return keep


import matplotlib.pyplot as plt


def plot_bbox(ax, dets, c=\'b\', title_name="title"):
    x1 = dets[:, 0]
    y1 = dets[:, 1]
    x2 = dets[:, 2]
    y2 = dets[:, 3]

    ax.plot([x1, x2], [y1, y1], c)
    ax.plot([x1, x1], [y1, y2], c)
    ax.plot([x1, x2], [y2, y2], c)
    ax.plot([x2, x2], [y1, y2], c)
    ax.set_title(title_name)


if __name__ == \'__main__\':
    # 1.创建画板fig
    fig = plt.figure(figsize=(12, 6))

    # 参数解释,前两个参数 1,2 表示创建了一个一行两列的框 第三个参数表示当前所在的框
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    plot_bbox(ax1, boxes, \'k\', title_name="before nms")  # before nms

    keep = py_cpu_nms(boxes, thresh=0.7)

    plot_bbox(ax2, boxes[keep], \'r\', title_name="after nms")  # after nms
    plt.show()

参考文献:
https://blog.csdn.net/weixin_42237113/article/details/105743296
https://blog.csdn.net/lz867422770/article/details/100019587

[目标检测][python][cpp]非极大值抑制(NMS)算法原理以及CPP实现

问题描述

在目标检测中,有一个很重要的算法,就是非极大值抑制算法,它本身是一个贪心算法。在多个目标检测预测框结果里找到极大的那个,也即是置信度最高的那个。最近有被问到有关NMS的CPP实现,大概查了一下,大部分都是用python写的,用cpp可能更困难一些。

解决思路

算法原理

输入:包含多个输入框和置信度的N*5矩阵,其中N为检测框的数量,阈值Th
输出:检测物体不重复的框M*5的矩阵,M为图片中目标数量

  1. 将检测矩阵中的所有框按照score排序
  2. 从头选择最大score的检测框,放入到输出矩阵中,然后遍历剩余的框,并计算与最大框的IOU,当IOU大于Th时,那么该框受到抑制,从输入框中删除。小于阈值则保留。
  3. 从输入检测矩阵的剩余的框中,找到下一个最大的框,扔进输出矩阵,重复第二步的操作。
  4. 直到输入框为空,即为所有的输入都被处理了,此时输出矩阵里的检测框即为图片中M个对象的检测框。

python实现

import numpy as np

def NMS(dets, thresh):
    x1 = dets[:,0]
    y1 = dets[:,1]
    x2 = dets[:,2]
    y2 = dets[:,3]
    scores = dets[:,4]
    areas = (x2 - x1 + 1) * (y2 - y1 + 1) 
    order=score.argsort()[::-1]
    keep = []
    while order.size() > 0:
        p = order[0]
        keep.append(p)
        xx1 = np.maximum(x1[p], x1[1:])
        yy1 = np.maximum(y1[p], y1[1:])
        xx2 = np.minimum(x2[p], y2[1:])
        yy2 = np.minimum(y2[p], y2[1:])

        inter = (xx2 - xx1 + 1) * (yy2 - yy1 + 1)
        inter = np.maximum(inter, 0)
        over = inter / (areas[p] + areas[1:] - inter)

        inds = np.where(over < thresh)[0]
        order =  order[inds + 1]
    
    return dets[keep]

CPP实现

#include <bits/stdc++.h>
#include <cmath>
using namespace std;

const int N = 1e6 + 1;
const int M = 82;

class Node
{
    public:
        double x1, x2, y1, y2;
        float score;

        double area()
        {
            double w = max(0.0, x2 - x1 + 1);
            double h = max(0.0, y2 - y1 + 1);
            return w * h;
        }
};

int cmp(const Node A, const Node B)
{
    return A.score > B.score;
}

double IoU(Node A, Node B)
{
    double x1 = max(A.x1, B.x1);
    double y1 = max(A.y1, B.y1);
    double x2 = max(A.x2, B.x2);
    double y2 = max(A.y2, B.y2);

    Node inter = {x1, x2, y1, y2, 0.0};

    return inter.area() / (A.area() + B.area() - inter.area());
}

vector<Node> NMS(vector<Node> dets, float therh)
{
    vector<Node>ans;
    sort(dets.begin(), dets.end(), cmp);
    while (dets.size() > 0)
    {
        Node p = dets[0];
        ans.push_back(p);
    
        for (int i=1; i< dets.size(); i++)
        {
            if (IoU(p, dets[i]) >= therh)
            {
                dets.erase(dets.begin()+i);
            }
        }
        dets.erase(dets.begin()+0);
    }
    return ans;
}

int main(int argc, char const *argv[])
{
    Node A = {1, 2, 3, 4, 0.6};
    Node B = {5, 6, 6, 8, 0.3};
    Node C = {1.2, 2.1, 3.1, 4.1, 0.66};
    vector<Node> dets;
    dets.push_back(A);
    dets.push_back(B);
    dets.push_back(C);
    vector<Node> ans = NMS(dets, 0.5);
    for (int i=0; i<ans.size(); i++)
    {
        cout<<ans[i].area()<<endl;
    }
    system("pause");
    return 0;
}

cpp的实现略微麻烦一些,主要是因为需要自己手写结构,并且需要生成排序算法,当然,由于使用了std::vector,在删除时需要移动元素,因此还有很大的可改进空间,比如增加一个flag数组。

其他变体

其他变体比如softNMS是将IoU大于阈值的框的score设定较高的衰减率,这样IoU较大的框的分数会不断下降,直至小于阈值。使用这种方法和NMS的区别在于,NMS直接删除,将对应框的score设置为0,而softNMS则设定一个下降值。


以上是关于非极大值抑制(NMS)算法详解的主要内容,如果未能解决你的问题,请参考以下文章

非极大值抑制算法(NMS)的python实现

[目标检测][python][cpp]非极大值抑制(NMS)算法原理以及CPP实现

[目标检测][python][cpp]非极大值抑制(NMS)算法原理以及CPP实现

非极大值抑制(NMS)

Pytorch机器学习—— YOLOV5中NMS非极大值抑制与DIOU-NMS等改进

非极大值抑制算法(Non-Maximum Suppression,NMS)