梗概
CenterNet通過預測每個目標的中心點,既而以中心點爲基準進行迴歸寬和高,以及由於下采樣帶來的點的偏置。將目標檢測用關鍵點檢測的思路來做,拋棄了由anchor生成的大量需要被抑制的樣本,故而不需要NMS做後處理,而且整個網絡只有一個檢測Head,不基於FPN爲BackBone需要多個檢測Head,整體速度就快了很多。
方法
網絡的總體結構可以化簡爲上圖所示。其中backbone的結構放在最後講解,對於一種新的檢測思路,重點思路是在如何對label編碼,如何對預測結果解碼上。實際上論文的順序也是最後介紹backbone。
檢測頭
假設我們已經得到了一個從backbone中得到的特徵圖,其shape爲, h和w是原圖的1/4。這個特徵經過檢測Head,得到了三樣東西
- key point heatmap: shape是 , c是類別數目,就是每個類別都有自己的單通道的heatmap。熱圖上的峯值對應了圖像的一個目標(屬於熱圖對應的類別)。
- Offset: 因爲是預測目標中心的位置。如果一個在原圖的目標的中心座標爲x,y。那這個目標對應在特徵圖上的位置就是 x//4, y // 4。 這裏用的是整除。 這個數字乘以4得到的數值就不會是原圖座標了。所以從heatmap上取到的峯值位置,未必等於原圖上目標的中心點。還需要一個偏置項,描述了heatmap上的取到的峯值位置離原圖座標下最終的目標的一個偏移量。
- Size: shape和offset一致,都是2通道的map。兩個通道分別描述目標的height和width。
所以總的輸出通道爲C+4。相較於Anchor Base的檢測網絡,需要N* K*(C+4),是不是小了很多呢。N是檢測頭的數目,K是anchor的數目
Label的編碼
三個分支的意義大家知道了,那三個分支對應的標籤應該是啥呢?
- heatmap:既然heatmap是預測目標的中心位置。那麼對於一張圖像中的一個目標在圖像上x,y的位置上,且所屬類別C。我們就應該在第C個類別對應的heatmap上,在[x//4, y//4]的位置上設置爲1。但是僅僅這樣是不夠的,一個目標的中心點是通過暴力的求平均得到的,網絡在訓練初期很難理解,爲啥離中心點一個像素單元的地方就變成了負樣本了。就是說網絡比較難get到什麼是物體的中心。因爲還需要用一個高斯覈對每個位置重新賦值一個label,這個label是在0-1之間,相當於起到了一個軟標籤的效果。離目標中心越遠的位置,其label值越接近0。 重疊位置的label賦值選擇二者的最大值。(其實也是關鍵點檢測常用的編碼label的方法)
- offSet: 如果你理解了offset的用途。那不難理解如何訓練Offset。我們希望offset輸出的是 原始座標整除stride和不整除stride 的差值。所以目標在offset上對應的label就是 在 [x//4, y//4]上的值爲 [x / 4 - x // 4, y / 4 - y // 4]
- Size:基於上面的兩個分支,我們已經可以準確獲得目標在原圖座標系下的中心點了。接下里,希望Size輸出基於峯值位置上目標框的高和寬。假設這個目標的GT框是(x1,y1; x2, y2), 則這個位置的label是 (x2 - x1; y2 - y1)。 注意是在原圖座標下計算的,沒有除以stride。同時尺度過大,作者在size分支的loss上乘上了0.1這個係數。loss放在後面說。
label的解碼
在inference的過程中,得到三個分支的特徵圖之後,如何解碼呢?
- 首先得到峯值位置(x,y),據作者說,用3x3的最大池化就能找到。轉到源碼分析部分。
- 加上(x,y)位置上的offset結果。 然後乘以4,映射到原圖座標下。
- 得到(x,y)位置上Size結果。 然後加加減減就能得到框的左上角座標和右下角座標了
訓練
在介紹了三個分支的label編碼之後,自然來談談loss。
- heatmap分支:用focal loss做二分類。對每一個通道(每個類別)都做。也是FCOS,RetainNet中常用的class分支的訓練方式。除以正樣本數目N。
- 剩下兩個分支都是L1 loss。除以正樣本數目N。(僅僅是目標中心的位置參與loss,哪怕靠近中心的位置都不參與loss的計算,也就是被ignore了,並不是被判負)
總的loss是
作者也提到,中心點附近的其他位置並不是直接判負。而是用的reduced negative loss。就是說附近的點是負樣本,但他們的label不是0.而是0-1的值。越接近0代表這個位置越可能是非目標的中心。
backbone
作者在論文中使用了三種backbone。分別是:
- Hourglass:這個網絡最開始被提出就是用來做關鍵點檢測的。作者改爲stacked的方式,就類似漸進式或者乾脆說級聯。就是一個encoder-decoder之後還接一個encoder-decoder。程明明組下的BASNet,做顯著性檢測的論文也是用的這種backbone。
- ResNet: 作者魔改了一下,在每個上採樣之前添加一個DCN(變形卷積)
- DLA: 一個做分類的網絡。
(a):hourglass ; (b):魔改的resNet; (d)作者魔改的DLA
源碼分析
按照慣例,還是來看看源碼。對照論文驗證一下。
- 首先看看解碼的地方。在 process中,獲得模型的三個分支的預測結果
hm = output['hm'].sigmoid_() # heatmap
wh = output['wh'] # size
reg = output['reg'] if self.opt.reg_offset else None # offset
然後進入ctdet_decode 這個函數按功能可以分三部分
nms: 獲得heatmap上的極值位置
topk : 這些極值位置上的前k個概率大的位置
得到目標框
NMS的部分很簡單,用map pool獲得3*3的格子中最大的值,然後和之前的輸入對比一下就可以得到極值位置
hmax = nn.functional.max_pool2d(
heat, (kernel, kernel), stride=1, padding=pad)
keep = (hmax == heat).float()
topk省略不介紹。
xs是中心位置, reg是offset。他們相加,就是準確的目標中心位置
xs = xs.view(batch, K, 1) + reg[:, :, 0:1]
ys = ys.view(batch, K, 1) + reg[:, :, 1:2]
然後加加減減就得到框的位置了。
bboxes = torch.cat([xs - wh[..., 0:1] / 2,
ys - wh[..., 1:2] / 2,
xs + wh[..., 0:1] / 2,
ys + wh[..., 1:2] / 2], dim=2)
唯獨和論文有出入的地方,是沒有乘上4(stride)。我找了很久也沒找到這個stride。我懷疑作者在訓練時候,size的label就是除以4的了。
這個時候,我們得到的是原圖小4倍的座標。然後在根據具體圖像大小還原回去就行了。
然後看看dataset裏面的一些細節:
- 高斯核的方差,是根據目標框的大小自動改變的。
radius = gaussian_radius((math.ceil(h), math.ceil(w)))
- heatmap的label製作過程:
def draw_umich_gaussian(heatmap, center, radius, k=1):
diameter = 2 * radius + 1
gaussian = gaussian2D((diameter, diameter), sigma=diameter / 6) # 先得到一個大小的高斯核
x, y = int(center[0]), int(center[1]) # 目標中心
height, width = heatmap.shape[0:2]
left, right = min(x, radius), min(width - x, radius + 1)
top, bottom = min(y, radius), min(height - y, radius + 1)
# 然後把高斯核的某部分貼到以目標中心爲中心的一片相同大小的區域上;如果重疊,取較大值。
masked_heatmap = heatmap[y - top:y + bottom, x - left:x + right]
masked_gaussian = gaussian[radius - top:radius + bottom, radius - left:radius + right]
if min(masked_gaussian.shape) > 0 and min(masked_heatmap.shape) > 0: # TODO debug
np.maximum(masked_heatmap, masked_gaussian * k, out=masked_heatmap)
return heatmap