1 目標(biāo)檢測(cè)基礎(chǔ)

在圖像分類任務(wù)里,我們假定圖片里只有一個(gè)主體對(duì)象,只需要關(guān)注和識(shí)別該對(duì)象的類別即可。然而,如果我們對(duì)一張圖片中的多個(gè)對(duì)象都感興趣,我們不僅想要知道它們各自的類別,還想知道它們?cè)趫D片中的具體位置。在計(jì)算機(jī)視覺中,我們稱這類任務(wù)為目標(biāo)檢測(cè)、物體檢測(cè)或?qū)ο髾z測(cè)(Object Detection)。

我們先來了解一下,在目標(biāo)檢測(cè)領(lǐng)域,人們是如何定義對(duì)象的位置的。我們僅僅關(guān)注下圖中的兩個(gè)主體對(duì)象:貓和狗。

import numpy as np
from matplotlib import pyplot as plt
np.set_printoptions(2)     # 修改了 NumPy 的打印精度
%matplotlib inline

img_name = '../images/catdog.jpg'
img = plt.imread(img_name)

plt.imshow(img)
plt.show()
貓 和 狗

1.1 邊界框

在目標(biāo)檢測(cè)中,我們通常使用邊界框(bounding box)來描述目標(biāo)位置。邊界框是一個(gè)矩形框,可用矩形左上角和右下角的坐標(biāo)確定。上圖的坐標(biāo)原點(diǎn)為左上角,原點(diǎn)往右和往下分別為 x 軸和 y 軸的正方向。

# 注意坐標(biāo)軸原點(diǎn)是圖片的左上角。bbox 是 bounding box 的縮寫。
dog_bbox = [300, 200, 600, 450]
cat_bbox = [30, 30, 230, 200]

將邊界框 (x_{左上}, y_{左上}, x_{右下}, y_{右下}) 格式轉(zhuǎn)換成 matplotlib 格式:(x_{左上}, y_{左上}, \text{寬}, \text{高})

def bbox_to_rect(bbox, color):
    return plt.Rectangle(
        xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
        fill=False, edgecolor=color, linewidth=2)

下面畫出圖像與邊界框:

fig, ax = plt.subplots()
plt.imshow(img)
ax.add_patch(bbox_to_rect(dog_bbox, 'blue'))
ax.add_patch(bbox_to_rect(cat_bbox, 'red'))
ax.autoscale_view()
plt.show()
邊界框

1.2 錨框

目標(biāo)檢測(cè)算法通常會(huì)在輸入圖片中采樣大量的區(qū)域,然后判斷這些區(qū)域是否有我們感興趣的物體,并調(diào)整區(qū)域邊緣從而更準(zhǔn)確預(yù)測(cè)物體的真實(shí)邊界框。不同的模型使用不同的區(qū)域采樣方法,這里我們介紹其中的一種:它以每個(gè)像素為中心生成數(shù)個(gè)大小和比例不同的邊界框(稱之為錨(máo)框,anchor box)。

假設(shè)輸入圖片高為 h,寬為 w,那么大小為 s\in (0,1] 和比例為 r > 0 的錨框形狀是:

\left( ws \sqrt{r}, \ \frac{hs}{\sqrt{r}}\right)

確定其中心點(diǎn)位置便可以固定一個(gè)錨框。


我們假設(shè)錨框的高和寬分別為 h_1, w_1,則有:

\begin{aligned} &s^2 = \frac{w_1h_1}{wh}\\ &\frac{w_1}{h_1} = \frac{w}{h}r \end{aligned}


我們可以通過不同的 sr,以及中心位置,來遍歷所有可能的區(qū)域。下面我們?cè)O(shè)定一組不同的大小的 s_1\,\ldots\,s_n,與不同大小的 r_1\,\ldots\,r_m。如果我們對(duì)每個(gè)像素都使用這些組合,則輸入圖片將會(huì)得到 wh mn 個(gè)錨框。為了減少計(jì)算量,通常我們僅僅對(duì)由包含 s_{1}r_{1} 對(duì)應(yīng)的錨框感興趣。這樣,我們的錨框數(shù)量則減少為 n + m-1 個(gè)。

上述的采樣方法實(shí)現(xiàn)在 contribe.ndarray 中的 MultiBoxPrior 函數(shù)。通過指定輸入數(shù)據(jù)(我們只需要訪問其形狀),錨框的采樣大小和比例,這個(gè)函數(shù)將返回所有采樣到的錨框。

from mxnet import  contrib, nd

h, w = img.shape[0:2]
x = nd.random.uniform(shape=(1, 3, h, w))  # 構(gòu)造一個(gè)輸入數(shù)據(jù),
y = contrib.nd.MultiBoxPrior(x, sizes=[.75, .5, .25], ratios=[1, 2, .5])
y.shape
(1, 1532800, 4)

y.shape 其返回結(jié)果格式為 (批量大小,錨框個(gè)數(shù),4)。可以看到我們生成了 1 百萬以上個(gè)錨框。將其變形成 (高,寬,n+m-1,4) 后,我們可以方便的訪問以任何一個(gè)像素為中心的所有錨框。下面例子里我們?cè)L問以 (250, 250) 為中心的 5 個(gè)錨框。它們各有四個(gè)元素,同前一樣是左上和右下的 x、y 軸坐標(biāo),但被分別除以了高和寬使得數(shù)值在 01 之間。

boxes = y.reshape((h, w, 5, 4))
boxes[250, 250, :, :]
[[ 0.11  0.15  0.67  0.9 ]
 [ 0.2   0.27  0.58  0.77]
 [ 0.3   0.4   0.48  0.65]
 [-0.01  0.26  0.79  0.79]
 [ 0.19 -0.01  0.59  1.05]]
<NDArray 5x4 @cpu(0)>
def show_bboxes(axes, bboxes, labels=None, colors=None):
    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = bbox_to_rect(bbox.asnumpy(), color)
        axes.add_patch(rect)
        if labels and len(labels) > i:
            text_color = 'k' if color == 'w' else 'w'
            axes.text(rect.xy[0], rect.xy[1], labels[i],
                      va='center', ha='center', fontsize=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))
bbox_scale = nd.array((w, h, w, h))  # 需要乘以高和寬使得符合我們的畫圖格式。
fig, ax = plt.subplots()
plt.imshow(img)
show_bboxes(ax, boxes[220, 350, :, :]*bbox_scale, ['s=.75, r=1', 's=.5, r=1', 's=.25, r=1', 's=.75, r=2', 's=.75, r=.5'])
多個(gè)錨框

1.3 IoU:交并比

在介紹如何使用錨框參與訓(xùn)練和預(yù)測(cè)前,我們先介紹如何計(jì)算兩個(gè)邊界框的距離。我們知道集合相似度的最常用衡量標(biāo)準(zhǔn)叫做 Jaccard 距離。給定集合 A, B,它們的距離定義為集合的交集除以集合的并集:

J(A, B) = \frac{|A\cap B|}{| A \cup B|}

邊界框指定了一塊像素區(qū)域,其可以看成是像素點(diǎn)的集合。因此我們可以定類似的距離,即我們使用兩個(gè)邊界框的相交面積除以相并面積來衡量它們的相似度。這被稱之為交集除并集(Intersection over Union,簡(jiǎn)稱 IoU,或稱為交并比)。它的取值范圍在 01 之間。0 表示邊界框不相關(guān),1 則表示完全一樣。

IoU

1.4 標(biāo)注訓(xùn)練集的錨框

在訓(xùn)練時(shí),我們將每個(gè)錨框都視作一個(gè)訓(xùn)練樣本。為了訓(xùn)練目標(biāo)檢測(cè)模型,我們需要為每個(gè)錨框標(biāo)注兩類標(biāo)簽:

  • 錨框所包含的類別,簡(jiǎn)稱類別
  • 真實(shí)邊界框相對(duì)于錨框的偏移量,簡(jiǎn)稱偏移量(offset)

目標(biāo)檢測(cè)的一般做法是:

  1. 生成多個(gè)錨框
  2. 預(yù)測(cè)每個(gè)錨框的類別與偏移量
  3. 依據(jù)預(yù)測(cè)是偏移量調(diào)整錨框的位置從而獲得預(yù)測(cè)邊界框
  4. 篩選預(yù)測(cè)邊界框得到需要輸出的預(yù)測(cè)邊界框

下面我們?cè)敿?xì)說明如何為錨框分配與其相似的真實(shí)邊界框:

假設(shè)圖像中錨框分別為 A_1, A_2, \cdots, A_ {n_a}, 真實(shí)邊界框分別為 B_1, B_2, \cdots, B_ {n_b}, 且 n_b \geq n_a。定義矩陣 X \in \mathbb{R}^{n_a \times n_b}, 其中 (X)_ {ij} 為錨框 A_i 與真實(shí)邊界框 B_j 的 IoU。

  1. 找出 X 中的最大元素,并將該元素的行、列索引分別記作 i_1, j_1。我們?yōu)殄^框 A_ {i_1} 分配真實(shí)邊界框 B_ {j_1}。(顯然,錨框 A_ {i_1} 與真實(shí)邊界框 B_{j_1} 的相似度為最高)
  2. 丟掉 X 中第 i_ 1 行和第 j_ 1 列的所有元素,找出 X 中剩余元素中的最大者,并將該元素的行、列索引分別記作 i_ 2, j_ 2。我們?yōu)殄^框 A_ {i_2} 分配真實(shí)邊界框 B_ {j_2}。依次類推,直到 X 中所有 n_ 列元素都被丟掉。
  3. 為剩余的 n_ - n_{a} 個(gè)錨框分配真實(shí)邊界框:給定其中的錨框 A_i,依據(jù) X 的第 {1} 行找到與 A_ i 的交并比最大的真實(shí)邊界框 B_j,只有當(dāng)該 IoU 的值大于預(yù)先設(shè)定的閾值時(shí),才為錨框 A_i 分配真實(shí)邊界框 B_j

如果一個(gè)錨框 A 被分配了真實(shí)邊界框 {B},將錨框 A的類別設(shè)為 {B} 的類別,并根據(jù) {B}A 的中心坐標(biāo)相對(duì)位置以及兩個(gè)框的相對(duì)大小為錨框 A 標(biāo)注偏移量。由于數(shù)據(jù)集中各個(gè)框的位置和大小各異,這些相對(duì)位置和相對(duì)大小通常需要一些特殊變換,才能使偏移量的分布更均勻從而更容易擬合。設(shè)錨框 A 及其被分配的真實(shí)邊界框 {B} 的中心坐標(biāo)分別為 (x_a, y_a),(x_b, y_b),A{B} 的寬分別為 w_{a}w_,高分別為 h_{a}, h_,一個(gè)常用的技巧是將 A 的偏移量標(biāo)注為

\left(\frac{\frac{x_b -x_a}{w_a}- \mu_x}{\sigma_x}, \frac{\frac{y_b -y_a}{h_a}- \mu_y}{\sigma_y}, \frac{\log \frac{w_a}{w_a}- \mu_w}{\sigma_w}, \frac{\log \frac{h_b}{h_a}- \mu_h}{\sigma_h} \right)

其中常數(shù)的默認(rèn)值為 \mu_x = \mu_y = \mu_w = \mu_h = 0, \sigma_x = \sigma_y = 0.1, \sigma_w = \sigma_h = 0.2。如果一個(gè)錨框沒有被分配真實(shí)邊界框,我們只需將該錨框的類別設(shè)為背景。類別為背景的錨框通常被稱為負(fù)類錨框,其余則被稱為正類錨框。

下面來看一個(gè)具體的例子。我們構(gòu)造 {6} 個(gè)錨框,其與真實(shí)邊界框的位置如下圖示。

ground_truth = nd.array([[0, .1, .08, .35, .42], [1, .45, .42, 1, 1]])
anchors = nd.array([[ .8, .1, 11, .3], [.1, .1, .35, .36],
                    [.15, .15, .35, .35], [.57, .45, .85, .85],
                   [0.57, 0.3, 0.92, 0.9], [0.47, 0.3, 0.82, 0.89]])


fig, ax = plt.subplots()
plt.imshow(img)
show_bboxes(ax, ground_truth[:,1:]*bbox_scale, ['cat','dog'])
show_bboxes(ax, anchors*bbox_scale, ['0', '1', '2', '3', '4', '5']);
錨框標(biāo)注

我們可以通過 contrib.nd 模塊中的 MultiBoxTarget 函數(shù)來對(duì)錨框生成標(biāo)號(hào)。我們把錨框和真實(shí)邊界框加上批量維(實(shí)際中我們會(huì)批量處理數(shù)據(jù)),然后構(gòu)造一個(gè)任意的錨框預(yù)測(cè)結(jié)果,其形狀為(批量大小,類別數(shù) +1,錨框數(shù)),其中第 0 類為背景。

labels = contrib.nd.MultiBoxTarget(anchors.expand_dims(axis=0),
                                   ground_truth.expand_dims(axis=0),
                                   nd.zeros((1, 2, 6)))

返回的結(jié)果里有三項(xiàng),均為 NDArray。第三項(xiàng)表示為錨框標(biāo)注的類別。

labels[2]
[[0. 1. 0. 0. 2. 0.]]
<NDArray 1x6 @cpu(0)>

返回值的第二項(xiàng)為掩碼(mask)變量,形狀為 (批量大小,錨框個(gè)數(shù) x 4)。掩碼變量中的元素與每個(gè)錨框的四個(gè)偏移量一一對(duì)應(yīng)。 由于我們不關(guān)心對(duì)背景的檢測(cè),有關(guān)負(fù)類的偏移量不應(yīng)影響目標(biāo)函數(shù)。通過按元素乘法,掩碼變量中的 0 可以在計(jì)算目標(biāo)函數(shù)之前過濾掉負(fù)類的偏移量。(其中正類錨框?qū)?yīng)的元素為 1,負(fù)類為 0

labels[1]
[[0. 0. 0. 0. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 0. 0. 0. 0.]]
<NDArray 1x24 @cpu(0)>

返回的第一項(xiàng)是為每個(gè)錨框標(biāo)注的四個(gè)偏移量,其中負(fù)類錨框的偏移量標(biāo)注為 0。

labels[0]
[[ 0.    0.    0.    0.    0.    0.77  0.    1.34  0.    0.    0.    0.
   0.    0.    0.    0.   -0.57  1.83  2.26 -0.17  0.    0.    0.    0.  ]]
<NDArray 1x24 @cpu(0)>

1.5 標(biāo)注測(cè)試集的錨框

預(yù)測(cè)同訓(xùn)練類似,是對(duì)每個(gè)錨框預(yù)測(cè)其包含的物體類別和與真實(shí)邊界框的位移。因?yàn)槲覀兩闪舜罅康腻^框,所以可能導(dǎo)致對(duì)同一個(gè)物體產(chǎn)生大量相似的預(yù)測(cè)邊界框。為了使得結(jié)果更加簡(jiǎn)潔,我們需要消除相似的冗余預(yù)測(cè)框。這里常用的方法是非極大值抑制(Non-Maximum Suppression,簡(jiǎn)稱 NMS)。對(duì)于相近的預(yù)測(cè)邊界框,NMS 只保留物體標(biāo)號(hào)預(yù)測(cè)置信度最高的那個(gè)。
關(guān)于“非極大值抑制”的詳細(xì)內(nèi)容見 非極大值抑制(NMS)。

具體來說,對(duì)于每個(gè)物體類別(非背景),我們先獲取每個(gè)預(yù)測(cè)邊界框里被判斷包含這個(gè)類別物體的概率。然后我們找到概率最大的那個(gè)邊界框,如果其置信度大于某個(gè)閾值,那么保留它到輸出。接下來移除掉其它所有的跟這個(gè)邊界框的 IoU 大于某個(gè)閾值的邊界框。在剩下的邊界框里我們?cè)僬页鲱A(yù)測(cè)概率最大的邊界框,一直重復(fù)前面的移除過程,直到我們遍歷保留或者移除了每個(gè)邊界框。

下面來看一個(gè)具體的例子。我們先構(gòu)造四個(gè)錨框,為了簡(jiǎn)單起見我們假設(shè)預(yù)測(cè)偏移全是 0,然后構(gòu)造了類別預(yù)測(cè)。

anchors = nd.array([[0.1, 0.08, 0.32, 0.42], [0.08, 0.2, 0.46, 0.65],
                    [0.45, 0.6, 0.82, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = nd.array([0] * anchors.size)
cls_probs = nd.array([[0] * 4,  # 背景的預(yù)測(cè)概率。
                      [0.1, 0.2, 0.3, 0.9],  # 貓的預(yù)測(cè)概率。
                      [0.9, 0.8, 0.5, 0.1]])    # 狗的預(yù)測(cè)概率。

在圖像上打印預(yù)測(cè)邊界框和它們的置信度(我隨便設(shè)定的):

fig = plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
            ['cat=0.9', 'cat=0.6', 'dog=0.7', 'dog=0.9'])
邊界框的置信度

我們使用 contrib.nd 模塊的 MultiBoxDetection 函數(shù)來執(zhí)行非極大值抑制并設(shè)閾值為 0.5。這里為 NDArray 輸入都增加了樣本維。我們看到,返回結(jié)果的形狀為(批量大小,錨框個(gè)數(shù),6)。其中每一行的 6 個(gè)元素代表同一個(gè)預(yù)測(cè)邊界框的輸出信息。第一個(gè)元素是索引從 0 開始計(jì)數(shù)的預(yù)測(cè)類別(0 為貓,1 為狗),其中 -1 表示背景或在非極大值抑制中被移除。第二個(gè)元素是預(yù)測(cè)邊界框的置信度。剩余的四個(gè)元素分別是預(yù)測(cè)邊界框左上角的 x, y 軸坐標(biāo)和右下角的 x, y 軸坐標(biāo)(值域在 0 到 1 之間)。

output = contrib.ndarray.MultiBoxDetection(
    cls_probs.expand_dims(axis=0), offset_preds.expand_dims(axis=0),
    anchors.expand_dims(axis=0), nms_threshold=0.5)
output
[[[1.   0.9  0.1  0.08 0.32 0.42]
  [0.   0.9  0.55 0.2  0.9  0.88]
  [1.   0.8  0.08 0.2  0.46 0.65]
  [1.   0.5  0.45 0.6  0.82 0.91]]]
<NDArray 1x4x6 @cpu(0)>

我們移除掉類別為 -1 的預(yù)測(cè)邊界框,并可視化非極大值抑制保留的結(jié)果。

fig = plt.imshow(img)
for i in output[0].asnumpy():
    if i[0] == -1:
        continue
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    show_bboxes(fig.axes, [nd.array(i[2:]) * bbox_scale], label)
NMS

實(shí)踐中,我們可以在執(zhí)行非極大值抑制前將置信度較低的預(yù)測(cè)邊界框移除,從而減小非極大值抑制的計(jì)算量。我們還可以篩選非極大值抑制的輸出,例如只保留其中置信度較高的結(jié)果作為最終輸出。

1.6 小結(jié)

  • 以每個(gè)像素為中心,我們生成多個(gè)大小和寬高比不同的錨框。
  • 交并比是兩個(gè)邊界框相交面積與相并面積之比。
  • 在訓(xùn)練集中,我們?yōu)槊總€(gè)錨框標(biāo)注兩類標(biāo)簽:一是錨框所含目標(biāo)的類別;二是真實(shí)邊界框相對(duì)錨框的偏移量。
  • 預(yù)測(cè)時(shí),我們可以使用非極大值抑制來移除相似的預(yù)測(cè)邊界框,從而令結(jié)果簡(jiǎn)潔。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容