非極大值抑制的作用
在進(jìn)行目標(biāo)檢測(cè)過(guò)程中溶推,我們的分類(lèi)器會(huì)對(duì)每一個(gè)滑動(dòng)窗口的內(nèi)容進(jìn)行分類(lèi)猜扮,而滑動(dòng)窗口是按照設(shè)定的步長(zhǎng)在圖像金字塔的每個(gè)圖層中從上到下怜瞒、從左向右移動(dòng)鸯屿,這樣一個(gè)目標(biāo)就會(huì)出現(xiàn)在多個(gè)滑動(dòng)窗口中盏档,最后我們就會(huì)獲得多個(gè)相交凶掰、重疊的矩形框。如下圖在目標(biāo)檢測(cè)過(guò)程中目標(biāo)上會(huì)產(chǎn)生多個(gè)矩形框蜈亩,我們希望從這些矩形框中挑選出一個(gè)最合適的矩形框且剔除多余的矩形框懦窘,使得每個(gè)目標(biāo)只被一個(gè)矩形框標(biāo)記。
非極大值抑制(Non Maximum Suppression)以下簡(jiǎn)稱(chēng) NMS勺拣,的主要作用是去除目標(biāo)檢測(cè)過(guò)程中產(chǎn)生的冗余矩形框奶赠。要實(shí)現(xiàn) NMS 首先需要計(jì)算矩形框之間的交并比(Intersection over Union),以下簡(jiǎn)稱(chēng) IoU药有。下圖以直觀的例子展示計(jì)算 IoU 的方法毅戈,左圖中的目標(biāo)(人)同時(shí)被兩個(gè)矩形框標(biāo)記,為了剔除多余的矩形框需要計(jì)算兩個(gè)矩形框的 IoU愤惰。IoU 的計(jì)算的方法如下圖中間的公式所示苇经,即兩個(gè)框的交集(紅色區(qū)域)與兩個(gè)框的并集(綠色區(qū)域)的比值。如果計(jì)算后的 IoU 大于事先設(shè)定的閾值宦言,則剔除較小的矩形框(下圖中最右邊圖片所示)扇单,通過(guò)這個(gè)過(guò)程我們就達(dá)到了剔除冗余的矩形框的目的。接下來(lái)我們將通過(guò)代碼來(lái)實(shí)現(xiàn)一個(gè) NMS 函數(shù)奠旺。
構(gòu)建非極大值抑制函數(shù)
通過(guò)前面一節(jié)的介紹我們已經(jīng)知道 NMS 的作用和原理蜘澜,接下來(lái)我們一步一步教會(huì)大家實(shí)現(xiàn)一個(gè) NMS 函數(shù)施流。首先在終端中輸入下面兩行命令下載本節(jié)實(shí)驗(yàn)所需圖片。
!wget https://labfile.oss.aliyuncs.com/courses/3096/man.jpg
!wget https://labfile.oss.aliyuncs.com/courses/3096/people.jpg
然后使用下面兩行命令下載實(shí)驗(yàn) 5 的代碼和訓(xùn)練好的模型鄙信。
!wget https://labfile.oss.aliyuncs.com/courses/3096/hog_detection.py
!wget https://labfile.oss.aliyuncs.com/courses/3096/model
首先我們導(dǎo)入 NumPy瞪醋、OpenCV 和下載好的代碼模塊 hog_detection.py 中的 run 方法。
import numpy as np
import cv2
from hog_detection import run
然后我們定義一個(gè)名為 NMS 的函數(shù)(見(jiàn)下面代碼)装诡。該函數(shù)有兩個(gè)參數(shù)银受,第一個(gè)參數(shù) boxes 表示目標(biāo)檢測(cè)過(guò)程中獲得的所有矩形框。第二個(gè)參數(shù) threshold 表示事先定義的一個(gè)閾值鸦采,當(dāng)兩個(gè)矩形框重疊的面積超過(guò)這個(gè)閾值時(shí)我們將剔除其中一個(gè)矩形框宾巍。
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")
在目標(biāo)檢測(cè)過(guò)程中我們的算法有可能沒(méi)有檢測(cè)到任何目標(biāo),那么這就表示在圖片中沒(méi)有用于標(biāo)記目標(biāo)的矩形框渔伯。所以下面的代碼第 2 行我們將用一個(gè) if 語(yǔ)句來(lái)判斷輸入的 boxes 的數(shù)量是否為 0顶霞,如果矩形框的數(shù)量為 0 則函數(shù)返回一個(gè)空列表。然后我們還需要將 boxes 轉(zhuǎn)換為 NumPy 數(shù)組并且將其中每個(gè)元素轉(zhuǎn)換為 float 浮點(diǎn)類(lèi)型(見(jiàn)代碼第 5 行)咱旱,因?yàn)楹竺嫖覀冃枰眠@些元素進(jìn)行算術(shù)運(yùn)算确丢。
代碼第 7 到 10 行,我們使用切片方法獲取每一個(gè) boxes 內(nèi)的元素并將其分別保存在 x1吐限、y1鲜侥、w1、h1 這四個(gè)數(shù)組中诸典。這四個(gè)數(shù)組中分別保存了每一個(gè) boxes 中的第一至四元素描函。x1 表示矩形框左上角頂點(diǎn)的橫坐標(biāo),y1 表示矩形框左上角頂點(diǎn)的縱坐標(biāo)狐粱,w1 是矩形框的寬舀寓,h1 是矩形框的高。 代碼第 11肌蜻、12 行我們使用這四個(gè)數(shù)組計(jì)算得出每個(gè)矩形框的右下角頂點(diǎn)橫坐標(biāo)的集合 x2 和 縱坐標(biāo)的集合 y2互墓。
代碼第 14 行表示我們需要計(jì)算每個(gè)矩形框的面積。這里分別將 w1 和 h1 加 1 是為了避免使用 area 計(jì)算 IoU 時(shí)分母為零的情況發(fā)生蒋搜。我們還初始化了代碼 15 行中的 temp 列表用于臨時(shí)存儲(chǔ)值篡撵。
代碼 17 行我們使用 NumPy 的 argsort 方法將 h1 中的元素從小到大排序并返回每個(gè)元素在 h1 中的下標(biāo),需要注意的是 idxs 中的元素是 h1 中元素的下標(biāo)豆挽,這些下標(biāo)排列的順序是按照其對(duì)應(yīng) h1 中元素的大小排列的育谬。
接下來(lái)我們使用 while 循環(huán)遍歷 idxs,當(dāng) idxs 中沒(méi)有元素時(shí)終止循環(huán)帮哈。代碼 20 到 22 行我們獲取 idxs 中最后一個(gè)元素并將其添加到 temp 中膛檀。
代碼 24 行我們使用 np.maximum 方法將 x1[i] 與 boxes 中其他矩形框的左上角橫坐標(biāo)兩兩比較, 將較大的值保存在數(shù)組 x1_m 中。同樣代碼 25 行將 y1[i] 與 boxes 中其他矩形框的左上角縱坐標(biāo)兩兩比較咖刃,將較大的值保存在數(shù)組 y1_m 中泳炉。兩個(gè)矩形框重疊的部分是矩形,所以這一步的目的是為了找到這個(gè)重疊矩形的左上角頂點(diǎn)嚎杨。同理胡桃,27、28 兩行代碼的目的是為了找出這個(gè)重疊矩形的右下角頂點(diǎn)磕潮。我們使用 np.minimum 將 x2[i] 與 boxes 中其他矩形框的右下角橫坐標(biāo)兩兩比較, 將較小的值保存在數(shù)組 x2_m 中容贝。同樣的再將 y2[i] 與 boxes 中其他矩形框的右下角縱坐標(biāo)兩兩比較自脯,將較小的值保存在數(shù)組 y2_m 中。
有了重疊矩形的兩個(gè)頂點(diǎn)坐標(biāo)斤富,我們就可以計(jì)算矩形的寬和高膏潮,進(jìn)而可以計(jì)算矩形的面積。第 30满力,31 行代碼是分別計(jì)算矩形的寬和高焕参,我們使用 np.maximum 方法來(lái)剔除掉沒(méi)有相交的矩形。如果兩個(gè)矩形框相交油额,則 x2_m - x1_m + 1 和 y2_m - y1_m + 1 大于零叠纷,如果兩個(gè)矩形框不相交則這兩個(gè)值小于零。
33 行代碼表示計(jì)算重疊矩形面積和 area 中的面積的比值 over潦嘶,這一步和計(jì)算 IoU 是等效的涩嚣。35 行代碼的目的是為了剔除重疊的矩形框。我們使用 np.where 判斷 over 中的元素是否大于設(shè)定的閾值 threshold掂僵,如果大于這個(gè)閾值則返回這個(gè)元素的下標(biāo)航厚。接著使用 np.concatenate 方法將 idxs 中最后的元素和返回的下標(biāo)拼接在一起。最后通過(guò) np.delete 方法從 idxs 中刪除這些下標(biāo)對(duì)應(yīng)的元素锰蓬。
通過(guò)上一步我們刪除了與 idxs 中 last 對(duì)應(yīng)的矩形框相互重疊且面積大于閾值的矩形框(同時(shí)也從 idxs 中刪除最后一個(gè)元素)幔睬,然后進(jìn)入下一個(gè)循環(huán)直到 idxs 中的元素個(gè)數(shù)為 0,最后我們通過(guò)下面一行代碼返回挑選后的矩形框芹扭,同時(shí)我們需要使用 astype 方法將 boxes 中的浮點(diǎn)類(lèi)型轉(zhuǎn)換為整數(shù)類(lèi)型麻顶。
接下來(lái),我們將使用非極大值抑制的函數(shù)來(lái)剔除檢測(cè)結(jié)果中多余的窗口冯勉。首先我們創(chuàng)建一個(gè) img_path 變量用于保存圖片路徑澈蚌,然后調(diào)用 run 函數(shù),run 函數(shù)將返回 2 個(gè)值 roi_loc 和 image灼狰。roi_loc 里保存了一個(gè)或多個(gè)矩形框的頂點(diǎn)坐標(biāo)宛瞄、寬和高,這些矩形框內(nèi)的區(qū)域被模型認(rèn)為是有人存在的。image 是縮放后輸入圖片份汗,我們將在這張圖片上用矩形框標(biāo)記出行人盈电。
img_path = "man.jpg"
roi_loc, image = run(img_path=img_path)
接下來(lái)我們將使用 cv2.rectangle 方法在 image 上畫(huà)出矩形框,這些矩形框是通過(guò) NMS 方法獲得的沒(méi)有重疊的矩形框杯活。
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)
最后我們通過(guò)下面的代碼來(lái)顯示檢測(cè)結(jié)果匆帚,首先從 matplotlib 導(dǎo)入 pyplot,然后我們使用 %matplotlib inline 魔法函數(shù)讓圖片在頁(yè)面中顯示旁钧。我們使用 plt.figure(figsize = (10,10)) 創(chuàng)建一個(gè)寬和高都是 10 英寸的圖像實(shí)例吸重。然后使用 resized[:,:,::-1] 切片方法將圖片通道的順序調(diào)轉(zhuǎn),最后使用 plt.imshow 在頁(yè)面中呈現(xiàn)繪圖后的結(jié)果歪今。
from matplotlib import pyplot as plt
%matplotlib inline
plt.figure(figsize = (10,10))
image = image[:,:,::-1]
plt.imshow(image)
如果腳本運(yùn)行正常我們能看到類(lèi)似下圖結(jié)果嚎幸,相較于左邊沒(méi)有使用 NMS 方法獲得的檢測(cè)圖片,我們剔除了冗余的矩形框使得目標(biāo)只被一個(gè)矩形框標(biāo)記寄猩。
將 img_path = "man.jpg" 修改為 img_path = "people.jpg" 我們將得到類(lèi)似下圖結(jié)果嫉晶,同樣的通過(guò)使用 NMS 方法我們剔除了大部分的矩形框。