基于YOLOv8与PyQt5的路面坑洞检测系统实战:从训练到部署
2026/9/18 13:24:33 网站建设 项目流程

1. 路面坑洞检测系统整体设计思路拆解

1.1 为什么选择YOLOv8而不是传统图像处理方法

做路面坑洞检测这件事,我最早试过用传统的边缘检测加阈值分割。OpenCV的Canny算子配合形态学操作,在光照均匀、坑洞边缘清晰的理想条件下确实能跑出结果。但实际道路场景太复杂了:柏油路面的纹理本身就是高频噪声,阴影和坑洞在灰度图上几乎无法区分,下雨后积水反光更是直接让阈值法失效。我印象很深的一次测试,同一段路早上八点和下午四点拍的两张照片,传统方法给出的检测结果差了将近40%。

后来转向深度学习方案,YOLOv8是我对比了Faster R-CNN、SSD和YOLOv5之后定下来的。原因很直接:Faster R-CNN精度够但推理速度太慢,GTX 1660 Ti上单帧要200ms以上,做视频流实时检测根本扛不住;SSD对小目标检测效果一般,而路面坑洞在车载摄像头画面里往往只占几十个像素;YOLOv5虽然成熟,但YOLOv8在相同参数量下mAP普遍高出2到3个百分点,而且Ultralytics把训练、验证、导出、部署的链路整合得非常顺滑,省去了大量造轮子的时间。

YOLOv8的Anchor-Free设计对坑洞检测特别友好。坑洞的形状极不规则,有的细长、有的接近圆形、有的连成一片,预设Anchor Box很难覆盖所有形态。Anchor-Free直接回归中心点和宽高,省去了聚类Anchor的步骤,在小数据集上表现更稳定。我自己的数据集只有3000多张标注图,用YOLOv8n训练100个epoch就能达到0.82的mAP@0.5,这个成绩在传统方法上想都不敢想。

1.2 PyQt5作为界面框架的取舍逻辑

检测模型跑通之后,下一步是把它包装成一个普通人能用的桌面工具。这里选PyQt5而不是Tkinter或者Web方案,有几个实际考量。

Tkinter太简陋了,做个带视频预览、参数调节、结果导出的界面会非常痛苦,控件样式也很难看。Web方案(Flask + 前端)虽然灵活,但部署时要开浏览器、配端口,对于工地现场那种“双击exe就能用”的需求来说太重了。PyQt5是Python生态里最成熟的桌面GUI框架,控件丰富、文档齐全、和OpenCV/NumPy的配合非常自然,而且PyInstaller打包成exe的流程已经很成熟。

还有一个关键点:PyQt5的QThread机制。视频检测是计算密集型任务,如果直接在主线程里跑推理,界面会直接卡死,点任何按钮都没反应。PyQt5的信号槽机制配合QThread,可以把推理放在子线程,主线程只负责界面刷新,用户体验流畅很多。这个设计在后面实际部署时救了我好几次。

1.3 系统整体架构与数据流

整个系统的数据流是这样的:用户通过界面选择视频文件或打开摄像头,帧数据进入推理线程,YOLOv8模型对每一帧做检测,检测结果(边界框、置信度、类别)通过信号槽传回主线程,主线程在QGraphicsView上绘制标注框和统计信息,同时把结果写入日志和CSV文件。

架构上分三层:界面层负责交互和展示,推理层封装YOLOv8的加载和推理,数据层处理视频读写、结果存储和日志记录。三层之间通过信号槽和回调函数通信,耦合度低,后面换模型或者改界面都不会互相影响。

注意:不要把所有逻辑塞进一个MainWindow类里。我第一版就是这么干的,后来想加一个“导出检测报告”的功能,发现代码已经乱到改不动了,只能重构。建议从一开始就分模块。

2. 环境搭建与核心依赖配置实操

2.1 Python环境与CUDA版本选择

环境配置这一步踩的坑最多,我先说结论:Python 3.9 + CUDA 11.8 + cuDNN 8.6 + PyTorch 2.0.1这个组合在我这里最稳定。GTX 1660 Ti虽然不算新卡,但6GB显存跑YOLOv8n和YOLOv8s完全够用,batch size设8或者16都没问题。

为什么选CUDA 11.8而不是更新的12.x?因为PyTorch对CUDA 11.8的支持最成熟,很多预编译包都是基于这个版本。你要是用CUDA 12.x,可能会遇到PyTorch版本和CUDA不匹配的问题,报错信息还特别隐晦。我试过一次用CUDA 12.1配PyTorch 2.1,训练时loss直接变成NaN,查了半天才发现是版本兼容性问题。

安装命令如下,用conda建一个独立环境:

conda create -n pothole python=3.9 conda activate pothole pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.0.200 pip install PyQt5==5.15.9 pip install opencv-python==4.8.1.78

验证GPU是否可用:

import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

如果输出True和你的显卡型号,说明环境没问题。如果输出False,先检查显卡驱动版本,GTX 1660 Ti需要驱动版本至少515以上。

2.2 PyQt5安装中的常见坑

PyQt5安装本身不复杂,但有几个坑值得单独说。第一个是安装时长问题,pip install PyQt5有时候会卡在下载环节,特别是网络不好的时候。我的经验是加个国内镜像源:

pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple

第二个坑是OpenGL导致界面无显示。这个问题在远程桌面或者虚拟机里特别常见,PyQt5默认用OpenGL渲染,但某些环境下OpenGL驱动不完整,界面就一片空白。解决办法是在代码开头设置软件渲染:

import os os.environ["QT_OPENGL"] = "software"

或者在主函数里加:

from PyQt5.QtCore import Qt Qt.QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)

第三个坑是高分辨率屏幕适配。现在很多笔记本是2K甚至4K屏,PyQt5默认的DPI缩放策略会导致界面元素特别小或者模糊。在创建QApplication之前加这两行:

QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)

2.3 项目目录结构设计

目录结构看着是小事,但后面代码多了就知道重要性了。我现在的结构是这样的:

pothole_detection/ ├── models/ │ └── yolov8n_pothole.pt ├── ui/ │ ├── main_window.py │ └── main_window.ui ├── core/ │ ├── detector.py │ ├── video_thread.py │ └── utils.py ├── data/ │ ├── images/ │ ├── labels/ │ └── dataset.yaml ├── output/ │ ├── logs/ │ └── results/ ├── main.py └── requirements.txt

core/detector.py封装YOLOv8的加载和推理,core/video_thread.py是QThread子类,ui/main_window.py只管界面逻辑。这样分工之后,改模型不用动界面代码,改界面不用碰推理逻辑。

3. 数据集准备与YOLOv8训练全流程

3.1 路面坑洞数据采集与标注策略

数据集的质量直接决定模型上限。我一开始从网上找了一些公开的坑洞数据集,但发现一个问题:不同来源的图片分辨率、拍摄角度、光照条件差异太大,直接混在一起训练,模型很难收敛。后来我决定自己采集,用手机固定在车前挡风玻璃上,在不同路段、不同时间段拍了大约5000张原始图片。

采集时注意几个点:第一,覆盖不同光照条件,早上、中午、傍晚、阴天、雨天都要有,否则模型在特定光照下会失效;第二,包含不同路面类型,柏油路、水泥路、有标线的、没标线的;第三,坑洞大小要多样,从拳头大到脸盆大都要有,这样模型才能学到尺度不变性。

标注用LabelImg,格式选YOLO格式(txt文件,每行是class_id x_center y_center width height,坐标归一化到0到1)。标注时有个经验:边界框不要贴得太紧,稍微留一点余量,因为坑洞边缘往往有破损过渡区,贴太紧反而让模型学到模糊的边界特征。我一般让框比实际坑洞大5到10个像素。

标注完成后做数据清洗,把以下几类图片剔除:模糊到看不清坑洞的、坑洞被车辆遮挡超过50%的、标注框明显错误的。清洗完剩下大约3200张有效图片,按8:1:1划分训练集、验证集、测试集。

3.2 dataset.yaml配置与训练参数设置

dataset.yaml文件长这样:

path: ./data train: images/train val: images/val test: images/test names: 0: pothole

就一个类别,简单直接。如果你的数据里还有裂缝、修补痕迹等其他路面病害,可以加类别,但要注意类别不平衡问题。

训练命令用Ultralytics的CLI:

yolo detect train data=dataset.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 lr0=0.01 patience=20

几个关键参数解释一下。imgsz=640是输入分辨率,GTX 1660 Ti跑640完全没问题,如果你想更快可以降到416,但小坑洞的检测精度会下降。batch=16是根据6GB显存定的,如果显存不够就降到8。lr0=0.01是初始学习率,YOLOv8默认用余弦退火策略,这个值在大多数情况下都合适。patience=20是早停耐心值,如果20个epoch验证集指标没提升就自动停止,省时间。

如果你想用Python脚本训练,方便记录参数:

from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.train( data="dataset.yaml", epochs=100, imgsz=640, batch=16, lr0=0.01, patience=20, device=0, project="runs/train", name="pothole_v1" )

3.3 训练过程监控与损失函数曲线解读

训练启动后,Ultralytics会在runs/train/pothole_v1/目录下生成一堆文件,其中results.csv记录了每个epoch的损失和指标。我习惯用pandas读出来画图:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/train/pothole_v1/results.csv") df.columns = df.columns.str.strip() fig, axes = plt.subplots(1, 2, figsize=(14, 5)) axes[0].plot(df["epoch"], df["train/box_loss"], label="train box loss") axes[0].plot(df["epoch"], df["val/box_loss"], label="val box loss") axes[0].set_xlabel("Epoch") axes[0].set_ylabel("Box Loss") axes[0].legend() axes[1].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP@0.5") axes[1].plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP@0.5:0.95") axes[1].set_xlabel("Epoch") axes[1].set_ylabel("mAP") axes[1].legend() plt.tight_layout() plt.savefig("training_curves.png", dpi=150)

看曲线有几个判断标准。训练损失和验证损失同步下降说明模型在正常学习;如果训练损失降但验证损失升,说明过拟合了,需要加数据增强或者减模型复杂度;如果两个损失都震荡不降,可能是学习率太大或者数据标注有问题。我这次训练到第78个epoch时验证mAP达到最高0.823,之后开始轻微过拟合,早停机制在第98个epoch触发,最终用的第78个epoch的权重。

3.4 模型评估与改进方向

训练完成后用验证集跑评估:

yolo detect val model=runs/train/pothole_v1/weights/best.pt data=dataset.yaml

输出会给出mAP@0.5、mAP@0.5:0.95、精确率、召回率等指标。我的模型精确率0.85、召回率0.79,召回率偏低说明有一些坑洞漏检了。分析漏检样本发现,主要是远处的小坑洞被阴影覆盖的坑洞

针对这两个问题,我做了两个改进。第一,在数据增强里加了mosaic=1.0scale=0.5,让模型多见一些小目标;第二,尝试了YOLOv8的ASFF(Adaptive Spatial Feature Fusion)改进,把不同尺度的特征图做自适应融合,对小目标检测有提升。改进后召回率提到0.84,mAP@0.5到0.86。

如果你也想做轻量化改进,可以试试把Backbone换成MobileNetV3或者ShuffleNetV2,参数量能降一半,推理速度提升30%左右,精度损失在2个百分点以内。对于嵌入式部署场景(比如RK3588),这个取舍是值得的。

4. PyQt5界面设计与多线程推理实现

4.1 界面布局与QSS样式美化

界面设计我用Qt Designer画好.ui文件,然后用pyuic5转成Python代码:

pyuic5 -x main_window.ui -o main_window.py

主界面分三个区域:左边是视频显示区,用QLabel或者QGraphicsView;右边是控制面板,放模型选择、置信度阈值滑块、IOU阈值滑块、开始/停止按钮;底部是状态栏和日志输出区。

QSS样式可以让界面看起来不那么“原生丑”:

QMainWindow { background-color: #2b2b2b; } QPushButton { background-color: #4a90d9; color: white; border-radius: 6px; padding: 8px 16px; font-size: 14px; } QPushButton:hover { background-color: #5aa0e9; } QSlider::groove:horizontal { height: 6px; background: #555; border-radius: 3px; } QSlider::handle:horizontal { background: #4a90d9; width: 16px; margin: -5px 0; border-radius: 8px; }

视频显示区我推荐用QGraphicsView而不是QLabel,因为QGraphicsView支持缩放和拖拽,用户想看坑洞细节时可以放大。把OpenCV的BGR帧转成QImage再转QPixmap,设置到QGraphicsScene上:

def cvimg_to_qpixmap(cv_img): height, width, channel = cv_img.shape bytes_per_line = 3 * width q_img = QImage(cv_img.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() return QPixmap.fromImage(q_img)

4.2 QThread推理线程与信号槽通信

推理线程是整个系统的核心。我定义一个VideoThread类继承QThread,在run方法里循环读帧、推理、发信号:

class VideoThread(QThread): frame_signal = pyqtSignal(np.ndarray) result_signal = pyqtSignal(list) stats_signal = pyqtSignal(dict) finished_signal = pyqtSignal() def __init__(self, model_path, source, conf=0.5, iou=0.45): super().__init__() self.model = YOLO(model_path) self.source = source self.conf = conf self.iou = iou self._running = True def run(self): cap = cv2.VideoCapture(self.source) frame_count = 0 pothole_count = 0 while self._running and cap.isOpened(): ret, frame = cap.read() if not ret: break results = self.model(frame, conf=self.conf, iou=self.iou, verbose=False) annotated = results[0].plot() boxes = results[0].boxes current_count = len(boxes) if boxes is not None else 0 pothole_count += current_count frame_count += 1 self.frame_signal.emit(annotated) self.result_signal.emit(boxes.data.cpu().numpy().tolist() if boxes is not None else []) self.stats_signal.emit({ "frame": frame_count, "current": current_count, "total": pothole_count }) cap.release() self.finished_signal.emit() def stop(self): self._running = False self.wait()

主线程里连接信号:

self.thread = VideoThread(model_path, source, conf, iou) self.thread.frame_signal.connect(self.update_frame) self.thread.stats_signal.connect(self.update_stats) self.thread.finished_signal.connect(self.on_finished) self.thread.start()

注意:不要在子线程里直接操作UI控件,所有UI更新必须通过信号槽回到主线程。我见过有人直接在run方法里调self.label.setPixmap(),程序直接崩溃。

4.3 置信度阈值与IOU阈值的动态调节

界面上放两个QSlider,一个控制置信度阈值(0.1到0.9),一个控制IOU阈值(0.1到0.9)。滑动时实时更新推理参数:

def on_conf_changed(self, value): self.conf = value / 100.0 self.conf_label.setText(f"{self.conf:.2f}") if self.thread and self.thread.isRunning(): self.thread.conf = self.conf

这里有个细节:修改参数时不要重启线程,直接改线程对象的属性就行,因为推理循环每次都会读最新的值。重启线程会导致视频从头开始,体验很差。

置信度阈值调低(比如0.3)能检出更多坑洞但误检也会增加,调高(比如0.7)误检少但可能漏检。实际使用时我建议默认0.5,然后根据场景微调。IOU阈值主要影响重叠框的合并,坑洞密集时调低一点(0.3到0.4)能避免框被合并掉。

4.4 检测结果可视化与统计面板

可视化部分,results[0].plot()已经帮我们画好了框和置信度,直接显示就行。但如果你想自定义样式,可以手动绘制:

for box in boxes: x1, y1, x2, y2 = map(int, box.xyxy[0]) conf = float(box.conf[0]) color = (0, 255, 0) if conf > 0.7 else (0, 255, 255) if conf > 0.5 else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f"{conf:.2f}", (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)

统计面板显示当前帧坑洞数、累计坑洞数、处理帧率。帧率计算用滑动窗口:

self.frame_times.append(time.time()) if len(self.frame_times) > 30: self.frame_times.pop(0) fps = len(self.frame_times) / (self.frame_times[-1] - self.frame_times[0])

5. 模型部署优化与性能调优实战

5.1 ONNX导出与推理加速

PyTorch模型直接推理在GTX 1660 Ti上大概25到30 FPS,对于实时检测够用但不算快。导出成ONNX再用ONNXRuntime推理,能提到40 FPS左右:

yolo export model=runs/train/pothole_v1/weights/best.pt format=onnx opset=12 simplify=True

推理代码改成:

import onnxruntime as ort session = ort.InferenceSession("best.onnx", providers=["CUDAExecutionProvider"])

ONNX的优势是跨平台,后面如果要部署到RK3588这类嵌入式设备,ONNX是必经之路。RK3588有NPU,需要把ONNX转成RKNN格式,用RKNN-Toolkit2做转换和量化。量化到INT8后模型体积缩小到原来的四分之一,推理速度提升3到5倍,精度损失在1到2个百分点。

5.2 TensorRT部署与C++集成思路

如果你追求极致性能,TensorRT是绕不开的。GTX 1660 Ti支持TensorRT 8.6,把ONNX转成TensorRT引擎:

trtexec --onnx=best.onnx --saveEngine=best.engine --fp16

FP16精度下推理速度能到60 FPS以上。C++集成的话,用TensorRT的C++ API加载引擎,配合OpenCV做前后处理。这块工作量不小,但如果你要做产品级部署,值得投入。

5.3 界面卡顿与内存泄漏排查

跑长时间视频时我遇到过两个问题。第一个是界面越来越卡,查下来是QImage对象没有及时释放,每帧都新建QImage导致内存堆积。解决办法是复用QImage缓冲区,或者手动调del。第二个是内存泄漏,OpenCV的VideoCapture没有正确release,程序跑几个小时后内存占用从200MB涨到2GB。确保在finally块里调cap.release()

还有一个常见问题是OpenGL导致界面无显示,前面提过,设置软件渲染就行。如果你用的是远程桌面,这个问题几乎必现。

6. 常见问题速查与避坑经验汇总

6.1 环境与依赖问题速查表

问题现象可能原因解决方法
torch.cuda.is_available()返回False显卡驱动版本过低更新驱动到515以上
PyQt5界面空白OpenGL驱动不完整设置QT_OPENGL=software
训练loss变NaNCUDA和PyTorch版本不匹配用CUDA 11.8 + PyTorch 2.0.1
pip安装PyQt5超时网络问题加国内镜像源
界面元素模糊高DPI缩放未适配设置AA_EnableHighDpiScaling

6.2 训练与推理常见异常处理

训练时显存不足:降低batch size,或者用YOLOv8n而不是YOLOv8m。GTX 1660 Ti的6GB显存跑YOLOv8n batch=16没问题,跑YOLOv8m只能batch=4。

推理时检测框抖动:视频相邻帧的检测结果不稳定,可以加一个简单的跟踪算法(比如IOU匹配)做平滑,或者对连续几帧的结果做投票。

小坑洞漏检严重:提高输入分辨率到1280,或者在数据增强里加mosaic和scale。如果还不行,考虑用YOLOv8的P2层特征图,专门检测小目标。

6.3 实操心得与独家技巧

第一个心得:数据集标注时,坑洞的边界框不要贴太紧。我一开始标得很精确,结果模型学到的边界特征很模糊,换一段路就失效。后来把框放大5到10个像素,泛化能力明显提升。

第二个心得:训练时用freeze参数冻结Backbone的前几层。如果你的数据集和COCO比较接近,冻结前10层能加快收敛,还能防止过拟合。命令是freeze=10

第三个心得:PyQt5界面设计时,把耗时操作全部放子线程。不只是推理,视频解码、结果保存、日志写入都应该异步。我见过有人把CSV写入放在主线程,结果每写一行界面就卡一下。

第四个心得:模型文件不要放在中文路径下。Ultralytics在某些版本对中文路径支持不好,会报编码错误。项目路径全用英文。

第五个心得:打包成exe时,把模型文件和依赖库一起打包。PyInstaller的--add-data参数可以指定额外文件,但要注意路径分隔符在Windows和Linux下不一样,用os.path.join处理。

6.4 后续扩展方向

这个系统目前只做了检测,后面可以加跟踪功能,用ByteTrack或者DeepSORT给每个坑洞分配ID,统计同一坑洞在视频中的持续帧数,过滤掉误检。还可以加GPS定位,把检测结果和经纬度绑定,生成路面病害地图。如果要做毕业设计,加一个Web端展示,用Flask把检测结果推送到网页,答辩时演示效果会好很多。

我在实际部署中发现,工地现场的光照条件比实验室恶劣得多,模型在强逆光和夜间几乎不可用。后来加了一个图像预处理模块,用CLAHE做自适应直方图均衡,夜间检测召回率从0.3提到了0.6。这个改进成本很低,但效果立竿见影,建议你也试试。

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

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

立即咨询