拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv8与PyQt5的锂电池表面缺陷检测系统设计与实现

基于YOLOv8与PyQt5的锂电池表面缺陷检测系统设计与实现

1. 系统整体设计与技术选型思路

1.1 为什么选YOLOv8做缺陷检测

锂电池表面缺陷检测这件事,放在工业质检场景里其实非常典型。极片划痕、凹坑、脏污、气泡、破损,这些缺陷直接关系到电池的安全性和使用寿命,所以产线上对这一块的检测要求一直很高。但传统机器视觉做法是图像差分加人工设计特征,遇到反光不均匀、背景复杂的情况就特别容易误检,而且换一个型号的电池就要重新调一套规则,维护成本相当高。

YOLOv8能在这种场景下胜出,核心原因在于它把目标检测做成了一条非常顺滑的流水线。作为单阶段检测器,它不像Faster R-CNN那样需要单独跑区域提议网络,而是一个前向推理直接输出类别和边界框,速度上有天然优势。在GTX 1660 Ti这种6G显存的卡上,yolov8s模型跑640分辨率输入基本能稳定到30-40 FPS,完全够用。另外ultralytics这个官方库把数据加载、增强、训练、验证、导出全部封装好了,不用自己写一堆训练循环,这对毕业设计来说能省下大量时间。

还有一点很实际:YOLOv8的工程生态非常成熟。无论是PyPI一键安装、自带预训练权重、支持ONNX/TensorRT导出,还是社区里铺天盖地的教程,都意味着你遇到问题基本都能搜到答案。对于毕设这种强交付导向的项目,选它绝对比从零搭一个检测网络靠谱得多。

1.2 为什么选PyQt5做桌面界面

到了界面这一层,核心诉求其实就三个:开发效率要高、能和OpenCV/NumPy方便互操作、打包成exe不要太痛苦。PyQt5在这三点上都很能打。它基于Qt C++库,性能没问题,同时Python绑定做得非常成熟,QImage转OpenCV的BGR格式只需要一行代码,图像数据直接共享内存,避免了序列化和拷贝的开销。

有人会问那PySide6呢?确实,PySide6是Qt官方支持的Python绑定,而且LGPL协议比GPL更宽松。但PyQt5在社区资料量上还是明显占优,网上几乎任何界面问题都能搜到现成答案,这对时间紧张的毕设来说太重要了。还有一点是PyQt5对Python 3.8-3.10的兼容性非常稳,很多坑都已经被人踩平了,而PySide6在一些旧依赖上偶尔会有小毛病。所以我个人推荐稳妥起见选PyQt5,除非你有需要Qt 6新特性的硬需求。

再说为什么不用Web前端或者Electron。虽然Web界面好看,但部署时还得带一个浏览器内核,而且和本地的摄像头、文件系统交互要实现跨语言桥接,复杂度一下子上去了。PyQt5直接在Python进程内跑,摄像头用OpenCV VideoCapture,图片用cv2.imread,检测结果直接画框,整个链路完全不用出Python环境,稳且快。

1.3 自适应界面设计的真实需求

自适应界面听起来像是个加分项,但放到实际场景里其实是刚需。实验室的显示器可能是1920x1080,答辩教室的投影仪分辨率可能是1280x800,导师笔记本的缩放比例可能是125%或150%。如果你用固定像素坐标布局,一旦分辨率变了,控件要么挤成一团,要么超出屏幕边界,现场演示翻车就非常尴尬。

解决这个问题的核心思路有三个层次。第一层是用Qt布局管理器,QHBoxLayout、QVBoxLayout、QGridLayout这套东西,让控件自动跟随窗口拉伸。第二层是处理图像显示区域,QLabel显示检测结果时,要在resizeEvent里重新缩放QPixmap,按比例缩放并居中显示。第三层是处理高DPI,在main.py开头设置Qt.AA_EnableHighDpiScaling属性,否则在150%缩放的屏幕上字体会发虚。这三层都做到位,界面才算真正做到了自适应。

2. 环境搭建与依赖配置

2.1 CUDA和PyTorch版本组合实测

环境配置是整个项目最容易被卡住的第一道关,其中CUDA、cuDNN、PyTorch三者的版本匹配关系是重灾区。先给出我实测稳定的一套组合:操作系统Windows 11,显卡GTX 1660 Ti,CUDA 11.8,cuDNN 8.6.0,Python 3.10,PyTorch 2.0.1,ultralytics 8.0.x。这套组合下yolov8n和yolov8s都能正常跑,训练和推理都没发现兼容性问题。

安装CUDA和cuDNN的要点是这样的。先下载CUDA 11.8安装包,安装后命令行执行nvidia-smi确认显卡能识别到。cuDNN不用单独安装到系统目录,直接把解压出来的bin、include、lib文件夹里的文件复制到CUDA安装目录对应文件夹下就行。千万别装CUDA 12.x再回头配PyTorch 2.0,虽然有些配置能跑,但踩坑概率会高很多。

PyTorch安装建议用官方推荐的pip镜像命令:

pip install torch==2.0.1+cu118 torchvision==0.15.2+cu118 --index-url https://download.pytorch.org/whl/cu118

装完验证一下:

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

如果cuda.is_available()返回False,说明PyTorch和CUDA版本没对上,或者环境变量里CUDA路径有问题。检查一下系统环境变量Path里有没有C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\bin,没有的话手动加进去然后重启终端。这个坑我见过太多人踩了,就是环境变量加得不对导致torch始终认不到GPU。

2.2 PyQt5安装与OpenGL导致界面无显示

PyQt5的安装相对简单,但有一个非常经典的坑:界面闪退或显示黑屏。正常情况下这样装就够了:

pip install pyqt5==5.15.9 pip install pyqt5-tools==5.15.9.3.3

如果你在Windows服务器或者某些远程虚拟环境下运行,会发现程序能启动但窗口显示不出来,或者直接崩溃报错。这个问题的根源在于PyQt5的QtWebEngine组件依赖系统OpenGL,而虚拟机或远程桌面环境里往往没有完整的OpenGL驱动。更常见的情况是直接闪退没有任何提示。

解决方法是把Qt的渲染模式切换到软件渲染。在main.py的最开头加上这几行:

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

如果还是不行,可以安装opengl32sw.dll替代系统的OpenGL实现:

pip install PyQt5-opengl

然后在环境变量里设置:

os.environ["QT_OPENGL_DLL"] = "opengl32sw"

这个问题虽然不常发生,但一旦遇到就很迷惑,我先写在这里给大家排雷。

2.3 工程目录结构规划

一个清晰的目录结构能让你少掉很多头发。我推荐这么划分:

lithium_battery_defect/ ├── main.py # 程序入口 ├── config/ │ └── settings.py # 全局配置(路径、模型参数、阈值) ├── models/ │ └── best.pt # 训练好的权重(YOLOv8格式) ├── ui/ │ ├── main_window.py # 主窗口界面 │ ├── widget_utils.py # 自定义控件辅助函数 │ └── style.qss # 界面样式 ├── detect/ │ ├── detector.py # 模型推理封装类 │ └── thread_utils.py # 推理线程、视频线程 ├── datasets/ │ ├── images/ # 原始图片 │ ├── labels/ # 标注txt文件 │ └── data.yaml # 数据集配置文件 ├── resources/ │ └── icon.png # 界面图标 └── output/ └── result/ # 检测结果输出目录

建议把模型权重、数据集、代码分开存。很多人习惯把所有文件堆在一个目录,后面找东西非常痛苦。而且等你要打包exe的时候,目录结构越清晰越容易配置打包参数。

3. 数据集制作与模型训练

3.1 锂电池表面缺陷类型与标注规范

锂电池表面常见的缺陷类型包括划痕、凹坑、脏污、气泡、破损、压伤等,不同厂家对缺陷的定义会有些差异,但大致类别是差不多的。这里需要注意一个关键点:缺陷类别的粒度不要分得太细。比如划痕和细划痕,如果它们的形态差异不够明显,模型很难区分,反而会把精度拉低。建议合并成5-6个大类,每类保证至少500-1000张样本。

标注工具我推荐直接用labelImg或labelme。labelImg适合做矩形框标注,保存为YOLO格式的txt文件;labelme适合多边形标注,但需要自己转格式。对YOLOv8来说,用labelImg就好,因为YOLOv8原生就是水平矩形框检测。

标注过程中的细节很重要。第一,缺陷区域要用尽量紧密的框框住整个可见区域,不要留太多背景,也不要切掉缺陷的边缘。第二,避免一个目标只框了一部分,特别是长条形的划痕,尽量框完整。第三,标注时要保持一致性,同样的形态在所有图片里都标同一类别。如果标注不一致,模型会学得很混乱。

数据集的目录结构要符合YOLO格式要求:

datasets/ ├── images/ │ ├── train/ # 训练集图片 │ └── val/ # 验证集图片 ├── labels/ │ ├── train/ # 训练集标注 │ └── val/ # 验证集标注 └── data.yaml # 数据集配置

data.yaml内容:

train: datasets/images/train val: datasets/images/val nc: 6 names: ['scratch', 'dent', 'dirty', 'bubble', 'crack', 'breakage']

注意data.yaml里面的路径,如果你在Windows下跑,建议写绝对路径或者相对训练命令的执行路径,避免出现反斜杠转义问题。

3.2 预训练权重选择与训练参数配置

YOLOv8有n/s/m/l/x五个尺寸,有的同学一上来就直接用yolov8x,结果1660Ti根本带不动,batch只能设1,训练速度慢到怀疑人生。根据我的实测,336张图、6个类别、训练300轮的话,GTX 1660Ti上yolov8n大概4小时能跑完,yolov8s大概8-10小时,yolov8m可能要2天。对毕业设计来说yolov8s是最推荐的平衡点,精度比n高一截,速度也足够。

训练参数这块,关键的是这几个:

from ultralytics import YOLO model = YOLO("yolov8s.pt") # 加载预训练权重 model.train( data="datasets/data.yaml", epochs=200, imgsz=640, batch=8, lr0=0.01, patience=50, device=0, workers=4, )
  • epochs:我建议设200左右,配合早停机制,不用真的跑满,通常到150轮左右就收敛了。
  • batch:1660Ti 6G显存上yolov8s最大能跑到16,但建议保守一点设8,留出显存给验证过程。
  • lr0:默认0.01就好,不用特意调。
  • patience:早停的容忍轮数,设为50表示50轮mAP没提升就提前结束。
  • workers:Windows下建议设4,过高容易报DataLoader worker错误。

训练产生的结果保存在runs/detect/train/目录下。里面有last.pt和best.pt,别搞混了。best.pt是根据验证集mAP保存的最优权重,我们部署时使用的就是它。

3.3 损失函数曲线的可视化与保存

训练过程中yolov8默认会输出很多指标,包括box_loss、cls_loss、dfl_loss,以及precision、recall、mAP50、mAP50-95。但是训练完成后,很多人不知道怎么看这些曲线,或者想单独生成损失函数曲线的图。其实ultralytics在训练过程中已经把曲线图保存在了runs/detect/train/results.png里,这里面包含了所有指标的曲线。

如果想要单独画某一条损失曲线,比如box_loss,可以读取训练时保存的CSV文件:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") plt.plot(df["epoch"], df["train/box_loss"], label="train_box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val_box_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.grid(True) plt.title("Box Loss Curve") plt.savefig("box_loss.png", dpi=150) plt.show()

这个CSV文件在训练结束后会生成在results.csv里,记录每一个epoch的指标。答辩的时候把这个图放在PPT里,导师会认为你真的理解了训练过程的迭代逻辑,加分作用非常明显。

训练结束后验证一下模型表现:

model = YOLO("runs/detect/train/best.pt") metrics = model.val(data="datasets/data.yaml") print(f"mAP50: {metrics.box.map50:.4f}, mAP50-95: {metrics.box.map:.4f}")

如果mAP50超过0.85,说明模型已经比较可靠了;如果只有0.6左右,就需要回来看是不是标注有问题、类别太相似、或者样本量不够。

3.4 模型导出与单张图测试

训练完成后,为了让推理速度更快,可以把权重导出为ONNX格式,或者如果部署在NVIDIA显卡上导出为TensorRT engine格式:

model = YOLO("runs/detect/train/best.pt") model.export(format="onnx", imgsz=640, half=True) model.export(format="engine", imgsz=640, half=True)

ONNX格式的好处是跨平台、跨框架通用,PyQt5程序里调用也方便。但要注意导出的onnx文件在推理时需要匹配的opencv版本或onnxruntime版本,养成好习惯就是用requirements.txt锁定版本。

用导出后的模型跑一张图测试:

from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("test.jpg", conf=0.25, iou=0.45) for r in results: boxes = r.boxes.xyxy.cpu().numpy() confs = r.boxes.conf.cpu().numpy() cls_ids = r.boxes.cls.cpu().numpy().astype(int) for box, conf, cls_id in zip(boxes, confs, cls_ids): print(f"类别: {model.names[cls_id]}, 置信度: {conf:.3f}, 坐标: {box}")

conf设0.25比较合理,太高会漏检,太低会多很多误检。

4. PyQt5自适应界面设计与检测模块集成

4.1 主窗口布局:从原型到完整界面

在动手写界面代码之前,先把界面的功能分区想清楚。一个完整的检测系统界面至少需要有以下几个模块:文件操作区(打开图片、打开文件夹、打开摄像头)、检测控制区(开始检测、停止检测、保存结果)、结果显示区(原图展示、检测结果展示)、状态信息区(模型信息、推理耗时、检测计数)、日志输出区(运行日志)。

基于这几个功能模块,主窗口我推荐用QMainWindow搭配中央部件。左侧固定宽度的控制面板用QVBoxLayout排列按钮和选项,右侧用QSplitter分上下两个QLabel显示原图和检测结果。QSplitter的好处是用户可以拖动分割条,调整图像显示区域的比例,这本身就是一种自适应的体验。底部用QTextEdit作为日志输出区域。

具体布局伪代码如下:

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("锂电池表面缺陷检测系统") self.resize(1280, 800) self.setup_ui() self.setup_detector() self.setup_thread() def setup_ui(self): # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central_widget) # 左侧控制面板 left_widget = QWidget() left_widget.setFixedWidth(240) left_layout = QVBoxLayout(left_widget) # 文件按钮 self.btn_open_image = QPushButton("打开图片") self.btn_open_dir = QPushButton("打开文件夹") self.btn_open_camera = QPushButton("打开摄像头") self.btn_stop_detect = QPushButton("停止检测") self.btn_save_result = QPushButton("保存结果") ... # 右侧显示区域 right_splitter = QSplitter(Qt.Vertical) self.label_original = QLabel("原始图像") self.label_result = QLabel("检测结果") right_splitter.addWidget(self.label_original) right_splitter.addWidget(self.label_result) main_layout.addWidget(left_widget) main_layout.addWidget(right_splitter)

有一点经验可以分享:按钮不要用太花哨的样式,但是一定要给按钮设一个固定高度,比如40px,不然在高DPI缩放下按钮会变得拥挤或者变形。可以写一个统一按钮高度的函数。

4.2 自适应分辨率的三种实现手段

自适应界面设计不是一句话的事,得从三个层面都做到位。

第一个层面是布局管理器自动伸缩。这一点用好QVBoxLayout、QHBoxLayout和QSplitter就基本实现了。不要在布局管理器里给控件设置固定的绝对坐标。如果某个控件确实需要固定宽度,比如左侧面板,可以设置setFixedWidth。但主显示区域一定要跟着窗口走,用QSplitter加addWidget的方式就能做到。

第二个层面是图像显示区域的等比例缩放。直接给QLabel设置setPixmap后,窗口缩放时图片并不会自动缩放,需要重写resizeEvent事件:

class ScalableLabel(QLabel): def __init__(self, text="", parent=None): super().__init__(text, parent) self._pixmap = QPixmap() self.setMinimumSize(1, 1) self.setAlignment(Qt.AlignCenter) # 保证不拉伸变形 self.setScaledContents(False) def set_image(self, img_bgr): # 将OpenCV BGR图像转为QImage h, w, ch = img_bgr.shape rgb_img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) qimage = QImage(rgb_img.data, w, h, ch * w, QImage.Format_RGB888) self._pixmap = QPixmap.fromImage(qimage.copy()) self.update_display() def update_display(self): if not self._pixmap.isNull(): scaled = self._pixmap.scaled( self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.setPixmap(scaled) def resizeEvent(self, event): super().resizeEvent(event) self.update_display()

这段代码的核心是Qt.KeepAspectRatio,保证图片在缩放时按比例缩放,不变形。同时重写resizeEvent,窗口大小变化时自动触发重新缩放。这样无论你把窗口拉多大多小,图像都会完整、等比例地显示在里面。

第三个层面是高分屏适配。在main.py的入口处加上:

import sys from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app = QApplication(sys.argv)

这两个属性要放在创建QApplication之前设置,否则不生效。另外字体方面建议统一设置应用字体,比如微软雅黑,避免默认字体在125%缩放下线条过细。

4.3 检测线程封装:不卡界面的关键

做桌面检测系统最容易犯的错误,就是直接在主线程里调用model.predict()。虽然代码写起来简单,但推理一次少说也要50-100ms,加上画图操作,界面会明显卡顿,拖拽窗口都会掉帧。更严重的是,如果图片分辨率大或者连续处理视频帧,界面直接无响应假死,这是答辩现场最忌讳的。

正确做法是用QThread将推理放到子线程,通过信号与槽返回结果。我封装了两个类:DetectionThread(负责跑模型推理)和VideoThread(负责读视频帧),它们之间用Python标准信号连接到UI更新。

class DetectionThread(QThread): result_ready = pyqtSignal(dict) log_message = pyqtSignal(str) def __init__(self, model_path, conf=0.25, iou=0.45): super().__init__() self.model_path = model_path self.conf = conf self.iou = iou self.model = None self._running = False self._img = None def run(self): # 在子线程中加载模型,避免启动卡顿 if self.model is None: from ultralytics import YOLO self.model = YOLO(self.model_path) while self._running: if self._img is not None: img = self._img self._img = None results = self.model.predict( img, conf=self.conf, iou=self.iou, verbose=False ) # 处理结果 for r in results: boxes = r.boxes.xyxy.cpu().numpy() confs = r.boxes.conf.cpu().numpy() cls_ids = r.boxes.cls.cpu().numpy().astype(int) names = self.model.names detections = [] for box, conf, cls_id in zip(boxes, confs, cls_ids): detections.append({ 'box': box, 'conf': float(conf), 'cls_id': int(cls_id), 'cls_name': names[int(cls_id)] }) drawn_img = r.plot() # YOLOv8内置画框 self.result_ready.emit({'img': drawn_img, 'detections': detections}) else: self.msleep(10) def update_image(self, img): self._img = img def start_detection(self): self._running = True self.start() def stop_detection(self): self._running = False self.wait() def load_model(self): # 预加载模型,避免首次推理缓慢 if self.model is None: from ultralytics import YOLO self.model = YOLO(self.model_path)

主线程中连接信号:

self.detect_thread = DetectionThread("models/best.pt") self.detect_thread.result_ready.connect(self.update_result_display) self.detect_thread.log_message.connect(self.log_output.append) # 打开图片的逻辑 def open_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.jpg *.jpeg *.png *.bmp)" ) if file_path: img = cv2.imread(file_path) # 先显示原图 self.label_original.set_image(img) # 发送给检测线程 self.detect_thread.update_image(img) if not self.detect_thread.isRunning(): self.detect_thread.start_detection()

这里有个小细节:模型加载不要放在__init__里,因为ultralytics第一次加载模型需要初始化CUDA上下文,放到子线程run方法里加载,可以让窗口先弹出,用户体验好很多。如果加载过程太久用户以为程序卡死了,反而不好。

4.4 视频与摄像头实时检测:帧率优化策略

处理视频文件或者摄像头实时流时,逻辑和单张图片不同,需要用一个单独的线程持续读帧,把每一帧发送到检测线程。注意两个线程之间不要直接用队列传输大数组,因为Python多线程受GIL限制,传输大数组会拖慢整体速度。更好的做法是使用共享变量加锁,或者直接用带锁的img变量。

class VideoThread(QThread): frame_ready = pyqtSignal(object) video_end = pyqtSignal() def __init__(self): super().__init__() self.cap = None self._running = False self.mode = None # 'camera' or 'video' def start_camera(self, camera_index=0, width=1280, height=720): self.cap = cv2.VideoCapture(camera_index) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) self.mode = 'camera' self._running = True self.start() def start_video(self, video_path): self.cap = cv2.VideoCapture(video_path) self.mode = 'video' self._running = True self.start() def run(self): while self._running and self.cap.isOpened(): ret, frame = self.cap.read() if not ret: self.video_end.emit() break if self.mode == 'camera': # 摄像头画面可以做一次镜像 frame = cv2.flip(frame, 1) self.frame_ready.emit(frame) # 控制读取帧率,避免CPU占用过高 self.msleep(30) # ~33FPS def stop(self): self._running = False if self.cap: self.cap.release() self.wait()

实时检测场景下,完整跑一次yolov8s 640推理大约需要80-120ms,而视频读帧只需要15ms左右,两者叠加会导致帧率下降。我的做法是检测线程本身只处理最新的一帧,丢掉积压的旧帧,保证实时性优先。这也是上面DetectionThread中_img变量用覆盖而非队列的原因。

5. 常见问题与排查技巧实录

5.1 PyQt5界面显示异常

OpenGL导致的界面无显示在前面环境配置部分已经讲过,这里再补充一个常见表现:程序启动后有窗口黑屏,或者点击按钮后整个界面变透明。除了设置QT_OPENGL=software之外,还有一种情况是显卡驱动太旧导致QtWebEngine初始化失败。建议优先升级显卡驱动,仍然不行就卸载重装PyQt5。

pip uninstall pyqt5 pyqt5-tools pyqt5-sip -y pip install pyqt5==5.15.9 pyqt5-tools==5.15.9.3.3

另外注意不要同时安装pyqt5和pyside6,两个包都装在同一个环境里会引起模块冲突,导致随机崩溃。

5.2 界面字体模糊与控件挤在一起

高分屏下如果没设置Qt.AA_EnableHighDpiScaling,界面看起来就像马赛克一样,字是糊的,按钮间距也不正常。这个必须在QApplication创建之前设置。如果设置之后字体还是偏小,可以全局设置字体大小:

from PyQt5.QtWidgets import QApplication font = QApplication.font() font.setPointSize(10) QApplication.setFont(font)

另外在125%/150%缩放下,QLabel显示图片的边缘可能会出现1px的白边,这是QPixmap缩放时的抗锯齿问题。可以在QLabel周围加一个QFrame边框来掩盖,视觉上会干净很多。

5.3 模型训练时显存不足或训练速度极慢

1660Ti 6G显存跑yolov8s,batch设8是比较稳的。如果报CUDA out of memory,按顺序排查:第一,其他程序是否占用显存,比如浏览器、微信偶尔会调用GPU;第二,降低batch到4或2;第三,换用yolov8n模型;第四,在train参数中加cache=False关闭数据缓存。

训练速度极慢还有一个容易被忽略的原因:数据集的图片路径存在中文或者空格,导致ultralytics的加载进程频繁崩盘重试。解决方案是数据集目录和训练项目目录全部用英文路径,不要有空格。

5.4 推理时模型加载慢或反复加载

第一次调用model.predict()时,ultralytics会做一次完整的模型初始化,包括加载权重、初始化CUDA上下文,这个过程耗时1-2秒很正常。如果你发现每检测一张图都要等1-2秒,说明每次predict时都重新实例化了模型。正确做法是创建一次YOLO对象,然后在循环里反复调用它的predict方法。上面的DetectionThread里就是这样做的,模型在run方法里只加载一次。

另外如果要追求更快的响应速度,可以先把第一帧放到线程里做预热:

num_samples = 2 result = model.predict(img, ...) # 前两帧用于预热,后面的推理速度会显著提升

这种预热策略在工业项目中很常见。

5.5 打包成exe时的各种坑

答辩前想把项目打包成exe,这个需求太常见了。但PyQt5 + ultralytics打包时坑非常多。推荐用PyInstaller:

pip install pyinstaller pyinstaller -D -w main.py --add-data "models/best.pt;models" --add-data "datasets/data.yaml;datasets"

几个关键问题:

  • -w参数是去掉控制台窗口,但是调试阶段建议不要加,不然看不到报错信息。
  • --add-data用来打包模型和数据文件,Windows下分隔符用分号,Linux/macOS用冒号。
  • ultralytics进去之后体积巨大,打包出来可能超过500M,这是正常的。如果嫌大可以只导入推理需要的模块,但效果有限。

打完包双击exe没反应,可以先在命令行里运行exe看报错。最常见的是缺DLL,用dumpbin /dependents查看依赖项,或者把dist目录下的文件手动补充到打包目录。另一种方式是降级ultralytics版本,有些版本在PyInstaller下兼容性更好。

6. 一些额外的经验与建议

我一直觉得,毕业设计真正锻炼人的地方不是按部就班跑通流程,而是在遇到各种莫名其妙的问题之后,能快速定位问题根源并解决。拿这个项目来说,从数据标注到模型训练再到界面整合,每一步都有很多细节,而正是这些细节决定了一个系统是demo级别还是能拿到高分级的作品。

有一点特别想强调:模型训练时的数据质量永远比模型结构重要。如果你发现效果不好,先别急着换backbone或者改attention,先回去看看标注是否合理、各类别样本量是否均衡、图片的对比度光照是否太多样。我见过有人为了刷那0.01的mAP去魔改网络结构,结果在答辩现场的测试集上反而翻车。扎实的数据整理和标注统一性,才是项目稳定性的最大保障。

在界面上,我建议在状态栏加入一个实时的“检测帧率”显示。PPT答辩的时候现场演示,评委看到FPS数值在跳,会比看到一张静态截图更有说服力。这个效果实现起来很简单,就是在检测线程返回结果时记录时间戳,计算一帧的时间倒数。

最后再分享一个小技巧。在PyQt5界面里显示检测结果时,YOLOv8已经提供了内置的r.plot()方法画出带标注的图像。但它是加载了自己默认的字体和颜色,对于锂电池缺陷检测这种工业场景,我建议手动重新画一遍,指定缺陷类别的颜色和边框粗细。比如划痕用红色、凹坑用黄色、脏污用蓝色,这样不同类别一眼就能区分开,展示效果好很多。手动画框代码也很简单,直接用cv2.rectangle和cv2.putText在检测结果图上绘制就行。

这个系统后续还有很多可以扩展的方向。比如把缺陷统计结果导出成Excel报表,或者接入PLC做自动分拣联动,甚至用LiteRT做边缘端部署。对于毕业设计而言,先把自己定位成完整的桌面应用,把检测链路和界面交互做好,就已经足够了。过程中踩过的坑,才是这个项目最大的收获。

返回列表