YOLOv8+PyQt5自行车违停检测系统:数据集训练与GUI部署实战
简介基于YOLOv8与PyQt5打造的自行车违规停放检测告警项目面向计算机视觉方向毕业设计、课程设计及竞赛场景也适合希望从数据集到部署完整走一遍的初学者可应用于共享单车规范管理等现实需求。资源内含自行车专用数据集、训练好的YOLOv8模型及P-R曲线、混淆矩阵等评估结果并配有交互式GUI界面、独立训练脚本和模型切换逻辑按超详细部署教程操作即可快速复现准确率约98%还可扩展训练其他目标检测模型。压缩包共805个文件数据层由276张jpg图像与247个txt标签组成涵盖标注框坐标与类别信息80个py及pyc源码分为检测、训练、GUI启动、数据划分等模块6个pt权重文件对应不同训练阶段另有yaml配置、md部署文档、ipynb演示、csv评估记录等多类型文件支撑全过程整体约431.65MB目录层级区分数据、模型、代码与文档便于定位与维护。项目可直接交付毕设或课设也可在此基础上增加告警记录、统计报表等功能作者可协助定制。目前已有1855人学习下载适合需要完整可运行视觉方案的学生与开发者。1. 自行车违停检测YOLOv8 PyQt5 这套源码包到底解决什么问题自行车违规停放检测第一反应是目标检测把车框出来不就行了真做起来才发现模型只能回答“哪里有自行车”回答不了“哪里违规”。违停判定的另一半——车有没有停在禁停区、要不要弹告警——必须由应用层自己写。这套基于 YOLOv8 PyQt5 的源码包恰好把这两半拼成了闭环YOLOv8 权重负责检出自行车PyQt5 界面负责显示检测结果、画禁停区并触发告警。包里还附带了一份可直接训练的数据集、模型权重、简易 GUI 和超详细部署教程面向的是要把“检测 告警 界面”整条线路跑通的人。适合正在挑毕设题目的学生也适合想把目标检测做成带界面的可演示应用的工程新手。接下来按这份资源的组织顺序从数据格式、环境部署、训练调参、GUI 联调和排错一路拆到验收。2. 源码包拆解数据集、权重与 GUI 模块之间是怎么咬合的2.1 目录结构先分清数据、权重、界面、工具四层拿到源码别急着跑先把结构看一遍。一份典型布局是这样的bicycle_parking_violation/ ├── datasets/ │ ├── images/ # 训练/验证图片 │ ├── labels/ # YOLO 格式的 txt 标注 │ └── labelme_json/ # Labelme 原始 JSON 标注备份不删 ├── weights/ │ └── best.pt # 训练好的权重 ├── gui/ │ ├── main_window.py # PyQt5 主窗口 │ ├── detector.py # YOLO 推理封装类 │ └── alarm_zone.py # 禁停区定义与判定规则 ├── tools/ │ ├── labelme2yolo.py # 标注格式转换脚本 │ └── plot_loss.py # 损失曲线绘制脚本 └── docs/ └── 部署教程.md这个结构把项目分成了四层数据层datasets、模型层weights、逻辑层tools/gui 里的检测器和判定器、交互层gui。我见过不少同学先打开 main_window.py 啃信号槽半天没跑起来原因就是没确认权重路径和标签顺序。正确顺序是先检查数据格式和 best.pt 在不在再去跑界面因为 detector.py 一启动就要加载权重路径一错直接闪退。这四层之间是单向依赖数据训练出权重权重被 detector 加载detector 的结果再交给 alarm_zone 判定最后才到界面显示。理解了这个链路后面改任何一层都有的放矢。资源里 GUI 部分的代码量不大但它是整条链路的收口后面所有训练成果都要在这里体现。2.2 数据集格式的对应关系Labelme JSON 到 YOLO txt 的换算数据集的标注是 Labelme 生成的 JSON里面用 points 记录多边形的顶点坐标YOLO 训练要的是归一化的中心点和宽高。两者之间的换算关系是读取图片真实宽高把多边形外接矩形的坐标除以宽高完成归一化。这份资源里同时保留了 JSON 和 txt意思是你可以直接拿官方权重做迁移学习也可以改了标注重新训练。# labelme2yolo.py 核心片段转换单个 JSON 文件 import json import os from PIL import Image def convert(json_path, out_dir): with open(json_path, encodingutf-8) as f: data json.load(f) img Image.open(data[imagePath]) # 必须以原图尺寸做归一化 w, h img.size txt_name os.path.splitext(os.path.basename(json_path))[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: for shape in data[shapes]: # 遍历所有标注对象 if shape[label] ! bicycle: continue # 只保留 bicycle 类别 ps shape[points] # [[x1,y1],[x2,y2]] x_min min(p[0] for p in ps) y_min min(p[1] for p in ps) x_max max(p[0] for p in ps) y_max max(p[1] for p in ps) bw (x_max - x_min) / w # 归一化宽度 bh (y_max - y_min) / h # 归一化高度 cx ((x_min x_max) / 2) / w # 归一化中心 x cy ((y_min y_max) / 2) / h # 归一化中心 y f.write(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)输出行格式固定是“类别ID 中心x 中心y 框宽 框高”。类别 ID 写成 0因为这份数据集是单一类别如果你的数据里混了行人、电动车就必须在建 data.yaml 时固定好 ID 映射不能只靠脚本里的字符串判断。一个容易被忽略的细节是 data[imagePath] 在 Labelme 新版本里可能只存文件名不存目录批量转换前要先拼接好完整路径。转换完还要做一次反向校验把 txt 里的归一化坐标乘回原图宽高画框对比原标注确认没有系统性偏移。2.3 违停判定不是 YOLO 干的检测框加禁停区规则才是完整逻辑很多人误以为“违停检测”是一个端到端模型实际上 YOLOv8 只负责找到自行车判断“是否违停”由应用层规则完成。常见做法是在 GUI 里预设一个多边形禁停区域检测到自行车后取检测框底部中心点用 OpenCV 的 pointPolygonTest 判断该点是否落在多边形内。# gui/alarm_zone.py 中的判定片段 import cv2 def is_in_forbidden_zone(zone_polygon, box): # box [x1, y1, x2, y2]均为原始帧像素坐标 foot_x (box[0] box[2]) / 2.0 foot_y box[3] # 取框底部 y接近车轮触地点 res cv2.pointPolygonTest(zone_polygon, (foot_x, foot_y), False) return res 0 # 0 表示正好在边界上也判违规为什么取底部中心而不是框的几何中心因为目标检测框通常比车身的真实着地位置偏上直接取中心会把地面上投影算错自行车停在围栏边沿时尤其容易误判。zone_polygon 的坐标空间必须和推理帧一致GUI 里的视频画面如果做了缩放禁停区也要同步换算否则画的时候看着准跑起来全偏。我一般是加载完视频后先取一帧分辨率再让用户在画面上用鼠标画多边形把坐标存成配置文件下次启动直接读。2.4 权重选型CPU 推理场景为什么倾向 yolov8s模型权重对应着推理速度和精度的平衡。这里列三个常见选项方便你自己改权重时做判断模型参数量CPU 推理帧率精度表现适合场景yolov8n最小约 30 FPS 以上小目标容易漏检快速验证、嵌入式yolov8s中等约 15-25 FPS均衡CPU 部署的常规选择yolov8m/l较大明显掉帧精度更高有独立 GPU 的演示环境这里有个常见的选型误区仅凭训练时的显卡定模型。大家的笔记本可能是 1660Ti 甚至 4070训练时上 yolov8m 完全没压力但答辩演示的电脑不一定是同一台如果对方机器只有核显推理会卡得没法看。我的建议是训练阶段可以先用官方 yolov8s.pt 做迁移学习跑通了再去对比更大模型交付的权重始终以“运行环境”为约束。资源里按 CPU 部署场景来组织权重档位和 GUI 的默认配置是匹配的你先不要为了追求精度直接换成 m等全链路跑通再逐步升级不迟。3. 部署与安装Ubuntu 20.04 CPU 版环境搭建与 PyQt5 安装的两处翻车3.1 为什么用 venv 而不是直接装进系统 Python在 Ubuntu 20.04 上配置 YOLOv8 CPU 环境最容易翻车的就是直接在系统 Python 里 pip install。系统自带的 Python 3.8 的 site-packages 被 apt 管理pip 往里面装东西时常碰到外部管理环境错误或者与旧版 setuptools、wheel 冲突。另外 PyTorch、OpenCV、PyQt5 之间的版本约束较紧混装会导致 import 时才能暴露的错误排错成本高。所以我习惯在项目根目录建独立 venv这套资源里的部署教程也是这么做的环境尽量干净答辩现场复现时顺便把这一步展示出来能省掉很多解释。另一个好处是 CPU 版 PyTorch 和 CUDA 版的依赖并不完全相同。如果一开始就装错了版本后期在普通笔记本上会出现能 import 但推理速度异常慢的现象且没有明显报错——这类问题最难定位属于环境层的“黑匣子”。3.2 完整安装命令从建虚拟环境到装好 GUI下面这组命令可以在不带独显的机器上跑通。在 20.04 的干净系统上按这个顺序执行基本一次通过# 1. 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 2. 安装 CPU 版 PyTorch不指定 index-url 会下载 CUDA 版又大又用不上 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 3. 安装 YOLOv8 官方库opencv-python 会被作为依赖一起装上 pip install ultralytics # 4. 安装 GUI 与标注工具 pip install pyqt5 labelme # 5. 补 Linux 下 Qt 运行时缺的图形库 sudo apt install -y libxcb-xinerama0 libegl1 libxkbcommon-x11-0顺序上有个细节先装 torch 再装 ultralytics否则 ultralytics 在安装时如果检测不到 torch会自动去拉一份符合它要求版本的 torch可能把刚装好的 CPU 版覆盖掉。OpenCV 需要从 pip 装而不是 Ubuntu 软件源里的 python3-opencv——20.04 自带的 OpenCV 4.2 太旧Ultralytics 对 opencv-python 有版本下限要求直接 apt 装会在运行时出现函数找不到的报错看起来是代码问题其实是依赖版本问题。如果你机器上有 NVIDIA 显卡把第 2 步的 --index-url 去掉pip 会自动拉对应的 CUDA 版但训完在 CPU 机器上部署时别忘了重新创建一个 CPU 环境不要图省事用原来的环境否则运行时会因为缺少 CUDA 相关库整个起不来。3.3 Labelme 报“无法安装 PyQt5”的典型链路与处理检索里“labelme 无法安装 pyqt5”是个高频词。实际遇到的情况大多是同一个链路直接 pip install labelme它自动拉 PyQt5而 PyQt5 的 wheel 有几十 MB默认源下载中途超时整个安装被迫回滚最后 labelme 和 PyQt5 都没装上。这问题看起来是 labelme 的根因是 PyQt5 没装成。# 先单独装 PyQt5指定国内镜像源避免超时 pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple # 再装 labelme此时 PyQt5 已存在安装速度会快很多 pip install labelme -i https://pypi.tuna.tsinghua.edu.cn/simple如果 Python 版本偏新PyQt5 没有对应轮子pip 可能会尝试用源码编译这时会报出“Building wheel for PyQt5”等很久最终失败。解决方法是把 Python 用到 3.8-3.10 区间这份资源在 Ubuntu 20.04 上默认 3.8 就是安全的。还有一类问题不在安装而在运行在 PyCharm 里打开项目明明终端里能 import PyQt5界面却全是红灯。基本可以断定是解释器没切换——Settings 里项目解释器要指向 venv/bin/python不能直接用系统 python。这个问题我见过太多次代码一行没写错卡在了环境选择上。3.4 安装完先跑一次环境自检环境装好后先别急着训练用一条命令把所有关键库导入一遍。这样能把缺失的运行时依赖提前暴露出来而不是等 GUI 启动时才报错。python -c import torch; print(torch, torch.__version__); import cv2; print(opencv, cv2.__version__); import ultralytics; print(ultralytics, ultralytics.__version__); from PyQt5.QtCore import QT_VERSION_STR; print(pyqt5, QT_VERSION_STR)命令里每一项如果输出正常说明核心依赖都在。常见的失败点有两个opencv 报 numpy 版本不兼容是 opencv-python 与 numpy 版本矩阵对不上可以单独升级 numpy 再试PyQt5 导入时报 missing Qt 插件是上一节提到的图形库缺失先把 apt 补丁打完再跑。自检不影响训练但它能把环境问题压缩在十分钟内解决而不是训练到一半才暴露。4. 训练自己的违停数据标注、格式转换、超参与损失曲线判读4.1 标注策略框到哪里才算框对了把数据集准备好之后第一步是标注。用 Labelme 画矩形框几个细节决定训练效果矩形框要把车轮完全包进去不能只框车架如果只框骨架模型学到的特征是“没有轮子的自行车”测试时遇到遮挡很容易漏检。类别名在所有图片里必须统一默认 bicycle 就全用 bicycle不要一张写 bicycle 另一张写 Bike。靠近图像边缘的自行车也要正常标注不要怕框不完整YOLO 对边缘目标有裁剪增强反而不标会让模型学成“边缘不该有车”。遮挡场景要单独补一批。违停数据集里常见自行车型号不一、有遮挡如果全是干净摆放的样本训练出来的模型一遇到真实监控视角就判不准。这套资源自带数据集的标注风格是矩形框加 bicycle 单类别。我自己标注时习惯先用模型辅助预标注再人工修正比纯手工快一倍但第一次做还是建议纯手动过一遍能直观感受正负样本边界。4.2 批量转换脚本JSON 目录整体转成 YOLO txt单文件转换脚本前面已经给了核心逻辑实际数据集往往是几百个 JSON 一起转。下面这个批量版本会把 datasets/labelme_json 下所有 JSON 转成 txt输出到 datasets/labelsimport json, os, glob from PIL import Image def labelme2yolo(json_path, out_dir): with open(json_path, encodingutf-8) as f: data json.load(f) img_path data[imagePath] # 兼容 Labelme 新版本只存文件名的情况按需拼接 if not os.path.isabs(img_path): img_path os.path.join(os.path.dirname(json_path), img_path) w, h Image.open(img_path).size txt_name os.path.splitext(os.path.basename(json_path))[0] .txt lines [] for shape in data[shapes]: if shape[label] ! bicycle: continue ps shape[points] x_min min(p[0] for p in ps) y_min min(p[1] for p in ps) x_max max(p[0] for p in ps) y_max max(p[1] for p in ps) bw (x_max - x_min) / w bh (y_max - y_min) / h cx ((x_min x_max) / 2) / w cy ((y_min y_max) / 2) / h lines.append(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n) with open(os.path.join(out_dir, txt_name), w) as f: f.writelines(lines) for jf in glob.glob(datasets/labelme_json/*.json): labelme2yolo(jf, datasets/labels)这段脚本最需要注意的参数是 img_path 拼接。Labelme 不同版本在 JSON 里记录 imagePath 的方式不一样有的记全路径有的只记文件名如果不处理批量转换时要么读不到图要么拿错尺寸做归一化最后生成的标注全部偏移。转换完成后要抽查几个 txt打开对应图片按归一化坐标换算回像素画框确认框和原标注一致再进训练否则数据错了训练多久都白搭。4.3 data.yaml 与训练命令普通电脑上的参数参考训练前要写 datasets/bike.yaml内容大致是train: datasets/images/train val: datasets/images/val nc: 1 names: [bicycle]路径建议给绝对路径或相对项目根目录的路径不要用 ~YOLO 在解析时对波浪号展开支持不可靠容易报路径找不到。训练命令用这条yolo taskdetect modetrain \ modelyolov8s.pt \ datadatasets/bike.yaml \ epochs100 imgsz640 batch16 workers4 seed42 \ patience20 cacheFalse几个参数按运行机器取舍epochs 设 100 配合 patience20意思是 20 轮验证集没提升就早停不用死等 100 轮batch 在 1660Ti 6G 显存上可以到 16CPU 纯软跑则降到 8否则内存会吃紧workers 在 Linux 上开 4Windows 上反而容易出 multiprocessing 报错Windows 用户建议设成 0。cacheFalse 是特意加的——默认不缓存避免大数据集一次性读入内存被 OOM如果机器内存充足可以改成 cachedisk只缓存到磁盘训练速度也有提升。4.4 损失曲线判读训练日志不能只看精度训练结束后runs/detect/train 目录下会生成 results.csv 和损失曲线图。很多人只看最后的验证集精度一旦精度不够就盲目加数据。实际该先看曲线形状。下面这段代码用 results.csv 画 box_lossimport pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) # ultralytics 生成的 CSV 列名前后有空格先清理 df.columns [c.strip() for c in df.columns] plt.plot(df[epoch], df[train/box_loss], labeltrain box_loss) plt.plot(df[epoch], df[val/box_loss], labelval box_loss) plt.xlabel(epoch) plt.ylabel(box_loss) plt.legend() plt.grid(True) plt.savefig(loss_curve.png, dpi150)判读经验train/box_loss 下降、val/box_loss 跟着下降是正常的train 一直降、val 在某个轮次后掉头上升说明过拟合要加数据增强或早停两边都不降先回去看标注有没有问题再看学习率是不是设太高、数据量是不是太少。这套代码不仅画损失还能顺手画 mAP50把 val 列里的 metrics/precision(B)、metrics/recall(B) 一并画出来。但要记住损失曲线只是一个诊断工具它告诉你模型在不在学不告诉你模型学得好不好——最终判据还是要放到 unseen 测试集上的表现。4.5 训练后第一件事先跑验证集而不是直接上 GUI训练完把模型拷到 GUI 目录前先做一次独立的验证yolo taskdetect modeval \ modelruns/detect/train/weights/best.pt \ datadatasets/bike.yaml这一步会输出每个类别的 precision、recall、mAP50 和 mAP50-95。在 GUI 里看的是“这个框框得准不准”验证集评估看的是“整体平均表现”两者互补。如果你的 mAP50 低于 0.8建议先回到数据和训练参数去调而不是急着在界面上看效果。GUI 只能放大单帧里的问题统计结论必须靠验证命令拿到。5. 避坑排查训练和 GUI 联调中最常见的四个问题5.1 训练启动就报 Dataset not found现象执行训练命令后报 train dataset not found 或 val dataset not found后面跟一串路径。原因data.yaml 里的 train/val 路径与当前工作目录不对应。最常见的是路径写得太绝对换一台机器或换文件夹路径就失效或用了 ~ 展开YOLO 对波浪号支持不可靠。解决把 data.yaml 改成相对项目根目录的路径并确保在项目根目录启动。例如train: datasets/images/train val: datasets/images/val nc: 1 names: [bicycle]如果还报错在 Python 里打印一下当前工作目录确认是不是在项目根目录。这个坑几乎每个第一次训练的人都会踩不是模型的问题是路径约定问题。从资源包里解压后先跑一下数据校验脚本确认图片数量和 txt 数量一致再去训练。5.2 cacheTrue 在普通电脑上直接把内存吃满现象训练初始化阶段输出显示 Loading dataset images 一直卡住随后进程被 OOM Killer 杀掉日志里出现 Killed。原因ultralytics 的 cache 参数如果设成 True会把数据集所有图片解码后缓存在内存里。数据集如果几千张CPU 机器内存只有 16G一次性缓存必然撑爆。这是一个很典型的“看着像代码问题实际是配置问题”的翻车现场。解决训练命令加 cacheFalse。如果确实想提速用 cachedisk让 ultralytics 把缓存写成磁盘文件而不是全部压进内存。在 1660Ti 或纯 CPU 环境建议先 False跑通一个 epoch 再调优不要一上来就追求加载速度。5.3 GUI 启动即报 Qt xcb 插件错误现象python gui/main_window.py 刚执行终端输出 could not load the Qt platform plugin xcb窗口直接退出。原因PyQt5 装在 Python 环境里了但它运行时依赖的 Linux 图形库缺失。Qt 通过 xcb 平台插件连接图形会话缺 libxcb-xinerama0 这类库时插件加载失败。这在最小化安装的 Ubuntu 20.04 上尤其常见。解决安装缺失的库sudo apt install -y libxcb-xinerama0 libegl1 libxkbcommon-x11-0如果装完还报错检查是否在远程无桌面环境里运行——GUI 程序需要有图形会话远程环境下要么用 X11 转发要么把 GUI 模式改成只输出检测结果不弹窗。这个坑也容易出现在云服务器上很多人把带界面的项目部署到无桌面的服务器上窗口当然起不来。5.4 界面卡顿与告警闪电式触发现象视频或摄像头推理时界面帧率明显低拖动窗口卡顿一辆自行车停在禁停区边缘告警在一两秒内反复响。原因两个问题叠加。第一推理和绘图放在 UI 主线程里跑PyQt5 的事件循环被阻塞界面自然会卡。第二单帧判定没有加时间窗口只要检测框底部中心点跨过边界线信号就在“区内/区外”之间来回跳告警状态跟着闪烁。解决推理放到 QThread 里每推理完一帧通过信号把结果抛给主线程刷新画面告警要加去抖逻辑连续 N 帧命中禁停区才触发连续 M 帧不命中才解除。这个方案在资源里的 main_window.py 已经实现了一部分如果你自己从头写 GUI重点就是线程和状态机这两块。具体到参数N 和 M 的取值要区分视频源视频流一般 N5、M10抓拍图片只有单帧无法用连续帧判定应该改用置信度阈值和区域重叠比例来降误报规则不一样。6. 进阶验证把项目从“能跑”调到“能答辩”的几个动作6.1 告警去抖连续帧投票是答辩演示的救命逻辑上面 5.4 提到告警闪烁解决它只需要一个简单的状态机同一辆自行车连续 N 次检测框都落在禁停区才确认违停连续 M 次不在才解除。这样答辩时的演示不会因为一帧抖动就让蜂鸣器跟着抖。class AlarmDebouncer: def __init__(self, hit5, release10): self.hit hit # 连续命中多少帧触发 self.release release # 连续离开多少帧解除 self.cnt 0 self.alarm False def update(self, in_zone): if in_zone: self.cnt min(self.cnt 1, self.hit 1) else: self.cnt self.cnt - 1 if self.cnt 0 else 0 self.alarm self.cnt self.hit # 达到阈值即保持告警 return self.alarm注意 if 分支里的写法in_zone 时计数只增到 hit1离开时递减这样告警一旦触发至少等 release 次帧后才能解除不会来回横跳。测试时把 hit 设 5、release 设 15在 25FPS 的笔记本摄像头下效果合适帧率越低越要调大这两个值否则一帧相当于 40ms触发条件会变得太灵敏。6.2 验收量化FPS、告警准确率与禁停区可视化“能跑”和“能答辩”之间差一组量化数据。建议跑一段固定视频记录三个数字推理 FPS、检测查全率、告警误触次数。资源里 GUI 右下角本来就显示 FPS可以再加一个统计面板记录总帧数、告警帧数和误报数。答辩时把这三组数字往界面上一放比口头说“效果很好”有力得多。可视化上再动一下把禁停区多边形在界面上用半透明色块画出来自行车违规后再给检测框加一个红色闪烁动画。用 QPainter 在 paintEvent 里覆盖图形即可注意保持像素坐标系一致。这个动作对演示的帮助非常大评委能一眼看到区域和告警的因果关系。从那以后我每次做检测告警项目都强制把“连续帧去抖、坐标空间核对、量化指标记录”三件事走一遍先测后调不做完不碰 UI。如果你也在做类似题目直接把这份源码包复现一遍再按上面的验证流程跑三组数据会比从零开始顺畅很多。希望帮到你。本文还有配套的精品资源点击获取