目錄
一、什么是NMS
二、NMS及其優化版本
1、soft NMS
2、GIoU NMS
3、DIoU NMS
4、CIoU NMS
正文
一、什么是NMS
1、定義:
非極大值抑制算法NMS廣泛應用於目標檢測算法,其目的是為了消除多余的候選框,找到最佳的物體檢測位置。
2、原理:
使用深度學習模型檢測出的目標都有多個框,如下圖,針對每一個被檢測目標,為了得到效果最好的那一個,需要使用一定的過濾技術把多余的框過濾掉。NMS應運而生。
現,假設有一個候選BOXES的集合B和其對應的SCORES集合S:
1、找出分數最高的那個框M;
2、將M對應的BOX從B中刪除;
3、將刪除的BOX添加到集合D中;
4、從B中刪除與M對應的BOX重疊區域大於閾值Nt的其他框;
5、重復上述步驟1到4。
偽代碼如下:
其中Si可表述成:
源代碼如下:
1、在FastRCNN中的python實現:

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 = scores.argsort()[::-1] keep = [] while order.size>0: i=order[0] keep.append(i) 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:]]) w=np.maximum(0.,xx2-xx1+1) h=np.maximum(0.,yy2-yy1+1) inter=w*h iou=inter/(areas[i]+areas[order[1:]]-inter) inds=np.where(iou<=thresh)[0] order=order[inds+1] return keep
2、在MaskRCNN中的python實現:

def non_max_suppression(boxes,scores,threshold): ''' 保留boxes的索引 boxes:[N,(y1,x1,y2,x2)],(y2,x2)可能會超過box的邊界 scores:box分數的一數組 threshold:Float型,用於過濾IoU的閾值 ''' assert boxes.shape[0]>0 if boxes.dtpye.kind!='f': boxes=boxes.astype(np.float32) #計算box面積 y1=boxes[:,0] x1=boxes[:,1] y2=boxes[:,2] y3=boxes[:,3] area=(y2-y1)*(x2-x1) #獲取根據分數排序的boxes的索引(最高的排在對前面) ixs=scores.argsort()[::-] pick=[] while len(ixs)>0: i=ixs[0] pick.append(i) iou=compute_iou(boxes[i],boxes[ixs[1:]],area[i],area[ixs[1:]]) remove_ixs=np.where(iou>threshold)[0]+1 ixs=np.delete(ixs,remove_ixs) ixs=np.delete(ixs,0) return np.array(pick,dtype=np.int32)
3、C++實現

static void sort(int n, const float* x, int* indices) { // 排序函數(降序排序),排序后進行交換的是indices中的數據 // n:排序總數// x:帶排序數// indices:初始為0~n-1數目 int i, j; for (i = 0; i < n; i++) for (j = i + 1; j < n; j++) { if (x[indices[j]] > x[indices[i]]) { //float x_tmp = x[i]; int index_tmp = indices[i]; //x[i] = x[j]; indices[i] = indices[j]; //x[j] = x_tmp; indices[j] = index_tmp; } } } int nonMaximumSuppression(int numBoxes, const CvPoint *points, const CvPoint *oppositePoints, const float *score, float overlapThreshold, int *numBoxesOut, CvPoint **pointsOut, CvPoint **oppositePointsOut, float **scoreOut) { // numBoxes:窗口數目// points:窗口左上角坐標點// oppositePoints:窗口右下角坐標點 // score:窗口得分// overlapThreshold:重疊閾值控制// numBoxesOut:輸出窗口數目 // pointsOut:輸出窗口左上角坐標點// oppositePoints:輸出窗口右下角坐標點 // scoreOut:輸出窗口得分 int i, j, index; float* box_area = (float*)malloc(numBoxes * sizeof(float)); // 定義窗口面積變量並分配空間 int* indices = (int*)malloc(numBoxes * sizeof(int)); // 定義窗口索引並分配空間 int* is_suppressed = (int*)malloc(numBoxes * sizeof(int)); // 定義是否抑制表標志並分配空間 // 初始化indices、is_supperssed、box_area信息 for (i = 0; i < numBoxes; i++) { indices[i] = i; is_suppressed[i] = 0; box_area[i] = (float)( (oppositePoints[i].x - points[i].x + 1) * (oppositePoints[i].y - points[i].y + 1)); } // 對輸入窗口按照分數比值進行排序,排序后的編號放在indices中 sort(numBoxes, score, indices); for (i = 0; i < numBoxes; i++) // 循環所有窗口 { if (!is_suppressed[indices[i]]) // 判斷窗口是否被抑制 { for (j = i + 1; j < numBoxes; j++) // 循環當前窗口之后的窗口 { if (!is_suppressed[indices[j]]) // 判斷窗口是否被抑制 { int x1max = max(points[indices[i]].x, points[indices[j]].x); // 求兩個窗口左上角x坐標最大值 int x2min = min(oppositePoints[indices[i]].x, oppositePoints[indices[j]].x); // 求兩個窗口右下角x坐標最小值 int y1max = max(points[indices[i]].y, points[indices[j]].y); // 求兩個窗口左上角y坐標最大值 int y2min = min(oppositePoints[indices[i]].y, oppositePoints[indices[j]].y); // 求兩個窗口右下角y坐標最小值 int overlapWidth = x2min - x1max + 1; // 計算兩矩形重疊的寬度 int overlapHeight = y2min - y1max + 1; // 計算兩矩形重疊的高度 if (overlapWidth > 0 && overlapHeight > 0) { float overlapPart = (overlapWidth * overlapHeight) / box_area[indices[j]]; // 計算重疊的比率 if (overlapPart > overlapThreshold) // 判斷重疊比率是否超過重疊閾值 { is_suppressed[indices[j]] = 1; // 將窗口j標記為抑制 } } } } } } *numBoxesOut = 0; // 初始化輸出窗口數目0 for (i = 0; i < numBoxes; i++) { if (!is_suppressed[i]) (*numBoxesOut)++; // 統計輸出窗口數目 } *pointsOut = (CvPoint *)malloc((*numBoxesOut) * sizeof(CvPoint)); // 分配輸出窗口左上角坐標空間 *oppositePointsOut = (CvPoint *)malloc((*numBoxesOut) * sizeof(CvPoint)); // 分配輸出窗口右下角坐標空間 *scoreOut = (float *)malloc((*numBoxesOut) * sizeof(float)); // 分配輸出窗口得分空間 index = 0; for (i = 0; i < numBoxes; i++) // 遍歷所有輸入窗口 { if (!is_suppressed[indices[i]]) // 將未發生抑制的窗口信息保存到輸出信息中 { (*pointsOut)[index].x = points[indices[i]].x; (*pointsOut)[index].y = points[indices[i]].y; (*oppositePointsOut)[index].x = oppositePoints[indices[i]].x; (*oppositePointsOut)[index].y = oppositePoints[indices[i]].y; (*scoreOut)[index] = score[indices[i]]; index++; } } free(indices); // 釋放indices空間 free(box_area); // 釋放box_area空間 free(is_suppressed); // 釋放is_suppressed空間 return LATENT_SVM_OK; }
二、NMS及其優化版本
1、soft NMS
NMS能解決大部分的重疊問題,但如下圖的情況就無法解決,紅色框和綠色框是當前的檢測結果,二者的得分分別是0.95和0.80。如果按照傳統的NMS進行處理,首先選中得分最高的紅色框,然后綠色框就會因為與之重疊面積過大而被刪掉。另一方面,NMS的閾值也不太容易確定,設小了會出現下圖的情況(綠色框因為和紅色框重疊面積較大而被刪掉),設置過高又容易增大誤檢。
思路:不要簡單粗暴地刪除所有IOU大於閾值的框,而是降低其置信度。
偽代碼如下:
NMS可以描述如下:將IOU大於閾值的窗口的得分全部置為0。
SoftNMS改進有兩種形式
一種是線性加權的:
一種是高斯加權的:
兩種方法的思路都是:M為當前得分最高框,Bi是待處理框,和M的IOU越大,Bi的得分就下降的越厲害。

def cpu_soft_nms(np.ndarray[float, ndim=2] boxes, float sigma=0.5, float Nt=0.3, float threshold=0.001, unsigned int method=0): cdef unsigned int N = boxes.shape[0] cdef float iw, ih, box_area cdef float ua cdef int pos = 0 cdef float maxscore = 0 cdef int maxpos = 0 cdef float x1,x2,y1,y2,tx1,tx2,ty1,ty2,ts,area,weight,ov for i in range(N): maxscore = boxes[i, 4] maxpos = i tx1 = boxes[i,0] ty1 = boxes[i,1] tx2 = boxes[i,2] ty2 = boxes[i,3] ts = boxes[i,4] pos = i + 1 # get max box while pos < N: if maxscore < boxes[pos, 4]: maxscore = boxes[pos, 4] maxpos = pos pos = pos + 1 # add max box as a detection boxes[i,0] = boxes[maxpos,0] boxes[i,1] = boxes[maxpos,1] boxes[i,2] = boxes[maxpos,2] boxes[i,3] = boxes[maxpos,3] boxes[i,4] = boxes[maxpos,4] # swap ith box with position of max box boxes[maxpos,0] = tx1 boxes[maxpos,1] = ty1 boxes[maxpos,2] = tx2 boxes[maxpos,3] = ty2 boxes[maxpos,4] = ts tx1 = boxes[i,0] ty1 = boxes[i,1] tx2 = boxes[i,2] ty2 = boxes[i,3] ts = boxes[i,4] pos = i + 1 # NMS iterations, note that N changes if detection boxes fall below threshold while pos < N: x1 = boxes[pos, 0] y1 = boxes[pos, 1] x2 = boxes[pos, 2] y2 = boxes[pos, 3] s = boxes[pos, 4] area = (x2 - x1 + 1) * (y2 - y1 + 1) iw = (min(tx2, x2) - max(tx1, x1) + 1) if iw > 0: ih = (min(ty2, y2) - max(ty1, y1) + 1) if ih > 0: ua = float((tx2 - tx1 + 1) * (ty2 - ty1 + 1) + area - iw * ih) ov = iw * ih / ua #iou between max box and detection box if method == 1: # linear if ov > Nt: weight = 1 - ov else: weight = 1 elif method == 2: # gaussian weight = np.exp(-(ov * ov)/sigma) else: # original NMS if ov > Nt: weight = 0 else: weight = 1 boxes[pos, 4] = weight*boxes[pos, 4] # if box score falls below threshold, discard the box by swapping with last box # update N if boxes[pos, 4] < threshold: boxes[pos,0] = boxes[N-1, 0] boxes[pos,1] = boxes[N-1, 1] boxes[pos,2] = boxes[N-1, 2] boxes[pos,3] = boxes[N-1, 3] boxes[pos,4] = boxes[N-1, 4] N = N - 1 pos = pos - 1 pos = pos + 1 keep = [i for i in range(N)] return keep
解釋如下:
如上圖,假如還檢測出了3號框,而我們的最終目標是檢測出1號和2號框,並且剔除3號框,原始的nms只會檢測出一個1號框並剔除2號框和3號框,而softnms算法可以對1、2、3號檢測狂進行置信度排序,可以知道這三個框的置信度從大到小的順序依次為:1-》2-》3(由於是使用了懲罰,所有可以獲得這種大小關系),如果我們再選擇了合適的置信度閾值,就可以保留1號和2號,同時剔除3號,實現我們的功能。
遺留問題:
置信度的閾值設置目前還是手工設置,這依然存在很大局限性,所以還有改進的空間。
2、GIoU NMS
在IOU LOSS上增加一個懲罰項,C為包圍預測框B和Bgt的最小區域大小。BBOX距離越大,懲罰項越大。
在包含的情況下,GIOU退化為IOU。
GIOU需要更多的迭代次數來收斂;
3、DIoU NMS
在YOLOV3上使用DIOU替換IOU,能提升5.9個mAp
添加一個懲罰項,用於最小化兩個BBOX的中心點距離,不僅考慮了重疊區域,還考慮了中心點距離。
4、CIOU NMS(Complete IOU )
除了考慮重疊區域,中心點距離,還加入了長寬比。
Rciou=ro^2(b,bgt)/c^2+alpha*v
v=4/pi^2*(arctan(w_gt/h_gt)-arctan(w/h))^2 :用來度量長寬比的相似性
損失函數:Lciou=1-IOU+Rciou
參考鏈接: