centernet論文與代碼剖析


本文官方鏈接,https://www.cnblogs.com/yanghailin/p/14034984.html,未經允許,勿轉載。

Centernet github地址:
https://github.com/xingyizhou/CenterNet

加qq群一起學習討論交流:1020395892

centernet訓練自己的數據集
https://www.cnblogs.com/yanghailin/p/13977665.html

1.論文簡介

1.1 前言

目標檢測器用水平框來確定一個圖像中的目標。最好的目標檢測器幾乎列舉了每個目標的全部的潛在的位置和分類結果。這種做法是奢侈的,低效的而且需要額外的后處理。本文中我們提出了一種不同的方法。我們建模一個對象為一個單個的點——即目標框的中心點。我們的檢測器使用關鍵點估計來找到中心點並且回歸出全部其他的目標屬性,比如大小,3D位置,方向甚至姿勢。
一階段檢測器把名為anchors的復雜排布的可能的框在圖像上滑動,並且對小圖直接進行分類而不修正框的內容。二階段檢測器對每個潛在的框重新計算了圖像特征,然后對這些新特征進行分類,(也就是repooling操作,如:roi pooling以及roi align)。后處理也就是非極大值抑制,然后通過計算框的IOU去除同一對象的多余檢測結果。這個后處理結果很難去微分和訓練,因此當前大多數的檢測器都不是端到端可訓練的。

1.2 centernet方法概況

本文中,我們提供了一個更簡單並且更高效的可替代的目標檢測方法。我們通過一個對象框中心的單個點來表示目標(圖2所示)。其他的屬性,比如目標尺寸,維度,3D范圍,方向,和姿勢在中心點的位置上直接通過圖像特征來回歸(多個屬性對應多個channel)。目標檢測問題就是一個標准的關鍵點估計問題。我們簡單的把一個輸入圖像喂到一個全卷積網絡當中生成了一個熱力圖。該熱力圖的峰值相應的就是目標的中心。圖像特征在每個峰值處預測目標的邊界框的高和寬。模型訓練使用標准的密集監督學習方法。推理過程就是一個網絡的前向運算,不需要非極大值抑制后處理。


1.3 速度和精度對比圖

1.4 關鍵點和損失

1.4.1關鍵點


表示一個寬度為W並且高度為H的輸入圖像。我們的目標是產生一個關鍵點的heatmap:

其中,R表示輸出步長,C是類別個數。一般R=4,在人體關鍵點估計中包含 C = 17 個人體關節類型,或在目標檢測的目標分類中 C = 80個類別。

表示對應於一個被檢測的關鍵點

是背景
我們使用幾個不同的全卷積編解碼網絡從一個圖像I來預測 Y^ :一個堆疊的hourglass網絡,上卷積的殘差網絡(ResNet)和深層聚合網絡(DLA)。

1.4.2關鍵點數據標簽

對於類別c中每一個關鍵點的真實值:

我們計算了一個低分辨率等價的表示:
(注意這里是向下取整)
我們之后把所有的真實的關鍵點分布在一個heatMap上

並使用一個高斯卷積核生成一個二維的正態分布:

1.4.3 關鍵點loss


其中α和β是focal loss的超參數。在所有實驗中我們采取 α = 2和β = 4
舉例:
的時候,易分樣本Y冒號(網絡預測值)接近於1,
是一個很小的值,loss很小。對於難分類的樣本來說,Y冒號(網絡預測值)接近於0,說明這個中心點還沒有學習到,所以要加大其訓練的比重,上面的公式的值就很大。所以對於難樣本就加大訓練的比重,對於簡單樣本比重很小。
同樣的,otherwise的時候,


也是控制難樣本學習比重大,易樣本學習比重小。對於Y=0的時候,對於簡單樣本,Y冒號接近於0,對於難樣本,Y冒號趨於1,學習比重就大。

這個系數是作者改進增加的。當是目標中心的時候為1該項目為0,當偏移目標中心的時候,這個值越來越大,代表對於目標中心不懲罰,偏移目標中心越多懲罰的就越多。比如:

這篇博客講解的centernet loss部分講的比較好,可以看下:
https://zhuanlan.zhihu.com/p/66048276

1.4.6 彌補量化誤差

為了彌補由輸出步長造成的量化誤差,即由於取整導致的小數部分缺失造成的誤差。我們對每個關鍵點額外預測了一個定位的偏移量.

一個目標的所有的類別C共享相同的預測偏移量,偏移量損失用L1 loss表示如下:

1.4.7 目標長寬的回歸
表示類別為 Ck的目標k:

該目標的中心點表示如下:

對於每一個目標k回歸其尺寸

為了限制計算負擔,我們為所有目標種類使用單一的尺寸預測

意思就是只預測兩個channel一個是W一個是H,而不是一個類別對應兩個channel。也使用L1 loss:

1.4.8 總的loss

其中,

1.5 基於anchor的方法和centernet對比


傳統的基於anchor的檢測方法,通常選擇與標記框IoU大於0.7的作為positive,相反,IoU小於0.3的則標記為negative,如下圖a。這樣設定好box之后,在訓練過程中使positive和negative的box比例為1:3來減少negative box的比例。
而在CenterNet中,每個中心點對應一個目標的位置,不需要進行overlap的判斷。那么怎么去減少negative center pointer的比例呢?CenterNet是采用Focal Loss的思想,在實際訓練中,中心點的周圍其他點(negative center pointer)的損失則是經過衰減后的損失(上文提到的),而目標的長和寬是經過對應當前中心點的w和h回歸得到的.

1.6 推理跑前向

在推理階段,我們對每個類別(一個類別一個channel)先獨立地提取heatmap上的峰值點。我們檢測所有比周圍八臨域都大的像素點,論文中使用的是3x3的最大池化,每個類別保留100個。
預測的位置偏移量:

預測的尺寸:​

所以,矩形框可以表示如下:

全部的輸出直接通過關鍵點估計來產生,不需要根據IOU來進行非極大值抑制算法或其他后處理算法。峰值關鍵點的提取作用就是充分的NMS的替代項,而且通過3x3的最大池化操作來被高效的執行。

2.centernet代碼---數據處理部分

2.1透視變換1---變換原圖到固定尺寸512

初始換關鍵參數c,s

c = np.array([img.shape[1] / 2., img.shape[0] / 2.], dtype=np.float32)
s = max(img.shape[0], img.shape[1]) * 1.0

隨機變換關鍵參數c,s

s = s * np.random.choice(np.arange(0.6, 1.4, 0.1))
w_border = self._get_border(128, img.shape[1])
h_border = self._get_border(128, img.shape[0])
c[0] = np.random.randint(low=w_border, high=img.shape[1] - w_border)
c[1] = np.random.randint(low=h_border, high=img.shape[0] - h_border)

根據c和s確定三組點,求得透視變換矩陣

trans_input = get_affine_transform(
  c, s, 0, [input_w, input_h])
inp = cv2.warpAffine(img, trans_input, 
                     (input_w, input_h),
                     flags=cv2.INTER_LINEAR)
def get_affine_transform(center,
                         scale,
                         rot,
                         output_size,
                         shift=np.array([0, 0], dtype=np.float32),
                         inv=0):
    if not isinstance(scale, np.ndarray) and not isinstance(scale, list):
        scale = np.array([scale, scale], dtype=np.float32)

    scale_tmp = scale
    src_w = scale_tmp[0]
    dst_w = output_size[0]
    dst_h = output_size[1]

    rot_rad = np.pi * rot / 180
    src_dir = get_dir([0, src_w * -0.5], rot_rad)
    dst_dir = np.array([0, dst_w * -0.5], np.float32)
    src = np.zeros((3, 2), dtype=np.float32)
    dst = np.zeros((3, 2), dtype=np.float32)
    src[0, :] = center #中心點
    src[1, :] = center + src_dir  # #中心點上面y-T
    dst[0, :] = [dst_w * 0.5, dst_h * 0.5]
    dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5], np.float32) + dst_dir

    src[2:, :] = get_3rd_point(src[0, :], src[1, :]) #中心點上面左邊 y-T  x-T
    dst[2:, :] = get_3rd_point(dst[0, :], dst[1, :])

    if inv:
        trans = cv2.getAffineTransform(np.float32(dst), np.float32(src))
    else:
        trans = cv2.getAffineTransform(np.float32(src), np.float32(dst))

    return trans

注意這里的c和s,c是在中心點范圍內一個隨機抖動,s也是會隨機忽大忽小,導致在原圖上面確定的三個點會隨機的超過圖像范圍,比如會有負數。見圖片:





可以看到,黃色點都是固定的三個位置,中心點和上面,左上。藍色點中心點和s會有隨機擾動。但是不管你怎么擾動,都映射到固定的3個點。
第一張圖是原圖,第二張是我處理過的,根據算出來的pt,如果是負數就補黑邊,第三張圖是固定的512*512大小,輸入網絡用的。

2.2透視變換2---變換gt坐標到固定尺寸512/4=128

output_h = input_h // self.opt.down_ratio  # 512/4=128
output_w = input_w // self.opt.down_ratio
num_classes = self.num_classes
trans_output = get_affine_transform(c, s, 0, [output_w, output_h])
hm = np.zeros((num_classes, output_h, output_w), dtype=np.float32)
wh = np.zeros((self.max_objs, 2), dtype=np.float32)
reg = np.zeros((self.max_objs, 2), dtype=np.float32)
ind = np.zeros((self.max_objs), dtype=np.int64)
reg_mask = np.zeros((self.max_objs), dtype=np.uint8)
bbox[:2] = affine_transform(bbox[:2], trans_output)
bbox[2:] = affine_transform(bbox[2:], trans_output)
h, w = bbox[3] - bbox[1], bbox[2] - bbox[0]
if h > 0 and w > 0:
  radius = gaussian_radius((math.ceil(h), math.ceil(w)))
  radius = max(0, int(radius))
  radius = self.opt.hm_gauss if self.opt.mse_loss else radius
  ct = np.array(
    [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], dtype=np.float32)
  ct_int = ct.astype(np.int32)
  draw_gaussian(hm[cls_id], ct_int, radius)
  wh[k] = 1. * w, 1. * h
  ind[k] = ct_int[1] * output_w + ct_int[0]
  reg[k] = ct - ct_int
  reg_mask[k] = 1
ret = {'input': inp, 'hm': hm, 'reg_mask': reg_mask, 'ind': ind, 'wh': wh}

上面就是訓練數據制作,hm,wh, reg, ind, mask.
Ground truth 熱力圖:

假設我們有30類,不包括背景
hm [30,128,128] wh [128,2] reg [128,2] ind[128] reg_mask[128]
128是預設的目標數量。
hm是高斯中心點熱力圖,一類對應一張圖。
wh是目標的長寬。
reg是中心點坐標由於取整帶來的偏差。
Ind是中心點相對於原圖的位置,相當於把一張圖片像素拉成一列,目標中心點坐標在第幾個位置。
reg_mask是記錄預設的128個目標的到底有無目標的標記,有目標是1。
有幾個重要函數:
計算高斯核半徑函數:

radius = gaussian_radius((math.ceil(h), math.ceil(w)))

heatmap 上使用高斯核有很多需要注意的細節。CenterNet 官方版本實際上是在 CornerNet 的基礎上改動得到的,有很多祖傳代碼。在使用高斯核前要考慮這樣一個問題。下圖來自於 CornerNet 論文中的圖示,紅色的是標注框,但綠色的其實也可以作為最終的檢測結果保留下來。那么這個問題可以轉化為綠框在紅框多大范圍以內可以被接受。使用 IOU 來衡量紅框和綠框的貼合程度,當兩者 IOU>0.7的時候,認為綠框也可以被接受,反之則不被接受。

那么現在問題轉化為,如何確定半徑 r, 讓紅框和綠框的 IOU 大於 0.7 。

以上是三種情況,其中藍框代表標注框,橙色代表可能滿足要求的框。這個問題最終變為了一個一元二次方程有解的問題,同時由於半徑必須為正數,所以 r 的取值就可以通過求根公式獲得。

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 * 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)

可以看到這里的公式和上圖計算的結果是一致的。需要說明的是,CornerNet 最開始版本中這里出現了錯誤,分母不是 2a,而是直接設置為 2 。
CenterNet也延續了這個 bug,CenterNet 作者回應說這個bug 對結果的影響不大。但是,思考一個問題:
Centernet為什么要沿用cornernet的高斯半徑計算方式?
cornernet只是為了獲得左上和右下兩個點,然后約束的三種方式進行高斯半徑計算,centernet只是為了獲得中心點的高斯半徑,和兩個點沒有關系。查詢了CenterNet論文還有官方實現的issue,其實沒有明確指出為何要用CornerNet的半徑,issue中回復也說是這是沿用了CornerNet的祖傳代碼。
看看后面用到半徑的代碼部分:在centerNet中,半徑的存在主要是用於計算高斯分布的sigma值,而這個值也是一個經驗性判定結果。
sigma=diameter / 6

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)
  
  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

舉例,當R=3時候:


畫高斯圖腳本:

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()

之前很多人在知乎上issue里討論這個半徑計算的時候,有提到這樣的問題,就是如果將CenterNet對應的2a改正確了,反而效果會差。
我覺得這個問題可能和這里的sigma=diameter / 6有一定的關系,作者當時用祖傳代碼(2a那部分有錯)進行調參,然后確定了sigma。這時這個sigma就和祖傳代碼是對應的,如果修改了祖傳代碼,同樣也需要改一下sigma或者調一下參數。
我在github上面也提問了:

Hi,Thanks for raising this up. It's a good point. Ideally, it should be different from the setting in CornerNet. However, it might not be important as in the modified focal loss, we calculate 1 - (1 - x) ** 4 of the gaussian weight as the negative weight. This makes a large circle near the center all close to 1.
Also, I have played a bit about different radius formulation, it turned out (h * w) ** 0.5 * 0.1 can work similarly well. I guess the exact form of the radius is not important.
Best,
Xingyi

3.centernet代碼---骨干網絡dla-34

Deep Layer Aggregation提出兩種抽象的網絡架構:Stage之間使用iterative deep aggregation【IDA】,Stage內使用Hierarchical deep aggregation【HDA】,稱為迭代聚合以及層次聚合。
IDA主要進行跨分辨率和尺度的融合,而HDA主要融合各個模組和通道的特征圖(原文為feature)。IDA根據基礎網絡結構,逐級提煉分辨率和聚合尺度(類似於殘差模塊)。HDA整合其自身的樹狀連接結構,並將各個層級聚合為不同等級的表征(空間尺度的融合,類似於FPN)。本文的策略可以通過混合使用IDA與HDA來共同提升效果。

語義融合與空間融合的區別:
語義融合:
融合的是不同層級間的通道內信息;
通道大多在通道數上不同,空間尺度上相同,不需要尺度對齊;
主要保留微觀信息。
空間融合:
融合的是不同層級間的特征圖信息;
通道數是相同的,空間尺度上等比縮放,需要尺度對齊,也就是文中所說的均一化和標准化,主要保留宏觀信息;
什么是聚合,與融合有什么不同?
融合分成語義融合和空間融合,語義(物體信息)融合能夠推斷“是什么”,空間融合能夠推斷“在哪里”。而聚合就是語義融合和空間融合的組合。
IDA和HDA分別有什么優點?
IDA是一種迭代式聚合,從圖(c)可以看到前面淺層信息不斷地與后面的深層信息聚合,這樣子,前面的淺層信息能夠不斷地提煉,能夠豐富的語義信息。但IDA呈序列性,難以組合不同塊的信息。HDA是一種樹狀結構塊的分層聚合,以更好地學習跨越不同深度的網絡特征層次的豐富(空間)信息。所以IDA和HDA的結構既能學習到語義信息也能學習到空間信息。

優點(1)融合分辨率和尺寸(2)避免最淺層的部分會對最終結果產生最深遠影響。
Centernet網絡概圖:

(hm): Sequential(
   (0): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
   (1): ReLU(inplace)
   (2): Conv2d(256, 38, kernel_size=(1, 1), stride=(1, 1))
 )
 (wh): Sequential(
   (0): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
   (1): ReLU(inplace)
   (2): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
 )
 (reg): Sequential(
   (0): Conv2d(64, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
   (1): ReLU(inplace)
   (2): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
 )

heatmap: 38代表類別數,一個類別對應一個通道
wh:2代表回歸的中心對應的長和寬
reg: 2代表中心點坐標x,y對應的偏移量
作者提供的基網絡還有hourglass,resnet,還有加了可變形卷積的dcn。有興趣的可以自行研究。

4.centernet代碼---損失函數

4.1heatmap loss

output['hm'] = _sigmoid(output['hm'])
hm_loss += self.crit(output['hm'], batch['hm'])

這里output[‘hm’]形狀是[1,38,128,128]
batch[‘hm’]形狀是[1,38,128,128]

def _neg_loss(pred, gt):
  pos_inds = gt.eq(1).float()
  neg_inds = gt.lt(1).float()
  neg_weights = torch.pow(1 - gt, 4)
  loss = 0
  pos_loss = torch.log(pred) * torch.pow(1 - pred, 2) * pos_inds
  neg_loss = torch.log(1 - pred) * torch.pow(pred, 2) * neg_weights * neg_inds
  num_pos  = pos_inds.float().sum()
  pos_loss = pos_loss.sum()
  neg_loss = neg_loss.sum()
  if num_pos == 0:
    loss = loss - neg_loss
  else:
    loss = loss - (pos_loss + neg_loss) / num_pos
  return loss

neg_weights是很重要的系數,1-gt的4次方,表明在中心點的時候不需要懲罰,但是隨着偏離中心點越多,這個系數就越大,懲罰就越大。

4.2wh loss

  wh_loss += self.crit_reg(
  output['wh'], batch['reg_mask'],
  batch['ind'], batch['wh'])

這里,output[‘wh’]形狀是[1,2,128,128]
batch[‘reg_mask’]形狀是[1,128]
batch[‘ind’]形狀是[1,128]
batch[‘wh’]形狀是[1,128,2]
由於output[‘wh’]與batch[‘wh’]形狀不一樣,需要根據ind到output[‘wh’]里面提取ind所記錄的位置上面的值。

class RegL1Loss(nn.Module):
  def __init__(self):
    super(RegL1Loss, self).__init__()
  
  def forward(self, output, mask, ind, target):
    pred = _transpose_and_gather_feat(output, ind) # [1,2,128,128]  [1,128]
# pred[1,128,2]   target[1,128,2]
    mask = mask.unsqueeze(2).expand_as(pred).float()
    loss = F.l1_loss(pred * mask, target * mask, size_average=False)
    loss = loss / (mask.sum() + 1e-4)
    return loss

def _gather_feat(feat, ind, mask=None):# #[1,128*128,2]  [1,128]
    dim  = feat.size(2) #2
    ind  = ind.unsqueeze(2).expand(ind.size(0), ind.size(1), dim) # [1,128,1]   [1,128,2]
    feat = feat.gather(1, ind) #[1,128,2]  -->> [1,128*128,2]  [1,128,2]
    return feat

def _transpose_and_gather_feat(feat, ind): # [1,2,128,128]  [1,128]
    feat = feat.permute(0, 2, 3, 1).contiguous() #[1,128,128,2]
    feat = feat.view(feat.size(0), -1, feat.size(3)) #[1,128*128,2]
    feat = _gather_feat(feat, ind)
    return feat

4.3reg loss



reg loss與wh loss一樣采用的是L1 loss

4.4centernet loss


5.centernet代碼--前向推理

5.1數據預處理

推理的時候c和s是固定的,不添加隨機操作。

new_height, new_width= image.shape[0:2]
inp_height, inp_width = 512,512
c = np.array([new_width / 2., new_height / 2.], dtype=np.float32)
s = max(height, width) * 1.0

trans_input = get_affine_transform(c, s, 0, [inp_width, inp_height])
inp_image = cv2.warpAffine(
    image, trans_input, (inp_width, inp_height),
    flags=cv2.INTER_LINEAR)
inp_image = ((inp_image / 255. - opt.mean) / opt.std).astype(np.float32)

images = inp_image.transpose(2, 0, 1).reshape(1, 3, inp_height, inp_width)

images = torch.from_numpy(images)

meta = {'c': c, 's': s,
        'out_height': inp_height // 4,
        'out_width': inp_width // 4}
return images, meta




在推理的時候三個點按照固定的規則取點,不做隨機擾動,可以看到,中心點,和s,右邊圖是仿射變換變換之后的固定512大小的。
效果圖看來好像是把圖像拉到中間。

5.2模型推理

output = model(images) 
hm = output['hm'].sigmoid_()  #[1,38,128,128]
wh = output['wh']  #[1,2,128,128]
reg = output['reg'] #[1,2,128,128]

5.3解碼

def _nms(heat, kernel=3):
    pad = (kernel - 1) // 2
    hmax = nn.functional.max_pool2d(
        heat, (kernel, kernel), stride=1, padding=pad)
    keep = (hmax == heat).float()
    return heat * keep
heat = _nms(heat)
scores, inds, clses, ys, xs = _topk(heat, K=K) # all is [1,100]
if reg is not None:
  reg = _transpose_and_gather_feat(reg, inds)
  reg = reg.view(batch, K, 2)
  xs = xs.view(batch, K, 1) + reg[:, :, 0:1]
  ys = ys.view(batch, K, 1) + reg[:, :, 1:2]
wh = _transpose_and_gather_feat(wh, inds)
wh = wh.view(batch, K, 2)
clses  = clses.view(batch, K, 1).float()
scores = scores.view(batch, K, 1)
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)
detections = torch.cat([bboxes, scores, clses], dim=2)

5.4坐標映射到原圖

def transform_preds_my(coords,trans):
    target_coords = np.zeros(coords.shape)
    # trans = get_affine_transform(center, scale, 0, output_size, inv=1)
    for p in range(coords.shape[0]):
        target_coords[p, 0:2] = affine_transform(coords[p, 0:2], trans)
    return target_coords
def ctdet_post_process_my(dets, c, s, h, w, conf_thresh):
#dets [x1,y1,x2,y2,score,cls]
  trans = get_affine_transform(c[0], s[0], 0, (w, h), inv=1)
  a = dets[0][[dets[0][:, 4] >= conf_thresh]]#去除分數低的
  a[:, :2] = transform_preds_my(
        a[:, 0:2],trans)
  a[:, 2:4] = transform_preds_my(
        a[:, 2:4],trans)
  return a    ##a[x1,y1,x2,y2,score,cls]

6.實驗部分

自己的三個數據集訓練測試結果。注意:本次實驗訓練都是用的尺寸512,dla-34(nodcn)的centernet。也訓練過dla-34(dcn)的,但是收斂loss沒有nodcn的低。

可以看出:
(1)pytorch和libtorch的mAp和顯存是一致的,時間libtorch會少。
(2)centernet的精度比refinedet稍微低2個點以內。但是在數據集3會高。數據集3中第6類refinedet的map為0,centernet為52.
(3)libtorch版本的centernet和refinedet的顯存差不多,整體運行時間centernet會比refinedet運行時間少10ms以上。

7.一些問題

7.1推理的時候坐標變換的問題


如圖,是推理的時候也同樣三個點做透視變換。把輸入圖固定為512.
然后模型推理輸出是128的heatmap熱力圖,然后再把坐標直接映射到原圖了?按照上面的,應該是映射到帶黑色的中間的這張圖?
我一開始就是這么認為的,應該就是把坐標映射到中間這張圖了,然而現象就是映射到原圖。

def pre_process(opt, image, scale=1.0, meta=None):
    height, width = image.shape[0:2]
    new_height = int(height * scale)
    new_width = int(width * scale)
    if 1:# if self.opt.fix_res:
        inp_height, inp_width = opt.input_h,opt.input_w
        c = np.array([new_width / 2., new_height / 2.], dtype=np.float32)
        s = max(height, width) * 1.0
    trans_input = get_affine_transform(c, s, 0, [inp_width, inp_height])

    inp_image = cv2.warpAffine(
        image, trans_input, (inp_width, inp_height),
        flags=cv2.INTER_LINEAR)
    inp_image = ((inp_image / 255. - opt.mean) / opt.std).astype(np.float32)
    images = inp_image.transpose(2, 0, 1).reshape(1, 3, inp_height, inp_width)
    images = torch.from_numpy(images)
    meta = {'c': c, 's': s,
            'out_height': inp_height // opt.down_ratio,
            'out_width': inp_width // opt.down_ratio}
    return images, meta

這里根據c和s確定三個點算法透視變換矩陣把原始圖片透視到512大小。並把c和s傳出供后續反透視變換。
模型推理出的坐標都是相對於heatmap熱力圖128大小的。

def ctdet_post_process_my(dets, c, s, h, w, conf_thresh):
  trans = get_affine_transform(c[0], s[0], 0, (w, h), inv=1)
  a = dets[0][[dets[0][:, 4] >= conf_thresh]]

  a[:, :2] = transform_preds_my(
        a[:, 0:2],trans)
  a[:, 2:4] = transform_preds_my(
        a[:, 2:4],trans)
  return a

可以看到這里,c和s傳進來,但是這里的w,h是128,inv為1了,是說需要把src和dst的坐標互換。 把128大小的圖像映射到以c和s為坐標系的圖像。
根據現象強行解釋一波:
中間圖就是c和s的圖像,但是這個c和s有負數,負數的基准還是原圖,相對於原圖的。所以這里其實就是映射到原圖的坐標。

7.2中心點重合問題

CenterNet中的沖突是目標框的中心點重合(基於輸出特征層計算的中心點),作者從COCO數據集的統計信息來看,這種重合框的比例非常少,不到0.1%,基本上不會對訓練穩定產生太大影響,因此沒有針對這個進行解決。
對於不同類別的中心點重合目前代碼里面參數self.opt.cat_spec_wh打開就可以,對於同類別中心點重合沒法解決。

cat_spec_wh = np.zeros((self.max_objs, num_classes * 2), dtype=np.float32)

cat_spec_wh[k, cls_id * 2: cls_id * 2 + 2] = wh[k]

小弟不才,同時謝謝友情贊助!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM