本期我们提供 MMTracking 里多目标跟踪(MOT)任务的食用指南。后续单目标跟踪的食用指南也在路上哦~
本文内容
MOT 任务简介
MOT 数据集介绍
MMTracking 支持的算法与数据集
上手指南
Tracktor 实现解析
1. MOT 任务简介
MOT 旨在检测和跟踪视频中出现的物体。
与视频目标检测相比,MOT 更加侧重于对视频内的同一目标进行关联。
2. MOT 数据集介绍
目前 MOT 领域主流的数据集为 MOT 15、MOT 16、 MOT 17、MOT 20,它主要侧重于密集场景下行人跟踪任务。
以 MOT 17 为例,训练集 7 个视频,测试集 7 个视频。该数据集的评估指标为 CLEAR MOT ,其中主要指标为 MOTA 和 IDF1。
3. MMTracking 支持的算法与数据集
MMTracking 目前支持以下 MOT 算法:
- SORT (ICIP 2016)
- DeepSORT (ICIP 2017)
- Tracktor (ICCV 2019)
MMTracking 目前支持 MOT 15、MOT 16、 MOT 17、MOT 20 数据集。
4. 上手指南
接下来,我们详细地介绍在 MMTracking 里如何运行 MOT demo、测试 MOT 模型、训练 MOT 模型。
使用 MMTracking,你只需要克隆一下 github 上面的仓库到本地,然后按照安装手册配置一下环境即可,如果安装遇到什么问题,可以给 MMTracking 提 issue,我们会尽快为小伙伴们解答。
假设已经将预训练权重放置在 MMTracking 根目录下的 checkpoints/ 文件夹下(预训练权重可以在相应的 configs 页面下载)。
运行 MOT demo
在 MMTracking 根目录下只需执行以下命令,即可使用 Tracktor 算法运行 MOT demo。
请注意,当运行 demo 时,需要 config 文件名包含 private字段,这是因为 private表示跟踪算法不需要外部的检测结果作为输入,而public表示跟踪算法需要外部的检测结果作为输入。
python demo/demo_mot.py \
configs/mot/deepsort/sort_faster-rcnn_fpn_4e_mot17-private.py \
--input demo/demo.mp4 \
--output mot.mp4 \
--show
测试 MOT 模型
在 MMTracking 根目录下使用以下命令即可测试 MOT 模型,并且评估模型的 CLEAR MOT metrics。
由于 Tracktor 算法由检测器和 ReID 模型两部分构成,MMTracking 将检测器的模型权重和 ReID 的模型权重写在了 config 文件里,所以并不需要将模型权重作为参数传入到执行脚本里。
./tools/dist_test.sh \
configs/mot/tracktor/tracktor_faster-rcnn_r50_fpn_4e_mot17-public-half.py \
8 --eval track
如上所提到的,如果想使用自己训练的检测器和 ReID 模型来测试 Tracktor 算法,则需要修改 config 文件里的检测器模型权重文件和 ReID 模型权重文件,具体方式如下:
model = dict(
detector=dict(
init_cfg=dict(
type='Pretrained',
checkpoint='/path/to/detector_model')),
reid=dict(
init_cfg=dict(
type='Pretrained',
checkpoint='/path/to/reid_model'))
)
训练 MOT 模型
对于像 SORT、DeepSORT、Tracktor 这样的 MOT 算法,由于算法本身由检测器和 ReID 模型两部分构成,所以需要分别训练一个检测器和一个 ReID 模型,之后再测试 MOT 模型。
A. 训练检测器
MMTracking 可以直接调用 MMDetection 的接口来训练检测器,为了达到这一目的,需要在 config 文件里加上一行 USE_MMDET=True。
请注意 MMTracking 下的 base config 和 MMDetection 下的有一些不同,detector仅仅是model的一个子组件,例如 MMDetection 下的 Faster R-CNN config 文件如下:
model = dict(
type='FasterRCNN',
...
)
但是在 MMTracking 下,config 文件如下:
model = dict(
detector=dict(
type='FasterRCNN',
...
)
)
比如使用以下命令即可在 MOT 17 数据集上训练检测器,并在每一个 epoch 之后评估 bbox mAP。
bash ./tools/dist_train.sh \
./configs/det/faster-rcnn_r50_fpn_4e_mot17-half.py \
8 --work-dir ./work_dirs/
B. 训练 ReID 模型
MMTracking 支持基于 MMClassification 来训练 ReID 模型。
比如使用以下命令即可在 MOT 17 数据集上训练 ReID 模型,并在每个 epoch 之后评估 mAP。
bash ./tools/dist_train.sh \
./configs/reid/resnet50_b32x8_MOT17.py \
8 --work-dir ./work_dirs/
在训练得到自己的检测器和 ReID 模型之后,就可以按照步骤 “2. 测试 MOT 模型”里提到的方式来运行自己的跟踪模型了。
其实在 MMTracking 当中已经支持了很多的 MOT 模型,并且提供了公共的 checkpoint 供大家把玩,在快速上手教程中有更详细地介绍,欢迎大家来试用并且提出你们宝贵的意见。
快速上手教程:
https://mmtracking.readthedocs.io/en/latest/quick_run.html
5. Tracktor 实现解析
经过上述步骤,我们已经了解了怎样运行 MOT 算法,但同时也对算法实现方式产生了一些兴趣,接下来将介绍 Tracktor 在 MMTracking 下的实现。
Tracktor 的配置文件
model = dict(
type='Tracktor',
detector=dict(type='FasterRCNN'),
reid=dict(type='BaseReID'),
motion=dict(type='CameraMotionCompensation'),
tracker=dict(type='TracktorTracker'))
Tracktor 的配置文件如上所示,可以看到 Tracktor 由 4 部分构成:
(1)detector:使用 Faster RCNN 算法,用来检测视频里的物体;
(2)reid:使用 ReID 模型对未跟踪上的物体进行关联;
(3)motion:使用 CameraMotionCompensation 算法,进行相邻帧的运动补偿;
(4)tracker:综合调配使用 detector、reid、motion 来关联相邻帧的物体。
接下来详细介绍这几个部分。
Detector 的配置文件
detector=dict(
type='FasterRCNN',
backbone=dict(type='ResNet'),
neck=dict(type='FPN'),
rpn_head=dict(type='RPNHead'),
roi_head=dict(type='StandardRoIHead',
bbox_roi_extractor=dict(type='SingleRoIExtractor'),
bbox_head=dict(type='Shared2FCBBoxHead')),
train_cfg=dict(
rpn=dict(
assigner=dict(type='MaxIoUAssigner'),
sampler=dict(type='RandomSampler')),
rpn_proposal=dict(),
rcnn=dict(
assigner=dict(type='MaxIoUAssigner'),
sampler=dict(type='RandomSampler'))),
test_cfg=dict(
rpn=dict(),
rcnn=dict()),
init_cfg=dict(type='Pretrained'))
detector 部分使用 Faster RCNN 算法,主要包含 7 个部分:
(1)backbone:使用 ResNet,用于提取图像特征图;
(2)neck:使用 FPN 算法,用于获取多尺度特征图;
(3)rpn_head:使用 RPN 算法,用于获取图像中的 proposals;
(4)roi_head:由 RoI Align 和 全连接层构成,用于预测 proposals 的类别和位置;
(5)train_cfg:detector 的训练配置;
(6)test_cfg:detector 的测试配置;
(7)init_cfg:detector 的初始化方式,在 Tracktor 里,通过这个参数来 load detector 的模型权重。
Reid 的配置文件
reid=dict(
type='BaseReID',
backbone=dict(type='ResNet'),
neck=dict(type='GlobalAveragePooling'),
head=dict(type='LinearReIDHead'),
init_cfg=dict(type='Pretrained'))
reid 部分包含 4 个部分:
(1)backbone:使用 ResNet,用于提取图像特征图;
(2)neck:全局池化平均,用于压缩图像特征大小;
(3)head:使用全连接层,来进一步提取图像特征,并压缩特征大小;
(4)init_cfg:reid 的初始化方式,在 Tracktor 里,通过这个参数来 load reid 的模型权重。
Motion 的配置文件
motion=dict(
type='CameraMotionCompensation',
warp_mode='cv2.MOTION_EUCLIDEAN',
num_iters=100,
stop_eps=1e-05)
motion 部分使用 CMC 算法来进行相邻帧的运动补偿,CMC 算法通过调用 ECC 算法来使用这 3 个参数,来获取矫正矩阵。
Tracker 的配置文件
tracker=dict(
type='TracktorTracker',
obj_score_thr=0.5,
regression=dict(
obj_score_thr=0.5,
nms=dict(type='nms', iou_threshold=0.6),
match_iou_thr=0.3),
reid=dict(
num_samples=10,
img_scale=(256, 128),
img_norm_cfg=None,
match_score_thr=2.0,
match_iou_thr=0.2),
momentums=None,
num_frames_retain=10)
tracker 部分由 5 部分构成:
(1)obj_score_thr:用于过滤检测框的 score;
(2)regression:当使用 detector 进行前后两帧之间的跟踪时,用于过滤跟踪框的配置;
(3)reid:对于 regression 部分未跟踪上的物体框,使用 reid 模型进行进一步关联;
(4)momentum:以动量的方式更新 tracklet,默认为 None,表示不使用动量的方式进行更新;
(5)num_frames_retain:如果一个物体持续消失了 num_frames_retain 帧,则认为该物体消失。如果一个物体在消失的 num_frames_retain 帧内,又被匹配上了,则认为该物体重新出现。
Tracker 的实现细节
由于 tracker 是 Tracktor 算法的核心,所以本文将它单独拿出来分析其在 MMTracking 下具体的实现细节。
tracker 的实现方式在
$MMTracking/mmtracking/mmtrack/models/mot/trackers/tracktor_tracker.py 下可以找到,在这里只将 forward 函数贴进来。
forward 函数的步骤根据算法原理主要分为七步:
第一步,初始化 tracklet,这一步一般在第一帧执行,也有可能在中间物体全部消失的某一帧进行;
第二步,使用 CMC 算法来进行相邻帧的运动补偿;
第三步,使用 detector 将上一帧的物体跟踪到当前帧;
第四步,使用第三步得到的跟踪坐标框,基于 IOU 过滤当前帧的坐标框;
第五步,使用 reid 模型将未跟踪上的物体关联起来;
第六步,对于 reid 模型也没有关联上的坐标框来说,认为其是新物体出现,分配新的 id;
第七步,根据上述得到的跟踪结果更新 tracklets。
本文将这 7 个步骤分别贴在了代码里的相应位置上作为注释,方便读者理解代码的逻辑,具体如下:
class TracktorTracker(BaseTracker):
def __init__(self,
obj_score_thr=0.5,
regression=dict(
obj_score_thr=0.5,
nms=dict(type='nms', iou_threshold=0.6),
match_iou_thr=0.3),
reid=dict(
num_samples=10,
img_scale=(256, 128),
img_norm_cfg=None,
match_score_thr=2.0,
match_iou_thr=0.2),
init_cfg=None,
**kwargs):
super().__init__(init_cfg=init_cfg, **kwargs)
self.obj_score_thr = obj_score_thr
self.regression = regression
self.reid = reid
def regress_tracks(self, x, img_metas, detector, frame_id, rescale=False):
"""Regress the tracks to current frame."""
@force_fp32(apply_to=('img', 'feats'))
def track(self,
img,
img_metas,
model,
feats,
bboxes,
labels,
frame_id,
rescale=False,
**kwargs):
if self.with_reid:
if self.reid.get('img_norm_cfg', False):
reid_img = imrenormalize(img, img_metas[0]['img_norm_cfg'],
self.reid['img_norm_cfg'])
else:
reid_img = img.clone()
valid_inds = bboxes[:, -1] > self.obj_score_thr
bboxes = bboxes[valid_inds]
labels = labels[valid_inds]
# 第一步,初始化 tracklet,这一步一般在第一帧执行,也有可能在中间物体全部消失的某一帧进行;
if self.empty:
num_new_tracks = bboxes.size(0)
ids = torch.arange(
self.num_tracks,
self.num_tracks + num_new_tracks,
dtype=torch.long)
self.num_tracks += num_new_tracks
if self.with_reid:
embeds = model.reid.simple_test(
self.crop_imgs(reid_img, img_metas, bboxes[:, :4].clone(),
rescale))
else:
# 第二步,使用 CMC 算法来进行相邻帧的运动补偿;
# motion
if model.with_cmc:
if model.with_linear_motion:
num_samples = model.linear_motion.num_samples
else:
num_samples = 1
self.tracks = model.cmc.track(self.last_img, img, self.tracks,
num_samples, frame_id)
if model.with_linear_motion:
self.tracks = model.linear_motion.track(self.tracks, frame_id)
# 第三步,使用 detector 将上一帧的物体跟踪到当前帧;
# propagate tracks
prop_bboxes, prop_labels, prop_ids = self.regress_tracks(
feats, img_metas, model.detector, frame_id, rescale)
# 第四步,使用第三步得到的跟踪坐标框,基于 IOU 过滤当前帧的坐标框;
# filter bboxes with propagated tracks
ious = bbox_overlaps(bboxes[:, :4], prop_bboxes[:, :4])
valid_inds = (ious < self.regression['match_iou_thr']).all(dim=1)
bboxes = bboxes[valid_inds]
labels = labels[valid_inds]
ids = torch.full((bboxes.size(0), ), -1, dtype=torch.long)
# 第五步,使用 reid 模型将未跟踪上的物体关联起来;
if self.with_reid:
prop_embeds = model.reid.simple_test(
self.crop_imgs(reid_img, img_metas,
prop_bboxes[:, :4].clone(), rescale))
if bboxes.size(0) > 0:
embeds = model.reid.simple_test(
self.crop_imgs(reid_img, img_metas,
bboxes[:, :4].clone(), rescale))
else:
embeds = prop_embeds.new_zeros((0, prop_embeds.size(1)))
# reid
active_ids = [int(_) for _ in self.ids if _ not in prop_ids]
if len(active_ids) > 0 and bboxes.size(0) > 0:
track_embeds = self.get(
'embeds',
active_ids,
self.reid.get('num_samples', None),
behavior='mean')
reid_dists = torch.cdist(track_embeds,
embeds).cpu().numpy()
track_bboxes = self.get('bboxes', active_ids)
ious = bbox_overlaps(track_bboxes,
bboxes[:, :4]).cpu().numpy()
iou_masks = ious < self.reid['match_iou_thr']
reid_dists[iou_masks] = 1e6
row, col = linear_sum_assignment(reid_dists)
for r, c in zip(row, col):
dist = reid_dists[r, c]
if dist <= self.reid['match_score_thr']:
ids[c] = active_ids[r]
# 第六步,对于 reid 模型也没有关联上的坐标框来说,认为其是新物体出现,分配新的 id;
new_track_inds = ids == -1
ids[new_track_inds] = torch.arange(
self.num_tracks,
self.num_tracks + new_track_inds.sum(),
dtype=torch.long)
self.num_tracks += new_track_inds.sum()
if bboxes.shape[1] == 4:
bboxes = bboxes.new_zeros((0, 5))
if prop_bboxes.shape[1] == 4:
prop_bboxes = prop_bboxes.new_zeros((0, 5))
bboxes = torch.cat((prop_bboxes, bboxes), dim=0)
labels = torch.cat((prop_labels, labels), dim=0)
ids = torch.cat((prop_ids, ids), dim=0)
if self.with_reid:
embeds = torch.cat((prop_embeds, embeds), dim=0)
# 第七步,根据上述得到的跟踪结果更新 tracklets。
self.update(
ids=ids,
bboxes=bboxes[:, :4],
scores=bboxes[:, -1],
labels=labels,
embeds=embeds if self.with_reid else None,
frame_ids=frame_id)
self.last_img = img
return bboxes, labels, ids
作为 MM 系列的成员, MMTracking 将持续更新,力图早日成长为一个完善的视频目标感知平台,而社区的声音能够帮助我们更好地了解到大家的需求,所以如果大家在使用的过程中遇到什么问题、想法、建议,或者有想支持的新数据集、新方法、新任务,欢迎在评论区里发言。请记住我们的 repo 是您永远的家!