简介:本资源为基于YOLOv8的人员轨迹跟踪算法实现包,面向计算机视觉方向的学习者、算法工程师及需要快速搭建行人跟踪demo的开发者。资源围绕YOLOv8目标检测与多目标跟踪的融合应用展开,可用于视频监控、客流统计、行为分析等场景,帮助读者理解检测与跟踪的完整链路。压缩包共8个文件,约50.08MB,包含pt模型权重、py推理脚本、requirements依赖清单、md说明文档、txt配置文件以及多段mp4演示视频,覆盖从环境配置到运行验证的主要环节。目前已有226人学习下载。通过该资源,读者可获得可直接运行的跟踪代码与预训练权重,借助演示视频直观对比输入输出效果,并参考说明文档快速完成环境搭建与参数调整,适合作为YOLOv8跟踪方向的入门实践与二次开发基础。
1. 从检测框到身份链:人员轨迹跟踪到底在解决什么
厂区安全帽检测、门店客流统计、工地越界告警——这些场景里,YOLOv8 给出的只是每一帧里散落的检测框,而业务真正要的是「谁、从哪来、到哪去、停留多久」。把逐帧检测框串成带 ID 的连续轨迹,就是人员轨迹跟踪算法要干的事。它本质上是「检测 + 关联」两段式流水线:YOLOv8 负责每帧找出人,跟踪器负责跨帧把同一个人的框连起来,再叠加业务规则输出轨迹。适合有 Python 基础、想快速把检测升级成轨迹分析的工程师,也适合拿它做毕业设计或落地原型。下面这套方案我按 CPU 也能跑通的最小闭环来写,GPU 只是加速,不是门槛。
2. 检测与跟踪的分工:为什么是 YOLOv8 加 ByteTrack
2.1 两段式流水线的职责边界
很多人一上来就想训一个端到端模型直接吐轨迹,结果发现标注成本高得离谱,而且换场景就得重标。常见做法是把问题拆开:检测器只关心「这一帧有没有人、人在哪」,跟踪器只关心「这一帧的框和上一帧的哪个框是同一个人」。这样检测器可以复用现成的 YOLOv8 预训练权重,跟踪器用成熟的关联算法,两者解耦后各自替换、各自调优。
YOLOv8 在这里的角色是「高质量候选框生产者」。它输出的是[x1, y1, x2, y2, conf, cls],跟踪器拿到这些框后,先按置信度过滤,再用运动预测和外观特征做匹配。检测质量直接决定跟踪上限——漏检一帧,轨迹就断;误检一个框,就可能凭空多出一个 ID。所以调跟踪之前,先把检测的置信度和 NMS 调稳。
跟踪器我一般选 ByteTrack,原因是它不依赖 ReID 外观模型也能跑得不错,对 CPU 友好,且对低分框的处理很聪明:高分框先匹配,剩下的低分框再和没匹配上的轨迹做二次关联,能救回一部分被遮挡的目标。相比 DeepSORT 需要额外跑一个 ReID 网络,ByteTrack 在人员密集但外观差异不大的场景里性价比更高。
2.2 环境搭建:CPU 版本也能跑通的最小命令
先解决「yolov8 环境配置」这个高频问题。Ubuntu 20.04 上 CPU 版本完全够做验证,装包不要贪多。
# 创建独立环境,避免和系统 Python 冲突 conda create -n track python=3.10 -y conda activate track # 安装 ultralytics(自带 YOLOv8 和 ByteTrack 接口) pip install ultralytics # 跟踪场景常用的两个包:opencv 读视频,lap 做匈牙利匹配 pip install opencv-python lap # 验证安装,能打印版本就说明环境通了 python -c "import ultralytics; print(ultralytics.__version__)"逻辑说明:ultralytics这个包把模型加载、推理、跟踪封装成了一套 API,model.track()内部就集成了 ByteTrack,不需要你单独 clone 跟踪仓库。lap是线性分配库,ByteTrack 做框与轨迹匹配时依赖它,缺了会在运行时报lap not found。参数上,Python 选 3.10 是因为 3.12 早期版本和部分 torch 轮子有兼容问题,3.10 最稳。
提示:如果你只有 CPU,第一次跑
model.track()会明显慢,属于正常现象,先把流程跑通再考虑上 GPU 或换 RK3588、Orin 这类边缘板。
2.3 最小可运行脚本:把视频跑成带 ID 的轨迹
下面这段是能直接抄的骨架,输入一段监控视频,输出带 ID 标注的结果视频和轨迹文本。
from ultralytics import YOLO import cv2 # 加载预训练权重,首次运行会自动下载 yolov8n.pt model = YOLO("yolov8n.pt") # 打开视频源,也可以换成 0 调用摄像头 cap = cv2.VideoCapture("input.mp4") writer = None # 用字典缓存每个 ID 的历史轨迹点 tracks = {} while cap.isOpened(): ret, frame = cap.read() if not ret: break # persist=True 是关键,它让跟踪器在帧之间保持状态 results = model.track( frame, persist=True, tracker="bytetrack.yaml", classes=[0], # 只跟踪 person 类,COCO 里 person 的 id 是 0 conf=0.3, # 检测置信度阈值,太低会引入误检 iou=0.5, # NMS 的 IoU 阈值 verbose=False, ) if writer is None: h, w = frame.shape[:2] writer = cv2.VideoWriter( "output.mp4", cv2.VideoWriter_fourcc(*"mp4v"), 25, (w, h) ) # 解析结果,把框和 ID 画出来 if results[0].boxes.id is not None: boxes = results[0].boxes.xyxy.cpu().numpy() ids = results[0].boxes.id.cpu().numpy().astype(int) for box, tid in zip(boxes, ids): x1, y1, x2, y2 = map(int, box) cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 tracks.setdefault(tid, []).append((cx, cy)) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"ID {tid}", (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) writer.write(frame) cap.release() writer.release() # 轨迹落盘,每行一个点,方便后续做停留时长、越界判断 with open("tracks.txt", "w") as f: for tid, pts in tracks.items(): for p in pts: f.write(f"{tid},{p[0]},{p[1]}\n")逻辑说明:persist=True是整段代码的命门,不加它每帧都会重新初始化跟踪器,ID 会疯狂跳变。classes=[0]把检测限制在人类,减少无关目标干扰匹配。tracks字典用 ID 做 key 累积中心点,这就是最朴素的轨迹表示,后面做停留分析、热力图都从它出发。
参数说明:conf=0.3是跟踪场景的经验值,比纯检测常用的 0.25 略高,因为低分框进入跟踪会制造假 ID;iou=0.5控制 NMS 合并程度,人群密集时可以降到 0.45 减少框被吞。yolov8n.pt是最小的 nano 模型,CPU 上帧率能接受,精度要求高再换yolov8s.pt或yolov8m.pt。
3. 让轨迹真正可用:ID 稳定性和业务指标
3.1 训练自己的数据集:从 labelme 标注到 YOLO 格式
预训练权重只认 COCO 的 80 类,如果你的场景是特定工装、特定角度,检测会飘,跟踪自然跟着崩。这时要「yolov8训练自己的数据集」。标注工具用 labelme 或 labelImg 都行,关键是转成 YOLO 需要的格式。
import os import json import cv2 # 把 labelme 的 json 转成 YOLO 的 txt:每行 class cx cy w h(归一化) def labelme_to_yolo(json_dir, out_dir, class_map): os.makedirs(out_dir, exist_ok=True) for name in os.listdir(json_dir): if not name.endswith(".json"): continue with open(os.path.join(json_dir, name)) as f: data = json.load(f) img = cv2.imread(os.path.join(json_dir, name.replace(".json", ".jpg"))) h, w = img.shape[:2] lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_map: continue (x1, y1), (x2, y2) = shape["points"] cx = (x1 + x2) / 2 / w cy = (y1 + y2) / 2 / h bw = abs(x2 - x1) / w bh = abs(y2 - y1) / h lines.append(f"{class_map[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, name.replace(".json", ".txt")), "w") as f: f.write("\n".join(lines)) labelme_to_yolo("labels_json", "labels_txt", {"person": 0})逻辑说明:YOLO 的标签是归一化后的中心点加宽高,和 labelme 的绝对坐标两点表示不同,必须转换。class_map把类别名映射成从 0 开始的整数,person 固定为 0,和跟踪时的classes=[0]对齐。
参数说明:坐标保留 6 位小数足够,多了没意义。转换后要检查有没有空 txt 文件,空标签会被训练器当成负样本,少量可以,大量说明标注有问题。
3.2 训练参数怎么设:yolov8模型训练参数含义
数据准备好后写data.yaml,指定训练集、验证集路径和类别数,然后开训。
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/track逻辑说明:model=yolov8n.pt表示在预训练权重上微调,比从零训收敛快得多。patience=20是早停,20 轮验证指标不涨就停,省时间。
参数说明:imgsz=640是速度和精度的平衡点,监控场景人比较小可以提到 960,但显存和耗时翻倍;batch=16在 8G 显存上比较稳,爆显存就降到 8;lr0=0.01是初始学习率,微调场景可以降到 0.001 更稳。训练完看runs/track/weights/best.pt,把它替换到跟踪脚本的YOLO()里即可。
3.3 从轨迹到业务指标:停留、越界、计数
有了 ID 和轨迹点,业务指标就是几何计算。停留时长用同一 ID 首末点的时间差,越界用线段相交判断,计数用「首次出现」去重。
# 判断点是否在多边形区域内,用于区域停留统计 import numpy as np def in_polygon(point, polygon): # polygon 是 [(x1,y1), (x2,y2), ...] return cv2.pointPolygonTest( np.array(polygon, dtype=np.int32), point, False ) >= 0 # 统计每个 ID 在目标区域内的停留帧数 dwell = {} for tid, pts in tracks.items(): count = sum(1 for p in pts if in_polygon(p, region)) if count > 0: dwell[tid] = count / 25.0 # 假设 25fps,换算成秒逻辑说明:pointPolygonTest返回正数表示点在多边形内,这是 OpenCV 自带的几何工具,比自己写射线法省事。停留秒数用帧数除以帧率,帧率要和视频实际帧率一致,否则数值全错。
参数说明:region是业务画的区域多边形,坐标要和视频分辨率同一坐标系。计数场景要注意 ID 切换问题,一个人被遮挡后重新出现可能拿到新 ID,导致重复计数,后面避坑章节会讲怎么缓解。
4. 避坑与排查:ID 跳变、漏检、性能这三座大山
4.1 现象:同一个人 ID 频繁跳变
原因:检测框抖动大、遮挡导致轨迹中断、persist没开或跟踪器每帧重置。解决:先确认persist=True;再把conf提到 0.4 以上过滤低质框;遮挡严重的场景换botsort.yaml,它带 ReID 外观特征,代价是慢一些。如果还跳,检查视频帧率是否被错误读取,帧率不对会让运动预测失准。
4.2 现象:人一多就漏检、轨迹断
原因:NMS 的iou太高把重叠的人框合并了,或者模型本身对小目标不敏感。解决:把iou降到 0.45,imgsz提到 960,必要时用yolov8s.pt换掉 nano。密集场景还可以开agnostic_nms=False保留类别内独立框。
4.3 现象:CPU 上跑得比蜗牛还慢
原因:模型太大、分辨率太高、每帧都在做全图推理。解决:换yolov8n.pt,imgsz降到 480 先验证流程,抽帧处理(每 2 帧检测一次,中间帧靠跟踪器预测)。真要实时,考虑 RK3588 或 Orin 这类带 NPU 的板子做部署,CPU 只适合离线分析。
4.4 现象:轨迹文本里同一 ID 出现在两个相距很远的位置
原因:ID 复用,旧轨迹没及时清理,新目标被分配了已消失的 ID。解决:在跟踪器配置里调track_buffer,控制轨迹丢失后保留多少帧;业务侧对轨迹做空间连续性校验,相邻两点距离突变超过阈值就断开。
4.5 现象:换了场景精度暴跌
原因:预训练权重的域差异,或者自己训的数据集类别不均衡。解决:用目标场景数据微调,标注时保证每个类别样本量接近;训练时开mosaic增强提升小目标泛化,但验证阶段关掉以免指标虚高。
5. 进阶:把跟踪结果做成可复现的评估闭环
做到这一步,脚本能跑、指标能出,但你怎么证明「这次调参比上次好」?靠肉眼看视频是不靠谱的,得有量化评估。人员跟踪常用的指标是 MOTA、IDF1 和 ID Switch 次数,MOT 格式的标注数据可以用motmetrics算。
import motmetrics as mm import numpy as np # acc 累积器,逐帧喂入真实框和预测框 acc = mm.MOTAccumulator(auto_id=True) # 每帧调用一次:gt_ids 真实 ID,pred_ids 预测 ID,距离矩阵用 IoU 或中心点距离 acc.update( gt_ids, # 例如 [1, 2, 3] pred_ids, # 例如 [1, 5, 3] distance_matrix # shape (len(gt_ids), len(pred_ids)) ) mh = mm.metrics.create() summary = mh.compute(acc, metrics=["mota", "idf1", "num_switches"], name="run") print(summary)逻辑说明:distance_matrix是真实框和预测框两两之间的距离,通常用1 - IoU或中心点欧氏距离,小于阈值才算匹配。num_switches就是 ID 跳变次数,这个数字比 MOTA 更能反映跟踪稳定性,调参时优先盯它。
参数说明:距离阈值一般取 0.5(IoU 口径)或按目标尺寸设中心点距离上限。评估要在同一段标注视频上跑,换视频结果不可比。
我自己的习惯是:每次改完conf、iou、跟踪器配置,都固定跑同一段 30 秒的标注片段,记录num_switches和idf1,只认数字不认感觉。血泪经验是,很多看起来「顺眼」的调参其实让 ID Switch 涨了一倍,只是肉眼没看出来。另外别迷信一步到位,先把检测调稳再动跟踪参数,两个一起改你根本不知道是谁的锅。希望帮到你。
本文还有配套的精品资源,点击获取