
简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的YOLOv8目标检测实战项目聚焦交通收费站场景下的ETC跟车逃费识别可作为毕业设计、课程设计或大作业的完整方案。压缩包共8个文件约15.91MB包含3个Python脚本、3个模型权重文件与2个说明文本分别对应可视化界面、模型训练与视频检测等核心模块部署门槛低简单配置即可运行。项目已完整跑通可输出核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图便于答辩展示与结果分析。目前已有46人学习下载。读者可直接获得源码、数据集、可视化页面与部署说明的一站式材料既能快速复现检测流程也能在此基础上修改扩展功能适合作为毕设保底与入门进阶的参考。1. 从收费站跟车逃费说起这套 YOLOv8 识别系统到底能跑出什么结果收费站 ETC 车道最头疼的场景不是设备故障而是前车刷 ETC 抬杆后后车贴着保险杠一脚油门蹭过去。人工稽核靠录像回放一天看几百条眼睛都花了还容易漏。这套基于 YOLOv8 的交通收费站 ETC 跟车逃费识别系统核心思路是把「车牌 车辆位置 车道状态」三路信息在视频流里做时序关联用目标检测框判断两车是否在同一 ETC 交易周期内先后通过同一车道。源码包里带了完整数据集、可视化界面和部署教程解压后按文档走CPU 环境也能先跑通推理看效果。适合做毕设或课程设计的同学也适合想快速验证 YOLOv8 在交通场景落地效果的工程师。我拿到包后第一件事是看数据目录结构和推理脚本的输入输出定义确认它是不是真能端到端跑而不是只给个模型权重让你自己猜。2. 拆开压缩包先看什么目录结构、依赖版本与数据集格式2.1 拿到源码包后的第一轮检查解压后别急着pip install先花五分钟把目录树过一遍。常见做法是看根目录下有没有requirements.txt、README.md、data/、weights/、ui/这几个关键目录。这套包的典型结构是data/放标注好的收费站场景图片和对应 YOLO 格式标签weights/放预训练权重或训练好的.pt文件ui/放可视化界面代码根目录下是训练和推理脚本。先确认data/里有没有images/和labels/两个子目录以及data.yaml里的nc类别数和names类别名是否和你的场景匹配。如果names里只有car和plate那它做的是车辆和车牌检测跟车逃费逻辑是在后处理里用 IOU 和时序做的。# 先看目录结构确认关键文件都在 find . -maxdepth 2 -type d | sort # 看依赖清单重点看 torch 和 ultralytics 的版本约束 cat requirements.txt # 看数据集配置文件确认类别数和类别名 cat data/data.yaml上面三条命令分别解决「有什么」「依赖什么版本」「检测几类目标」。find的-maxdepth 2防止目录太深刷屏requirements.txt里如果写的是ultralytics8.0.0这种宽松约束建议手动锁到具体小版本比如8.0.196避免新版 API 变动导致推理脚本报错。data.yaml里的nc必须和labels/下 txt 文件里的类别索引最大值一致否则训练时直接报索引越界。2.2 数据集格式与标注检查YOLOv8 用的是 YOLO 格式标注每张图对应一个同名.txt每行是class_id x_center y_center width height坐标都归一化到 0~1。这套包里的数据集如果是收费站实拍常见类别是etc_gantry、car、plate三类。拿到后先抽查几张图的标注框是否贴合目标尤其注意车牌框有没有把 ETC 天线也框进去。我一般会写个短脚本统计每个类别的框数量和尺寸分布如果plate类的框平均宽高小于 20 像素训练时就要考虑放大输入尺寸或者做数据增强。import os from collections import Counter label_dir data/labels/train counter Counter() sizes [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) w, h float(parts[3]), float(parts[4]) counter[cls_id] 1 sizes.append((cls_id, w, h)) print(类别分布:, counter) # 按类别看平均宽高判断小目标占比 for cls_id in counter: cls_sizes [(w, h) for c, w, h in sizes if c cls_id] avg_w sum(w for w, _ in cls_sizes) / len(cls_sizes) avg_h sum(h for _, h in cls_sizes) / len(cls_sizes) print(f类别 {cls_id}: 平均宽 {avg_w:.4f}, 平均高 {avg_h:.4f})这段脚本输出两个关键信息类别是否均衡、小目标占比。如果plate类平均宽高低于 0.02归一化后说明车牌在图中占比很小训练时imgsz建议设到 1280 而不是默认的 640。另外如果某个类别样本数为 0说明标注文件里根本没出现该类后处理逻辑里对应的判断分支就是死代码得先补数据。2.3 环境配置CPU 版和 GPU 版的取舍热词里有人搜ubuntu20.04搭建yolov8环境cpu版本说明不少人是没独显的笔记本或者实验室服务器排队等不到卡。这套包如果带了 CPU 推理脚本那torch装 CPU 版就行pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu。但要注意CPU 推理一张 1080p 图大概 200~500ms做视频流实时分析会掉帧。如果只是跑通 demo 看效果CPU 够用如果要接摄像头做实时逃费判断建议至少上一张 6G 显存的卡gtx1660ti跑yolov8这个热词也说明 1660Ti 是很多人的入门卡跑yolov8n或yolov8s在 640 尺寸下能到 30fps 以上。# CPU 环境安装示例 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics opencv-python pyqt5 # 验证安装 python -c from ultralytics import YOLO; print(ok)虚拟环境隔离是血泪经验别在系统 Python 里直接装否则opencv和pyqt5版本冲突能把界面搞崩。ultralytics包自带YOLO类from ultralytics import YOLO能打印ok就说明基础环境通了。pyqt5是可视化界面的依赖如果包里的界面用的是tkinter那pyqt5可以不装看ui/目录下的 import 语句决定。3. 训练自己的数据集从 data.yaml 到 best.pt 的完整链路3.1 训练命令与关键参数含义YOLOv8 的训练入口就一行命令但参数怎么设直接决定模型能不能用。这套包如果带了预训练权重yolov8n.pt建议从它开始微调而不是从头训。从头训需要几千张标注图才能收敛微调几百张就能出效果。关键参数里imgsz控制输入分辨率收费站场景车牌小设 1280 比 640 召回率高batch看显存6G 卡跑 1280 尺寸建议batch4或8epochs一般 100~300看验证集 mAP 不再上升就可以停lr0初始学习率默认 0.01微调时降到 0.001 更稳。# 从预训练权重微调训练 ETC 跟车场景 yolo detect train \ datadata/data.yaml \ modelweights/yolov8n.pt \ imgsz1280 \ epochs150 \ batch8 \ lr00.001 \ patience30 \ projectruns/etc_train \ nameexp01patience30表示验证集指标 30 轮不提升就早停省时间。project和name决定输出目录训练完best.pt和last.pt都在runs/etc_train/exp01/weights/下。训练过程中会打印每轮的box_loss、cls_loss、mAP50重点看mAP50有没有到 0.85 以上低于 0.7 说明数据质量或标注有问题别急着部署先回去查标注。3.2 训练过程监控与损失曲线热词里有人搜yolov8画损失函数曲线图说明训练完想看收敛情况。ultralytics训练时自动在runs/下生成results.csv里面记录了每轮的损失和指标。用pandas读出来画图就行不用额外装 tensorboard。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/etc_train/exp01/results.csv) df.columns df.columns.str.strip() # 列名可能带空格 fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(df[epoch], df[train/box_loss], labelbox_loss) axes[0].plot(df[epoch], df[train/cls_loss], labelcls_loss) axes[0].set_xlabel(epoch) axes[0].set_ylabel(loss) axes[0].legend() axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) axes[1].set_xlabel(epoch) axes[1].set_ylabel(mAP) axes[1].legend() plt.tight_layout() plt.savefig(train_curve.png, dpi150)box_loss下降说明框回归在收敛cls_loss下降说明分类在收敛。如果box_loss震荡不降检查学习率是不是太大如果mAP50早早到 0.9 但mAP50-95很低说明框的位置不够准可以试试加mosaic增强或者调box损失权重。这张图也是毕设论文里必备的训练过程图直接放进去就行。3.3 验证集评估与混淆矩阵训练完别只看 loss跑一次验证看混淆矩阵。yolo detect val会输出每个类别的 precision、recall、mAP还会生成confusion_matrix.png。收费站场景最怕的是把car误检成plate或者漏检小目标车牌。如果plate的 recall 低于 0.8说明漏检多后处理里跟车判断就会漏掉逃费车辆。yolo detect val \ modelruns/etc_train/exp01/weights/best.pt \ datadata/data.yaml \ imgsz1280 \ batch8 \ conf0.25 \ iou0.5conf0.25是置信度阈值低于这个值的框不参与评估。iou0.5是 NMS 的 IoU 阈值两个框重叠超过 0.5 就只保留置信度高的那个。验证完看runs/val/下的confusion_matrix.png对角线越深越好非对角线有值说明有类别混淆。如果plate和car混淆严重说明标注时车牌框和车身框有重叠得回去重新标。4. 跟车逃费判定逻辑从检测框到逃费事件的时序关联4.1 单帧检测到多帧跟踪的过渡YOLOv8 只做单帧检测要判断「跟车逃费」必须引入跟踪。常见做法是用ByteTrack或BoT-SORTultralytics里直接model.track()就能调。跟踪后每辆车有唯一track_id再结合 ETC 交易信号如果有或者车道线圈触发信号判断同一track_id是否在极短时间内跟随前车通过。如果没有交易信号就纯靠视觉前车通过 ETC 天线区域后后车在 0.5 秒内也通过同一区域且两车距离小于阈值就标记为疑似逃费。from ultralytics import YOLO import cv2 model YOLO(runs/etc_train/exp01/weights/best.pt) cap cv2.VideoCapture(data/test_video.mp4) track_history {} # track_id - [(frame_idx, x_center, y_center)] while cap.isOpened(): ret, frame cap.read() if not ret: break results model.track(frame, persistTrue, conf0.3, iou0.5, verboseFalse) if results[0].boxes.id is not None: boxes results[0].boxes.xywh.cpu().numpy() ids results[0].boxes.id.int().cpu().numpy() for box, tid in zip(boxes, ids): x, y, w, h box track_history.setdefault(tid, []).append((len(track_history), x, y)) # 后续用 track_history 做时序判断 cap.release()persistTrue让跟踪器在帧间保持状态verboseFalse关掉每帧日志刷屏。track_history存每个track_id的中心点轨迹后面判断跟车就是看两个不同track_id的轨迹在时间轴上是否重叠且空间距离近。注意model.track()第一次调用会初始化跟踪器如果视频开头几帧没检测到目标跟踪器可能延迟启动这是正常现象。4.2 逃费判定的阈值与逻辑判定逻辑的核心是三个阈值时间窗口T_window、空间距离D_thresh、置信度conf_thresh。常见做法是T_window0.8sD_thresh1.5个车身长度conf_thresh0.4。如果前车track_idA在t1时刻通过 ETC 天线区域后车track_idB在t10.3s也通过同一区域且两车中心点距离小于D_thresh就输出逃费告警。这套逻辑在代码里就是一个滑动窗口加距离判断不难写难的是阈值调参。收费站车速不同、摄像头角度不同阈值都要微调。def check_tailgating(track_history, etc_zone, fps25, t_window0.8, d_thresh200): track_history: {track_id: [(frame_idx, x, y), ...]} etc_zone: (x1, y1, x2, y2) ETC 天线触发区域 fps: 视频帧率用于把帧差转成秒 t_window: 时间窗口秒 d_thresh: 两车中心点像素距离阈值 events [] frame_window int(t_window * fps) for tid, traj in track_history.items(): for i, (fidx, x, y) in enumerate(traj): if etc_zone[0] x etc_zone[2] and etc_zone[1] y etc_zone[3]: # 当前车进入 ETC 区域找时间窗口内其他进入的车 for other_tid, other_traj in track_history.items(): if other_tid tid: continue for ofidx, ox, oy in other_traj: if abs(ofidx - fidx) frame_window: dist ((x - ox) ** 2 (y - oy) ** 2) ** 0.5 if dist d_thresh: events.append((tid, other_tid, fidx, dist)) return eventsetc_zone需要根据摄像头画面手动标定一般是画面中 ETC 天线杆下方的矩形区域。d_thresh设 200 像素是 1080p 下的经验值如果摄像头是 720p 要按比例缩小。events里会输出所有疑似跟车对但会有重复实际部署时要做去重同一对车只报一次。这个函数是纯 CPU 计算不占显存可以放在推理循环里每帧调一次。4.3 可视化界面怎么接推理结果包里的可视化界面如果是 PyQt5 写的通常有一个QThread跑推理循环通过信号槽把检测框和告警信息发到主线程刷新。接的时候注意别在子线程里直接操作 UI 控件否则界面会卡死或者崩溃。常见做法是子线程发pyqtSignal(dict)主线程收到后更新QLabel的 pixmap 和告警列表。如果界面是tkinter就用after()轮询队列。不管哪种推理帧率别设太高CPU 推理 10fps 就够了设 30fps 反而让界面响应变慢。from PyQt5.QtCore import QThread, pyqtSignal import cv2 class InferThread(QThread): frame_ready pyqtSignal(object, list) # 帧图像, 告警列表 def __init__(self, model, video_path): super().__init__() self.model model self.video_path video_path self.running True def run(self): cap cv2.VideoCapture(self.video_path) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.model.track(frame, persistTrue, verboseFalse) alerts [] # 这里填跟车判定结果 self.frame_ready.emit(frame, alerts) cap.release() def stop(self): self.running False self.wait()frame_ready信号带两个参数原始帧和告警列表。主线程收到后把帧转成QImage显示告警列表更新到QListWidget。stop()里先置runningFalse再wait()保证线程安全退出。如果直接terminate()OpenCV 的VideoCapture可能没释放下次打开视频会报错。5. 避坑与排查部署这套系统时最容易翻车的五个地方5.1 现象训练时 loss 变成 NaN几轮后直接崩原因通常是学习率太大或者数据里有脏标注。YOLOv8 默认lr00.01如果数据集小且没预训练权重前几轮梯度爆炸就会 NaN。另外标注文件里如果有坐标超出 0~1 范围或者某行只有 4 个字段也会导致 loss 计算异常。解决方法是先把lr0降到 0.001加warmup_epochs3让学习率慢慢升然后写脚本扫一遍所有labels/下的 txt把坐标不在 [0,1] 的行删掉或修正。5.2 现象推理时检测框乱飞同一辆车忽大忽小这是imgsz和训练时不一致导致的。训练用 1280推理用 640模型看到的尺度变了框回归就不准。解决方法是推理时imgsz必须和训练时一致model.predict(frame, imgsz1280)。另外如果视频有畸变比如广角摄像头边缘拉伸也会让框变形常见做法是先做去畸变再推理或者把etc_zone标定在画面中心区域避开边缘。5.3 现象跟踪 ID 频繁跳变同一辆车换了好几个 IDByteTrack在目标被遮挡或者检测置信度低时会丢 ID。收费站场景里前车挡住后车车牌后车 ID 就容易跳。解决方法是把track的conf阈值调低到 0.2让更多低置信度框参与跟踪同时开persistTrue保持跟踪器状态。如果还跳可以换BoT-SORT它对遮挡更鲁棒但计算量稍大。另外视频帧率太低低于 15fps也会导致跟踪不稳建议用 25fps 以上的视频源。5.4 现象可视化界面打开就闪退控制台报 Qt 平台插件错误这是 PyQt5 在 Linux 下缺xcb依赖的典型问题。ubuntu20.04下跑pip install pyqt5后直接运行会报could not load the Qt platform plugin xcb。解决方法是装系统依赖sudo apt install libxcb-xinerama0 libxcb-cursor0 libxkbcommon-x11-0。如果是在无显示器的服务器上跑界面本来就不该启动用xvfb-run包一层或者直接跑无界面的推理脚本。5.5 现象CPU 推理速度太慢视频分析一卡一卡CPU 跑 YOLOv8 默认用torch的 CPU 后端没做量化也没用onnxruntime。解决方法是先把.pt导出成onnx然后用onnxruntime推理速度能快 2~3 倍。导出命令是yolo export modelbest.pt formatonnx imgsz1280推理时用onnxruntime.InferenceSession加载。如果还慢把imgsz降到 640精度会掉一点但速度翻倍。实在要实时还是得上 GPUgtx1660ti跑yolov8n在 640 下能到 40fps 以上。6. 进阶技巧用 ONNX 导出把 CPU 推理速度压榨到极限CPU 部署这套系统最现实的优化路径就是 ONNX Runtime。PyTorch 的 CPU 推理在 1080p 下每帧 300ms 起步换成 ONNX Runtime 加intra_op_num_threads调优后能压到 100ms 左右。导出时注意imgsz和训练一致opset用 12 或 17别用太新的版本否则某些 CPU 的指令集不支持。导出后先用onnxruntime跑一张测试图对比 PyTorch 和 ONNX 的输出框是否一致如果偏差超过 1 个像素说明导出有精度损失得检查dynamic轴设置。import onnxruntime as ort import numpy as np import cv2 # 导出命令yolo export modelbest.pt formatonnx imgsz1280 opset12 session ort.InferenceSession( best.onnx, providers[CPUExecutionProvider], ) # 设置线程数一般设为物理核心数 session.set_providers([CPUExecutionProvider]) session_options session.get_session_options() session_options.intra_op_num_threads 8 img cv2.imread(test.jpg) img cv2.resize(img, (1280, 1280)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR-RGB, HWC-CHW img np.ascontiguousarray(img, dtypenp.float32) / 255.0 img img[None, ...] # 加 batch 维 outputs session.run(None, {session.get_inputs()[0].name: img}) # outputs[0] 形状通常是 (1, 4nc, 8400)需要做 NMS 后处理intra_op_num_threads设成物理核心数别设逻辑核心数超线程反而拖慢。session.get_inputs()[0].name拿到输入名一般是images。输出(1, 4nc, 8400)里 4 是cx, cy, w, hnc是类别数8400 是候选框数。后处理要做置信度过滤和 NMS这部分可以直接抄ultralytics的non_max_suppression函数或者自己写一个简化版。ONNX 推理的坑在于预处理必须和训练时完全一致归一化用/255.0还是mean/std要看训练配置搞错了框全偏。从那以后我每次导出 ONNX 都强制走一遍「PyTorch 和 ONNX 同图对比」的流程输出框偏差超过 2 像素就回退查预处理。这套 ETC 跟车逃费识别系统在 CPU 上跑 ONNX 后单路视频分析能到 8~10fps够做离线稽核了。希望帮到你。本文还有配套的精品资源点击获取