本期我们提供 MMTracking 里多目标跟踪(MOT)任务的食用指南。后续单目标跟踪的食用指南也在路上哦~
本文内容
MOT 任务简介
MOT 数据集介绍
MMTracking 支持的算法与数据集
上手指南
Tracktor 实现解析
1. MOT 任务简介
MOT 旨在检测和跟踪视频中出现的物体。
与视频目标检测相比,MOT 更加侧重于对视频内的同一目标进行关联。
文章链接:https://mp.weixin.qq.com/s/POimc9lhveOXuowEMnPzqw
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 供大家把玩,在快速上手教程中有更详细地介绍,欢迎大家来试用并且提出你们宝贵的意见。
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 是您永远的家!
文章来源:公众号【OpenMMLab】
2021-10-09 12:52