ATOM(accurate tracking of overlap maximization)
ATOM的作者提出,近些年来的视觉跟踪的鲁棒性提升十分巨大,但跟踪的准确性的提升却被限制了。大多数人们努力研究,只为得到一个强大的分类器来区分前后景,就只用一个简答的多尺度搜索来估计目标的bbox(bounding box)。
作者认为单目标跟踪可以被分为两个子任务——目标分类和目标估计。目标分类旨在确定目标物体在图像某个位置的存在,但是只能得到目标状态的一部分信息,例如在图像中的坐标。而目标估计模块则是讲信息完善,得到准确的bbox。这因为若只是在最简单场景,比如目标至于摄像机平行移动,那么目标估计和目标分类并不会有太大的区别,而一般的场景下物体可能会在姿势和视角上经历彻底的变化,大大复杂化了bbox的估计,所以需要目标估计模块。
目标分类
此模块的任务是粗略地讲目标从背景干扰物之中区分出来,通过以从当前帧提取的骨干特征为基础的目标置信度得分来得到一个粗糙的2D位置,以ResNet-18为骨干网络.
分类网络用到了两层全卷积层,公式如下:
f
(
x
;
w
)
=
ϕ
2
(
w
2
∗
ϕ
1
(
w
1
∗
x
)
)
f(x ; w)=\phi_{2}\left(w_{2} * \phi_{1}\left(w_{1} * x\right)\right)
f(x;w)=ϕ2(w2∗ϕ1(w1∗x))
其中,
x
x
x表示骨干网络提取的特征,
w
w
w为网络权重,
(
∗
)
(*)
(∗)代表多通道卷积,
ϕ
\phi
ϕ激活函数,并且采用相关滤波的方法来计算loss,建立了
L
2
L^{2}
L2目标函数:
L
(
w
)
=
∑
j
=
1
m
γ
j
∥
f
(
x
j
;
w
)
−
y
j
∥
2
+
∑
k
λ
k
∥
w
k
∥
2
L(w)=\sum_{j=1}^{m} \gamma_{j}\left\|f\left(x_{j} ; w\right)-y_{j}\right\|^{2}+\sum_{k} \lambda_{k}\left\|w_{k}\right\|^{2}
L(w)=j=1∑mγj∥f(xj;w)−yj∥2+k∑λk∥wk∥2
其中,
y
y
y是回归目标,
γ
j
\gamma_{j}
γj是一个权重,表示着对应的训练样本的重要程度,
λ
\lambda
λ为正则项。作者在稳重特别指出,传统深度学习的梯度下降等算法的收敛速度相比较而言不是很快,它们不适合在线的深度学习training,所以作者设计了一种新的优化算法:
误差项和正则项的残差分别为:
r
j
(
w
)
=
Y
j
(
f
(
x
j
;
w
)
−
y
j
)
r_{j}(w)=\sqrt{Y_{j}}\left(f\left(x_{j} ; w\right)-y_{j}\right)
rj(w)=Yj(f(xj;w)−yj)
r
m
+
k
(
w
)
=
λ
k
w
k
r_{m+k}(w)=\sqrt{\lambda_{k}} w_{k}
rm+k(w)=λkwk
其中,
j
∈
{
1
,
…
,
m
}
,
k
=
1
,
2
j \in\{1, \ldots, m\},k={1,2}
j∈{1,…,m},k=1,2。
然后,目标函数即可写为
L
(
w
)
=
∥
r
(
w
)
∥
2
L(w)=\|r(w)\|^{2}
L(w)=∥r(w)∥2,作者又使用了二次牛顿-高斯近似,
r
(
w
)
r(w)
r(w)是拼接后的向量,再讲式子在参数
w
w
w处一阶泰勒展开,即可得到:
L
~
w
(
Δ
w
)
≈
L
(
w
+
Δ
w
)
=
∥
r
(
w
+
Δ
w
)
∥
2
≈
∥
r
w
+
J
w
Δ
w
∥
2
=
Δ
w
T
J
w
T
J
w
Δ
w
+
2
Δ
w
T
J
w
T
r
w
+
r
w
T
r
w
\begin{aligned} \tilde{L}_{w}(\Delta w) & \approx L(w+\Delta w) \\ &=\|r(w+\Delta w)\|^{2} \\ & \approx\left\|r_{w}+J_{w} \Delta w\right\|^{2} \\ &=\Delta w^{\mathrm{T}} J_{w}^{\mathrm{T}} J_{w} \Delta w+2 \Delta w^{\mathrm{T}} J_{w}^{\mathrm{T}} r_{w}+r_{w}^{\mathrm{T}} r_{w} \end{aligned}
L~w(Δw)≈L(w+Δw)=∥r(w+Δw)∥2≈∥rw+JwΔw∥2=ΔwTJwTJwΔw+2ΔwTJwTrw+rwTrw
其中,
J
w
=
∂
r
∂
w
J_{w}=\frac{\partial r}{\partial w}
Jw=∂w∂r,这是一个正定二次函数(positive definite quadratic function),可以采用共轭梯度下降法进行求解。
目标估计
目标估计模块是在得到目标分类模块输出的目标粗略信息后,对bbox进行优化的模块,以ResNet-18为骨干网络,分为两个分支——参考分支和测试分支。此模块受IoU-Net启发,采用一个IoU预测器,其输入有:(1)当前帧的骨干特征;(2)当前帧估计的bbox;(3)参考帧的IoU得分;(4)参考帧的目标bbox。预测器会输出每一个当前帧的预测的bbox 的IoU得分。
其中,图像特征
x
∈
R
W
×
H
×
D
x \in \mathbb{R}^{W \times H \times D}
x∈RW×H×D,bbox为
B
=
(
c
x
/
w
,
c
y
/
h
,
log
w
,
log
h
)
B=\left(c_{x} / w, c_{y} / h, \log w, \log h\right)
B=(cx/w,cy/h,logw,logh),
(
c
x
,
c
y
)
\left(c_{x}, c_{y}\right)
(cx,cy)是bbox中心在图像中的坐标。网络通过一个PrPool层对
x
x
x中
B
B
B给出的范围进行池化,生成一个事先决定好大小的特征图
x
b
x_{b}
xb。从本质上来说,PrPool是一个持续的自适应平均池化的变化,其最关键的优势在于bbox的坐标
B
B
B是可以进行微分的,使得bbox可以通过梯度上升和最大化IoU得分来进行优化。
Prpool的forward和backward如下:
import torch.autograd as ag
def forward(ctx, features, rois, pooled_height, pooled_width, spatial_scale):
"""
Prpool的前向运算
参数:
ctx:存参数,用与反向传播
features:需要Prpool操作的特征
pooled_height...spatial_scale:高、宽、规模
"""
_prroi_pooling = _import_prroi_pooling()
pooled_height = int(pooled_height)
pooled_width = int(pooled_width)
spatial_scale = float(spatial_scale)
# .contiguous()是将张量变成在内存中连续分布的形式,可能是加速计算速度
features = features.contiguous()
rois = rois.contiguous()
params = (pooled_height, pooled_width, spatial_scale)
if features.is_cuda:
output = _prroi_pooling.prroi_pooling_forward_cuda(features, rois, *params)
ctx.params = params
ctx.save_for_backward(features, rois, output)
else:
# 只能用gpu计算
raise NotImplementedError('Precise RoI Pooling only supports GPU (cuda) implememtations.')
return output
def backward(ctx, grad_output):
"""
Prpool操作的反向传播
参数:
ctx:前向传播搜集的参数
grad_output:输出的梯度
"""
_prroi_pooling = _import_prroi_pooling()
features, rois, output = ctx.saved_tensors
grad_input = grad_coor = None
if features.requires_grad:
grad_output = grad_output.contiguous()
grad_input = _prroi_pooling.prroi_pooling_backward_cuda(features, rois, output, grad_output, *ctx.params)
if rois.requires_grad:
grad_output = grad_output.contiguous()
grad_coor = _prroi_pooling.prroi_pooling_coor_backward_cuda(features, rois, output, grad_output, *ctx.params)
return grad_input, grad_coor, None, None, None
对与目标检测来说,IoU-Net是针对每一个物体分类的,但在目标跟踪之中,目标的分类是未知的。此外,和目标检测不同的是,跟踪的目标不一定需要属于定义好的类别,也不一定在已有的数据集里。因此,原来的IoU-Net需要修改,就需要target-specific的IoU-Net(通过采用第一帧注释)。由于IoU预测任务的高层性质,作者认为其无法训练,在单独的一帧上进行微调也不行,需要离线训练。
单纯地将参考图像的特征和当前帧的特征进行融合是无效的。于是作者提出了一个modulation-based的网络结构,在只有第一帧参考图像的情况下对任意物体进行预测IoU。参考分支中参考图像的特征为
x
0
x_{0}
x0,注释的bbox为
B
0
B_{0}
B0,生成规定调制向量记为
c
(
x
0
,
B
0
)
c(x_{0},B_{0})
c(x0,B0)。
生成调制向量的代码:
def get_modulation(self, feat, bb):
"""生成调制向量
参数:
feat: 来自参考图像的骨干特征;维度 (batch, feature_dim, H, W)
bb: 参考图像中目标的bbox(x,y,w,h);维度 (batch, 4)"""
feat3_r, feat4_r = feat
c3_r = self.conv3_1r(feat3_r)
batch_size = bb.shape[0]
batch_index = torch.arange(batch_size, dtype=torch.float32).view(-1, 1).to(bb.device)
# 将输入的bbox从xywh的形式转换到x0y0x1y1的形式
bb = bb.clone()
bb[:, 2:4] = bb[:, 0:2] + bb[:, 2:4]
roi1 = torch.cat((batch_index, bb), dim=1)
roi3r = self.prroi_pool3r(c3_r, roi1)
c4_r = self.conv4_1r(feat4_r)
roi4r = self.prroi_pool4r(c4_r, roi1)
fc3_r = self.fc3_1r(roi3r)
# 拼接两个向量
fc34_r = torch.cat((fc3_r, roi4r), dim=1)
fc34_3_r = self.fc34_3r(fc34_r)
fc34_4_r = self.fc34_4r(fc34_r)
return fc34_3_r, fc34_4_r
测试分支采用了更多卷积层以及更大规模的池化来提取用于IoU预测的特征,记为
z
(
x
,
b
)
z(x,b)
z(x,b),大小为
K
∗
K
∗
D
z
K*K*D_{z}
K∗K∗Dz,
K
K
K是PrPool层的空间输出尺寸。紧接着通过一个channel-wise的矩阵乘法,将
z
z
z和
c
c
c相乘(所谓的channel-wise,简单讲就是用一个维度和输入特征图的通道数相同的向量和特征图的通道相乘),这就为IoU预测提供了一个target-specif的表示,融合了参考目标的外观信息,最终:
IoU
(
B
)
=
g
(
c
(
x
0
,
B
0
)
⋅
z
(
x
,
B
)
)
\operatorname{IoU}(B)=g\left(c\left(x_{0}, B_{0}\right) \cdot z(x, B)\right)
IoU(B)=g(c(x0,B0)⋅z(x,B))
其中的变量,标注如下:
IoU预测器的结构代码为:
"""参数:
input_dim: 两个骨干层的特征维度
pred_input_dim: 预测网络的输入维度
pred_inter_dim: 预测网络的中间维度"""
def __init__(self, input_dim=(128,256), pred_input_dim=(256,256), pred_inter_dim=(256,256)):
super().__init__()
self.conv3_1r = conv(input_dim[0], 128, kernel_size=3, stride=1)
self.conv3_1t = conv(input_dim[0], 256, kernel_size=3, stride=1)
self.conv3_2t = conv(256, pred_input_dim[0], kernel_size=3, stride=1)
self.prroi_pool3r = PrRoIPool2D(3, 3, 1/8)
self.prroi_pool3t = PrRoIPool2D(5, 5, 1/8)
self.fc3_1r = conv(128, 256, kernel_size=3, stride=1, padding=0)
self.conv4_1r = conv(input_dim[1], 256, kernel_size=3, stride=1)
self.conv4_1t = conv(input_dim[1], 256, kernel_size=3, stride=1)
self.conv4_2t = conv(256, pred_input_dim[1], kernel_size=3, stride=1)
self.prroi_pool4r = PrRoIPool2D(1, 1, 1/16)
self.prroi_pool4t = PrRoIPool2D(3, 3, 1 / 16)
self.fc34_3r = conv(256 + 256, pred_input_dim[0], kernel_size=1, stride=1, padding=0)
self.fc34_4r = conv(256 + 256, pred_input_dim[1], kernel_size=1, stride=1, padding=0)
self.fc3_rt = LinearBlock(pred_input_dim[0], pred_inter_dim[0], 5)
self.fc4_rt = LinearBlock(pred_input_dim[1], pred_inter_dim[1], 3)
self.iou_predictor = nn.Linear(pred_inter_dim[0]+pred_inter_dim[1], 1, bias=True)
预测IoU得分的具体代码为:
def predict_iou(self, modulation, feat, proposals):
"""预测IoU得分
参数:
modulation: 目标的调制向量;维度 (batch, feature_dim)
feat: 测试图像的IoU特征( get_iou_feat函数的输出);维度 (batch, feature_dim, H, W).
proposals: 即将被预测IoU的box;维度 (batch, num_proposals, 4)."""
fc34_3_r, fc34_4_r = modulation
c3_t, c4_t = feat
batch_size = c3_t.size()[0]
# 进行调制
c3_t_att = c3_t * fc34_3_r.view(batch_size, -1, 1, 1)
c4_t_att = c4_t * fc34_4_r.view(batch_size, -1, 1, 1)
# 添加batch_index
batch_index = torch.arange(batch_size, dtype=torch.float32).view(-1, 1).to(c3_t.device)
num_proposals_per_batch = proposals.shape[1]
# 将xywh的形式转换为x0y0x1y1的形式
proposals_xyxy = torch.cat((proposals[:, :, 0:2], proposals[:, :, 0:2] + proposals[:, :, 2:4]), dim=2)
# 添加batch_index
roi2 = torch.cat((batch_index.view(batch_size, -1, 1).expand(-1, num_proposals_per_batch, -1),
proposals_xyxy), dim=2)
roi2 = roi2.view(-1, 5).to(proposals_xyxy.device)
roi3t = self.prroi_pool3t(c3_t_att, roi2)
roi4t = self.prroi_pool4t(c4_t_att, roi2)
fc3_rt = self.fc3_rt(roi3t)
fc4_rt = self.fc4_rt(roi4t)
fc34_rt_cat = torch.cat((fc3_rt, fc4_rt), dim=1)
iou_pred = self.iou_predictor(fc34_rt_cat).view(batch_size, num_proposals_per_batch)
return iou_pred
代码
下载代码到自己服务器上,git clone https://github.com/visionml/pytracking.git。因为作者的一部分代码是引用的IoU-Net的源码,所以需要补全代码,git submodule update --init ,这样以后代码就完整了。
环境配置
作者提供了Windows和Linux相关的配置文件。我一开始是在Windows上面配置的,但是需要下载visual studio,因为代码中的pycocoapi和ninja库需要vs中的VC部分编译,由于对vs不熟悉,所以我还是选择了在Linux下配置环境。(需要在Windows配置的可以看这篇ATOM在Windows上运行)
Linux的配置除了pytorch之外,其余只需要按照作者给的sh文件直接bash即可。由于直接用conda安装pytorch的话,会到国外的网站进行下载,若是有工具,那也无妨,若是没有工具,可以用pip安装,记得添加清华源。
代码运行
首先run_video.py可以有两种运行方式,一种是打开视频自己在第一帧框取bbox,第二种是输入bbox的四个参数。由于项目需求,我需要采用第二种方式,遇到以下报错:
错误是在basetracker.py中,需要将
因为收到的optional_box参数还是一个字符串,需要将其分解成四个整型的数。
跟踪情况演示:
补充说明
项目的web板块采用Django框架进行部署,用户通过网页上传视频,选取初始bbox,服务器后端开始解析视频,返回结果,web部分的代码还有部分未完成,等之后补上演示效果吧。