把 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.yamlpothole.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)又关闭,导致第二帧读不到。正确做法是只打开一次摄像头,循环read,read失败时打印日志而不是静默跳过。
另一个与视频相关的问题是处理速度跟不上摄像头帧率。普通 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”。
排查顺序:
- 先看错误文本,是路径不存在,还是文件内容不对。
- 确认路径没有中文,Windows 下尤其容易出问题。
- 确认权重文件确实是 YOLOv8 训练出来的,不是只复制了名字。
- 检查 ultralytics 版本和训练时版本是否一致。
如果是在 GUI 里加载失败,先把加载代码放到独立脚本里跑一遍,排除 UI 环境的影响。
7.2 PyQt5 黑屏和闪退
现象:程序启动后窗口黑屏、闪退,或者点击按钮后崩溃。
排查顺序:
- 在纯 PyQt5 环境里做一个空窗口,先排除环境问题。
- 把 OpenCV 的
cv2.imshow全部注释掉,确保窗口焦点不让给 OpenCV。 - 检查图像转换时数组是否被过早释放。
- 保留完整 traceback,而不是只看“段错误”或“崩溃”。
很多时候闪退问题不是 PyQt5 代码写错,而是 Qt 运行库冲突。这时候优先检查是否安装了多个 Qt 版本,或者 PyQt5 相关包版本不一致。
7.3 摄像头无画面或只识别一次
现象:点击打开摄像头后能看到画面,但检测只在第一帧执行一次,后续不再更新。
排查顺序:
- 确认 VideoCapture 是否只 open 一次,不要每帧都打开。
- 确认读取循环里是否调用
read()。 - 确认
read()返回值第一项是否为 True。 - 确认处理完一帧后 QTimer 是否继续触发。
- 不要每次循环都新建 YOLO 模型对象,模型只需要初始化一次。
| 原因 | 表现 | 处理 |
|---|---|---|
| 模型只在初始化时加载 | 正常 | 保持这样 |
| 模型在循环里重复加载 | 极度卡顿 | 移到初始化阶段 |
| read() 失败 | 画面停顿/黑屏 | 打印系统日志,检查摄像头占用 |
| QTimer 被 stop | 无后续帧 | 检查停止按钮逻辑 |
7.4 其他常见问题
- “下拉框闪退”:先最小复现,再查运行环境,最后查代码。
- “文本框超链接点击后无反应”:
setOpenLinks(False)+ 连接linkActivated。 - “训练时内存不足”:降低 batch、imgsz,关闭数据加载的多个 worker。
- “推理速度突然变慢”:先看是否有多个程序占用 GPU 显存,再看输入分辨率是不是被无意调大。
8. 项目落地建议
8.1 建议跑通顺序
我建议按这个顺序推进:
- 用预训练权重跑通单张图片检测。
- 准备自己的坑洼数据集,训练并导出 best.pt。
- 写一个无 GUI 的 Python 脚本,完成图片、视频、摄像头检测。
- 把检测结果封装成函数,接到 PyQt5 界面上。
- 处理批量任务和多路输入。
- 打包交付。
不要一上来就做多线程和多摄像头。功能越复杂,排查问题越难。先把单任务跑稳,再扩展。尤其是把“摄像头实时检测”和“多摄像头并发”分开,摄像头单路能跑通,才有资格考虑并发。
8.2 可以扩展的方向
这个项目后续可以扩展的方向很多:
- 缺陷分类:坑洼、裂缝、修补块、井盖下沉等多类别。
- 结果统计:输出每个坑洼的面积估算、中心点坐标,方便后续道路养护。
- 报警:当检测到较大坑洼时,界面弹窗或生成报告。
- 数据回传:将检测结果写入数据库,便于巡检记录查询。
- 模型改进:小目标检测头、注意力模块、更轻量的主干网络。
对于大部分实际需求,建议先做“多类别分类”和“结果统计”,这两项对道路巡检的价值最直接。面积估算可以用像素面积加简单比例换算,不一定需要深度相机。
8.3 长期维护的关键
这类系统最容易出的问题不是模型精度不够,而是运行环境变更后无人维护。建议:
- 把
requirements.txt固定下来,记录你的 Python 版本和关键依赖版本。 - 把模型文件放在项目内相对路径下,不要依赖系统盘某个绝对路径。
- 每个功能都保留控制台日志,方便远程排错。
- 给 GUI 增加“模型选择”下拉框,这样以后换模型不用改代码。
踩过几次之后我能确定一点:坑洼检测系统真正落地,不是看模型 mAP 是 80 还是 90,而是看你能不能把输入、推理、显示、保存四个环节串得稳。不管你是用 GTX1660Ti 做轻量训练,还是准备在 RK3588、Jetson 上边缘部署,第一步都应该先跑通最小闭环。先把一张图片检测出来,然后慢慢加视频、加摄像头、加并发,最后你会发现,最难的部分不是 YOLOv8,而是环境、路径和线程。