前言:
前段时间读了一些基于Transformer的方法,和它们一样,TraDeS同样也是端到端的方式,主要基于DCN和CenterNet,它最大的亮点是用跟踪结果来辅助检测,进而在困难场景下有更好的表现。而且TraDeS可以解决2D、3D和分割的问题。
论文: 论文
0.Abstract
前面提到,TraDeS仍然是联合检测跟踪(Joint detection and tracking,JDT)的模型,它利用跟踪的线索来辅助检测。它利用cost volume来预测跟踪偏移,这个跟踪偏移也被用来传播之前目标的信息,从而提高当前帧的检测和分割效果。
cost volume解释:
cost volume是立体视觉中的名词,用于深度估计和光流估计。它的本意是衡量双目匹配中衡量左右视图相似性的一个4D tensor,在本文中也是一个4D tensor,衡量两帧之间嵌入特征的相似度。
\space
1.Introduction
首先diss TBD(Tracking by detection)的缺点,常规操作了。
之后说当前的JDT算法有两个问题:
-
检测步骤仍然比较独立,并没有利用好跟踪的信息。作者认为跟踪结果有助于检测,尤其是一些困难的场景(例如部分遮挡和运动模糊)
-
通常的Re-ID loss和detection loss在一个骨干网络中并不兼容,甚至会损伤检测的精度。原因是re-ID主要关注类内方差,而检测的目的是扩大类间差异,最小化类内方差。
最后一个观点跟FairMOT中提到的类似。FairMOT的文章中说对于将Re-ID任务视为另一阶段的任务(也就是Re-ID是个相对独立的任务),这样Re-ID的精度会很大程度上依赖于检测的精度,且两个任务会相互竞争。
\space
TraDes解决上述两个问题的方法是将跟踪结合到检测中,并且用一个自习设计的Re-ID的学习策略。
具体地,作者提出了基于Cost volume的关联模块(Cost Volume based Association,CVA)和运动指导的特征整合模块(Motion-guided Feature Warper,MFW)。
与CenterNet相似,TraDeS中的feature map中的点不是代表目标就是代表背景。
CVA和MFW的大致工作流程:
-
CVA逐点(point wise)通过主干网络提取Re-ID的特征,来构建一个cost volume,这个cost volume可以存储相邻两帧的嵌入特征对的匹配相似度。
-
之后,用cost volume来推算追踪偏移(tracking offset),也就是所有两帧中潜在目标中心的时空位移。追踪偏移和嵌入特征一起,用来指导简单的两轮长时间的数据关联。
-
之后,MFW将追踪偏移作为运动线索来将前一帧的目标特征传递到当前帧。
-
最后,传递过来的特征和当前特征一起被用来得到检测和分割。
大体的工作流程如下(数学细节见第3部分):
\space
之前说Re-ID和检测不能很好兼容的原因是Re-ID关注类内方差,而检测关注类间方差。作者提出的CVA模块中cost volume监督Re-ID的嵌入特征,隐式地考虑了不同目标类别和背景区域。因此Re-ID就可以较多地关注到类间方差。
此外,由于追踪偏移是基于外观特征相似度来预测的,因此在快速运动场景、低帧率、跨数据集(train和test用不同的数据集)都可以取得比较好的结果。因此,预测的追踪偏移就可以作为一个鲁棒的运动线索来指导MFW中的特征传播。当前帧被遮挡或者模糊的目标在之前的帧中可能是清晰的,所以通过MFW模块,前帧特征传播可以支撑当前帧特征去恢复潜在的不可见目标。
\space
2. Preliminaries
由于TraDeS是基于CenterNet的,因此在这里简单介绍了一下CenterNet。
对于三通道输入图像 I ∈ R H × W × 3 \bm I\in \mathbb R^{H \times W \times 3} I∈RH×W×3并且产生feature map f = ϕ ( I ) ∈ R H / 4 × W / 4 × 64 \bm f=\phi(\bm I)\in \mathbb R^{H/4 \times W/4 \times 64} f=ϕ(I)∈RH/4×W/4×64.
在产生feature map之后,一系列卷积head分支就会产生热度图 P ∈ R H / 4 × W / 4 × N c l s \bm P\in \mathbb R^{H/4 \times W/4 \times N_{cls}} P∈RH/4×W/4×Ncls(其中 N c l s N_{cls} Ncls是类别的数目)和其他为特定任务服务的maps,例如2D和3D的目标大小map。
在CenterNet的基础上,TraDeS增加了一个分支,来预测tracking offset map O B ∈ R H / 4 × W / 4 × 2 \bm O^B \in \mathbb R^{H/4 \times W/4 \times 2} OB∈RH/4×W/4×2, O B \bm O^B OB计算的是所有点在 t t t时刻相对于 t − τ t-\tau t−τ时刻的时空位移。
\space
3.TraDeS tracker
这个图…无力吐槽
待补充 有空了回来写(精华都在开头的图里了)。这篇文章的启示意义比较大,用Cost volume来衡量了两时刻之间目标的位移,就是“用追踪结果反哺检测”的所在。
之前一些基于Transformer的方法在帧间传递信息的方法是传递queries,或者也将多帧的特征进行融合。TraDeS用了一种可解释性更强和更显式的方式。
最终的损失函数与CenterNet相似,但是多了一个CVA损失。CVA损失也是交叉熵的形式,如果当前帧 t t t目标 ( i , j ) (i,j) (i,j)在 t − τ t-\tau t−τ时刻的时候在 ( k , l ) (k,l) (k,l)处,记 Y i , j , k , l = 1 Y_{i,j,k,l}=1 Yi,j,k,l=1,否则为0.当 Y i , j , k , l = 1 Y_{i,j,k,l}=1 Yi,j,k,l=1时,loss取将cost volume池化后的向量 C i , j , l W C_{i,j,l}^W Ci,j,lW和 C i , j , k H C_{i,j,k}^H Ci,j,kH的加权对数和,否则为0,如下式所示:
最终的loss为:
L = L C V A + L d e t + L m a s k L=L_{CVA}+L_{det}+L_{mask} L=LCVA+Ldet+Lmask
其中 L d e t L_{det} Ldet是2D和3D检测损失, L m a s k L_{mask} Lmask是分割损失。
4. 代码解读
由于TraDeS结构有点复杂, 因此对代码作简单解读以备忘
顺着执行顺序整理.
4.1 前向传播
主要的模型都在在src/lib/model/base_model.py中. 我们先看forward函数:
forward函数首先提取当前图像特征, 如图中的 P h i Phi Phi部分, 以DLA34作为backbone:
def forward(self, x, pre_img=None, pre_hm=None, addtional_pre_imgs=None, addtional_pre_hms=None, inference_feats=None):
"""
x: 当前图像
pre_img: 前一帧图像
pre_hm: 前一帧head embedding
addtional_pre_imgs: 更多的之前的图像
addtional_pre_hms: 更多的之前的图像的head embedding
inference_feats: 训练时传入的为None
"""
cur_feat = self.img2feats(x) # 转换为特征图
其中
def img2feats(self, x):
x = self.base(x) # DLA34网络
x = self.dla_up(x) # DLAup
y = []
for i in range(self.last_level - self.first_level):
y.append(x[i].clone())
self.ida_up(y, 0, len(y)) # IDAup
return [y[-1]]
随后进入TraDeS的剩余部分, 被封装在self.TraDeS中.
if self.opt.trades:
feats, embedding, tracking_offset, dis_volume, h_volume_aux, w_volume_aux \
= self.TraDeS(cur_feat, pre_img, pre_hm, addtional_pre_imgs, addtional_pre_hms, inference_feats) # TraDes主要模块 前向传播
在得到加强特征feats后, 进入不同的head进行不同任务的推理:
if self.opt.model_output_list: # 转为onnx才会用到
for s in range(self.num_stacks):
z = []
for head in sorted(self.heads):
z.append(self.__getattr__(head)(feats[s]))
out.append(z)
else: # 正常走这里
for s in range(self.num_stacks):
z = {
}
if self.opt.trades:
z['embedding'] = embedding
z['tracking_offset'] = tracking_offset
if not self.opt.inference:
z['h_volume'] = dis_volume[0]
z['w_volume'] = dis_volume[1]
assert len(h_volume_aux) == self.opt.clip_len - 2
for temporal_id in range(2, self.opt.clip_len):
z['h_volume_prev{}'.format(temporal_id)] = h_volume_aux[temporal_id-2]
z['w_volume_prev{}'.format(temporal_id)] = w_volume_aux[temporal_id-2]
for head in self.heads:
z[head] = self.__getattr__(head)(feats[s]) # 进行不同任务的预测
out.append(z)
if self.opt.inference:
return out, cur_feat[0].detach().cpu().numpy()
else:
return out
4.2 TraDeS模块
下面来看self.TraDes(). 该部分的主要作用是把当前帧和过去帧的特征进行计算与打包, 传给CVA和MFW模块.
注意, MFW中的(5)式是在这部分计算的, 也就是代码里的support feature:
def TraDeS(self, cur_feat, pre_img, pre_hm, addtional_pre_imgs, addtional_pre_hms, inference_feats):
"""
cur_feat: 当前特征
pre_img, pre_hm, addtional_pre_imgs, addtional_pre_hms: 同self.forward()
inference_feats: None
"""
feat_list = [] # 存储帧的特征
feat_list.append(cur_feat[0]) # 首先加入当前帧特征
support_feats = [] # 论文(5)式 用以计算特征图和类别无关热度图的乘积
if self.opt.inference:
for prev_feat in inference_feats:
feat_list.append(torch.from_numpy(prev_feat).to(self.opt.device)[:, :, :, :])
while len(feat_list) < self.opt.clip_len: # only operate in the initial frame
feat_list.append(cur_feat[0])
for idx, feat_prev in enumerate(feat_list[1:]):
pre_hm_i = addtional_pre_hms[idx]
pre_hm_i = self.avgpool_stride4(pre_hm_i)
support_feats.append(pre_hm_i * feat_prev)
else:
feat2 = self.img2feats_prev(pre_img) # 获取之前帧的feature
pre_hm_1 = self.avgpool_stride4(pre_hm) # 为了减少计算量 进行池化
feat_list.append(feat2[0]) # 将之前帧的feature加入到feature list
support_feats.append(feat2[0] * pre_hm_1)
for ff in range(len(addtional_pre_imgs) - 1): # 同理 加入更多的之前帧的特征与support feature
feats_ff = self.img2feats_prev(addtional_pre_imgs[ff])
pre_hm_i = self.avgpool_stride4(addtional_pre_hms[ff])
feat_list.append(feats_ff[0][:, :, :, :]) # 构成之前多少帧feature的集合
support_feats.append(feats_ff[0][:, :, :, :]*pre_hm_i)
return self.CVA_MFW(feat_list, support_feats) # 主要模块 CVA + MFW
4.3 CVA_MFW模块
这是TraDeS的核心部分. 我们在有当前帧和之前帧的特征图后, 就要进入CVA进行运动的预测, 之后进入MFW对运动线索进一步预测, 和之前帧的特征结合得到更强的传播特征.
def CVA_MFW(self, feat_list, support_feats):
prop_feats = [] # 帧间传播的特征
attentions = [] # 为了计算特征增强的权重
h_max_for_loss_aux = []
w_max_for_loss_aux = []
feat_cur = feat_list[0] # 当前帧特征
batch_size = feat_cur.shape[0]
h_f = feat_cur.shape[2] # 当前特征图高
w_f = feat_cur.shape[3] # 当前特征图宽
h_c = int(h_f / 2)
w_c = int(w_f / 2)
prop_feats.append(feat_cur) # 传播特征列表先加入当前特征
embedding = self.embedconv(feat_cur) # embedding conv为图中的\sigma网络 三层卷积层组成
embedding_prime = self.maxpool_stride2(embedding) # 减少计算量, 进行池化
# (B, 128, H, W) -> (B, H*W, 128):
embedding_prime = embedding_prime.view(batch_size, self.embed_dim, -1).permute(0, 2, 1)
attention_cur = self.attention_cur(feat_cur) # 预测当前特征的类别无关热度图 self.attention_cur 为一层卷积 shape: (bs, H, W, 1)
attentions.append(attention_cur)
for idx, feat_prev in enumerate(feat_list[1:]): # 对于以前的帧
# Sec. 4.1: Cost Volume based Association
# 对当前帧进行与之前帧的cost volume计算 得到预测的运动tracking offset
c_h, c_w, tracking_offset = self.CVA(embedding_prime, feat_prev, batch_size, h_c, w_c)
# tracking offset output and CVA loss inputs
if idx == 0:
tracking_offset_output = tracking_offset
h_max_for_loss = c_h
w_max_for_loss = c_w
else:
h_max_for_loss_aux.append(c_h)
w_max_for_loss_aux.append(c_w)
# Sec. 4.2: Motion-guided Feature Warper
# 经过MFW 产生传播得到特征
prop_feat = self.MFW(support_feats[idx], tracking_offset, feat_cur, feat_prev, batch_size, h_f, w_f)
prop_feats.append(prop_feat)
attentions.append(self.attention_prev(prop_feat)) # 对传播特征也预测attention 用以计算特征增强的权重
attentions = torch.cat(attentions, dim=1) # (B,T,H,W) T为帧数
adaptive_weights = F.softmax(attentions, dim=1) # shape: (bs, 1, H, W)
adaptive_weights = torch.split(adaptive_weights, 1, dim=1) # 3*(B,1,H,W)
# feature aggregation (MFW)
enhanced_feat = 0
for i in range(len(adaptive_weights)):
enhanced_feat += adaptive_weights[i] * prop_feats[i] # 论文(6)式 为了解决遮挡等问题提出的特征增强
# adaptive_weights通过softmax计算 自动赋予特征的权重
return [enhanced_feat], embedding, tracking_offset_output, [h_max_for_loss, w_max_for_loss], h_max_for_loss_aux, w_max_for_loss_aux
其中的CVA模块:
def CVA(self, embedding_prime, feat_prev, batch_size, h_c, w_c):
embedding_prev = self.embedconv(feat_prev)
_embedding_prev = self.maxpool_stride2(embedding_prev)
_embedding_prev = _embedding_prev.view(batch_size, self.embed_dim, -1)
# Cost Volume Map
# 将当前帧的每个位置的embedding都与之前帧的进行相似度计算
c = torch.matmul(embedding_prime, _embedding_prev) # (B, H*W/4, H*W/4)
c = c.view(batch_size, h_c * w_c, h_c, w_c) # (B, H*W, H, W)
c_h = c.max(dim=3)[0] # (B, H*W, H) # 求W维度的最大值 即最大池化
c_w = c.max(dim=2)[0] # (B, H*W, W) # 求H维度的最大值 即最大池化
c_h_softmax = F.softmax(c_h * self.tempature, dim=2) # 在W维度作softmax
c_w_softmax = F.softmax(c_w * self.tempature, dim=2) # 在H维度作softmax
v = torch.tensor(self.v, device=self.opt.device) # (1, H*W, H)
m = torch.tensor(self.m, device=self.opt.device)
# 一通改变维度
off_h = torch.sum(c_h_softmax * v, dim=2, keepdim=True).permute(0, 2, 1) # 与模板相乘 预测offset
off_w = torch.sum(c_w_softmax * m, dim=2, keepdim=True).permute(0, 2, 1)
off_h = off_h.view(batch_size, 1, h_c, w_c)
off_w = off_w.view(batch_size, 1, h_c, w_c)
off_h = nn.functional.interpolate(off_h, scale_factor=2) # 插值
off_w = nn.functional.interpolate(off_w, scale_factor=2)
tracking_offset = torch.cat((off_w, off_h), dim=1)
return c_h, c_w, tracking_offset
MFW模块:
def MFW(self, support_feat, tracking_offset, feat_cur, feat_prev, batch_size, h_f, w_f):
# deformable conv offset input
# 将CVA预测的offset与特征图之差结合(结合通过concat实现)进一步对运动进行预测
off_deform = self.gamma(tracking_offset, feat_cur, feat_prev, batch_size, h_f, w_f)
mask_deform = torch.tensor(np.ones((batch_size, 9, off_deform.shape[2], off_deform.shape[3]),
dtype=np.float32)).to(self.opt.device)
# feature propagation
# 通过可变型卷积 以gamma输出的offset 和 与类别无关中心热度图相乘的特征 为输入 计算可变型卷积
prop_feat = self.dcn1_1(support_feat, off_deform, mask_deform) # 得到传播特征
return prop_feat