ICCV2019 官方源码
CenterNet通过预测每个目标的中心点,既而以中心点为基准进行回归宽和高,以及由于下采样带来的点的偏置。将目标检测用关键点检测的思路来做,抛弃了由anchor生成的大量需要被抑制的样本,故而不需要NMS做后处理,而且整个网络只有一个检测Head,不基于FPN为BackBone需要多个检测Head,整体速度就快了很多。
网络的总体结构可以化简为上图所示。其中backbone的结构放在最后讲解,对于一种新的检测思路,重点思路是在如何对label编码,如何对预测结果解码上。实际上论文的顺序也是最后介绍backbone。
假设我们已经得到了一个从backbone中得到的特征图,其shape为 R h , w , D R^{h, w, D} Rh,w,D, h和w是原图的1/4。这个特征经过检测Head,得到了三样东西
所以总的输出通道为C+4。相较于Anchor Base的检测网络,需要N* K*(C+4),是不是小了很多呢。N是检测头的数目,K是anchor的数目
三个分支的意义大家知道了,那三个分支对应的标签应该是啥呢?
在inference的过程中,得到三个分支的特征图之后,如何解码呢?
在介绍了三个分支的label编码之后,自然来谈谈loss。
总的loss是
l o s s = L h e a t m a p + L o f f s e t + 0.1 ∗ L s i z e loss = L_{heatmap}+L_{offset} + 0.1*L_{size} loss=Lheatmap+Loffset+0.1∗Lsize
作者也提到,中心点附近的其他位置并不是直接判负。而是用的reduced negative loss。就是说附近的点是负样本,但他们的label不是0.而是0-1的值。越接近0代表这个位置越可能是非目标的中心。
作者在论文中使用了三种backbone。分别是:
(a):hourglass ; (b):魔改的resNet; (d)作者魔改的DLA
按照惯例,还是来看看源码。对照论文验证一下。
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)))
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