YOLOv8与PyQt5实战:构建坑洼路面实时检测桌面工具
2026/9/7 21:55:21 网站建设 项目流程

把 YOLOv8 和 PyQt5 组合在一起做坑洼路面缺陷检测,最大的收益是能在一套桌面工具里同时完成图像检测、视频检测、摄像头实时检测和结果保存。这个项目适合两类人:一类是刚学完 YOLOv8 基础、想让模型不再是命令行黑窗口的人;另一类是正在做道路巡检或路面缺陷数据整理,需要把检测结果可视化、给非技术人员使用的人。整套系统不难,难点在于把模型推理和 UI 交互串得稳。下面按我实际落地时习惯的顺序拆一遍:先确认需求,再配置环境,接着准备模型,最后写 GUI 和排查问题。

1. 先说清楚这套系统到底做什么

1.1 坑洼检测本质上是一个目标检测任务

无论界面做得多复杂,核心还是一句:给定一张路面图片,返回“坑洼(pothole)”的类别、置信度和矩形框。整体上,系统可以拆成两半:YOLOv8 负责目标检测,PyQt5 负责把检测结果展示成人能直接操作的窗口。YOLOv8 是 Ultralytics 开源的检测模型,结构上包含 Backbone、Neck 和 Head,官方提供了不同规模的权重,从 n 到 x 都有,通常我们用 n 或 s 就够了。PyQt5 是 Python 常用的桌面 GUI 框架,能做出按钮、下拉框、表格、图片显示区域和日志窗口。

先想清楚这一点很重要。因为很多人一开始就把精力花在“把界面做得好看”上,结果模型精度和运行速度反而拖后腿。实际做的时候,我建议先把模型部分跑通,再套界面。

1.2 为什么选 YOLOv8 + PyQt5 而不是 Web 端

可能有人会问:用 Flask/FastAPI 做网页展示不是更方便吗?网页端的优势是远程访问,但道路巡检这种场景经常是离线环境,现场一台工控机或笔记本,接上 USB 摄像头就得干活。这时候 PyQt5 的优势很明显:不依赖浏览器,不需要部署 Web 服务,双击启动,直接显示视频流。YOLOv8 的优势是接口简单,训练、验证、预测、导出都有统一命令;PyQt5 的成熟度也足够高,控件丰富,IO 方便。

另外一个实际考虑:PyQt5 适合做成“单机工具”,交付给非技术用户时,只要把 Python 环境一起打进去,或者用 PyInstaller 打包成 exe,对方不需要装深度学习环境。Web 方案遇到的浏览器摄像头权限、跨域、并发连接问题,在桌面端基本不存在。

这个系统还可以拆成四个模块:输入模块、检测模块、显示模块、输出模块。输入模块负责处理图片、视频文件、USB 摄像头;检测模块负责加载模型和执行推理;显示模块负责刷新检测画面、表格和日志;输出模块负责保存标注图、导出检测结果文本和统计数据。

这样拆分之后,出问题的定位速度会快很多。输入模块有问题就看路径、编码和帧读取;检测模块有问题就看模型路径、推理参数和显存;显示模块有问题就看图像格式、像素格式和 Qt 刷新;输出模块有问题就看目录权限、文件名冲突和写入失败。后面所有排查思路,都围绕这四个模块展开。

2. 环境和依赖配置

2.1 先判断 GPU 还是 CPU

很多人会问“YOLOv8 需要用到 GPU 吗?”。答案很明确:训练建议有 NVIDIA GPU;如果只做推理,CPU 也能跑,只是速度受限制。以常见的 GTX1660Ti 6GB 为例,跑 YOLOv8n 的单张 640×640 图片推理,速度很快;跑 YOLOv8s 稍慢但也可接受。如果换成纯 CPU 环境,单张图片可能从几十毫秒涨到几百毫秒甚至一秒钟以上;如果还要处理摄像头实时视频,帧率会很低。

所以配置环境前,先摸清你的机器:

方案硬件条件适合场景预期表现(参考)
CPU 推理任意现代 CPU少量图片检测、学习 Demo单张图片 0.3~2 秒,视频卡顿
NVIDIA GPU 推理GTX1660Ti 及以上 6GB 显存图片、视频、摄像头YOLOv8n/YOLOv8s 单帧几十毫秒到上百毫秒
NVIDIA GPU 训练显存 6GB~8GB 以上自己标注数据集训练需要控制 batch 和 imgsz
NPU/边缘设备RK3588、Jetson Orin Nano 等部署到巡检小车或边缘盒子需要将模型导出为 ONNX/RKNN 等格式

如果你的机器没有 NVIDIA GPU,也不代表不能做这个项目。选 YOLOv8n 权重,把输入尺寸降到 480 或 416,处理单张图片还是可以接受的。但你要清楚:低配置能跑不代表适合批量跑,更不代表适合做实时视频。批量任务中每帧都累积耗时,摄像头实时检测更是对单帧耗时非常敏感。

2.2 安装步骤和常见坑

建议使用虚拟环境,不要把 PyQt5 和 ultralytics 直接装进系统 Python。虚拟环境的好处是隔离依赖,以后打包也方便。可以用 conda 或 venv:

conda create -n pothole python=3.9 -y conda activate pothole pip install ultralytics pyqt5 opencv-python numpy

注意 Python 版本不要太新。比如 Python 3.12、3.13 下 PyQt5 可能出现跳版本兼容问题。更稳妥的是 3.8 到 3.10 之间。具体版本号在你本地确认,不要照抄网上所有安装命令。

常见问题:

  • 装 PyQt5 后程序启动闪退:优先检查显卡驱动、Qt 插件路径、Python 位数是否一致。
  • 下拉框闪退:可能是 QComboBox 的 item 数据问题,也可能是对象被提前释放;要先把运行环境固定住,再查代码。我遇到过一次是 pyqt5-tools 版本和 PyQt5 版本不匹配,在无界面环境里用 Qt Designer 后删除多余插件就好了。
  • opencv-python 和 PyQt5 一起使用时,不要直接混合cv2.imshow和 QLabel 显示:会抢窗口焦点,甚至崩溃。统一用 QLabel 显示转换后的 QImage。

注意:不要把 PyQt5 和 ultralytics 一起装进同一个旧环境里还不做隔离。环境出错时最难排查的不是代码,而是依赖互相污染。

3. 模型准备:从预训练权重到自己的坑洼数据集

3.1 先用预训练权重跑通一条完整链路

在写 GUI 之前,先用官方权重验证环境。执行:

yolo detect predict model=yolov8n.pt source=test.jpg

如果这行命令能输出结果,就说明 YOLOv8 环境基本正常。之后再用自己的数据训练。

在 GUI 里加载的也是同一个 torch 模型。用 ultralytics 的 Python 接口:

from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.predict("test.jpg", conf=0.3, imgsz=640)

这里conf是置信度阈值,调低能召回更多目标,但误检也会增加;调高则相反。实际坑洼检测我一般先用 0.25 到 0.3 观察,再根据路面背景复杂度调整。如果画面里出现大量误检,就把阈值调到 0.4 以上;如果出现漏检,就把阈值降低。

3.2 准备坑洼数据集和标注格式

如果要用自己的数据集,需要把图片和标注文件整理成 YOLO 格式。YOLO 标签数据长这样:

图片:image_001.jpg

对应标签:image_001.txt

内容示例:

0 0.438 0.621 0.221 0.153

含义是:类别 id、归一化中心 x、中心 y、归一化宽度 w、高度 h。

目录结构示例:

datasets/pothole/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── pothole.yaml

pothole.yaml内容:

train: datasets/pothole/images/train val: datasets/pothole/images/val nc: 1 names: ['pothole']

收集坑洼图片时,常见问题是背景单一、角度固定,模型容易过拟合到“黑色区域”而不是“坑洼本身”。所以训练集要尽量包含晴天、阴天、湿润路面、阴影、不同相机高度等场景。标注时,不要把整块大面积破损都标成一个框,但也不要切得过碎。判断标准是:框能不能大致包住一个独立的坑洼区域,同时相邻坑洼不混在一起。

3.3 训练参数和数据增强

数据准备好后,命令可以写成:

yolo detect train data=datasets/pothole/pothole.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=8

新手最容易犯的错误是一上来就把 batch 和 imgsz 拉满。batch=8在 6GB 显存上跑 YOLOv8n 比较接近安全边界;如果显存不足,减小 batch 或 imgsz。imgsz=640是 YOLOv8 的常用输入尺寸,坑洼在画面里偏小时,可以尝试imgsz=800或 1024,但训练和推理速度都会下降。

数据增强方面,YOLOv8 默认带了 mosaic、翻转、颜色抖动等增强。不要把所有增强参数都开满,否则训练集精度和验证集精度会出现较大落差。训练过程中重点看runs/detect/train/exp/results.png,里面包含 train/loss、val/loss、mAP 等曲线。判断标准:损失曲线总体下降,验证集的 mAP 不再上升时,可以先停掉;如果 val loss 明显回升,说明过拟合。

如果自己不太会看指标,可以先看两个点:训练结束后的best.pt能不能在验证集图片上稳定框出坑洼;换几张训练时没见过的真实路面照片,看漏检率和误检率高不高。不要只盯着训练集表现。

3.4 小目标坑洼的改进方向

如果坑洼是小目标,直接用 YOLOv8n 往往漏检。常见改进方向:

  • 提高输入分辨率:imgsz从 640 提到 1024 或更高。
  • 增加小目标检测头:在浅层特征图上增加检测分支,YOLOv8 原始结构里对小目标不算友好,自行改的时候要重新训练。
  • 修改特征融合模块:例如借鉴 C2f 的变体或类似 CFFM 的注意力融合设计,让高低层特征更好融合。
  • 使用更重的模型:YOLOv8s/m 在小目标上通常比 n 有更好表现,但显存占用和速度也要接受。

改网络属于进阶内容。如果不是为了发论文或打比赛,我建议先按原始 YOLOv8,把数据集、输入尺寸和置信度调好,再考虑结构改动。

3.5 导出模型

训练结束后,权重路径是runs/detect/train/exp/weights/best.pt。桌面端直接用.pt文件即可。如果后续要部署到 RK3588、Jetson 等边缘设备,可以导出 ONNX,再根据平台转换成对应格式。导出 ONNX:

yolo export model=runs/detect/train/exp/weights/best.pt format=onnx imgsz=640

导出前要注意模型输入尺寸要和训练一致,否则推理精度可能下降。

4. PyQt5 桌面界面:功能编排比画控件更重要

4.1 界面模块划分

一个基础的坑洼检测界面不需要做得太复杂。建议包括:

区域控件作用
输入区按钮:选择图片/选择视频/打开摄像头决定数据来源
模型区下拉框:模型权重选择切换不同模型 run
显示区QLabel显示原图或检测结果
结果区QTableWidget显示每个框的类别、置信度、坐标
日志区QTextBrowser打印推理耗时、错误信息
控制区开始检测/停止检测控制视频和摄像头流程

整个界面逻辑可以简化成:用户选择输入源,按下检测,界面把任务交给检测线程,检测线程返回结果,界面刷新图片和表格。

选择图片和打开视频是两个不同入口,建议共用同一个检测函数,只在读取方式上分开。很多人把图片检测和视频检测写成两套,后来改参数要改两遍。更好的方式是把输入统一成 numpy 数组:图片读出来是数组,视频读出来也是数组,检测函数只接收数组和来源标识,界面只管显示。

4.2 检测必须放到子线程

不要在 UI 主线程里直接调用model.predict。原因很简单:视频连续帧推理可能耗时几百毫秒,主线程一旦阻塞,窗口会无响应,拖动、关闭都会卡死。正确做法是把检测封装到 QThread 或 QRunnable 中,使用信号把结果传回主线程。

示例:

from PyQt5.QtCore import QThread, pyqtSignal class DetectThread(QThread): result_ready = pyqtSignal(object, object) # frame, boxes def run(self): results = self.model.predict(self.source, conf=0.3, imgsz=640) # 解析结果并发送回主线程

这里信号参数可以根据需要调整。只要检测在子线程,界面刷新和停止按钮就不会假死。

4.3 下拉框闪退和超链接点击问题

在 PyQt5 使用中经常遇到“下拉框闪退”。遇到这类问题,不要先怀疑 YOLO,先做最小复现:新建一个空窗口,只放 QComboBox,能稳定闪退就是 Qt 运行环境问题;稳定不闪退再往项目里加代码。常见原因包括:QComboBox 的 item 对象被提前释放、信号触发槽函数里操作了已关闭的窗口、PyQt5 和 Qt 的 DLL 版本不匹配。

另外,PyQt5 的 QTextBrowser 默认可以显示 HTML。如果要把“跳转到桌面端某个目录”或“点击后执行自定义操作”,不要依赖默认链接跳转。可以这样做:

self.log_browser.setOpenLinks(False) self.log_browser.linkActivated.connect(self.handle_link) def handle_link(self, link): # 这里写自定义逻辑,比如打开日志文件夹 import os os.startfile(link) # Windows 示例

这样可以避免点链接时跳到没意义的浏览器页面,也能让“查看检测结果目录”这种操作从日志区直接触达。

4.4 图像显示时的关键坑

在 PyQt5 中显示 OpenCV 图像时,最常见的坑是不做BGR -> RGB转换,导致画面颜色偏蓝偏暗。另一个坑是 QImage 生命周期问题。用rgb_image.data创建 QImage 时,如果原始数组在显示函数结束后被回收,画面可能出现花屏或黑线。稳妥做法是显示时把数据复制一份,或者把rgb_image保存为窗口类的成员变量。

rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimage = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.label_image.setPixmap(QPixmap.fromImage(qimage))

如果发现显示区域大小不合适,可以用setScaledContents(True)让图片缩放填满 QLabel。注意这会影响画框坐标对应的显示位置,所以不要把缩放后的显示图直接拿去保存检测结果。保存结果时,应该基于原始分辨率重新绘制。

5. 核心代码实现与跑通顺序

5.1 先做一个无界面的图片检测脚本

先实现最简单的图片检测,整条链路跑通再写 GUI,能节省大量调试时间。图片检测核心代码如下:

import cv2 from ultralytics import YOLO model = YOLO("runs/detect/train/exp/weights/best.pt") image = cv2.imread("test.jpg") results = model.predict(image, conf=0.3, imgsz=640) for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls = int(box.cls[0].item()) label = f"pothole {conf:.2f}" cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(image, label, (int(x1), int(y1) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

这段代码的逻辑很简单:读取图片,推理,拿到坐标框,画到图上。判断成功标准是能看到框位置合理、标签和置信度正常。

我这里刻意没有写置信度过滤的二次判断,因为model.predict(conf=0.3)已经过滤过了。如果你在代码里手动拿到box.conf再过滤,也不要重复赋一个不同的阈值,否则结果会让人困惑。

5.2 视频文件和摄像头实时检测

视频检测比图片检测多了一个“循环读帧”的过程。可以把读取帧放在子线程里,也可以用 QTimer 驱动。QTimer 的好处是不需要自己处理 while 循环,界面更稳定。

一个建议的流程:

  • 打开 VideoCapture;
  • QTimer 每隔 30~50 毫秒读取一帧;
  • 将帧交给检测函数;
  • 在界面上刷新结果。

“摄像头只识别一次”这个问题,通常不是模型的问题,而是摄像头读取逻辑写错了。比如有些代码在打开摄像头后只read了一次,然后把结果放到了循环外面;或者每帧都重新VideoCapture(0)又关闭,导致第二帧读不到。正确做法是只打开一次摄像头,循环readread失败时打印日志而不是静默跳过。

另一个与视频相关的问题是处理速度跟不上摄像头帧率。普通 USB 摄像头是 30 FPS,如果处理一帧需要 0.2 秒,实际显示不到 5 FPS,画面看起来会卡。这不是 bug,而是计算瓶颈。可以先降低检测分辨率,比如把帧缩到 640 再推理,或者隔帧检测。隔帧检测的含义是:画面显示始终要流畅,检测结果可以每 2 帧或 3 帧更新一次,视觉上更舒服。

如果摄像头画面只识别一次,不要先怀疑模型,先检查帧读取循环。

5.3 批量图片检测和设备无关的结构

批量检测时,不要用循环里硬编码文件名。建议用glob获取图片列表,在输出目录里按原文件名加_detected后缀保存。每处理完一张图就写一行日志,处理失败时打印路径并继续,而不是让整个程序中断。要统计成功数、失败数,最后输出一个汇总文本。这些看起来很简单,但真正用起来才知道有多重要。

import glob import os image_paths = glob.glob("samples/*.jpg") os.makedirs("output", exist_ok=True) for path in image_paths: try: image = cv2.imread(path) results = model.predict(image, conf=0.3, imgsz=640) # 绘制后保存到 output/xxx_detected.jpg print("[OK]", path) except Exception as e: print("[FAIL]", path, e)

在写 GUI 时,也是同样的思路:检测函数保持“输入一个 numpy 数组,输出绘制结果和框信息”,不关心来源是图片、视频还是摄像头。这样后续接口化、多线程化都容易。

6. 真实硬件上能跑成什么样

6.1 GTX1660Ti 推理参考

很多人在问 GTX1660Ti 跑 YOLOv8 需要 GPU 吗。直接说结论:GTX1660Ti 有 6GB 显存,跑 YOLOv8n 推理完全没问题,跑 YOLOv8s 推理也可以接受;如果要训练,batch 就要控制。不要在 6GB 显卡上尝试 YOLOv8m 以上的大模型,显存很容易不够。低配置能跑不代表适合批量跑,批量任务中每帧都累积耗时,必须看吞吐量和队列长度。

给一个判断方法:先跑 100 张代表图片,记录总耗时,得到平均单帧时间。如果你的摄像头是 30 FPS,单帧推理必须在 33ms 以内;如果平均在 100ms,那只能做到约 10 FPS。如果用 CPU,实测某些机器单帧超过 300ms,只能用于图片巡检,不适合实时视频。

6.2 批量任务和多摄像头并发架构

批量图片检测比较简单:按顺序处理即可。但如果要处理大量视频或接入多个摄像头,就要设计并发。

最容易出问题的做法是:为每个摄像头启动一个无限循环线程,每个线程里面做一次完整推理。这样线程数一多,CPU/GPU 切换开销很大,还可能出现同一模型在不同线程同时推理导致的竞争。更稳妥的做法是:摄像头读取线程只负责抓帧,把帧放到队列;一个或两个推理 Worker 从队列取帧;检测完的结果再发回主界面。

并发数不要拍脑袋。先用 1 路摄像头测,再用 2 路、3 路测,观察显存、内存和 CPU 占用。如果显存接近上限,就降低输入尺寸或增大队列的消费间隔。批量任务还要考虑失败重试:某个文件损坏时,要能跳过并记录,而不是卡在循环里。

6.3 边缘设备部署思路

热搜里也常常看到 RK3588 部署 YOLOv8、Jetson Orin Nano 部署 YOLOv8。如果你要把这套检测能力放到巡检小车上,PyQt5 桌面端只能作为调试工具,真正的推理建议放在边缘设备上。一般流程是:先在电脑上训练得到 best.pt,导出 ONNX,再在边缘设备上转换成对应格式,比如 RKNN、TensorRT 等。边缘设备上不一定要带 GUI,可以只出检测结果,通过协议发给上位机显示。

7. 常见报错排查链路

7.1 模型加载失败

现象:代码报FileNotFoundError或“.pt is not a YOLO model”。

排查顺序:

  1. 先看错误文本,是路径不存在,还是文件内容不对。
  2. 确认路径没有中文,Windows 下尤其容易出问题。
  3. 确认权重文件确实是 YOLOv8 训练出来的,不是只复制了名字。
  4. 检查 ultralytics 版本和训练时版本是否一致。

如果是在 GUI 里加载失败,先把加载代码放到独立脚本里跑一遍,排除 UI 环境的影响。

7.2 PyQt5 黑屏和闪退

现象:程序启动后窗口黑屏、闪退,或者点击按钮后崩溃。

排查顺序:

  1. 在纯 PyQt5 环境里做一个空窗口,先排除环境问题。
  2. 把 OpenCV 的cv2.imshow全部注释掉,确保窗口焦点不让给 OpenCV。
  3. 检查图像转换时数组是否被过早释放。
  4. 保留完整 traceback,而不是只看“段错误”或“崩溃”。

很多时候闪退问题不是 PyQt5 代码写错,而是 Qt 运行库冲突。这时候优先检查是否安装了多个 Qt 版本,或者 PyQt5 相关包版本不一致。

7.3 摄像头无画面或只识别一次

现象:点击打开摄像头后能看到画面,但检测只在第一帧执行一次,后续不再更新。

排查顺序:

  1. 确认 VideoCapture 是否只 open 一次,不要每帧都打开。
  2. 确认读取循环里是否调用read()
  3. 确认read()返回值第一项是否为 True。
  4. 确认处理完一帧后 QTimer 是否继续触发。
  5. 不要每次循环都新建 YOLO 模型对象,模型只需要初始化一次。
原因表现处理
模型只在初始化时加载正常保持这样
模型在循环里重复加载极度卡顿移到初始化阶段
read() 失败画面停顿/黑屏打印系统日志,检查摄像头占用
QTimer 被 stop无后续帧检查停止按钮逻辑

7.4 其他常见问题

  • “下拉框闪退”:先最小复现,再查运行环境,最后查代码。
  • “文本框超链接点击后无反应”:setOpenLinks(False)+ 连接linkActivated
  • “训练时内存不足”:降低 batch、imgsz,关闭数据加载的多个 worker。
  • “推理速度突然变慢”:先看是否有多个程序占用 GPU 显存,再看输入分辨率是不是被无意调大。

8. 项目落地建议

8.1 建议跑通顺序

我建议按这个顺序推进:

  1. 用预训练权重跑通单张图片检测。
  2. 准备自己的坑洼数据集,训练并导出 best.pt。
  3. 写一个无 GUI 的 Python 脚本,完成图片、视频、摄像头检测。
  4. 把检测结果封装成函数,接到 PyQt5 界面上。
  5. 处理批量任务和多路输入。
  6. 打包交付。

不要一上来就做多线程和多摄像头。功能越复杂,排查问题越难。先把单任务跑稳,再扩展。尤其是把“摄像头实时检测”和“多摄像头并发”分开,摄像头单路能跑通,才有资格考虑并发。

8.2 可以扩展的方向

这个项目后续可以扩展的方向很多:

  • 缺陷分类:坑洼、裂缝、修补块、井盖下沉等多类别。
  • 结果统计:输出每个坑洼的面积估算、中心点坐标,方便后续道路养护。
  • 报警:当检测到较大坑洼时,界面弹窗或生成报告。
  • 数据回传:将检测结果写入数据库,便于巡检记录查询。
  • 模型改进:小目标检测头、注意力模块、更轻量的主干网络。

对于大部分实际需求,建议先做“多类别分类”和“结果统计”,这两项对道路巡检的价值最直接。面积估算可以用像素面积加简单比例换算,不一定需要深度相机。

8.3 长期维护的关键

这类系统最容易出的问题不是模型精度不够,而是运行环境变更后无人维护。建议:

  • requirements.txt固定下来,记录你的 Python 版本和关键依赖版本。
  • 把模型文件放在项目内相对路径下,不要依赖系统盘某个绝对路径。
  • 每个功能都保留控制台日志,方便远程排错。
  • 给 GUI 增加“模型选择”下拉框,这样以后换模型不用改代码。

踩过几次之后我能确定一点:坑洼检测系统真正落地,不是看模型 mAP 是 80 还是 90,而是看你能不能把输入、推理、显示、保存四个环节串得稳。不管你是用 GTX1660Ti 做轻量训练,还是准备在 RK3588、Jetson 上边缘部署,第一步都应该先跑通最小闭环。先把一张图片检测出来,然后慢慢加视频、加摄像头、加并发,最后你会发现,最难的部分不是 YOLOv8,而是环境、路径和线程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询