ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于深度学习的交通流量检测系统:YOLOv8+DeepSORT实战全解析

基于深度学习的交通流量检测系统:YOLOv8+DeepSORT实战全解析 简介基于深度学习的交通流量检测系统毕业设计项目面向计算机、人工智能及相关专业学生旨在利用 CNN/RNN 等模型对监控视频中的车辆数量、速度与交通状况进行自动监测和分析。压缩包共含2000个文件总大小130.5MB其中 JavaScript 文件多达1411个另有415个 Markdown 文档和171个 JSON 配置以及少量 HTML 与 CSSJS 多用于前端交互及可视化展示MD 文档可用于快速了解设计思路与使用方式。目前已有666人浏览学习说明其内容具备一定参考价值。项目覆盖数据预处理、模型选择与训练、性能评估到系统集成等关键环节代码中实际应用了 ECharts 与 Element Plus 等前端框架能够清晰展示深度学习检测结果的可视化流程同时大量 Markdown 文档提供了设计说明与使用指引便于读者快速上手适合用于毕业设计参考、功能扩展或复试作品准备。1. 一份《基于深度学习的交通流量检测系统.zip》摆在面前时先看这三件事毕设季最常被问到的一句话是“老师这个 zip 怎么跑不起来”。标题里写着“基于深度学习的交通流量检测系统.zip”的交付包解压后通常是三层目录数据集、模型源码、已经训好的权重文件。学生拿到手的第一反应是直接双击 train.py结果要么 CUDA 版本对不上要么数据集路径写死要么跑完发现没有输出视频。我的建议是先看项目里有没有README.md再检查数据集是 VOC 格式还是 YOLO 格式最后确认权重文件的后缀名——这三件事决定了你要花半小时还是两小时。这份项目要解决的问题很直接从路侧摄像头画面里把车检测出来、跟踪住然后输出“这条路上每分钟过了多少辆车”。检测模型负责框出车辆跟踪模块负责把每个车框串成轨迹最终统计模块按时间窗口累加。它适合两类人一是正在做毕业设计、需要一套能演示还能写进论文的完整方案的学生二是刚接触视觉落地、想看看真实项目里检测器和跟踪器是怎么配合的工程师。这文章按我从数据准备到统计实现再到排障的思路来讲每个环节给可抄的代码和参数踩过坑的位置会直接标出来。2. 技术选型为什么是 YOLOv8 DeepSORT以及流量检测和普通目标检测的区别2.1 检测器只解决“车在哪里”流量统计依赖跟踪 ID两套模型的分工先说一个新人最容易绕进去的弯。用深度学习的模型做交通流量检测核心难点不在“检测”而在“跟踪”。YOLO 这类检测器每一帧都是独立的它对视频里的车辆没有记忆——上一帧框住一辆白色轿车这一帧它不知道这辆车还是不是同一辆。而流量统计的本质问题恰恰是“同一辆车不能数两次不同车要分别计数”这就必须有跟踪模块。常见做法是检测器加跟踪器的两段式架构检测器每帧输出所有车辆的边界框、类别和置信度跟踪器把这些框和已有的轨迹做匹配给每条轨迹分配一个唯一的 track_id。DeepSORT 是这类方案里最通用的选择它在卡尔曼滤波预测位置的基础上用外观特征和运动特征做级联匹配对遮挡和短期消失有一定容忍度。选型理由也很朴素YOLOv8 在 RTX 3060 上跑 s 模型能做到几十帧DeepSORT 论文代码成熟两个模型的耦合方式有大量现成案例不挑环境毕设文档里也写得出“算法对比”这一节。这里有一个必须明确的边界检测精度和跟踪精度是两个指标别混为一谈。检测模型的 mAP 只说明“框得准不准”不代表“数得对不对”。我见过一份交付包检测 mAP 0.87看起来很体面但在实际视频里一个弯道处每辆车 ID 都在 30 帧内跳变结果每分钟车流量比人工数多出 40%。第四章会讲怎么用 track_id 把统计做稳这里先记住这句话流量检测系统里检测是地基跟踪是承重墙统计是装修。2.2 数据集从哪来UA-DETRAC 与自采路侧视频的取舍训练检测模型的数据集是另一个决策点。公开数据集里UA-DETRAC 是交通场景常用的选择它拍摄的是真实路口的视频帧包含轿车、公交车、货车等类别遮挡和天气情况覆盖比较全。另一个选择是 BDD100K夜间和雨雾场景更丰富但类别多、标注风格偏语义任务直接拿来训检测器需要做过滤。如果是做毕设我更推荐先用 UA-DETRAC 跑通流程再用自己录制的路侧视频做补充测试。自采数据这件事不同学校条件差别很大。有条件的用三脚架架相机在路口拍一小时取不同时段的 2000 到 3000 帧做标注没条件的从公开数据集里抽帧生成自己的子集。要注意的是标注格式的统一。很多公开数据集是 VOC 风格每张图配一个 XML 文件框是左上角和右下角坐标而 YOLO 系列训练需要 txt 文件内容是“类别编号 中心点x 中心点y 宽 高”四个值全部除以图片宽高做归一化。这种格式差异是解压 zip 后最常见的卡点第三章会给转换脚本。如果项目包里已经带了训练好的.pt权重文件通常意味着作者默认你“直接推理”不从头训练。这时数据集目录往往只是验证用。即便如此我还是建议你花半天把数据准备流程走一遍——答辩老师最爱问“你的模型数据怎么来的标注了多少张”没有这个过程答案会很虚。2.3 解压后先修目录处理“绝对路径写死”这个毕设通病很多 zip 交付包跑不起来的根本原因不是代码错而是路径写死。训练脚本里写着dataset /home/ubuntu/project/VOCdevkit换到你的 Windows 机器上必然报错。我拿到任何一个项目包第一件事是全局搜索.py文件里的盘符和/home/路径全部改成相对路径或Path(__file__).parent拼接。另一个常见问题是数据集目录结构和 YOLO 预期的images/train、labels/train不一致。YOLOv8 的yaml配置文件里train和val字段指向的是“图片所在目录”标签目录会自动按同级labels寻找图片文件名必须和标签文件名一致扩展名不同。检查点通常有三个图片是不是都被imread读得到、标签里的类别编号是不是从 0 开始、有没有空标签文件。这三个问题占了训练启动报错的七成。3. 训练你自己的检测模型VOC 转 YOLO 格式、训练命令与六组必调参数3.1 环境准备torch 与 CUDA 版本匹配的最小命令深度学习环境配置是解压 zip 之后的第一道坎。YOLOv8 基于 PyTorchPyTorch 和 CUDA 版本不匹配时最常见报错是NVIDIA GeForce RTX 3060 with CUDA capability sm_86 is not compatible。我一般建议先创建独立 conda 环境再按显卡驱动版本反推 CUDA 版本。conda create -n traffy python3.10 -y conda activate traffy # 先查驱动支持的 CUDA 版本nvidia-smi 右上角显示的 CUDA Version 是“驱动支持的上限” # 这里安装的是 PyTorch 预编译版自带 CUDA 运行时不另外装 CUDA Toolkit pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python逻辑说明cu118表示 CUDA 11.8 的预编译版本如果你的驱动上限是 12.x也可以换cu121安装包如果机器没有 NVIDIA 显卡把--index-url这一段去掉即可安装 CPU 版但训练时每轮迭代慢 10 倍以上不推荐用来训模型。安装成功后可以用python -c import torch; print(torch.cuda.is_available())验证输出True才说明 GPU 链路通了。参数说明里的门道在于python3.10是 Ultralytics 在 2024 年后官方测试覆盖较全的版本Python 3.12 在部分依赖特别是lap库上会编译失败而 DeepSORT 的lap依赖是后期跟踪模块必须装的。如果你解压的项目里要求lap0.4Windows 上大概率没有预编译 wheel需要装 Visual Studio Build Tools。这个坑放在第五章专门讲这里先提示。3.2 VOC 标注转 YOLO 格式脚本、归一化换算与目录陷阱假设数据集解压后是 VOC 风格目录结构是JPEGImages/、Annotations/、ImageSets/Main/。YOLO 训练要求图片和标签分离图片放images/train标签放labels/train。下面这段脚本把 VOC 的 XML 解析出来换算成 YOLO 需要的归一化 txt。import xml.etree.ElementTree as ET import os, random # 类别映射按你的数据集实际类别改 CLASS_MAP {car: 0, bus: 1, truck: 2, motorbike: 3} def voc2yolo_one(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAP: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 归一化中心点坐标除以宽高框宽高同样除以宽高 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) img_name os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, img_name .txt), w) as f: f.write(\n.join(lines)) # 用法把 JPEGImages 和 Annotations 整理为 # dataset/images/train、dataset/labels/train 两个平级目录 xml_dir Annotations out_dir labels/train os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): voc2yolo_one(os.path.join(xml_dir, xml_file), out_dir)逻辑说明XML 里存的是绝对像素坐标YOLO 需要的是 0~1 之间的相对坐标核心换算就是(x1x2)/(2*width)和(x2-x1)/width。坐标系如果算错训练出来的框会整体偏移表现形式是检测框比车大一圈或偏到车的一侧mAP 看着还行但实际位置不对。参数说明CLASS_MAP的编号必须从 0 开始连续排类别列表和训练用的data.yaml里的names顺序要一一对应。常见翻车点是把car编成 1 号同时data.yaml里names: [car]只有一项导致训练时所有标签都报索引越界。类别编号在 YOLO 体系里没有“从 1 开始更合理”的说法一切以names的索引为准。3.3 YOLOv8 训练参数epochs、imgsz、batch 与早停数据准备好之后训练命令本身不复杂但参数的影响差别很大。我常用的是下面的配置按 3000 张图、单卡 RTX 3060 12G 来设。yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs60 \ imgsz640 \ batch16 \ optimizerAdamW \ lr00.001 \ patience10 \ projectruns/traffic_detect \ nameexp1逻辑说明modelyolov8s.pt不是从头训练而是加载在 COCO 上预训练过的权重做迁移学习这是深度学习项目里最能白嫖的加速手段。预训练模型已经学会了边缘、纹理、车辆轮廓这些通用特征在交通数据上只需要微调后面的特征层。epochs60对 3000 张图来说是足够的再往后容易过拟合patience10表示在验证集 mAP 连续 10 轮没有提升时自动停止这比手动盯曲线省心得多。参数说明一比一讲清楚imgsz640是训练时缩放后的输入分辨率不是原始图分辨率。640 是精度和显存占用的平衡点如果数据里有大量远端小目标可以加到 960但显存占用接近翻倍batch 要相应减半。lr00.001对 AdamW 偏保守但稳定初学者不建议动它SGD 的话可以设 0.01。训练过程中注意看终端里的box_loss和cls_loss如果验证损失在 30 轮后持续上升而训练损失还在下降就是过拟合信号这时该做的是减少 epochs 或者往数据里加增强不是继续堆轮数。这里还要补一句很多人喜欢直接在yolo命令里调一堆增强参数比如hsv_h0.015、mosaic1.0。我的习惯是默认增强先不动只有当验证集表现和实际视频表现差异极大时才去改。交通场景的车辆外观相对统一强增强有时反而让模型在真实路况下不稳定这一点书里写得少实践经验更重要。4. 从检测框到“辆/分钟”虚拟计数线、track_id 去重和车流统计实现4.1 虚拟计数线的实现叉积判断车辆进出方向检测模型训练完成后进入系统核心的统计阶段。统计逻辑要用三条规则约束同一个 track_id 只计一次数车辆必须按行驶方向穿越预设的计数线计数线两侧的缓冲区要足够大避免在边缘反复触发。常用做法是“虚拟计数线”——在画面里定义一条线段当车辆中心点从线的一侧移动到另一侧时判定为一次穿越。实现这个判断不需要计算点到线的距离用向量叉积的符号变化即可。import numpy as np # 计数线由两个端点定义A 是起点B 是终点 LINE_A np.array([320, 240]) LINE_B np.array([960, 240]) def cross_product(p, aLINE_A, bLINE_B): # 向量 AB 与向量 AP 的叉积符号表示 P 在线段的哪一侧 ab b - a ap p - a return ab[0] * ap[1] - ab[1] * ap[0] def should_count(prev_center, curr_center): # side 从正变负或相反说明中心点穿过了计数线 side_prev np.sign(cross_product(prev_center)) side_curr np.sign(cross_product(curr_center)) return side_prev ! side_curr逻辑说明叉积的结果本质上是一个有向面积它的正负代表点在由向量 AB 构成的直线的哪一侧。当车辆中心点从上一帧的正侧变到当前的负侧说明它越过了这条线。这个判断对倾斜的计数线同样有效不需要算出垂足位置计算量小适合每帧对每个跟踪目标都跑一次。参数说明计数线的位置和角度直接决定统计口径。线不要贴近画面边缘否则车辆刚入画就被计数跟踪还没稳定容易造成重复计数。实际调试时线上方和下方各留 30 到 50 像素的缓冲只有目标连续两帧都在同一个侧面时才开始记录能显著减少抖动导致的重复触发。4.2 用 max_age / n_init 抑制 id 跳变DeepSORT 调参DeepSORT 里最影响统计准确性的参数是max_age、n_init和max_dist。n_init是“一个新检测框被连续匹配多少帧才确立为新轨迹”默认 3max_age是“轨迹丢失后最多保留多少帧等待重新匹配”默认 30max_dist是外观特征匹配的代价阈值默认 0.2。在车流密集场景里max_dist0.2往往太严格。车辆外观相似度高遮挡和光照变化会让同一辆车的外观特征距离超过阈值导致新的 ID 不断生成。我一般会放宽到 0.3~0.4同时把max_age调大到 40 或 50让轨迹在车辆被遮挡的几帧里活下来。代价是如果两辆车确实不同但外观极像跟踪可能把它们的轨迹连成一条——这就是“跟踪漂移”。from deep_sort_realtime.deepsort_tracker import DeepSort # 参数按路侧交通场景调整过直接抄作业可以用 tracker DeepSort( max_age45, # 车辆被遮挡/漏检时轨迹保留 45 帧等待重连 n_init2, # 连续匹配 2 帧就确认为正式轨迹 max_dist0.35, # 外观特征匹配阈值调大防止密集场景 ID 跳变 embeddermobilenet # 轻量特征提取器CPU 也能跑 )逻辑说明max_age和n_init是此消彼长的关系。n_init设太小误检的框会被当成新轨迹产生大量无效 ID设太大真正的新车要等好几帧才被计入。路侧场景里我更倾向n_init2因为车辆进入画面后移动速度快等太久车辆已经走到计数线附近提前给了新 ID。max_dist一栏的参数和检测器的置信度阈值直接关联如果检测器输出的框置信度普遍偏低说明特征质量差这时调大max_dist是治标回头去提升检测模型置信度是治本。参数说明里最容易踩坑的是“用了embedder但没装tensorflow程序直接报错”。DeepSORT 的官方实现是 TensorFlow 的 Inception 特征提取器而deep_sort_realtime这个库支持切换成mobilenet。如果你的机器没有 TensorFlow 环境换embeddermobilenet之前先检查依赖是否完整更省事的办法是选择纯 PyTorch 的跟踪实现比如BoxMOT但性能上 DeepSORT 的历史积累更厚论文里也好引用。4.3 统计结果怎么算按时间窗口输出辆/分钟与道路占有率有了穿越计数线的判断和稳定的 track_id统计变成按时间窗口累加的工作。流量统计的单位一般有两种绝对流量辆/分钟和道路占有率单位时间内车辆占用的时间比例。前者用计数线穿越次数除以窗口时长后者需要知道车辆在检测区域内停留的时间用所有轨迹在窗口内的存活帧数总和对总帧数求比值。class TrafficCounter: def __init__(self, window_seconds60, fps25): self.window_seconds window_seconds self.fps fps self.window_frames window_seconds * fps self.frame_count 0 self.crossed_ids set() self.history [] def update(self, tracks): # tracks 是 DeepSORT 输出的 [(bbox, track_id, ...)] 列表 self.frame_count 1 for bbox, track_id in tracks: center ((bbox[0] bbox[2]) / 2, (bbox[1] bbox[3]) / 2) if should_count(center, track_id): if track_id not in self.crossed_ids: self.crossed_ids.add(track_id) # 每个时间窗口结束记录一次并清空统计集合 if self.frame_count self.window_frames: volume len(self.crossed_ids) self.history.append(volume) self.crossed_ids.clear() self.frame_count 0 print(f过去 {self.window_seconds} 秒流量: {volume} 辆)逻辑说明crossed_ids用集合结构做去重这是流量统计里最简单有效的手段——同一个 track_id 无论穿越计数线多少次只记一次。窗口结束时清空集合开始下一个 60 秒的统计周期。这里的假设是车辆不会在窗口内反复穿越同一条线比如掉头如果是大型路口需要额外记录穿越方向只在固定方向穿越时才累加。参数说明窗口大小的选择取决于你要统计的断面类型。城市路口一般用 5 分钟或 15 分钟的聚合值因为单分钟波动太大60 秒窗口里红灯等待会造成长时间零流量视觉上不好看。答辩演示建议同时展示“实时流量曲线”和“累计柱状图”把history里的数据喂给 matplotlib 或前端图表比终端打印数字有说服力得多。5. 避坑交通流量检测常见的五个翻车场景及排查思路5.1 小目标漏检远端车辆“没框”现象视频里近处车辆框得挺好远处路口的车完全没框流量统计比实际少三成。原因路侧摄像头的透视效应导致远端车辆在画面里只有 20x20 像素左右而训练时的imgsz640把整张图等比缩放小目标直接被“缩没了”。深度学习模型对小目标的敏感度天然低于中大型目标这是特征提取层的局限。解决最常见做法是提升推理分辨率。推理时把imgsz设到 960 或 1280如果显存不够只对远端区域做一块 ROI 裁切后单独推理再把框映射回原坐标。另外一个效果不错的方法是在训练数据里手动复制粘贴小尺寸车辆增加小目标样本占比。注意别掉进另一个坑——调高推理分辨率后归一到 640 训练过的模型可能会把近距离车辆重复检测同一个车出两个框这时适当提高 NMS 的 IoU 阈值到 0.6。5.2 拥堵车流下两车间距太小NMS 把一车带走了现象早高峰排队路段两辆车前后间距不足一个车身检测输出只剩一个车框统计瞬间少 1 辆。原因YOLO 的 NMS 后处理会合并 IoU 超过阈值的重叠框。堵车时两车离得近两个真实框的 IoU 可能超过 0.5NMS 判定是同一个目标后车框被抑制。这不是模型“没看到”是后处理把结果吞了。解决把 NMS 阈值从默认的 0.45 降低到 0.3允许更多的重叠框保留。代价是可能把一辆车的多个碎片框都放出来后续需要靠跟踪器的匹配来纠正。另一个更稳的做法是改用yolov8m或yolov8l这类更大模型大模型对密集重叠的区分能力明显更好显存够就优先换模型而不是死磕 NMS 参数。5.3 track_id 反复出现新 ID同一辆车被数了多次现象画面里一辆白色轿车从入画到出画显示 ID 从 12 跳到 47 又跳到 89流量统计比人工数多出一倍。原因DeepSORT 是短时跟踪器超过max_age帧的遮挡就会丢弃轨迹。当车辆被路灯杆或大车遮挡超过这个帧数原轨迹被判死车辆重现时被当成新目标分配新 ID。交叉路口里这种现象在转弯车辆上尤其严重。解决优先调大max_age到 45 到 60 帧。同时视觉上做规避把计数线放在没有遮挡的路段中段而不是设在路口中央。如果遮挡无法避免最彻底的办法是给逻辑层加“轨迹合并”检测到新 ID 的起始位置和某个已丢失轨迹的最后位置距离小于阈值时判定是同车沿用旧 ID。这个逻辑成本低对毕设演示的提升立竿见影。5.4 夜间和逆光场景调对比度还是换训练策略现象晚间测试视频里车灯区域过曝车身淹没在黑暗中检测置信度普遍掉到 0.3 以下统计结果几乎不可用。原因训练集的日间图像特征清晰的边缘、车身颜色在夜间不成立模型依赖的特征被噪声干扰。把亮度调低不会自动让模型适应夜视因为模型学的是像素分布不是亮度语义。解决两条路都能走。第一条是数据层面在训练时加入夜间图像的 Mosaic 增强或者用 CLAHE 做对比度受限的自适应直方图均衡化后再送进模型。第二条是策略层面——直接引入红外或可见光融合的输入但对毕设来说成本过高不推荐。实际项目里我更倾向于先做 CLAHE 预处理再评估效果多数场景能把 mAP 拉回可接受范围代价是每帧多 2~3 ms 的预处理耗时几乎可以忽略。5.5 视频推理只有 0.5 fps你的检测器只该跑在采集端现象模型输出一切正常但处理一个 5 分钟的视频要跑 20 分钟答辩现场演示变成卡顿播片。原因很多人把 YOLO 检测器和 DeepSORT 放在同一台没有 GPU 的笔记本上跑检测器每帧推理 200ms跟踪器再算特征又是 100ms叠加起来就是这个速度。深度学习模型的推理负载主要在检测器跟踪器反而是轻量的。解决推理链路分阶段落地。采集端有 GPU 的机器用高分辨率跑检测加跟踪输出带 track_id 的标注视频和统计 CSV展示端只负责回放结果视频和图表不需要再跑模型。如果必须实时推理就把检测器换成yolov8n或yolov8n-pose的轻量版本同时把输入分辨率降到 480p牺牲部分小目标精度换流畅度。记住一条经验实时统计和离线分析是两个工程别指望一个配置通吃两件事。6. 调试技巧与进阶方向让毕设从“能跑”到“能答辩”一个交通流量检测系统做到能出数、能画框只是及格线。答辩评估里的“工作量”和“思考深度”往往体现在调试过程是否严谨、有没有量化对比。分享两个我常用的验证方法。第一个是数据一致性核对。挑一段 3 分钟的视频人工分段数车把每一分钟的人工计数和系统输出并排列出来。如果系统输出在 5% 误差内说明核心链路没问题如果差距大回看可视化视频重点观察计数线附近有没有 ID 反复横跳。这个操作只需要一个表格但它的说服力比十张训练损失曲线都强因为在答辩场景里“你的系统数得准不准”是最难被追问的问题。第二个是健壮性测试矩阵。准备四段视频日间通畅、日间拥堵、夜间、雨天分别记录 mAP、FPS、流量误差率做成一张对比表放进论文附录。这张表的意义在于证明系统不是“只在训练集上有效”而是对场景变化有真实响应。表格里如果出现“夜间误差 40%”这样的数据别慌如实写。如实呈现问题比包装完美更能体现工程意识答辩老师喜欢看到学生知道自己系统的边界。进阶方向上如果想把系统做成真能部署的东西优先考虑把推理服务化。用 FastAPI 包一层 HTTP 接口输入视频路径输出 CSV 和标注视频这样前端展示和后端算法彻底解耦。再进一步模型量化到 FP16 或 INT8在 Jetson Nano 这类边缘设备上跑实时推理就是完整的边缘计算落地形态了。但这一步的前提是先确保检测和跟踪链路没有硬伤否则量化只是把错误结果算得更快。我这些年看毕设项目翻车率最高的永远不是算法本身而是数据没理清、路径没改完、参数没调到位。每次拿到一个 zip我都会强制自己先跑通推理再动训练先看输出再问为什么——这个习惯救了我很多次。希望你也能拿这套方案跑出稳定的结果带着完整的数据和边界认知去答辩而不是只靠一张“loss 下降图”撑场子。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进