拓冰建站拓冰建站
首页 / 资讯中心 / 正文

YOLOv5+PyQt5口罩检测系统实战:从模型到可交付桌面应用

简介本资源是一个基于YOLOv5的轻量级口罩检测系统完整实现面向计算机视觉初学者、深度学习实践者及防疫类智能应用开发者解决图像、视频与摄像头场景下的实时口罩佩戴状态识别问题适用于课堂实验、课程设计或小型安防监控原型开发。压缩包共150个文件含44个配置与训练参数yaml文件、40个核心py脚本涵盖模型加载、GUI逻辑、帧处理与结果可视化、16个jpg/jpeg测试图及3个pt模型权重文件辅以Dockerfile、教程ipynb和README文档整体139.86MB结构清晰、开箱即用。已有1501人学习下载提供从环境搭建、模型微调到PyQt5界面封装的全流程代码包含多尺度检测适配、实时帧率优化逻辑及UI交互细节如上传/启动/结果显示模块是深入理解目标检测工程化落地的优质实践样本。1. 为什么一个口罩检测系统要同时绑定 YOLOv5 和 PyQt5——不是堆技术而是解决真实交付断层你手上有训练好的 YOLOv5s.pt 模型detect.py跑通了准确率 92.3%但当客户比如学校后勤处、社区服务中心或小型工厂安全部点开终端输入python detect.py --source test.jpg时他皱眉问“这黑框框是啥能不能像微信一样点一下图片就出结果”——这就是工业级落地的真实断层算法能力 ≠ 用户可用性。YOLOv5 提供的是检测能力内核而 PyQt5 承担的是人机交互外壳它把cv2.imshow()的临时窗口封装成带按钮、状态栏、进度条、结果高亮框的独立桌面应用把--source 0的命令行参数转化为“点击【打开摄像头】按钮后自动拉起 USB 摄像头并实时渲染”把results/下散落的exp/*.jpg输出路径映射为界面上可一键保存的【导出检测图】按钮。本项目不涉及模型重训或结构修改核心动作是用torch.hub.load()加载预训练权重用cv2.VideoCapture接入视频流再用 PyQt5 的QGraphicsViewQPixmap实现毫秒级画面更新。适合刚跑通 YOLOv5 demo、但卡在“怎么让非技术人员也用得上”的 Python 开发者以及需要快速交付可执行.exe文件的毕设/政企轻量项目。2. 从 YOLOv5 检测逻辑到 PyQt5 界面线程的解耦设计2.1 为什么不能直接在主线程里调用 model(img)——PyQt5 的事件循环阻塞陷阱PyQt5 是单线程 GUI 框架所有界面刷新按钮响应、绘图更新都依赖QApplication.exec_()启动的主事件循环。若在按钮回调中直接执行results model(img)而 YOLOv5 的前向推理尤其 CPU 模式下耗时 80–200ms界面会卡死鼠标悬停无反馈、按钮按下去不弹起、进度条冻结。常见错误写法def on_detect_clicked(self): img cv2.imread(test.jpg) results self.model(img) # ❌ 阻塞主线程 self.show_result(results)正确做法是将检测任务移出主线程使用QThreadQObject.moveToThread()构建工作线程。关键不是“多线程”而是分离计算与渲染工作线程只负责model(img)和结果打包主线程只负责接收信号并更新 UI 元素。这样即使检测耗时 300ms界面仍能响应取消按钮、切换标签页等操作。2.2 构建可复用的 DetectorWorker 类封装模型加载、预处理与结果解析我们定义一个继承自QObject的工作类它不直接继承QThread这是 PyQt5 官方推荐的现代写法而是通过moveToThread绑定# detector_worker.py import torch import cv2 import numpy as np from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot class DetectorWorker(QObject): # 定义信号检测完成时发射结果和原始图像 detection_finished pyqtSignal(dict, np.ndarray) # {boxes, confs, classes}, original_img detection_error pyqtSignal(str) def __init__(self, weights_pathyolov5s.pt, devicecpu): super().__init__() self.weights_path weights_path self.device device self.model None self._load_model() def _load_model(self): try: # 使用 torch.hub 加载兼容 yolov5 v6.0 和 v7.0 self.model torch.hub.load(ultralytics/yolov5, custom, pathself.weights_path, force_reloadFalse) self.model.to(self.device).eval() except Exception as e: raise RuntimeError(f模型加载失败: {e}) pyqtSlot(np.ndarray) def run_detection(self, img_bgr): 接收 BGR 格式 numpy 图像执行检测并发射结果 try: # YOLOv5 输入要求 RGB且需保持原始尺寸不强制 resize img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) # 推理注意此处 batch1无需额外封装 results self.model(img_rgb) # 解析 results.pandas().xyxy[0] 获取 DataFrame或直接取 .xyxy[0] tensor pred_tensor results.xyxy[0].cpu().numpy() # shape: (N, 6), [x1,y1,x2,y2,conf,class_id] # 过滤置信度 0.5 的口罩检测class_id0 对应 mask需确认你的数据集标签顺序 mask_detections pred_tensor[pred_tensor[:, 4] 0.5] # 构建结果字典便于主线程处理 result_dict { boxes: mask_detections[:, :4], # x1,y1,x2,y2 confs: mask_detections[:, 4], # confidence classes: mask_detections[:, 5].astype(int) # class id } self.detection_finished.emit(result_dict, img_bgr) except Exception as e: self.detection_error.emit(f检测异常: {str(e)})提示pyqtSlot(np.ndarray)装饰器声明该方法可被 Qt 信号安全调用results.xyxy[0]是 YOLOv5 默认输出格式返回归一化坐标若模型导出为 ONNX 或 TorchScript需调整解析逻辑class_id顺序取决于训练时data.yaml中names:字段顺序务必验证口罩是否为索引 0。2.3 主界面中启动工作线程信号-槽连接与资源管理在主窗口类如MaskDetectionApp中创建线程与 worker 实例并建立信号连接# main_window.py from PyQt5.QtCore import QThread from detector_worker import DetectorWorker class MaskDetectionApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(YOLOv5 口罩检测系统) self.init_ui() # 创建工作线程与 worker self.detector_thread QThread() self.detector_worker DetectorWorker(weights_pathweights/best.pt, devicecpu) self.detector_worker.moveToThread(self.detector_thread) # 连接信号 self.detector_thread.started.connect(self.detector_worker.run_detection) self.detector_worker.detection_finished.connect(self.on_detection_finished) self.detector_worker.detection_error.connect(self.on_detection_error) # 启动线程注意此时未触发检测只是准备就绪 self.detector_thread.start() def on_detect_image(self): 点击【图片检测】按钮触发 file_path, _ QFileDialog.getOpenFileName(self, 选择图片, , Image Files (*.jpg *.jpeg *.png)) if not file_path: return try: img_bgr cv2.imread(file_path) if img_bgr is None: raise ValueError(图片读取失败) # 发送图像到工作线程注意必须是 numpy.ndarray不能是路径字符串 self.detector_worker.run_detection(img_bgr) # ✅ 此处触发线程内 run_detection except Exception as e: self.statusBar().showMessage(f加载失败: {e}) def on_detection_finished(self, result_dict, original_img): 工作线程检测完成主线程更新 UI # 在 original_img 上绘制检测框BGR 格式 for i, box in enumerate(result_dict[boxes]): x1, y1, x2, y2 map(int, box) cv2.rectangle(original_img, (x1, y1), (x2, y2), (0, 255, 0), 2) label fMask {result_dict[confs][i]:.2f} cv2.putText(original_img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 转为 QPixmap 显示在 QLabel 上 h, w, ch original_img.shape bytes_per_line ch * w q_img QImage(original_img.data, w, h, bytes_per_line, QImage.Format_RGB888).rgbSwapped() self.result_label.setPixmap(QPixmap.fromImage(q_img).scaled( self.result_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.statusBar().showMessage(f检测完成{len(result_dict[boxes])} 个口罩) def closeEvent(self, event): 退出时清理线程资源 self.detector_thread.quit() self.detector_thread.wait() event.accept()注意self.detector_worker.run_detection(img_bgr)是在主线程中调用但它内部通过pyqtSlot机制被 Qt 自动路由到detector_thread中执行无需手动thread.start()closeEvent中必须显式quit()wait()否则工作线程可能残留导致程序无法退出。3. 实现三类检测模式图片、视频文件、摄像头实时流的统一架构3.1 图片检测最小闭环验证聚焦结果可视化与导出图片检测是最基础模块用于验证模型与界面集成是否正确。核心在于一次加载 → 一次推理 → 一次绘制 → 一次显示。上述on_detect_image已实现此流程。但需补充两个实用功能结果导出用户常需保存带框图用于汇报。添加按钮绑定def on_export_result(self): if not hasattr(self, last_result_img) or self.last_result_img is None: return save_path, _ QFileDialog.getSaveFileName(self, 保存检测结果, detected.jpg, JPEG (*.jpg)) if save_path: cv2.imwrite(save_path, self.last_result_img) # 注意self.last_result_img 应在 on_detection_finished 中缓存 self.statusBar().showMessage(f已保存至 {save_path})批量图片检测支持可选增强若需处理文件夹用QDirIterator遍历但需改用队列机制避免阻塞from queue import Queue # 在 DetectorWorker 中增加 batch_run 方法逐张处理并 emit 多次信号3.2 视频文件检测帧级控制与进度反馈的关键参数视频检测本质是连续图片检测但需解决三个问题① 如何控制播放/暂停/停止② 如何显示当前帧位置与总帧数③ 如何避免因检测慢导致帧堆积即“越检测越卡”。解决方案使用cv2.VideoCapture的CAP_PROP_POS_FRAMES和CAP_PROP_FRAME_COUNT属性并引入帧跳过逻辑def on_detect_video(self): file_path, _ QFileDialog.getOpenFileName(self, 选择视频, , Video Files (*.mp4 *.avi *.mov)) if not file_path: return cap cv2.VideoCapture(file_path) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps cap.get(cv2.CAP_PROP_FPS) or 25.0 # 启动定时器每 40ms约25fps读一帧 self.video_timer QTimer() self.video_timer.timeout.connect(lambda: self.process_video_frame(cap, total_frames)) self.video_timer.start(40) # 与视频原始 fps 解耦保证界面流畅 def process_video_frame(self, cap, total_frames): ret, frame cap.read() if not ret: self.video_timer.stop() cap.release() self.statusBar().showMessage(视频播放结束) return current_frame int(cap.get(cv2.CAP_PROP_POS_FRAMES)) self.progress_bar.setValue(int(current_frame / total_frames * 100)) # 关键仅对关键帧检测如每3帧检测1次避免计算过载 if current_frame % 3 0: self.detector_worker.run_detection(frame.copy()) # copy 防止多线程冲突参数说明current_frame % 3是平衡速度与精度的常用策略frame.copy()避免 OpenCV 内部缓冲区被多线程覆盖QTimer的 40ms 间隔确保 UI 响应而非强求与视频帧率一致。3.3 摄像头实时检测设备枚举、自动适配与低延迟渲染摄像头检测是体验最敏感的模块。用户插入 USB 摄像头后期望“点开即用”而非手动填设备 ID。需实现自动枚举可用摄像头遍历0,1,2,...直到cv2.VideoCapture(i)返回有效对象动态分辨率适配不同摄像头支持不同分辨率优先尝试1280x720失败则降级双缓冲防闪烁使用QGraphicsScene替代QLabel.setPixmap()避免频繁QPixmap创建导致卡顿。def init_camera(self, cam_id0): self.cap cv2.VideoCapture(cam_id) if not self.cap.isOpened(): self.statusBar().showMessage(f摄像头 {cam_id} 打开失败) return False # 尝试设置分辨率部分摄像头支持 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) actual_w self.cap.get(cv2.CAP_PROP_FRAME_WIDTH) actual_h self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) self.statusBar().showMessage(f摄像头已启用分辨率 {int(actual_w)}x{int(actual_h)}) # 初始化 QGraphicsView 场景 self.scene QGraphicsScene() self.graphics_view.setScene(self.scene) # 启动摄像头定时器比视频更短间隔如 33ms ≈ 30fps self.camera_timer QTimer() self.camera_timer.timeout.connect(self.grab_camera_frame) self.camera_timer.start(33) return True def grab_camera_frame(self): ret, frame self.cap.read() if not ret: return # 缩放至界面合适大小如 640x480减少 GPU 渲染压力 display_frame cv2.resize(frame, (640, 480)) # 转为 QImage 并显示此处用 QGraphicsPixmapItem 实现双缓冲 h, w, ch display_frame.shape bytes_per_line ch * w q_img QImage(display_frame.data, w, h, bytes_per_line, QImage.Format_RGB888).rgbSwapped() pixmap QPixmap.fromImage(q_img) # 清空旧项添加新项避免内存泄漏 self.scene.clear() self.scene.addPixmap(pixmap)提示cv2.resize()在 CPU 端完成比在 GPU 上缩放更稳定QGraphicsScene.clear()是防止addPixmap不断叠加导致内存暴涨的关键。4. 模型与界面协同优化YOLOv5 参数调优与 PyQt5 性能加固4.1 YOLOv5 推理阶段必调的 3 个参数影响速度与精度的杠杆YOLOv5 的model()调用默认参数并非最优。在DetectorWorker._load_model()后应显式配置参数推荐值作用适用场景halfTrueTrue仅 CUDA启用 FP16 推理速度提升 1.5–2x显存减半GPU 设备模型已转为 halfdnnFalseFalse默认禁用 OpenCV DNN 后端用原生 PyTorch避免 DNN 后端兼容性问题agnostic_nmsTrueTrue类别无关 NMS合并重叠框口罩/未戴口罩视为同类单一类检测仅口罩# 在 DetectorWorker._load_model() 中修改 self.model torch.hub.load(ultralytics/yolov5, custom, pathself.weights_path, force_reloadFalse) self.model.to(self.device) if self.device ! cpu and hasattr(torch.cuda, is_available) and torch.cuda.is_available(): self.model.half() # ✅ 启用半精度 self.model.eval()注意model.half()必须在model.to(device)之后调用CPU 模式下half()无效会报错故加device ! cpu判断。4.2 PyQt5 界面卡顿的 4 个根因与对应修复代码症状根因修复方案代码示例拖拽窗口卡顿QGraphicsView默认启用 OpenGL强制禁用setViewportUpdateMode(QGraphicsView.FullViewportUpdate)self.graphics_view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)多次点击按钮重复触发未禁用按钮检测开始时button.setEnabled(False)结束时恢复self.detect_btn.setEnabled(False)→self.detect_btn.setEnabled(True)内存持续增长QGraphicsScene未 clear每次addPixmap前scene.clear()self.scene.clear(); self.scene.addPixmap(pixmap)高 DPI 屏幕模糊未启用高 DPI 支持在if __name__ __main__:中添加QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)app QApplication(sys.argv); app.setAttribute(Qt.AA_EnableHighDpiScaling)4.3 将系统打包为独立可执行文件PyInstaller 最小可行配置最终交付物需为.exeWindows或.appmacOS避免用户安装 Python 环境。使用 PyInstaller 时关键参数如下# Windows 打包命令假设主脚本为 main.py pyinstaller --onefile --windowed \ --add-data weights;weights \ # 包含模型文件夹 --add-data images;images \ # 包含示例图片 --hidden-import torch \ --hidden-import cv2 \ --hidden-import PIL \ --name MaskDetector \ main.py--onefile生成单个 exe 文件--windowed隐藏控制台窗口PyQt5 应用必需--add-data将weights/文件夹复制到打包后目录代码中用sys._MEIPASS定位路径import sys import os def resource_path(relative_path): 获取资源文件的绝对路径兼容 PyInstaller 打包 try: base_path sys._MEIPASS except Exception: base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 使用 model_path resource_path(weights/best.pt)验证要点打包后先在无 Python 环境的干净机器上测试重点检查cv2.VideoCapture(0)是否能正常打开摄像头、模型文件是否被正确解压到临时目录。5. 实时检测中的帧率稳定性技巧基于时间戳的动态帧采样策略5.1 为什么固定QTimer间隔会导致实际 FPS 波动QTimer的timeout信号是“尽力而为”当检测耗时超过定时器间隔如设定 33ms但某帧检测耗时 60ms下一帧会立即触发导致帧率飙升但内容重复若连续多帧超时则出现卡顿。根本矛盾在于采集频率硬件≠ 处理频率算法≠ 显示频率UI。5.2 实施基于系统时间戳的自适应采样保证视觉流畅性的硬核方案核心思想不依赖QTimer计时而是每次成功处理完一帧后主动计算下一帧应采集的时间点并用QTimer.singleShot()延迟触发。这样无论检测快慢显示节奏始终稳定。import time class MaskDetectionApp(QMainWindow): def __init__(self): # ... 其他初始化 self.last_capture_time 0 self.target_interval 1.0 / 25.0 # 目标 25 FPS单位秒 def start_camera_adaptive(self): 启动自适应摄像头采集 self.camera_active True self._schedule_next_capture() def _schedule_next_capture(self): if not self.camera_active: return now time.time() next_time self.last_capture_time self.target_interval if now next_time: # 距离下一帧还有时间延迟触发 delay_ms int((next_time - now) * 1000) QTimer.singleShot(delay_ms, self._capture_and_process) else: # 已超时立即处理下一帧不累积延迟 self._capture_and_process() def _capture_and_process(self): ret, frame self.cap.read() if ret: # 执行检测异步不阻塞 self.detector_worker.run_detection(frame.copy()) self.last_capture_time time.time() # 递归调度下一帧 self._schedule_next_capture()效果即使某帧检测耗时 100ms后续帧仍严格按 40ms 间隔显示用户感知为“轻微卡顿”而非“疯狂跳帧”QTimer.singleShot比time.sleep()更符合 Qt 事件循环不会阻塞 UI。5.3 验证帧率稳定性的简易方法在状态栏实时显示 FPS在on_detection_finished中加入计时统计def __init__(self): # ... 其他 self.fps_counter [] self.fps_window 30 # 统计最近30帧 def on_detection_finished(self, result_dict, original_img): # ... 绘制逻辑 self.fps_counter.append(time.time()) if len(self.fps_counter) self.fps_window: self.fps_counter.pop(0) if len(self.fps_counter) 2: fps len(self.fps_counter) / (self.fps_counter[-1] - self.fps_counter[0]) self.statusBar().showMessage(f检测完成{len(result_dict[boxes])} 个口罩 | 实时FPS: {fps:.1f})此方案不依赖硬件 VSync纯软件实现且对 CPU/GPU 负载变化鲁棒。当你看到状态栏FPS: 24.8稳定波动在 ±0.5 范围内说明实时检测模块已达到生产可用水平。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门