☰
YOLOv9+DeepSort多目标跟踪实战:从环境搭建到调优避坑
2026/9/27 23:05:28 网站建设 项目流程

简介:本资源面向计算机视觉方向的学习者与开发者,提供一套基于YOLOv9与DeepSort融合的目标跟踪算法Python实现,可用于行人、车辆等目标的实时检测与多目标轨迹跟踪场景,适合具备一定深度学习基础、希望快速上手跟踪项目的中高级读者。压缩包共8个文件,约16.85MB,包含Python主程序、Jupyter Notebook实验脚本、YAML与conda环境配置、coco.names类别文件、README说明文档及演示动图等,覆盖从环境搭建到推理运行的完整环节。目前已有595人学习下载。读者可借助其中的检测与跟踪代码、配置文件及示例数据,理解YOLOv9检测结果如何输入DeepSort完成ID关联与轨迹维护,并参考Notebook快速复现实验、调整参数或迁移到自有数据集,为二次开发与算法对比提供可运行的基础框架。

1. 拆开这份 YOLOv9+DeepSort 跟踪源码:它到底能跑出什么结果

如果你手头有一段固定机位的监控视频,想让它自动标出每个行人并分配一个稳定 ID,那这份 YOLOv9+DeepSort 的 Python 源码包就是冲这个场景来的。检测端用 YOLOv9 出框,跟踪端用 DeepSort 做 ID 关联,两者串起来就是一条完整的多目标跟踪流水线。它适合两类人:一类是刚学完 Python 基础语法、想找一个能跑通的目标跟踪算法项目练手的新手;另一类是做安防、零售客流、交通统计的从业者,需要一个能直接改、能换模型、能接自己视频的基线工程。源码包通常包含检测推理脚本、DeepSort 跟踪器实现、权重加载逻辑和一段示例视频入口,拿到手不用从零搭架构,改几个路径就能出带 ID 的标注视频。这一章先把它的能力边界说清楚,后面再拆怎么装环境、怎么调参、怎么避坑。

2. 环境搭建与依赖安装:从 python 安装到 vscode python 环境配置

2.1 为什么这套源码对 Python 版本和 CUDA 这么挑

YOLOv9 的推理依赖 PyTorch,而 PyTorch 的 GPU 版本又和 CUDA 驱动强绑定,这是整条链路里最容易翻车的一环。源码里如果用的是.pt权重,那推理时走的是 PyTorch 原生加载,不需要额外编译;但如果权重是.engine格式,就必须经过 TensorRT 转换,这时候 CUDA、cuDNN、TensorRT 三个版本必须对齐,错一个就报serialization或invalid device function。我一般建议新手先用 CPU 或单卡 GPU 把流程跑通,确认检测框和 ID 都正常,再去折腾 TensorRT 加速。Python 版本选 3.8 到 3.10 之间最稳,3.11 以上有些旧版torchvision轮子还没跟上,装的时候会卡在编译阶段。

DeepSort 部分依赖numpy、scipy、opencv-python和filterpy,其中filterpy负责卡尔曼滤波,scipy负责匈牙利算法的线性分配。这几个包版本冲突不多,但opencv-python和opencv-contrib-python不能同时装,否则cv2会指向错误的那一个,导致cv2.dnn模块缺失。常见做法是建一个干净的虚拟环境,把依赖一次性装齐,不要混用系统 Python。

2.2 从零建环境:命令与参数说明

下面这套流程是我在 Linux 和 Windows 上都跑过的,顺序不要乱。先建虚拟环境,再装 PyTorch,最后装跟踪相关依赖。

# 创建虚拟环境,Python 版本建议 3.9 python -m venv yolov9_deepsort_env # 激活环境:Linux/macOS source yolov9_deepsort_env/bin/activate # 激活环境:Windows yolov9_deepsort_env\Scripts\activate # 先装 PyTorch,以 CUDA 11.8 为例,具体版本按自己驱动改 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 装跟踪和图像处理依赖 pip install numpy scipy opencv-python filterpy matplotlib pyyaml tqdm # 如果源码里有 requirements.txt,优先用它 pip install -r requirements.txt

python -m venv建出来的环境是隔离的,不会污染系统 Python,这一点在同时维护多个项目时特别重要。--index-url指定 PyTorch 官方轮子源,比默认源快很多,也避免装到 CPU 版本。filterpy和scipy是 DeepSort 的硬依赖,缺一个都会在tracker.py导入阶段直接报ModuleNotFoundError。装完之后用python -c "import torch; print(torch.cuda.is_available())"验证 GPU 是否可用,返回True再往下走。

2.3 vscode 配置 python 环境与调试入口

在 vscode 里按Ctrl+Shift+P,输入Python: Select Interpreter,选中刚才建的yolov9_deepsort_env里的python可执行文件。这一步不做的话,终端里跑得通、调试器里却报找不到模块,是新手最常见的困惑。然后在.vscode/launch.json里加一个调试配置,把入口脚本和参数写进去,后面改参数不用每次敲命令行。

{ "version": "0.2.0", "configurations": [ { "name": "YOLOv9+DeepSort Debug", "type": "python", "request": "launch", "program": "${workspaceFolder}/track.py", "console": "integratedTerminal", "args": [ "--source", "test_video.mp4", "--weights", "yolov9-c.pt", "--conf", "0.4", "--show" ] } ] }

program指向源码里的主入口,不同包可能叫track.py、demo.py或main.py,以实际文件名为准。args里--source可以是视频路径,也可以是0表示摄像头;--conf是检测置信度阈值,后面调参章节会细说。console设为integratedTerminal是为了让--show弹出的窗口能正常显示,用内部调试控制台有时会卡住画面。

3. 检测与跟踪主流程:YOLOv9 出框、DeepSort 关联 ID 的完整链路

3.1 一帧画面在源码里到底走了哪几步

理解主流程比死记参数重要。每一帧进来,先经过 YOLOv9 做前向推理,输出一批边界框,每个框带坐标、置信度和类别。源码里通常会用一个置信度阈值先过滤掉低分框,再用 NMS 去掉重叠框。剩下的框送进 DeepSort,DeepSort 内部做两件事:一是用卡尔曼滤波根据上一帧轨迹预测当前帧每个目标可能出现的位置,二是用匈牙利算法把预测框和检测框做匹配。匹配成功的轨迹更新状态并保留原 ID,没匹配上的检测框新建轨迹,连续多帧没匹配上的轨迹则删除。

这个「预测—匹配—更新」的循环就是 DeepSort 的核心。卡尔曼滤波负责运动预测,外观特征负责在遮挡时保持 ID 不乱跳。源码里外观特征提取通常用一个小的 ReID 网络,如果包里带了ckpt.t7之类的权重文件,那就是它。没有这个权重,DeepSort 会退化成纯运动匹配,遮挡一多 ID 就频繁切换。

3.2 主循环代码结构与关键参数

下面这段是主流程的骨架,不同源码包变量名可能不同,但逻辑一致。

import cv2 import torch from models.experimental import attempt_load from deep_sort.utils.parser import get_config from deep_sort.deep_sort import DeepSort # 加载 YOLOv9 检测模型 model = attempt_load('yolov9-c.pt', map_location='cuda:0') model.eval() # 初始化 DeepSort,传入 ReID 权重和配置文件 cfg = get_config() cfg.merge_from_file('deep_sort/configs/deep_sort.yaml') deepsort = DeepSort( cfg.DEEPSORT.REID_CKPT, max_dist=cfg.DEEPSORT.MAX_DIST, # 外观匹配最大距离 min_confidence=cfg.DEEPSORT.MIN_CONFIDENCE, nms_max_overlap=cfg.DEEPSORT.NMS_MAX_OVERLAP, max_iou_distance=cfg.DEEPSORT.MAX_IOU_DISTANCE, max_age=cfg.DEEPSORT.MAX_AGE, # 轨迹丢失后保留帧数 n_init=cfg.DEEPSORT.N_INIT, # 连续命中多少次才确认轨迹 nn_budget=cfg.DEEPSORT.NN_BUDGET, use_cuda=True ) cap = cv2.VideoCapture('test_video.mp4') while True: ret, frame = cap.read() if not ret: break # YOLOv9 推理,输出框和置信度 pred = model(frame)[0] # 过滤低置信度并做 NMS,得到 bbox_xywh、confidences、cls bbox_xywh, confidences, cls = post_process(pred, conf_thres=0.4, iou_thres=0.5) # 送入 DeepSort 更新,返回带 track_id 的框 outputs = deepsort.update(bbox_xywh, confidences, cls, frame) for x1, y1, x2, y2, track_id in outputs: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f'ID {track_id}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('track', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

max_dist控制外观特征匹配的宽松程度,调大更容易匹配上但可能串 ID,调小则容易断轨。max_age是轨迹丢失后还能保留多少帧,行人被遮挡两三秒时这个值要适当调大,否则遮挡结束会分配新 ID。n_init是轨迹确认门槛,设为 3 表示连续三帧匹配上才输出 ID,能过滤掉一闪而过的误检。nn_budget限制每个轨迹保存的外观特征数量,太大占显存,太小外观描述不稳定。这几个参数在deep_sort.yaml里都有默认值,先按默认跑,出问题再针对性调。

3.3 换自己的视频和模型:路径与类别过滤

把test_video.mp4换成自己的文件,注意 OpenCV 对中文路径支持不好,路径里不要带中文和空格。如果只想跟踪人,可以在后处理里按类别过滤,COCO 预训练模型里人的类别 ID 是 0。

# 只保留行人,过滤其他类别 person_mask = cls == 0 bbox_xywh = bbox_xywh[person_mask] confidences = confidences[person_mask] cls = cls[person_mask]

换模型时,YOLOv9 有yolov9-c.pt、yolov9-e.pt等不同规模,字母越靠后精度越高、速度越慢。源码里attempt_load的路径改成新权重文件名即可,不用改其他代码。如果换成自己训练的权重,类别数和类别名要对上,否则画出来的标签会错位。

4. 避坑与排查:ID 跳变、显存溢出、依赖冲突的现场记录

4.1 现象:同一个人 ID 频繁切换,遮挡后变成新 ID

原因通常有三个。一是 ReID 权重没加载成功,DeepSort 退化成纯 IoU 匹配,遮挡一出现就断。二是max_age设得太小,轨迹还没等到目标重新出现就被删了。三是检测框抖动太大,卡尔曼滤波预测位置和实际检测偏差过大,匹配失败。解决办法:先确认ckpt.t7路径正确且文件完整,再把max_age从默认 30 调到 50 到 70 之间试,最后检查检测置信度阈值是不是太低导致框在目标边缘跳来跳去,适当提高到 0.5 左右。

4.2 现象:跑几帧后报 CUDA out of memory

原因是显存被检测模型、ReID 模型和视频帧缓存同时占用。YOLOv9 的e版本比c版本吃显存多不少,如果显卡只有 6G 或 8G,换小模型是最直接的办法。另外nn_budget设太大也会让每个轨迹缓存过多特征向量,把它从默认 100 降到 50 能省不少显存。还有一个容易忽略的点:torch.no_grad()没加,推理时还在建计算图,显存会持续增长。在主循环外面套上with torch.no_grad():就能解决。

4.3 现象:ImportError 或版本不兼容,卡在 filterpy 或 scipy

原因是虚拟环境没激活,或者 pip 装到了系统 Python 里。先which python确认当前解释器路径在虚拟环境目录下,再pip list看filterpy和scipy是否都在。如果scipy版本过高导致linear_sum_assignment接口变化,降到 1.7 到 1.9 之间通常能解决。opencv-python和opencv-contrib-python同时存在时,先pip uninstall掉其中一个,只留opencv-python。

4.4 现象:视频输出正常但窗口不显示,或按 q 没反应

原因是cv2.imshow和cv2.waitKey的配合问题。waitKey(1)里的数字是毫秒,太小会导致窗口无响应,改成waitKey(30)给 GUI 留出刷新时间。另外在无显示器的服务器上跑,imshow会直接报错,这时候把--show去掉,改成用cv2.VideoWriter写文件输出。

# 无显示器环境下改为写视频文件 fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter('output.mp4', fourcc, 30, (w, h)) out.write(frame)

4.5 现象:跟踪结果整体偏移,框和人对不上

原因是 YOLOv9 输出的坐标格式和 DeepSort 期望的格式不一致。YOLO 系列常见输出是xywh中心点加宽高,而 DeepSort 的update有的版本期望xywh,有的期望xyxy。看源码里update函数签名和内部第一行怎么处理,如果是xyxy就手动转换。这个坑不报错,只是结果错位,排查时优先核对坐标格式。

5. 进阶调优与验证:把跟踪效果从「能跑」推到「能用」

5.1 用 MOT 指标量化跟踪质量,而不是靠肉眼看

肉眼看视频只能判断「大概没跳」,要真正验证得用 MOT 指标。常见做法是把跟踪结果按 MOTChallenge 格式写成 txt,每行是帧号,ID,x,y,w,h,置信度,-1,-1,-1,然后用py-motmetrics算 MOTA、IDF1、ID Switch 三个核心指标。MOTA 反映整体准确度,IDF1 反映 ID 保持能力,ID Switch 直接数 ID 跳变次数。这三个数比看视频靠谱得多,调参前后各跑一次,才知道改动是正收益还是负收益。

import motmetrics as mm acc = mm.MOTAccumulator(auto_id=True) # gt_ids 和 pred_ids 是当前帧的真实 ID 和预测 ID # gt_boxes 和 pred_boxes 是对应框,格式为 (x, y, w, h) dist = mm.distances.iou_matrix(gt_boxes, pred_boxes, max_iou=0.5) acc.update(gt_ids, pred_ids, dist) mh = mm.metrics.create() print(mh.compute(acc, metrics=['mota', 'idf1', 'num_switches'], name='eval'))

max_iou=0.5是匹配阈值,低于这个 IoU 视为不匹配。num_switches就是 ID Switch 次数,调max_age和max_dist时盯着这个数变化,比盲调高效。

5.2 检测端和跟踪端分开调,别一起动

血泪经验:同时改 YOLO 置信度和 DeepSort 参数,出了问题根本不知道是哪边的锅。我一般固定检测端,先把conf定在 0.4 到 0.5 之间,确认检测框稳定不抖,再去调跟踪端。跟踪端优先调max_age和max_dist,这两个对 ID Switch 影响最大。n_init影响的是轨迹确认速度,调大能减少误检但会让新目标出现时 ID 延迟几帧才显示,按场景取舍。

参数作用调大后果调小后果
conf检测置信度阈值漏检增多误检增多
max_age轨迹丢失保留帧数遮挡后 ID 更稳,但可能保留错误轨迹遮挡后易分配新 ID
max_dist外观匹配最大距离匹配更宽松,可能串 ID匹配更严格,易断轨
n_init轨迹确认帧数新目标 ID 出现延迟误检更容易被确认

5.3 一个具体技巧:用检测框面积过滤远景小目标

固定机位视频里,远处行人框只有十几个像素,YOLOv9 检出来置信度低,DeepSort 匹配时外观特征也几乎不可用,这些框是 ID 跳变的主要来源。在送入 DeepSort 之前加一道面积过滤,把宽高小于某个阈值的框直接丢掉,能明显降低 ID Switch。

# 过滤掉宽或高小于 20 像素的框 areas = bbox_xywh[:, 2] * bbox_xywh[:, 3] keep = areas > 400 # 20x20 bbox_xywh = bbox_xywh[keep] confidences = confidences[keep] cls = cls[keep]

400这个阈值按自己视频分辨率调,1080p 下 20x20 是个保守起点。这个操作会牺牲远景目标的召回,但换来的是近景目标 ID 稳定,做客流统计时通常划算。

从那以后我每次拿到新的跟踪源码包,都强制先跑一遍默认参数、记录 MOTA 和 ID Switch 基线,再动任何一个参数。没有基线,调参就是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询