
简介面向计算机视觉方向毕业设计的一套完整目标跟踪源码将YOLOv9检测与DeepSORT多目标跟踪结合适合需要完成实时检测跟踪课题或学习多目标关联算法的学生与开发者。压缩包共8个文件约16.85MB含Python执行脚本、Jupyter分步示例、类别映射表、依赖清单、环境配置与说明文档及2个演示gif等便于快速搭建环境并查看运行效果。已有549人学习。源码贯穿数据预处理、YOLOv9模型加载、卡尔曼滤波初始化、特征提取与相似度度量、逐帧跟踪关联及可视化后处理等关键环节配合Jupyter逐段演示可帮助理解检测头输出如何进入跟踪器、如何利用重识别特征抑制ID Switch等问题既能直接支撑毕业设计实验也能作为深入掌握检测与跟踪工程化实现的练习项目。该组合方案在实时性与精度之间取得较好平衡可扩展到行人、车辆等常见目标的持续跟踪场景。1. YOLOv9DeepSort目标跟踪源码先解释为什么检测和跟踪要分开看做过视频目标跟踪的人多半都撞过这个画面检测框画得挺准但同一个目标在连续帧里一会儿是ID 3过两帧又变成ID 17刚稳下来又消失。这不是检测模型不行而是目标跟踪这一段没接通。这套毕业设计级的YOLOv9DeepSort Python源码就是用来解决这个断点的。它用YOLOv9做检测前端逐帧出框DeepSort做后端把框关联成连续轨迹包里object_tracking.py能直接跑视频YOLOv9_DeepSORT.ipynb适合分段调试configs和coco.names都备齐conda环境一键建。适合毕业设计、视觉课设以及想快速验证检测加跟踪效果的工程师。2. 检测与跟踪的分工逻辑YOLOv9出框DeepSort关联成轨迹2.1 YOLOv9为什么适合当检测前端先分清两件事检测回答这一帧里有什么跟踪回答上一帧那个目标现在在哪儿。这套源码选择YOLOv9不是因为它名字新而是因为它在速度和漏检率之间取的平衡点比较稳。YOLOv9没在前向推理路径上堆太多冗余模块而是把重心放在backbone的梯度流动上可编程梯度信息PGI在反向传播时用辅助分支保住浅层特征GELAN块又把跨层信息和高效计算拼到同一条链路。实际跑起来的感觉是人群、车辆这类COCO通用场景小目标不轻易丢低置信度的真实物体也有机会被下一帧捞回来这对跟踪是友好的。因为跟踪逻辑再顺检测漏一帧就得多等一个生命周期。工程上我更关心它的输出形态YOLOv9的bbox带着坐标和置信度直接能转给跟踪器。这个源码里检测结果被整理成统一列表跟踪端不关心你是否换了检测器。如果一个算法封装得干净检测和跟踪分别是两个函数错误边界清楚谁出问题一查就定位这是毕业设计最想要的工程结构。很多人一上来就钻进网络结构其实对于复现这套源码先把出框和关联拆开理解比背一堆结构名有用得多。2.2 DeepSort的关联链路预测、门控、匹配三步走DeepSort的核心链路可以拆成三步。第一步是卡尔曼滤波预测每个track维护一组状态包括中心位置、长宽比、高度和对应的速度新帧到来前先按线性运动模型推算出这个目标大概会在画面的哪个位置。注意这一步预测的不是随机猜测而是基于历史状态做时间更新所以当目标被遮挡、YOLO暂时没有输出时track还能靠预测值和后续观测做关联。第二步是门控新一帧的检测框要和每个预测做比较运动马氏距离离得太远的先淘汰外观上用ReID网络抽出一个embedding跟这个track的历史特征队列做余弦距离比较。第三步是匹配DeepSort把马氏距离和余弦距离加权成综合代价矩阵交给匈牙利算法一次性分配分配不完的检测先暂存连续多帧没有被匹配的track判定为丢失。为什么不能只做IOU匹配因为目标一旦出画再入画位置跳变很大IOU直接归零但ReID的外观embedding还能保持相似。DeepSort训练ReID时用了度量学习约束同一目标不同帧的embedding拉近不同目标互相推远这是它比传统颜色直方图方法强的根本原因。这套源码中ReID特征提取在helpers层预训练权重一并提供通用场景直接沿用没问题自定义数据集效果不好时需要把这个特征网络换掉我在第4.3节讲更换方法。还有个容易误解的地方DeepSort并不是每帧都把检测结果和所有track全量比较一遍。实际实现是先按已确认track和未确认track分两级级联匹配里先处理外观稳定、刚更新过的轨迹再去补匹配不上的。这样做计算量小也能避免新track频繁抢旧track的ID。帧循环里速度慢的瓶颈往往不在算法本身而在ReID推理和画框。2.3 源码包里的文件别只当档案看这套资源里不是只有一段训练脚本而是完整检测跟踪管道。拿到手建议先按职责拆开别只盯着一个主脚本。压缩包内文件可以对应到几个层次data是输入素材位configs和coco.names管配置object_tracking.py和helpers是核心conda.yml和requirements.txt是环境。我习惯先画一张文件职责表排查时好定位。文件/目录在管道中的职责排查时看什么object_tracking.py主流程读帧、检测、跟踪、画框输出报错在上半段还是下半段YOLOv9_DeepSORT.ipynb分段演示检测和跟踪过程单段调试确认哪一层出问题helpers/DeepSort封装、画框和工具函数卡跟踪逻辑就翻这里configs/模型路径、尺寸、跟踪参数调参基本都落在这层coco.namesCOCO 80类类别名称类别和权重不匹配时先查它conda.yml / requirements.txt依赖清单双轨环境问题先对齐这两份文件名摆在这儿不是让你背而是为了报错时不慌。比如哪天运行object_tracking.py时提示找不到某个配置优先看configs里路径变量八成是相对路径问题而不是代码崩了。把跟踪器当黑匣子扔着不管、只看最终画框是这套源码最容易被浪费掉的部分。愿意打开看一眼后面调参就是有依据的。2.4 单帧里的完整流动从frame到track把上面概念落到一个帧循环里顺序是这样的OpenCV读取一帧BGR图像图像被resize和归一化后进YOLOv9YOLO输出原始预测经过conf阈值和NMS过滤留下若干bbox每框从[x1,y1,x2,y2]转成中心坐标和宽高的xywh格式连同score和class拼成数组数组和原图一起交给tracker.updatetracker内部先卡尔曼预测再和检测做匹配返回已确认track的ID、坐标和置信度最后把track结果画到图上并写进输出视频。这个过程在源码里就是object_tracking.py主循环并不需要你去动网络结构。新手最容易懵的是为什么最终显示的不是YOLO画的框而是tracker画的框。原因很简单显示层要的是ID加框的整体结果所以源码里画图函数取的是DeepSort的track输出而不是检测器的原始输出如果你自己改代码时画了两遍把ID叠加在检测框上视觉上就会混乱。打开helpers里的draw逻辑很快能看懂这一层。3. 跑通环境的三件事conda双轨依赖、权重放位、两种运行入口3.1 conda.yml和requirements.txt到底该按哪个来这类视觉工程最常见的环境坑是两份依赖清单各有各的版本主张。conda.yml通常把python版本和基础包固定住requirements.txt则补齐了cv2、torch、munkres之类的依赖。常见做法是以conda.yml为准建虚拟环境再用requirements.txt补装。我建议的执行顺序是这样的conda env create -f conda.yml conda activate yolov9_deepsort # 实际环境名以conda.yml里的name字段为准 pip install -r requirements.txt python -c import torch, cv2; print(torch.__version__, cv2.__version__, torch.cuda.is_available())第一行创建环境第二行激活第三行补pip依赖第四行是验证。最后一行最关键如果torch.cuda.is_available()返回False说明GPU检测没生效。这时候别急着往下跑视频否则模型推理会在CPU上慢慢爬你会以为是源码性能差其实只是环境问题。常见做法是停在这里按机器CUDA版本重装对应torch再跑一次同样验证。3.2 权重文件和coco.names对齐才是模型能加载的前提源码包本身是工程实现但YOLOv9预训练权重一般是单独放的。拿到手后先建一个weights目录把权重统一放进去命名与configs里的变量保持一致。这一步遗漏最常见的报错就是加载模型时直接提示文件不存在或者给你一个看起来莫名其妙的open blob错误。# 当前目录是工程根目录先建权重目录 mkdir -p weights # 权重准备好之后放到配置里写好的路径 cp ~/Downloads/yolov9-c.pt weights/ ls -lh weights/这段操作不是玄学而是为了让相对路径固定下来。我习惯把coco.names和weights都放在工程根目录下不搞散落式路径。排查别人代码时卡在路径上的时间往往比卡在参数上的还多。coco.names要跟预训练权重配套如果你自己训练的模型只有几个类别却仍然用80类的coco.names后面画框和解析类别时就会对不上号。3.3 跑通第一段视频先走最短命令别一上来就调参第一次跑不要开一堆复杂参数就选一段5到10秒的短视频把置信度和输出路径指定好其余用默认。这是为了确认管道是通的不是为了让结果好看。命令行参数名在不同版本的object_tracking.py里可能略有差异运行前先看一眼入口函数的parse_args或者执行python object_tracking.py --help确认这是最稳的做法。我一般这么跑python object_tracking.py \ --source data/demo.mp4 \ --weights weights/yolov9-c.pt \ --conf 0.5 \ --output outputs/demo_result.mp4参数不复杂--source是输入视频--weights是检测模型路径--conf是检测置信度阈值--output是结果保存位置。第一次跑就盯着终端里的FPS和track数量看FPS过低先考虑降分辨率或换tiny权重track数量一直是0说明目标还没被确认成轨迹要去看置信度和n_init这两个参数。如果程序正常走完打开结果视频确认里面每辆车的ID在连续帧之间没有频繁跳变这个工程就算立住了。3.4 notebook入口适合拆开看但别依赖它做性能测试YOLOv9_DeepSORT.ipynb的作用是把整条管道拆成逐格步骤加载权重、跑单帧检测、初始化tracker、循环视频。如果直接跑object_tracking.py抛错我建议先在notebook里逐格执行定位是加载段还是更新段的问题。notebook的坑在于循环帧时显示逻辑和保存逻辑会拖慢速度用它测出来的FPS不代表真实性能。所以我的习惯是排查用notebook性能验证用object_tracking.py两条路径互不替代。4. 调参实操置信度、NMS和DeepSort的max_age这样配4.1 YOLOv9侧的两个截门conf和nms_iou检测阶段有两个参数直接影响跟踪质量一个是conf一个是NMS的iou阈值。conf设得高框更干净但容易漏检设得低遮挡目标能检出来但会产生一堆碎框干扰跟踪。这个取舍没有万能数得按场景给。我自己的参数区间是这样的参数常见区间场景判断conf0.25 ~ 0.5人群、密集商城场景宁多勿漏conf0.5 ~ 0.7车辆稀疏场景要求框少而准nms_iou0.45 ~ 0.7重叠目标多时调低避免框互相吞并这张表只当起点。血泪经验是如果跟踪结果里目标走走停停断断续续先回头查conf而不是一上来就动DeepSort参数。因为很多track之所以被判定消失根本不是跟踪器的错而是检测阶段压根没在这个目标上输出框。检测端的漏检跟踪器背不了这个锅。4.2 DeepSort侧参数max_age、n_init和max_distDeepSort参数通常聚合在跟踪器初始化的配置里字段名各有差异但逻辑上逃不开几个关键的。以下面这段常见配置为例我一般先不动这些值跑完一轮再根据效果改# 开源工程里很常见的DeepSort初始化参数字段名以你手上的版本为准 tracker_args { model_path: weights/deepsort.onnx, # ReID特征模型 max_dist: 0.3, # 外观特征余弦距离上限越大越容易关联 min_confidence: 0.3, # 送入跟踪器的检测最低置信度 max_iou_distance: 0.7, # 外观匹配不上时用的IOU兜底阈值 max_age: 70, # 轨迹连续丢多少帧后删除 n_init: 3, # 连续匹配多少帧后确认新轨迹 nn_budget: 100, # 轨迹特征样本总数上限 }逐个说透max_age是轨迹存活期目标消失后track会再等max_age帧这段时间内如果目标重新出现还能续上旧ID调大它ID切换会减少但代价是误关联的可能性也会变高。n_init是新目标转正需要的连续命中帧数调小能让新轨迹更快出现但震荡框更容易被当成目标。max_dist控制外观匹配的宽容度调大特征不太像的两个框也可能配上对调小ReID严格ID容易断多目标交错时不容易交换身份。max_iou_distance是兜底方案外观匹配不上时tracker会退化用位置IOU续命。实战中我最常做的调整是这三步ID频繁断就把max_age加到100以上max_dist调到0.35先看ID切换是否下降误匹配多把max_dist收回0.25同时调大n_init到5让噪声框不被立即确认目标出画再入画总是换ID就要加更强的ReID模型。每次只改一个参数保留同段视频做验证不然你根本不知道是哪个参数起了作用。4.3 把YOLOv9换成自己的检测器接缝只看这里有些用户想用自己的数据集替换检测权重后发现跟踪还是按原逻辑跑。其实DeepSort并不关心分类模型长什么样它只接收检测框列表。这份源码里YOLOv9检测输出被转成跟踪器输入是在主循环里完成的一小段格式一般是这种结构# 将检测结果统一转换为[x1, y1, x2, y2, score, class]数组 bbox_xywh np.array( [[x1, y1, x2 - x1, y2 - y1, score, class_id], ...], dtypenp.float32, ) tracker.update(bbox_xywh, frame)要注意两点坐标务必转成xywh置信度留在score位最后的class_id传给DeepSort后只在可视化时用于显示不影响匹配逻辑。所以你自己训练的模型只要在这个位置把YOLO的result对象换成统一列表后面的ReID和卡尔曼滤波原样复用工程改动量很小。5. 避坑与排查从OOM到ID跳变的五条踩坑记录5.1 刚跑几十帧就CUDA error: out of memory现象object_tracking.py启动后前几帧正常过了几十帧直接报CUDA out of memory退出。原因这类视频工程的默认输入尺寸往往设得很大比如把1080p源放到960以上分辨率推理显存占用随分辨率呈平方级增长小卡很快就满。还有一个隐蔽因素ReID特征网络也在GPU上同时跑两段显存叠加后直接顶穿。解决先把检测输入尺寸降到640很多实现里是一个imgsz参数如果还不行换成yolov9-tiny这类小权重。我的底线是6G显存以下640加tiny优先不开大batch检测和ReID尽量保持同一个推理会话不要同时开多个进程。5.2 目标过遮挡后ID跳变不停现象目标被柱子遮住几帧再出现后ID从5跳到12群体行走时更明显。原因ReID提取外观特征时遮挡后的画面与遮挡前差异大余弦距离超过max_dist新旧状态匹配不上卡尔曼滤波在目标被挡住后会继续外推位置偏移也逐渐加大双重因素导致旧track被判死、新track建立。解决第一选择是调大max_age让track多活一段时间出障碍后还有机会匹配回来第二看max_dist如果恢复效果差就把距离上限调大一点比如0.3调0.35。这两个方向本质都是放宽关联已经频繁出现ID交换时不要两个同时乱调先动max_age。5.3 检测每帧都有框tracker输出却一直是0现象画面上彩色检测框正常在动但标注的ID和轨迹数量全是零日志里track数量不涨。原因跟踪器的min_confidence把低置信度检测过滤掉了或者n_init设置得比较大新track连续三帧才能被确认而视频里的目标一直在动中途某帧掉检就永远转不正。更常见的是检测置信度阈值和min_confidence设了同一档紧卡着临界值检测结果送不进跟踪器。解决先把跟踪器里的min_confidence调到0.1做一次验证一般就能看到ID出现再把n_init从默认3暂时降到1尽快确认新track。逻辑是先把验证链路打通再逐档把阈值拉回正常而不是全靠猜。5.4 部分视频格式读取后帧序错乱、时间戳卡住现象跑MP4一切正常换了一组AVI视频轨迹忽前忽后甚至视频后半段直接卡死。原因OpenCV底层的视频解码对不同编码格式支持不一致。同样是.aviMJPEG编码和H264编码走的是不同解码分支处理速度和帧序表现差别很大。解决我的做法是统一先转成H264的MP4再送进源码ffmpeg -i input.avi -c:v libx264 -pix_fmt yuv420p -crf 23 input_conv.mp4输出用yuv420p是为了兼容老播放器crf 23是画质和码率的平衡点。转码后直接把--source指到新文件帧序问题基本消失。5.5 装完requirements后torch被覆盖cuda不能用现象本来在conda环境验证torch.cuda.is_available()是True执行pip install -r requirements.txt后变False推理速度骤降。原因requirements.txt里可能固定了基于CPU的torch或某个旧版本pip在安装时把已有cuda版torch覆盖掉了。这是双轨依赖最常见的坑conda和pip版本主张打架pip后装就赢了。解决先按conda.yml建环境再用pip补装requirements发现覆盖后重新指定cuda版本重装# 重新安装适用于本机CUDA的torch版本号以官方索引为准 pip install torch --index-url https://download.pytorch.org/whl/cu121注意这一步不能盲抄先看nvidia-smi里DRIVER版本支持的CUDA再选对应cu版本。我见过太多人环境一崩就重装整套环境其实只要固定好torch这一根线其他就顺了。6. 进阶把跟踪结果变成业务数据——跨线计数与轨迹验证跟踪的真正价值不在花哨的框而在每个track_id连续带出的位置序列。拿这套源码跑通后我建议你往下走一步把每帧跟踪输出里的track_id和中心点记录成结构化数据再在这个序列上做业务判断。比如跨线计数是视频车流统计里最常见的需求而它其实不需要额外模型逻辑非常朴素判断一个目标的中心点从参考线上方落到下方且该track_id从未被计数过就记一次向下穿越。import csv from collections import defaultdict line_y 720 # 参考线在画面中的纵坐标 crossed set() # 已穿越的track_id trajectories defaultdict(list) with open(crossing_count.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame_id, track_id, cx, cy, event]) for frame_id, track_boxes in enumerate(frame_outputs): for box in track_boxes: track_id box[track_id] cx, cy box[center] trajectories[track_id].append((cx, cy)) if len(trajectories[track_id]) 2: continue prev_y trajectories[track_id][-2][1] if prev_y line_y cy and track_id not in crossed: crossed.add(track_id) writer.writerow([frame_id, track_id, cx, cy, down])这段代码每来一个track_id就把中心点追加进轨迹判定条件看前一个点和当前点是否跨过参考线。两点说明frame_outputs在源码里是每帧tracker返回的已确认轨迹更新结果直接从那份输出提取就行crossed集合保证同一个ID只在第一次穿越时计数不会因为后续振荡多次累加。如果你在真实场景里发现计数偏多大概率是检测框在参考线附近抖动导致轨迹反复穿越只要保留track_id集合去重绝大多数误报能挡掉。验证方法也回到这个思路用同一段视频跑两遍每次看两条曲线的ID数量和跨线事件是否一致如果不一致先回去看检测帧再看max_age而不是怀疑计数逻辑。从那以后我每次拿到一套检测加跟踪的源码都会强迫自己先跑一遍固定视频的ID轨迹连续性记录切换次数再动参数。这套流程能挡住八成瞎调。希望帮到你。本文还有配套的精品资源点击获取