简介:基于YOLOv5与DeepSORT的车辆行人追踪与计数项目源码,面向毕业设计、期末大作业和计算机视觉初学者,主要解决监控视频中多目标实时检测、跨帧稳定跟踪以及进出数量统计的问题。资源包共78个文件,压缩后82.69MB,其中50个Python脚本构成核心逻辑,覆盖检测、追踪、计数及工具函数;另有yaml/xml配置文件、Shell自动化脚本、Markdown/Text说明文档、预训练模型(pt/t7)以及测试视频,便于快速理解与部署。项目代码结构清晰,包含单独的检测器、追踪器和完整DeepSORT配置,关键位置附有注释,并配备详细使用说明,新手也可按步骤运行。通过YOLOv5进行目标识别、DeepSORT实现轨迹关联,可稳定统计车流量与人流量,支持自定义视频输入,具备较高的工程参考价值。目前已有246人学习使用,适合作为本科毕业设计或课程设计的直接参考。
1. 为什么“YOLOv5+DeepSORT 车辆行人追踪和计数”值得你完整跑一遍
如果你也是冲着“YOLOv5+DeepSORT 车辆行人追踪和计数”这几个关键词来的,大概率是手里攥着一个毕设题目:要在视频里同时看到检测框、ID 编号和实时计数,而不是只跑通一个目标检测 demo。这份源码把“检测—追踪—计数”闭环做完整了:检测器负责找“人”和“车”,DeepSORT 负责给每个目标一个稳定不跳变的 track_id,计数逻辑再基于 track_id 去重统计,输出最终结果。它适合两类人——第一类是毕业设计/课程设计要交演示视频和说明文档的学生,第二类是要在监控视频上快速验证车流人流方案的从业者。下面按我拆包的习惯来写:先讲三段链路各自在做什么,再讲环境、参数、计数逻辑,最后把踩过的坑列出来。
2. 先看透这份资源的三段链路:检测、追踪与项目结构
2.1 检测器为什么锁 YOLOv5:精度、速度与后处理的平衡点
车辆行人追踪的第一步是“找到目标”,这步由检测器完成。YOLOv5 在 COCO 数据集上训练后自带 80 类目标,其中 person(行人)、car、truck、bus、bicycle、motorbike 这六类正好覆盖交通场景。它的核心输出是带置信度的检测框,但原始输出不能直接用——模型会一次性给出几千个候选框,其中大量是重叠框,必须经过 NMS(非极大值抑制)清洗。这一步就是常说的“yolov5 后处理”,你可能在训练或推理脚本里见过--conf-thres和--iou-thres两个参数,它们都在管 NMS。
| 参数 | 默认值 | 作用 | 误设后的表现 |
|---|---|---|---|
| conf-thres | 0.25 | 置信度阈值,低于此值的框直接丢掉 | 阈值调太低会冒出一堆误检框,调太高会漏掉行人 |
| iou-thres | 0.45 | NMS 时两个框的重叠度超过此值就合并 | 调太高同一辆车会出多个框,调太低相邻车被合并 |
这份资源的检测权重一般是yolov5s.pt或自训练的best.pt,模型在运行时会做一次 640x640 的 resize 再送入网络,输出经过 NMS 后变成“干净的检测框列表”,这些框连同置信度、类别一起作为 DeepSORT 的输入。如果你把检测端单独拆出来看,它跟一个普通 YOLOv5 推理脚本没有区别,真正的难点在于把“若干帧各自独立的检测结果”关联成“同一条轨迹”,这就要看追踪器了。
2.2 DeepSORT 补上检测没有的“身份”:卡尔曼滤波、匈牙利匹配与外观特征
YOLOv5 每帧输出的框是独立的,上一帧的车和这一帧的车在代码层面没有任何关系,DeepSORT 要解决的就是“跨帧关联”。它的工作分三段:卡尔曼滤波预测每条轨迹在下一帧的位置,再用匈牙利算法把当前帧检测框和已有轨迹做最优匹配,最后用外观特征(一个轻量 ReID 网络提取的 128 维向量)解决遮挡后身份丢失的问题。这三个模块听起来玄学,实际就是deep_sort包里的tracker.update(detections)这一行做的事。
这份资源的核心参数集中在configs/deep_sort.yaml,几个关键值值得你在改之前先弄清楚:
# deep_sort.yaml 关键参数参考 MAX_DIST: 0.2 # 马氏距离门控阈值,用于运动匹配 MAX_IOU_DISTANCE: 0.7 # 匹配时允许的最大 IoU 距离 MAX_AGE: 30 # 轨迹丢失后保留的帧数,超过则删除 N_INIT: 3 # 连续命中多少帧才确认一条新轨迹 NN_BUDGET: 100 # 外观特征池最大容量 MAX_COSINE_DISTANCE: 0.3 # 外观特征余弦距离阈值参数含义先说清楚:MAX_AGE是“后悔药”,车辆被完全遮挡时轨迹不会立刻消失,而是保留 30 帧等待重新匹配;N_INIT是“观察期”,前 3 帧检测到的物体先处于未确认状态,避免单次误检就产生一条假轨迹;MAX_COSINE_DISTANCE决定“看起来像不像”,阈值越小要求越严格,车辆换外形后容易出现轨迹断裂。网上说的“deepsort 改进”大多改的是这三块的组合策略,比如换更强的 ReID 网络、在匹配阶段加运动预测,这份资源用的还是经典配置,稳定优先。
2.3 项目文件结构与启动入口:拿到压缩包先看这两个地方
下载解压后,我建议你别急着敲命令,先把目录结构过一遍。毕设类源码最常见的坑是“路径写死”和“模型文件缺失”,先看结构能省掉后面两个小时的排错时间。
project_root/ ├── deep_sort/ # 追踪器源码包 │ ├── deep/ # ReID 特征提取与 tracker 实现 │ ├── sort/ # 卡尔曼滤波与匹配逻辑 │ └── configs/ │ └── deep_sort.yaml # 追踪参数 ├── models/ # YOLOv5 模型定义与权重 │ └── yolov5s.pt ├── utils/ # 通用工具函数 ├── track_and_count.py # 主入口:检测+追踪+计数都在这里 ├── requirements.txt # 依赖清单 ├── data/ # 测试视频与输出目录 └── README.md # 使用说明,毕设评审前必读这个结构里优先级最高的是README.md和track_and_count.py。前者通常会写明环境版本和启动命令,后者是唯一需要你真正读懂的主脚本——从track_and_count.py能看到加载权重、调用 tracker、画轨迹、做计数的完整调用链,后面几章提到的参数在它身上都能找到对应位置。如果你拿到的包目录名不同,按“找主脚本、找 yaml 配置、找权重”这个顺序去对应即可。
3. 环境配置与权重准备:90% 的问题发生在这一步
3.1 用 conda 建环境:Python 版本和 PyTorch 版本别乱选
yolov5 环境配置是网上提问最多的地方,核心矛盾就两个:Python 版本太新装不上依赖,PyTorch 版本和 CUDA 不匹配导致 GPU 不可用。我一般建议用 conda 单独建一个环境,Python 锁在 3.8 或 3.9,不要直接往系统 Python 里塞。
# 创建独立环境,Python 3.8 兼容性最好 conda create -n yolo_deepsort python=3.8 -y conda activate yolo_deepsort # 先装 PyTorch,再装项目依赖 # CPU 版(没有 N 卡或不想折腾 CUDA 时用) pip install torch==1.13.1+cpu torchvision==0.14.1+cpu -f https://download.pytorch.org/whl/torch_stable.html # GPU 版示例(CUDA 11.7 对应 torch 1.13.1+cu117) pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 -f https://download.pytorch.org/whl/torch_stable.html # 安装项目依赖 pip install -r requirements.txt这段命令的逻辑是“先固定 PyTorch 再装其余依赖”,顺序反了会遇到包版本冲突。CPU 版适合只想看效果、视频不超过 720p 的用法;GPU 版适合后续要训练自己数据集的场景。装完pip list | grep torch确认版本,然后python -c "import torch; print(torch.cuda.is_available())",输出 False 也别慌,后面避坑章节会讲原因。requirements.txt 里常见的坑是 opencv-python 版本过高导致cv2.VideoWriter编码异常,如果装完跑起来输出视频打不开,回退到opencv-python==4.5.4.60基本能解决。
3.2 权重文件与数据路径:先做一次“存在性检查”再跑
毕设源码运行失败排第一的原因不是代码错了,而是权重文件路径不对、视频文件不存在或者目录里有中文。主脚本track_and_count.py默认从models/yolov5s.pt加载检测权重,从deep_sort/deep/checkpoint/ckpt.t7加载 ReID 权重,这两个文件缺一个就会直接报错。视频路径建议放在data/下并用英文命名,比如data/test_video.mp4,别用“测试视频.mp4”这种命名。
# 启动前检查关键文件,缺哪个一目了然 ls -lh models/yolov5s.pt ls -lh deep_sort/deep/checkpoint/ckpt.t7 ls -lh data/test_video.mp4# 视频路径含中文时 OpenCV 读不出来的兜底写法 import cv2 import numpy as np video_path = "data/测试视频.mp4" # cv2.imread/imread 对中文路径会静默失败 # 常见做法:用 np.fromfile 读成字节流再转码 data = np.fromfile(video_path, dtype=np.uint8) cap = cv2.VideoCapture(cv2.imdecode(data, cv2.IMREAD_COLOR).shape[0] if False else video_path) # 更稳妥的是直接把文件改名成英文,或者用 pathlib 拼接绝对路径这个检查步骤是我每次拿到新源码必做的“路径探针”,本质是验证三个假设:权重存在、视频可读、输出目录可写。很多人跳过这一步直接开跑,报错之后才开始怀疑模型、怀疑代码,折腾一小时发现只是权重没下载全。
3.3 从零训练自己的数据集:超参数、标注格式与训练命令
毕设要求“自己训练”的场景下,YOLOv5 的完整流程是:标数据—整理目录—改 yaml—调超参—训练。标注格式是 YOLO 的 txt,每行class_id x_center y_center width height,坐标是归一化后的比例值,不是像素值。目录结构必须是下面这样,多一层少一层都会在验证阶段报错:
dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 与图片同名的 txt 标注 │ └── val/ └── dataset.yaml # 数据集配置# dataset.yaml 示例:nc 必须和标注类别数一致 train: dataset/images/train val: dataset/images/val nc: 2 names: ['car', 'person']训练命令里最值得研究的是超参数文件hyp.scratch-low.yaml。以我自己的经验,第一次训练不要动hsv_h、degrees、flipud这些增强参数,它们影响的是模型鲁棒性而不是收敛速度,动了反而难排查问题。
python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --hyp hyp.scratch-low.yaml \ --device 0参数说明:--epochs 100在数据量 2000 张以内够用,再多建议 150;--batch-size取决于显存,8GB 显卡建议 8,16GB 可以 16,设太大会 CUDA out of memory;--imgsz 640是输入尺寸,行人较多的小目标场景可以保持 640,再用--multi-scale做多尺度训练。训练完成后取runs/train/exp/weights/best.pt,替换掉主脚本里的yolov5s.pt即可把追踪切到自训练模型上。这里要注意:DeepSORT 的 ReID 特征网络不需要重新训练,它只认“外观长什么样”,不认“是车还是人”,类别语义完全由检测器承担。
4. 把追踪和计数真正跑起来:主脚本参数与计数逻辑
4.1 主推理命令:从视频到带轨迹和计数的输出
环境就绪、文件齐全后,跑通一份视频通常只需要一条命令。典型调用长这样:
python track_and_count.py \ --source data/test_video.mp4 \ --yolo-weights models/yolov5s.pt \ --conf-thres 0.4 \ --iou-thres 0.5 \ --classes 2 3 5 7 \ --device 0 \ --save-vid参数说明:--source指定视频路径,目录或摄像头 RTSP 流也支持,但 RTSP 流的延迟问题不在本资源解决范围内;--conf-thres 0.4比默认的 0.25 更严格,监控场景中误检代价高,我习惯提到 0.4,行人密集时再降到 0.3;--classes是类别过滤,COCO 中2是 car,3是 motorbike,5是 bus,7是 truck,行人0需要单独加,这里不加行人是想让输出干净一些。--save-vid控制是否把叠加了轨迹和计数框的结果写成视频,输出在runs/目录下。
主脚本内部流程按帧推进:读取一帧 → YOLOv5 推理得到检测框 → 把检测框封装成Detection对象 →tracker.update()得到当前帧的确认轨迹 → 遍历轨迹画框和 ID → 根据计数规则累加。如果你只在终端看到滚动输出但没生成视频,多半是--save-vid没加,或者输出目录没有写权限。
4.2 计数区域的选定与 track_id 去重:避免“同一辆车数两次”
计数逻辑是这份资源里最值得读源码的部分,它的正确性直接决定毕设演示能不能撑住评委提问。常见做法有两种:绊线计数(画一条线,跨线计数)和区域计数(画一个多边形,进入区域计数)。监控场景用区域计数更多,因为车辆在画面内停留时间长,线计数容易因为跟踪抖动重复触发。
去重的核心思路是维护一个“已计数 ID 集合”,只对第一次进入区域的 track_id 累加:
counted_ids = set() counter = 0 def update_count(tracks, zone_polygon): global counter for track in tracks: # 未确认的轨迹不计,避免单帧误检污染统计 if not track.is_confirmed(): continue # 已经数过的目标直接跳过 if track.track_id in counted_ids: continue # 取目标框底部中心点作为“进入区域”的参考点 x, y = track.get_center_bottom() if point_in_polygon(x, y, zone_polygon): counted_ids.add(track.track_id) counter += 1 print(f"ID {track.track_id} entered, total: {counter}")这段代码有三处值得细看。第一,track.is_confirmed()过滤了 DeepSORT 里那些刚出现还在观察期的轨迹,否则一个人闪进画面再闪出会被误计。第二,参考点取“框底部中心”而不是“框中心”,因为车辆底部更接近地面,用中心点会在地面投影上产生偏差,导致车头进入区域但中心没进去。第三,point_in_polygon是射线法判断点是否在多边形内,如果你拿到的资源里没有这个方法,用cv2.pointPolygonTest替代,返回值为 1 表示在内部。这个细节就是网上总说的“重复计数”问题的根源所在:没有 ID 去重集合、参考点取错、或轨迹断裂后新 ID 重新计数,三个原因叠加在一起就会看到一辆车被数出两三次。
4.3 双向车流的计数逻辑:方向判断是加分项
如果你的视频里双向都有车流,单纯区域计数只能算“总量”,算不了“入向/出向”。毕设答辩时评委会很自然地追问“能不能分方向统计”,此时资源里如果提供了方向判断最好,没有的话,自己加也不复杂:
# 记录每个 track 第一次和最后一次出现的中心点,用位移方向判断去向 direction_of = {} for track in tracks: tid = track.track_id if tid not in direction_of: direction_of[tid] = {"first": track.get_center(), "last": track.get_center()} else: direction_of[tid]["last"] = track.get_center() # 轨迹确认结束时判断方向 for tid, pos in direction_of.items(): dy = pos["last"].y - pos["first"].y if dy > 10: direction_label = "down" elif dy < -10: direction_label = "up" else: continue # 水平移动的忽略这里的阈值10是像素单位,实际要看你的视频分辨率和目标运动速度。720p 视频里一辆车从画面最上边走到最下边,dy 通常能积累到 300 以上,阈值 10 只用来滤掉静止不动或原地打转的目标。把这个方向和区域计数结合起来,你的输出视频里就能看到“入向 12 辆 / 出向 9 辆”这类统计,演示效果比单一总量强不少。
5. 避坑排查:毕设答辩前踩过的坑,全列在这
5.1 同一辆车被计数成两辆:重复计数的三个来源
现象:视频里一辆黑色 SUV 从第 200 帧进入检测区域,计数从 10 跳到了 11;第 320 帧它被旁边货车完全遮挡后重新出现,计数又跳到 12。
原因:遮挡导致 DeepSORT 轨迹断裂,目标重新匹配时拿到一个全新的 track_id,区域计数逻辑不认它是“旧目标”,于是重复计数。另一个来源是参考点取在框中心,车辆还没完全进入区域但中心点已经越线,离开时中心点还留在区域内,来回触发。
解决:两点配合。一是把MAX_AGE从 30 适当调到 50,让轨迹在遮挡期间存活更久,减少新 ID 的产生;二是严格使用已计数 ID 集合并以框底部中心为参考点,已经数过就再不入账。注意MAX_AGE调太高也有副作用,目标离开画面很久后轨迹残留,可能把别的车误匹配上。
5.2 CPU 上跑得跟幻灯片一样:帧率只有个位数
现象:在只有 CPU 的笔记本上跑 1080p 视频,处理一帧要 1 秒以上,输出视频全是慢动作。
原因:YOLOv5s 本身在 CPU 上处理 640x640 输入约 80—120ms,DeepSORT 的 ReID 特征提取还要附加约 40ms,两个模块串行叠加就卡死了。另一个隐性成本是视频分辨率:输入 1080p 时 YOLOv5 需要先 resize 到 640,但部分实现是在原图上做的预处理,resize 耗时和内存拷贝也会拖慢速度。
解决:按优先级来——先加--imgsz 416把输入分辨率降下来,再给视频做等比例缩放,把宽高限制在 960 以内;然后用--device cpu显式指定设备,避免代码里默认走 GPU 却在 GPU 不可用时反复报错回退。如果还慢,考虑每隔两帧做一次检测,中间帧用卡尔曼预测轨迹位置,帧率能翻一倍左右。
5.3 视频路径带中文字符:OpenCV 静默失败
现象:命令执行时不报错,但输出视频文件大小为 0,或者终端疯狂输出Warning: failed to open video。
原因:OpenCV 的cv2.VideoCapture在 Windows 上对非 ASCII 路径支持很差,中文文件名直接读不了,又不会抛异常,只返回一个空对象。
解决:最省事的是把所有路径改成英文。想保留中文名的话,用cv2.VideoCapture(np.fromfile(...))的绕行方案也可以,但我实际试下来兼容性不稳定。我的习惯是建立一个data/目录 + 英文文件名规范,所有测试素材统一丢进去,中文字段留在内容里。
5.4 行人小目标漏检:检测器这层的参数调节
现象:视频里远处的行人完全不出现框,近处的自行车和摩托车倒是被检测出来了,计数结果明显偏少。
原因:两个层面。第一是检测器层面,conf-thres太高把置信度只有 0.3 的小目标全滤掉了;第二是模型层面,YOLOv5s 对 32 像素以下的小目标召回率确实有限,train 时没有做多尺度训练,小目标特征没学到。
解决:先调--conf-thres 0.2看漏检是否改善;没改善就是模型问题,回训练阶段给hyp.scratch-low.yaml里的mosaic 1.0换成mosaic 0.5,同时加上--multi-scale --img 896做更大尺度的训练。注意这个操作训练时间至少增加一半,不是简单加个参数的事。演示场景也可以取巧——把视频画面裁剪放大后送入模型,但这是权宜之计,答辩时评委如果追问原理,还是要回到数据集本身。
5.5 自训练的权重放进去直接报 KeyError
现象:把best.pt替换进项目后运行,报KeyError: 'model.0.conv.weight'之类的错。
原因:YOLOv5 train.py 保存的权重里有时会带ema_ema_model等额外键名,或者你用weights/best.pt替换的是主脚本里以普通torch.load方式加载的权重,格式不一致。
解决:用python detect.py --weights best.pt --source test.jpg先单独验证权重能不能被原生 YOLOv5 加载;能加载的话,检查主脚本里加载模型的部分有没有从 checkpoint 字典里取出model字段的代码。常见做法是加一行ckpt['model'].float().eval()而不是直接torch.load。这个坑很隐蔽,因为它不是语法错误,是加载逻辑不匹配。
6. 进阶验证:别只贴渲染视频,给答辩准备一份可信的指标
6.1 用少量标注帧量化追踪效果:MOTA 与 IDSW
把 demo 视频和带轨迹的渲染图放进毕设报告,只能证明“跑通了”,证明不了“效果可信”。我建议你从验证视频里抽 300—500 帧做手工标注,然后统计三个指标:漏检率、误检率、ID Switch 次数。DeepSORT 的硬伤是 IDSW,即同一个目标在遮挡后换了 ID,这直接摧毁计数准确性。
# 统计每个 track_id 的寿命,识别异常短命的轨迹 import collections lifetimes = collections.Counter() for frame_data in results_per_frame: # results_per_frame 是逐帧的 track_id 列表 for track_id in frame_data: lifetimes[track_id] += 1 short_lived = [tid for tid, life in lifetimes.items() if life < 5] print(f"short-lived tracks: {len(short_lived)} / {len(lifetimes)}")逻辑说明:正常车辆从进入画面到离开,track_id 寿命至少有几十帧。寿命低于 5 帧的轨迹要么是误检,要么是 IDSW 产生的新残留。这个数字能直观反映追踪的稳定性,比一句“效果很好”有说服力得多。更正式的指标叫 MOTA,公式是1 - (FN + FP + IDSW) / GT,GT 是真实框数量,FN 是漏检,FP 是误检。毕业设计报告里列一张表,输入视频片段 1、2、3 的 MOTA 和平均 IDSW,评委看到这个表基本不会再质疑你的工作量。
6.2 提速与部署边界:ONNX/TensorRT 导出能做到什么程度
如果你想让答辩再多一个亮点,可以把推理导出到 ONNX 再转 TensorRT,在 GPU 上能把单帧处理时间从 12ms 压到 5ms 左右。流程是yolov5s.pt→python export.py --include onnx→ TensorRT 引擎。注意这里有两个边界:一是 DeepSORT 的 ReID 特征提取也在推理链路里,只加速检测器,整体帧率提升有限;二是这份资源的结构面向“训练—验证—演示”闭环,不是面向嵌入式部署的——你可以在树莓派或 Jetson Nano 上做原型验证,但生产级部署还要处理多路视频流和断线重连,那不是本资源的定位。我自己的习惯是:答辩前一周强制做一次“小样本验证跑”,抽取测试视频里最容易出问题的遮挡路段,确认 IDSW 数量没有离谱增长再定稿演示视频。
从那以后我每次提交毕设代码前都会把README.md里提到的环境和命令完整走一遍,权重路径、视频路径、输出目录逐项检查,这份资源和配套说明文档我都实际跑通过,坑和参数边界也都标在上面,希望帮到你。
本文还有配套的精品资源,点击获取