YOLO v8目标跟踪详细解读(一)

在此之前,我们已经对yolo系列做出了详细的探析,有兴趣的朋友可以参考yolov8等文章。YOLOV8对生态进行了优化,目前已经支持了分割,分类,跟踪等功能,这对于我们开发者来说,是十分便利。今天我们对YOLO v8中支持的目标跟踪进行详细解读。
在这里插入图片描述代码地址:yolov8

一、算法简介

目标跟踪现阶段是强烈依赖目标检测结果的,主流的目标跟踪pipeline,简单的总结一下:

首先,目标检测模型输出的检测框是我们跟踪的对象。假设我们在第一帧确定了跟踪对象,即给定检测框id,那当检测模型输出第2帧检测结果,我们需要将第2帧的检测框与第1帧检测框的id进行匹配。那么匹配的过程是怎样的呢?

有人说,直接用两帧检测框的IOU去匹配呗,将IOU看作cost_matrix,利用匈牙利算法去匹配两帧的id。理想情况下,是没问题的,但是,当我们处于crowd场景下,遮挡,光照等给检测带来误差,那么,IOU直接匹配就不那么靠谱了。

远古神器卡尔曼滤波器有神奇的疗效,我们将检测框的坐标看作状态变量,通过状态转移矩阵可以预测出下一帧检测框的位置。然后,下一帧的检测框坐标作为观测量,可以对预测量进行更新并矫正,从而更好的预测下下帧的检测框位置。

总结来说,就是利用卡尔曼滤波器预测检测框位置,构建合适的cost_matrix并利用匈牙利算法匹配轨迹与当前帧检测框的id,同时加入适当的逻辑,就能构建一个效果不错的跟踪器。

二、代码详解

YOLOv8采用2022年提出的跟踪算法BoT-SORT和ByteTrack两种算法实现目标跟踪。如果你想体验该算法的跟踪效果只需要执行以下代码。

from ultralytics import YOLOmodel = YOLO('yolov8n.pt')
results = model.track(source=".avi", show=True,  save=True)

在Yolo V8中,实现对一个视频中运动目标跟踪的核心代码块为文件:.\ultralytics\tracker\trackers\byte_tracker.py 中类BYTETracker.update()。下面我们不赘述卡尔曼以及匈牙利算法的原理,仅从代码逻辑来拆解。

在这之前,需要了解一个重要的类class BaseTrack,该对象为跟踪的基类,用于处理基本的跟踪属性与操作。class STrack(BaseTrack),class BOTrack(STrack),BoT-SORT和ByteTrack的track都是继承于此。从代码中我们可以发现该类记录了跟踪id,is_activated 激活状态等等操作与属性。在跟踪过程中,每帧的检测框都会分配一个track

class BaseTrack:"""Base class for object tracking, handling basic track attributes and operations."""_count = 0track_id = 0is_activated = Falsestate = TrackState.Newhistory = OrderedDict()features = []curr_feature = Nonescore = 0start_frame = 0frame_id = 0time_since_update = 0# Multi-cameralocation = (np.inf, np.inf)@propertydef end_frame(self):"""Return the last frame ID of the track."""return self.frame_id@staticmethoddef next_id():"""Increment and return the global track ID counter."""BaseTrack._count += 1return BaseTrack._countdef activate(self, *args):"""Activate the track with the provided arguments."""raise NotImplementedError## ........

在跟踪前,先初始化跟踪器,其中self.tracked_stracks列表保存了可跟踪的轨迹,self.lost_stracks保存丢失的轨迹,self.removed_stracks保存被移除的轨迹。当self.lost_stracks的成员在满足被移除的条件后则变为self.removed_stracks的成员,而过程中若被新的track匹配上,则变为self.tracked_stracks的成员。self.frame_id用来记录帧数,self.kalman_filter表示卡尔曼滤波器,后面称之为KF。

def __init__(self, args, frame_rate=30):"""Initialize a YOLOv8 object to track objects with given arguments and frame rate."""self.tracked_stracks = []  # type: list[STrack]self.lost_stracks = []  # type: list[STrack]self.removed_stracks = []  # type: list[STrack]self.frame_id = 0self.args = argsself.max_time_lost = int(frame_rate / 30.0 * args.track_buffer)self.kalman_filter = self.get_kalmanfilter()self.reset_id()

接下来正式进入update逻辑讲解。每检测一帧就会执行一次update,因此self.frame_id帧数+1,activated_starcks 用来保存该帧激活的轨迹,refind_stracks保存该帧重新激活的轨迹,lost_stracks 保存该帧丢失的轨迹,removed_stracks 保存该帧移除的轨迹。results为YOLOv8的检测结果,bboxes 是检测框坐标,后面接上了索引。

def update(self, results, img=None):"""Updates object tracker with new detections and returns tracked object bounding boxes."""self.frame_id += 1activated_starcks = []refind_stracks = []lost_stracks = []removed_stracks = []scores = results.confbboxes = results.xyxycls = results.cls# Add indexbboxes = np.concatenate([bboxes, np.arange(len(bboxes)).reshape(-1, 1)], axis=-1)

byte_tracker根据置信度将检测框分成两类,当置信度高于self.args.track_high_thresh(0.5)时,我们将其称为第一检测框,当置信度低于0.5且高于0.1时,称为第二检测框。

		remain_inds = scores > self.args.track_high_threshinds_low = scores > self.args.track_low_threshinds_high = scores < self.args.track_high_threshinds_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]cls_keep = cls[remain_inds]cls_second = cls[inds_second]

为每个检测框分配track,上面讲到,track类提供了跟踪属性与操作。目前,YOLOV8中的self.args.with_reid=False,与特征相关的还未实现。

detections = self.init_track(dets, scores_keep, cls_keep, img) 
def init_track(self, dets, scores, cls, img=None):"""Initialize track with detections, scores, and classes.为每一个det分配一个track"""if len(dets) == 0:return []if self.args.with_reid and self.encoder is not None:features_keep = self.encoder.inference(img, dets)return [BOTrack(xyxy, s, c, f) for (xyxy, s, c, f) in zip(dets, scores, cls, features_keep)]  # detectionselse:return [BOTrack(xyxy, s, c) for (xyxy, s, c) in zip(dets, scores, cls)]  # detections

步骤一:将可跟踪轨迹与第一检测框进行关联匹配。首先,遍历self.tracked_stracks可跟踪轨迹,将已激活的轨迹添加到tracked_stracks中,未被激活的轨迹加入unconfirmed未证实轨迹。将丢失轨迹与激活轨迹合并到strack_pool中,注意剔除重复track_id。重点来了,self.multi_predict(strack_pool),我们需要利用KF将strack_pool中前一帧轨迹通过状态转移矩阵预测出该帧最优估计以及状态协方差矩阵,并计算轨迹池中的轨迹与该帧检测出的bbox的距离作为cost_matrix,利用匈牙利算法进行关联匹配。

通过匈牙利算法,我们获得matches(关联成功后轨迹与检测框的索引),u_track是未关联上的轨迹,u_detection未关联上的检测框。

		unconfirmed = []tracked_stracks = []  # type: list[STrack]for track in self.tracked_stracks:if not track.is_activated:unconfirmed.append(track)else:tracked_stracks.append(track)# Step 2: First association, with high score detection boxesstrack_pool = self.joint_stracks(tracked_stracks, self.lost_stracks) ##将丢失track与激活track合并到strack_pool中,注意剔除重复track_id。# Predict the current location with KFself.multi_predict(strack_pool) ## 第一帧step4已初始化KF,第二帧的track通过KF的状态转移方程预测最优估计以及状态协方差矩阵if hasattr(self, 'gmc') and img is not None:warp = self.gmc.apply(img, dets)STrack.multi_gmc(strack_pool, warp)STrack.multi_gmc(unconfirmed, warp)dists = self.get_dists(strack_pool, detections) ## 计算轨迹池中的轨迹与该帧检测出的bbox的距离,并利用匈牙利算法进行匹配。matches, u_track, u_detection = matching.linear_assignment(dists, thresh=self.args.match_thresh)

遍历matches,已激活的轨迹需要根据该帧检测框的信息去更新轨迹属性,KF根据测量值(该帧的检测框坐标)矫正最优估计,并更新状态协方差矩阵。而未激活的轨迹重新关联上检测框,需要重新激活,re_activate与update功能类似。

 		for itracked, idet in matches:track = strack_pool[itracked]det = detections[idet]if track.state == TrackState.Tracked: ## 已激活的tracktrack.update(det, self.frame_id)  ## 确定匹配后更新track信息,KF根据测量值矫正最优估计,并更新状态协方差矩阵activated_starcks.append(track)else: ## 未激活的track重新匹配上需要重新激活track.re_activate(det, self.frame_id, new_id=False)refind_stracks.append(track)def update(self, new_track, frame_id):"""Update a matched track:type new_track: STrack:type frame_id: int:return:"""self.frame_id = frame_idself.tracklet_len += 1new_tlwh = new_track.tlwhself.mean, self.covariance = self.kalman_filter.update(self.mean, self.covariance,self.convert_coords(new_tlwh))self.state = TrackState.Trackedself.is_activated = Trueself.score = new_track.scoreself.cls = new_track.clsself.idx = new_track.idxdef re_activate(self, new_track, frame_id, new_id=False):"""Reactivates a previously lost track with a new detection."""self.mean, self.covariance = self.kalman_filter.update(self.mean, self.covariance, self.convert_coords(new_track.tlwh))self.tracklet_len = 0self.state = TrackState.Trackedself.is_activated = Trueself.frame_id = frame_idif new_id:self.track_id = self.next_id()self.score = new_track.scoreself.cls = new_track.clsself.idx = new_track.idx

步骤二: 将可跟踪轨迹与第二检测框进行关联匹配。首先,为第二检测框分配track,将轨迹池strack_pool中在步骤一中未关联上的轨迹存放在r_tracked_stracks中。计算r_tracked_stracks与第二检测框的IOU,将IOU作为cost_matric进行匈牙利匹配,注意与步骤一匹配过程相比,步骤二的匹配阈值从0.8降低至0.5。由于第二检测框置信度较低,坐标回归质量较差,为了捞回更多的轨迹,适当降低阈值是必要的。遍历matches,更新可激活轨迹,并重新激活未激活轨迹。若步骤二中仍存在未关联上的轨迹,需将其状态改成lost丢失

		detections_second = self.init_track(dets_second, scores_second, cls_second, img) ## 0.1<置信度<0.5的box分配trackr_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked] ## 第一次未匹配上的track# TODOdists = 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)for it in u_track: ## 第2次还未匹配上的track,将其状态改成lost丢失track = r_tracked_stracks[it]if track.state != TrackState.Lost:track.mark_lost()lost_stracks.append(track)

u_detection是步骤一中没有关联上的track(即检测框),unconfirmed未证实的轨迹是可跟踪轨迹中is_tracked=False的轨迹,这里需要与u_detection进行匹配尝试。如若关联成功,则需要更新轨迹将is_tracked置为True,并更新其KF相关参数,否则该轨迹被移除

 # Deal with unconfirmed tracks, usually tracks with only one beginning framedetections = [detections[i] for i in u_detection]dists = self.get_dists(unconfirmed, detections) ## 这里的detections是第一检测框未匹配上的matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7) for itracked, idet in matches: ## unconfirmed未证实的轨迹与步骤一未匹配上的检测框关联成功,更新轨迹并加入激活轨迹unconfirmed[itracked].update(detections[idet], self.frame_id)activated_starcks.append(unconfirmed[itracked])for it in u_unconfirmed: ## 未证实轨迹仍然关联失败,则移除未证实轨迹track = unconfirmed[it]track.mark_removed()removed_stracks.append(track)

步骤三:初始化新轨迹。当我们在处理第一帧时,因为可跟踪轨迹等列表均为空,无法匹配,因此直接进入步骤三,将检测框置信度高于self.args.new_track_thresh的track激活,is_tracked置为True,初始化KF,并将track存放于列表activated_starcks中,等待合并到self.tracked_stracks可跟踪轨迹中。当frame_id != 1时,activate不会将is_tracked置为True,未匹配上的track存于activated_starcks中,等待合并到self.tracked_stracks。注意由于is_tracked=False,这部分在步骤一中被归纳为unconfirmed未证实轨迹。

		## 处理第一帧时,因为无法匹配,直接进入Step 4,激活track,将is_tracked置True,初始化KF,并将track存放于列表activated_starcks中,等待合并到self.tracked_stracks可跟踪轨迹中。## frame_id!=1时,activate不会将is_tracked置为True,未匹配上的track存于activated_starcks中,等待合并到self.tracked_stracks。注意这部分在220行被归纳到unconfirmed中。for inew in u_detection:track = detections[inew]if track.score < self.args.new_track_thresh:continuetrack.activate(self.kalman_filter, self.frame_id)activated_starcks.append(track)

步骤四: 可跟踪轨迹在经过步骤一与步骤二后,仍未与检测框关联成功,其状态会变为lost丢失,并从可跟踪轨迹中移除,并入self.lost_stracks丢失轨迹中。如果丢失轨迹中的track在30帧内仍未匹配上,则将其移除。

 # Step 5: Update statefor track in self.lost_stracks: ## 如果超过30帧lost_track仍未匹配上,则移除if self.frame_id - track.end_frame > self.max_time_lost:track.mark_removed()removed_stracks.append(track)

步骤五: 更新self.tracked_stracks,self.lost_stracks,self.removed_stracks列表。将activated_starcks,refind_stracks合并到可跟踪轨迹中,继续在下一帧进行关联跟踪。将lost_stracks在self.tracked_stracks列表中出现的track剔除(丢失已找回),将lost_stracks在self.removed_stracks列表中出现的track剔除(丢失已移除)。remove_duplicate_stracks将self.tracked_stracks, self.lost_stracks列表中IOU接近的track去重,保留最新出现的track。最后return被激活的轨迹,其坐标是KF修正后的值。

		self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]self.tracked_stracks = self.joint_stracks(self.tracked_stracks, activated_starcks) ## 将激活的tracks合并到self.tracked_stracks列表中self.tracked_stracks = self.joint_stracks(self.tracked_stracks, refind_stracks)self.lost_stracks = self.sub_stracks(self.lost_stracks, self.tracked_stracks) ## 将lost_stracks在self.tracked_stracks列表中出现的track剔除(丢失已找回)self.lost_stracks.extend(lost_stracks)self.lost_stracks = self.sub_stracks(self.lost_stracks, self.removed_stracks) ## 将lost_stracks在self.removed_stracks列表中出现的track剔除(丢失已移除)self.tracked_stracks, self.lost_stracks = self.remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)self.removed_stracks.extend(removed_stracks)if len(self.removed_stracks) > 1000:self.removed_stracks = self.removed_stracks[-999:]  # clip remove stracks to 1000 maximumreturn np.asarray([x.tlbr.tolist() + [x.track_id, x.score, x.cls, x.idx] for x in self.tracked_stracks if x.is_activated],dtype=np.float32)

代码详解过于冗长,文字单调,缺少精炼的图释,有不理解的或者讲解错误的地方还望指出,接下来会对目标跟踪中的卡尔曼滤波进行剖析,有兴趣的朋友可以关注留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/86118.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java基础入门篇——数组初识

一、数组 1.假设某公司有100个员工&#xff0c;需要统计某公司员工的工资情况&#xff0c;首先需要声明100个变量来分别记每个员工的工资&#xff0c;那么如果按照之前的做法&#xff0c;可能定义的结构如下所示&#xff1a; int a1,a2,a3,......a100; 要求你输出这100个员工…

Android系统组件——AMS,App启动中的AMS流程

AMS&#xff08;Activity Manager Service&#xff09;是Android系统中非常重要的一个组件&#xff0c;负责管理应用程序的生命周期、进程调度以及任务栈的管理等任务。本文将从AMS的原理、数据结构、SystemServer加载AMS以及App启动中的AMS流程等方面进行详细介绍&#xff0c;…

【LeetCode】105. 从前序与中序遍历序列构造二叉树

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 105. 从前序与中序遍历序列构造二叉树 力扣 题目描述&#xff1a; 给定两个整数数组 preord…

小结:基于 JavaWeb 的宠物店管理系统

宠物店管理系统 系统介绍系统展示登录界面用户注册页面 店主主界面宠物信息管理页面修改宠物信息 宠物出入库管理页面宠物订单查询页面宠物账单查看页面用户信息管理页面修改用户信息 用户主界面宠物订购页面用户订购支付页面 个人资料编辑页面个人订单查看页面 系统说明开发环…

【力扣每日一题】2023.8.11 矩阵对角线元素的和

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个矩阵&#xff0c;让我们把矩阵对角线上的元素都加起来返回。 那么矩阵的对角线是有两条的&#xff0c;一条是从左上到右下…

CSS3 中新增了哪些常见的特性?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 圆角&#xff08;Border Radius&#xff09;⭐ 渐变&#xff08;Gradients&#xff09;⭐ 阴影&#xff08;Box Shadow&#xff09;⭐ 文本阴影&#xff08;Text Shadow&#xff09;⭐ 透明度&#xff08;Opacity&#xff09;⭐ 过渡&…

Azure Kinect DK + ROS1 Noetic使用教程

作者&#xff1a; Herman Ye Galbot Auromix 版本&#xff1a; V1.0 测试环境&#xff1a; Ubuntu20.04 更新日期&#xff1a; 2023/08/08 注1&#xff1a; 本文内容中的硬件由 Galbot 提供支持。 注2&#xff1a; Auromix 是一个机器人爱好者开源组织。 注3&#xff1a; 本文在…

当前服务器版本不支持该功能,请联系经销商升级服务器 - - 达梦数据库报错

当前服务器版本不支持该功能&#xff0c;请联系经销商升级服务器 - - 达梦数据库报错 环境介绍1 搭建测试环境2 报错内容3 标准版介绍 环境介绍 某项目使用标准版数据库中&#xff0c;使用insert into 正常操作表&#xff0c;插入数据时报错&#xff0c;表为普通表。 1 搭建测…

Maven: ‘mvn‘ is not recognized as an internal or external command

下载并配置好Maven之后&#xff0c;CMD测试安装是否成功&#xff1a;mvn -v 提示&#xff1a; mvn is not recognized as an internal or external command, operable program or batch file. 检查环境变量&#xff1a; MAVEN_HOME: %MAVEN_HOME%\bin: 看上去没问题&#x…

2.若依前后端分离版第一个增删查改

1.介绍 若依提供了代码生成功能&#xff0c;单表的CRUD可以直接用若依框架提供的代码生成进行创建。 2.实现 2.1 在数据库创建业务表test_teacher 2.2 生成代码 运行系统&#xff0c;进入菜单[系统工具]-》[代码生成],点击导入按钮&#xff0c;选择需要生成代码的表进行导…

UML—浅谈常用九种图

目录 概述: 1.用例图 2.静态图 3.行为图&#xff1a; 4.交互图&#xff1a; 5.实现图&#xff1a; 概述: UML的视图是由九种视图组成的&#xff0c;分别是用例图、类图、对象图、状态图、活动图、序列图、协作图、构件图、实施图。我们可以根据这9种图的功能和实现的目的…

JavaWeb学习|JavaBean;MVC三层架构;Filter;Listener

1.JavaBean 实体类 JavaBean有特定的写法: 必须要有一个无参构造 属性必须私有化。 必须有对应的get/set方法 用来和数据库的字段做映射 ORM; ORM:对象关系映射 表--->类 字段-->属性 行记录---->对象 2.<jsp&#xff1a;useBean 标签 3. MVC三层架构 4. Filter …

如何调教让chatgpt读取自己的数据文件(保姆级图文教程)

提示&#xff1a;如何调教让chatgpt读取自己的数据文件(保姆级图文教程) 文章目录 前言一、如何投喂自己的数据&#xff1f;二、调教步骤总结 前言 chatgpt提示不能读取我们提供的数据文件&#xff0c;我们应该对它进行调教。 一、如何投喂自己的数据&#xff1f; 让chatgpt读…

华为Mate30报名鸿蒙 HarmonyOS 4.0.0.108 系统更新

华为 Mate 30 系列于 2019 年 11 月 1 日上市&#xff0c;包括 Mate 30 4G / 5G、Mate 30 Pro 4G / 5G、保时捷设计版 Mate30 共五款机型。华为 Mate 30 系列 5G 版搭载麒麟 990 5G 处理器&#xff0c;同时支持 SA 及 NSA 5G 双模&#xff0c;适配三大运营商的 5G / 4G / 3G / …

以mod_jk方式整合apache与tomcat(动静分离)

前言&#xff1a; 为什么要整合apache和tomcat apache对静态页面的处理能力强&#xff0c;而tomcat对静态页面的处理不如apache&#xff0c;整合后有以下好处 提升对静态文件的处理性能 利用 Web 服务器来做负载均衡以及容错 更完善地去升级应用程序 jk整合方式介绍&#…

【构建卷积神经网络】

构建卷积神经网络 卷积网络中的输入和层与传统神经网络有些区别&#xff0c;需重新设计&#xff0c;训练模块基本一致 全连接层&#xff1a;batch784&#xff0c;各个像素点之间都是没有联系的。 卷积层&#xff1a;batch12828&#xff0c;各个像素点之间是有联系的。 impor…

将vsCode 打开的多个文件分行(栏)排列,实现全部显示,便于切换文件

目录 1. 前言 2. 设置VsCode 多文件分行(栏)排列显示 1. 前言 主流编程IDE几乎都有排列切换选择所要查看的文件功能&#xff0c;如下为Visual Studio 2022的该功能界面&#xff1a; 图 1 图 2 当在Visual Studio 2022打开很多文件时&#xff0c;可以按照图1、图2所示找到自…

Golang struct 结构体指针类型 / 结构体值类型

struct类型的内存分配机制 结构体变量之间的赋值是值拷贝。 type stu struct {Name stringSlice []stringMap1 map[string]string }func main() {s : stu{}s.Slice make([]string, 6)s.Slice[1] "ssss"s.Slice[2] "xxxx"s.Map1 make(map[string]stri…

基础堆排序

目录 基础堆排序 一、概念及其介绍 二、适用说明 三、过程图示 基础堆排序

【Opencv入门到项目实战】(十):项目实战|文档扫描|OCR识别

所有订阅专栏的同学可以私信博主获取源码文件 文章目录 1.引言1.1 什么是光学字符识别 (OCR)1.2 应用领域 2.项目背景介绍3.边缘检测3.1 原始图像读取3.2 预处理3.3 结果展示 3.轮廓检测4.透视变换5.OCR识别5.1 tesseract安装5.2 字符识别 1.引言 今天我们来看一个OCR相关的文…