ByteTracker行人跟踪核心代码解读

byteTracker中因为目标检测和行人跟踪是解耦的,因此这里主要分析的是byteTracker中的代码。

也即是分析当给定一帧图片frame_id,给定这帧中的box列表,行人跟踪类是怎么跟踪每条轨迹的。

也就是https://github.com/ifzhang/ByteTrack中位于目录tutorials/trades/byte_tracker中的代码。

首先这个代码中最重要的两个类,一个是轨迹类STrack,一个是跟踪类BYTETracker类
前者是每条轨迹,后者管理目前视频流中的所有轨迹,并在新的一帧到来之后通过调用类方法update更新当前视频流中的轨迹状态(可能有新的轨迹,可能有旧的轨迹失去跟踪目标框,可能有目标框匹配成功,可能有之前丢失目标框的轨迹重新匹配成功)。

之所以写这个代码解读,是因为每次重新捡起来阅读的时候都被各种参数搞得晕头转向,很容易混淆不同参数的含义。索性写个详细的笔记记录一下。

一、轨迹类STrack

这里面保存的都是轨迹自身的状态。跟踪类BYTETracker类通过调用轨迹的属性和方法来进行轨迹状态的管理。

1 属性集合

属性名称类型含义
self._tlwh列表保存目标框属性
self.is_activatedbool该轨迹是否是激活状态(也就是保持追踪中),如果是视频流的第一帧,那么第一次update的时候,该属性就是True,如果不是第一帧,那么需要第二次匹配成功的时候才会设置为True(考虑一下误识别的情况,这就可以理解了,只有当连续两帧都匹配成功,才认为这个轨迹可靠)。
self.kalman_filter类实例在第一次调用activate()函数时,设置卡尔曼滤波器
self.mean, self.covariancendarray保存卡尔曼滤波对于这个轨迹的mean和convariance.其中len(mean)=8(x,y,a,h,va,vy,va,vh),v表示速度,covariance为[8,8]。在第一次调用activate()函数时进行初始化,初始化根据目标框的坐标进行估计
self.tlbrndarray如果self.is_activated=True,那么就是根据卡尔曼滤波预测的左上角和右下角坐标,如果self.is_activated=False,那么就是当前检测框观测值的左上角和右下角坐标
self.tlwhndarray如果self.is_activated=True,那么就是根据卡尔曼滤波预测的左上角和宽高,如果self.is_activated=False,那么就是当前检测框观测值的左上角和宽高
self.scorefloat轨迹分数,采用当前帧的目标框分数作为轨迹分数
self.tracklet_lenint轨迹追踪的帧数,初始为0,后面每追踪成功(调用update方法),则+1
self.stateTrackStateTrackState.New,Tracked,Lost,Removed四种状态,初始化为new。如果调用activate()/re_activate()/update方法时转换为Tracked状态,调用mark_lost()方法转为Lost状态,调用mark_removed()方法转为Removed方法。
self.track_idintself.is_activated=True 轨迹的id,全局唯一标志
self.start_frameint第一次调用activate方法时,这个轨迹的frame_id
self.frame_idint目前位置,最后一次出现该轨迹的匹配框的帧编号
self.end_frameint最后一帧出现该轨迹的帧编号,同frame_id

2. 方法集合

方法名称备注
init(self, tlwh, score)每个目标框都会初始化一个track,但是此时的self.is_activated=False
activate(self, kalman_filter, frame_id)激活这条轨迹,如果是当前帧是视频流的第一帧,那么设置self.is_activated=True,否则这个属性依旧是False;设置state属性为TrackState.Tracked
update(self, new_track, frame_id)更新轨迹信息,设置is_activate=True主要更新self.frame_id,self.score,tracklet_len,卡尔曼滤波mean,conv;
re_activate(self, new_track, frame_id, new_id=False)重新激活这个轨迹(之前处于丢失状态),重新计算卡尔曼滤波的mean, covariance;重新计算self.tracklet_len;更新self.frame_id,self.score;
mark_lost(self)将state参数设置为TrackState.Lost

这里需要注意两个概念:
1. is_activated状态和activate函数的关系
不是调用了activate函数,轨迹的is_activated状态就是True。
is_activated状态可以理解为该轨迹是否大概率为真实的轨迹。
如果这状态为True,我们会认为这个轨迹是一个真实的轨迹,已经通过多帧的匹配得到的认证,是在追踪逻辑中高优先级的一个轨迹。但是如果这个轨迹是第一次出现,也就是目前只有一帧图片中出现了目标框,那么需要后续帧能够存在匹配的目标框来确保这个轨迹是真实的,而不是检测器的误检,那么此时这个is_activated状态就是False。第一帧数据除外,第一帧数据中的高分目标框默认生成的轨迹is_activated状态就是True。

调用了activate函数是表明当前这个目标框分数较高,很可能是一个新的轨迹的第一帧。

is_activated状态和state状态的关系
正如上面说了is_activated的含义,state的含义单纯表示当前轨迹在当前帧的状态。
初始化为New;
对于第一次出现且没有匹配轨迹的高分检测框,会调用activate函数初始化一条轨迹,那么状态就是Tracked;
对于能够匹配到目标框的轨迹,通过调用轨迹的update方法,依旧将状态维持为Tracked;
对于当前帧丢失匹配的情况,将状态转为Lost;
对于多帧丢失乃至于超过BYTETracker定义max_time_lost的,则将其state设置为Removed。

二、轨迹追踪类BYTETracker

整个服务维护一个BYTETracker对象,负责当前视频所有轨迹的追踪和维护。其内部的类属性中最上面的三个列表维护了当前视频流中的所有轨迹,其余类属性为各种阈值参数。

属性类

属性名称类型含义
self.tracked_strackslist[STrack]维护当前追踪中的轨迹列表
self.lost_strackslist[STrack]维护到前一帧为止的追踪中丢失了检测框的轨迹列表
self.removed_strackslist[STrack]维护删除的轨迹列表
self.frame_idint当前视频流的帧id,默认从1开始,每次调用update方法,+1
self.det_threshfloat检测阈值,高于这个阈值且无法在当前轨迹中找到匹配的检测框可以生成一条新的轨迹。
self.track_thresh高质量检测框的分数阈值
self.match_threshfloat轨迹和目标框匹配的阈值
self.low_threshfloat低质量检测框的分数阈值
self.max_time_lostint一条轨迹如果在该值数量的帧中都丢失了检测框,则认为这个轨迹丢失

方法类

方法名称备注
init()初始化,初始化上述的属性
reset(self)对于新的视频流需要重新初始化这个类的属性
step(self, output_results)比较复杂,详细见下面

step方法详解

step(self, output_results)函数中的操作
先定义的一些用于保存中间结果的函数内列表

  • activated_starcks = []#存在的轨迹并且已经出于激活状态is_activate=True
  • refind_stracks = []#当前帧中重新找到匹配的轨迹,这些轨迹在之前的状态中为Lost状态
  • lost_stracks = []#当前帧中丢失匹配的轨迹,之前这些轨迹的状态为Tracked
  • removed_stracks = []#当前帧中需要删除的轨迹,这些轨迹到目前为止的丢失匹配帧数超过了self.max_time_lost
  • unconfirmed = []#存在轨迹但是is_activate=False,但是因为这个轨迹出现的帧数不多(目前这版中认为出现1次的为unconfirmed,两次及以上就是确定的真实轨迹),因此不确定是否能真实(也就是目前还没有激活的轨迹)
  • tracked_stracks = [] #self.tracked_stracks中的轨迹且满足轨迹属性is_activated为True

将当前帧检测到的目标框按照阈值分为detections,det_second,分别表示高质量检测框和低质量检测框;
将当前类中处于追踪状态的轨迹self.tracked_stracks列表,按照属性track.is_activated是否为True划分到tracked_stracks列表和unconfirmed列表中。其中unconfirmed中的轨迹为上一帧中首次出现了高质量目标框,但是因为出现的次数只有一次,所以还不能确定是否真的是一个新轨迹,需要在本帧中进行验证的新轨迹,这类的轨迹称为低优先级轨迹;将tracked_stracks+self.lost_stracks的轨迹集联合起来作为高优先级轨迹,统一更新高优先级轨迹的卡尔曼滤波得到本帧的预测结果。

具体的步骤如下

  1. 将高质量检测框和步骤2中的高优先级轨迹的预测框进行计算IOU距离,然后乘上检测框的score作为一个联合分数cost_matrix,显然如果IOU越大且score越大,那么认为检测框和2中的轨迹关联性越大。利用匈牙利算法和阈值self.match_thresh,根据上面计算得到的cost_matrix进行匹配,结果集合分为三个matches, u_track, u_detection,分别表示匹配的(轨迹,目标框),未能匹配的轨迹,未能匹配的高质量检测框
  2. 对于匹配的(轨迹,目标框),如果轨迹的状态为Tracked,那么更新轨迹状态(调用轨迹的update方法),将这个轨迹添加到activated_starcks列表中
  3. 对于匹配的(轨迹,目标框),如果轨迹的状态为其他,那么重新激活这个轨迹(调用轨迹的re_activate方法),将这个轨迹添加到refind_stracks列表中
  4. 将低质量的检测框和步骤1中未能匹配的高优先级轨迹u_track中的追踪中轨迹(不考虑lost_strack中的轨迹),执行步骤1计算cost_matrix,需要注意这里的cost_matrix不需要考虑检测框的score权重。将得到的代价矩阵匈牙利匹配算法,此时匹配的阈值为0.4,同样获得matches, u_track, u_detection_second
  5. 对于步骤4中匹配的(轨迹、目标框),更新轨迹状态(调用轨迹的update方法),将这个轨迹添加到activated_starcks列表中
  6. 对于步骤4中u_track轨迹,将其轨迹状态设置为lost(self.state = TrackState.Lost),将这个轨迹添加到lost_stracks列表中
  7. 对于步骤1中的未能匹配的高质量检测框u_detection和unconfirmed列表中的低优先级轨迹执行IOU距离计算获得代价矩阵,然后执行匈牙利匹配,得到matches, u_unconfirmed依旧未匹配的轨迹, u_detection依旧未匹配的高质量目标框。
  8. 对于matches中的(轨迹、目标框),更新轨迹状态(调用轨迹的update方法),因为这里的轨迹是原本unconfirmed的,也就是出现过一次,轨迹状态为state=TrackState.New,is_activate=False的轨迹,这里调用update之后,轨迹的状态为TrackState.Tracked,is_activated = True。将这个轨迹添加到activated_starcks列表中。
  9. 对于步骤7中的u_unconfirmed依旧未匹配的轨迹,标记其状态为state = TrackState.Removed。并将这个轨迹添加到removed_stracks中。这个轨迹因为只在上帧出现依次,本帧没有能够匹配的目标框,因此不认为这个轨迹值得存在了。
    10.对于步骤7中的u_detection依旧未匹配的高质量目标框,如果其分数大于阈值self.det_thresh,那么认为这可能是一个新轨迹的第一帧,将其转换为一个新轨迹,调用activate方法;并将这条新轨迹添加到activated_starcks列表中。
  10. 对于类属性self.lost_stracks中的轨迹,判断到当前为止丢失的帧数量,如果超过阈值,那么标记该轨迹的状态为state = TrackState.Removed,并将该轨迹添加到removed_stracks列表。
    请添加图片描述

虽然上面的过程因为参与的参数和列表名称太多显得非常的复杂,但是通过下面这张图可以清晰的看到每个步骤的:

通过上面的逻辑表述和脑图表述基本可以确定方法列表中保存的额内容

  • activated_starcks中保存了高优先级轨迹和高质量目标框的匹配轨迹;高优先级轨迹和低质量目标框中的匹配轨迹;低优先级轨迹和高质量目标框的匹配轨迹;什么都未能匹配上的高质量目标框单独构成一条新轨迹(其状态为TrackState.Tracked,is_activated=False,也就是对于下一帧来说这个轨迹是一个unconfirmed低优先级的轨迹)。
  • refind_stracks中保存了原本self.lost_stracks中的轨迹,但是在这一帧中找到了匹配的目标框的轨迹。
  • lost_stracks中保存了本帧中高优先级轨迹无论在高质量目标框还是低质量目标框中都无法匹配的轨迹;
  • removed_stracks保存了本帧中unconfirmed低优先级轨迹在本帧没有找到目标框匹配的轨迹,以及高优先级轨迹中丢失目标框的帧数超过阈值的轨迹;

最后就是根据上述的5个函数属性来更新类属性self.tracked_stracks,self.lost_stracks,self.removed_stracks

  • self.tracked_stracks=原本的self.tracked_stracks中到现在为止状态依旧为TrackState.Tracked的轨迹+方法属性activated_starcks列表+方法属性refind_stracks
  • self.lost_stracks=原本self.lost_stracks中到目前位置状态依旧为TrackState.Lost的轨迹+方法属性lost_stracks列表
  • self.removed_stracks = 原本的self.removed_stracks+方法属性removed_stracks列表

最后输出self.tracked_stracks中track.is_activated的轨迹作为当前帧的显式跟踪的轨迹。

通过上述的逻辑,每帧视频流都会传入目标框,通过step方法不停的更新类属性中的self.tracked_stracks,self.lost_stracks,self.removed_stracks列表来实现轨迹的跟踪。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年8月8日
下一篇 2023年8月8日

相关推荐