目标跟踪ByteTrack论文代码详解(Python)

在这里插入图片描述

ByteTrack: Multi-Object Tracking by Associating Every Detection Box

论文:https://arxiv.org/pdf/2110.06864.pdf
github:https://github.com/ifzhang/ByteTrack

一. 背景

多目标跟踪(MOT)的目标是估计视频中目标的边界框和ID。大多数方法通过关联得分高于阈值的检测框来获得ID。对于检测分数较低的目标(如被遮挡)会直接丢弃,造成不可忽略的真目标缺失和轨迹碎片化。

二. 解决方法

为了解决这一问题,本文提出了一种简单、有效、通用的关联方法,通过关联几乎所有的检测框来跟踪,而不是仅仅关联得分高的检测框。

三. 总结

1. 两种框
  1. 检测框
  2. 跟踪框(航迹)
2. 两个阈值
  1. 是否为高分检测:track_thresh
  2. 是否可新启航迹:high_thresh
    high_thresh一般大于track_thresh
3. 两次匹配
  1. 所有航迹与高分检测框匹配:
    匹配失败的高分检测框新启航迹
  2. 第一次匹配失败的航迹与低分检测框再匹配
    匹配失败的低分检测框记为背景
    匹配失败的航迹记为lost等待重生或消亡(连续30帧都失败)
4. 一个指标

两次匹配均使用IoU(交并比),因为低分数检测框通常包含严重的遮挡或运动模糊,并且外观特征不可靠,因此不适用外观相似度

四. 伪代码

在这里插入图片描述

五. 代码详解(Python)

此跟踪算法的核心逻辑在yolox.tracker.byte_tracker的类class BYTETracker(object)
以下详解对最核心的代码逐步解释,源码请到原作者的github仓库

这里用到了一个自定义的类型STrack,表示航迹
strack有几个标志Flag:

# 可查询四种状态
class TrackState(object):
	New = 0
	Tracked = 1
	Lost = 2
	Removed = 3

# 表示是否激活
is_activated = Flase

首先是几个重要的列表的定义,用来存放各种状态的stracks

self.tracked_stracks = []  # 此列表存放成功追踪到的航迹
self.lost_stracks = []  # 此列表暂存丢失航迹(可能由于完全遮挡或者出视野)
self.removed_stracks = []  # 此列表存放彻底删除的航迹(丢失过长时间)

activated_starcks = []  # 临时列表放已激活的航迹
refind_stracks = []  # 临时列表放原本丢失又重生的航迹
lost_stracks = []    # 临时列表放丢失的航迹
removed_stracks = [] # 临时列表放删除的航迹

还有三个阈值要进行说明:

args.track_thresh    # detect的分数高于此值才被认为是高分检测框
args.match_thresh    # 计算IoU距离时高于此值才被认为匹配

# detect的分数高于det_thresh才会激活一个新追踪目标
# 这个值比一般要求的高分检测框还要大0.1,激活新目标的条件更严格
self.det_thresh = args.track_thresh + 0.1    

BYTETracker的主要方法def update(self, output_results, img_info, img_size):

先从检测器处获得检测结果output_results,包含detect box和score

scores = output_results[:, 4]
bboxes = output_results[:, :4]

按检测器给出的score分为高分检测框和低分检测框

remain_inds = scores > self.args.track_thresh  
inds_low = scores > 0.1
inds_high = scores < self.args.track_thresh

# 下面用了 _second 表示是第二步匹配时会用到的
inds_second = np.logical_and(inds_low, inds_high)   # 低于阈值但高于0.1
dets_second = bboxes[inds_second]  # 分数低于阈值但高于0.1的那些检测框

dets = bboxes[remain_inds]    # 分数高的那些检测框

把高分检测器的检测框转为STrack类型便于后续的匹配

if len(dets) > 0:
    detections = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
                  (tlbr, s) in zip(dets, scores_keep)]

新识别到的航迹处于非激活状态(除了第一帧),先放入unconfirmed列表,后面再处理
在上一帧已经是追踪状态、激活状态的的就放入tracked_stracks列表

unconfirmed = []    # 临时列表放上一帧新激活的还没有开始追踪的
tracked_stracks = []    # 临时列表放上一帧就已经在追踪的
for track in self.tracked_stracks:
    if not track.is_activated:
        unconfirmed.append(track)
    else:
        tracked_stracks.append(track)

接下来就开始了第一次匹配,将所有航迹与高分检测框匹配

# 所有航迹包括了新激活的、追踪中的、丢失的
strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)

# 使用Kalman滤波器预来预测航迹的新位置
STrack.multi_predict(strack_pool)

# 使用IoU距离匹配
# strack_pool里面有所有航迹,detections里面有所有高分检测框
dists = matching.iou_distance(strack_pool, detections)
if not self.args.mot20:
	dists = matching.fuse_score(dists, detections)
# 返回值
# matches 匹配成功的航迹与检测框
# u_track 匹配失败剩余的航迹(既没有与之匹配的检测框)
# u_detection 匹配失败剩余的检测框(既没有与之匹配的跟踪航迹)
# 参数match_thresh是设定多大的距离认为是匹配
matches, u_track, u_detection = matching.linear_assignment(dists, 
												thresh=self.args.match_thresh)

# 把匹配成功的航迹进行状态更新
# 那些处于跟踪状态的的航迹则更新位置,放入激活航迹的列表
# 那些处于丢失状态的航迹则再次激活,放入重生航迹的列表
for itracked, idet in matches:
	track = strack_pool[itracked]
	det = detections[idet]
	if track.state == TrackState.Tracked:
		track.update(detections[idet], self.frame_id)
		activated_starcks.append(track)
	else:
		track.re_activate(det, self.frame_id, new_id=False)
		refind_stracks.append(track)

第一次匹配失败的航迹并没有丢掉,还再进行第二次的匹配,与低分检测框匹配。这就是这篇论文的创新,关联了低分检测框,解决了严重遮挡导致轨迹丢失的问题。

# dets_second里面有分数高于0.1但是低于阈值的低分检测框
if len(dets_second) > 0:
    # 同样地,把检测器的检测框转为STrack类型便于后续的匹配
    detections_second = [STrack(STrack.tlbr_to_tlwh(tlbr), s) for
                          (tlbr, s) in zip(dets_second, scores_second)]
else:
	detections_second = []

# 取出第一次匹配失败剩余的航迹,只要跟踪中的,丢失的不做处理
r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked]
# 第二次匹配也用IoU距离,不能用外观形似度
dists = matching.iou_distance(r_tracked_stracks, detections_second)
# 这里的匹配阈值比第一次的小了,放宽条件
matches, u_track, u_detection_second = matching.linear_assignment(dists, thresh=0.5)
# 第二次匹配成功的话那就是正规军啦,与第一次的同等地位,放入“编制”!
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)

# u_track里面是第二次匹配还是失败的剩余航迹,也不删除还有得用,列为丢失等待重生
for it in u_track:
	track = r_tracked_stracks[it]
	if not track.state == TrackState.Lost:
		track.mark_lost()
		lost_stracks.append(track)

第一次匹配中失败的高分检测框很可能是新进入的目标,没有与之对应的航迹,因此新启航迹

# u_detection 第一次匹配中失败的高分检测框
for inew in u_detection:
	track = detections[inew]
	# 新启的条件严格一点,看开头说明的三个阈值
	if track.score < self.det_thresh:
		continue
	# 激活一个kalman滤波器,下一帧开始预测航迹新位置
	track.activate(self.kalman_filter, self.frame_id)
	activated_starcks.append(track)

把那些在lost_stracks列表里存在过长时间(一直没得到重生)的就认为轨迹消亡了,可以删除了

for track in self.lost_stracks:
	if self.frame_id - track.end_frame > self.max_time_lost:
		track.mark_removed()
		removed_stracks.append(track)

把所有临时列表里的轨迹放入最终的列表,作为这一帧确定的结果

# 上一帧本来就跟踪状态的就一样地放进来
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
# 上面代码成功匹配的轨迹都加进activated_starcks了,把他们添加进来
self.tracked_stracks = joint_stracks(self.tracked_stracks, activated_starcks)
# 这帧重生的也添加进来
self.tracked_stracks = joint_stracks(self.tracked_stracks, refind_stracks)
# 轨迹重生了,那就要从self.lost_stracks中移除
self.lost_stracks = sub_stracks(self.lost_stracks, self.tracked_stracks)
# 这帧被列为丢失的,放到self.lost_stracks
self.lost_stracks.extend(lost_stracks)
# 被删除的肯定原来是在self.lost_stracks里面,要把它们从中移除 
self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
# 这帧刚被列为删除的,放入self.removed_stracks,等待下一帧才会被移除
self.removed_stracks.extend(removed_stracks)
# 前面把各种各样杂七杂八的都放进来了,可能会有重复的,最后筛一遍。:) 细,真细啊
self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值