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.mzph.cn/news/32586.shtml

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

相关文章

【Vue+Element-plus】记录后台首页多echart图静态页面

一、页面效果 二、完整代码 Index.vue <template><div><div><DateTime /><!-- {{username}} --></div><el-row :gutter"20"><el-col :span"8"><div class"grid-content bg-purple"><P…

【Python数据容器】--- 元组的基本使用

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【Python小白从入门到精通】&#x1f388; 本专栏旨在分享学习Python的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 元组…

LeetCode150道面试经典题-删除有序数组中的重复项(简单)

1.题目 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯一元素的数量为 k &#xff0c…

spring按条件注入@Condition及springboot对其的扩展

概述 spring的ioc极大的方便了日常开发&#xff0c;但随着业务的迭代。配置的一些参数在某些情况下需要按条件注入。 比如原先定义的db公共模块下&#xff0c;相关的配置和工具类只是基于mysql的。但是后续有模块需要使用mongo/es等其他数据库&#xff0c;又想继续使用db公共…

【12】Git工具 协同工作平台使用教程 Gitee使用指南 腾讯工蜂使用指南【Gitee】【腾讯工蜂】【Git】

tips&#xff1a;少量的git安装和使用教程&#xff0c;更多讲快速使用上手Gitee和工蜂平台 一、准备工作 1、下载git Git - Downloads (git-scm.com) 找到对应操作系统&#xff0c;对应版本&#xff0c;对应的位数 下载后根据需求自己安装&#xff0c;然后用git --version验…

K8S MetalLB LoadBalancer

1. 简介 kubernetes集群没有L4负载均衡&#xff0c;对外暴漏服务时&#xff0c;只能使用nodePort的方式&#xff0c;比较麻烦&#xff0c;必须要记住不同的端口号。 LoadBalancer&#xff1a;使用云提供商的负载均衡器向外部暴露服务&#xff0c;外部负载均衡器可以将流量路由…

日常BUG——使用Long类型作id,后端返回给前段后精度丢失问题

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;日常BUG、BUG、问题分析☀️每日 一言 &#xff1a;存在错误说明你在进步&#xff01; 一、问题描述 数据库long类型Id: 前端返回的Id实体类: Data ApiModel("xxx") public class …

【广州华锐视点】VR燃气轮机故障判断模拟演练系统

VR燃气轮机故障判断模拟演练系统由广州华锐视点开发&#xff0c;是一款基于虚拟现实技术的教育工具&#xff0c;旨在为学生提供一个安全、高效、互动的学习环境&#xff0c;帮助他们更好地掌握燃气轮机的故障诊断技能。 这款VR实训软件能够模拟真实的燃气轮机故障诊断场景&…

案例13 Spring MVC参数传递案例

基于Spring MVC实现HttpServletRequest、基本数据类型、Java Bean、数组、List、Map、JSON方式的参数传递。 1. 创建项目 选择Maven快速构建web项目&#xff0c;项目名称为case13-springmvc02。 2. 配置Maven依赖 <?xml version"1.0" encoding"UTF-8&quo…

前沿分享-鱼形机器人

可能并不太前沿了&#xff0c;是21年底的新闻了&#xff0c;但是看见了就顺便发一下吧。 大概就是&#xff0c;通过在pH响应型水凝胶中编码不同的膨胀速率而构建了一种环境适应型变形微机器人,让微型机器人直接向癌细胞输送药物从而减轻药物带来副作用。 技术原理是&#xff0c…

数据结构:力扣刷题

题一&#xff1a;删除有序数组中的重复项 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的…

C++:模拟实现vector以及vector的迭代器失效和拷贝问题

文章目录 实现的功能模拟实现迭代器失效隐含浅拷贝问题 实现的功能 模拟实现 由于前面实现了string&#xff0c;因此这里实现过程不为重点&#xff0c;重点为关于迭代器失效和拷贝问题 template <class T> class vector { public:typedef T* iterator;typedef const T*…

非计算机科班如何丝滑转码?

近年来&#xff0c;很多人想要从其他行业跳槽转入计算机领域。非计算机科班如何丝滑转码&#xff1f; 如何规划才能实现转码&#xff1f; 对于非计算机科班的人来说&#xff0c;想要顺利转码成为计算机相关岗位的从业者&#xff0c;需要经过以下几个步骤&#xff1a; 规划转码…

(docker)mysql镜像拉取-创建容器-容器的使用【个人笔记】

【容器的第一次创建】 容器的第一次创建&#xff0c;需要先下载镜像&#xff0c;从 镜像拉取 0、可以搜索镜像的版本 docker search mysql1、先拉取MySQL的镜像&#xff0c;默认拉取最新版&#xff0c;使用下面的命令拉取mysql镜像 docker pull mysql也可以指定mysql的版本…

MySQL 事务原理:事务概述、隔离级别、MVCC

文章目录 一、事务1.1 事务概述1.2 事务控制语句1.3 ACID特性 二、隔离级别2.1 隔离级别的分类2.1.1 读未提交&#xff08;RU&#xff09;2.1.2 读已提交&#xff08;RC&#xff09;2.1.3 可重复读&#xff08;RR&#xff09;2.1.4 串行化 2.2 命令2.3 并发读异常2.3.1 脏读2.3…

Linux 发行版 Debian 12.1 发布

导读在今年 6 月初&#xff0c;Debian 12“bookworm”发布&#xff0c;而日前 Debian 迎来了 12.1 版本&#xff0c;主要修复系统用户创建等多个安全问题。 Debian 是最古老的 GNU / Linux 发行版之一&#xff0c;也是许多其他基于 Linux 的操作系统的基础&#xff0c;包括 Ub…

Redis单机,主从,哨兵,集群四大模式

Redis 单机模式 Redis 单机模式是指 Redis 数据库在单个服务器上以独立的、单一的进程运行的模式。在这种模式下&#xff0c;Redis 不涉及数据分片或集群配置&#xff0c;所有的数据和操作都在一个实例中进行。以下是关于 Redis 单机模式的详细介绍&#xff1a; 单一实例&#…

数据分析两件套ClickHouse+Metabase(二)

Metabase篇 Metabase安装部署 任何问题请查看 -> 官方文档 jar包从GitHub下载 -> 地址 同样有个问题, 默认数据源里没有ClickHouse, 不过ClickHouse官方提供了插件包 -> 插件包 在安装metabase目录下新建一个plugins文件夹, 把下载的clickhouse.metabase-driver.ja…

spark 图计算 助力解决 dataframe中的链式依赖

链式依赖说明 name newName a b c d b c 我们需要的结果 即我们可以支持获取到链式转换的 起点 重点 以及链式的中间转换过程顺序数组. 特别说明: 出版只支持 单向 无分叉的图,其他复杂场景暂时未测试. 场景举例: 比如某件商品价格变化,我们需要知…

手机里视频太大怎么压缩?压缩教程分享

现在视频文件的体积越来越大了&#xff0c;动不动就是几个GB起步&#xff0c;如果后期再剪辑处理一下&#xff0c;更是会占据更多的设备空间了&#xff0c;还会导致我们传输受到限制&#xff0c;这时候就需要我们对视频进行压缩处理&#xff0c;下面给大家分享几个简单的方法&a…