简介:这份资源围绕YOLOv8目标检测模型构建了一套完整的人员轨迹跟踪算法实现,面向计算机视觉入门与进阶开发者、需要做行人追踪项目的学生及工程人员。它解决的是从检测到多目标跟踪的落地问题,适合安防监控、客流统计、视频分析等场景。压缩包共8个文件,约50.08MB,包含4段mp4演示视频、1个pt权重文件、1个py脚本、1个txt依赖清单、1个md说明文档,覆盖模型权重、运行脚本与效果验证素材。已有226人学习下载,说明该方案具备一定参考价值。读者可获得可直接运行的YOLOv8人员跟踪代码、预训练权重、依赖配置说明以及多段输入输出对比视频,便于快速复现检测与轨迹绘制效果,并在此基础上调整参数、替换视频源或迁移到自有数据。整体结构紧凑,适合作为课程设计、毕业设计或算法验证的起步模板。
1. 从检测框到运动轨迹:这套 YOLOv8 跟踪方案到底能跑出什么
很多人第一次做人员轨迹跟踪,都是拿 YOLOv8 跑通检测后直接接一个现成的跟踪器,结果发现画面里人一多,ID 就乱跳,轨迹线像毛线团一样缠在一起。这套基于 YOLOv8 的人员轨迹跟踪算法,核心思路是把检测、跟踪、轨迹管理拆成三层:YOLOv8 负责逐帧出人框,ByteTrack 或 BoT-SORT 负责把框和上一帧的轨迹做关联,再用一个轻量的轨迹缓存模块把每个 ID 的历史坐标串成折线。它解决的不是“能不能检测到人”,而是“同一个人在第 3 帧和第 300 帧能不能被认成同一个 ID”。适合做安防巡检、客流统计、工地安全帽轨迹回放这类需要连续身份保持的场景,也适合拿来做毕业设计里“检测+跟踪”的完整闭环。如果你只跑过 YOLOv8 的 detect 脚本,没碰过跟踪器的关联逻辑,这套东西正好补上中间缺的那一环。
2. 环境搭建与权重准备:从零把 YOLOv8 跟踪链路跑起来
2.1 选 Ultralytics 还是自己搭推理后端
常见做法是直接用 Ultralytics 官方包,因为它把 YOLOv8 的推理、训练、导出都封装好了,跟踪器也内置了 ByteTrack 和 BoT-SORT 的接口。如果你后面要部署到 RK3588 或 Orin 这类板端,前期在 PC 上先用 Ultralytics 把跟踪逻辑调通,再导出 ONNX 或 RKNN,比一上来就啃底层推理省事得多。我一般会在 Ubuntu 20.04 上建一个干净的 conda 环境,Python 锁 3.9 或 3.10,PyTorch 按显卡选版本。GTX1660Ti 跑 YOLOv8n 跟踪,640 输入尺寸下大概能到 40 到 60 FPS,够实时看效果;如果换成 YOLOv8m,帧率会掉到 20 左右,轨迹平滑度反而因为丢帧变差,所以人员跟踪场景我更倾向用 n 或 s 模型。
2.2 安装命令与权重下载
# 创建环境,Python 版本不要超过 3.10,否则某些依赖轮子对不上 conda create -n yolov8_track python=3.10 -y conda activate yolov8_track # 安装 PyTorch,GTX1660Ti 属于图灵架构,CUDA 11.8 版本比较稳 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 Ultralytics,它会自动拉取 opencv、numpy 等依赖 pip install ultralytics # 验证安装,跑一行检测看看环境通不通 yolo predict model=yolov8n.pt source='https://ultralytics.com/images/bus.jpg'这段命令里,conda create的python=3.10是关键,3.11 以上有些 opencv 轮子还没跟上。PyTorch 的 index-url 指向 cu118 源,GTX1660Ti 用这个版本不会报 CUDA 版本不匹配。最后一行yolo predict会自动下载yolov8n.pt权重到当前目录,如果卡在下载,可以手动去 Ultralytics 的 GitHub release 页面找yolov8n.pt放进项目根目录。权重文件不大,n 版本大概 6MB 左右,s 版本 22MB,m 版本 50MB 上下。
2.3 跟踪推理的最小可运行脚本
from ultralytics import YOLO import cv2 # 加载检测模型,这里用 n 版本,速度快,适合实时跟踪 model = YOLO("yolov8n.pt") # 打开视频文件,也可以换成 0 调用摄像头 cap = cv2.VideoCapture("test_people.mp4") # 跟踪器配置:ByteTrack 对遮挡恢复更稳,BoT-SORT 对相机运动更友好 tracker_config = "bytetrack.yaml" while cap.isOpened(): ret, frame = cap.read() if not ret: break # persist=True 是跟踪的关键,它让模型在帧间保持轨迹状态 results = model.track( frame, persist=True, tracker=tracker_config, classes=[0], # 只跟踪 person 类,COCO 里 person 的 id 是 0 conf=0.4, # 检测置信度阈值,太低会引入误检轨迹 iou=0.5, # NMS 的 IoU 阈值,人多时适当调低 verbose=False ) # 把带轨迹 ID 的画面画出来 annotated = results[0].plot() cv2.imshow("YOLOv8 Tracking", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()model.track和model.predict的区别就在persist=True,它让跟踪器在连续调用之间保留轨迹状态。classes=[0]把检测范围锁死在 person 类,避免其他类别干扰 ID 分配。conf=0.4是我在人员场景常用的起点,低于 0.3 时画面边缘的误检会生成短命轨迹,高于 0.6 又会漏掉远处小目标。tracker参数可以换成botsort.yaml,两者在遮挡和相机抖动下的表现差异后面会细说。
3. 跟踪器选型与参数调优:ByteTrack 和 BoT-SORT 到底怎么选
3.1 两种跟踪器的关联逻辑差异
ByteTrack 的核心是“两次关联”:先用高分检测框和现有轨迹匹配,再把低分检测框捡回来和没匹配上的轨迹做第二次关联。这个设计对人员被部分遮挡后重新出现的场景特别有效,因为遮挡时检测分数会掉,但低分框还能被利用。BoT-SORT 在 ByteTrack 基础上加了相机运动补偿和更精细的外观特征,适合相机本身在动的情况,比如云台摄像头或者车载场景。代价是 BoT-SORT 计算量更大,GTX1660Ti 上跑 YOLOv8n + BoT-SORT 会比 ByteTrack 低 5 到 8 FPS。如果你相机固定、人员走动为主,ByteTrack 足够;如果相机有轻微晃动或者要跟拍,BoT-SORT 的轨迹连续性明显更好。
3.2 关键参数含义与调整方向
| 参数 | 作用 | 人员场景建议值 | 调大后果 | 调小后果 |
|---|---|---|---|---|
| track_high_thresh | 第一次关联的检测分数门槛 | 0.5 | 漏跟远处人员 | 引入误检轨迹 |
| track_low_thresh | 第二次关联的低分门槛 | 0.1 | 低分框参与过多,ID 跳变 | 遮挡恢复变差 |
| new_track_thresh | 新建轨迹的分数门槛 | 0.6 | 新人员进入画面延迟建档 | 短命轨迹增多 |
| track_buffer | 轨迹丢失后保留帧数 | 30 | 遮挡后 ID 切换减少,但内存占用上升 | 遮挡后容易分配新 ID |
| match_thresh | 匹配时的 IoU 或特征距离阈值 | 0.8 | 匹配过松,ID 混淆 | 匹配过严,轨迹断裂 |
这些参数在bytetrack.yaml或botsort.yaml里改,Ultralytics 会从包目录读取,你也可以复制一份到项目里改,然后在model.track里传自定义路径。我一般先把track_buffer从默认 30 调到 30 到 50 之间试,人员被柱子挡两三秒再出来,ID 能不能接上就看这个值。new_track_thresh别低于 0.5,否则画面里飘过的误检会不断新建轨迹,画面上全是短线段。
3.3 轨迹绘制与 ID 颜色管理
import numpy as np # 用 ID 做哈希映射颜色,保证同一个 ID 在整段视频里颜色不变 def get_color(track_id): np.random.seed(track_id) return tuple(int(c) for c in np.random.randint(0, 255, 3)) # 在 results[0].boxes 里,id 属性就是跟踪 ID boxes = results[0].boxes if boxes.id is not None: for box, track_id in zip(boxes.xyxy, boxes.id): x1, y1, x2, y2 = map(int, box) color = get_color(int(track_id)) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f"ID {int(track_id)}", (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)这段代码解决的是“同一个 ID 颜色跳来跳去”的问题。np.random.seed(track_id)让相同 ID 每次生成的随机颜色一致,不会因为帧不同而变色。boxes.id在persist=True时才有值,如果为 None 说明当前帧没有活跃轨迹。画框时用xyxy坐标,注意 OpenCV 的坐标是整数,map(int, box)做转换。轨迹线可以再维护一个dict,把每个 ID 的历史中心点存下来,超过一定长度就丢弃最老的点,这样画面不会越画越乱。
4. 避坑与排查:人员轨迹跟踪里最容易翻车的五个地方
4.1 现象:ID 频繁跳变,同一个人几帧换一个号
原因通常是检测框抖动导致 IoU 匹配失败,或者track_buffer太小,人一转身轨迹就断了。解决方法是先把conf提到 0.5 以上,减少低质量框;再把track_buffer加到 50,给遮挡恢复留时间;如果相机有运动,换 BoT-SORT 并开启gmc_method做全局运动补偿。还有一个隐蔽原因是视频帧率不稳,model.track按帧处理,如果输入视频本身有丢帧,轨迹预测会偏。
4.2 现象:画面里人一多,轨迹线交叉后 ID 互换
这是匹配阈值match_thresh太松的典型表现。ByteTrack 默认 0.8,人员密集时两个框的 IoU 可能都超过阈值,跟踪器就分不清谁是谁。把match_thresh降到 0.7 到 0.75 之间,让匹配更严格。如果还不行,就得引入外观特征,BoT-SORT 的with_reid选项可以开,但会额外加载一个 ReID 模型,GTX1660Ti 上帧率会再掉一截,需要权衡。
4.3 现象:跟踪脚本跑几分钟后内存暴涨
常见原因是轨迹缓存没有清理。persist=True会让跟踪器一直保留历史轨迹,如果视频里不断有人进出,轨迹字典会越来越大。解决方法是定期调用model.predictor.trackers[0].reset()或者手动限制轨迹数量,超过一定长度就丢弃最老的。另一个原因是results[0].plot()每帧都生成新图像,如果不用imshow及时释放,OpenCV 的窗口缓冲也会堆积。
4.4 现象:RK3588 或 Orin 上部署后跟踪 ID 全乱
板端部署时,很多人只导出了检测模型,跟踪逻辑还在 Python 里跑,结果帧率对不上,跟踪器的时间假设被破坏。正确做法是把跟踪器也移植到板端,或者至少保证检测和跟踪在同一进程里按固定帧率处理。RK3588 上跑 YOLOv8n 的 RKNN 模型,检测能到 30 FPS 以上,但 ByteTrack 的关联计算在 CPU 上做,如果 CPU 占用太高,帧间间隔不稳,ID 就会跳。建议把track_buffer适当调大,抵消帧率波动。
4.5 现象:训练自己的数据集后,跟踪效果反而比预训练模型差
这通常不是跟踪器的问题,而是检测模型过拟合了。自己标注的数据集如果场景单一,模型在新视频里漏检率上升,跟踪器拿不到稳定检测框,轨迹自然断。解决方法是先用预训练权重跑一遍跟踪,确认跟踪链路没问题,再换自己训练的模型对比。如果确实要微调,冻结 backbone 只训 head,学习率设小一点,避免把通用特征训丢。
5. 轨迹后处理与效果验证:把零散 ID 串成可回放的路径
5.1 轨迹平滑与中心点提取
检测框的中心点直接连起来,画面上会有明显的锯齿,因为框本身在抖。我一般用滑动平均或者 Savitzky-Golay 滤波对中心点序列做平滑。下面这段代码把每个 ID 的中心点存进字典,再用简单移动平均输出平滑轨迹。
from collections import defaultdict, deque # 每个 ID 维护一个固定长度的中心点队列 track_history = defaultdict(lambda: deque(maxlen=30)) def update_tracks(results): boxes = results[0].boxes if boxes.id is None: return for box, track_id in zip(boxes.xyxy, boxes.id): x1, y1, x2, y2 = map(float, box) cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 track_history[int(track_id)].append((cx, cy)) def draw_smooth_track(frame, track_id, window=5): points = list(track_history[track_id]) if len(points) < window: return # 简单移动平均,window 越大越平滑但延迟越高 smooth = [] for i in range(len(points) - window + 1): xs = [p[0] for p in points[i:i+window]] ys = [p[1] for p in points[i:i+window]] smooth.append((int(sum(xs)/window), int(sum(ys)/window))) for i in range(1, len(smooth)): cv2.line(frame, smooth[i-1], smooth[i], (0, 255, 0), 2)deque(maxlen=30)限制每个 ID 最多存 30 个点,避免内存无限增长。window=5是平滑窗口,太小平滑效果不明显,太大轨迹会滞后于实际位置。人员正常行走速度下,5 到 7 帧的窗口比较合适。这段逻辑放在model.track之后调用,先更新历史再画线。
5.2 验证跟踪稳定性的两个指标
光看画面不够客观,我习惯算两个数:ID Switch 次数和 MOTA。ID Switch 就是同一个人被分配了新 ID 的次数,手动数几段视频就能有个大概。MOTA 需要标注数据,公式是 1 减去(漏检+误检+ID Switch)除以真实框总数。没有标注数据时,可以看轨迹长度的分布:如果大量轨迹只有几帧就消失,说明跟踪器不稳定;如果轨迹长度集中在几十帧以上,说明 ID 保持得不错。另一个土办法是把视频调速到 0.5 倍慢放,肉眼盯几个关键遮挡段,看 ID 有没有断。
5.3 导出跟踪结果做后续分析
跟踪跑通后,通常要把每个 ID 的轨迹导出成 CSV 或 JSON,方便做热力图、停留时间统计。下面这段把track_history写进文件。
import csv with open("tracks.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["track_id", "frame_index", "cx", "cy"]) for track_id, points in track_history.items(): for idx, (cx, cy) in enumerate(points): writer.writerow([track_id, idx, round(cx, 2), round(cy, 2)])注意这里frame_index用的是队列内的相对索引,如果要绝对帧号,需要在更新时额外存一个帧计数器。导出后可以用 pandas 做分组统计,算每个 ID 的轨迹长度、平均速度、停留区域。这套数据拿去做客流热力图或者工地轨迹回放,比单纯看视频直观得多。
从那以后我每次调跟踪参数,都强制先跑一段 30 秒的遮挡测试视频,把 ID Switch 数出来再改track_buffer和match_thresh,不再凭感觉调。希望帮到你。
本文还有配套的精品资源,点击获取