基于PySide6的YOLOv8/11目标检测GUI界面——智能安全帽检测系统
📖 前言
在工业安全领域,智能安全帽检测是保障工人生命安全的重要技术手段。本文将介绍如何利用YOLOv8/YOLOv11目标检测算法与PySide6 GUI框架,开发一套功能完整的智能安全帽检测系统。系统支持:
- 动态切换检测模型(YOLOv8n/v8s/v11等)
- 实时调整置信度(Confidence)和IOU阈值
- 支持摄像头、图像、视频三种检测模式
- 实时统计佩戴/未佩戴安全帽人数
- 可视化系统日志及报警记录
通过本文,您将掌握PySide6界面设计与YOLO深度学习模型结合的实战技巧,并了解多线程处理、实时数据可视化等关键技术。
🎥 效果展示
检测结果如下图
🔧 实现步骤与核心代码
首先,需要安装PySide6库,可以使用以下命令进行安装:
pip install pyside6
接下来,可以创建一个Python脚本
import sys
import os
import time
import cv2
import numpy as np
from threading import Thread, Lock
from queue import Queue
from PySide6 import QtWidgets, QtCore, QtGui
from PIL import Image
from ultralytics import YOLO# 常量定义
DEFAULT_MODEL_PATH = 'weights/v11/helmet_v11_shuffleNetV2_CCFM_Bifformer.engine'
SETTINGS_FILE = "app_settings.ini"class MWindow(QtWidgets.QMainWindow):def __init__(self):super().__init__()self._load_settings()self._setup_ui()self._setup_vars()self._setup_connections()self._apply_styles()self._update_controls_state(False)# 初始化YOLO模型self.model_lock = Lock()self.load_model(self.model_path)# 启动处理线程Thread(target=self.frame_analyze_thread, daemon=True).start()def _load_settings(self):"""加载应用设置"""self.settings = QtCore.QSettings(SETTINGS_FILE, QtCore.QSettings.Format.IniFormat)self.model_path = self.settings.value("model/path", DEFAULT_MODEL_PATH)self.conf_threshold = float(self.settings.value("model/conf", 0.45))self.iou_threshold = float(self.settings.value("model/iou", 0.5))geometry = self.settings.value("window/geometry")if geometry:try:if isinstance(geometry, str):geometry = QtCore.QByteArray.fromHex(geometry.encode())if isinstance(geometry, QtCore.QByteArray):self.restoreGeometry(geometry)except Exception:passdef _save_settings(self):"""保存应用设置"""self.settings.setValue("model/path", self.model_path)self.settings.setValue("model/conf", self.conf_threshold)self.settings.setValue("model/iou", self.iou_threshold)geometry_bytes = self.saveGeometry()self.settings.setValue("window/geometry", geometry_bytes.toHex().data().decode())self.settings.sync()def _setup_ui(self):"""设置用户界面"""self.setWindowTitle('智能安全帽检测系统')self.setWindowIcon(QtGui.QIcon("helmet_icon.png"))central_widget = QtWidgets.QWidget()self.setCentralWidget(central_widget)main_layout = QtWidgets.QVBoxLayout(central_widget)main_layout.setContentsMargins(10, 10, 10, 10)main_layout.setSpacing(10)# 视频显示区域video_container = QtWidgets.QWidget()video_layout = QtWidgets.QHBoxLayout(video_container)video_layout.setContentsMargins(0, 0, 0, 0)video_layout.setSpacing(10)self.video_label_ori = self._create_video_label("实时视频流")self.video_label_proc = self._create_video_label("分析结果")video_layout.addWidget(self.video_label_ori, 1)video_layout.addWidget(self.video_label_proc, 1)# 控制面板control_panel = QtWidgets.QFrame()control_panel.setObjectName("controlPanel")control_panel.setFixedWidth(350)control_layout = QtWidgets.QVBoxLayout(control_panel)control_layout.setContentsMargins(10, 10, 10, 10)control_layout.setSpacing(15)# 模型设置组model_group = QtWidgets.QGroupBox("模型设置")model_layout = QtWidgets.QFormLayout(model_group)model_layout.setSpacing(10)# 模型路径选择self.model_path_label = QtWidgets.QLabel(os.path.basename(self.model_path))self.model_path_label.setToolTip(self.model_path)self.model_path_label.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse |QtCore.Qt.TextInteractionFlag.TextSelectableByKeyboard)self.model_select_btn = QtWidgets.QPushButton("模型")model_layout.addRow(self.model_select_btn, self.model_path_label)# 置信度滑动条self.conf_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)self.conf_slider.setRange(0, 100)self.conf_slider.setValue(int(self.conf_threshold * 100))self.conf_value_label = QtWidgets.QLabel(f"{self.conf_threshold:.2f}")conf_layout = QtWidgets.QHBoxLayout()conf_layout.addWidget(self.conf_slider)conf_layout.addWidget(self.conf_value_label)model_layout.addRow("置信度:", conf_layout)# IOU滑动条self.iou_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)self.iou_slider.setRange(0, 100)self.iou_slider.setValue(int(self.iou_threshold * 100))self.iou_value_label = QtWidgets.QLabel(f"{self.iou_threshold:.2f}")iou_layout = QtWidgets.QHBoxLayout()iou_layout.addWidget(self.iou_slider)iou_layout.addWidget(self.iou_value_label)model_layout.addRow("IOU阈值:", iou_layout)# 统计信息组stats_group = QtWidgets.QGroupBox("实时统计")stats_layout = QtWidgets.QFormLayout(stats_group)stats_layout.setSpacing(10)self.total_count_label = QtWidgets.QLabel("0")self.helmet_count_label = QtWidgets.QLabel("0")self.no_helmet_label = QtWidgets.QLabel("0")stats_layout.addRow("总人数:", self.total_count_label)stats_layout.addRow("佩戴安全帽:", self.helmet_count_label)stats_layout.addRow("未佩戴安全帽:", self.no_helmet_label)# 控制按钮组btn_group = QtWidgets.QGroupBox("设备控制")btn_layout = QtWidgets.QGridLayout(btn_group)btn_layout.setSpacing(10)self.cam_btn = self._create_tool_button("摄像头", "camera-video")self.video_btn = self._create_tool_button("图片/视频", "document-open")self.snapshot_btn = self._create_tool_button("截图", "camera-photo")self.stop_btn = self._create_tool_button("停止", "media-playback-stop")btn_layout.addWidget(self.cam_btn, 0, 0)btn_layout.addWidget(self.video_btn, 0, 1)btn_layout.addWidget(self.snapshot_btn, 1, 0)btn_layout.addWidget(self.stop_btn, 1, 1)# 日志区域log_group = QtWidgets.QGroupBox("系统日志")log_layout = QtWidgets.QVBoxLayout(log_group)self.log_text = QtWidgets.QTextBrowser()self.log_text.setObjectName("logText")self.log_text.setMaximumHeight(250)log_layout.addWidget(self.log_text)control_layout.addWidget(model_group)control_layout.addWidget(stats_group)control_layout.addWidget(btn_group)control_layout.addWidget(log_group)control_layout.addStretch(1)main_layout.addWidget(video_container, 3)main_layout.addWidget(control_panel, 1)self.status_bar_label = QtWidgets.QLabel("就绪")self.statusBar().addPermanentWidget(self.status_bar_label)self.statusBar().setObjectName("statusBar")# 添加标题标签title_label = QtWidgets.QLabel("智能安全帽检测系统")title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)title_label.setStyleSheet("""QLabel {font-size: 30pt;font-weight: bold;color: #66B2FF;padding: 15px 0;border-bottom: 2px solid #555555;}""")main_layout.addWidget(title_label)# 添加水平容器放置视频和控制面板content_widget = QtWidgets.QWidget()content_layout = QtWidgets.QHBoxLayout(content_widget)content_layout.setContentsMargins(0, 0, 0, 0)content_layout.addWidget(video_container, 3)content_layout.addWidget(control_panel, 1)main_layout.addWidget(content_widget)def _setup_vars(self):"""初始化变量"""self.cap = Noneself.frame_queue = Queue(maxsize=3)self.is_processing = Falseself.fps = 0self.frame_count = 0self.start_time = time.time()self.stats = {'total': 0, 'helmet': 0, 'no_helmet': 0}self.last_processed_frame = Noneself.model = Noneself._model_ready = False # 添加这一行# 定时器self.timer = QtCore.QTimer()self.timer.setInterval(30) # 约33fpsself.current_image = None # 新增:保存当前打开的原始图片self.is_image_mode = False # 新增:标记当前是否为图片模式def _setup_connections(self):"""连接信号与槽"""self.cam_btn.clicked.connect(self.start_camera)self.video_btn.clicked.connect(self.open_image_or_video_file)self.snapshot_btn.clicked.connect(self.take_snapshot)self.stop_btn.clicked.connect(self.stop)self.timer.timeout.connect(self.update_frame)self.model_select_btn.clicked.connect(self._select_model)self.conf_slider.valueChanged.connect(self.update_conf_threshold)self.iou_slider.valueChanged.connect(self.update_iou_threshold)def _create_video_label(self, placeholder_text):"""创建视频显示标签"""label = QtWidgets.QLabel(placeholder_text)label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)label.setMinimumSize(480, 360)label.setObjectName("videoLabel")label.setProperty("placeholderText", placeholder_text)return labeldef _create_tool_button(self, text, icon_name):"""创建工具按钮"""btn = QtWidgets.QPushButton(text)icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon())if not icon.isNull():btn.setIcon(icon)btn.setIconSize(QtCore.QSize(20, 20))btn.setObjectName("toolButton")return btndef _apply_styles(self):"""应用样式表"""style_sheet = """QMainWindow { background-color: #2D2D2D; }QWidget#controlPanel { background-color: #3C3C3C; border-radius: 8px; }QGroupBox { color: #E0E0E0; font-weight: bold; border: 1px solid #555555; border-radius: 6px; margin-top: 10px; padding: 15px 5px 5px 5px; }QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 10px; padding: 0 5px; color: #66B2FF; }QLabel { color: #D0D0D0; padding: 2px; }QLabel#videoLabel { background-color: #1A1A1A; border: 1px solid #444444; border-radius: 8px; color: #777777; font-size: 16pt; font-weight: bold; }QPushButton#toolButton { background-color: #555555; color: white; border: none; padding: 10px; border-radius: 5px; min-height: 30px; text-align: center; }QPushButton#toolButton:hover { background-color: #6A6A6A; }QPushButton#toolButton:pressed { background-color: #4A4A4A; }QPushButton#toolButton:disabled { background-color: #404040; color: #888888; }QPushButton { background-color: #007ACC; color: white; border: none; padding: 8px 15px; border-radius: 4px; }QPushButton:hover { background-color: #0090F0; }QPushButton:pressed { background-color: #0060A0; }QTextBrowser#logText { background-color: #252525; color: #C0C0C0; border: 1px solid #444444; border-radius: 4px; font-family: Consolas, Courier New, monospace; }QStatusBar { background-color: #252525; color: #AAAAAA; }QStatusBar::item { border: none; }QToolTip { background-color: #FFFFE1; color: black; border: 1px solid #AAAAAA; padding: 4px; border-radius: 3px; }"""self.setStyleSheet(style_sheet)def load_model(self, model_path):"""加载YOLO模型"""try:with self.model_lock:self.model = YOLO(model_path)self.log(f"模型加载成功: {os.path.basename(model_path)}", "success")self._model_ready = Trueself._update_controls_state(False)except Exception as e:self.log(f"模型加载失败: {str(e)}", "error")self._model_ready = Falseself._update_controls_state(False, force_disable_sources=True)def start_camera(self):"""启动摄像头"""if not self.cap:self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)if self.cap.isOpened():self.timer.start()self.is_processing = Trueself.log("摄像头已启动")self._update_controls_state(True)self.start_time = time.time()self.frame_count = 0else:self.log("无法打开摄像头", "error")def open_image_or_video_file(self):"""打开图片或视频文件"""path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开图片或视频文件", "","视频文件 (*.mp4 *.avi *.mov *.mkv);;图片文件 (*.jpg *.jpeg *.png *.bmp);;所有文件 (*.*)")if path:file_extension = os.path.splitext(path)[1].lower()if file_extension in ['.mp4', '.avi', '.mov', '.mkv']: # 视频文件self.cap = cv2.VideoCapture(path)if self.cap.isOpened():self.timer.start()self.is_processing = Trueself.log(f"已打开视频文件: {path}")self._update_controls_state(True)self.start_time = time.time()self.frame_count = 0else:self.log("无法打开视频文件", "error")elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp']: # 图片文件image = cv2.imread(path)if image is not None:self._process_image(image)self.log(f"已加载图片文件: {path}")else:self.log("无法打开图片文件", "error")else:self.log("不支持的文件格式", "warning")def update_frame(self):"""更新视频帧"""ret, frame = self.cap.read()if not ret:self.stop()return# 计算FPSself.frame_count += 1if self.frame_count % 10 == 0:self.fps = 10 / (time.time() - self.start_time)self.start_time = time.time()h, w = frame.shape[:2]self.status_bar_label.setText(f"运行中 | FPS: {self.fps:.1f} | 分辨率: {w}x{h}")# 显示原始帧self._display_frame(frame, self.video_label_ori, "Original")# 添加到处理队列if self.frame_queue.qsize() < 3:self.frame_queue.put(frame.copy())def _display_frame(self, frame, label, label_name="Unknown"):"""显示帧到标签"""try:if frame.ndim == 3 and frame.shape[2] == 3:rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)q_image_format = QtGui.QImage.Format.Format_RGB888bytes_per_line = 3 * frame.shape[1]elif frame.ndim == 2:rgb_frame = frameq_image_format = QtGui.QImage.Format.Format_Grayscale8bytes_per_line = frame.shape[1]else:returnh, w = rgb_frame.shape[:2]if not rgb_frame.flags['C_CONTIGUOUS']:rgb_frame = np.ascontiguousarray(rgb_frame)qimg = QtGui.QImage(rgb_frame.data, w, h, bytes_per_line, q_image_format)label.setProperty("image_ref", qimg)pixmap = QtGui.QPixmap.fromImage(qimg)label_size = label.size()if label_size.width() > 0 and label_size.height() > 0:scaled_pixmap = pixmap.scaled(label_size, QtCore.Qt.AspectRatioMode.KeepAspectRatio,QtCore.Qt.TransformationMode.SmoothTransformation)label.setPixmap(scaled_pixmap)else:label.setPixmap(pixmap)except Exception as e:label.setText(f"显示错误 ({label_name})")def frame_analyze_thread(self):"""帧分析线程"""while True:if not self.frame_queue.empty():frame = self.frame_queue.get()# 如果是图片,直接处理if self.is_image_mode:# 使用模型推理(带当前参数)with self.model_lock:results = self.model.predict(frame,conf=self.conf_threshold,iou=self.iou_threshold,verbose=False)[0]# 绘制结果proc_frame = results.plot(line_width=2, labels=True)self.last_processed_frame = proc_frame# 更新统计信息self.update_stats(results)# 显示处理结果self._display_frame(proc_frame, self.video_label_proc, "Processed")# 跳过视频处理逻辑continue# 使用模型推理(带参数)with self.model_lock:results = self.model.predict(frame,conf=self.conf_threshold,iou=self.iou_threshold,verbose=False)[0]# 更新统计信息self.update_stats(results)# 绘制结果proc_frame = results.plot(line_width=2, labels=True)self.last_processed_frame = proc_frameself._display_frame(proc_frame, self.video_label_proc, "Processed")# 限制处理频率time.sleep(0.01)def _process_image(self, image):"""处理图片并显示结果"""self.is_image_mode = True # 标记为图片模式self.current_image = image.copy() # 保存原始图片副本self.is_processing = Trueself._update_controls_state(True)# 使用模型推理(带参数)with self.model_lock:results = self.model.predict(image,conf=self.conf_threshold,iou=self.iou_threshold,verbose=False)[0]# 更新统计信息self.update_stats(results)# 绘制结果proc_image = results.plot(line_width=2, labels=True)self._display_frame(image, self.video_label_ori, "Original")self.last_processed_frame = proc_imageself._display_frame(proc_image, self.video_label_proc, "Processed")def _select_model(self):"""选择模型文件"""if self.is_processing:QtWidgets.QMessageBox.warning(self, "警告", "请先停止当前处理,再选择新模型。")returnfilter_str = "YOLO 模型 (*.pt *.engine *.onnx);;所有文件 (*.*)"start_dir = os.path.dirname(self.model_path) if os.path.dirname(self.model_path) else ""path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选择模型文件", start_dir, filter_str)if path and path != self.model_path:self.model_path = pathself.model_path_label.setText(os.path.basename(path))self.model_path_label.setToolTip(path)self.log(f"选择新模型: {path}", "info")self.load_model(path)self._save_settings() # 保存新模型路径def update_stats(self, results):"""更新统计信息"""helmet_count = 0no_helmet_count = 0for box in results.boxes:cls = int(box.cls)if cls == 1:helmet_count += 1elif cls == 0:no_helmet_count += 1self.stats = {'total': helmet_count + no_helmet_count,'helmet': helmet_count,'no_helmet': no_helmet_count}# 更新界面self.total_count_label.setText(str(self.stats['total']))self.helmet_count_label.setText(str(self.stats['helmet']))self.no_helmet_label.setText(str(self.stats['no_helmet']))def update_conf_threshold(self, value):"""更新置信度阈值"""self.conf_threshold = value / 100.0self.conf_value_label.setText(f"{self.conf_threshold:.2f}")self.log(f"置信度阈值更新为: {self.conf_threshold:.2f}")self._trigger_async_reprocess() # 新增:触发异步处理def update_iou_threshold(self, value):"""更新IOU阈值"""self.iou_threshold = value / 100.0self.iou_value_label.setText(f"{self.iou_threshold:.2f}")self.log(f"IOU阈值更新为: {self.iou_threshold:.2f}")self._trigger_async_reprocess() # 新增:触发异步处理def _trigger_async_reprocess(self):"""触发异步重新处理(线程安全)"""if self.is_image_mode and self.current_image is not None:# 清空队列确保只处理最新图片with self.frame_queue.mutex:self.frame_queue.queue.clear()# 将原始图片放入处理队列self.frame_queue.put(self.current_image.copy())def take_snapshot(self):"""截图"""if self.last_processed_frame is not None:timestamp = time.strftime("%Y%m%d_%H%M%S")suggested_filename = f"snapshot_{timestamp}.png"path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "保存截图", suggested_filename, "PNG图像 (*.png);;JPEG图像 (*.jpg *.jpeg)")if path:try:save_frame = cv2.cvtColor(self.last_processed_frame, cv2.COLOR_RGB2BGR)success = Image.fromarray(save_frame)success.save(path)if success:self.log(f"截图已保存: {path}", "success")else:self.log(f"截图保存失败: {path}", "error")except Exception as e:self.log(f"保存截图时出错: {str(e)}", "error")def log(self, message, level="info"):"""记录日志"""color_map = {"info": "#A0A0FF","success": "#77FF77","warning": "#FFFF77","error": "#FF7777"}color = color_map.get(level, "#E0E0E0")timestamp = time.strftime('%H:%M:%S')formatted_message = f'<span style="color: #888;">[{timestamp}]</span> <span style="color:{color};">{message}</span>'self.log_text.append(formatted_message)self.log_text.moveCursor(QtGui.QTextCursor.MoveOperation.End)def _update_controls_state(self, is_running, force_disable_sources=False):"""更新控件状态"""disable_start_buttons = is_running or not self._model_ready or force_disable_sourcesself.cam_btn.setDisabled(disable_start_buttons)self.video_btn.setDisabled(disable_start_buttons)self.model_select_btn.setDisabled(is_running)self.stop_btn.setEnabled(is_running)self.snapshot_btn.setEnabled(is_running and self._model_ready)def stop(self):"""停止处理"""self.is_image_mode = False # 新增:重置图片模式标记self.current_image = None # 新增:清空缓存图片self.timer.stop()self.is_processing = Falseif self.cap:self.cap.release()self.cap = Noneself.frame_queue.queue.clear()self.video_label_ori.setText(self.video_label_ori.property("placeholderText"))self.video_label_proc.setText(self.video_label_proc.property("placeholderText"))self.last_processed_frame = Noneself._update_controls_state(False)self.status_bar_label.setText("已停止")self.log("系统已停止")def _select_model(self):"""选择模型文件"""if self.is_processing:QtWidgets.QMessageBox.warning(self, "警告", "请先停止当前处理,再选择新模型。")returnfilter_str = "YOLO 模型 (*.pt *.engine *.onnx);;所有文件 (*.*)"start_dir = os.path.dirname(self.model_path) if os.path.dirname(self.model_path) else ""path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选择模型文件", start_dir, filter_str)if path and path != self.model_path:self.model_path = pathself.model_path_label.setText(os.path.basename(path))self.model_path_label.setToolTip(path)self.log(f"选择新模型: {path}", "info")self.load_model(path)def closeEvent(self, event):"""关闭事件处理"""self.stop()self._save_settings()event.accept()if __name__ == "__main__":if hasattr(QtCore.Qt, 'AA_EnableHighDpiScaling'):QtWidgets.QApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):QtWidgets.QApplication.setAttribute(QtCore.Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)app = QtWidgets.QApplication(sys.argv)# 创建占位图标(如果不存在)if not os.path.exists("helmet_icon.png"):img = QtGui.QImage(64, 64, QtGui.QImage.Format.Format_ARGB32)img.fill(QtGui.QColor("blue"))painter = QtGui.QPainter(img)painter.setPen(QtGui.QColor("white"))painter.setFont(QtGui.QFont("Arial", 20))painter.drawText(img.rect(), QtCore.Qt.AlignmentFlag.AlignCenter, "H")painter.end()img.save("helmet_icon.png")# 创建模型目录(如果不存在)weights_dir = os.path.dirname(DEFAULT_MODEL_PATH)if weights_dir and not os.path.exists(weights_dir):os.makedirs(weights_dir)window = MWindow()window.show()sys.exit(app.exec())