CenterNet 數(shù)據(jù)加載解析

本文主要解讀CenterNet如何加載數(shù)據(jù)瞬哼,并將標(biāo)注信息轉(zhuǎn)化為CenterNet規(guī)定的高斯分布的形式刽锤。

1. YOLOv3和CenterNet流程對(duì)比

CenterNet和Anchor-Based的方法不同凤瘦,以YOLOv3為例,大致梳理一下模型的框架和數(shù)據(jù)處理流程驻粟。

YOLOv3是一個(gè)經(jīng)典的單階段的目標(biāo)檢測(cè)算法役电,圖片進(jìn)入網(wǎng)絡(luò)的流程如下:

  • 對(duì)圖片進(jìn)行resize,長(zhǎng)和寬都要是32的倍數(shù)庶柿。
  • 圖片經(jīng)過網(wǎng)絡(luò)的特征提取后村怪,空間分辨率變?yōu)樵瓉淼?/32。
  • 得到的Tensor去代表圖片不同尺度下的目標(biāo)框浮庐,其中目標(biāo)框的表示為(x,y,w,h,c)甚负,分別代表左上角坐標(biāo),寬和高,含有某物體的置信度梭域。
  • 訓(xùn)練完成后斑举,測(cè)試的時(shí)候需要使用非極大抑制算法得到最終的目標(biāo)框。

CenterNet是一個(gè)經(jīng)典的Anchor-Free目標(biāo)檢測(cè)方法病涨,圖片進(jìn)入網(wǎng)絡(luò)流程如下:

  • 對(duì)圖片進(jìn)行resize懂昂,長(zhǎng)和寬一般相等,并且至少為4的倍數(shù)没宾。
  • 圖片經(jīng)過網(wǎng)絡(luò)的特征提取后,得到的特征圖的空間分辨率依然比較大沸柔,是原來的1/4循衰。這是因?yàn)镃enterNet采用的是類似人體姿態(tài)估計(jì)中用到的骨干網(wǎng)絡(luò),基于heatmap提取關(guān)鍵點(diǎn)的方法需要最終的空間分辨率比較大褐澎。
  • 訓(xùn)練的過程中会钝,CenterNet得到的是一個(gè)heatmap,所以標(biāo)簽加載的時(shí)候工三,需要轉(zhuǎn)為類似的heatmap熱圖迁酸。
  • 測(cè)試的過程中,由于只需要從熱圖中提取目標(biāo)俭正,這樣就不需要使用NMS奸鬓,降低了計(jì)算量。

2. CenterNet部分詳解

設(shè)輸入圖片為I\in R^{W\times H\times 3}, W代表圖片的寬掸读,H代表高串远。CenterNet的輸出是一個(gè)關(guān)鍵點(diǎn)熱圖heatmap。
\hat{Y}\in[0,1]^{\frac{W}{R}\times\frac{H}{R}\times C}
其中R代表輸出的stride大小儿惫,C代表關(guān)鍵點(diǎn)的類型的個(gè)數(shù)澡罚。

舉個(gè)例子,在COCO數(shù)據(jù)集目標(biāo)檢測(cè)中肾请,R設(shè)置為4留搔,C的值為80,代表80個(gè)類別铛铁。

如果\hat{Y}_{x,y,c}=1代表檢測(cè)到一個(gè)物體隔显,表示對(duì)類別c來說,(x,y)這個(gè)位置檢測(cè)到了c類的目標(biāo)避归。

既然輸出是熱圖荣月,標(biāo)簽構(gòu)建的ground truth也必須是熱圖的形式。標(biāo)注的內(nèi)容一般包含(x1,y1,x2,y2,c),目標(biāo)框左上角坐標(biāo)梳毙、右下角坐標(biāo)和類別c哺窄,按照以下流程轉(zhuǎn)為ground truth:

  • 得到原圖中對(duì)應(yīng)的中心坐標(biāo)p=(\frac{x1+x2}{2}, \frac{y1+y2}{2})
  • 得到下采樣后的feature map中對(duì)應(yīng)的中心坐標(biāo)\tilde{p}=\lfloor \frac{p}{R}\rfloor, R代表下采樣倍數(shù),CenterNet中R為4
  • 如果輸入圖片為512,那么輸出的feature map的空間分辨率為[128x128], 將標(biāo)注的目標(biāo)框以高斯核的方式將關(guān)鍵點(diǎn)分布到特征圖上:

Y_{xyc}=exp(-\frac{(x-\tilde p_x)^2+(y-\tilde p_y)^2}{2\sigma ^2_p})

其中\sigma_p是一個(gè)與目標(biāo)大小相關(guān)的標(biāo)準(zhǔn)差(代碼中設(shè)置的是)萌业。對(duì)于特殊情況坷襟,相同類別的兩個(gè)高斯分布發(fā)生了重疊,重疊元素間最大的值作為最終元素生年。下圖是知乎用戶OLDPAN分享的高斯分布圖婴程。

圖源知乎@OLDPAN

3. 代碼部分

datasets/pascal.py 的代碼主要從getitem函數(shù)入手,以下代碼已經(jīng)做了注釋抱婉,其中最重要的兩個(gè)部分一個(gè)是如何獲取高斯半徑(gaussian_radius函數(shù))档叔,一個(gè)是如何將高斯分布分散到heatmap上(draw_umich_gaussian函數(shù))。

def __getitem__(self, index):
    img_id = self.images[index]
    img_path = os.path.join(
        self.img_dir, self.coco.loadImgs(ids=[img_id])[0]['file_name'])
    ann_ids = self.coco.getAnnIds(imgIds=[img_id])
    annotations = self.coco.loadAnns(ids=ann_ids)

    labels = np.array([self.cat_ids[anno['category_id']]
                        for anno in annotations])
    bboxes = np.array([anno['bbox']
                        for anno in annotations], dtype=np.float32)

    if len(bboxes) == 0:
        bboxes = np.array([[0., 0., 0., 0.]], dtype=np.float32)
        labels = np.array([[0]])

    bboxes[:, 2:] += bboxes[:, :2]  # xywh to xyxy

    img = cv2.imread(img_path)
    height, width = img.shape[0], img.shape[1]
    # 獲取中心坐標(biāo)p
    center = np.array([width / 2., height / 2.],
                        dtype=np.float32)  # center of image
    scale = max(height, width) * 1.0  # 仿射變換

    flipped = False
    if self.split == 'train':
        # 隨機(jī)選擇一個(gè)尺寸來訓(xùn)練
        scale = scale * np.random.choice(self.rand_scales)
        w_border = get_border(128, width)
        h_border = get_border(128, height)
        center[0] = np.random.randint(low=w_border, high=width - w_border)
        center[1] = np.random.randint(low=h_border, high=height - h_border)

        if np.random.random() < 0.5:
            flipped = True
            img = img[:, ::-1, :]
            center[0] = width - center[0] - 1

    # 仿射變換
    trans_img = get_affine_transform(
        center, scale, 0, [self.img_size['w'], self.img_size['h']])
    img = cv2.warpAffine(
        img, trans_img, (self.img_size['w'], self.img_size['h']))

    # 歸一化
    img = (img.astype(np.float32) / 255.)
    if self.split == 'train':
        # 對(duì)圖片的亮度對(duì)比度等屬性進(jìn)行修改
        color_aug(self.data_rng, img, self.eig_val, self.eig_vec)

    img -= self.mean
    img /= self.std
    img = img.transpose(2, 0, 1)  # from [H, W, C] to [C, H, W]

    # 對(duì)Ground Truth heatmap進(jìn)行仿射變換
    trans_fmap = get_affine_transform(
        center, scale, 0, [self.fmap_size['w'], self.fmap_size['h']]) # 這時(shí)候已經(jīng)是下采樣為原來的四分之一了

    # 3個(gè)最重要的變量
    hmap = np.zeros(
        (self.num_classes, self.fmap_size['h'], self.fmap_size['w']), dtype=np.float32)  # heatmap
    w_h_ = np.zeros((self.max_objs, 2), dtype=np.float32)  # width and height
    regs = np.zeros((self.max_objs, 2), dtype=np.float32)  # regression

    # indexs
    inds = np.zeros((self.max_objs,), dtype=np.int64)
    # 具體選擇哪些index
    ind_masks = np.zeros((self.max_objs,), dtype=np.uint8)

    for k, (bbox, label) in enumerate(zip(bboxes, labels)):
        if flipped:
            bbox[[0, 2]] = width - bbox[[2, 0]] - 1
        
        # 對(duì)檢測(cè)框也進(jìn)行仿射變換
        bbox[:2] = affine_transform(bbox[:2], trans_fmap)
        bbox[2:] = affine_transform(bbox[2:], trans_fmap)
        # 防止越界
        bbox[[0, 2]] = np.clip(bbox[[0, 2]], 0, self.fmap_size['w'] - 1)
        bbox[[1, 3]] = np.clip(bbox[[1, 3]], 0, self.fmap_size['h'] - 1)
        # 得到高和寬
        h, w = bbox[3] - bbox[1], bbox[2] - bbox[0]

        if h > 0 and w > 0:
            obj_c = np.array([(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], 
                                dtype=np.float32) # 中心坐標(biāo)-浮點(diǎn)型
            obj_c_int = obj_c.astype(np.int32) # 整型的中心坐標(biāo)
            # 根據(jù)一元二次方程計(jì)算出最小的半徑
            radius = max(0, int(gaussian_radius((math.ceil(h), math.ceil(w)), self.gaussian_iou)))
            # 得到高斯分布
            draw_umich_gaussian(hmap[label], obj_c_int, radius)

            w_h_[k] = 1. * w, 1. * h
            
            # 記錄偏移量
            regs[k] = obj_c - obj_c_int  # discretization error
            # 當(dāng)前是obj序列中的第k個(gè) = fmap_w * cy + cx = fmap中的序列數(shù)
            inds[k] = obj_c_int[1] * self.fmap_size['w'] + obj_c_int[0]
            # 進(jìn)行mask標(biāo)記
            ind_masks[k] = 1

    return {'image': img, 'hmap': hmap, 'w_h_': w_h_, 'regs': regs, 
            'inds': inds, 'ind_masks': ind_masks, 'c': center, 
            's': scale, 'img_id': img_id}

4. heatmap上應(yīng)用高斯核

heatmap上使用高斯核有很多需要注意的細(xì)節(jié)蒸绩。CenterNet官方版本實(shí)際上是在CornerNet的基礎(chǔ)上改動(dòng)得到的衙四,有很多祖?zhèn)鞔a。

在使用高斯核前要考慮這樣一個(gè)問題患亿,下圖來自于CornerNet論文中的圖示传蹈,紅色的是標(biāo)注框,但綠色的其實(shí)也可以作為最終的檢測(cè)結(jié)果保留下來步藕。那么這個(gè)問題可以轉(zhuǎn)化為綠框在紅框多大范圍以內(nèi)可以被接受惦界。使用IOU來衡量紅框和綠框的貼合程度,當(dāng)兩者IOU>0.7的時(shí)候咙冗,認(rèn)為綠框也可以被接受沾歪,反之則不被接受。

圖源CornerNet

那么現(xiàn)在問題轉(zhuǎn)化為乞娄,如何確定半徑r, 讓紅框和綠框的IOU大于0.7瞬逊。

image

以上是三種情況,其中藍(lán)框代表標(biāo)注框仪或,橙色代表可能滿足要求的框确镊。這個(gè)問題最終變?yōu)榱艘粋€(gè)一元二次方程有解的問題,同時(shí)由于半徑必須為正數(shù)范删,所以r的取值就可以通過求根公式獲得蕾域。

def gaussian_radius(det_size, min_overlap=0.7):
    # gt框的長(zhǎng)和寬
    height, width = det_size

    a1 = 1
    b1 = (height + width)
    c1 = width * height * (1 - min_overlap) / (1 + min_overlap)
    sq1 = np.sqrt(b1 ** 2 - 4 * a1 * c1)
    r1 = (b1 + sq1) / (2 * a1)

    a2 = 4
    b2 = 2 * (height + width)
    c2 = (1 - min_overlap) * width * height
    sq2 = np.sqrt(b2 ** 2 - 4 * a2 * c2)
    r2 = (b2 + sq2) / (2 * a2)

    a3 = 4 * min_overlap
    b3 = -2 * min_overlap * (height + width)
    c3 = (min_overlap - 1) * width * height
    sq3 = np.sqrt(b3 ** 2 - 4 * a3 * c3)
    r3 = (b3 + sq3) / (2 * a3)
    
    return min(r1, r2, r3)

可以看到這里的公式和上圖計(jì)算的結(jié)果是一致的,需要說明的是到旦,CornerNet最開始版本中這里出現(xiàn)了錯(cuò)誤旨巷,分母不是2a,而是直接設(shè)置為2添忘。CenterNet也延續(xù)了這個(gè)bug采呐,CenterNet作者回應(yīng)說這個(gè)bug對(duì)結(jié)果的影響不大,但是根據(jù)issue的討論來看搁骑,有一些人通過修正這個(gè)bug以后斧吐,可以讓AR提升1-3個(gè)百分點(diǎn)又固。以下是有bug的版本,CornerNet最新版中已經(jīng)修復(fù)了這個(gè)bug煤率。

def gaussian_radius(det_size, min_overlap=0.7):
  height, width = det_size

  a1  = 1
  b1  = (height + width)
  c1  = width * height * (1 - min_overlap) / (1 + min_overlap)
  sq1 = np.sqrt(b1 ** 2 - 4 * a1 * c1)
  r1  = (b1 + sq1) / 2

  a2  = 4
  b2  = 2 * (height + width)
  c2  = (1 - min_overlap) * width * height
  sq2 = np.sqrt(b2 ** 2 - 4 * a2 * c2)
  r2  = (b2 + sq2) / 2

  a3  = 4 * min_overlap
  b3  = -2 * min_overlap * (height + width)
  c3  = (min_overlap - 1) * width * height
  sq3 = np.sqrt(b3 ** 2 - 4 * a3 * c3)
  r3  = (b3 + sq3) / 2

  return min(r1, r2, r3)

同時(shí)有一些人認(rèn)為圓并不普適仰冠,提出了使用橢圓來進(jìn)行計(jì)算,也有人在issue中給出了推導(dǎo)蝶糯,感興趣的可以看以下鏈接:https://github.com/princeton-vl/CornerNet/issues/110

5. 高斯分布添加到heatmap上

def gaussian2D(shape, sigma=1):
    m, n = [(ss - 1.) / 2. for ss in shape]
    y, x = np.ogrid[-m:m + 1, -n:n + 1]

    h = np.exp(-(x * x + y * y) / (2 * sigma * sigma))
    h[h < np.finfo(h.dtype).eps * h.max()] = 0
    # 限制最小的值
    return h

def draw_umich_gaussian(heatmap, center, radius, k=1):
    # 得到直徑
    diameter = 2 * radius + 1
    gaussian = gaussian2D((diameter, diameter), sigma=diameter / 6) 
    # sigma是一個(gè)與直徑相關(guān)的參數(shù)
    # 一個(gè)圓對(duì)應(yīng)內(nèi)切正方形的高斯分布

    x, y = int(center[0]), int(center[1])

    height, width = heatmap.shape[0:2]

    # 對(duì)邊界進(jìn)行約束洋只,防止越界
    left, right = min(x, radius), min(width - x, radius + 1)
    top, bottom = min(y, radius), min(height - y, radius + 1)

    # 選擇對(duì)應(yīng)區(qū)域
    masked_heatmap = heatmap[y - top:y + bottom, x - left:x + right]
    # 將高斯分布結(jié)果約束在邊界內(nèi)
    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)
        # 將高斯分布覆蓋到heatmap上,相當(dāng)于不斷的在heatmap基礎(chǔ)上添加關(guān)鍵點(diǎn)的高斯昼捍,
        # 即同一種類型的框會(huì)在一個(gè)heatmap某一個(gè)類別通道上面上面不斷添加识虚。
        # 最終通過函數(shù)總體的for循環(huán),相當(dāng)于不斷將目標(biāo)畫到heatmap
    return heatmap

使用matplotlib對(duì)gaussian2D進(jìn)行可視化妒茬。

import numpy as np
y,x = np.ogrid[-4:5,-3:4]
sigma = 1
h=np.exp(-(x*x+y*y)/(2*sigma*sigma))
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(x,y,h)
plt.show()
高斯分布可視化結(jié)果

6. 參考

[1]https://zhuanlan.zhihu.com/p/66048276

[2]https://www.cnblogs.com/shine-lee/p/9671253.html

[3]https://zhuanlan.zhihu.com/p/96856635

[4]http://xxx.itp.ac.cn/pdf/1808.01244

[5]https://github.com/princeton-vl/CornerNet/issues/110

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末舷礼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子郊闯,更是在濱河造成了極大的恐慌,老刑警劉巖蛛株,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件团赁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡谨履,警方通過查閱死者的電腦和手機(jī)欢摄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笋粟,“玉大人怀挠,你說我怎么就攤上這事『Σ叮” “怎么了绿淋?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)尝盼。 經(jīng)常有香客問我吞滞,道長(zhǎng),這世上最難降的妖魔是什么盾沫? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任裁赠,我火速辦了婚禮,結(jié)果婚禮上赴精,老公的妹妹穿的比我還像新娘佩捞。我一直安慰自己,他們只是感情好蕾哟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布一忱。 她就那樣靜靜地躺著莲蜘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掀潮。 梳的紋絲不亂的頭發(fā)上菇夸,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音仪吧,去河邊找鬼庄新。 笑死,一個(gè)胖子當(dāng)著我的面吹牛薯鼠,可吹牛的內(nèi)容都是我干的择诈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼出皇,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼羞芍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起郊艘,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤荷科,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后纱注,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體畏浆,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狞贱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刻获。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瞎嬉,死狀恐怖蝎毡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情氧枣,我是刑警寧澤沐兵,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站便监,受9級(jí)特大地震影響痒筒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茬贵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一簿透、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧解藻,春花似錦老充、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽觅够。三九已至,卻和暖如春巷嚣,著一層夾襖步出監(jiān)牢的瞬間喘先,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工廷粒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窘拯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓坝茎,卻偏偏與公主長(zhǎng)得像涤姊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嗤放,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354