简介基于YOLOv5与DeepSort的车流量统计算法项目面向智能交通、视频监控及目标跟踪方向的开发者与研究者重点解决密集车流下车辆检测、跟踪与计数难题。资源共117个文件约79.17MB包含55个Python源码、20个YAML配置文件、预训练的PyTorch模型权重.pt、Dockerfile与相关部署脚本以及mp4/gif演示视频和docx版操作手册便于从环境构建到效果验证的端到端复现。项目已吸引288人学习具备较高参考热度。通过这套实战代码可掌握YOLOv5目标检测与DeepSort多目标跟踪的工程化整合方法理解检测框与跟踪ID关联、车流量统计阈值设定等关键细节适合作为毕业设计、课程项目或算法竞赛的基座也可按需扩展车型分类与车速估算能力。1. 车流量统计卡在哪密集车流下的一串乱跳的 ID做车流量统计的人都有过这种体验一段普通的城市路口视频车少的时候检测器基本靠谱一到早晚高峰车辆排队、跟车、互相遮挡全部涌上来同一辆车一分钟内能换三次 ID最终计数结果可能差出三成。这不是某个检测模型不行而是「能看见车」和「认得这是同一辆车」是两种能力。基于 YOLOv5DeepSort 实现的车流量统计算法正是把这两个能力拆开YOLOv5 负责在密集车流里把车框出来DeepSort 负责给每辆车一个固定 ID再用跨线逻辑统计。这套方案不挑高配设备一张入门级显卡就能完成训练和推理适合正在做智慧交通项目、毕业设计或路侧计数的工程团队直接抄作业。2. YOLOv5 DeepSort 的选型与架构为什么这个组合能扛住密集车流2.1 为什么不能只用检测器数车只用检测器做车流统计的硬伤在于检测器对每一帧独立推理帧与帧之间的目标没有任何关联。路口车辆排队时车距小、重叠多前车被后车挡住检测框可能在第 100 帧出现、第 101 帧消失、第 102 帧又出现统计逻辑会把它当成两辆车。另一个常见问题是车辆长时间停在画面里驶出画面又重新驶入这些都无法保持身份。DeepSort 解决的正是“把同一个目标认回来”。它维护轨迹和外观特征在检测框之间做数据关联。密集车流的隔帧漏检只要轨迹还在 max_age 保留期内就能接回去计数不会翻倍。一句话检测器负责“有没有车”DeepSort 负责“是不是同一辆”。这两个环节缺一个车流量统计在密集场景下都不可用。2.2 整体架构一帧视频变成一个计数的完整链路先亮出工程结构这是我从一个可运行的深度学习实战项目案例里整理出的最小目录traffic-counter/ ├── yolov5/ │ ├── detect.py # 官方推理入口我们只复用里面的模型加载 │ ├── data/vehicle.yaml # 类别文件 │ └── weights/best.pt # 训练好的车辆检测权重 ├── deep_sort/ │ ├── deepsort.py # 跟踪器封装 │ └── weights/osnet_x0_25_msmt17.pt ├── counter/ │ ├── count_line.py # 跨线计数逻辑 │ └── smoother.py # 计数防抖 ├── run.py # 主流程 └── data/video.mp4链路很简单读帧 → YOLOv5 检测 → 过滤类别为车辆 → DeepSort 更新 → 得到带 ID 的框 → 送入跨线计数器 → 输出平滑后的总数。主流程代码可以浓缩成下面这样import cv2 from yolov5_loader import load_detector from deep_sort import DeepSort from counter.count_line import CountLine from counter.smoother import SmoothedCounter # 1. 检测器加载训练好的权重固定置信度和 NMS 阈值 model load_detector(yolov5/weights/best.pt, conf0.25, iou0.45) # 2. 跟踪器DeepSort 初始化密集车流建议 max_age 调大 tracker DeepSort( model_pathdeep_sort/weights/osnet_x0_25_msmt17.pt, max_age45, n_init3, max_dist0.35, ) # 3. 跨线计数器与防抖器 line CountLine(p1(0, 400), p2(1920, 400)) smoother SmoothedCounter(window10) cap cv2.VideoCapture(data/video.mp4) while True: ret, frame cap.read() if not ret: break dets model(frame) # 检测结果: [x1, y1, x2, y2, conf, cls] boxes, confs, clss filter_vehicle(dets) # 只保留车辆类别 ok, outputs tracker.update(boxes, confs, clss, frame) if ok and outputs is not None: for x1, y1, x2, y2, track_id in outputs: line.check(track_id, (x1, y1, x2, y2)) total smoother.update(line.total) cv2.putText(frame, ftotal: {total}, (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(frame, frame)filter_vehicle 的作用是把类别限定在 car、bus、truck、motorbike避免行人影响计数DeepSort 的 update 接收检测框列表、置信度、类别和原图原图用于提取外观特征。line.check 内部维护每个 track_id 的跨线状态smoother 输出的 total 才是最终要上屏的数字。这段链路已经足够跑通一个最小可运行版本后面章节再逐个调参数。2.3 YOLOv5 的模型选型先选 s 还是 m面向车流量统计最稳妥的是 yolov5s 起步跑通后再看是否换 yolov5m。密集车流的难点是远处排队的小车s 模型在 1280 输入下对小目标召回略弱m 模型明显更稳但推理时间多 30% 到 50%。我的经验是项目先按 yolov5m 训练如果上线设备是普通显卡再用 s 的权重重新训练如果是边缘设备两个都不合适得走 TensorRT 量化。选型还要看 YOLOv5 网络结构图它的 backbone 是 CSPDarknet颈部用 FPN PAN 做多尺度特征融合让近处大车和远处小车都有一层特征图覆盖这正好对车流场景有效。很多远处排队的小车在特征图上只有几十个像素如果模型下采样倍数太高这些目标基本就没了。密集车流里我更关注大特征层的质量它直接决定小目标召回率也决定后续 DeepSort 能拿到多少有效检测框。提示先跑通 s 模型再根据密集小目标漏检率决定是否升级到 m 模型不要一开始就在 m 模型上死磕超参。3. YOLOv5 训练自己的数据集标注、超参数与后处理全为密集车流改3.1 数据准备从公开车辆数据集到自己补标注用 YOLOv5 训练自己的数据集时常见做法是用 UA-DETRAC、BDD100K 这类公开车辆数据先做预训练再到自己的路口场景补数据。重点是采样策略不要随便抽帧要专门把“排队”“跟车”“高峰期”“夜间”的片段抽出来保证密集车流的样本占比。标注软件用 labelImg 或 Labelme 都可以关键是标注规范。标注原则有两条第一被遮挡的车辆也要标标它的可见部分不要因为挡住一半就不标第二紧挨在一起的两辆车框的边界要贴合车辆本身框太大或太小都会把 NMS 搞乱。数据文件写到 vehicle.yaml# data.yaml train: /workspace/datasets/vehicle/train val: /workspace/datasets/vehicle/val nc: 4 names: [car, bus, truck, motorbike]不建议把行人放进去路侧视角行人路过会引起跟踪器 ID 混乱后面还要在 filter_vehicle 里加名单排除不如训练时就不学。3.2 训练超参数与命令把 yolov5 超参数落在车流场景用 YOLOv5 官方仓库的接口训练我常用的命令是cd yolov5 python train.py \ --data data/vehicle.yaml \ --weights yolov5m.pt \ --img 1280 \ --batch 16 \ --epochs 100 \ --multi-scale \ --cache几个关键参数--img 1280 是为了提升远处小车和密集排队小目标的召回640 会漏得厉害--multi-scale 让模型在训练时随机缩放输入等于对遮挡和尺度变化做数据增强--batch 视显存而定8G 左右跑 yolov5m 建议降到 8。训练过程中盯住 val/mAP_0.5:0.95密集车流场景下 AP50 达标不算难难的是小目标的 AP_S如果 AP_S 偏低后处理再努力也救不回来。超参数文件我一般会在官方 hyp 基础上做两处调整mosaic 保持开启copy_paste 提高到 0.2 左右。密集车流的车辆之间相互遮挡copy_paste 能人为制造“半辆车”的训练样本让模型学会被挡住时也给出完整框。这里有个玄学点anchor 不要随便动先用 autoanchor 算一遍如果推荐的 anchor 尺寸比例和车辆尺度差很多再考虑改。3.3 检测后处理置信度和 NMS 的反直觉调法训练完成导出 best.pt 后推理时真正影响结果的是 YOLOv5 后处理里的置信度阈值和 NMS 阈值python detect.py \ --weights runs/train/exp/weights/best.pt \ --source data/video.mp4 \ --conf-thres 0.20 \ --iou-thres 0.45 \ --img 1280 \ --classes 0 1 2 3conf-thres 在密集车流里不要设太高。很多从开源项目直接抄作业的同学用 0.4 甚至 0.5结果是远处小车全被过滤掉晚高峰的统计量直接少两三成。0.20 左右能保住大部分被遮挡的小车增加的误检由跟踪器的外观匹配再过滤一层。iou-thres 反而要相对低一点NMS 用的 IOU 阈值在密集车流如果设 0.6 以上两辆紧挨的车框高度重叠NMS 可能把其中一辆吃掉0.45 会让保留框更宽松。如果还是漏可以打开 --augment 跑推理用 TTA 换召回代价是速度掉一半我只在出最终结果时用。4. DeepSort 跟踪与跨线计数参数设置和防抖逻辑4.1 DeepSort 核心参数与改进方向把 ID 切换问题按回去DeepSort 算法同时使用运动特征和外观特征做关联。运动特征用卡尔曼滤波预测轨迹下一帧位置外观特征用一个小型 ReID 网络提取目标的向量。密集车流场景下两者缺一不可光用运动特征车一互相遮挡就串光用外观特征同色车辆一多又容易误匹配。初始化代码与核心参数如下from deep_sort import DeepSort tracker DeepSort( model_pathdeep_sort/weights/osnet_x0_25_msmt17.pt, max_dist0.35, # 轨迹与检测框的级联匹配门控距离 max_age45, # 轨迹丢失后保留帧数密集遮挡场景尽量大 n_init3, # 轨迹确认需要的连续命中帧数 max_iou_distance0.7, max_cosine_distance0.3, nn_budget100, # 外观特征库容量 )不同开源实现的 DeepSort 接口字段名略有差异有的叫 max_dist有的叫 max_cosine_distance实际落地时按你引入的仓库来映射参数含义是一致的。max_dist 控制轨迹与检测框之间的最大门控距离密集场景我建议 0.35 左右设成 0.2 会很严格车辆被挡一帧后轨迹就接不上ID 直接中断。max_age 是轨迹丢失后的保留帧数默认 30 对密集车流不够路口大车挡住小车三四秒是常事给到 45 到 60 更保险。n_init 是轨迹被确认前必须连续匹配的帧数偏大能过滤闪烁的误检轨迹但真实车辆刚进画面的头几帧不会被计数3 是比较平衡的值。nn_budget 是特征库容量100 够用别为了省内存设太小同车型车辆一多历史特征不足会明显降低匹配效果。想进一步做 DeepSort 改进常见做法是替换外观特征提取网络比如把默认的轻量 ReID 换成 osnet_x1_0 或带注意力机制的 backbone。更强的主干网络提出的特征更稳定ID Switch 在密集车流下能再降一截代价是推理帧率下降落地时要先测 FPS 再决定。4.2 跨线计数用检测框底部中心判断越线计数逻辑不复杂但细节决定误差。我一般不用框中心点作为跨线参考点而是用检测框底边中点import numpy as np class CountLine: def __init__(self, p1, p2): self.p1 np.array(p1, dtypenp.float32) self.line np.array(p2, dtypenp.float32) - self.p1 self.counts {} # lane_id - 计数值 self.last_side {} # track_id - 上一次所在侧 self.total 0 def check(self, track_id, bbox, lane_id0): # 用检测框底部中心的像素坐标 bottom_x (bbox[0] bbox[2]) / 2.0 bottom_y bbox[3] pt np.array([bottom_x, bottom_y], dtypenp.float32) # 用叉积判断目标在线的哪一侧 side np.sign(np.cross(self.line, pt - self.p1)) last self.last_side.get(track_id, 0) if last ! 0 and side ! 0 and side ! last: self.total 1 history self.counts.get(lane_id, 0) self.counts[lane_id] history 1 self.last_side[track_id] side return self.total为什么用底边中点车辆检测框的底边贴近路面透视变形小越线瞬间更接近真实物理位置框中心会随车辆遮挡左右漂移底边相对可靠。判断逻辑是记录每个 ID 上一次在线的哪一侧符号发生变化才算越线避免车头在线上来回摆动反复计数。多个 track_id 共用这个类时计数要按 lane_id 分桶后面做车道级统计会方便很多。4.3 计数防抖为什么不能直接显示 total密集车流里 ID 还是可能抖动尤其是大车遮挡小车后重新出现的场景。直接把 total 画到管理方大屏上会出现“数字退回去”“数字跳加两次”的现象。常见做法是用滑动窗口取中位数from collections import deque class SmoothedCounter: def __init__(self, window10): self.window window self.history deque(maxlenwindow) self.last_value 0 def update(self, value): self.history.append(value) arr sorted(self.history) self.last_value arr[len(arr) // 2] return self.last_value窗口长度 10 表示约 0.3 到 0.5 秒的延迟按 25 到 30 FPS 算管理方大屏完全能接受。逻辑说明单帧误增会被历史中位数吃掉车辆真实通过后数值会平稳上升而不是跳变。如果报表需要更严格的防重还可以在 CountLine 里加一道 pending 机制车辆跨线后不立即加一要求该 ID 连续出现且位置越过线保持 3 帧以上再确认这样能把误触发压到最低代价是延迟多几帧。5. 密集车流场景避坑指南五个高频翻车点与现场排查5.1 现象同一个 ID 一分钟内跳了七八次计数虚高现象是大屏上 total 一直涨回放发现一辆车被数了两次以上。原因是检测器在密集车流里帧间框抖动DeepSort 关联失败或者 max_dist 设太紧遮挡一帧就丢轨迹重新匹配时分配了新 ID。解决办法是把 max_dist 放宽到 0.35max_age 提到 45 以上检查检测框是否频繁闪烁如果是提高输入分辨率到 1280 并重新训练有条件就换更强的 ReID 权重。改完先跑同一段 30 分钟视频对比 ID Switch 次数正常应低于总目标数的 5%。5.2 现象排队停住的车起步后被重复计数路口红灯排了一排车绿灯起步后 total 莫名多了好几辆。原因是车辆静止时检测框一直存在DeepSort 的轨迹也一直存活车辆一旦起步如果中间丢失过轨迹重新分配 ID跨线逻辑会把它当成新车。解决思路是给计数加冻结机制class CountLine: def __init__(self): self.total 0 self.freeze_until {} # (track_id, lane_id) - 冻结结束帧号 def check(self, track_id, lane_id, frame_id): key (track_id, lane_id) if frame_id self.freeze_until.get(key, 0): return False self.total 1 # 同一辆车在计数后 90 帧内不允许再次计数 self.freeze_until[key] frame_id 90这里的 key 用 (track_id, lane_id) 是为了防止车辆骑线时在两个车道内被各计一次。90 帧大约是 3 秒对“排队停了一整轮才重新起步”的场景并不够更彻底的做法是把冻结绑定在物理位置上车辆越过线后继续前进并且一段时间内没在计数线附近重新出现才解除冻结。上面这段展示的是思路实际实现要结合你的计数线位置和车速来定帧数。5.3 现象夜间和阴天把路灯、路牌当车白天计数正常夜间误检一大堆total 虚高的现象很常见。原因是 YOLOv5 训练数据里缺乏夜间样本路灯、反光标识在暗背景下轮廓和车灯接近置信度超过阈值被当作车辆送入跟踪器甚至产生真实 ID。解决方向是训练数据按 20% 左右比例加入夜间段视频用亮度抖动、对比度抖动做数据增强推理时把 conf-thres 适当提到 0.25 到 0.30。路灯、路牌这类静态目标特征明显它们不会越过计数线但会增加 ID 数量拖慢跟踪器。可以在跟踪后按目标平均速度过滤低于某个像素/帧阈值的静态轨迹直接丢弃。5.4 现象大车挡住小车重新出现后计数翻倍一辆公交过线时挡住身后的小车公交车过去后小车被当成新车计数。原因是遮挡期间小车的轨迹被判定丢失重新匹配后分配新 ID跨线统计无法知道它之前已经过半。把 max_age 调大是第一步但遮挡超过 60 帧就要靠计数逻辑兜底对每个 ID 记录历史轨迹末点的位置新 ID 出现时如果在计数线附近且外观特征与已消失的旧 ID 相似度很高可以合并到旧 ID 的计数资格。更稳妥的方案是不依赖单条虚拟线把计数区域延长为一条带车辆全程在带内保持同一个 ID 就算一次。5.5 现象推理速度越来越慢视频开始延迟开头流畅跑十几分钟后延迟越来越大这个问题在边缘设备上尤其明显。原因有两种一是跟踪器内部维护的轨迹数量不断累积静态误检目标没有及时回收二是检测输入过大、后处理 TTA 打开GPU 负载持续过高。解决方法是给 DeepSort 加轨迹回收逻辑# 每 30 帧清理一次久未更新的轨迹 # 不同 deep_sort 实现的内部接口名有差异以下为常见写法 for track in tracker.tracker.tracks: if track.time_since_update 60: tracker.tracker.deactivate(track)如果目标设备是树莓派这类边缘盒子模型导出为 TensorRT 或 ONNX 后分辨率通常要降到 640 甚至 512密集车流的检测召回会下降这时要靠跟踪器兜底而不是硬扛精度。我的排查顺序是先看 GPU 占用再看 tracker 内部轨迹数日志最后才动模型。6. 进阶技巧车道级统计与车速估计把计数做成一份能交付的报表6.1 车道级统计把 ROI 分区绑定到车道上只有总数很难满足路侧管理方的需求他们要的是每个车道的通过量。做法是在画面上标出每个车道的多边形判断检测框底部中心落在哪个区域再按车道分桶计数。CountLine 里已经按 lane_id 统计了这里只需要加一个点映射def point_to_lane(pt, lane_polys): # lane_polys: {lane_id: 多边形坐标数组} for lane_id, poly in lane_polys.items(): if cv2.pointPolygonTest(poly, pt, False) 0: return lane_id return -1lane_polys 的坐标要和原图分辨率一致视频分辨率变了要重新标定。密集车流下车辆骑线行驶时用检测框底部中心点比用框中心更接近车道线主流做法是每个车道设一条独立虚拟线骑线车最多只在两个车道里选择一次不会重复计数。6.2 从跟踪轨迹估算车速车流量统计配上平均车速就是一份比较完整的交通流报表。最简单的方法是在画面上选两个物理距离已知的点记录同一个 ID 通过两个点的帧数差再换算车速。更通用的是利用 DeepSort 输出的轨迹坐标直接算近期平均速度def estimate_speed(track_history, fps, scale1.0): # track_history: 该 ID 最近 5 帧的 [(x, y)] 列表 if len(track_history) 5: return 0.0 dist_px np.linalg.norm(np.array(track_history[-1]) - np.array(track_history[0])) t (len(track_history) - 1) / fps return dist_px * scale / t if t 0 else 0.0scale 是像素到实际距离的换算比例需要现场标定。注意车速估计只对轨迹质量较好的目标可靠遮挡严重的 ID 算出来的速度跳变很大报表里要加一个置信度字段否则甲方会拿着异常数据来核对。报表侧我一般用 Django 起一个只读接口把车道流量和平均车速按 15 分钟聚合一次返给前端大屏这种前后端分离的做法在项目实战里很常见但接口只消费结果文件不要让它直接去跑检测线程前后端解耦后整个系统才不容易被拖垮。我自己的习惯是交付前把计数结果、跟踪 ID、车速记录一起写进 CSV再抽两段视频逐帧回放核对。车流量统计这个方向八成的翻车都出在数据和参数上很少是模型结构问题。把检测、跟踪、计数三段各自的边界摸清密集车流下的误差就能压到可交付范围。希望帮到你。本文还有配套的精品资源点击获取