
你训练好了一个 YOLOv8 坑洼检测模型精度不错日志里 mAP 也好看了。然后呢把它扔给道路养护部门的人用他们看到终端里滚动的坐标和置信度只会一脸茫然。这才是坑洼检测系统从“算法能跑”到“工具可用”之间真正要跨过的坎你需要一个图形界面一个普通用户打开就能选图片、选视频、开摄像头实时检测的桌面应用。这篇文章要解决的就是这件事基于 YOLOv8 做坑洼路面缺陷检测再用 PyQt5 把模型封装成完整的桌面系统。我会给你一套能直接落地的工程路径包括环境配置、数据集准备、模型训练、GUI 设计、摄像头实时检测、结果导出以及每一步真正容易踩的坑。读完你可以照着搭出一套自己的路面检测系统而不是停留在跑通一个 demo 的层面。1. 为什么坑洼检测系统需要“算法 界面”两条腿先看清楚一件事纯用 YOLOv8 做目标检测门槛并不高。官方仓库克隆下来装好依赖准备数据集训练脚本跑起来很快就能拿到一个检测模型。但这套流程默认的使用者是开发者本人交互方式是命令行输出是终端文本和检测结果图片。放到真实业务场景里这套链路几乎是断的。真实使用坑洼检测系统的人是谁可能是道路巡检员可能是市政养护人员也可能是现场施工人员。他们没有 Python 环境不关心 mAP 是什么更不可能打开命令行敲python detect.py。他们需要的是打开一个软件窗口拖进去一张路面照片界面直接框出坑洼位置和严重程度。这个需求不是模型能解决的而是 GUI 层的任务。所以这个项目的本质是把一个深度学习模型封装成普通用户能操作的桌面工具。PyQt5 在这里的角色不是花架子而是整个系统的交付界面。模型负责“看懂”图像PyQt5 负责“让用户用起来”。两者结合才构成一个完整的产品雏形。另一个值得说的点PyQt5 做 AI 检测界面在工程上很成熟。它的QThread机制可以处理摄像头实时流避免界面卡死QLabel可以直接显示图像帧信号槽机制天然适合把检测结果异步推送到界面上。相比之下如果用 Flask 做 Web 界面还要处理浏览器兼容性和视频流推送如果用 Tkinter界面美观度和控件丰富度又差一截。PyQt5 是桌面端 AI 工具比较稳的选型。这套系统的应用场景也比较明确道路日常巡检用车载摄像头连续采集路面图像实时检测坑洼。市政报修审核市民上报路面破损后用系统复核破损类型和位置。道路质量评估批量处理历史巡检图片统计坑洼分布和密度。施工后验收对新铺设路面进行缺陷检测辅助质量把关。2. YOLOv8 与 PyQt5 的核心概念先把基础搭对2.1 YOLOv8 的目标检测能力YOLOv8 是 Ultralytics 推出的目标检测模型在 YOLOv5 的基础上重构了网络结构。它的核心思路仍然是单阶段检测输入一张图像模型直接回归出目标框的位置和类别不需要像两阶段检测器那样先生成候选区域再做分类。这里要理解两个关键点C2f 模块替代了 C3 模块。YOLOv8 的骨干网络使用了 C2f 结构通过 split 操作和更多分支融合在不明显增加计算量的前提下提升了梯度流动让模型更容易学到路面纹理里的细粒度特征。坑洼在图像里的表现往往是边缘不规则的暗色区域局部纹理和周围平整路面差异大这种细粒度特征正是 C2f 更擅长捕捉的。Anchor-Free 检测头。YOLOv8 去掉了传统的 anchor 预设改为直接预测对象中心点和宽高。这对坑洼检测有一个实际好处坑洼的尺寸变化非常大——小的可能只有几十像素大的几乎占满整个车道——Anchor-Free 方式少了手动调 anchor 尺寸的环节模型对尺度变化的适应能力更强。从实际训练角度看YOLOv8 对算力的要求相对友好。虽然训练阶段有 GPU 会快很多但在 GTX 1660 Ti 这种入门级显卡上也能跑只是 batch size 和图像尺寸需要适当降低。推理阶段更轻松CPU 也能运行只是帧率会低一些。2.2 PyQt5 在检测系统中的定位PyQt5 是 Qt5 框架的 Python 绑定用来开发桌面图形界面。在这个项目里PyQt5 承担以下具体职责文件选择与加载通过QFileDialog让用户选择本地图片或视频。图像显示用QLabel或自定义控件显示检测后的图像帧。摄像头管理通过QTimer或QThread周期性抓取摄像头帧并送入模型。结果展示在表格控件中列出每个检测框的类别、置信度和坐标。报表生成把检测结果导出为 CSV 或带标注的图片文件。一个常见的误区是直接在 UI 线程里跑模型推理。坑洼检测模型单帧推理虽然只有几十毫秒但在循环检测视频或摄像头画面时UI 线程一旦被推理占用窗口就会无响应。正确做法是把检测放到QThread的工作线程里通过信号把结果传回主线程更新界面。2.3 为什么是 YOLOv8 PyQt5而不是其他组合做路面缺陷检测你有很多技术组合可选。但 YOLOv8 PyQt5 是当前综合成本最低的一套。对比其他方案组合方案优势劣势YOLOv8 Flask Web部署方便浏览器访问视频流实时性差界面交互不够原生YOLOv8 Tkinter轻量Python 内置控件样式老旧复杂界面开发效率低YOLOv8 PyQt5界面美观控件丰富线程机制完善打包体积较大学习曲线略陡YOLOv8 Electron界面现代化资源占用大Python 与 Node 通信复杂度高对道路巡检这类工具型软件桌面端是更自然的使用形态。PyQt5 在性能和开发效率之间给了比较均衡的答案。3. 环境准备与前置条件版本冲突是第一个坑3.1 开发环境清单搭建环境是第一道关。很多人在这一步就栽了跟头最常见的错误是 PyQt5 和 YOLOv8 的依赖互相覆盖。推荐环境如下操作系统Windows 10/11 或 Ubuntu 20.04/22.04Python 版本3.8 到 3.10不推荐 3.11部分依赖可能出现预编译轮子缺失深度学习框架PyTorch 2.x具体版本根据你的 CUDA 版本选择目标检测框架Ultralytics YOLOv8GUI 框架PyQt5图像处理OpenCV-Python3.2 安装步骤建议先创建虚拟环境避免把系统 Python 环境搞乱# 创建虚拟环境 conda create -n pothole python3.9 # 激活环境 conda activate pothole # 安装 PyTorchCPU 版本示例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装 YOLOv8 pip install ultralytics # 安装 PyQt5 pip install PyQt5 # 安装 OpenCV pip install opencv-python如果你有 NVIDIA GPU 且 CUDA 环境已配置好PyTorch 的安装命令需要换成对应 CUDA 版本的命令具体以 PyTorch 官网为准。有一个安装细节容易踩坑ultralytics包会自动依赖opencv-python如果你之前装过其他版本的 OpenCV可能会出现版本冲突。建议先安装ultralytics再安装PyQt5最后确认 OpenCV 版本可用。验证安装是否成功# 验证文件check_env.py import torch from ultralytics import YOLO import PyQt5 import cv2 print(PyTorch 版本:, torch.__version__) print(CUDA 是否可用:, torch.cuda.is_available()) print(YOLOv8 模块导入成功) print(PyQt5 版本:, PyQt5.QtCore.PYQT_VERSION_STR) print(OpenCV 版本:, cv2.__version__)运行命令python check_env.py如果所有信息正常输出说明环境已经就绪。CUDA 是否可用显示False不影响代码编写但会影响训练速度训练环节可以改用 CPU 版或调小参数。3.3 数据集准备是决定精度的上限环境只是基础坑洼检测系统的精度上限其实在数据集。网上有一些公开的路面缺陷数据集覆盖沥青路面、水泥路面的坑洼、裂缝、修补区域。更稳妥的做法是使用公开数据集进行验证如果你已经有道路巡检的历史图片也可以通过标注工具制作自己的数据集。标注格式推荐使用 YOLO 格式每个图片对应一个同名的.txt文件文件里每一行是class_id x_center y_center width height注意坐标全部要归一化到 0 到 1 之间。比如一张 1920x1080 的图片中一个坑洼检测框左上角在 (480, 270)右下角在 (960, 540)则标注为0 0.375 0.375 0.25 0.25解释一下这个换算过程x_center (480 960) / 2 / 1920 0.375y_center (270 540) / 2 / 1080 0.375width (960 - 480) / 1920 0.25height (540 - 270) / 1080 0.25标注工具可以选择 LabelImg 或 LabelStudio前者轻量后者功能更强。标注时有一个建议坑洼的边缘通常不规则标注框不要卡得太紧留出少量边缘区域可以让模型学习到坑洼与周围路面的对比特征反而有助于训练收敛。如果你是基于道路车载摄像头拍的道路画面做训练建议多收集不同光照条件的数据——顺光、逆光、阴天、傍晚。坑洼检测受光照影响非常明显阴影和低光照区域容易被漏检数据多样性直接决定模型的泛化能力。4. 模型训练从数据到可用权重4.1 准备数据集目录结构YOLOv8 训练需要特定的数据集目录结构。以pothole_dataset为根目录推荐结构如下pothole_dataset/ ├── images/ │ ├── train/ # 训练集图片 │ └── val/ # 验证集图片 ├── labels/ │ ├── train/ # 训练集标注 │ └── val/ # 验证集标注 └── pothole.yaml # 数据集配置文件其中pothole.yaml的内容# 文件路径pothole_dataset/pothole.yaml path: /absolute/path/to/pothole_dataset # 数据集根目录的绝对路径 train: images/train val: images/val nc: 2 names: [pothole, crack]这里nc是类别数量names是类别名称。你的项目如果只检测坑洼可以设为nc: 1类别名改为[pothole]。如果还要检测裂缝、修补区域就对应增加。训练集和验证集按比例划分常见是 8:2 或 9:1。要注意同一路段连续帧的图片不要同时出现在训练集和验证集中否则会造成数据泄漏验证结果虚高真实场景效果反而不好。4.2 训练参数选择写训练脚本前先理解几个关键参数的含义epochs训练轮数。一般 100 轮起步坑洼数据集如果量不大50 轮左右可能就收敛了。imgsz输入图片尺寸。默认 640如果你的坑洼目标比较小在整张巡检图里只占几十像素可以改成 800 或 1024。代价是训练和推理速度都会变慢。网络热词里提到 YOLOv8 小目标检测头说明在无人机的视角下坑洼面积占比会更小图像尺寸可能还要更大。batch批次大小。取决于 GPU 显存16GB 显存可以尝试 batch 16 或 32。device指定 GPU 或 CPU。CPU 训练很慢建议有 GPU 再训练。project和name训练日志和权重的输出路径。最小训练命令yolo train datapothole_dataset/pothole.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0yolov8n.pt是 nano 版本预训练权重模型最小、速度最快适合快速跑通流程。如果追求更高精度可以换yolov8s.pt甚至yolov8m.pt。道路坑洼检测对实时性要求不高巡检车一般车速不快精度优先级高于速度建议从yolov8s.pt开始尝试。训练开始后日志里会输出每个 epoch 的box_loss、cls_loss、dfl_loss以及验证集的mAP50、mAP50-95。你关注的应该是 mAP50它表示当 IoU 阈值为 0.5 时所有类别的平均精度是目标检测最常用的评估指标。训练完成后权重文件保存在runs/detect/train/weights/目录下其中best.pt是验证集效果最好的权重last.pt是最后一个 epoch 的权重。部署时用best.pt。5. PyQt5 界面开发从零搭建检测系统 GUI5.1 整体界面布局设计这个系统的界面可以分成几个功能区检测模式切换区、图像显示区、检测结果列表区、操作区。下面是一个适合做路面检测的布局方案-------------------------------------------------------------- | 标题 | 模式切换图片/视频/摄像头 | 设置按钮 | -------------------------------------------------------------- | | | 图像/视频显示区域 | | | -------------------------------------------------------------- | 打开图片 打开视频 开始摄像头 停止检测 导出结果 | -------------------------------------------------------------- | 序号 | 类别 | 置信度 | x坐标 | y坐标 | 宽度 | 高度 | --------------------------------------------------------------这个布局有几个设计上的考虑模式切换用QTabWidget或QComboBox实现三种检测模式共用同一个显示区域和结果表格。结果显示区域用QLabel显示QPixmap当视频实时播放时用paintEvent绘制检测框。表格控件用QTableWidget每次检测完成后清空并重新填充。5.2 项目代码结构建议不要把代码堆在一个文件里。推荐按功能拆分pothole_detector/ ├── main.py # 程序入口 ├── detector.py # YOLOv8 检测封装 ├── ui/ │ ├── __init__.py │ ├── main_window.py # 主窗口 │ └── worker.py # 摄像头/视频工作线程 └── resources/ └── models/ └── best.pt # 训练好的权重这样分层的好处是检测逻辑与界面逻辑分离后续如果要换模型或换 GUI 框架无需大改整体结构。5.3 核心代码实现先写检测封装类它的职责是加载模型、执行推理、返回结构化结果# 文件路径detector.py import cv2 import numpy as np from ultralytics import YOLO class PotholeDetector: YOLOv8 坑洼检测封装类 def __init__(self, model_path: str, conf_threshold: float 0.25): :param model_path: 训练好的权重文件路径 (best.pt) :param conf_threshold: 置信度阈值低于该值的结果会被过滤 self.model YOLO(model_path) self.conf_threshold conf_threshold def detect_frame(self, frame: np.ndarray) - tuple: 对单帧图像执行检测 :param frame: BGR 格式图像 (OpenCV 默认格式) :return: (annotated_frame, detections) annotated_frame: 绘制了检测框的图像 detections: 检测结果列表每个元素为 [class_name, confidence, x, y, w, h] results self.model(frame, confself.conf_threshold, verboseFalse) # 取第一张图的检测结果 result results[0] # 原始图像不带检测框 annotated_frame result.plot() detections [] for box in result.boxes: class_id int(box.cls[0]) class_name result.names[class_id] confidence float(box.conf[0]) # xywh 格式中心点坐标 宽高 x_center, y_center, width, height box.xywh[0].tolist() # 转成左上角坐标方便表格显示和后续处理 x int(x_center - width / 2) y int(y_center - height / 2) detections.append([ class_name, round(confidence, 4), x, y, int(width), int(height) ]) return annotated_frame, detections def detect_image(self, image_path: str) - tuple: 检测单张图片 :param image_path: 图片文件路径 :return: (annotated_frame, detections) frame cv2.imread(image_path) if frame is None: raise ValueError(f无法读取图片: {image_path}) return self.detect_frame(frame)这个封装类有几个值得注意的细节result.plot()是 Ultralytics 提供的方法它会把检测框、类别名称、置信度直接绘制在图像副本上返回。省去了自己用cv2.rectangle画框的步骤代码更简洁画出来的效果也更统一。坐标转换成左上角格式是为了在QTableWidget里显示时更直观。PyQt5 的表格不需要中心点坐标用户更习惯看到框的左上角位置和宽高。接下来写主窗口。主窗口要处理三个核心交互打开图片检测、打开视频检测、摄像头实时检测# 文件路径ui/main_window.py import cv2 import sys from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTableWidget, QTableWidgetItem, QFileDialog, QComboBox, QMessageBox, QHeaderView ) from detector import PotholeDetector class VideoThread(QThread): 视频/摄像头处理线程避免阻塞 UI change_pixmap_signal pyqtSignal(object) update_table_signal pyqtSignal(list) def __init__(self, video_source, detector: PotholeDetector, parentNone): super().__init__(parent) self.video_source video_source # 0 表示默认摄像头或视频文件路径 self.detector detector self._is_running True def run(self): cap cv2.VideoCapture(self.video_source) if not cap.isOpened(): self.change_pixmap_signal.emit(None) return while self._is_running: ret, frame cap.read() if not ret: break annotated_frame, detections self.detector.detect_frame(frame) self.change_pixmap_signal.emit(annotated_frame) self.update_table_signal.emit(detections) cap.release() def stop(self): self._is_running False self.wait() class MainWindow(QMainWindow): def __init__(self, detector: PotholeDetector): super().__init__() self.detector detector self.video_thread None self.current_frame None self.setWindowTitle(道路坑洼缺陷检测系统 - YOLOv8 PyQt5) self.setMinimumSize(1000, 700) self._init_ui() def _init_ui(self): central_widget QWidget() self.setCentralWidget(central_widget) main_layout QVBoxLayout() central_widget.setLayout(main_layout) # 模式选择与操作按钮区 top_layout QHBoxLayout() self.mode_combo QComboBox() self.mode_combo.addItems([图片检测, 视频检测, 摄像头实时检测]) top_layout.addWidget(self.mode_combo) self.open_btn QPushButton(打开文件) self.open_btn.clicked.connect(self.open_file) top_layout.addWidget(self.open_btn) self.start_btn QPushButton(开始) self.start_btn.clicked.connect(self.start_detection) top_layout.addWidget(self.start_btn) self.stop_btn QPushButton(停止) self.stop_btn.clicked.connect(self.stop_detection) self.stop_btn.setEnabled(False) top_layout.addWidget(self.stop_btn) self.export_btn QPushButton(导出结果) self.export_btn.clicked.connect(self.export_results) top_layout.addWidget(self.export_btn) top_layout.addStretch() main_layout.addLayout(top_layout) # 图像显示区 self.image_label QLabel(请选择检测模式并打开文件) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumHeight(400) self.image_label.setStyleSheet( border: 1px solid #ccc; background-color: #f8f8f8; ) main_layout.addWidget(self.image_label, stretch1) # 结果表格区 self.result_table QTableWidget() self.result_table.setColumnCount(7) self.result_table.setHorizontalHeaderLabels( [序号, 类别, 置信度, x, y, 宽度, 高度] ) self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) main_layout.addWidget(self.result_table, stretch0) def open_file(self): mode self.mode_combo.currentText() if mode 图片检测: file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.jpeg *.png *.bmp) ) if file_path: try: annotated_frame, detections self.detector.detect_image(file_path) self.show_frame(annotated_frame) self.update_table(detections) except Exception as e: QMessageBox.critical(self, 检测失败, str(e)) elif mode 视频检测: file_path, _ QFileDialog.getOpenFileName( self, 选择视频, , 视频文件 (*.mp4 *.avi *.mov *.mkv) ) if file_path: self.start_video_thread(file_path) def start_detection(self): mode self.mode_combo.currentText() if mode 摄像头实时检测: self.start_video_thread(0) # 0 表示默认摄像头 elif mode 视频检测: self.open_file() elif mode 图片检测: self.open_file() def start_video_thread(self, source): if self.video_thread and self.video_thread.isRunning(): self.video_thread.stop() self.video_thread VideoThread(source, self.detector) self.video_thread.change_pixmap_signal.connect(self.show_frame) self.video_thread.update_table_signal.connect(self.update_table) self.video_thread.finished.connect(lambda: self.stop_btn.setEnabled(False)) self.video_thread.start() self.stop_btn.setEnabled(True) self.start_btn.setEnabled(False) def stop_detection(self): if self.video_thread and self.video_thread.isRunning(): self.video_thread.stop() self.stop_btn.setEnabled(False) self.start_btn.setEnabled(True) def show_frame(self, frame): if frame is None: QMessageBox.warning(self, 提示, 无法打开视频或摄像头) self.stop_detection() return self.current_frame frame # OpenCV 默认 BGR 格式需要转成 RGB 再显示 rgb_image cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w q_image QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap QPixmap.fromImage(q_image) # 缩放图片适配显示区域 scaled_pixmap pixmap.scaled( self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) def update_table(self, detections): self.result_table.setRowCount(len(detections)) for i, det in enumerate(detections): self.result_table.setItem(i, 0, QTableWidgetItem(str(i 1))) self.result_table.setItem(i, 1, QTableWidgetItem(str(det[0]))) self.result_table.setItem(i, 2, QTableWidgetItem(str(det[1]))) self.result_table.setItem(i, 3, QTableWidgetItem(str(det[2]))) self.result_table.setItem(i, 4, QTableWidgetItem(str(det[3]))) self.result_table.setItem(i, 5, QTableWidgetItem(str(det[4]))) self.result_table.setItem(i, 6, QTableWidgetItem(str(det[5]))) def export_results(self): if not self.current_frame is not None: QMessageBox.warning(self, 提示, 没有可导出的结果) return file_path, _ QFileDialog.getSaveFileName( self, 保存检测结果, detection_result.jpg, 图片文件 (*.jpg *.png) ) if file_path: cv2.imwrite(file_path, self.current_frame) QMessageBox.information(self, 导出成功, f结果已保存到:\n{file_path})然后是程序入口# 文件路径main.py import sys from PyQt5.QtWidgets import QApplication from detector import PotholeDetector from ui.main_window import MainWindow def main(): app QApplication(sys.argv) # 模型路径根据实际位置修改 model_path resources/models/best.pt detector PotholeDetector(model_path, conf_threshold0.35) window MainWindow(detector) window.show() sys.exit(app.exec_()) if __name__ __main__: main()5.4 代码中几个关键工程点这段代码里最值得解释的是VideoThread。为什么不用QTimer配合cap.read()直接在主线程里读摄像头原因是摄像头的cap.read()是阻塞操作。如果当前帧读取或模型推理耗时较长QTimer回调里的代码就会阻塞 UI 事件循环窗口表现为卡顿、无法拖动、按钮无响应。把视频读取和推理放到QThread里主线程只负责接收信号刷新界面这是 PyQt5 做实时视频检测的标准做法。另一个细节是show_frame里做了一次cv2.cvtColor。OpenCV 读入的图像是 BGR 顺序而QImage和QPixmap期望的是 RGB 顺序。如果跳过这一步直接显示所有人脸和路面的红蓝通道会被颠倒检测框颜色也会异常。这是新手最容易忽略的地方。还有一个细微问题需要提醒QThread退出时要在closeEvent里清理。当用户关闭窗口时如果线程还在运行程序可能崩溃。在主窗口加一个关闭事件处理def closeEvent(self, event): if self.video_thread and self.video_thread.isRunning(): self.video_thread.stop() event.accept()6. 系统整合与功能测试验证6.1 从训练到界面的完整链路整套系统的数据流是训练好的best.pt权重文件放在resources/models/目录下。程序启动时PotholeDetector类加载权重初始化 YOLO 模型。用户在界面上选择图片、视频或摄像头。检测模块对每一帧执行推理返回标注图像和结构化检测数据。界面刷新图像显示区同时把检测框数据填入表格。用户可以将标注后的图像导出保存。整个链条中模型权重是核心资产界面只是外壳。换数据集重新训练后只要保留best.pt路径不变界面代码完全不用动。6.2 验证测试的预期效果以图片检测模式为例选择一张包含坑洼和裂缝的道路图片预期效果是图片加载后显示区在 1 秒内出现标注结果。坑洼位置被红色或绿色矩形框标出框上方显示类别名称和置信度。表格列出所有检测框的坐标和置信度信息。检测框数量与图片中的缺陷数量基本对应可能有个别误检或漏检这取决于模型训练效果。真正需要关注的验证标准是推理耗时。图片检测应该毫秒级完成视频检测或摄像头实时检测时帧率至少在 10 FPS 以上界面才能流畅显示。如果帧率过低优先排查是不是 GPU 没有参与推理或者输入图像尺寸设得过大。6.3 检测结果的工程化导出表格数据也可以导出为 CSV方便后续统计和上报。补充一个导出逻辑import csv def export_to_csv(self, detections, file_path): 将检测结果导出为 CSV 文件 with open(file_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([序号, 类别, 置信度, x, y, 宽度, 高度]) for i, det in enumerate(detections): writer.writerow([i 1] det)加入utf-8-sig编码是为了让 Excel 直接打开 CSV 时不乱码。这个细节在实际交付时很有用巡检人员拿到 CSV 可以直接用 Excel 统计坑洼数量。7. 常见问题与排查方法实践过程中最常遇到的问题整理成一张排查表问题现象可能原因排查方式解决方案PyQt5 安装后导入失败Python 版本过高或 PyQt5 缺少绑定文件pip show PyQt5检查安装状态使用 Python 3.8-3.10 创建虚拟环境重新安装中文路径图片无法检测OpenCV 的 imread 不支持中文路径检查返回的 frame 是否为 None将图片放到纯英文路径下或基于文件流读取视频检测界面卡死推理在主线程执行阻塞 UI观察界面能否拖动、按钮能否响应使用 QThread 承载视频读取和推理逻辑摄像头打不开摄像头被其他程序占用或权限不足用系统自带相机先测试关闭占用程序检查系统摄像头权限检测框绘制位置错乱BGR 与 RGB 通道顺序未转换查看检测结果图的颜色是否偏蓝在 show_frame 中执行 cv2.cvtColor 转换模型推理速度慢没有使用 GPU或 imgsz 太大查看 torch.cuda.is_available()安装 CUDA 版 PyTorch降低 imgsz导出图片显示异常导出的 frame 是 BGR 格式而用户用其他工具查看检查文件能否正常打开按需在导出前转换颜色空间下拉框切换后界面不刷新模式切换后未重新初始化控件状态检查按钮 enable 状态和信号连接在 currentTextChanged 信号中更新按钮状态训练时显存不足batch 或 imgsz 太大查看 GPU 显存占用降低 batch 到 4 或 8降低 imgsz 到 544这里面要特别提一下中文路径问题。cv2.imread在 Windows 下遇到中文路径会返回None而且不报错。很多开发者调试半天找不到原因结果只是图片路径里有中文。稳妥的做法是在项目文档里明确要求用户把待检测文件放到英文路径下或者用cv2.imdecode(np.fromfile(...))代替imreaddef imread_unicode(file_path): 支持中文路径的图片读取 import numpy as np data np.fromfile(file_path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)8. 工程落地最佳实践与优化建议8.1 置信度阈值的调优策略代码里我建议把默认置信度阈值设为 0.25但实际部署时这个值需要针对场景调优。如果现场漏检多坑洼没识别出来适当降低阈值到 0.2如果误检多正常路面被标成坑洼提高到 0.35 或 0.4。可行的做法是在界面上加一个阈值滑动条让现场人员按需调整。8.2 延迟加载模型提升启动速度如果每次程序启动都加载模型用户要等好几秒才能看到界面。工程上更友好的做法是先把界面显示出来再用后台线程加载模型。界面代码可以加一个“模型加载中”的状态提示模型加载完成后自动进入可用状态。这个小优化对用户体验提升很大。8.3 批量检测的加入实际巡检场景中用户面对的不是一张图片而是一整个文件夹的巡检照片。在界面上增加“批量检测文件夹”功能遍历目录下所有图片逐张检测并保存结果到输出文件夹是这个项目最值得优先扩展的能力。8.4 模型部署时的资源要求关于跑 YOLOv8 是否需要 GPU这个要分角色说训练阶段建议使用 GPU。GTX 1660 Ti 这类入门级显卡可以把模型训练起来7GB 左右显存跑yolov8n或yolov8s问题不大但 batch 要调小。显存更大的显卡体验会好很多。推理阶段CPU 也能运行但在摄像头实时检测场景下帧率比较低体验一般。建议至少使用一张中端独立显卡做推理加速。8.5 安全与合规提醒如果你要在真实道路上部署这套系统有几个原则必须遵守训练数据应使用公开数据集或自己合法采集的道路图像不能未经授权抓取他人的图像数据。摄像头录制道路画面时要注意行人、车牌等个人信息的保护尽量选择非敏感路段或在数据处理时做脱敏。系统检测结果只能作为道路养护决策的参考不能替代专业检测机构的鉴定。8.6 打包分发与交付开发完成后把 Python 脚本交付给别人并不现实——对方需要安装 Python、安装依赖、配置模型路径。更好的交付方式是用 PyInstaller 打包成独立执行文件pip install pyinstaller pyinstaller -w -F main.py --name pothole_detector --hidden-importultralytics说明一下这几个参数-w打包成窗口程序不显示控制台。-F打包成单个 exe 文件。--hidden-importultralytics确保 ultralytics 模块被包含进包。打包时需要把best.pt一并放入建议放在resources/models/目录下并在代码中使用相对路径或运行时自动查找。打包后整个系统就是一个双击即可运行的桌面工具这才达到可以交付给非技术人员的状态。9. 例行总结从 Demo 到产品还差哪几步回顾一下这套基于 YOLOv8 PyQt5 的坑洼路面缺陷检测系统核心价值不只是“用 YOLOv8 检测坑洼”而是完成了一个完整的工程闭环数据集准备、模型训练、桌面界面封装、实时视频处理、结果导出、独立打包交付。真正让这个项目有分量的是那个把模型能力呈现给普通用户的 PyQt5 界面以及所有界面背后的线程处理、坐标转换、文件交互、异常处理细节。如果你照着本文做完这个项目你得到的不仅是一个可以检测坑洼的软件更是一套可以复用的工程模板。把类别从坑洼换成裂缝、更换数据重新训练同一套框架就能适配道路缺陷检测的其他场景这也是这个技术在工厂缺陷检测、火灾烟雾检测等场景反复出现的原因检测模型在变但“模型 GUI 桌面工具”的产品形态是稳定的。如果你想继续深入我建议按这几个方向进阶。一是模型结构的改进。YOLOv8 的检测头可以做针对性优化比如增加小目标检测头来捕捉画面中较小区域的坑洼注意力机制的引入也有助于减少复杂背景干扰。二是工程性能优化研究 TensorRT 或 ONNX Runtime 对模型推理速度的优化让实时检测从 15 FPS 提升到 30 FPS 以上。三是功能完整性增加巡检轨迹记录、缺陷位置经纬度标注、自动报表生成等功能让系统真正嵌入道路养护的业务流程。现阶段可以先把这个项目完整做一遍跑通从训练到打包的全链路然后挑一个真实场景做验证。路面缺陷检测这个方向的生命力在于它贴近真实需求是值得认真打磨的落地项目。