简介:基于YOLOv5与DeepSort的车辆行人追踪计数项目,是一套面向毕业设计、课程设计和期末大作业的完整可运行源码。项目将YOLOv5目标检测与DeepSort多目标跟踪有机结合,能够对视频或实时画面中的车辆和行人进行精准识别、连续跟踪与自动计数,代码中附有详细注释,逻辑清晰,新手根据使用说明即可完成环境配置和部署。资源包共包含七十八个文件,主要涉及五十个Python脚本、六个YAML配置文件、预训练权重文件、说明文档以及测试视频等,整体大小约八十二点六九兆字节,目录模块划分明确,便于按需调用和二次开发。目前该项目已有两百四十六人学习下载,适合需要快速搭建智能监控、交通流量统计等应用场景的开发者参考。整体经过调试稳定,界面简洁,既能直接作为毕业设计演示,也可在此基础之上扩展车型识别、越界报警等功能,具有较高的实用价值与完成度。
1. 基于YOLOv5+Deepsort实现车辆行人追踪和计数项目源码:毕设为什么值得盯住这条链路
“基于YOLOv5+Deepsort实现车辆行人追踪和计数项目源码”这个标题,看起来只是把一个仓库名写进了论文封面,但它实际上覆盖了视频分析项目里最完整的一条链路:视频进、框出来,框变成轨迹,轨迹变成计数值。用YOLOv5做单帧目标检测,用Deepsort把同一个车辆的检测结果连成一条轨迹,最后基于轨迹去统计进出方向和数量。这个组合最适合两类人:一是毕业设计需要可演示、可量化效果的开发者;二是已经有了检测基础、想做计数的从业者。它解决的问题很具体——单帧检测本身无法回答“这一辆车是不是刚才那一辆”,只有加追踪,才能得到不重不漏的计数。在开源社区里,围绕这套组合的轮子已经很多,但大多数“源码包”只是把两个模型拼在一起,关键的计数逻辑和参数配置反而没人讲。下面重点讲清楚的关键点,就是把你从“能跑通”拉到“能解释为什么跑通、参数为什么这么设”的状态。
2. 把检测层先做稳:用YOLOv5跑通车辆行人的识别与后处理
检测层是整个计数项目的地基。计数器看似依赖后端的追踪和跨线逻辑,但绝大多数计数误差的源头都在检测层:漏检导致轨迹断掉,错检导致track_id乱跳。先把YOLOv5的输出吃透,DeepSORT才不会跟着翻车。
2.1 最小可用命令:一条视频直接让YOLOv5输出车辆行人框
拿到项目源码后,第一步不是打开IDE读代码,而是先让模型对一段车辆视频产生逐帧检测结果。在YOLOv5的源码根目录下,最直接的命令是:
python detect.py --weights yolov5s.pt --source ./test_video.mp4 --conf-thres 0.35 --iou-thres 0.45 --classes 0 2 5 7 --save-txt这段命令的要点在参数上:--conf-thres 0.35把置信度阈值从默认的0.25提到0.35,先过滤掉一批误检;--iou-thres 0.45控制NMS的并交比阈值,阈值越小,重叠框被合并得越彻底;--classes 0 2 5 7是COCO数据集中本次要用的类别索引,0代表person,2、5、7分别代表car、bus、truck,如果只需要轿车和行人,可以直接写0 2。--save-txt会把每帧检测结果落到label文件里,这比盯终端打印更容易定位问题。
跑完命令需要理解一件事:detect.py只是做单帧目标检测,它没有能力知道“上一帧的框和这一帧的框是不是同一辆车”。这也是为什么不能只靠YOLOv5做计数——车辆一旦重叠、遮挡,单帧检测结果就互相隔离。当要在自己的代码里集成而不是用命令行时,常见做法是在项目内部实例化模型,循环读取视频帧推理,把检测结果组织成数组供后续追踪使用。
2.2 检测结果的正确读法:xyxy、置信度与类别索引缺一不可
在项目源码里,检测结果进入DeepSORT之前,通常要过一道后处理转换。这个转换就是“yolov5后处理”在毕设中最常见的形态。YOLOv5官方推理接口返回的results对象,在调用results.xyxy[0]后得到的是一个NumPy数组,每行格式为[x1, y1, x2, y2, confidence, class_id]。我一般会先做一个过滤函数,把不需要的类别和不稳定的低置信度框剔掉:
import numpy as np import torch # 加载本地权重,source='local' 表示从当前源码目录加载模型 model = torch.hub.load('.', 'custom', path='yolov5s.pt', source='local') model.conf = 0.35 model.iou = 0.45 # 只保留 person(0), car(2), bus(5), truck(7) model.classes = [0, 2, 5, 7] def parse_detections(results): """把YOLOv5检测结果转换成追踪层要的格式""" det = results.xyxy[0].cpu().numpy() if len(det) == 0: return np.empty((0, 6)), np.empty((0, 4)) # 按置信度排序,确保高分目标排在前面 det = det[det[:, 4].argsort()[::-1]] return det[:, :6], det[:, :4]参数说明在这里。model.conf和model.iou是YOLOv5模型对象的属性,推理时内部直接使用,不需要每次传参。det[:, :4]是四个坐标,det[:, 4]是置信度,det[:, 5]是类别索引。为什么我要按置信度排序?因为DeepSORT的级联匹配内部会优先处理置信度高的目标,让高分目标先抢占轨迹,能减少低分误检对ID稳定性造成的影响。
检查代码运行效果有一个硬性指标:同一辆车的检测框,在连续30帧里要至少稳定出现25帧。如果只有15帧,说明置信度阈值太高或者模型本身对远距离小目标不敏感,这种情况不要急着调DeepSORT参数,回去看检测层才是正路。
2.3 让模型适配自己的场景:训练自己的数据集时超参数怎么定
很多毕设数据的场景和COCO差距不小,比如校园门口、夜间停车场、无人机俯视视角。在这些场景里,直接用官方yolov5s权重会出现明显的漏检。如果项目源码里提供了原始标注,建议还是训练一下。“yolov5训练自己的数据集”流程本身不复杂:用labelImg或labelme标注Pascal VOC格式,转成YOLO的txt格式,然后准备一个data.yaml:
train: ./dataset/images/train val: ./dataset/images/val nc: 2 names: ['person', 'vehicle']这里的nc和names必须和标注类别完全一致。如果只分行人和车辆两类,把所有车辆统一标成vehicle,可以大大降低训练难度。训练命令里,我习惯用--img 640 --batch 16 --epochs 100 --hyp hyp.scratch-low.yaml,前三个参数不新鲜,关键是超参数文件。默认的hyp.scratch-low.yaml已经够稳,不要一上来就改学习率;真正影响车辆行人项目的yolov5超参数是fl_gamma(focal loss系数)和mosaic(马赛克增强)。在夜间场景和密集人群下,把fl_gamma从默认值调大到2.0,小目标召回率会有可感知的提升。对计数项目,只需要盯住recall这个指标,不用管map精度多刷了零点几。
检测层的作用是:追踪和计数解决的是“同一目标是谁”,检测解决的是“有没有目标”。检测层没做稳,后面所有ID逻辑都是在错误的前提下盖楼。
3. 追踪层如何接管检测框:DeepSORT的卡尔曼滤波、级联匹配与参数调节
单帧检测已经输出框了,为什么还要追踪?因为视频分析里的“同一个目标”这个概念,单帧根本不存在。DeepSORT的作用是把每一帧检测结果按“身份”连接起来,为每个目标分配一个稳定ID。车辆行人追踪和计数的成败,本质上是让这个ID在目标整个生命周期内保持不变。
3.1 DeepSORT在项目管线里的位置:检测结果如何变成稳定ID
我见过不止一个毕设项目,把检测结果和追踪结果混淆:把当前帧每个框的xyxy传入DeepSORT的update函数,拿回来一堆tracks,然后把tracks画在帧上。方向是对的,但中间有坑。DeepSORT期望的输入不只是坐标,还要类别、置信度,以及当前帧原图。原图是用来裁剪目标区域、提取外观特征向量的。
from deep_sort import DeepSort deepsort = DeepSort( model_path="ckpt.t7", # ReID外观特征提取权重的路径 max_dist=0.2, # 外观特征余弦距离阈值 max_iou_distance=0.7, # 追踪框和检测框的IOU距离阈值 max_age=40, # 目标丢失后轨迹保留帧数 n_init=3, # 连续匹配成功几帧后确认新ID nn_budget=100 # 每个轨迹保留外观样本的数量 ) # 解析第2章得到的检测结果 # det[:, :4] 是 xyxy 坐标,det[:, 4] 是置信度,det[:, 5] 是类别ID x1, y1, x2, y2 = det[:, 0], det[:, 1], det[:, 2], det[:, 3] # DeepSORT实际使用的是中心点坐标加宽高 bbox_xywh = np.column_stack([ (x1 + x2) / 2.0, (y1 + y2) / 2.0, (x2 - x1), (y2 - y1) ]) confidences = det[:, 4] class_ids = det[:, 5].astype(int) tracks = deepsort.update( bbox_xywh, confidences, class_ids, frame_original # 当前帧BGR原图,不能传空图 )这里最需要强调的不是坐标格式,而是frame_original。很多复现到一半的人发现追踪效果和作者视频差很远,一对比代码才发现传进update的是一张经过letterbox填充后的归一化图,特征提取器输入的不是实际目标,追踪效果自然不行。代码里,max_dist控制外观特征匹配的容忍度,值越小,要求两张图越像;max_age控制一条轨迹丢失后保留的最长帧数,车辆场景40帧是在普通路口视频里比较稳的起点;n_init=3表示新目标要连续3帧都匹配成功才被确认,等于防止单次误检直接获得正式ID。
3.2 决定ID稳不稳的三个参数:max_dist、max_age和n_init
卡尔曼滤波在DeepSORT里做的事情很简单:用历史运动状态预测目标下一帧的位置。匹配阶段,用预测位置与实际检测框算IOU和外观余弦距离,放进代价矩阵,匈牙利算法在这个矩阵上找全局最优匹配。这就是ID能跨帧持有的原因。调max_age时,实际是在决定“预测”能撑多久。
初学者最常犯的错是同样一套参数用在所有场景。车辆行人的运动速度、遮挡程度、重叠频率差别很大,参数必须分开调。这里有一个典型参数表,是我在一个路口录像上反复试过的起始值:
| 参数 | 车辆场景 | 行人密集场景 | 说明 |
|---|---|---|---|
| max_dist | 0.3 | 0.5 | 行人穿着相似度高,阈值太低会频繁丢轨迹 |
| max_iou_distance | 0.5 | 0.7 | 密集人群重叠多,IOU阈值要放宽 |
| max_age | 25~40 | 50~60 | 车辆被遮挡恢复后位置变化大,轨迹不宜保留过久 |
| n_init | 3 | 2 | 行人目标小,检测不稳定,少等一帧确认更顺 |
先把这组参数套进去跑一遍,再按现场微调。注意一个原则:调参的顺序永远是先调检测置信度,再调max_age,最后才调max_dist。因为检测漏了会导致轨迹断,max_age是给断了的轨迹一个补救窗口,max_dist则是最后一道外观把关。如果先动了max_dist,很难分辨计数改善到底来自检测还是追踪。
3.3 DeepSORT改进方向:用外观ReID特征解决遮挡后的身份变化
“deepsort改进”在毕业设计里几乎是必问加分项,不是没理由的——原版DeepSORT在新版PyTorch上加载权重时常有问题,而且ReID特征模型本身对行人训练得多,对车辆外观的区分能力一般。两辆同型号、同颜色的车并排走过,外观特征几乎一样,仅靠外观匹配是不够的。常见做法是更换更强的ReID模型,或通过增大nn_budget让特征队列保留更多历史外观。但我的经验是,毕设里最划算的改进不是换网络,而是在前端过滤检测噪声:把第2章里的conf阈值从0.25提到0.4,并做一次轻量NMS,就能减少一半的ID跳变。追踪器的好坏和输入质量强相关,权重网络只是兜底。不要为了改进而改进,先用参数把现有链路压榨干,再考虑替换模型。
4. 从追踪到计数:跨线判定、去重与结果的落地实现
如果只是追踪,项目只能演示“框跟着人走”。毕设里要有数字输出,就得把轨迹变成计数。计数逻辑通常放在追踪返回tracks之后、画框之前,因为每一帧拿到的tracks里带有当前track_id和经过卡尔曼滤波平滑后的中心点,这是进行跨线判断的最小输入。
4.1 跨线计数的判定逻辑:用叉积符号检测轨迹是否穿过虚拟线
虚拟线是计数项目最常用的方案:在画面上定义一条线段,作为“门槛”。这条线通常划在路口或门禁位置,方向和车辆行进方向垂直。跨线判定我一般用叉积符号变化,而不是求点到直线距离后直接比较。原因很简单:车辆直线行进时,轨迹点可能离虚拟线很近但没有穿过去,距离法会误报。叉积法根据点在线的哪一侧来做判断,只有当两侧发生翻转时才认为是一次穿越,更自然。
import numpy as np LINE_START = (320, 540) # 虚拟线起点(图像像素坐标) LINE_END = (960, 540) # 虚拟线终点 def cross_value(point, line_start, line_end): """点在线段哪一侧,返回叉积值""" x, y = point xa, ya = line_start xb, yb = line_end return (xb - xa) * (y - ya) - (yb - ya) * (x - xa) def has_crossed(prev_point, curr_point, line_start, line_end): """上一帧和当前帧中心点分布在线的两侧,说明发生了穿越""" c1 = cross_value(prev_point, line_start, line_end) c2 = cross_value(curr_point, line_start, line_end) # 当前帧刚好落在线上的情况,等下一帧再看 if abs(c2) < 1e-6: return False return c1 * c2 < 0逻辑说明:cross_value计算的是向量叉积的z分量,正负号用于区分点在线段的哪一侧,绝对值大小用于衡量点到线的距离。函数里唯一要注意的是abs(c2) < 1e-6的兜底,轨迹点恰好压在虚拟线上时,当前帧不判定,等下一帧位置明确后再比较,避免反复横跳。
这里有一个隐形的调参点:绘制虚拟线时需要按原视频分辨率来定坐标,不要拿letterbox之后的尺寸去标。因为检测框坐标已经映射回原图,虚拟线也要在原图空间画,否则坐标错位会导致计数的穿越点系统性偏移,全部计数朝一个方向散开。验证方法也很简单:画线出图时,把线叠加到第一帧视频上,放大检查线的位置是不是真的横跨了目标打算走的路面。
4.2 解决“重复计数”:用Track ID加时间窗口锁定计数状态
重复计数是这类项目被答辩老师问得最多的一个点。同一个track_id穿过虚拟线只能计一次,这个逻辑大家都懂,代码也简单,但实际场景里麻烦的是:一辆车停了又走、走了又停,ID还在,账已经记了。如果只是死板地维护一个counted_ids = set(),目标ID被清空后再出现(比如离开画面很久重新接入),复用相同ID时就会被当成旧目标,造成漏计数。所以我的方案是用时间戳做去重:
count_history = {} # track_id -> (last_count_frame, direction) def try_count(track_id, frame_idx, direction): """只有当前ID没有在本轮计过数,而且没有被长时间锁定时才允许计数""" if track_id in count_history: last_frame, last_dir = count_history[track_id] # 同一ID在120帧内不重复计数,防止线附近来回震荡 if frame_idx - last_frame < 120: return False # 如果方向和上一次完全相同,说明只是抖动,不重复计数 if last_dir == direction: return False # 超过120帧没有再次出现,或方向已变化,允许重新计数 count_history[track_id] = (frame_idx, direction) return True这个函数的作用不只是去重,它还顺带处理了方向合并:同一个ID先朝A方向计了一次,之后如果反向穿越,按业务需求可能算第二次。这里的120帧是时间窗口参数,在30fps下对应4秒。停车场出入口前走走停停的车辆,4秒内不会重复计数;但真的掉头折返,超过4秒后就会被接受。参数值可以按场景调,监测拥堵路口时我会放大到300帧,而人流量统计场景120帧足够。计数方向的判定由叉积变化的符号决定:c1 > 0 and c2 < 0可以记为一个方向,反之为另一个方向。
4.3 计数结果怎么落地:OpenCV叠加、CSV和JSON导出
有了一套跨线判定,下一步是把计数值输出出来。毕设里通常要求两个形式:画面上的实时数字,和后处理用的结构化数据。OpenCV叠加是逐帧做的,放在画框的同一位置;CSV/JSON导出则要同时带上track_id和时间戳,方便后面做人工校验。
import csv import json from datetime import datetime # 叠加显示 cv2.putText(frame, f"IN: {count_in} OUT: {count_out}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) # 事件级导出,一行代表一次有效穿越 with open("count_log.csv", "a", newline="") as f: writer = csv.writer(f) writer.writerow([frame_idx, track_id, direction, datetime.now().isoformat()]) # 帧级别汇总,用于事后分析 summary = { "frame": frame_idx, "car_in": count_in, "car_out": count_out, "person_in": count_in_person, "person_out": count_out_person }这段代码里最容易被忽略的是时间戳:答辩时老师经常问“你的计数怎么证明是对的”,一份带帧号、track_id、方向的日志就是证据。CSV按事件导出的好处是每一行只对应一次穿越,不用在几千行帧级数据里捞数;JSON则是给后续可视化脚本用的。实际项目中建议用“事件级CSV + 帧级JSON”双写,CSV给人看,JSON给程序读。
注意:上面那段CSV写入是示例流程,真正运行时要确保
try_count返回True之后再写这一行,不要在每帧结束时遍历全部count_history,否则会把同一事件重复写多次。
5. YOLOv5+DeepSORT避坑指南:5个导致计数翻车的常见问题与排查
追踪计数项目能跑通的源码很多,能在不同场景下稳定计数的很少。下面5条是我认为最典型的踩坑记录,按现象、原因、解决的顺序说,每一条都可以直接对号入座。
5.1 一个行人被数成三个人:ID跳变引发重复计数
现象:视频里一个人从画面左侧走到右侧,输出total从1直接跳到3,检查跟踪log,发现同一瞬间出现的track_id在12、14、15之间来回切换。
原因:行人在行走过程中,展腿、背包或衣物遮挡会导致检测框出现间歇性丢失。检测一旦丢帧,DeepSORT的轨迹在n_init判定失败后死亡,下一次检测出现时重新创建了新ID,同一真实目标因此被数了三次。
解决:先回到检测层,把conf_thres从默认0.25提高到0.4,稳定单帧召回;再把DeepSORT的n_init从3降到2,让新ID更快建立;最后把max_age加大到50帧左右,给检测抖动预留缓冲。按这个顺序排查,大多数ID跳变问题在参数阶段就结束了,不需要动模型。
5.2 停在计数线附近的车被反复+1:轨迹点在线上震荡
现象:一辆车在停止线前减速停车,车辆并没有真正穿过虚拟线,计数值却在两秒内从1涨到5。
原因:车的中心点恰好压在虚拟线附近,帧间抖动使中心点在线的两侧来回跳动,叉积符号反复翻转,跨线判定被连续触发。
解决:两条路一起走。第一,try_count里加时间窗口,同一ID在120帧内只能最多计一次;第二,在has_crossed前加一个最小位移检查,只有当前帧中心点相对上一帧移动超过比如8像素,才认为是真实运动状态,静止抖动被过滤。位移阈值要用原图像素尺度,按视频分辨率上下调整。
5.3 环境配置的玄学:ReID权重加载报错与依赖版本冲突
现象:conda创建了Python 3.8环境,torch也装好了,yolov5命令行能正常出框,但运行到deepsort.update()时,torch.load()报权重文件格式错,或者sklearn版本对不上直接卡死。
原因:很多DeepSORT源码实现停留在torch 1.x时代,新装torch 2.x在加载旧checkpoint时会因为权重序列化格式变化报错;另外sklearn从1.2开始废弃了部分旧接口,追踪器内部调老接口会抛异常。这就是“yolov5环境配置”里最隐蔽的一层。
解决:把环境固定为python 3.8 + torch 1.13 + scikit-learn 1.0的兼容组合。如果项目已经在新版torch上跑通了yolov5,就不必把两个模型放进同一个环境,可以用两个虚拟环境分别跑,再用文件或消息队列交接检测结果。在树莓派或Windows上做开发时尤其值得这样隔离,避免一次牵动所有依赖。
5.4 目标消失很久再出现:max_age不是越大越稳
现象:一辆车在画面外停了8秒后重新进入路口,系统依旧沿用它原来的track_id,把这次进入当成了再一次穿过计数线,结果方向统计错乱。
原因:max_age设到200,意味着轨迹在目标消失后还能存活200帧。车辆重新出现时,外观ReID特征匹配评分和预测位置恰好过阈值,老ID被“复活”,最终它和真实的新目标占用了同一次计数机会。
解决:把车辆场景的max_age收到25~40帧,行人场景50~60帧。原因很朴素:车辆在路口被遮挡的时间通常不超过1秒,离开久了身份本来就该重建。判断max_age是否过大,有一个快速办法——把消失再出现那一小段单独剪出来,观察复活老ID后轨迹中心点是否突然跳了一大段距离,如果是,调小max_age即可。
5.5 部署到树莓派5这类边缘设备时帧率大跌:后处理是隐形瓶颈
现象:在PC上跑25fps的计数程序,部署到树莓派5之后掉到2fps,车辆已经过了线,计数画面还没跟上。
原因:模型本身确实慢,但更重要的瓶颈在Python侧。YOLOv5每次前向的letterbox填充、NMS,以及逐帧把批量结果从GPU搬回CPU再做后处理,这些开销在边缘设备上没有GPU加速时被完全放大,导致吞吐量骤降。
解决:面向边缘部署时,先把--img从640降到320,再把--weights换成yolov5n;把类别过滤从运行时过滤提前到模型解析后立刻截断,只保留person和车辆;然后将模型导出为TensorRT或ONNX,ONNX在树莓派上可以配合ONNX Runtime CPU后端运行。还有一个很有效的操作:关掉YOLOv5的detect.py入口,改为直接调用模型推理接口,避免每次重新创建结果对象带来的零碎开销。这套做下来,帧率通常能翻倍,计数结果虽然还会滞后半秒,但不再出现目标过线后才记录的事件。
6. 让计数值经得起复核:手工验证方法、轨迹尾迹调试与一条实用技巧
毕设答辩时,老师大概率会问“你这计数准吗”。代码里没有置信区间,所以要自己先做人工校验,并把校验手段带进演示流程。
6.1 用一段30秒视频做人工对照:记录帧号、方向与ID
选一段目标不太密集、能一眼数清的视频,放慢到0.5倍速,人肉记录一次穿越对应的帧号、方向、ID,与程序导出的CSV比对。不要只对数,要对ID:数值对得上但ID对不上,说明系统在靠错误补偿得到正确总数,后面换场景大概率翻车。
6.2 把轨迹尾迹画出来,一秒定位断点
调试阶段在每帧上画出每个track_id最近15帧的中心点连线,可以很快发现ID在哪个位置断开、又在哪里重新建立。这个方法比盯着一串控制台日志直观得多。
6.3 实用技巧:对中心点做轻量平滑,抑制抖动
smooth_pos = {} def smooth_center(track_id, cx, cy, alpha=0.3): """对追踪返回的中心点做指数平滑,供跨线判定使用""" if track_id not in smooth_pos: smooth_pos[track_id] = (cx, cy) return cx, cy px, py = smooth_pos[track_id] sx = alpha * cx + (1 - alpha) * px sy = alpha * cy + (1 - alpha) * py smooth_pos[track_id] = (sx, sy) return sx, sy这个平滑在计数判定的上游做,只影响轨迹中心点,不影响检测框坐标。我的习惯是,新场景先不看精度,先看连续30秒内每辆车的ID是否能全程稳定走完;ID稳定了再谈计数。用这套方法调过的项目,基本没有在答辩现场翻过车。希望帮到你。
本文还有配套的精品资源,点击获取