0-目標檢測模型的基礎

嵌入式視覺 發表於 2022-12-01

前言

邊界框:在⽬標檢測⾥,我們通常使⽤邊界框(bounding box,縮寫是 bbox)來描述⽬標位置。邊界框是⼀個矩形框,可以由矩形左上⻆的 xy 軸座標與右下⻆的 xy 軸座標確定。

檢測網路中的一些術語解釋:

  1. backbone:翻譯為主幹網路,主要指用來做特徵提取作用的網路,早期分類網路 VGGResNet 等去掉用於分類的全連線層的部分就是 backbone 網路。
  2. neck: 指放在 backbonehead 之間的網路,作用是更好的融合/利用 backbone 提取的 feature,可以理解為特徵增強模組,典型的 neck 是如 FPN 結構。
  3. head:檢測頭,輸出想要結果(分類+定位)的網路,放在模型最後。如 YOLO 使用特定維度的 conv 獲取目標的類別和 bbox 資訊。

一,anchor box

⽬標檢測演算法通常會在輸⼊影像中取樣⼤量的區域,然後判斷這些區域中是否包含我們感興趣的⽬標,並調整區域邊緣從而更準確地預測⽬標的真實邊界框(ground-truth bounding box)。不同的模型使⽤的區域取樣⽅法可能不同。兩階段檢測模型常用的⼀種⽅法是:以每個畫素為中⼼⽣成多個⼤小和寬⾼⽐(aspect ratio)不同的邊界框。這些邊界框被稱為錨框(anchor box)。

Faster RCNN 模型中,每個畫素都生成 9 個大小和寬高比都不同的 anchors。在程式碼中,anchors 是一組由 generate_anchors.py 生成的矩形框列表。其中每行的 4 個值 (x1,y1,x2,y2) 表示矩形左上和右下角點座標。9 個矩形共有 3 種形狀,長寬比為大約為 {1:1, 1:2, 2:1} 三種, 實際上透過 anchors 就引入了檢測中常用到的多尺度方法。generate_anchors.py 的程式碼如下:

注意,這裡生成的只是 base anchors,其中一個 框的左上角座標為 (0,0) 座標(特徵圖左上角)的 9 個 anchor,後續還需網格化(meshgrid)生成其他 anchor。同一個 scale,但是不同的 anchor ratios 生成的 anchors 面積理論上是要一樣的。

import numpy as np
import six
from six import __init__  # 相容python2和python3模組


def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2],
                         anchor_scales=[8, 16, 32]):
    """Generate base anchors by enumerating aspect ratio and scales.

    Args:
        base_size (number): The width and the height of the reference window.
        ratios (list of floats): anchor 的寬高比
        anchor_scales (list of numbers): anchor 的尺度

    Returns: Base anchors in a single-level feature maps.`(R, 4)`.
        bounding box is `(x_{min}, y_{min}, x_{max}, y_{max})`
    """
    import numpy as np
    py = base_size / 2.
    px = base_size / 2.

    anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4),
                           dtype=np.float32)
    for i in six.moves.range(len(ratios)):
        for j in six.moves.range(len(anchor_scales)):
            // 乘以感受野值,得到縮放後的 anchor 大小
            h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
            w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i])

            index = i * len(anchor_scales) + j
            anchor_base[index, 0] = px - w / 2.
            anchor_base[index, 1] = py - h / 2.

            anchor_base[index, 2] = px + h / 2.
            anchor_base[index, 3] = py + w / 2.
    return anchor_base


# test
if __name__ == "__main__":
    bbox_list = generate_anchor_base()
    print(bbox_list)

程式執行輸出如下:

[[ -82.50967 -37.254833 53.254833 98.50967 ]
[-173.01933 -82.50967 98.50967 189.01933 ]
[-354.03867 -173.01933 189.01933 370.03867 ]
[ -56. -56. 72. 72. ]
[-120. -120. 136. 136. ]
[-248. -248. 264. 264. ]
[ -37.254833 -82.50967 98.50967 53.254833]
[ -82.50967 -173.01933 189.01933 98.50967 ]
[-173.01933 -354.03867 370.03867 189.01933 ]]

二,IOU

交併比(Intersection-over-Union,IoU),目標檢測中使用的一個概念,是模型產生的候選框(candidate bound)與原標記框(ground truth bound)的交疊率,即它們的交集與並集的比值。最理想情況是完全重疊,即比值為 1。計算公式如下:

IOU計算公式

程式碼實現如下:

# _*_ coding:utf-8 _*_
# 計算iou

"""
bbox的資料結構為(xmin,ymin,xmax,ymax)--(x1,y1,x2,y2),
每個bounding box的左上角和右下角的座標
輸入:
    bbox1, bbox2: Single numpy bounding box, Shape: [4]
輸出:
    iou值
"""
import numpy as np
import cv2

def iou(bbox1, bbox2):
    """
    計算兩個bbox(兩框的交併比)的iou值
    :param bbox1: (x1,y1,x2,y2), type: ndarray or list
    :param bbox2: (x1,y1,x2,y2), type: ndarray or list
    :return: iou, type float
    """
    if type(bbox1) or type(bbox2) != 'ndarray':
        bbox1 = np.array(bbox1)
        bbox2 = np.array(bbox2)

    assert bbox1.size == 4 and bbox2.size == 4, "bounding box coordinate size must be 4"
    xx1 = np.max((bbox1[0], bbox2[0]))
    yy1 = np.min((bbox1[1], bbox2[1]))
    xx2 = np.max((bbox1[2], bbox2[2]))
    yy2 = np.min((bbox1[3], bbox2[3]))
    bwidth = xx2 - xx1
    bheight = yy2 - yy1
    area = bwidth * bheight  # 求兩個矩形框的交集
    union = (bbox1[2] - bbox1[0])*(bbox1[3] - bbox1[1]) + (bbox2[2] - bbox2[0])*(bbox2[3] - bbox2[1]) - area  # 求兩個矩形框的並集
    iou = area / union

    return iou


if __name__=='__main__':
    rect1 = (461, 97, 599, 237)
    # (top, left, bottom, right)
    rect2 = (522, 127, 702, 257)
    iou_ret = round(iou(rect1, rect2), 3) # 保留3位小數
    print(iou_ret)

    # Create a black image
    img=np.zeros((720,720,3), np.uint8)
    cv2.namedWindow('iou_rectangle')
    """
    cv2.rectangle 的 pt1 和 pt2 引數分別代表矩形的左上角和右下角兩個點,
    coordinates for the bounding box vertices need to be integers if they are in a tuple,
    and they need to be in the order of (left, top) and (right, bottom). 
    Or, equivalently, (xmin, ymin) and (xmax, ymax).
    """
    cv2.rectangle(img,(461, 97),(599, 237),(0,255,0),3)
    cv2.rectangle(img,(522, 127),(702, 257),(0,255,0),3)
    font  = cv2.FONT_HERSHEY_SIMPLEX
    cv2.putText(img, 'IoU is ' + str(iou_ret), (341,400), font, 1,(255,255,255),1)
    cv2.imshow('iou_rectangle', img)
    cv2.waitKey(0)

程式碼輸出結果如下所示:

程式執行結果

三,Focal Loss

Focal Loss 是在二分類問題的交叉熵(CE)損失函式的基礎上引入的,所以需要先學習下交叉熵損失的定義。

3.1,Cross Entropy

在深度學習中我們常使用交叉熵來作為分類任務中訓練資料分佈和模型預測結果分佈間的代價函式。對於同一個離散型隨機變數 \(\textrm{x}\) 有兩個單獨的機率分佈 \(P(x)\)\(Q(x)\),其交叉熵定義為:

P 表示真實分佈, Q 表示預測分佈。

\[H(P,Q) = \mathbb{E}_{\textrm{x}\sim P} log Q(x)= -\sum_{i}P(x_i)logQ(x_i) \tag{1} \]

但在實際計算中,我們通常不這樣寫,因為不直觀。在深度學習中,以二分類問題為例,其交叉熵損失(CE)函式如下:

\[Loss = L(y, p) = -ylog(p)-(1-y)log(1-p) \tag{2} \]

其中 \(p\) 表示當預測樣本等於 \(1\) 的機率,則 \(1-p\) 表示樣本等於 \(0\) 的預測機率。因為是二分類,所以樣本標籤 \(y\) 取值為 \(\{1,0\}\),上式可縮寫至如下:

\[CE = \left\{\begin{matrix} -log(p), & if \quad y=1 \\ -log(1-p), & if\quad y=0 \tag{3} \end{matrix}\right. \]

為了方便,用 \(p_t\) 代表 \(p\)\(p_t\) 定義如下:

\[p_t = \{\begin{matrix} p, & if \quad y=1\\ 1-p, & if\quad y=0 \end{matrix} \]

\((3)\)式可寫成:

\[CE(p, y) = CE(p_t) = -log(p_t) \tag{4} \]

前面的交叉熵損失計算都是針對單個樣本的,對於所有樣本,二分類的交叉熵損失計算如下:

\[L = \frac{1}{N}(\sum_{y_i = 1}^{m}-log(p)-\sum_{y_i = 0}^{n}log(1-p)) \]

其中 \(m\) 為正樣本個數,\(n\) 為負樣本個數,\(N\) 為樣本總數,\(m+n=N\)。當樣本類別不平衡時,損失函式 \(L\) 的分佈也會發生傾斜,如 \(m \ll n\) 時,負樣本的損失會在總損失占主導地位。又因為損失函式的傾斜,模型訓練過程中也會傾向於樣本多的類別,造成模型對少樣本類別的效能較差。

再衍生以下,對於所有樣本,多分類的交叉熵損失計算如下:

\[L = \frac{1}{N} \sum_i^N L_i = -\frac{1}{N}(\sum_i \sum_{c=1}^M y_{ic}log(p_{ic}) \]

其中,\(M\) 表示類別數量,\(y_{ic}\) 是符號函式,如果樣本 \(i\) 的真實類別等於 \(c\) 取值 1,否則取值 0; \(p_{ic}\) 表示樣本 \(i\) 預測為類別 \(c\) 的機率。

對於多分類問題,交叉熵損失一般會結合 softmax 啟用一起實現,PyTorch 程式碼如下,程式碼出自這裡


import numpy as np

# 交叉熵損失
class CrossEntropyLoss():
    """
    對最後一層的神經元輸出計算交叉熵損失
    """
    def __init__(self):
        self.X = None
        self.labels = None
    
    def __call__(self, X, labels):
        """
        引數:
            X: 模型最後fc層輸出
            labels: one hot標註,shape=(batch_size, num_class)
        """
        self.X = X
        self.labels = labels

        return self.forward(self.X)
    
    def forward(self, X):
        """
        計算交叉熵損失
        引數:
            X:最後一層神經元輸出,shape=(batch_size, C)
            label:資料onr-hot標註,shape=(batch_size, C)
        return:
            交叉熵loss
        """
        self.softmax_x = self.softmax(X)
        log_softmax = self.log_softmax(self.softmax_x)
        cross_entropy_loss = np.sum(-(self.labels * log_softmax), axis=1).mean()
        return cross_entropy_loss
    
    def backward(self):
        grad_x =  (self.softmax_x - self.labels)  # 返回的梯度需要除以batch_size
        return grad_x / self.X.shape[0]
        
    def log_softmax(self, softmax_x):
        """
        引數:
            softmax_x, 在經過softmax處理過的X
        return: 
            log_softmax處理後的結果shape = (m, C)
        """
        return np.log(softmax_x + 1e-5)
    
    def softmax(self, X):
        """
        根據輸入,返回softmax
        程式碼利用softmax函式的性質: softmax(x) = softmax(x + c)
        """
        batch_size = X.shape[0]
        # axis=1 表示在二維陣列中沿著橫軸進行取最大值的操作
        max_value = X.max(axis=1)
        #每一行減去自己本行最大的數字,防止取指數後出現inf,性質:softmax(x) = softmax(x + c)
        # 一定要新定義變數,不要用-=,否則會改變輸入X。因為在呼叫計算損失時,多次用到了softmax,input不能改變
        tmp = X - max_value.reshape(batch_size, 1)
        # 對每個數取指數
        exp_input = np.exp(tmp)  # shape=(m, n)
        # 求出每一行的和
        exp_sum = exp_input.sum(axis=1, keepdims=True)  # shape=(m, 1)
        return exp_input / exp_sum

3.2,Balanced Cross Entropy

對於正負樣本不平衡的問題,較為普遍的做法是引入 \(\alpha \in(0,1)\) 引數來解決,上面公式重寫如下:

\[CE(p_t) = -\alpha log(p_t) = \left\{\begin{matrix} -\alpha log(p), & if \quad y=1\\ -(1-\alpha)log(1-p), & if\quad y=0 \end{matrix}\right. \]

對於所有樣本,二分類的平衡交叉熵損失函式如下:

\[L = \frac{1}{N}(\sum_{y_i = 1}^{m}-\alpha log(p)-\sum_{y_i = 0}^{n}(1 - \alpha) log(1-p)) \]

其中 \(\frac{\alpha}{1-\alpha} = \frac{n}{m}\),即 \(\alpha\) 引數的值是根據正負樣本分佈比例來決定的,

3.3,Focal Loss Definition

雖然 \(\alpha\) 引數平衡了正負樣本(positive/negative examples),但是它並不能區分難易樣本(easy/hard examples),而實際上,目標檢測中大量的候選目標都是易分樣本。這些樣本的損失很低,但是由於難易樣本數量極不平衡,易分樣本的數量相對來講太多,最終主導了總的損失。而本文的作者認為,易分樣本(即,置信度高的樣本)對模型的提升效果非常小,模型應該主要關注與那些難分樣本(這個假設是有問題的,是 GHM 的主要改進物件)

Focal Loss 作者建議在交叉熵損失函式上加上一個調整因子(modulating factor\((1-p_t)^\gamma\),把高置信度 \(p\)(易分樣本)樣本的損失降低一些。Focal Loss 定義如下:

\[FL(p_t) = -(1-p_t)^\gamma log(p_t) = \{\begin{matrix} -(1-p)^\gamma log(p), & if \quad y=1\\ -p^\gamma log(1-p), & if\quad y=0 \end{matrix} \]

Focal Loss 有兩個性質:

  • 當樣本被錯誤分類且 \(p_t\) 值較小時,調製因子接近於 1loss 幾乎不受影響;當 \(p_t\) 接近於 1,調質因子(factor)也接近於 0容易分類樣本的損失被減少了權重,整體而言,相當於增加了分類不準確樣本在損失函式中的權重。
  • \(\gamma\) 引數平滑地調整容易樣本的權重下降率,當 \(\gamma = 0\) 時,Focal Loss 等同於 CE Loss\(\gamma\) 在增加,調製因子的作用也就增加,實驗證明 \(\gamma = 2\) 時,模型效果最好。

直觀地說,調製因子減少了簡單樣本的損失貢獻,並擴大了樣本獲得低損失的範圍。例如,當\(\gamma = 2\) 時,與 \(CE\) 相比,分類為 \(p_t = 0.9\) 的樣本的損耗將降低 100 倍,而當 \(p_t = 0.968\) 時,其損耗將降低 1000 倍。這反過來又增加了錯誤分類樣本的重要性(對於 \(pt≤0.5\)\(\gamma = 2\),其損失最多減少 4 倍)。在訓練過程關注物件的排序為正難 > 負難 > 正易 > 負易。

難易正負樣本

在實踐中,我們常採用帶 \(\alpha\)Focal Loss

\[FL(p_t) = -\alpha (1-p_t)^\gamma log(p_t) \]

作者在實驗中採用這種形式,發現它比非 \(\alpha\) 平衡形式(non-\(\alpha\)-balanced)的精確度稍有提高。實驗表明 \(\gamma\) 取 2,\(\alpha\) 取 0.25 的時候效果最佳。

網上有各種版本的 Focal Loss 實現程式碼,大多都是基於某個深度學習框架實現的,如 PytorchTensorFlow,我選取了一個較為清晰的程式碼作為參考,程式碼來自 這裡

後續有必要自己實現以下,有時間還要去看看 Caffe 的實現。

# -*- coding: utf-8 -*-
# @Author  : LG
from torch import nn
import torch
from torch.nn import functional as F

class focal_loss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2, num_classes = 3, size_average=True):
        """
        focal_loss損失函式, -α(1-yi)**γ *ce_loss(xi,yi)
        步驟詳細的實現了 focal_loss損失函式.
        :param alpha:   阿爾法α,類別權重.      當α是列表時,為各類別權重,當α為常數時,類別權重為[α, 1-α, 1-α, ....],常用於 目標檢測演算法中抑制背景類 , retainnet中設定為0.25
        :param gamma:   伽馬γ,難易樣本調節引數. retainnet中設定為2
        :param num_classes:     類別數量
        :param size_average:    損失計算方式,預設取均值
        """
        super(focal_loss,self).__init__()
        self.size_average = size_average
        if isinstance(alpha,list):
            assert len(alpha)==num_classes   # α可以以list方式輸入,size:[num_classes] 用於對不同類別精細地賦予權重
            print(" --- Focal_loss alpha = {}, 將對每一類權重進行精細化賦值 --- ".format(alpha))
            self.alpha = torch.Tensor(alpha)
        else:
            assert alpha<1   #如果α為一個常數,則降低第一類的影響,在目標檢測中為第一類
            print(" --- Focal_loss alpha = {} ,將對背景類進行衰減,請在目標檢測任務中使用 --- ".format(alpha))
            self.alpha = torch.zeros(num_classes)
            self.alpha[0] += alpha
            self.alpha[1:] += (1-alpha) # α 最終為 [ α, 1-α, 1-α, 1-α, 1-α, ...] size:[num_classes]

        self.gamma = gamma

    def forward(self, preds, labels):
        """
        focal_loss損失計算
        :param preds:   預測類別. size:[B,N,C] or [B,C]    分別對應與檢測與分類任務, B 批次, N檢測框數, C類別數
        :param labels:  實際類別. size:[B,N] or [B],為 one-hot 編碼格式
        :return:
        """
        # assert preds.dim()==2 and labels.dim()==1
        preds = preds.view(-1,preds.size(-1))
        self.alpha = self.alpha.to(preds.device)
        preds_logsoft = F.log_softmax(preds, dim=1) # log_softmax
        preds_softmax = torch.exp(preds_logsoft)    # softmax

        preds_softmax = preds_softmax.gather(1,labels.view(-1,1))
        preds_logsoft = preds_logsoft.gather(1,labels.view(-1,1))
        self.alpha = self.alpha.gather(0,labels.view(-1))
        loss = -torch.mul(torch.pow((1-preds_softmax), self.gamma), preds_logsoft)  # torch.pow((1-preds_softmax), self.gamma) 為focal loss中 (1-pt)**γ

        loss = torch.mul(self.alpha, loss.t())
        if self.size_average:
            loss = loss.mean()
        else:
            loss = loss.sum()
        return loss

mmdetection 框架給出的 focal loss 程式碼如下(有所刪減):

# This method is only for debugging
def py_sigmoid_focal_loss(pred,
                          target,
                          weight=None,
                          gamma=2.0,
                          alpha=0.25,
                          reduction='mean',
                          avg_factor=None):
    """PyTorch version of `Focal Loss <https://arxiv.org/abs/1708.02002>`_.
    Args:
        pred (torch.Tensor): The prediction with shape (N, C), C is the
            number of classes
        target (torch.Tensor): The learning label of the prediction.
        weight (torch.Tensor, optional): Sample-wise loss weight.
        gamma (float, optional): The gamma for calculating the modulating
            factor. Defaults to 2.0.
        alpha (float, optional): A balanced form for Focal Loss.
            Defaults to 0.25.
        reduction (str, optional): The method used to reduce the loss into
            a scalar. Defaults to 'mean'.
        avg_factor (int, optional): Average factor that is used to average
            the loss. Defaults to None.
    """
    pred_sigmoid = pred.sigmoid()
    target = target.type_as(pred)
    pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)
    focal_weight = (alpha * target + (1 - alpha) *
                    (1 - target)) * pt.pow(gamma)
    loss = F.binary_cross_entropy_with_logits(
        pred, target, reduction='none') * focal_weigh
    return loss

四,NMS

4.1,NMS 介紹

在目標檢測中,常會利用非極大值抑制演算法(NMS,non maximum suppression)對生成的大量候選框進行後處理,去除冗餘的候選框,得到最佳檢測框(bbox),以加快目標檢測的效,其本質思想搜素區域性最大值,抑制非極大值。許多目標檢測模型都利用到了 NMS 演算法,如 DPM,YOLO,SSD,Faster R-CNN 等。NMS過程如下圖所示:

NMS過程

以上圖為例,每個選出來的 Bounding Box 檢測框(即 BBox)用(x,y,h,w, confidence score,Pdog,Pcat)表示,confidence score 表示 backgroundforeground 的置信度得分,取值範圍[0,1]。Pdog, Pcat 分佈代表類別是狗和貓的機率。如果是 100 類的目標檢測模型,BBox 輸出向量為 5+100=105

4.2,NMS 演算法過程

NMS 的目的就是除掉重複的邊界框,其主要是透過迭代的形式,不斷地以最大得分的框去與其他框做 IoU 操作,並過濾那些 IoU 較大的框。

其實現的思想主要是將各個框的置信度進行排序,然後選擇其中置信度最高的框 A,將其作為標準選擇其他框,同時設定一個閾值,當其他框 B 與 A 的重合程度超過閾值就將 B 捨棄掉,然後在剩餘的框中選擇置信度最大的框,重複上述操作。多目標檢測的 NMS 演算法過程如下:

for object in all objects:
1. 將所有 bboxs 按照 confidence 排序,並標記當前 confidence 最大的 bbox,即要保留的 bbox;
2. 計算當前最大 confidence 對應的 bbox 和剩下所有 bboxIOU
3. 去除 IOU 大於設定閾值的 bbox,得到新的 bboxs
4. 對於新生下來的 bboxs,迴圈執行步驟 2、3,直到所有的 bbox 都滿足要求(即無法再移除 bbox)。

nms 的 python 程式碼如下

import numpy as np

def py_nms(bboxs, thresh):
    """Pure Python NMS baseline.注意,這裡的計算都是在矩陣層面上計算的
    greedily select boxes with high confidence and overlap with current maximum <= thresh
    rule out overlap >= thresh
    :param bboxs: [[x1, y1, x2, y2 score],] # ndarray, shape(-1,5)
    :param thresh: retain overlap < thresh
    :return: indexes to keep
    """
    if(bboxs) == 0:
        return [][]
    bboxs = npa.array(bboxs)
    # 計算 n 個候選框的面積大小
    x1 = bboxs[:,0]
    x2 = bboxs[:, 1]
    y1 = bboxs[:, 2]
    y2 = bboxs[:, 3]
    scores = bboxs[:, 4]
    areas = (x2 - x1 + 1)*(y2 - y1 + 1)

    # 1,對bboxs 按照置信度排序,獲取排序後的下標號,argsort 函式預設從小到大排序
    order = np.argsort(scores)  # order shape is (4,)
    picked_bboxs = []

    while order.size > 0:
        # 1, 保留當前 confidence 最大的 bbox加入到返回框列表中
        index = order[-1]
        picked_bboxs.append(bboxs[index]]

        # 2,計算當前 confidence 最大的 bbox 和剩下 bbox 的 IOU
        xx1 = np.maximum(x1[-1], x1[order[:-1]])
        xx2 = np.maximum(x2[-1], x2[order[:-1]])
        yy1 = np.maximum(y1[-1], y1[order[:-1]])
        yy1 = np.maximum(y2[-1], y2[order[:-1]])

        # 計算相交框的面積,注意矩形框不相交時 w 或 h 算出來會是負數,用0代替
        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        overlap_area = w * h
        
        IOUs = overlap_area/(areas[index] + areas[order[:-1]] - overlap_area)

        # 3,只保留 `IOU` 小於設定閾值的 `bbox`,得到新的 `bboxs`,更新剩下來 bbox的索引
        remain_index = np.where(IOUs < thresh)  # np.where 來找到符合條件的 index
        order = order[remain_index]
    return picked_bboxs

# test
if __name__ == "__main__":
    bboxs = np.array([[30, 20, 230, 200, 1],
                     [50, 50, 260, 220, 0.9],
                     [210, 30, 420, 5, 0.8],
                     [430, 280, 460, 360, 0.7]])
    thresh = 0.35
    keep_bboxs = py_nms(bboxs, thresh)
    print(keep_bboxs)

程式輸出如下:

[0, 2, 3]
[[ 30. 20. 230. 200. 1. ]
[210. 30. 420. 5. 0.8]
[430. 280. 460. 360. 0.7]]

另一個版本的 nms 的 python 程式碼如下:

from __future__ import print_function
import numpy as np
import time

def intersect(box_a, box_b):
    max_xy = np.minimum(box_a[:, 2:], box_b[2:])
    min_xy = np.maximum(box_a[:, :2], box_b[:2])
    inter = np.clip((max_xy - min_xy), a_min=0, a_max=np.inf)
    return inter[:, 0] * inter[:, 1]

def get_iou(box_a, box_b):
    """Compute the jaccard overlap of two sets of boxes.  The jaccard overlap
    is simply the intersection over union of two boxes.
    E.g.:
        A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)
        The box should be [x1,y1,x2,y2]
    Args:
        box_a: Single numpy bounding box, Shape: [4] or Multiple bounding boxes, Shape: [num_boxes,4]
        box_b: Single numpy bounding box, Shape: [4]
    Return:
        jaccard overlap: Shape: [box_a.shape[0], box_a.shape[1]]
    """
    if box_a.ndim==1:
        box_a=box_a.reshape([1,-1])
    inter = intersect(box_a, box_b)
    area_a = ((box_a[:, 2]-box_a[:, 0]) *
              (box_a[:, 3]-box_a[:, 1]))  # [A,B]
    area_b = ((box_b[2]-box_b[0]) *
              (box_b[3]-box_b[1]))  # [A,B]
    union = area_a + area_b - inter
    return inter / union  # [A,B]

def nms(bboxs,scores,thresh):
    """
    The box should be [x1,y1,x2,y2]
    :param bboxs: multiple bounding boxes, Shape: [num_boxes,4]
    :param scores: The score for the corresponding box
    :return: keep inds
    """
    if len(bboxs)==0:
        return []
    order=scores.argsort()[::-1]
    keep=[]
    while order.size>0:
        i=order[0]
        keep.append(i)
        ious=get_iou(bboxs[order],bboxs[i])
        order=order[ious<=thresh]
    return keep

五,Soft NMS 演算法

Soft NMS 演算法是對 NMS 演算法的改進,是發表在 ICCV2017 的論文 中提出的。NMS 演算法存在一個問題是可能會把一些相鄰檢測框框給過濾掉(即將 IOU 大於閾值的視窗的得分全部置為 0 ),從而導致目標的 recall 指標比較低。而 Soft NMS 演算法會為相鄰檢測框設定一個衰減函式而非徹底將其分數置為零。Soft NMS 演算法流程如下圖所示:

soft nms 演算法流程

原來的 NMS 演算法可以透過以下分數重置函式來描述:

硬NMS演算法

論文對 NMS 原有的分數重置函式的改進有兩種形式,一種是線性加權的。設 \(s_i\) 為第 \(i\) 個 bbox 的 score, 則在應用 Soft NMS 時各個 bbox score 的計算公式如下:

線性加權形式的soft NMS演算法

另一種是高斯加權形式的,其不需要設定 iou 閾值 \(N_t\)。高斯懲罰係數(與上面的線性截斷懲罰不同的是, 高斯懲罰會對其他所有的 bbox 作用),計算公式圖如下:

高斯加權形式的soft NMS演算法

注意,這兩種形式,思想都是 \(M\) 為當前得分最高框,\(b_{i}\) 為待處理框, \(b_{i}\)\(M\) 的 IOU 越大,bbox 的得分 \(s_{i}\) 就下降的越厲害 ( \(N_{t}\) 為給定閾值)。Soft NMS 在每輪迭代時,先選擇分數最高的預測框作為 \(M\),並對 \(B\) 中的每一個檢測框 \(b_i\) 進行 re-score,得到新的 score,當該框的新 score 低於某設定閾值時,則立即將該框刪除。

soft nmspython 程式碼如下:

def soft_nms(bboxes, Nt=0.3, sigma2=0.5, score_thresh=0.3, method=2):
    # 在 bboxes 之後新增對應的下標[0, 1, 2...], 最終 bboxes 的 shape 為 [n, 5], 前四個為座標, 後一個為下標
    res_bboxes = deepcopy(bboxes)
    N = bboxes.shape[0]  # 總的 box 的數量
    indexes = np.array([np.arange(N)])  # 下標: 0, 1, 2, ..., n-1
    bboxes = np.concatenate((bboxes, indexes.T), axis=1)  # concatenate 之後, bboxes 的操作不會對外部變數產生影響

    # 計算每個 box 的面積
    x1 = bboxes[:, 0]
    y1 = bboxes[:, 1]
    x2 = bboxes[:, 2]
    y2 = bboxes[:, 3]
    scores = bboxes[:, 4]
    areas = (x2 - x1 + 1) * (y2 - y1 + 1)

    for i in range(N):
        # 找出 i 後面的最大 score 及其下標
        pos = i + 1
        if i != N - 1:
            maxscore = np.max(scores[pos:], axis=0)
            maxpos = np.argmax(scores[pos:], axis=0)
        else:
            maxscore = scores[-1]
            maxpos = 0

        # 如果當前 i 的得分小於後面的最大 score, 則與之交換, 確保 i 上的 score 最大
        if scores[i] < maxscore:
            bboxes[[i, maxpos + i + 1]] = bboxes[[maxpos + i + 1, i]]
            scores[[i, maxpos + i + 1]] = scores[[maxpos + i + 1, i]]
            areas[[i, maxpos + i + 1]] = areas[[maxpos + i + 1, i]]

        # IoU calculate
        xx1 = np.maximum(bboxes[i, 0], bboxes[pos:, 0])
        yy1 = np.maximum(bboxes[i, 1], bboxes[pos:, 1])
        xx2 = np.minimum(bboxes[i, 2], bboxes[pos:, 2])
        yy2 = np.minimum(bboxes[i, 3], bboxes[pos:, 3])
        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        intersection = w * h
        iou = intersection / (areas[i] + areas[pos:] - intersection)

        # Three methods: 1.linear 2.gaussian 3.original NMS
        if method == 1:  # linear
            weight = np.ones(iou.shape)
            weight[iou > Nt] = weight[iou > Nt] - iou[iou > Nt]
        elif method == 2:  # gaussian
            weight = np.exp(-(iou * iou) / sigma2)
        else:  # original NMS
            weight = np.ones(iou.shape)
            weight[iou > Nt] = 0

        scores[pos:] = weight * scores[pos:]

    # select the boxes and keep the corresponding indexes
    inds = bboxes[:, 5][scores > score_thresh]
    keep = inds.astype(int)

    return res_bboxes[keep]

六,目標檢測的不平衡問題

論文 Imbalance Problems in Object Detection 給出了詳細的綜述。這篇論文主要是系統的分析了目標檢測中的不平衡問題,並按照問題進行分類,提出了四類不平衡,並對每個問題現有的解決方案批判性的提出了觀點,且給出了一個實時跟蹤最新的不平衡問題研究的網頁。

6.1,介紹

文章指出當有關輸入屬性的分佈影響效能時,就會出現與輸入屬性相關的不平衡問題。論文將不平衡問題歸為四類:

  • Class imbalance: 類別不平衡。不同類別的輸入邊界框的數量不同,包括前景/背景和前景/前景類別的不平衡,RPNFocal Loss 就是解決這類問題。
  • Scale imbalance: 尺度不平衡,主要是目標邊界框的尺度不平衡引起的,也包括將物體分配至 feature pyramid 時的不平衡。典型如 FPN 就是解決物體多尺度問題的。
  • Spatial imbalance: 空間不平衡。包括不同樣本對迴歸損失貢獻的不平衡,IoU 分佈的不平衡,和目標分佈位置的不平衡。
  • Objective imbalance:不同任務(分類、迴歸)對總損失貢獻的不平衡。

參考資料