37从传统算法到深度学习:目标检测入门实战 --非极大值抑制

非极大值抑制的作用

在进行目标检测过程中,我们的分类器会对每一个滑动窗口的内容进行分类,而滑动窗口是按照设定的步长在图像金字塔的每个图层中从上到下、从左向右移动,这样一个目标就会出现在多个滑动窗口中,最后我们就会获得多个相交、重叠的矩形框。如下图在目标检测过程中目标上会产生多个矩形框,我们希望从这些矩形框中挑选出一个最合适的矩形框且剔除多余的矩形框,使得每个目标只被一个矩形框标记。


image.png

非极大值抑制(Non Maximum Suppression)以下简称 NMS,的主要作用是去除目标检测过程中产生的冗余矩形框。要实现 NMS 首先需要计算矩形框之间的交并比(Intersection over Union),以下简称 IoU。下图以直观的例子展示计算 IoU 的方法,左图中的目标(人)同时被两个矩形框标记,为了剔除多余的矩形框需要计算两个矩形框的 IoU。IoU 的计算的方法如下图中间的公式所示,即两个框的交集(红色区域)与两个框的并集(绿色区域)的比值。如果计算后的 IoU 大于事先设定的阈值,则剔除较小的矩形框(下图中最右边图片所示),通过这个过程我们就达到了剔除冗余的矩形框的目的。接下来我们将通过代码来实现一个 NMS 函数。


image.png

构建非极大值抑制函数

通过前面一节的介绍我们已经知道 NMS 的作用和原理,接下来我们一步一步教会大家实现一个 NMS 函数。首先在终端中输入下面两行命令下载本节实验所需图片。

!wget https://labfile.oss.aliyuncs.com/courses/3096/man.jpg
!wget https://labfile.oss.aliyuncs.com/courses/3096/people.jpg

然后使用下面两行命令下载实验 5 的代码和训练好的模型。

!wget https://labfile.oss.aliyuncs.com/courses/3096/hog_detection.py
!wget https://labfile.oss.aliyuncs.com/courses/3096/model

首先我们导入 NumPy、OpenCV 和下载好的代码模块 hog_detection.py 中的 run 方法。

import numpy as np
import cv2
from hog_detection import run

然后我们定义一个名为 NMS 的函数(见下面代码)。该函数有两个参数,第一个参数 boxes 表示目标检测过程中获得的所有矩形框。第二个参数 threshold 表示事先定义的一个阈值,当两个矩形框重叠的面积超过这个阈值时我们将剔除其中一个矩形框。

def NMS(boxes, threshold):
    if len(boxes) == 0:
        return []
    
    boxes = np.array(boxes).astype("float")

    x1 = boxes[:,0]
    y1 = boxes[:,1]
    w1 = boxes[:,2]
    h1 = boxes[:,3]
    x2 = x1 + w1
    y2 = y1 + h1
    
    area = (w1 + 1) * (h1 + 1)
    temp = []
    
    idxs = np.argsort(h1)
    
    while len(idxs) > 0:
        last = len(idxs) - 1
        i = idxs[last]
        temp.append(i)   
        
        x1_m = np.maximum(x1[i], x1[idxs[:last]])
        y1_m = np.maximum(y1[i], y1[idxs[:last]])
        
        x2_m = np.minimum(x2[i], x2[idxs[:last]])
        y2_m = np.minimum(y2[i], y2[idxs[:last]])
        
        w = np.maximum(0, x2_m - x1_m + 1)
        h = np.maximum(0, y2_m - y1_m + 1)
        
        over = (w * h) / area[idxs[:last]]
        
        idxs = np.delete(idxs, np.concatenate(([last],  
            np.where(over > threshold)[0])))  

    return boxes[temp].astype("int")

在目标检测过程中我们的算法有可能没有检测到任何目标,那么这就表示在图片中没有用于标记目标的矩形框。所以下面的代码第 2 行我们将用一个 if 语句来判断输入的 boxes 的数量是否为 0,如果矩形框的数量为 0 则函数返回一个空列表。然后我们还需要将 boxes 转换为 NumPy 数组并且将其中每个元素转换为 float 浮点类型(见代码第 5 行),因为后面我们需要用这些元素进行算术运算。
代码第 7 到 10 行,我们使用切片方法获取每一个 boxes 内的元素并将其分别保存在 x1、y1、w1、h1 这四个数组中。这四个数组中分别保存了每一个 boxes 中的第一至四元素。x1 表示矩形框左上角顶点的横坐标,y1 表示矩形框左上角顶点的纵坐标,w1 是矩形框的宽,h1 是矩形框的高。 代码第 11、12 行我们使用这四个数组计算得出每个矩形框的右下角顶点横坐标的集合 x2 和 纵坐标的集合 y2。
代码第 14 行表示我们需要计算每个矩形框的面积。这里分别将 w1 和 h1 加 1 是为了避免使用 area 计算 IoU 时分母为零的情况发生。我们还初始化了代码 15 行中的 temp 列表用于临时存储值。
代码 17 行我们使用 NumPy 的 argsort 方法将 h1 中的元素从小到大排序并返回每个元素在 h1 中的下标,需要注意的是 idxs 中的元素是 h1 中元素的下标,这些下标排列的顺序是按照其对应 h1 中元素的大小排列的。
接下来我们使用 while 循环遍历 idxs,当 idxs 中没有元素时终止循环。代码 20 到 22 行我们获取 idxs 中最后一个元素并将其添加到 temp 中。
代码 24 行我们使用 np.maximum 方法将 x1[i] 与 boxes 中其他矩形框的左上角横坐标两两比较, 将较大的值保存在数组 x1_m 中。同样代码 25 行将 y1[i] 与 boxes 中其他矩形框的左上角纵坐标两两比较,将较大的值保存在数组 y1_m 中。两个矩形框重叠的部分是矩形,所以这一步的目的是为了找到这个重叠矩形的左上角顶点。同理,27、28 两行代码的目的是为了找出这个重叠矩形的右下角顶点。我们使用 np.minimum 将 x2[i] 与 boxes 中其他矩形框的右下角横坐标两两比较, 将较小的值保存在数组 x2_m 中。同样的再将 y2[i] 与 boxes 中其他矩形框的右下角纵坐标两两比较,将较小的值保存在数组 y2_m 中。
有了重叠矩形的两个顶点坐标,我们就可以计算矩形的宽和高,进而可以计算矩形的面积。第 30,31 行代码是分别计算矩形的宽和高,我们使用 np.maximum 方法来剔除掉没有相交的矩形。如果两个矩形框相交,则 x2_m - x1_m + 1 和 y2_m - y1_m + 1 大于零,如果两个矩形框不相交则这两个值小于零。
33 行代码表示计算重叠矩形面积和 area 中的面积的比值 over,这一步和计算 IoU 是等效的。35 行代码的目的是为了剔除重叠的矩形框。我们使用 np.where 判断 over 中的元素是否大于设定的阈值 threshold,如果大于这个阈值则返回这个元素的下标。接着使用 np.concatenate 方法将 idxs 中最后的元素和返回的下标拼接在一起。最后通过 np.delete 方法从 idxs 中删除这些下标对应的元素。
通过上一步我们删除了与 idxs 中 last 对应的矩形框相互重叠且面积大于阈值的矩形框(同时也从 idxs 中删除最后一个元素),然后进入下一个循环直到 idxs 中的元素个数为 0,最后我们通过下面一行代码返回挑选后的矩形框,同时我们需要使用 astype 方法将 boxes 中的浮点类型转换为整数类型。
接下来,我们将使用非极大值抑制的函数来剔除检测结果中多余的窗口。首先我们创建一个 img_path 变量用于保存图片路径,然后调用 run 函数,run 函数将返回 2 个值 roi_loc 和 image。roi_loc 里保存了一个或多个矩形框的顶点坐标、宽和高,这些矩形框内的区域被模型认为是有人存在的。image 是缩放后输入图片,我们将在这张图片上用矩形框标记出行人。

img_path = "man.jpg"
roi_loc, image = run(img_path=img_path)

接下来我们将使用 cv2.rectangle 方法在 image 上画出矩形框,这些矩形框是通过 NMS 方法获得的没有重叠的矩形框。

for (x, y, w, h) in NMS(roi_loc, threshold=0.3):
    cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)

最后我们通过下面的代码来显示检测结果,首先从 matplotlib 导入 pyplot,然后我们使用 %matplotlib inline 魔法函数让图片在页面中显示。我们使用 plt.figure(figsize = (10,10)) 创建一个宽和高都是 10 英寸的图像实例。然后使用 resized[:,:,::-1] 切片方法将图片通道的顺序调转,最后使用 plt.imshow 在页面中呈现绘图后的结果。

from matplotlib import pyplot as plt
%matplotlib inline

plt.figure(figsize = (10,10))
image = image[:,:,::-1]
plt.imshow(image)

如果脚本运行正常我们能看到类似下图结果,相较于左边没有使用 NMS 方法获得的检测图片,我们剔除了冗余的矩形框使得目标只被一个矩形框标记。


image.png

将 img_path = "man.jpg" 修改为 img_path = "people.jpg" 我们将得到类似下图结果,同样的通过使用 NMS 方法我们剔除了大部分的矩形框。


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容