基于YOLOv5与DeepSORT的行人车辆跟踪计数实战
发布时间:2026/9/28 17:18:59来源:尧图网络
简介这份资源面向计算机相关专业的毕业设计、课程设计与项目开发人群提供一套基于 Python、YOLOv5 与 DeepSORT 实现的行人或车辆跟踪计数系统。项目将目标检测与多目标跟踪结合可对视频中的行人或车辆进行实时追踪并统计数量适合作为智能交通、安防监控等方向的入门实战案例。压缩包共 122 个文件约 129.7MB包含 38 个 py 源码文件、58 个 pyc 编译文件、9 个 yaml 配置、5 个 xml 标注文件以及权重文件、演示视频、说明文档和 Dockerfile 等覆盖从模型加载到跟踪计数的完整流程。运行环境为 Win10、PyCharm 与 Python3.6依赖 PyTorch 1.7.0 以上及 OpenCV执行 main.py 即可在控制台启动检测。源码经过严格测试并附有 md 文档与演示视频便于读者理解目录结构、复现运行效果并在此基础上进行功能扩展或二次开发。目前已有 66 人学习关注。1. 从一段路口视频说起行人车辆跟踪计数到底在数什么假设你手里有一段路口监控视频需求是统计 5 分钟内经过的汽车和行人各有多少。用 YOLOv5 逐帧检测并不难难的是同一辆车在第 1 帧和第 30 帧都被检测到了你不能把它算成两辆。这就是跟踪计数要解决的核心问题检测负责看到跟踪负责认出这还是同一个目标计数负责在它第一次出现时记一笔。基于 pythonyolov5 和 deepsort 实现的这套方案本质是把三个模块串成一条流水线YOLOv5 出检测框DeepSORT 给每个框分配稳定 ID再用一条虚拟线或一个区域判断目标是否越界越界且是新 ID 就计数加一。这套东西适合谁做计算机毕业设计、课程设计的学生需要一份能跑通、能演示、能写进论文的完整项目也适合刚入门目标跟踪、想搞明白检测跟踪计数怎么拼起来的工程师。它不追求工业级精度但胜在链路清晰、依赖成熟、单卡就能跑。下面我按环境怎么搭 → 检测怎么接 → 跟踪怎么配 → 计数逻辑怎么写 → 坑在哪的顺序把这条流水线拆开讲透。2. 环境搭建与 YOLOv5 推理链路从 conda 到第一帧检测框2.1 为什么选 conda 而不是裸 pipYOLOv5 对 PyTorch、torchvision、numpy 的版本比较敏感裸 pip 装很容易出现 torch 和 torchvision 版本对不上、CUDA 版本错位的问题。我一般用 conda 建独立环境把 Python 版本锁在 3.8 或 3.9这两个版本和 YOLOv5 各版本兼容性最好。python 安装教程网上很多但真正省事的是直接用 conda 一步到位避免后面反复卸了重装。# 创建独立环境python 版本锁 3.8 conda create -n yolo_track python3.8 -y conda activate yolo_track # 安装 pytorch按自己 CUDA 版本选这里以 CUDA 11.3 为例 pip install torch1.12.1cu113 torchvision0.13.1cu113 \ --extra-index-url https://download.pytorch.org/whl/cu113 # 安装 YOLOv5 依赖 pip install -r requirements.txt逻辑说明先隔离环境再装和 CUDA 匹配的 torch最后装 YOLOv5 的 requirements。参数上torch1.12.1和torchvision0.13.1是配套的别只改一个。如果你没有 N 卡把cu113去掉装 CPU 版也能跑只是帧率会掉到个位数演示够用、实时不够。2.2 YOLOv5 加载与推理的最小代码环境好了之后先别急着接跟踪单独把检测跑通确认模型能出框。这一步是后面所有工作的地基检测不稳跟踪全是玄学。import torch import cv2 import numpy as np # 加载 YOLOv5 模型weights 换成自己训练或官方预训练的 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.4 # 置信度阈值低于这个的框直接丢 model.iou 0.45 # NMS 的 IoU 阈值控制重叠框合并 model.classes [0, 2] # 只保留 person(0) 和 car(2)按 COCO 类别索引 cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # YOLOv5 接受 RGBOpenCV 读进来是 BGR要转 results model(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) # 拿到 xyxy 格式的框、置信度、类别 detections results.xyxy[0].cpu().numpy() for x1, y1, x2, y2, conf, cls in detections: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.imshow(detect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明model.conf和model.iou是两个必调参数。conf 调低会出更多框但误检变多调高会漏检iou 控制 NMS 合并程度密集场景调低一点能减少框被误合并。model.classes只保留你关心的类别行人车辆计数就留 person 和 car能显著减少后续跟踪的计算量。results.xyxy[0]是 [x1,y1,x2,y2,conf,cls] 的数组这是喂给 DeepSORT 的标准输入格式。2.3 检测帧率不够时的三个降本手段很多人第一次跑会发现帧率只有 5 到 10视频卡成 PPT。常见做法有三个一是换更小的模型yolov5s 换 yolov5n精度掉一点但速度快一倍二是跳帧检测每 2 帧或 3 帧检测一次中间帧靠跟踪器预测位置补上这也是 DeepSORT 的强项三是把输入尺寸从 640 降到 416model.imgsz 416速度提升明显。我一般先跳帧因为跟踪器本来就能在检测缺失时靠卡尔曼滤波预测跳帧对跟踪连续性影响最小。3. DeepSORT 接入把检测框变成稳定 ID 的四个关键配置3.1 DeepSORT 到底在做什么DeepSORT 在 SORT 的基础上加了一个外观特征提取网络ReID每个检测框会过一个 CNN 得到一个特征向量跟踪时不仅看位置匹配卡尔曼滤波预测 匈牙利算法关联还看外观相似度。这就是为什么同一辆车被短暂遮挡后重新出现ID 还能接上——位置对不上但外观特征对得上。理解这一点后面调参就有方向了位置匹配靠max_dist外观匹配靠max_iou_distance和特征库。3.2 把 YOLOv5 检测结果喂给 DeepSORTDeepSORT 的输入是[x1, y1, x2, y2, conf, cls]的数组正好是 YOLOv5 的输出格式所以对接很顺。下面是最小可跑版本。from deep_sort_realtime.deepsort_tracker import DeepSort # 初始化跟踪器 tracker DeepSort( max_age30, # 目标丢失后保留 ID 的帧数 n_init3, # 连续检测到几帧才确认一个新 ID max_iou_distance0.7, # 位置匹配的 IoU 距离阈值 embeddermobilenet, # 外观特征提取网络 embedder_gpuTrue ) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) detections results.xyxy[0].cpu().numpy() # 转成 DeepSORT 要的格式[left, top, w, h, conf, class] ds_input [] for x1, y1, x2, y2, conf, cls in detections: ds_input.append(([float(x1), float(y1), float(x2 - x1), float(y2 - y1)], float(conf), int(cls))) tracks tracker.update_tracks(ds_input, frameframe) for track in tracks: if not track.is_confirmed(): continue track_id track.track_id l, t, r, b track.to_ltrb() cv2.rectangle(frame, (int(l), int(t)), (int(r), int(b)), (0, 255, 0), 2) cv2.putText(frame, fID {track_id}, (int(l), int(t) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)逻辑说明max_age30表示目标消失 30 帧内 ID 不注销遮挡场景可以调大但太大会导致 ID 串到别的目标上。n_init3是防抖连续 3 帧检测到才确认能过滤掉一闪而过的误检。max_iou_distance0.7控制位置匹配的宽松度密集场景调小、稀疏场景调大。embeddermobilenet是外观特征网络换成clip精度更高但更慢毕业设计用 mobilenet 足够。3.3 参数怎么调一张对照表参数作用调大后果调小后果建议值max_age丢失后保留 ID 帧数ID 不易断但易串ID 易断20~40n_init确认新 ID 的连续帧数新目标确认慢误检易成 ID2~3max_iou_distance位置匹配阈值匹配宽松易串匹配严格易断0.6~0.8embedder外观特征网络精度高速度慢速度快精度低mobilenet这张表是我踩坑后总结的实际调的时候一次只动一个参数看视频里 ID 切换的次数别一次改一堆否则你根本不知道是哪个参数起的作用。4. 计数逻辑虚拟线、区域判定与去重4.1 计数为什么不能简单按 ID 累加最直觉的做法是维护一个已见 ID 集合每出现一个新 ID 就计数加一。但问题是目标走出画面再走回来DeepSORT 可能给它分配新 ID于是同一辆车被数了两次。所以计数必须绑定越界事件而不是ID 出现。常见做法是画一条虚拟线判断目标的中心点是否从线的一侧移动到另一侧只有发生穿越才计数并且记录这个 ID 已经计过数防止来回抖动重复计。4.2 虚拟线计数的实现counted_ids set() # 已经计过数的 ID line_y 300 # 虚拟线在画面中的 y 坐标 prev_centers {} # 记录每个 ID 上一帧的中心点 def check_cross(track_id, center_y): 判断目标是否从上往下穿过虚拟线 if track_id in counted_ids: return False if track_id not in prev_centers: prev_centers[track_id] center_y return False prev_y prev_centers[track_id] prev_centers[track_id] center_y # 上一帧在线以上这一帧在线以下判定为穿越 if prev_y line_y center_y: counted_ids.add(track_id) return True return False total_count 0 for track in tracks: if not track.is_confirmed(): continue l, t, r, b track.to_ltrb() center_y (t b) / 2 if check_cross(track.track_id, center_y): total_count 1逻辑说明counted_ids是去重的后悔药一个 ID 只计一次。prev_centers记录上一帧位置用来判断穿越方向。prev_y line_y center_y表示从上往下穿如果要统计双向再加一个反向判断即可。line_y根据你的视频分辨率调一般放在画面中间偏下避免目标刚进画面就被计。4.3 区域计数与多类别分开统计如果需求不是过线而是进入某个区域把线换成多边形用cv2.pointPolygonTest判断中心点是否在区域内进入时计数。多类别分开统计也简单DeepSORT 的 track 里带了类别信息按 cls 分别维护counted_ids和计数器就行。行人车辆分开计数就是两个独立的字典和两个总数。提示计数逻辑一定要和目标跟踪解耦跟踪器只负责给稳定 ID计数模块只消费 ID 和位置。这样后面换跟踪器或改计数规则互不影响。5. 避坑与排查ID 乱跳、漏检、计数翻倍的五个血泪经验5.1 ID 频繁切换同一目标一会儿一个号现象视频里同一辆车 ID 从 3 跳到 7 又跳到 12。原因通常是检测框抖动太大或者外观特征区分度不够。解决先把 YOLOv5 的 conf 从 0.4 提到 0.5减少低质量框再把max_iou_distance从 0.7 调到 0.8让位置匹配更宽松如果还不行换embedderclip提升外观特征质量。我遇到过一次是输入分辨率太低导致框抖把 imgsz 从 416 提到 640 就稳了。5.2 目标被遮挡后 ID 丢失重新出现变成新 ID现象车被前车挡住几秒出来之后 ID 变了计数多算一次。原因max_age太小遮挡期间 ID 被注销。解决把max_age从 30 提到 50给遮挡留足缓冲。但注意别调太大否则目标离开画面后 ID 还挂着容易串到新目标上。另一个办法是降低n_init让新 ID 确认更快减少遮挡后重新确认的延迟。5.3 计数翻倍一辆车数了两次现象总数比实际多。原因多半是虚拟线附近目标来回抖动中心点反复穿越。解决counted_ids去重是必须的另外加一个冷却时间同一个 ID 计数后 N 帧内不再判定。还可以把虚拟线加粗成一条带中心点必须完全穿过带才计数避免边界抖动。5.4 帧率太低视频卡顿现象处理速度跟不上画面一顿一顿。原因检测和跟踪都在每帧跑计算量叠加。解决跳帧检测每 2 帧检测一次中间帧只跑跟踪器预测或者换 yolov5n 模型再不行降 imgsz。这三个手段可以叠加我一般先跳帧性价比最高。5.5 类别串了行人被当成车计数现象行人计数里混进了车。原因YOLOv5 的model.classes没设对或者 DeepSORT 返回的类别索引和你的映射对不上。解决确认 COCO 类别索引person 是 0car 是 2bicycle 是 1在计数模块里按 track 的 cls 字段分流别用检测框的 cls 直接累加因为跟踪器可能对类别做平滑。6. 进阶技巧用跳帧轨迹平滑把计数精度再提一档前面讲的都是能跑通的基础版如果你要拿去做毕业设计答辩或者想在实际场景里把精度再压一压有两个技巧值得试。第一个是跳帧检测 跟踪补位。具体做法是每 3 帧才跑一次 YOLOv5中间两帧只调tracker.update_tracks([], frameframe)传空检测列表让 DeepSORT 靠卡尔曼滤波预测位置。这样检测计算量降到三分之一帧率能翻倍而跟踪连续性几乎不受影响因为 DeepSORT 本来就有预测能力。代价是目标快速变向时预测会偏但计数场景里目标运动相对平滑影响很小。第二个是轨迹平滑后再判穿越。原始中心点会抖直接判穿越容易误触发。做法是给每个 ID 维护一个长度为 5 的中心点队列取中位数或均值作为判定点再和虚拟线比较。这样单帧抖动被平滑掉计数更稳。代码上就是用一个deque(maxlen5)存历史中心点判穿越时用平滑后的值。from collections import deque smooth_centers {} # id - deque of center_y def smooth_and_check(track_id, center_y): if track_id not in smooth_centers: smooth_centers[track_id] deque(maxlen5) smooth_centers[track_id].append(center_y) # 取中位数抗单帧抖动 sorted_vals sorted(smooth_centers[track_id]) return sorted_vals[len(sorted_vals) // 2]逻辑说明deque(maxlen5)自动丢弃旧值只保留最近 5 帧。取中位数而不是均值是因为中位数对异常值更鲁棒单帧检测跳变不会带偏判定点。这个平滑只用在计数判定上不影响跟踪器内部状态所以不会干扰 ID 稳定性。验证方法上我一般会准备一段带标注的短视频人工数出真实过线数量然后跑三组参数对比基础版、跳帧版、跳帧平滑版看哪组误差最小。别只看总数还要看每个 ID 的计数是否合理有没有明显串号。参数没有万能值你的场景光照、密度、目标速度都会影响唯一靠谱的办法就是拿自己的视频反复试。我自己做这类项目最大的教训是别一上来就调跟踪器参数先把检测质量提上去。检测框抖跟踪再牛也白搭检测稳了DeepSORT 默认参数就能跑得不错。另外计数逻辑一定要和跟踪解耦我早期把计数写死在跟踪循环里后来想换计数规则改得想砸键盘。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网