©PaperWeekly 原创 · 作者|黄飘
学校|华中科技大学硕士生
研究方向|多目标跟踪
最近基于深度学习的多目标跟踪算法越来越多,有用于特征提取的,有改进单目标跟踪器的,也有提升数据关联的。如果真的要总结的话那就太多了,所以我准备分类别进行介绍,这次我主要介绍端到端的数据关联方法。后期我会逐步整理这部分代码到我的 Github,相关 MOT 和数据关联的基础知识可以去我的专栏查看。
DAN
论文标题:Deep Affinity Network for Multiple Object Tracking
论文作者:ShiJie Sun, Naveed Akhtar, HuanSheng Song, Ajmal Mian, Mubarak Shah
备注信息:PAMI 2019
论文链接:https://arxiv.org/abs/1810.11780
代码链接:https://github.com/shijieS/SST.git
这是一篇创作于 2017.9,并刊登于 PAMI 2019 的端到端多目标跟踪框架,对于后续几篇文章都有着启示作用。DAN 框架 [1] 很简洁新颖:
作者提出这个框架的目的是将表观特征和数据关联算法结合形状端到端的联合框架,在 NVIDIA GeForce GTX Titan 上的效率为 6.7FPS,下面我结合文章内容具体地解析算法。
1.1 数据准备与增强
要实现端到端的训练,如何将多目标跟踪数据集整理出来是第一大难关。这里作者采用了单目标跟踪算法中常用的跨帧匹配方式,允许相隔 1~30 帧的的视频帧进行跨帧数据关联,这样就可以解决 MOT 样本数量不足的问题。
然而,视频类数据集还存在一个问题,即相邻帧的信息几乎一样,使得样本缺乏多样性。因此作者采用了几类数据增强方法:
作者在文中提到了三种数据增强方法:
光学扰动。即先将 RGB 图像转换到 HSV 格式,然后将 S 通道的每个值随机放缩 [0.7,1.5],最后转换到 RGB 格式,并将值域约束到 0~255 或者 0~1;
图像扩展放缩。本质上就是图像随机放缩,但是为了保证输入尺寸大小一致,作者在图像四周增加 [1,1.2] 倍的 padding,padding 像素值为图像均值;
随机裁剪。基于裁剪比例 [0.8,1] 在原图上裁剪,不过要求无论怎么裁剪,图像上所有目标框的中心点一定不能丢失,最后再放缩至指定尺寸。
而作者在代码中实际上使用了更复杂的数据增强方法:
self.augment = Compose([
ConvertFromInts(),
PhotometricDistort(),
Expand(self.mean),
RandomSampleCrop(),
RandomMirror(),
ToPercentCoords(),
Resize(self.size),
SubtractMeans(self.mean),
ResizeShuffleBoxes(),
FormatBoxes(),
ToTensor()
])
除去一些常规的数据类型转换和归一化操作,我们可以看到还多了一个随机水平镜像的操作RandomMirror
,并且以上操作的概率为 0.3。
除此之外,熟悉 MOT Challenge 数据集的人应该知道,在该数据集中,无论目标是否可见都会标注,因此作者将可视度低于 0.3 的目标视为不可见,即认为不存在。另外,端到端的框架要保证输入输出的尺寸不变,因此作者设定了每一帧中出现的目标数量上限 Nm,如 80。
1.2 特征提取器
特征提取器作为网络的第一个阶段,由上面的图示可以知道,作者利用孪生网络结构,通过特征提取器处理指定两帧中出现的所有目标框。特征提取器的结构为 VGG 网络+自定义扩展的结构组成:
为了利用多尺度多感受野信息,作者取了以上 9 层的特征图信息,得到了长度为 520 的特征向量,即 60+80+100+80+60+50+40+30+20,可以看到 520 这个数据就是 9 层特征通道之和。不过文中作者并没有介绍是如何处理这些特征的,我们通过代码来观察:
def forward_feature_extracter(self, x, l):
'''
extract features from the vgg layers and extra net
:param x:
:param l:
:return: the features
'''
s = list()
x = self.forward_vgg(x, self.vgg, s)
x = self.forward_extras(x, self.extras, s)
x = self.forward_selector_stacker1(s, l, self.selector)
return x
def forward_vgg(self, x, vgg, sources):
for k in range(16):
x = vgg[k](x)
sources.append(x)
for k in range(16, 23):
x = vgg[k](x)
sources.append(x)
for k in range(23, 35):
x = vgg[k](x)
sources.append(x)
return x
def forward_extras(self, x, extras, sources):
for k, v in enumerate(extras):
x = v(x) #x = F.relu(v(x), inplace=True) #done: relu is unnecessary.
if k % 6 == 3: #done: should select the output of BatchNormalization (-> k%6==2)
sources.append(x)
return x
def forward_selector_stacker1(self, sources, labels, selector):
'''
:param sources: [B, C, H, W]
:param labels: [B, N, 1, 1, 2]
:return: the connected feature
'''
sources = [
F.relu(net(x), inplace=True) for net, x in zip(selector, sources)
]
res = list()
for label_index in range(labels.size(1)):
label_res = list()
for source_index in range(len(sources)):
# [N, B, C, 1, 1]
label_res.append(
# [B, C, 1, 1]
F.grid_sample(sources[source_index], # [B, C, H, W]
labels[:, label_index, :] # [B, 1, 1, 2
).squeeze(2).squeeze(2)
)
res.append(torch.cat(label_res, 1))
return torch.stack(res, 1)
从代码可以看到,9 层不同分辨率的特征图都通过采样操作降维了,其中``source表示的是特征图BxCxHxW,
labels`表示的是一个框中心坐标 BxNx1x1x2,也就是说每个特征图都会采样每个框的映射位置。
因此最后通过得到就是 Bx(CxN) 的输出,也就是图中的 Nmx520,这个过程的确挺出乎我意料的。
1.3 亲和度估计
亲和度估计实际上就是特征相似度计算和数据关联过程的集成,第一步我们可以看到作者将两个 520xNm 大小的特征图变成了一个 1040xNmxNm 大小的亲和度矩阵,比较难理解:
这里我们观察一下作者的代码:
def forward_stacker2(self, stacker1_pre_output, sta