简介:一份基于YOLOv8的多端车流检测系统完整项目资料,面向人工智能、通信、自动化、电子信息等专业学生与开发者,可直接用于毕业设计、课程设计或作为科研项目起点。系统涵盖模型训练、多端部署与可视化检测流程,适合已有深度学习基础的学习者进阶。压缩包共397个文件,约16.94MB,以Python源码(150个py文件)为核心,辅以34个YAML配置、预训练模型pt、UI界面、SQL数据库、环境配置文件及演示视频,同时包含大量png/jpg测试图片与pyc运行缓存,便于调试与结果验证。已有165人浏览学习,项目代码经测试运行成功,并获导师指导及答辩评审95分高分认可。资源附带的文档与全套资料可帮助使用者快速掌握YOLOv8车流检测的工程实现,尤其适合需要完整项目经验支撑的毕设与课设场景,并可在现有代码基础上做二次开发扩展功能。
1. 一套YOLOv8多端车流检测系统:毕设答辩前你真正需要的是完整闭环
毕设季最容易翻车的项目,不是模型没训出来,而是答辩时被追问“这套系统换一台电脑、换一个路口还能跑吗”。基于YOLOv8的多端车流检测系统,正是冲着这个问题去的:它把车辆检测、车流统计、视觉展示拆成训练端、边缘端和Web端,同一份权重既能在GTX 1660Ti这样的普通显卡上训练,也能跑到RK3588这类边缘设备上做实时推理,最后用统一界面把车流数据展示出来。这套闭环特别适合急着出完整成果、又不想在部署上失控的本科毕业设计和开源项目。我会按选型、训练、部署、避坑的顺序,把能直接抄的命令和参数写出来。
2. 为什么是YOLOv8而不是Faster R-CNN:网络结构、多端架构与1660Ti环境配置
2.1 看清YOLOv8的检测头,才知道多端转移为什么省心
车流检测的时间背景是白天、黑夜、雨雾、拥堵,目标尺度从几十像素到整屏。很多人一上来就选Faster R-CNN,理由是两阶段精度高,但在GTX 1660Ti上跑实时视频流,单帧推理时间很容易突破150 ms,处理1080p的车流视频几乎卡成PPT。YOLOv8作为单阶段检测器,在同样的硬件上能把延迟压到30到60 ms,对车流计数场景已经够用。它更大的价值在于工程生态:ultralytics仓库把数据加载、训练、验证、导出串成一条命令,后续转ONNX、转RKNN的坑位比传统Detection代码少很多,毕设阶段不必去补既有框架的存量债务。
从网络结构上看,YOLOv8相对YOLOv5的主要改动集中在三处。主干网络用C2f替换了C3,把更多的梯度流拆成多条分支再融合,同等参数下特征表达能力更好;颈部仍是PAN结构,深浅层特征交叉融合,这对车流里远小目标和近处大目标同时存在的情况很关键;检测头从anchor-based改成anchor-free,每个位置直接预测四个边框值和类别概率,少算了anchor匹配那一步,训练目标也更接近真实车流分布。输出层使用DFL和CIOU组合做回归损失,训练收敛曲线比早期YOLO好看,也更容易在毕设里写清楚。很多论文里的网络结构图把C2f和检测头画得很大,但真正影响多端部署的是输入尺寸和输出通道数,画图时别漏掉这两个信息。
那“多端”到底指哪几端?我一般拆成三端:训练端是带NVIDIA显卡的PC,负责标注、训练、评估;边缘端是RK3588这类Arm盒子,负责加载RKNN格式的模型做实时推理;展示端是一个轻量Web页面,负责把车辆框、计数结果和帧率展示出来。这样做的好处是训练和推理解耦,不会因为嵌入式端内存不足而反推整个训练策略;同一个数据集和同一份best.pt权重,通过ONNX和RKNN两级转换,就能在不同算力平台上复用。
这里要补充一点选型边界:如果项目要求的是夜间低光、遮挡严重的特定路口,YOLOv8并不天然比Faster R-CNN强,真正的差距在训练数据分布和后续优化。如果学校要求必须写清楚改进点,可以把YOLOv8的检测头改成动态标签分配或在C2f里引入注意力,但这属于加分项;先把基线跑通,再谈改进,否则开题到中期都压着一个跑不动的黑匣子,非常难受。
2.2 在GTX 1660Ti上把环境一次配好:最小命令清单
环境配置是新手最容易翻车的地方。常见组合是Python 3.10、PyTorch 2.x、Ultralytics 8.x。不要自己去源码编译,直接用pip安装即可:
conda create -n traffic python=3.10 -y conda activate traffic pip install ultralytics这段命令创建名为traffic的虚拟环境,Python版本锁定3.10,目前PyTorch和OpenCV对它的兼容性都很好。第3行直接用pip装ultralytics,它会自动带上torch CPU版,但这里有个容易踩的坑:如果直接执行上面命令,torch会安装CPU版本,虽然能训练但速度极慢。必须在装ultralytics之前先装对应CUDA的torch,常见的安装方式是:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118在GTX 1660Ti这类6 GB显存卡上,我习惯装cu118而不是更高版本,原因是cu118对应驱动的兼容范围更宽,装完后不容易在import torch时报找不到CUDA的错。如果你的显卡驱动较新,也可以选择cu121,但不要在还没跑通模型前去升级显卡驱动,这是很多血泪经验的来源。
环境装好后,用一张预训练权重先验证推理链路:
yolo predict model=yolov8s.pt source=https://ultralytics.com/images/bus.jpg注意这个示例源是ultralytics官方的示例图片。如果网络受限,就换成任意一张本地图片路径,例如source=./test.jpg。命令会自动下载yolov8s.pt权重,然后输出预测图。看到bus.jpg里检测出person和bus就说明环境没问题。这里选yolov8s而不是n或m是有原因的:8s约11 M参数量,在1660Ti上推理帧率接近40 FPS,精度适中;8n更快但车流小目标检出率不足,8m能要到精度但6 GB显存训练时batch会缩得很难受。对毕设而言,yolov8s是最小后悔药。
装完环境后可以用一条命令确认GPU能真正参与训练:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"如果输出True NVIDIA GeForce GTX 1660 Ti,说明推理和训练都会走GPU。如果输出False,优先确认是不是装了CPU版torch,不要急着重装系统。最后补充一点:不要在这个阶段去装很多改进版本的YOLOv8源码。网上那些带注意力机制、偏置解耦的增强版本,中间层改动很大,一旦开箱跑不通,排查成本足够你做两个课题。先用官方ultralytics跑通,之后需要改进时再用官方代码里支持的卷积替换方式做增量修改。
3. 训练自己的车流数据集:从VOC转YOLO到损失曲线,把mAP调到能答辩的水平
3.1 数据目录与data.yaml:公开车流数据和自采路口视频怎么混合
车流检测的公开数据集中,UA-DETRAC和BDD100K比较常见。前者是固定相机视角、车辆类别和后尾灯场景,后者包含驾驶视角,天气和时段更全。做毕设时不要贪多,我的习惯是:从公开数据集抽一个子集,再自己架摄像机在路口拍约20到30分钟视频,抽帧筛选出300到500张白天和傍晚的图片,和公开数据混合成一个小而均衡的数据集。这样做的原因是,公开数据集分辨率、相机高度和毕设演示环境差异很大,只用公开数据会导致换个路口就漏检;只有自己的图片才能让答辩现场的演示视频在同一个场景下表现正常。
目录结构我一般建成本地项目下的固定格式:
traffic_dataset/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ └── traffic.yaml这个结构同时也是ultralytics默认支持的格式。images和labels的train/val子目录必须同名一一对应,图片后缀和标签后缀分别放在两个根目录下;标签文件每行是class_id x_center y_center width height,坐标全部归一化到0到1。traffic.yaml内容则是指向这些目录和数据集的元信息:
path: /home/user/traffic_dataset train: images/train val: images/val names: 0: car 1: bus 2: truck 3: motorcycle 4: bicycle这里path写绝对路径最省事,省得train和val写成相对路径时因为工作目录不同而爆红。类别只保留车流场景需要的类,不要从COCO全量80类里带过来一堆person、dog之类;类别越少,同类别在特征空间的区分度越集中,训练出来的车流检测模型也越不容易把树干误检成bus。
我一般会额外做一次“按时间段划分”而不是随机划分:把同一段视频抽出的帧按时间顺序排列,前80%作为train,后20%作为val。如果随机划分,相邻帧几乎重复,验证损失会很乐观,到了答辩现场换成新视频马上露馅。这是数据层面避坑的关键,后面避坑章还会专门展开。
3.2 用Python把VOC格式转成YOLO标签的脚本与坐标边界坑
公开车流数据通常是VOC格式,标注用xml存储。YOLO训练需要txt标签,所以需要一个转换脚本。这里给一个可直接使用的版本:
import os import xml.etree.ElementTree as ET from pathlib import Path classes = ['car', 'bus', 'truck', 'motorcycle', 'bicycle'] def convert_xml(xml_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in classes: continue cls_id = classes.index(name) 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) x_center = (x1 + x2) / 2.0 / w y_center = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines)) xml_dir = Path('Annotations') txt_dir = Path('labels/train') txt_dir.mkdir(parents=True, exist_ok=True) for xml_file in xml_dir.glob('*.xml'): convert_xml(xml_file, txt_dir / (xml_file.stem + '.txt'))这段脚本做了三件事:遍历Annotations下的xml,读取图片尺寸和每个目标的类别、边框;把左上角右下角坐标归一化成YOLO格式的x_center、y_center、width、height;最后写入同名txt。需要留意的是坐标钳制到0到1这一步,车流数据集里偶尔会有标注框出画面边界的脏数据,如果不做钳制,训练时损失直接变为nan,那种情况极难排查。另一个细节是xml里值经常带换行符,这里统一用float()强转,但如果文本里混入了不可见字符,需要先value = ''.join(value.split())再转换。
3.3 在GTX 1660Ti上启动YOLOv8训练并画出损失曲线
数据准备好了,训练命令非常简单:
cd /home/user/traffic_dataset/.. yolo train model=yolov8s.pt data=traffic_dataset/traffic.yaml \ epochs=100 imgsz=640 batch=8 device=0 \ patience=10 project=runs name=traffic_yolov8s这个命令用预训练的yolov8s.pt做迁移学习,epochs给100轮,输入分辨率640,显存6 GB的GTX 1660Ti下batch用8比较稳妥,如果中途OOM就降到4。patience是早停轮数,连续10轮验证损失不下降就停止,不会白跑。训练过程中可以随时查看命令行输出的mAP50、mAP50-95、loss/train_box等指标;训练结束后,runs/traffic_yolov8s/weights/best.pt和last.pt分别保存最优和最后权重。注意评判优先用best.pt,这是默认选择。
训练结束后,runs/traffic_yolov8s/下会出现results.png,里面包含了train和val的box loss、cls loss、mAP50和mAP50-95曲线。很多毕设小组直接把这张图贴到论文里,但更值得做的是打开看它的形状:如果train loss持续下降、val loss在第40轮后反弹,那就是过拟合,说明epochs太多或数据量太少;如果两条曲线在第80轮还在同步下降,可以再把epochs加到150。画图本身不需要额外代码,ultralytics已经帮我们产出了,答辩中讲清楚每条曲线的含义比贴代码更有加分效果。
为了更直观地验证预测效果,我还会在训练结束后跑一段验证视频:
yolo predict model=runs/traffic_yolov8s/weights/best.pt \ source=test_video.mp4 \ conf=0.35 iou=0.45 \ show=True save=Trueconf和iou是推理阶段最常用的两个参数。conf默认0.25,车流场景建议提到0.35,减少树影和窗格的误检;iou保持0.45到0.5,过低会重复框住同一辆车,会把车流计数结果放大。这里save=True会保存带框的视频,用来做演示素材正好。
4. 多端部署实战:从ONNX导出到RK3588,再用Flask做Web演示界面
4.1 把训练好的YOLOv8导出ONNX,再转成RK3588的RKNN模型
训练产出best.pt后,离真正跑在一个嵌入式盒子还有两步。第一步是把PyTorch权重导出为ONNX,ultralytics一条命令就能完成:
yolo export model=runs/traffic_yolov8s/weights/best.pt format=onnx opset=12 simplify=Trueopset设成12,RKNN工具链解析ONNX的兼容性较好;simplify=True会用onnx-simplifier去掉多余节点,转换出的模型更小,也更不容易在目标平台上报不支持的算子。导出success后,同目录下会出现best.onnx,可以先在PC上用onnxruntime跑一遍,确认输出张量和pt一致,再进入RKNN转换。
转RKNN的常见做法是使用RKNN-Toolkit2的Python API在PC上完成,而不是直接在板子上转。原因是量化阶段需要跑代表性的校准图片,PC内存和显存都更充裕。关键脚本如下:
from rknn.api import RKNN rknn = RKNN() ret = rknn.config( target_platform='rk3588', mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], quantized_dtype='w8a8', quantized_algorithm='normal', quantized_method='layer' ) print('config:', ret) rknn.load_onnx(model='best.onnx') rknn.build( do_quantization=True, dataset='rknn_calib.txt' ) rknn.export_rknn('traffic_yolov8s.rknn')这里config中的mean_values和std_values要和训练时保持一致,YOLOv8官方预处理是减0除255,所以mean为0,std为255。quantized_dtype='w8a8'表示对权重和激活都做8bit量化,RK3588的NPU跑得最快;quantized_method='layer'逐层决定量化范围,是精度和速度之间的折中方案。do_quantization=True启用校准量化,依赖rknn_calib.txt这个清单:
test_frames/frame_0001.jpg test_frames/frame_0002.jpg ...每行一张图片路径,我一般从验证集按时间均匀抽300张,覆盖白天、傍晚、阴天、路灯开启等车流场景。量化后板上推理的精度通常能保留90%以上,但如果校准图片只用20张且全是白天,夜间mAP会掉得很厉害,后面避坑章会专门展开。
4.2 在RK3588上跑RKNN推理,并把结果回传Web端
部署端我通常用RK3588开发板,系统自带NPU驱动。推理代码用RKNN Python接口加队列帧读取,是最常见的落地写法。这里给一个核心循环,说明车流检测和画框流程:
import cv2 from rknn.api import RKNN rknn = RKNN() rknn.load_rknn('traffic_yolov8s.rknn') rknn.init_runtime() capture = cv2.VideoCapture('rtsp://edge-camera/stream') while True: ok, frame = capture.read() if not ok: break input_frame = cv2.resize(frame, (640, 640)) input_frame = input_frame[:, :, ::-1] # BGR to RGB outputs = rknn.inference(inputs=[input_frame]) boxes, classes, scores = postprocess(outputs) for box, score, cls in zip(boxes, scores, classes): cv2.rectangle(frame, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) cv2.imshow('traffic', frame)这段代码有两点要重点说明。第一,RKNN推理前要把BGR转成RGB,并缩放到训练时相同的640x640;如果不转色,模型输出的置信度会整体偏低且类别错乱。第二,outputs从RKNN返回后通常是原始层的feature map,还需要在Python里做一次解码头、聚类和NMS,这部分逻辑在训练后的模型导出阶段会自动附在ONNX里,但RKNN工具链不会自动补齐,所以要自己在项目里维护一个后处理模块。这也是RK3588部署YOLOv8最大的工作量,很多人以为做完RKNN转换就万事大吉,其实后处理才是真正的坑。
如果是毕设演示,我更推荐先做一个Flask轻量Web端,而不是桌面PyQt界面,原因是Web端可以让老师和评委在手机、平板上同时看到检测视频流,演示效果更直观。代码简化为:
from flask import Flask, render_template, Response import cv2 from ultralytics import YOLO app = Flask(__name__) model = YOLO('runs/traffic_yolov8s/weights/best.pt') def generate_frames(): cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break results = model(frame, verbose=False) annotated = results[0].plot() ret, buffer = cv2.imencode('.jpg', annotated) yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n') @app.route('/video_feed') def video_feed(): return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)这个Web端虽然简单,但足以把检测结果实时推送到浏览器,且不依赖桌面环境,适合部署在RK3588板子上配合前面的RKNN推理做演示。如果要加车流计数,只需要在results[0].boxes里拿到每个目标的类别,然后对视频帧中的每辆车做框中心点跟踪,这里可以用最简单的按IoU匹配相邻帧的方式;毕设答辩时,能讲清楚“我按帧差IoU做轻量跟踪”比直接调用DeepSORT更有可信度。
如果演示机上没有NVIDIA显卡,Flask用CPU跑YOLOv8s速度只有5到8 FPS,画面会很卡。因此部署端和展示端最好分开:RK3588做NPU推理,Flask只做Web流转发和统计展示,否则现场容易翻车。
5. 车流检测落地中的5个避坑点:从夜间漏检到RK3588量化掉分
5.1 夜间黑色轿车漏检,加暗化增强反而更差
现象:白天场景mAP50接近0.93,一到夜间视频,黑色轿车直接消失,bus和truck还能检测到。
原因:最直接的原因是训练集中夜间样本太少,模型把近黑色区域和路面纹理混在一起。有人会在训练时把所有图片随机调暗,期望提高鲁棒性,结果发现夜间目标不仅没召回,白天背景也被压低,这是“增强策略和真实场景不匹配”导致的翻车。
解决:我先按时间线从测试视频抽帧,标注出100到200张夜间图片,放到训练集里重新训练,而不是统一做亮度增强。增强参数只开HSV扰动和轻微曝光,在ultralytics里通过hsv_h、hsv_s、hsv_v控制,例如hsv_h=0.015、hsv_s=0.7、hsv_v=0.4;同时不要加过大的随机暗化,保持光照分布的原始性。重新训练后夜间车漏检减少最明显的是黑色轿车。
5.2 远处小目标车流漏检,换大模型不如提高输入分辨率
现象:在高速高架场景,远处车辆只有约20乘20像素,用yolov8s在640分辨率下mAP50不错,但实拍视频里超过50米的目标全部跳过。
原因:YOLOv8默认下采样到640乘640输入,远处车辆的特征经过多次池化后只剩几个像素,后面的脖子和头部很难从中恢复语义。很多同学第一反应是换yolov8m或者yolov8l,但6 GB显存下训练不动,而且大模型对本就稀疏的小目标特征增益有限。
解决:在测试阶段对视频做ROI切片,把远处车道区域在原图中截取出来放大到640后再送入模型,检测框再映射回原图坐标。实在不想做切片,可以训练时imgsz=960、模型用yolov8n,显存开销可控,但推理延迟会增加。对于毕设,用ROI切片策略性价比最高:只需要在视频里框一个固定车道区域,不会引入太多新代码。
5.3 RK3588量化后精度掉了10个点,校准集要按场景抽
现象:PyTorch模型在PC上mAP50为0.90,转换RKNN后在板子上只有0.80,尤其夜间和雨天掉得更狠。
原因:量化是把FP32权重压缩成INT8,精度损失本身可控,翻车大多出在校准集。有人图省事从训练集随机抽20张图片,里面几乎全是白天,量化统计出的激活范围对夜间场景完全不适用,等于用白天数据定义了整个动态范围。
解决:校准集应该从验证集按场景和时间抽取300到500张,覆盖白天、傍晚、夜间、阴天和雨天,且每张图片都要是模型接受的原图尺寸或经过和训练相同的缩放。在RKNN的config里,quantized_algorithm可以用mmse代替normal,当正常量化掉点严重时,mmse会为了保精度小幅增加量化误差分布的计算量;如果单层量化导致个别层输出异常,再把关键层指定为float16保留。量化后一定要在板子上重复验证夜间视频,不要在PC上只测一张白天图就宣布完成。
5.4 毕设翻车:训练和测试用了同一段视频的相邻帧
现象:训练时mAP50-95接近0.75,换一段新路口的视频后掉到0.5,答辩时被老师质疑工作是否真实。
原因:很多开源车流项目演示视频只有几分钟,直接把整段视频抽帧后随机分train/val,相邻帧高度相似,等于让模型“背”了验证帧。最终得到的优异mAP是数据泄漏的假成绩,不是模型的泛化能力。
解决:数据划分必须按时间顺序而不是随机划分。我会取同一摄像头连续视频,前8分钟抽帧做训练,最后2分钟抽帧做验证,中间空出30秒过渡保证没有相邻帧泄漏。如果混合了多个路口,那么每个路口都要各取独立时间段进验证集。这个习惯写进毕设的“实验设计”里,答辩时非常加印象分。
5.5 GTX 1660Ti训练OOM:不是模型选错,是batch和缓存策略没调整
现象:训练启动后不到10秒报CUDA out of memory,甚至把浏览器都卡死。
原因:1660Ti只有6 GB显存,训练时若采用默认的batch=16,很容易爆掉。部分教程默认读者用的是3090、4090,照抄batch数值是OOM最直接的原因。有时候不是batch,而是在Windows下同时开了视频解码和浏览器,显存和内存都被占了一部分。
解决:把batch降到8或4,并在训练命令中显式加workers=2降低数据加载线程压力。如果还想稳一点,直接换yolov8n模型,输入分辨率保持640,6 GB显存下训练batch可以回到16。同时关闭其他GPU程序,不要一边训练一边在浏览器里播放高码率视频,这是最容易忽略的习惯问题。
6. 验收技巧:把损失曲线、视频回放和计数误差放在一张表里判断
模型训练完后,不要只贴mAP。我会做一次“三件套”验证:第一件看runs下的results.png里val loss有没有反常回升;第二件把best.pt跑三段不同时段的视频,输出带框视频,用肉眼确认有没有把树影、路灯当车;第三件在每段视频里人工数一遍真实车数,和模型计的数对比。为了不让计数过程太枯燥,我通常写个简单脚本把每类计数结果输出到csv,然后手工汇总成验证表。实际执行时,我用一个很简单的统计脚本:
import cv2 from ultralytics import YOLO model = YOLO('best.pt') counts = {'car': 0, 'bus': 0, 'truck': 0, 'motorcycle': 0, 'bicycle': 0} for video in ['day.mp4', 'night.mp4', 'rain.mp4']: cap = cv2.VideoCapture(video) frame_id = 0 while cap.isOpened(): ok, frame = cap.read() if not ok: break if frame_id % 5 == 0: result = model(frame, verbose=False)[0] for box in result.boxes: cls_name = result.names[int(box.cls)] if cls_name in counts: counts[cls_name] += 1 frame_id += 1 cap.release() print(video, counts)注意:这是“按帧累计”的近似统计,不是真正的目标计数,因为同一辆车会出现在多帧里。但用于验证检测器“有没有把类别认错”已经够用。我在毕设演示里会把这个脚本的结果做成一张表:白天、夜间、雨天三段视频分别统计真实车辆数、检测数、单帧推理时间,并标注漏检最多的是哪一类,然后用文字解释漏检原因。这个习惯帮我在答辩前提前发现过夜间黑色轿车漏检,也避免了现场被问到遮挡场景时无话可说。
最后说一个我的习惯:无论换多少次参数,都要把改过的数据集划分和时间戳记录在实验笔记里,不要只记录最终命令。否则一周后你自己也说不清当前权重是用哪些图片训出来的,这比模型本身不work更让人崩溃。希望这篇笔记能把你在车流检测毕设里的坑提前扫掉,少走几段弯路。
本文还有配套的精品资源,点击获取