前言
本文仅根据 MEGA《Memory enhanced global-local aggregation for video object detection》的代码进行解读。MEGA 基于二阶段方法,主要想看如何把目标位置编码结合到特征融合中。
通过对上一篇 YOLOV++ 和本文的 MEGA 进行调研,当前比较主流的 VID 方案都是通过视频中的其他帧对当前需要检测的帧以特征融合的方式优化检测。融合特征并非是对全图的特征图进行融合,而是每张图出一些候选目标(Anchor、Region Proposal),将他们的特征融合到当前帧的候选目标特征中,再通过 Head 进行预测。
推理流程概述
本节我们暂时跳过各种计算的细节,先看看检测当前帧时需要融合哪些帧的信息。
上方视频模拟了对一个总帧数为 60 帧的视频的检测流程,其中 Key 帧代表当前检测的帧。一共有三组图像会参与:
(1)Global:一共10帧,从视频所有帧中随机抽取的
(2)Local:一共25帧,Key 帧前后相邻的帧,且包含 Key 帧本身
(3)Memory:一共25帧,最初是空的,会在过去的检测过程中逐渐填充队列
Two-Stage 网络结构
省略 Backbone 的结构,大致是用 ResNet 提取图像特征
RPN
后续会先根据 obj
得分挑选出 Top6000,然后通过 NMS 得到 300 或 75 个 Proposals(对于 Key 帧为300个,其他用于融合的帧为75个)。
ROI
这里附带了最后的两个全连接层检测头,单帧检测基本按此流程得到最终结果,而 MEGA 就是对 Proposals Features [300,1024]
进行特征融合后再用检测头得到结果。
特征融合流程
整个融合的过程分为好几个阶段,但是融合方式都是利用 Attention。我们先跳过 Attention 中具体的计算方式,先看究竟是什么特征之间在融合,梳理整体融合的流程。
1. Global
首先梳理手头的素材,仅看各个 Proposals 的特征
feat_key 300,1024
feat_local 1875,1024 1875 = 25 * 75
feat_local_dis 675,1024 675 = 25 * 15
其实就是从原本的75个Proposals中选择前15个,也就是obj得分更高的15个
feat_global 750,1024 750 = 10 * 75
当前所有特征都仅来自单张图像,通过 Attention('global', idx=0)
(idx 代表对权重的索引,不同阶段使用的权重不同,但相同阶段对不同特征之间融合使用的权重相同),将 feat_global
融合入前三组特征。
2. Local
Local 阶段包含 3 个子阶段,并且涉及 Memory 较为复杂。
为了方便说明,暂且忽略 Memory,每个子阶段的融合方式都是 Attention('local', idx=stage)
,将 feats_ref
融入 feats_cur
,新得到的 feats_cur
决定了下一次需要融合的特征,具体如下:
stage1
feats_cur 675,1024 key+local_dis
feats_ref 1875,1024 local
stage2
feats_cur = feats_cur 675,1024 key+local_dis
feats_ref = feats_cur[300:] 375,1024 local_dis
stage3
feats_cur = feats_cur[:300] 300,1024 key
feats_ref = feats_cur[300:] 375,1024 local_dis
现在说明一下 Memory,Memory 构造是 3 个长度为 25 的队列,也就对应了 3 个 stage。每次融合特征前会先获取当前 Memory 中的特征参与融合,然后用当前阶段的 feats_ref
中的第一帧的特征来更新 Memory,即加入到队列(也就是对应 Local 队列中第一帧的特征,并且这个特征经过了各种融合)。获取的 Memory 特征会与 feats_ref
拼接到一起(torch.cat([feats_ref, memory["feats"]], dim=0)
),融入 feats_cur
。
3. Global
经过上述融合后得到了 feats_cur
作为当前帧的特征,于是通过 Attention('global', idx=1)
再次融入 Global 特征(idx 不同)。至此,接上前文中的检测头即可获得检测结果。
融合模块
QKV
这里以 Global Attention 为例进行说明,把两组特征转化为 QKV。Position 代表的是位置编码,仅在 Local Attention 中使用,大致理解就是用一个 64 维向量来衡量这些 Proposal 在图中的位置关系。
Attention
这里面红色的 Add 部分仅在 Local Attention 中使用,也就是 Global Attention 在 Permute 后直接连接 Softmax。
与常规 Attention 不同之处主要在于 u
,从代码来看并不能理解其用途,单纯是可训练的参数。
us = []
for i in range(self.stage):
us.append(nn.Parameter(torch.Tensor(self.groups, 1, self.embed_dim)))
for weight in [us[i]]:
torch.nn.init.normal_(weight, std=0.01)
Position
下面是计算 Position 的总流程,输入为 Proposals 的坐标,首先计算各个坐标 xywh 之间的差异 position_matrix
,然后将差异传化为 64 维的向量 position_embedding
,最后对维度进行调整。
def cal_position_embedding(self, rois1, rois2):
# [num_rois, num_nongt_rois, 4]
position_matrix = self.extract_position_matrix(rois1, rois2)
# [num_rois, num_nongt_rois, 64]
position_embedding = self.extract_position_embedding(position_matrix, feat_dim=64)
# [64, num_rois, num_nongt_rois]
position_embedding = position_embedding.permute(2, 0, 1)
# [1, 64, num_rois, num_nongt_rois]
position_embedding = position_embedding.unsqueeze(0)
return position_embedding
(1)matrix
d
x
=
log
∣
(
x
1
−
x
2
)
/
w
1
∣
dx=\log{|(x_1-x_2) / w_1|}
dx=log∣(x1−x2)/w1∣
d
y
=
log
∣
(
y
1
−
y
2
)
/
h
1
∣
dy=\log{|(y_1-y_2) / h_1|}
dy=log∣(y1−y2)/h1∣
d
w
=
log
(
w
1
/
w
2
)
dw=\log{(w_1/w_2)}
dw=log(w1/w2)
d
h
=
log
(
h
1
/
h
2
)
dh=\log{(h_1/h_2)}
dh=log(h1/h2)
delta_x = center_x - center_x_ref.transpose(0, 1)
delta_x = delta_x / bbox_width
delta_x = (delta_x.abs() + 1e-3).log()
delta_width = bbox_width / bbox_width_ref.transpose(0, 1)
delta_width = delta_width.log()
(2)embedding
def extract_position_embedding(position_mat, feat_dim, wave_length=1000.0):
"""
position_mat: [n1, n2, 4]
feat_dim: 64
"""
device = position_mat.device
feat_range = torch.arange(0, feat_dim / 8, device=device)
dim_mat = torch.full((len(feat_range),), wave_length, device=device).pow(8.0 / feat_dim * feat_range)
dim_mat = dim_mat.view(1, 1, 1, -1).expand(*position_mat.shape, -1)
"""
dim_mat: [1.0000, 2.3714, 5.6234, 13.3352, 31.6228, 74.9894, 177.8279, 421.6965]
1000 的 [0, 1/8, 2/8, ..., 7/8] 次方
expand 至 [n1, n2, 4, 8] 大小
后续将 xywh 都复制8份分别除以 dim_mat 中的值后计算 sin 和 cos
于是 xywh 都有16个数值, 累计64个数值作为 Position 的特征向量
"""
position_mat = position_mat.unsqueeze(3).expand(-1, -1, -1, dim_mat.shape[3])
position_mat = position_mat * 100.0
div_mat = position_mat / dim_mat
sin_mat, cos_mat = div_mat.sin(), div_mat.cos()
embedding = torch.cat([sin_mat, cos_mat], dim=3)
embedding = embedding.reshape(embedding.shape[0], embedding.shape[1], embedding.shape[2] * embedding.shape[3])
return embedding