NMS(非极大值抑制)一步步详解
要达到的效果
首先,我们要知道NMS要实现的功能是什么,知道它要做什么,再去了解它是怎么做到的。
我们现在这里有一张图片,模型算法识别到有五个候选框,NMS要做的,就是将每一个目标中,只留下一个候选框。
这种我们用肉眼看,一眼就能看出,哪个anchor需要留下,哪个anchor需要剔除,但是代码中需要用算法来计算,也就是NMS(非极大值抑制)。
好,首先我们根据图片中的5个anchor,建立一组数据,用来做NMS的计算。
a = np.array([[191, 89, 413, 420, 0.80], # 0
[281, 152, 573, 510, 0.99], # 1
[446, 294, 614, 471, 0.65], # 2
[50, 453, 183, 621, 0.98], # 3
[109, 474, 209, 635, 0.78]]) # 4
这一组数据中的几个元素解释为:[左上角x, 左上角y, 右下角x, 右下角y, score]。
源码
再看一下NMS算法的源码,我们再根据源码一步步地来做解释:
def nms(boxes, thresh):
"""Pure Python NMS baseline."""
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
scores = boxes[:, 4]
# 计算每一个anchor的面积
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
# 按照从小到大排序后返回下标,然后顺序取反,即从大到小对应的下标
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
# 置信度高的预测框即当前框与其他框的交集
# 选择的区域就是取最大的x1, y1和最小的 x2, y2
xx1 = np.maximum(x1[i], x1[order[1:]]) # 这个就是较差区域的左上角的坐标,下面以此类推
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
# 计算交叉区域的面积,就是用当前的anchor与其它的anchor计算,是否有相交的面积,如果有,那相交的面积是多少
w = np.maximum(0.0, xx2 - xx1 + 1) # 计算w
h = np.maximum(0.0, yy2 - yy1 + 1) # 计算h
inter = w * h # 交叉面积
# 计算IOU, 相交区域 / (当前区域 + 某区域面积 - 相交区域面积)
ovr = inter / (areas[i] + areas[order[1:]] - inter)
# 保留IOU小于阈值的框
inds = np.where(ovr <= thresh)[0]
# 因为ovr数组的长度比order数组少一个,所以这里要将所有下标后移一位
order = order[inds + 1]
return boxes[keep]
if __name__ == "__main__":
a = np.array([[191, 89, 413, 420, 0.80], # 0
[281, 152, 573, 510, 0.99], # 1
[446, 294, 614, 471, 0.65], # 2
[50, 453, 183, 621, 0.98], # 3
[109, 474, 209, 635, 0.78]]) # 4
nms_result = nms(a, 0.2)
源码一步步详解
1)首先看代码开头
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
scores = boxes[:, 4]
我们刚才讲过,数据中的几个元素解释为:[左上角x, 左上角y, 右下角x, 右下角y, score]。
那么这里的代码,其实就是将x值、y值、score都分别取出来而已。
可以得到结果:
x1= [191. 281. 446. 50. 109.]
y1= [ 89. 152. 294. 453. 474.]
x2= [413. 573. 614. 183. 209.]
y2= [420. 510. 471. 621. 635.]
scores= [0.8 0.99 0.65 0.98 0.78]
2)然后再计算一下,每一个anchor的面积
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
因为x1,y1为左上角的坐标点,x2,y2为右下角的坐标点,所以 x2-x1相当于求宽,y2-y1相当于求高,宽乘以高就能求出anchor的面积。
结果:
areas= [ 74036. 105187. 30082. 22646. 16362.]
3)根据score从大到小排序
order = scores.argsort()[::-1]
argsort() 这个函数其实是求的从小到大排序,且返回的是下标。例如在我的数据中,从小到大排序应该是[0.65, 0.78, 0.80, 0.98, 0.99],但是其返回的是下标,所以score.argsort()=[2, 4, 0, 3, 1]。
然后再用[::-1]反序排列,就可以得到从大到小的下标顺序,所以最后得到的结果应该为:
order= [1 3 0 4 2]
4)遍历scores
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
这里遍历的是order,也就是已经被从大到小排序过的scores下标。
因为已经做好了排序,所以第一个必定是评分最高的anchor,默认它为正确的anchor,将其下标保存至keep中。
5)求当前框与其他框的交集
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
当前框即为x1[i], y1[i], x2[i], y2[i]。因为i为order[0],所以order从1开始往后所有的值,都为其他框。
而又因为order是经过从大到小排序的,所以order[0]应该就是评分为0.99的anchor数据。
这里要计算的是,将当前框与其他所有框一个个做对比,求左上角坐标更大的值,右下角坐标更小的值。
从图中可以看到,经过计算之后,xx1,yy1,xx2,yy2就可以求出两个anchor相交部分的顶点坐标,然后用当前框去一一计算,算出它与其它每一个框的相交部分。
xx1= [281. 281. 281. 446.]
yy1= [453. 152. 474. 294.]
xx2= [183. 413. 209. 573.]
yy2= [510. 420. 510. 471.]
6)计算每一个anchor相交部分的面积
w = np.maximum(0.0, xx2 - xx1 + 1) # 计算w
h = np.maximum(0.0, yy2 - yy1 + 1) # 计算h
inter = w * h # 交叉面积
从第5)步可以得知,
xx1,yy1,xx2,yy2为两个anchor的相交部分的顶点坐标,也就是左上和右下的顶点坐标。
跟第2)步相同,求出相交部分的宽高,即可求出相交面积。但因为可能某些anchor并不相交,所以其宽高可能为负数,所以用np.maximum来保证w,h最小也为0。
inter= [0. 35777. 0. 22784.]
这里可以看到,一共是求出了4个交叉面积,其中两个均为0,说明没有相交的面积。
需要注意的是,因为我们这里用的是评分为0.99的当前框与其他框来做的计算,所以inter的值中只有两个相交面积,这是正确的。
7)计算IOU
ovr = inter / (areas[i] + areas[order[1:]] - inter)
这里涉及到一个算法,IOU的计算。IOU即交并比,是目标检测中衡量目标检测算法准确度的一个重要指标,顾名思义,即交集与并集的比值。
代码中的inter就是第6)步求出的交集,并集则是将score为0.99的当前框,加上score为0.80的其它框面积想加,再减去两个框中间的交集。
用当前框0.99与其它每一个其他框都一一计算,得到ovr的结果为:
ovr= [0. 0.24941093 0. 0.20255145]
8)计算阈值
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1]
因为我们需要剔除不符合要求的anchor,所以设定一个阈值,当IOU大于这个阈值时,就认为它们与当前框是属于同一个目标的anchor,而因为当前框的score最大,所以其他框都会被剔除。
这里我们将阈值thresh设为0.2,那么ovr经过计算剔除后,得到结果:
inds= [0 2]
注意,这里返回的是下标。因为可能一张图片中有多个目标多个anchor,所以再移动下标后,进行下一次while循环,继续计算。
根据第3)步order的计算结果,移动下标后:
order= [3 4]
9)返回结果
return boxes[keep]
计算完所有的while之后,keep中已经保存下所有目标的anchor下标了,再把这个下标用于anchor的数据boxes中,就可以得到最终结果:
nms_result= [[281. 152. 573. 510. 0.99]
[ 50. 453. 183. 621. 0.98]]
OVER!