开篇寄语 且行且忘且随风,且行且看且从容。 魔方导读 多目标跟踪技术(MOT)旨在针对视频帧画面中感兴趣或想要追踪的目标进行检测并获取在图像中的位置,且对每个目标分配一个ID,在目标运动过程中,维持每个目标ID保持不变!!!作为计算机视觉领域非常重要的研究方向之一,可广泛应用于智慧城市、智能零售、安防监控、自动驾驶、机器人等领域。本文介绍一种与经典的多目标跟踪算法DeepSort不同的方法——ByteTrack,一种基于tracking-by-detection范式的跟踪方法。 由魔方智能CV空间调研、整理、创作或转载,如有侵权,请联系后台作相应处理!! 往期回顾: YOLOv5-v7.0 | 集成检测、分类、分割任务【CV第三篇】目标检测风云二十年【CV第二篇】图像分类经典网络结构精选论文合集【CV第一篇】计算机视觉四大基本任务![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) Paper:https://arxiv.org/abs/2110.06864 Code:https://github.com/ifzhang/ByteTrack 算法概述ByteTrack是基于tracking-by-detection范式的跟踪方法。大多数多目标跟踪方法通过关联分数高于阈值的检测框来获取目标ID。对于检测分数较低的目标,例如遮挡目标,会被简单的丢弃,这带来了不可忽略的问题,包括大量的漏检和碎片化轨迹。为了解决该问题,作者提出了一种简单、高效且通用的数据关联方法BYTE,通过关联每个检测框而不仅仅是高分检测框来进行跟踪。对于低分检测框,利用它们与轨迹的相似性来恢复真实目标并过滤掉背景检测。 BYTE能轻松应用到9种state-of-the-art的MOT方法中,并取得1-10个点不等的IDF1指标的提升。为了提高MOT的最先进性能,设计了一种简单而强大的跟踪器——ByteTrack,首次在MOT17测试集上实现了80.3 MOTA、77.3 IDF1和63.1 HOTA,在单个V100 GPU上运行速度为30 FPS。 BYTEBYTE是作者提出来的一种新的数据关联方法,是整篇论文最关键所在。区别于其他只对高置信度检测框进行数据关联的方法,BYTE几乎保留所有检测框并进行身份匹配,并将其划分为高置信度和低置信度两组。首先将高置信度检测框关联进轨迹中,然后再将低置信度检测框与未匹配的跟踪对象相关联来保留低置信度检测框并过滤背景。 对照整个BYTE伪代码如下: ![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) 第一步:通过检测器获取检测框和对应的检测分数,对检测框进行分类,如果分数高于T_high,将检测框分类为高置信度组,分数低于T_high,高于T_low时,将检测框分类为低置信度组。 ![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) 第二步:匹配过程使用到了检测框和卡尔曼滤波估计结果之间的相似度,这里可以采用IoU或Re-ID特征间距离来作为相似度度量。然后基于相似度采用匈牙利算法进行匹配,并保留那些未匹配到轨迹的高置信度检测框以及未匹配到检测框的轨迹。这部分对应伪代码中的17到19行。 ![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) 第三步:关联那些第一次关联剩下的轨迹以及低置信度检测框。之后保留那些第二次匹配过后仍然未匹配到边界框的轨迹,并删除那些低置信度边界框中在第二次匹配过后未找到对应轨迹的边界框,因为这些边界框被认定为是不包含任何物体的背景。对应伪代码中的第20到21行。 ![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) 我们发现单独使用IoU作为第二关联中的相似度很重要,因为低分数检测框通常包含严重的遮挡或运动模糊,并且外观特征不可靠。因此,当将BYTE应用于其他基于Re-ID的跟踪器时,我们在第二关联中不采用外观相似性。 第四步:将那些未匹配到对应轨迹的高置信度边界框作为新出现的轨迹进行保存,对应伪代码的23到27行。对两次匹配都没有匹配到的检测框,将它们初始化成新的轨迹。 class BYTETracker(object):
def __init__(self, args, frame_rate=30):
self.tracked_stracks = [] # type: list[STrack]
self.lost_stracks = [] # type: list[STrack]
self.removed_stracks = [] # type: list[STrack]
self.frame_id = 0
self.args = args
#self.det_thresh = args.track_thresh
self.det_thresh = args.track_thresh + 0.1
self.buffer_size = int(frame_rate / 30.0 * args.track_buffer) # 缓冲的帧数,超过这么多帧丢失目标才算真正的丢失
self.max_time_lost = self.buffer_size
self.kalman_filter = KalmanFilter()
def update(self, output_results, img_info, img_size):
"""
追踪的主要逻辑函数
"""
self.frame_id += 1
activated_starcks = []
refind_stracks = []
lost_stracks = []
removed_stracks = []
# output_results 是 [xyxy,score] 或者 [xyxy, score, conf] 的情况
if output_results.shape[1] == 5:
scores = output_results[:, 4]
bboxes = output_results[:, :4]
else:
output_results = output_results.cpu().numpy()
scores = output_results[:, 4] * output_results[:, 5]
bboxes = output_results[:, :4] # x1y1x2y2
img_h, img_w = img_info[0], img_info[1]
scale = min(img_size[0] / float(img_h), img_size[1] / float(img_w))
bboxes /= scale
# 找到置信度高的框,作为第一次关联的框
remain_inds = scores > self.args.track_thresh
inds_low = scores > 0.1
inds_high = scores < self.args.track_thresh
# 找到置信度低的框,作为第二次关联的框
inds_second = np.logical_and(inds_low, inds_high)
dets_second = bboxes[inds_second]
dets = bboxes[remain_inds]
scores_keep = scores[remain_inds]
scores_second = scores[inds_second]
if len(dets) > 0:
'''Detections'''
# 封装初始框为STrack的格式
detections = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
(tlbr, s) in zip(dets, scores_keep)]
else:
detections = []
''' Add newly detected tracklets to tracked_stracks'''
unconfirmed = []
tracked_stracks = [] # type: list[STrack]
for track in self.tracked_stracks:
# 当track只有一帧的记录时,is_activated=False
if not track.is_activated:
unconfirmed.append(track)
else:
tracked_stracks.append(track)
''' Step 2: First association, with high score detection boxes'''
# 将已经追踪到的track和丢失的track合并
# 其中注意:丢失的 track 是指某一帧可能丢了一次,但是仍然在缓冲帧范围之内,所以依然可以用来匹配
strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
# Predict the current location with KF
STrack.multi_predict(strack_pool) # 先用卡尔曼滤波预测每一条轨迹在当前帧的位置
# 让预测后的track和当前帧的detection框做cost_matrix,用的方式为 IOU 关联
# 这里的iou_distance 函数中调用了track.tlbr,返回的是预测之后的 track 坐标信息
dists = matching.iou_distance(strack_pool, detections)
if not self.args.mot20:
dists = matching.fuse_score(dists, detections)
# 用匈牙利算法算出相匹配的 track 和 detection 的索引,
# 以及没有被匹配到的 track 和没有被匹配到的 detection 框的索引
matches, u_track, u_detection = matching.linear_assignment(dists, thresh=self.args.match_thresh)
for itracked, idet in matches:
# 找到匹配到的所有track&detection pair 并且用 detection 来更新卡尔曼的状态
track = strack_pool[itracked]
det = detections[idet]
# 对应 strack_pool 中的 tracked_stracks
if track.state == TrackState.Tracked:
track.update(detections[idet], self.frame_id)
activated_starcks.append(track)
else:
# 对应 strack_pool 中的 self.lost_stracks,重新激活 track
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track)
''' Step 3: Second association, with low score detection boxes'''
# association the untrack to the low score detections
if len(dets_second) > 0:
'''Detections'''
detections_second = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
(tlbr, s) in zip(dets_second, scores_second)]
else:
detections_second = []
# 找出strack_pool中没有被匹配到的track(这帧目标被遮挡的情况)
r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked]
dists = matching.iou_distance(r_tracked_stracks, detections_second)
matches, u_track, u_detection_second = matching.linear_assignment(dists, thresh=0.5)
# 在低置信度的检测框中再次与没有被匹配到的 track 做 IOU 匹配
for itracked, idet in matches:
track = r_tracked_stracks[itracked]
det = detections_second[idet]
if track.state == TrackState.Tracked:
track.update(det, self.frame_id)
activated_starcks.append(track)
else:
track.re_activate(det, self.frame_id, new_id=False)
refind_stracks.append(track)
# 如果 track 经过两次匹配之后还没有匹配到 box 的话,就标记为丢失了
for it in u_track:
track = r_tracked_stracks[it]
if not track.state == TrackState.Lost:
track.mark_lost()
lost_stracks.append(track)
'''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
# 处理第一次匹配时没有被track匹配的检测框(一般是这个检测框第一次出现的情形)
detections = [detections[i] for i in u_detection]
dists = matching.iou_distance(unconfirmed, detections) # 计算未被匹配的框和不确定的track之间的cost_matrix
if not self.args.mot20:
dists = matching.fuse_score(dists, detections)
matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7)
for itracked, idet in matches:
unconfirmed[itracked].update(detections[idet], self.frame_id)
activated_starcks.append(unconfirmed[itracked])
# 匹配不上的unconfirmed_track直接删除,说明这个track只出现了一帧
for it in u_unconfirmed:
track = unconfirmed[it]
track.mark_removed()
removed_stracks.append(track)
""" Step 4: Init new stracks"""
# 经过上面这些步骤后,如果还有没有被匹配的检测框,说明可能画面中新来了一个物体,
# 那么就直接将他视为一个新的track,但是这个track的状态并不是激活态。
# 在下一次循环的时候会先将它放到unconfirmed_track中去,
# 然后根据有没有框匹配它来决定是激活还是丢掉
for inew in u_detection:
track = detections[inew]
if track.score < self.det_thresh:
continue
track.activate(self.kalman_filter, self.frame_id)
activated_starcks.append(track)
""" Step 5: Update state"""
# 对于丢失目标的track来说,判断它丢失的帧数是不是超过了buffer缓冲帧数,超过就删除
for track in self.lost_stracks:
if self.frame_id - track.end_frame > self.max_time_lost:
track.mark_removed()
removed_stracks.append(track)
# print('Ramained match {} s'.format(t4-t3))
# 指上一帧匹配上的track
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
# 加上这一帧新激活的track(两次匹配到的track,以及由unconfirm状态变为激活态的track
self.tracked_stracks = joint_stracks(self.tracked_stracks, activated_starcks)
# 加上丢帧目标重新被匹配的track
self.tracked_stracks = joint_stracks(self.tracked_stracks, refind_stracks)
# self.lost_stracks 在经过这一帧的匹配之后如果被重新激活的话就将其移出列表
self.lost_stracks = sub_stracks(self.lost_stracks, self.tracked_stracks)
# 将这一帧丢掉的track添加进列表
self.lost_stracks.extend(lost_stracks)
# self.lost_stracks 如果在缓冲帧数内一直没有被匹配上被 remove 的话也将其移出 lost_stracks 列表
self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
# 更新被移除的track列表
self.removed_stracks.extend(removed_stracks)
# 将这两段 track 中重合度高的部分给移除掉
self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
# get scores of lost tracks
# 得到最终的结果,也就是成功追踪的track序列
output_stracks = [track for track in self.tracked_stracks if track.is_activated]
return output_stracksdemo展示 ![](data:image/svg+xml;utf8,svg%20xmlns='http://www.w3.org/2000/svg'%20width='0'%20height='0'/svg) 参考: https://github.com/ifzhang/ByteTrackhttp://www.manongjc.com/detail/64-pqvfwkbdkhkwrzu.htmlhttps://zhuanlan.zhihu.com/p/90835266原文链接:关注公众号,回复【ByteTrack】可获取论文和注释版源码!!!
|