本文提供 MMTracking 里单目标跟踪(SOT)任务的食用指南。后续单目标跟踪的食用指南也在路上哦~
本文内容
SOT 任务简介
SOT 数据集介绍
所支持的 SOT 算法与数据集
上手指南
SiameseRPN++ 实现解析
1. SOT 任务简介
SOT 更加侧重人机交互,算法需要在给定一个任意类别,任意形状目标的情况下,能够对该目标进行持续跟踪。
比如,我们该如何抓住一只“骑上心爱的小摩托”逃课的人类幼崽呢——
2. SOT 数据集介绍
目前 SOT 领域主流的数据集为 OTB100、VOT2018、VOT2020、UAV123、TrackingNet、LaSOT、GOT-10K。
除了 VOT 系列数据集采用 VOT 评估标准外,其余数据集一般采用 OPE 评估标准。VOT 评估标准的主要指标为 EAO、Accuracy、Robustness,OPE 评估标准的主要指标为 Success、Norm Precision、Precision。
3. 所支持的 SOT 算法与数据集
MMTracking 目前支持以下 SOT 算法:
- SiameseRPN++ (CVPR 2019)
链接:https://arxiv.org/abs/1812.11703
MMTracking 目前支持 OTB100、UAV123、TrackingNet、LaSOT 数据集。
4. 上手指南
接下来,本文详细地介绍在 MMTracking 里如何运行 SOT demo、测试 SOT 模型、训练 SOT 模型。
使用 MMTracking,你只需要克隆一下 github 上面的仓库到本地,然后按照安装手册配置一下环境即可,如果安装遇到什么问题,可以给 MMTracking 提 issue,我们会尽快为小伙伴们解答。
安装手册:
https://github.com/open-mmlab/mmtracking/blob/master/docs/install.md
假设已经将预训练权重放置在 MMTracking 根目录下的 checkpoints/ 文件夹下(预训练权重可以在相应的 configs 页面下载)。
运行 SOT demo
在 MMTracking 根目录下只需执行以下命令,即可使用 SiameseRPN++ 算法运行 SOT demo。
python ./demo/demo_sot.py \
./configs/sot/siamese_rpn/siamese_rpn_r50_1x_lasot.py \
--input ${VIDEO_FILE} \
--checkpoint checkpoints/siamese_rpn_r50_1x_lasot_20201218_051019-3c522eff.pth \
--output ${OUTPUT} \
--show
测试 SOT 模型
在 MMTracking 根目录下使用以下命令即可在 LaSOT 数据集上测试 SOT 模型,并且使用 OPE 评估标准评估模型。
./tools/dist_test.sh \
configs/sot/siamese_rpn/siamese_rpn_r50_1x_lasot.py 8 \
--checkpoint checkpoints/siamese_rpn_r50_1x_lasot_20201218_051019-3c522eff.pth \
--out results.pkl \
--eval track
训练 SOT 模型
在 MMTracking 根目录下使用以下命令即可训练 SOT 模型,并且在第 10 个 epoch 到第 20 个 epoch 使用 OPE 评估标准评估模型。
bash ./tools/dist_train.sh \
./configs/sot/siamese_rpn/siamese_rpn_r50_1x_lasot.py 8 \
--work-dir ./work_dirs/
其实在 MMTracking 当中已经支持了很多的 SOT 模型,并且提供了公共的 checkpoint 供大家使用,在快速上手教程中也有更详细地介绍。
快速上手教程:
https://mmtracking.readthedocs.io/en/latest/quick_run.html
5. SiameseRPN++ 实现解析
经过上述步骤,本文已经介绍了怎样运行 SOT 算法,接下来将介绍 SiameseRPN++ 在 MMTracking 下的实现。
SiameseRPN++ 的配置文件
model = dict(type='SiamRPN',backbone=dict(type='SOTResNet'),neck=dict(type='ChannelMapper'),head=dict(type='SiameseRPNHead'),train_cfg=dict(rpn=dict(assigner=dict(type='MaxIoUAssigner'),sampler=dict(type='RandomSampler'),num_neg=16,exemplar_size=127,search_size=255)),test_cfg=dict(exemplar_size=127,search_size=255,context_amount=0.5,center_size=7,rpn=dict(penalty_k=0.05, window_influence=0.42, lr=0.38)))
SiameseRPN++ 的配置文件如上所示,可以看到 SiameseRPN++ 由 5 部分构成:
(1)backbone:使用 ResNet-50,用于提取图像特征图;
(2)neck:使用 ChannelMapper (几个 conv layer),用于统一 ResNet 不同 level 的特征图的通道数;
head:使用 SiameseRPNHead,进行相邻帧的目标跟踪;
(3)train_cfg:SiameseRPN++ 训练时的超参。其中 assigner 负责对正样本对以 IoU 为基准分配正负样本,sampler 基于 assigner 进行正负样本采样;num_neg 表示对负样本对采样负样本的数量;
(4)test_cfg:SiameseRPN++ 测试时的超参。其中 rpn 里的 3 个超参对算法性能影响较大,因此一般需要在不同的数据集下选择不同的超参。
SiameseRPN++ Head 实现解析
由于 head 是 SiameseRPN++ 算法的重点,本文将 head 的 forward 部分与 forward 之后获取最终跟踪 bbox 部分进行源码解析。
Forward 部分相对比较简单,主要包含以下 5 步:
第一步,计算不同 level score map 的加权系数
第二步,对某一 level 的 template feature 和 search feature 使用correlation head 得到 score map
第三步,对某一 level 的 template feature 和 search feature 使用correlation head 得到 regression map
第四步,使用第一步中的加权系数聚合不同 level 的 score map
第五步,使用第一步中的加权系数聚合不同 level 的 regression map
本文将这 5 个步骤分别贴在了代码里 forward 函数的相应位置上作为注释,方便读者理解代码的逻辑,具体如下。
获取最终跟踪 box 的部分比较复杂,包含了一些后处理部分,主要有以下 10 步:
第一步,获取 anchor
第二步,获取 2D 汉明窗,作为预测框 score 的惩罚项
第三步,获取每个 anchor 对应的 score 和预测 box
第四步,计算预测框大小惩罚项
第五步,计算预测框宽高比惩罚项
第六步,使用第四步、第五步的惩罚项惩罚预测的 score
第七步,使用第二步得到的汉明窗,惩罚预测的 score
第八步,根据惩罚之后的 score,选取得分最高的预测框作为跟踪框
第九步,将得到跟踪框的坐标从 search 图像变换到原始图像上
第十步,使用上一帧的跟踪框来平滑当前预测的跟踪框,作为最终的跟踪结果
本文将这 10 个步骤分别贴在了代码里 get_bbox 函数的相应位置上作为注释,方便读者理解代码的逻辑,具体如下。
由于 get_bbox 的实现较为复杂,建议读者对着源码进行查看,方便理解。
向上向右滑动查看完整代码
@HEADS.register_module()
class SiameseRPNHead(BaseModule):
"""Siamese RPN head.
This module is proposed in
"SiamRPN++: Evolution of Siamese Visual Tracking with Very Deep Networks.
`SiamRPN++ <https://arxiv.org/abs/1812.11703>`_.
Args:
anchor_generator (dict): Configuration to build anchor generator
module.
in_channels (int): Input channels.
kernel_size (int): Kernel size of convs. Defaults to 3.
norm_cfg (dict): Configuration of normlization method after each conv.
Defaults to dict(type='BN').
weighted_sum (bool): If True, use learnable weights to weightedly sum
the output of multi heads in siamese rpn , otherwise, use
averaging. Defaults to False.
bbox_coder (dict): Configuration to build bbox coder. Defaults to
dict(type='DeltaXYWHBBoxCoder', target_means=[0., 0., 0., 0.],
target_stds=[1., 1., 1., 1.]).
loss_cls (dict): Configuration to build classification loss. Defaults
to dict( type='CrossEntropyLoss', reduction='sum', loss_weight=1.0)
loss_bbox (dict): Configuration to build bbox regression loss. Defaults
to dict( type='L1Loss', reduction='sum', loss_weight=1.2).
train_cfg (Dict): Training setting. Defaults to None.
test_cfg (Dict): Testing setting. Defaults to None.
init_cfg (dict or list[dict], optional): Initialization config dict.
Defaults to None.
"""
def __init__(self,
anchor_generator,
in_channels,
kernel_size=3,
norm_cfg=dict(type='BN'),
weighted_sum=False,
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[0., 0., 0., 0.],
target_stds=[1., 1., 1., 1.]
),
loss_cls=dict(
type='CrossEntropyLoss', reduction='sum',
loss_weight=1.0
),
loss_bbox=dict(
type='L1Loss', reduction='sum', loss_weight=1.2
),
train_cfg=None,
test_cfg=None,
init_cfg=None,
*args,
**kwargs
):
super(SiameseRPNHead, self).__init__(init_cfg)
self.anchor_generator = build_prior_generator(anchor_generator)
self.bbox_coder = build_bbox_coder(bbox_coder)
self.train_cfg = train_cfg
self.test_cfg = test_cfg
self.assigner = build_assigner(self.train_cfg.assigner)
self.sampler = build_sampler(self.train_cfg.sampler)
self.fp16_enabled = False
self.cls_heads = nn.ModuleList()
self.reg_heads = nn.ModuleList()
for i in range(len(in_channels)):
self.cls_heads.append(
CorrelationHead(in_channels[i], in_channels[i],
2 * self.anchor_generator.num_base_anchors[0],
kernel_size, norm_cfg))
self.reg_heads.append(
CorrelationHead(in_channels[i], in_channels[i],
4 * self.anchor_generator.num_base_anchors[0],
kernel_size, norm_cfg))
self.weighted_sum = weighted_sum
if self.weighted_sum:
self.cls_weight = nn.Parameter(torch.ones(len(in_channels)))
self.reg_weight = nn.Parameter(torch.ones(len(in_channels)))
self.loss_cls = build_loss(loss_cls)
self.loss_bbox = build_loss(loss_bbox)
@auto_fp16()
def forward(self, z_feats, x_feats):
"""Forward with features `z_feats` of exemplar images and features
`x_feats` of search images.
Args:
z_feats (tuple[Tensor]): Tuple of Tensor with shape (N, C, H, W)
denoting the multi level feature maps of exemplar images.
Typically H and W equal to 7.
x_feats (tuple[Tensor]): Tuple of Tensor with shape (N, C, H, W)
denoting the multi level feature maps of search images.
Typically H and W equal to 31.
Returns:
tuple(cls_score, bbox_pred): cls_score is a Tensor with shape
(N, 2 * num_base_anchors, H, W), bbox_pred is a Tensor with shape
(N, 4 * num_base_anchors, H, W), Typically H and W equal to 25.
"""
assert isinstance(z_feats, tuple) and isinstance(x_feats, tuple)
assert len(z_feats) == len(x_feats) and len(z_feats) == len(
self.cls_heads)
# 第一步,计算不同 level score map 的加权系数
if self.weighted_sum:
cls_weight = nn.functional.softmax(self.cls_weight, dim=0)
reg_weight = nn.functional.softmax(self.reg_weight, dim=0)
else:
reg_weight = cls_weight = [
1.0 / len(z_feats) for i in range(len(z_feats))
]
cls_score = 0
bbox_pred = 0
for i in range(len(z_feats)):
# 第二步,对某一 level 的 template feature 和 search feature 使用correlation head 得到 score map
cls_score_single = self.cls_heads[i](z_feats[i], x_feats[i])
# 第三步,对某一 level 的 template feature 和 search feature 使用correlation head 得到 regression map
bbox_pred_single = self.reg_heads[i](z_feats[i], x_feats[i])
# 第四步,使用第一步中的加权系数聚合不同 level 的 score map
cls_score += cls_weight[i] * cls_score_single
# 第五步,使用第一步中的加权系数聚合不同 level 的 regression map
bbox_pred += reg_weight[i] * bbox_pred_single
return cls_score, bbox_pred
@force_fp32(apply_to=('cls_score', 'bbox_pred'))
def get_bbox(self, cls_score, bbox_pred, prev_bbox, scale_factor):
"""Track `prev_bbox` to current frame based on the output of network.
Args:
cls_score (Tensor): of shape (1, 2 * num_base_anchors, H, W).
bbox_pred (Tensor): of shape (1, 4 * num_base_anchors, H, W).
prev_bbox (Tensor): of shape (4, ) in [cx, cy, w, h] format.
scale_factor (Tensr): scale factor.
Returns:
tuple(best_score, best_bbox): best_score is a Tensor denoting the
score of `best_bbox`, best_bbox is a Tensor of shape (4, )
with [cx, cy, w, h] format, which denotes the best tracked
bbox in current frame.
"""
score_maps_size = [(cls_score.shape[2:])]
# 第一步,获取 anchor
if not hasattr(self, 'anchors'):
self.anchors = self.anchor_generator.grid_priors(
score_maps_size, cls_score.device)[0]
# Transform the coordinate origin from the top left corner to the
# center in the scaled feature map.
feat_h, feat_w = score_maps_size[0]
stride_w, stride_h = self.anchor_generator.strides[0]
self.anchors[:, 0:4:2] -= (feat_w // 2) * stride_w
self.anchors[:, 1:4:2] -= (feat_h // 2) * stride_h
# 第二步,获取 2D 汉明窗,作为预测框 score 的惩罚项
if not hasattr(self, 'windows'):
self.windows = self.anchor_generator.gen_2d_hanning_windows(
score_maps_size, cls_score.device)[0]
# 第三步,获取每个 anchor 对应的 score 和预测 box
H, W = score_maps_size[0]
cls_score = cls_score.view(2, -1, H, W)
cls_score = cls_score.permute(2, 3, 1, 0).contiguous().view(-1, 2)
cls_score = cls_score.softmax(dim=1)[:, 1]
bbox_pred = bbox_pred.view(4, -1, H, W)
bbox_pred = bbox_pred.permute(2, 3, 1, 0).contiguous().view(-1, 4)
bbox_pred = self.bbox_coder.decode(self.anchors, bbox_pred)
bbox_pred = bbox_xyxy_to_cxcywh(bbox_pred)
def change_ratio(ratio):
return torch.max(ratio, 1. / ratio)
def enlarge_size(w, h):
pad = (w + h) * 0.5
return torch.sqrt((w + pad) * (h + pad))
# 第四步,计算预测框大小惩罚项
# scale penalty
scale_penalty = change_ratio(
enlarge_size(bbox_pred[:, 2], bbox_pred[:, 3]) / enlarge_size(
prev_bbox[2] * scale_factor, prev_bbox[3] * scale_factor))
# 第五步,计算预测框宽高比惩罚项
# aspect ratio penalty
aspect_ratio_penalty = change_ratio(
(prev_bbox[2] / prev_bbox[3]) /
(bbox_pred[:, 2] / bbox_pred[:, 3]))
# 第六步,使用第四步、第五步的惩罚项惩罚预测的 score
# penalize cls_score
penalty = torch.exp(-(aspect_ratio_penalty * scale_penalty - 1) *
self.test_cfg.penalty_k)
penalty_score = penalty * cls_score
# 第七步,使用第二步得到的汉明窗,惩罚预测的 score
# window penalty
penalty_score = penalty_score * (1 - self.test_cfg.window_influence) \
+ self.windows * self.test_cfg.window_influence
# 第八步,根据惩罚之后的 score,选取得分最高的预测框作为跟踪框
best_idx = torch.argmax(penalty_score)
best_score = cls_score[best_idx]
best_bbox = bbox_pred[best_idx, :] / scale_factor
final_bbox = torch.zeros_like(best_bbox)
# 第九步,将得到跟踪框的坐标从 search 图像变换到原始图像上
# map the bbox center from the searched image to the original image.
final_bbox[0] = best_bbox[0] + prev_bbox[0]
final_bbox[1] = best_bbox[1] + prev_bbox[1]
# 第十步,使用上一帧的跟踪框来平滑当前预测的跟踪框,作为最终的跟踪结果
# smooth bbox
lr = penalty[best_idx] * cls_score[best_idx] * self.test_cfg.lr
final_bbox[2] = prev_bbox[2] * (1 - lr) + best_bbox[2] * lr
final_bbox[3] = prev_bbox[3] * (1 - lr) + best_bbox[3] * lr
return best_score, final_bbox
超参搜索工具
如上所提,SiameseRPN++ 测试时,性能受到 rpn 里 3 个超参的影响较大,需要在不同的数据集下选择不同的超参,因此我们提供了 SiameseRPN++ 的超参搜索工具,搜索脚本在
$MMTracking/tools/analysis/sot/sot_siamderpn_param_search.py可以找到,本文接下来介绍它的使用方法。
在 MMTracking 根目录下使用以下命令即可在 UAV123 数据集上基于 OPE 评估标准搜索超参,搜索的结果将会保存在 ${LOG_FILENAME} 文件里。
./tools/analysis/sot/dist_sot_siamrpn_param_search.sh \
[${CONFIG_FILE}] [$GPUS] \
[--checkpoint ${CHECKPOINT}] \
[--log ${LOG_FILENAME}] \
[--eval ${EVAL}] \
[--penalty-k-range 0.01,0.22,0.05] \
[--lr-range 0.4,0.61,0.05] \
[--win-infu-range 0.01,0.22,0.05]
在 MMTracking 根目录下使用以下命令即可在 OTB100 数据集上基于 OPE 评估标准搜索超参,搜索的结果将会保存在 ${LOG_FILENAME} 文件里。
./tools/analysis/sot/dist_sot_siamrpn_param_search.sh \
[${CONFIG_FILE}] [$GPUS] \
[--checkpoint ${CHECKPOINT}] \
[--log ${LOG_FILENAME}] \
[--eval ${EVAL}] \
[--penalty-k-range 0.3,0.45,0.02] \
[--lr-range 0.35,0.5,0.02] \
[--win-infu-range 0.46,0.55,0.02]
同时请注意在 MMTracking model zoo 里提供的所有结果是没有进行超参搜索的。
作为 MM 系列的成员, MMTracking 将持续更新,力图早日成长为一个完善的视频目标感知平台,而社区的声音能够帮助我们更好地了解到大家的需求,所以如果大家在使用的过程中遇到什么问题、想法、建议,或者有想支持的新数据集、新方法、新任务,欢迎在评论区里发言。请记住我们的 repo 是您永远的家!