
简介一套基于YOLOv8与PyQt5的共享自行车识别检测系统面向计算机相关专业学生、教师及企业开发者适用于课程设计、毕业设计、竞赛演示也可扩展为违规停放检测告警项目。系统已跑通并达到98%准确率提供自行车数据集、训练好的权重、评估曲线、精美GUI界面和分步部署说明方便快速复现与二次开发。压缩包共889个文件约466MB核心包括jpg图像与txt标注构成的数据集、py源码与缓存、yaml配置、pt权重、ui界面文件、md文档以及少量辅助脚本和示例视频目录清楚便于按需查阅目前已有1849人学习下载。除了直接运行的检测计数功能内容中还包含评估结果表格、Docker配置、静态页面和推理源码有助于理解从数据处理到模型调用的完整流程。配套文档覆盖安装、调用与界面操作用户可在此基础上扩展功能或向作者咨询适合入门和进阶场景。1. 共享自行车乱停乱放为什么盯着监控做检测是最快的路子共享自行车在带来出行便利的同时也让城市管理者头疼早晚高峰的地铁口、写字楼门前经常堆满密密麻麻的单车行人都过不去。物业和城管一天派人清运好几次成本高且时效差。我参与过某街道的市容巡检项目方案换了好几轮最终落到一条路线用固定摄像头或巡检摄像头实时识别单车违规停放超过设定停留时间就触发告警让运维人员精准处理。这条路线里YOLOv8负责“看得见”PyQt5负责“看得懂”——前者做目标检测后者把检测结果变成可视化界面和告警事件二者配合能在普通办公电脑上跑起来也方便演示和上报。这套基于YOLOv8PyQt5实现的共享自行车识别检测系统正因为门槛低、见效快成了很多违规停放检测告警项目首选的开源方案。这篇文章把我自己从数据集到GUI落地的完整路径讲一遍包含模型训练参数、界面布局思路和几个容易翻车的坑。2. YOLOv8检测原理与系统架构为什么这套组合更适合告警场景2.1 YOLOv8相对v5/v7的改动检测效果提升在哪里做目标检测绕不开YOLO。选择YOLOv8不是因为版本号最大而是它的设计更适合“小目标密集场景”的监控画面。共享单车在摄像头视角下经常三五辆挨在一起车身细长车筐和车轮特征不明显老版本模型容易出现漏检和误检。YOLOv8改了几处关键结构第一backbone用C2f模块替换了C3模块。C2f借鉴了梯度流设计的思路把不同层级的特征图进行更多分支融合让小目标的纹理信息传得更深。实际测试中同样训练50轮v8在单车这类细长目标的mAP50上比v5s高2到3个百分点这在告警场景里意味着少漏检。第二检测头从anchor-based改成了anchor-free。v5需要预设anchor尺寸训练前得用K-means聚类如果聚类结果不贴合实际目标尺寸召回率会很差。v8直接在特征图上回归物体中心点和宽高省掉了聚类步骤也简化了部署配置。对于共享单车这种长宽比极不统一的目标横放竖放斜放都有anchor-free的适应性明显更好。第三损失函数用DFLVFL的组合。DFL让框回归的置信度分布更平滑VFL解决正负样本不平衡问题。监控画面里绝大多数区域是背景正样本比例极低VFL能有效防止模型训练时被背景主导。以上改动带来的结果就是YOLOv8可以在较低算力下获得较高的检测精度且导出ONNX、量化部署都比较顺。这套系统我建议直接用YOLOv8s精度与速度的平衡最好。v8n虽然快但在密集停放场景下漏检率高v8m又会在CPU上跑不动GUI体验会变差。2.2 系统整体架构检测端、界面端、告警端各自负责什么整个系统的模块划分要清晰否则后面加功能时会改到想哭。我的做法是把系统拆成三层数据接入层、检测推理层、业务展示层。数据接入层负责获取视频流。本地USB摄像头、RTSP监控流、本地视频文件三路源都要能接入。RTSP流用的是OpenCV的VideoCapture注意设置缓存帧数否则画面延迟越来越大。这个模块独立出来方便切换不同摄像头品牌。检测推理层是核心加载YOLOv8的权重文件对传入的每一帧做推理输出检测框、类别置信度、目标坐标。这一层用单独的线程跑不能和界面主线程混在一起否则一卡一卡的告警功能也会延迟。业务展示层是PyQt5的职责。它负责实时显示视频画面和检测框同时维护一个告警事件列表。当某个自行车目标在画面中停留超过设定阈值就生成一条告警记录包括截图、时间、位置、置信度。这样用户在界面上既能看实时画面也能查历史告警。告警模块再往下可以扩展对接钉钉/企业微信群机器人推送告警截图做数据库存储方便事后溯回。我最初只做了本地日志和截图保存后来又加了CSV导出功能因为街道管理人员要求把告警数据导成表格做月度考核。system/ ├── main.py # 程序入口 ├── detector.py # YOLOv8推理封装 ├── camera.py # 视频流接入 ├── gui/ │ ├── main_window.py # 主界面 │ ├── alarm_list.py # 告警列表组件 │ └── video_widget.py # 视频显示控件 ├── utils/ │ ├── config.py # 参数配置 │ └── logger.py # 日志与截图工具 └── weights/ └── best.pt # 训练好的模型权重这个目录结构有两点好处第一检测逻辑和界面逻辑分离即使你不想用PyQt5想换成Flask做Web界面detector.py和camera.py可以原封不动地复用。第二告警逻辑独立在utils/logger.py里想要改变告警判定规则时不需要动界面代码。2.3 环境选型PyQt5版本与CUDA匹配的几个注意点环境配置是新手最容易卡住的地方。我从零开始配过两遍踩了不少坑这里直接给结论。Python版本我建议用3.9到3.11之间的版本PyTorch和PyQt5的兼容性最好。PyQt5直接用pip安装即可。CUDA版本配PyTorch时注意不要盲目装最新版。比如CUDA 12.x配PyTorch 2.x虽然能跑但某些显卡驱动版本低的话会直接报“no kernel image is available”错误。稳妥做法是先用CPU模式跑通整个流程再装CUDA版的PyTorch。PyQt5的版本也有讲究。最新的PyQt5 5.15.x在Python 3.10以上可能会有中文显示乱码的小问题建议用5.15.9或5.15.10这两个版本对Qt插件的加载处理比较稳定。另外如果设计界面时用了Qt Designer生成的.ui文件确保装了pyqt5-tools转换工具pyuic5在Windows下才随包提供。另一个容易忽略的点是OpenCV的contrib包。监控视频的推流拉流用到的rtsp协议在基础版OpenCV里已经支持但如果要用到跟踪算法比如ByteTrack需要额外安装opencv-contrib-python。我一般直接装contrib版一次到位。装完环境后建议跑一个最小验证脚本确认torch能调用GPU。import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))如果输出显示CUDA不可用检查NVIDIA驱动版本和PyTorch的CUDA版本是否匹配不建议继续往下走因为后面训练会慢好几倍。3. 数据集准备与模型训练从采集到训练出一条能用不翻车的权重3.1 数据集采集与标注类别怎么定、标注框怎么画训练效果的上限由数据集决定这句话我说过无数次。共享自行车检测任务的数据集核心问题是类别定义和场景覆盖。先想清楚检测目标。违规停放检测不需要区分品牌或颜色所以类别设置一个bicycle就够了。但如果你在特定场景还需要检测电动车、三轮车建议拆分类别因为电动车和自行车的形态差异会影响检测头的回归效果。每类目标少于500张的训练效果都不会好。采集数据时注意场景多样性。同一辆车在晴天、阴天、夜晚、逆光下的表现完全不一样。用一个月内不同时间段、不同天气下的监控画面来做数据比一天内拍一千张同样角度的图要有用得多。我在项目中加入了“夜间”和“雨天”两类特殊场景生成告警时若置信度较低则标记为“低置信度告警”避免漏报。标注工具用labelImg或labelme都可以。我偏好labelImg因为它对矩形框支持友好、导出YOLO格式方便。标注框的边界要尽量贴近车把和车轮的极限点。共享单车常见的标注错误有两种一是只标车身不标车轮导致长宽比失真二是把相邻两辆挨得太近的车画成一个框模型训练时框的中心点位置会不稳定。标注完成后检查一下每张图的目标数量。如果单张图中自行车数量超过10辆建议分析周边的遮挡情况不要贪多贪全。我处理过一个极端画面地铁口堆了三十多辆车模型在密集堆叠处的漏检率很难靠调参压下来。数据划分我习惯按7:2:1训练集:验证集:测试集并且打乱顺序。打乱用Python的random.shuffle即可不要手动拖动。import os import random data_root datasets/bicycle train_ratio, val_ratio 0.7, 0.2 imgs [f for f in os.listdir(os.path.join(data_root, images)) if f.endswith(.jpg)] random.shuffle(imgs) train_count int(len(imgs) * train_ratio) val_count int(len(imgs) * val_ratio) train_imgs imgs[:train_count] val_imgs imgs[train_count:train_count val_count] test_imgs imgs[train_count val_count:]上面这段代码通过列表切片划分数据集确保每辆车只出现在一个集合中避免模型“见过”验证集中的图片导致评估结果虚高。random.shuffle每次运行结果不同所以保存划分后的文件名列表方便复现。标注文件格式和yaml配置准备好后就可以开始训练了。3.2 训练配置与命令batch、epoch、imgsz等参数的经验值训练用ultralytics的CLI或Python API都行。CLI简单Python API灵活。我一般用Python脚本因为它方便记录训练过程中的中间指标。关键参数的经验值如下imgsz设为640或960。共享自行车细长960能让小目标特征保留更完整但显存占用翻倍。8GB显存建议用640起步实测mAP差距约1-2个点。batch设为8到16。batch太大容易收敛过快导致过拟合太小则噪声大训练不稳定。我的经验是8GB显存用batch816GB用batch16。epochs设置为100到150。50轮时模型可能尚未收敛尤其是数据量少时。我最常用120轮配合早停机制。patience设为20连续20轮验证集mAP不升则停止训练节省时间。from ultralytics import YOLO model YOLO(yolov8s.pt) results model.train( databicycle.yaml, epochs120, imgsz640, batch8, patience20, workers4, device0, cacheTrue, namebicycle_yolov8s, )这段代码加载了预训练权重yolov8s.pt作为初始权重在自有数据集上微调。pretrained权重能显著加快收敛不推荐从头训练。data参数指向数据集配置文件里面包含图片路径和类别定义。cacheTrue把图片提前缓存进内存训练时减少磁盘IO等待。workers是数据加载线程数Windows下设为0更稳定设为4以上偶尔会报进程相关错误。训练完会在runs/detect/bicycle_yolov8s/目录下生成weights/best.pt和last.pt。best.pt是验证集上表现最好的权重部署用它last.pt是最后一轮的一般仅用于断点续训。还有个参数值得一提augment。YOLOv8默认开启轻度数据增强随机翻转、颜色扰动如果发现训练集本身已经很丰富可以适当调低增强强度防止模型把有效特征也当作噪声忽略掉。# bicycle.yaml path: datasets/bicycle train: images/train val: images/val test: images/test names: 0: bicycle注意path字段使用相对路径时以当前运行目录为基准建议写绝对路径或用os.path.abspath转一下。我在不同机器上跑训练时就因为路径问题折腾了半天。3.3 训练结果怎么看mAP、PR曲线、混淆矩阵的读法训练完成后不要急着装GUI先在验证集上把结果搞明白。打开runs/detect/bicycle_yolov8s/目录下的results.csv文件里面有每一轮的指标变化。metrics/precision(B)和metrics/recall(B)分别代表精确率和召回率。对违规停放告警来说召回率比精确率更重要——漏报一辆乱停的车比误报更严重。如果训练结束recall低于90%优先考虑增加数据而非调参。metrics/mAP50(B)是IoU阈值0.5下的平均精度共享单车这类目标通常能到95%以上。如果mAP50只有80%左右说明模型学到了部分特征但有明显盲区检查是否训练集里某类场景太少。PR曲线和混淆矩阵在验证集中更有参考价值。PR曲线靠左上的位置越饱满正样本置信度越集中。混淆矩阵能看到“背景被误检为自行车”的比例这个指标在复杂背景监控画面里很关键。我在树影摇动的场景中遇到的误检就是靠混淆矩阵定位出来的。另外打开几张测试图片跑一下推理。from ultralytics import YOLO model YOLO(runs/detect/bicycle_yolov8s/weights/best.pt) results model.predict(test_images/sample1.jpg, conf0.5, saveTrue)conf设为0.5会过滤低置信度检测框但监控画面中远处的小目标置信度通常低于0.5容易漏检。建议保存两套权重一套conf0.25用于白天远距离巡检一套conf0.6用于近景告警判定。实际部署时告警模块中置信度的判断阈值要和检测模型本身的阈值区分开。4. 基于PyQt5的告警GUI开发把检测结果做成能直接用的界面4.1 界面布局实时画面、检测结果、告警记录三区怎么排一个用于违规停放检测告警的GUI核心是让巡检人员一眼看懂当前状态。界面布局上我采用三区划分左侧是主视频显示区占界面约60%宽度右侧上方是实时检测参数区包括当前帧率、检测到的车辆数、置信度等信息右侧下方是告警事件列表按时间倒序展示历史告警记录。用Qt Designer绘制的话布局结构是QHBoxLayout作为根布局左侧放一个QLabel用于显示OpenCV图像右侧用QVBoxLayout分上下两区块。视频显示区建议重写QLabel的paintEvent避免每帧QPixmap重复创建造成的性能损耗。右键菜单可以在视频画面上直接点击检测框查看目标坐标和置信度这个功能看似花哨实际排查误检时非常有用。我加了这个功能后发现告警准确率提升了原因不是算法变好了而是人工审核更容易了。告警列表用QTableWidget实现列设计为告警编号、发生时间、检测框坐标、置信度、停留时长、状态待处理/已处理。双击某条告警记录可以跳转到对应的截图文件这个交互逻辑用信号槽实现。界面底部放一个控制栏包含开始检测、暂停检测、导出告警报表三个按钮。4.2 核心代码摄像头取流、推理线程、信号槽通信PyQt5和检测模型的结合最大的坑是线程模型。PyQt的UI操作必须在主线程中执行而推理要放到工作线程二者通过信号槽通信。由于推理耗时较长如果直接在主线程中调用模型推理界面必然卡死。正确的做法是用QThread子类来跑推理循环每次推理完成后发射一个自定义信号把检测结果交给主线程更新UI。import cv2 from PyQt5.QtCore import QThread, pyqtSignal class DetectThread(QThread): frame_ready pyqtSignal(object, dict) alarm_triggered pyqtSignal(dict) def __init__(self, model_path, source0): super().__init__() self.cap cv2.VideoCapture(source) self.model YOLO(model_path) self.running True def run(self): while self.running: ret, frame self.cap.read() if not ret: continue results self.model(frame, verboseFalse)[0] boxes results.boxes.xyxy.cpu().numpy() confs results.boxes.conf.cpu().numpy() self.frame_ready.emit(frame, {boxes: boxes, confs: confs}) def stop(self): self.running False self.cap.release()这段代码中DetectThread继承QThread在run方法里循环读取视频帧、执行推理、通过信号发射结果。frame_ready信号有两个参数原始帧和检测结果字典。主线程收到信号后绘制检测框并更新界面。注意frame_ready信号携带的frame是一个numpy数组Qt信号默认不支持numpy类型直接传递但pyqtSignal(object, dict)中object可以兼容任意Python对象所以没有问题。如果信号定义为pyqtSignal(np.ndarray, dict)Qt会有告警提示但运行正常也不建议这么做。Qt信号与槽的另一个关键约束信号不能直接传QImage对象因为QImage的底层数据可能被垃圾回收。务必在信号参数中传numpy数组或字节流在槽函数中再把numpy数组转为QPixmap显示。我在项目中转成QPixmap时需要做一步BGR转RGB否则画面颜色偏蓝。def update_frame(self, frame, detect_info): rgb_image cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape qimg QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg))4.3 违规停放告警逻辑停留时间判定与截图留存方案有了检测框之后告警逻辑才是业务核心。共享自行车违规停放有一个重要特征停留时间很长。如果是骑车经过的行人目标在视频画面中出现的帧很少违规停放的车辆则会连续出现在多帧画面中位置几乎不变。利用这一点既能区分真实违规又能过滤大部分误检。具体做法是把每一帧的检测框坐标保存到目标列表中按IoU判断是否为同一个目标。def update_track(self, boxes, frame_time): for box in boxes: x1, y1, x2, y2 box.astype(int) matched False for track_id, track in self.tracks.items(): if self.calc_iou(track[last_box], box) 0.6: track[last_box] box track[start_time] track.get(start_time, frame_time) track[last_time] frame_time matched True break if not matched: track_id len(self.tracks) 1 self.tracks[track_id] { last_box: box, start_time: frame_time, last_time: frame_time }这段代码用简单的IoU匹配代替了复杂的跟踪算法性能开销小对固定摄像头足够用。track的起始时间是第一次出现的时间如果当前时间减去start_time超过设定的停留阈值比如5分钟就触发告警。告警事件记录当前帧的截图文件名以告警编号和时间命名方便后续复核。停留时长阈值可以在界面上的设置项中调整。监控区域若是地铁口建议设为3分钟——行人短暂停留不为过超过3分钟基本可以确认是停放。办公楼下建议5分钟因为早晚高峰员工停车动作较快。告警触发后把信息发到另一个信号alarm_triggered主线程接收后插入表格并弹出系统托盘通知。截图保存在本地文件夹路径可按日期生成避免单目录文件过多影响检索。def save_alarm_screenshot(self, frame, track_id, frame_time): save_dir os.path.join(alarms, datetime.now().strftime(%Y%m%d)) os.makedirs(save_dir, exist_okTrue) filepath os.path.join(save_dir, f{track_id}_{frame_time}.jpg) cv2.imwrite(filepath, frame)5. 落地避坑6个让你翻车的真实问题和排查思路5.1 摄像头画面延迟越来越大的问题现象程序运行10分钟后画面延迟从1秒增加到10秒以上点击“停止检测”按钮也无法立即退出界面卡死。原因VideoCapture没有设置缓存区大小。RTSP摄像头的底层网络缓冲会持续累积帧而推理速度跟不上帧率导致旧帧堆积延迟不断增长。解决限制缓冲区帧数并跳帧处理。self.cap cv2.VideoCapture(source) self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)同时在推理循环中加入帧间隔控制比如每处理一帧后sleep(0.03)等价于约30FPS上限。这一步能保证新帧实时到达虽然会牺牲一些流畅度。5.2 PyQt窗口点击无响应、提示“QThread: Destroyed while thread is still running”现象关闭窗口时程序报错退出有时直接卡死。原因QThread在窗口关闭前未正确停止或者在窗口析构时线程仍持有摄像头资源。解决在窗口关闭事件中强制停止线程并等待。def closeEvent(self, event): self.detect_thread.stop() self.detect_thread.wait() event.accept()注意stop()里要释放VideoCapture否则下次启动时摄像头设备仍被占用。5.3 训练时显存不足OOM现象设置batch16imgsz960后训练中途抛出RuntimeError: CUDA out of memory。中途终止后重新启动显存仍然占满。原因除了batch和imgsz过大外还有一个隐藏原因——上一次训练未正常释放显存新进程启动时显存被残留占用。解决先清掉残留GPU进程然后降参数。训练前习惯用nvidia-smi检查GPU占用有残留进程就用taskkill强制结束。参数上如果你只有8GB显存老老实实batch8、imgsz640不要贪大。另外cacheTrue会预加载大量图片到内存显存不够时改cacheFalse。5.4 夜间场景漏检严重现象白天测试效果不错一到晚上摄像头补光灯亮度不足车辆轮廓模糊检测框频繁消失。原因训练集中夜间图片占比不够多网络学不到夜间的颜色和纹理特征。解决在数据集中增加30%以上夜间图像并应用亮度扰动数据增强。ultralytics支持在训练配置中开启hsv增强把hsv_h、hsv_s、hsv_v的值适当调大让模型对亮度变化更鲁棒。还有一种做法是推理前对帧做自适应直方图均衡化。import cv2 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(2.0, (8, 8)) enhanced cv2.cvtColor(clahe.apply(gray), cv2.COLOR_GRAY2BGR)CLAHE增强后夜间检测召回率能提升10%左右副作用是可能增加少量误检。5.5 PyInstaller打包后exe运行报“无法定位程序输入点”现象打包成功但双击exe时报找不到Qt相关DLL的入口点或明明装了CUDA却说torch不可用。原因PyInstaller默认不会把所有动态库都收进来PyTorch和PyQt5的依赖项分布在不同包中尤其是OpenCV的FFmpeg相关文件。解决用--collect-all参数强制收集。pyinstaller --collect-all ultralytics --collect-all torch --collect-all cv2 main.py另外建议在打包脚本里加上--noconsole这样运行时不会弹出黑框但调试时还是先去掉这个参数方便看错误输出。如果你的模型包含了.pt文件记得加--add-data把权重文件打包进去。5.6 告警漏报但检测框正常显示现象画面上检测框一直正常但告警记录就是不发。原因停留时间判断需要目标跨多帧匹配如果IoU阈值设得太严格比如大于0.8车辆稍微抖动一下就会断开匹配重新计数。解决把IoU匹配阈值降到0.5到0.6之间同时为每个track增加一个“last_seen”时间如果超过3秒未匹配则移除该track防止把不同目标衔接错误。阈值调低后注意场景中如果目标长时间静止配合停留时长阈值即可避免误报。6. 从Demo到真上线验证方法、性能优化与部署技巧一套检测系统能不能真正用于违规停放告警最终要看它在一个完整场景中的综合指标而不只是某个测试视频里的FPS和mAP。验证我一般分三步走。第一步回放某路口8小时的监控视频让程序连续运行统计检测到目标的总数和告警触发总数第二步人工剪辑出其中1小时片段逐帧数出真实停放行为数量和系统检出数量做对比计算检出率和误报率第三步让运维人员连续试用两周记录他们在实际操作中的反馈——比如告警是否太频繁、界面操作是否顺手。在性能优化上我强烈建议把模型导出成ONNX格式再部署。虽然YOLOv8原生的PyTorch推理已经能做到实时但ONNX在CPU上的推理速度能再快20%到30%且可以脱离PyTorch环境运行。导出后配合onnxruntime-gpu在低端显卡上能跑到稳定的40FPS以上。VAL模式下不需要梯度计算也可以把模型的half精度打开进一步降低显存占用。yolo export modelweights/best.pt formatonnx dynamicTrue opset12 simplifyTrue导出后对比原模型在验证集上的mAP差异一般在0.5%以内可以接受。如果差异较大检查是否开启了dynamic维度某些编辑器不支持动态输入。部署环节分两种。如果只是给内部人员演示或小范围试点PyQt5桌面程序足够如果要覆盖多个路口同时监控建议把检测部分抽成HTTP服务GUI只负责展示和告警。我做了内网版本的“某跨平台系统”后又用Flask包了一层告警回调接口多个摄像头可以并行接入。界面上的另一个实用技巧是告警列表中的数据同时写入SQLite本地数据库而不是直接存内存。程序崩溃重启后历史告警仍能查到。导出报表时直接从数据库读取支持按日期范围和路段过滤更方便做月度统计。最后我有一个习惯所有参数置信度阈值、停留时长阈值、摄像头编号、截图路径都集中在config.py里不散落在代码各处。改一个阈值只需要改配置文件的唯一入口。换新场景时场景A的模型和处理逻辑不会影响场景B。这套共享自行车识别检测系统对我来说最大的感悟是算法模型虽然重要但真正让它被用起来的是界面和告警逻辑的可靠性。希望这篇笔记能帮你少走几段弯路把时间花在数据质量和场景适配这些真正出效果的地方希望帮到你。本文还有配套的精品资源点击获取