实时目标追踪:ByteTrack算法步骤详解和代码逐行解析

您所在的位置:网站首页 轨迹是啥 实时目标追踪:ByteTrack算法步骤详解和代码逐行解析

实时目标追踪:ByteTrack算法步骤详解和代码逐行解析

2023-08-22 06:09| 来源: 网络整理| 查看: 265

文章目录 ByteTrack算法简介算法步骤分析:算法框架流程:一、 对追踪轨迹和边界框进行分类二、对轨迹进行第一次追踪(仅针对激活状态的轨迹的高分匹配)三、对轨迹进行第二次追踪(仅针对激活状态的轨迹的低分匹配)四、对未激活状态的轨迹进行追踪五、新建轨迹六、返回结果 算法实现流程 算法源码(逐行注释)

ByteTrack算法简介

ByteTrack算法是一种基于目标检测的追踪算法,和其他非ReID的算法一样,仅仅使用目标追踪所得到的bbox进行追踪。追踪算法使用了卡尔曼滤波预测边界框,然后使用匈牙利算法进行目标和轨迹间的匹配。 ByteTrack算法的最大创新点就是对低分框的使用,作者认为低分框可能是对物体遮挡时产生的框,直接对低分框抛弃会影响性能,所以作者使用低分框对追踪算法进行了二次匹配,有效优化了追踪过程中因为遮挡造成换id的问题。

没有使用ReID特征计算外观相似度非深度方法,不需要训练利用高分框和低分框之间的区别和匹配,有效解决遮挡问题 算法步骤分析:

算法步骤分析会分为两块来写,第一块会按照比较习惯的思路为大家整理出算法的主要框架流程,读者可以很清楚的了解算法的原理和步骤。第二块会按照作者代码实现的步骤和思路来讲解,方便大家更容易读懂作者的代码,以及跟上作者的代码设计思路

算法框架流程:

算法的主要思路就是创建追踪的轨迹,然后利用追踪的轨迹对每一帧的目标进行匹配,逐帧来匹配目标,从而形成完整的轨迹。

首先,我们需要先理清关键的两个单元,第一个是追踪的轨迹,是代码从第一帧开始创建,包含了持续追踪和中断追踪的一切轨迹;第二个是当前帧的边界框,当前帧的边界框是仅从当前帧中获取到的边界框,不包含任何以往帧的信息。

追踪轨迹状态解析:

激活状态:激活追踪了两帧以上的目标框(包括第一帧时目标框新建的轨迹)

未激活状态:在视频中间出现的新轨迹,并且暂未匹配到轨迹的第二点

新轨迹:新生成的轨迹

已追踪轨迹:在前一帧成功追踪的轨迹

失追轨迹:在前n帧失去追踪的轨迹(n30)

当开始扫描第一帧时,此时还没有任何的轨迹出现:算法会将所有的目标框都创建轨迹对象,并储存起来。 注意:此时所有创建的轨迹都会被标注为已追踪轨迹。

从第二帧开始,算法就会逐步构建轨迹,步骤如下:

一、 对追踪轨迹和边界框进行分类

对所有的追踪轨迹分为激活和未激活两类(激活追踪了两帧以上的目标框(包括第一帧时目标框新建的轨迹))

对所有的当前帧边界框分为高分和低分两类(按照边界框的得分阈值进行分类(官方是0.5))

二、对轨迹进行第一次追踪(仅针对激活状态的轨迹的高分匹配) 将所有的已追踪轨迹和失追轨迹合并,称为初步追踪轨迹预测初步追踪轨迹的下一帧边界框可能的位置和大小(使用卡尔曼滤波预测边界框,网上有大量详细介绍,篇幅有限,就不赘述)计算初步追踪轨迹预测的下一帧边界框与当前帧高分边界框之间的IoU(交并比)值,获取一个两两之间IoU的关系损失矩阵(IoU越小表示关联程度越大,IoU最大值为1,代表边界框间没有交集)根据IoU损失矩阵,使用匈牙利算法对初步追踪轨迹和当前帧高分边界框进行匹配,获得三个结果:已匹配的轨迹与边界框,未成功匹配的轨迹,未成功匹配的当前帧边界框。(匈牙利算法可以根据损失矩阵对两两之间进行一对一的匹配,返回匹配成功和不成功的结果,篇幅有限不赘述)使用已成功匹配的当前帧边界框更新初步追踪轨迹(把初步追踪轨迹中的框改为当前帧边界框,id还是原来的id) 三、对轨迹进行第二次追踪(仅针对激活状态的轨迹的低分匹配) 找出第一次匹配中没匹配到的轨迹,筛选出其中的已追踪轨迹(因为低分匹配不匹配那些已经失追的轨迹,可能作者认为这样比较合理)因为这些轨迹之前也已经预测过下一帧的边界框了,所以这里不用预测计算上述轨迹和当前帧的低分边界框之间的IoU使用匈牙利算法对上述追踪轨迹和当前帧低分边界框进行匹配使用已成功匹配的当前帧边界框更新上述追踪轨迹将此时还未成功追踪的轨迹标记为失追轨迹(扔给下一帧去追踪了) 四、对未激活状态的轨迹进行追踪

个人理解:我觉得这一步存在的意义可能是作者认为在视频中间突然出现的目标可信度可能不太高(可能出现重复轨迹什么的),所以对中间才新出现的轨迹比较谨慎处理

找出第一步中未成功匹配的当前帧边界框(没有成功匹配的高分边界框),并且找出未激活的轨迹计算上述轨迹和当前帧的边界框之间的IoU使用匈牙利算法对上述追踪轨迹和边界框进行匹配使用已成功匹配的当前帧边界框更新上述追踪轨迹此时将未成功追踪的未激活轨迹直接标记为已删除轨迹(作者新好狠,直接给人删了) 五、新建轨迹

如果到现在还没有成功匹配的高分边界框,就能认为是新出现的东西了,会给它分配一个新的轨迹以及新的id(低分框就之间当成误判扔掉了,不会生成新轨迹)。

注意:此时不是第一帧了,新增的轨迹都是未激活状态,如果下一帧不能成功匹配的话就会被无情删除了

六、返回结果

此时就可以放回所有已追踪的轨迹了(当然不包含失追轨迹和删除轨迹),所有轨迹都有一个唯一的id,这个结果就可以拿去作为每一帧追踪的结果了!

算法实现流程

代码实现和上面步骤基本一致,除了一些封装成类的细节和中间变量被我省略了。大家直接读我逐行注释的源码就行了

算法源码(逐行注释)

这是byte_tracker.py 文件,不包含目标检测部分,是追踪的完整代码

import numpy as np from collections import deque import os import os.path as osp import copy import torch import torch.nn.functional as F from .kalman_filter import KalmanFilter from tracker import matching from .basetrack import BaseTrack, TrackState # 这个类是用来存放轨迹的,每个轨迹都有一些自己的属性,例如id、边界框、预测框、状态等等 class STrack(BaseTrack): shared_kalman = KalmanFilter() def __init__(self, tlwh, score): # wait activate self._tlwh = np.asarray(tlwh, dtype=np.float) self.kalman_filter = None self.mean, self.covariance = None, None self.is_activated = False self.score = score self.tracklet_len = 0 def predict(self): mean_state = self.mean.copy() if self.state != TrackState.Tracked: mean_state[7] = 0 self.mean, self.covariance = self.kalman_filter.predict(mean_state, self.covariance) @staticmethod def multi_predict(stracks): if len(stracks) > 0: multi_mean = np.asarray([san.copy() for st in stracks]) multi_covariance = np.asarray([st.covariance for st in stracks]) for i, st in enumerate(stracks): if st.state != TrackState.Tracked: multi_mean[i][7] = 0 multi_mean, multi_covariance = STrack.shared_kalman.multi_predict(multi_mean, multi_covariance) for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)): stracks[i].mean = mean stracks[i].covariance = cov def activate(self, kalman_filter, frame_id): """Start a new tracklet""" self.kalman_filter = kalman_filter self.track_id = self.next_id() self.mean, self.covariance = self.kalman_filter.initiate(self.tlwh_to_xyah(self._tlwh)) self.tracklet_len = 0 self.state = TrackState.Tracked if frame_id == 1: self.is_activated = True # self.is_activated = True self.frame_id = frame_id self.start_frame = frame_id def re_activate(self, new_track, frame_id, new_id=False): self.mean, self.covariance = self.kalman_filter.update( self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh) ) self.tracklet_len = 0 self.state = TrackState.Tracked self.is_activated = True self.frame_id = frame_id if new_id: self.track_id = self.next_id() self.score = new_track.score def update(self, new_track, frame_id): """ Update a matched track :type new_track: STrack :type frame_id: int :type update_feature: bool :return: """ self.frame_id = frame_id self.tracklet_len += 1 new_tlwh = new_track.tlwh self.mean, self.covariance = self.kalman_filter.update( self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh)) self.state = TrackState.Tracked self.is_activated = True self.score = new_track.score @property # @jit(nopython=True) def tlwh(self): """Get current position in bounding box format `(top left x, top left y, width, height)`. """ if self.mean is None: return self._tlwh.copy() ret = self.mean[:4].copy() ret[2] *= ret[3] ret[:2] -= ret[2:] / 2 return ret @property # @jit(nopython=True) def tlbr(self): """Convert bounding box to format `(min x, min y, max x, max y)`, i.e., `(top left, bottom right)`. """ ret = self.tlwh.copy() ret[2:] += ret[:2] return ret @staticmethod # @jit(nopython=True) def tlwh_to_xyah(tlwh): """Convert bounding box to format `(center x, center y, aspect ratio, height)`, where the aspect ratio is `width / height`. """ ret = np.asarray(tlwh).copy() ret[:2] += ret[2:] / 2 ret[2] /= ret[3] return ret def to_xyah(self): return self.tlwh_to_xyah(self.tlwh) @staticmethod # @jit(nopython=True) def tlbr_to_tlwh(tlbr): ret = np.asarray(tlbr).copy() ret[2:] -= ret[:2] return ret @staticmethod # @jit(nopython=True) def tlwh_to_tlbr(tlwh): ret = np.asarray(tlwh).copy() ret[2:] += ret[:2] return ret def __repr__(self): return 'OT_{}_({}-{})'.format(self.track_id, self.start_frame, self.end_frame) # 正片开始 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): self.frame_id += 1 activated_starcks = [] #保存当前帧匹配到持续追踪的轨迹 refind_stracks = [] #保存当前帧匹配到之前目标丢失的轨迹 lost_stracks = [] #保存当前帧没有匹配到目标的轨迹 removed_stracks = [] #保存当前帧 # 第一步:将objects转换为x1,y1,x2,y2,score的格式,并构建strack 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 #第二步:根据scroe和track_thresh将strack分为detetions(dets)(>=)和detections_low(dets_second) remain_inds = scores > self.args.track_thresh inds_low = scores > 0.1 inds_high = scores } for t in tlista: stracks[t.track_id] = t for t in tlistb: tid = t.track_id if stracks.get(tid, 0): del stracks[tid] return list(stracks.values()) def remove_duplicate_stracks(stracksa, stracksb): pdist = matching.iou_distance(stracksa, stracksb) pairs = np.where(pdist timeq: dupb.append(q) else: dupa.append(p) resa = [t for i, t in enumerate(stracksa) if not i in dupa] resb = [t for i, t in enumerate(stracksb) if not i in dupb] return resa, resb


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3