【Python+Qt】自定义QDockWidget窗口弹出时最大化功能

该文章已生成可运行项目,

项目场景:

使用 PySide6(PyQt 应该类似) 开发 GUI 界面,主界面分为很多个板块(使用 QDockWidget),想实现每个 QDockWidget 在 floating 弹出时,默认最大化 floating 窗口,便于用户操作。

但是 QDockWidget 默认只有 close、float 两个按钮,且单击 float 按钮或双击标题栏,QDockWidget 只会简单地弹出,标题栏按钮均消失不见,且无法最大化。

希望增加 QDockWidget 功能,实现 floating 弹出后,QDockwidget 标题栏有最大化、最小化按钮,且支持自动最大化窗口。


解决方案(初)

参考博客 Qt实战10.支持最小化和最大化的QDockWidget,主题思路为创建一个新类继承 QDockWidget 并重写方法,即

def event(self, event: QEvent):
    if event.type() == QEvent.ZOrderChange:
        if self.isFloating():
            w = QWidget(self)
            self.setMaximumSize(w.maximumSize())
            self.setWindowFlags(Qt.Dialog | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint)
            self.show()
    return super(DockWidget, self).event(event)

为了防止用户手残将 QDockWidget 窗口关掉没法恢复,self.setWindowFlags 不添加 Qt.WindowCloseButtonHint,取消关闭按钮功能。

但该方法仍然存在不足:

  1. 若要恢复窗口,只能双击标题栏,点击最小化按钮会使 QDockWidget 收起。希望点击最小化后实现恢复窗口功能。
  2. 窗口 floating 弹出时,只能由用户手动点击最大化按钮实现窗口最大化,无法自动实现最大化功能。

解决方案(终)

经过一顿修改,完整代码如下:

from PySide6.QtCore import Qt, QEvent
from PySide6.QtWidgets import QWidget, QDockWidget


class DockWidget(QDockWidget):
    def __init__(self, parent=None, need_maximize=False):
        super(DockWidget, self).__init__(parent)
        self.window_flags = self.windowFlags()
        self.maximum_size = self.maximumSize()
        self.need_maximize = need_maximize
        self.show_flag = False

    def event(self, event: QEvent):
        if event.type() == QEvent.ZOrderChange:
            if self.isFloating():
                w = QWidget(self)
                self.setMaximumSize(w.maximumSize())
                self.setWindowFlags(Qt.Dialog | Qt.WindowMinimizeButtonHint)
                self.show()
                self.show_flag = True
        return super(DockWidget, self).event(event)

    def changeEvent(self, event: QEvent):
        if event.type() == QEvent.WindowStateChange:
            if self.isMinimized():
                self.setFloating(False)
                self.setMaximumSize(self.maximum_size)
                self.setWindowFlags(self.window_flags)
                self.setWindowState(Qt.WindowNoState)
                self.show()
        if self.need_maximize and (self.windowState() & Qt.WindowNoState == Qt.WindowNoState) and self.show_flag:
            self.showMaximized()
            self.show_flag = False
        return super(DockWidget, self).changeEvent(event)


class DockMaximizeWidget(DockWidget):
    def __init__(self, parent=None):
        super(DockMaximizeWidget, self).__init__(parent, need_maximize=True)

逐段代码分析

  1. 最小化按钮功能更改:changeEvent

    def changeEvent(self, event: QEvent):
            if event.type() == QEvent.WindowStateChange:
                if self.isMinimized():
                    self.setFloating(False)
                    self.setMaximumSize(self.maximum_size)
                    self.setWindowFlags(self.window_flags)
                    self.setWindowState(Qt.WindowNoState)
                    self.show()
            ...
    
    • 使用 event.type() == QEvent.WindowStateChangeself.isMinimized() 检测窗口是否被最小化;
    • self.setFloating(False) 取消窗口浮动状态;
    • self.setMaximumSize(self.maximum_size) self.setWindowFlags(self.window_flags) 用于恢复窗口默认属性;
    • self.setWindowState(Qt.WindowNoState) 将 WindowState 状态清空,防止后续检测不到最小化行为;
    • self.show() 显示 floating 窗口。

    上述代码用于更改最小化按钮的行为,实现 docker 窗口恢复。


    ...
    if self.need_maximize and (self.windowState() & Qt.WindowNoState == Qt.WindowNoState) and self.show_flag:
        self.showMaximized()
        self.show_flag = False
        return super(DockWidget, self).changeEvent(event)
    ...
    
    • self.need_maximize 控制窗口是否要默认最大化;
    • self.windowState() & Qt.WindowNoState == Qt.WindowNoState 确保窗口处于默认状态;
    • self.show_flag 记录窗口的最大化行为;
    • self.showMaximized() 最大化窗口。

    重要:不能在 event 事件中执行 self.showMaximized(),否则最大化无效。

    上述代码效果为,当 docker 窗口从 non-floating 状态变成 floating 弹出状态时,自动最大化窗口。


    ...
    return super(DockWidget, self).changeEvent(event)
    

    一定要调用super(DockWidget, self).changeEvent(event),否则会出现 docker 标题消失等迷之 bug。

  2. 自定义弹出窗口:event

    def event(self, event: QEvent):
       if event.type() == QEvent.ZOrderChange:
           if self.isFloating():
               w = QWidget(self)
               self.setMaximumSize(w.maximumSize())
               self.setWindowFlags(Qt.Dialog | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint)
               self.show()
               self.show_flag = True
       return super(DockWidget, self).event(event)
    
    • 增加 self.show_flag = True 记录窗口从 non-floating 状态变成 floating 弹出状态;
    • 其余与前文博客内容相同。
  3. 类初始化:__init__

    def __init__(self, parent=None, need_maximize=False):
        super(DockWidget, self).__init__(parent)
        self.window_flags = self.windowFlags()
        self.maximum_size = self.maximumSize()
        self.need_maximize = need_maximize
        self.show_flag = False
    
    • need_maximize 设置弹出时窗口是否默认最大化;
    • window_flags maximum_size 记录窗口默认状态;
    • self.show_flag = False 清空窗口弹出记录。
  4. 再包装一下

    class DockMaximizeWidget(DockWidget):
        def __init__(self, parent=None):
            super(DockMaximizeWidget, self).__init__(parent, need_maximize=True)
    

    DockWidget 支持最大、最小化按钮,DockMaximizeWidget 窗口弹出时默认最大化,在 QtDesigner 中可以直接对 QDockWidget 进行类提升,使用方便。


站在巨人的肩膀上+自己动手丰衣足食,完美解决问题,mark 一下。图懒得放了。

END

本文章已经生成可运行项目
# -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np import json import time import logging import platform from datetime import datetime from scipy import ndimage from scipy.spatial import distance # PyQt5 导入 from PyQt5.QtWidgets import ( QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBar, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog, QCheckBox # 添加 QCheckBox ) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap # 可能需要这些用于图像显示 # 海康 SDK 导入 sys.path.append("D:\\海康\\MVS\\Development\\Samples\\Python\\BasicDemo") from MvCameraControl_class import * from MvErrorDefine_const import * from CameraParams_header import * # 自定义模块导入 from CamOperation_class import CameraOperation from PyUICBasicDemo import Ui_MainWindow # 如果使用 UI 文件生成 # 图像处理库导入 try: from skimage.feature import local_binary_pattern from skimage import exposure except ImportError: # 提供替代方案或错误处理 pass # 配置日志系统 logging.basicConfig( level=logging.DEBUG, # 设置为DEBUG级别获取更多信息 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("cloth_inspection_debug.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统启动 - 增强版") # 全局变量 current_sample_path = "" # 当前使用的样本路径 detection_history = [] # 检测历史记录 sample_features = {} # 存储样本特征数据 config_file = "detection_config.json" # 配置文件路径 # 加载配置文件 def load_config(): global config_file if os.path.exists(config_file): try: with open(config_file, 'r') as f: return json.load(f) except Exception as e: logging.error(f"加载配置文件失败: {str(e)}") return {} return {} # 保存配置文件 def save_config(config): global config_file try: with open(config_file, 'w') as f: json.dump(config, f, indent=4) logging.info("配置文件已保存") except Exception as e: logging.error(f"保存配置文件失败: {str(e)}") # 初始化配置 default_config = { "threshold": 0.05, "min_defect_area": 50, "max_defect_area": 5000, "texture_analysis": True, "color_sensitivity": 0.8, "edge_detection": True, "adaptive_threshold": True, "multi_scale_levels": 3, "feature_extraction": "histogram", # histogram, lbp, sift, orb "defect_classification": True } app_config = {**default_config, **load_config()} # 帧监控线程 class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True def run(self): while self.running: if self.cam_operation: status = self.cam_operation.get_frame_status() frame_text = "有帧" if status.get('current_frame', False) else "无帧" self.frame_status.emit(f"帧状态: {frame_text}") QThread.msleep(500) def stop(self): self.running = False # ======================== 增强版布料印花检测算法 ======================== def extract_features(image): """ 提取图像的多尺度特征 :param image: 输入图像 (灰度或彩色) :return: 特征字典 """ features = {} # 确保图像是灰度图 if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image # 1. 直方图特征 hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) hist = cv2.normalize(hist, hist).flatten() features['histogram'] = hist.tolist() # 2. LBP纹理特征 (局部二值模式) radius = 3 n_points = 8 * radius lbp = local_binary_pattern(gray, n_points, radius, method='uniform') lbp_hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) lbp_hist = lbp_hist.astype("float") lbp_hist /= (lbp_hist.sum() + 1e-7) # 归一化 features['lbp'] = lbp_hist.tolist() # 3. 颜色特征 (如果是彩色图像) if len(image.shape) == 3: # 在HSV空间中计算颜色直方图 hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) h_hist = cv2.calcHist([hsv], [0], None, [180], [0, 180]) s_hist = cv2.calcHist([hsv], [1], None, [256], [0, 256]) v_hist = cv2.calcHist([hsv], [2], None, [256], [0, 256]) h_hist = cv2.normalize(h_hist, h_hist).flatten() s_hist = cv2.normalize(s_hist, s_hist).flatten() v_hist = cv2.normalize(v_hist, v_hist).flatten() features['h_hist'] = h_hist.tolist() features['s_hist'] = s_hist.tolist() features['v_hist'] = v_hist.tolist() # 4. 边缘特征 edges = cv2.Canny(gray, 100, 200) edge_density = np.sum(edges > 0) / (edges.shape[0] * edges.shape[1]) features['edge_density'] = edge_density # 5. 多尺度分析 scales = [1.0, 0.5, 0.25] scale_features = [] for scale in scales: scaled = cv2.resize(gray, None, fx=scale, fy=scale) # 计算小波变换 (近似) coeffs = cv2.dct(np.float32(scaled)/255.0) coeffs_flat = coeffs[:10, :10].flatten() scale_features.extend(coeffs_flat.tolist()) features['multi_scale'] = scale_features return features def compute_feature_distance(features1, features2): """ 计算两个特征集之间的距离 :param features1: 特征集1 :param features2: 特征集2 :return: 距离分数 (0-1, 0表示完全相同) """ # 直方图距离 (卡方距离) hist_dist = distance.chisquare(features1['histogram'], features2['histogram'])[0] / 1000 # LBP距离 (巴氏距离) lbp_dist = distance.bhattacharyya(features1['lbp'], features2['lbp']) # 颜色距离 (如果存在) color_dist = 0 if 'h_hist' in features1 and 'h_hist' in features2: h_dist = distance.chisquare(features1['h_hist'], features2['h_hist'])[0] / 1000 s_dist = distance.chisquare(features1['s_hist'], features2['s_hist'])[0] / 1000 v_dist = distance.chisquare(features1['v_hist'], features2['v_hist'])[0] / 1000 color_dist = (h_dist + s_dist + v_dist) / 3 # 边缘密度差异 edge_dist = abs(features1['edge_density'] - features2['edge_density']) # 多尺度特征差异 (欧氏距离) scale_dist = distance.euclidean(features1['multi_scale'], features2['multi_scale']) / 100 # 加权组合距离 weights = { 'hist': 0.3, 'lbp': 0.3, 'color': 0.2, 'edge': 0.1, 'scale': 0.1 } total_dist = ( weights['hist'] * hist_dist + weights['lbp'] * lbp_dist + weights['color'] * color_dist + weights['edge'] * edge_dist + weights['scale'] * scale_dist ) # 归一化到0-1范围 total_dist = min(1.0, max(0.0, total_dist)) return total_dist def adaptive_threshold_diff(sample_gray, test_gray): """ 自适应阈值差异检测 :param sample_gray: 样本灰度图像 :param test_gray: 测试灰度图像 :return: 二值差异图像 """ # 计算绝对差异 diff = cv2.absdiff(sample_gray, test_gray) # 自适应阈值 block_size = max(11, min(sample_gray.shape[0] // 20, 51)) adaptive_thresh = cv2.adaptiveThreshold( diff, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block_size, 5 ) # 形态学操作增强缺陷区域 kernel = np.ones((3, 3), np.uint8) enhanced = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_CLOSE, kernel) enhanced = cv2.morphologyEx(enhanced, cv2.MORPH_OPEN, kernel) return enhanced def detect_defects(sample_gray, test_gray, diff_map): """ 检测并分类缺陷 :param sample_gray: 样本灰度图像 :param test_gray: 测试灰度图像 :param diff_map: 差异图 :return: 缺陷列表, 标记图像 """ # 查找连通区域 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(diff_map, connectivity=8) defects = [] marked_image = cv2.cvtColor(test_gray, cv2.COLOR_GRAY2BGR) # 最小和最大缺陷面积 min_area = app_config.get("min_defect_area", 50) max_area = app_config.get("max_defect_area", 5000) for i in range(1, num_labels): # 跳过背景 area = stats[i, cv2.CC_STAT_AREA] # 过滤太小或太大的区域 if area < min_area or area > max_area: continue # 获取缺陷区域 x, y, w, h = stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] defect_roi = diff_map[y:y+h, x:x+w] # 计算缺陷特征 sample_roi = sample_gray[y:y+h, x:x+w] test_roi = test_gray[y:y+h, x:x+w] # 缺陷类型分类 defect_type = classify_defect(sample_roi, test_roi, defect_roi) # 保存缺陷信息 defects.append({ "type": defect_type, "area": area, "location": (x, y, w, h), "centroid": (int(centroids[i][0]), int(centroids[i][1])) }) # 在标记图像上绘制 color = (0, 0, 255) # 默认红色 if defect_type == "color": color = (0, 165, 255) # 橙色 elif defect_type == "texture": color = (0, 255, 255) # 黄色 elif defect_type == "missing": color = (255, 0, 0) # 蓝色 elif defect_type == "stain": color = (0, 255, 0) # 绿色 cv2.rectangle(marked_image, (x, y), (x+w, y+h), color, 2) cv2.putText(marked_image, f"{defect_type}", (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) return defects, marked_image def classify_defect(sample_roi, test_roi, defect_mask): """ 分类缺陷类型 :param sample_roi: 样本区域 :param test_roi: 测试区域 :param defect_mask: 缺陷掩码 :return: 缺陷类型 (color, texture, missing, stain) """ # 计算颜色差异 color_diff = np.mean(np.abs(sample_roi.astype(np.float32) - test_roi.astype(np.float32))) # 计算纹理差异 (使用LBP) radius = 2 n_points = 8 * radius lbp_sample = local_binary_pattern(sample_roi, n_points, radius, method='uniform') lbp_test = local_binary_pattern(test_roi, n_points, radius, method='uniform') hist_sample, _ = np.histogram(lbp_sample.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) hist_test, _ = np.histogram(lbp_test.ravel(), bins=np.arange(0, n_points + 3), range=(0, n_points + 2)) hist_sample = hist_sample.astype("float") hist_test = hist_test.astype("float") hist_sample /= (hist_sample.sum() + 1e-7) hist_test /= (hist_test.sum() + 1e-7) texture_diff = distance.chisquare(hist_sample, hist_test)[0] # 计算边缘密度差异 edges_sample = cv2.Canny(sample_roi, 50, 150) edges_test = cv2.Canny(test_roi, 50, 150) edge_density_sample = np.sum(edges_sample > 0) / edges_sample.size edge_density_test = np.sum(edges_test > 0) / edges_test.size edge_diff = abs(edge_density_sample - edge_density_test) # 根据特征分类 if color_diff > 30 and texture_diff < 10: return "color" # 颜色缺陷 elif texture_diff > 15 and color_diff < 20: return "texture" # 纹理缺陷 elif edge_diff > 0.1 and np.mean(test_roi[defect_mask > 0]) < np.mean(sample_roi[defect_mask > 0]) - 20: return "missing" # 缺失图案 else: return "stain" # 污渍 def check_print_quality(sample_image_path, test_image, threshold=None): """ 增强版布料印花检测算法 :param sample_image_path: 合格样本图像路径 :param test_image: 内存中的测试图像 (numpy数组) :param threshold: 差异阈值 :return: 是否合格, 差异值, 标记图像, 缺陷列表 """ global sample_features, app_config # 使用配置中的阈值 if threshold is None: threshold = app_config.get("threshold", 0.05) # 读取样本图像 try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_COLOR) if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None, None # 确保测试图像是彩色 if len(test_image.shape) == 2: # 如果是灰度图像 test_image = cv2.cvtColor(test_image, cv2.COLOR_GRAY2BGR) # 确保两个图像大小一致 try: test_image = cv2.resize(test_image, (sample_image.shape[1], sample_image.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None, None # 特征提取和比较 if sample_image_path not in sample_features: logging.info(f"提取样本特征: {sample_image_path}") sample_features[sample_image_path] = extract_features(sample_image) test_features = extract_features(test_image) feature_diff = compute_feature_distance(sample_features[sample_image_path], test_features) # 转换为灰度图像进行像素级比较 sample_gray = cv2.cvtColor(sample_image, cv2.COLOR_BGR2GRAY) test_gray = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) # 自适应阈值差异检测 diff_map = adaptive_threshold_diff(sample_gray, test_gray) # 计算差异比例 diff_pixels = np.count_nonzero(diff_map) total_pixels = sample_gray.size pixel_diff_ratio = diff_pixels / total_pixels # 综合差异分数 combined_diff = 0.7 * feature_diff + 0.3 * pixel_diff_ratio # 检测和分类缺陷 defects, marked_image = detect_defects(sample_gray, test_gray, diff_map) # 判断是否合格 is_qualified = combined_diff <= threshold and len(defects) == 0 return is_qualified, combined_diff, marked_image, defects # ======================== UI更新函数 ======================== def update_diff_display(diff_ratio, is_qualified, defects): """ 更新差异度显示控件 """ # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 显示缺陷数量 defect_count = len(defects) ui.lblDefectCount.setText(f"缺陷数量: {defect_count}") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") ui.lblDefectCount.setStyleSheet("color: green;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") ui.lblDefectCount.setStyleSheet("color: red;") def update_diff_threshold(value): """ 当滑块值改变更新阈值显示 """ global app_config app_config["threshold"] = value / 100.0 ui.lblDiffValue.setText(f"{value}%") save_config(app_config) def update_min_defect_area(value): """ 更新最小缺陷面积阈值 """ global app_config app_config["min_defect_area"] = value ui.lblMinAreaValue.setText(f"{value} px²") save_config(app_config) def update_max_defect_area(value): """ 更新最大缺陷面积阈值 """ global app_config app_config["max_defect_area"] = value ui.lblMaxAreaValue.setText(f"{value} px²") save_config(app_config) def toggle_texture_analysis(state): """ 切换纹理分析功能 """ global app_config app_config["texture_analysis"] = (state == Qt.Checked) save_config(app_config) def toggle_edge_detection(state): """ 切换边缘检测功能 """ global app_config app_config["edge_detection"] = (state == Qt.Checked) save_config(app_config) def toggle_adaptive_threshold(state): """ 切换自适应阈值功能 """ global app_config app_config["adaptive_threshold"] = (state == Qt.Checked) save_config(app_config) def update_color_sensitivity(value): """ 更新颜色敏感度 """ global app_config app_config["color_sensitivity"] = value / 100.0 ui.lblColorSensitivityValue.setText(f"{value}%") save_config(app_config) # 布料印花检测功能(增强版) def check_print(): global isGrabbing, obj_cam_operation, current_sample_path, detection_history, app_config logging.info("检测印花质量按钮按下") # 1. 检查相机状态 if not isGrabbing: logging.warning("相机未取流") QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 2. 检查相机操作对象 if not obj_cam_operation: logging.error("相机操作对象未初始化") QMessageBox.warning(mainWindow, "错误", "相机未正确初始化!", QMessageBox.Ok) return # 3. 检查样本路径 if not current_sample_path or not os.path.exists(current_sample_path): logging.warning(f"无效样本路径: {current_sample_path}") QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return # 使用进度对话框防止UI阻塞 progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: # 4. 获取当前帧 logging.info("尝试获取当前帧") test_image = obj_cam_operation.get_current_frame() progress.setValue(30) if test_image is None: logging.warning("获取当前帧失败") QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return # 5. 获取差异度阈值 diff_threshold = app_config.get("threshold", 0.05) logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(50) # 6. 执行检测 is_qualified, diff_ratio, marked_image, defects = check_print_quality( current_sample_path, test_image, threshold=diff_threshold ) progress.setValue(70) # 检查返回结果是否有效 if is_qualified is None: logging.error("检测函数返回无效结果") QMessageBox.critical(mainWindow, "检测错误", "检测失败,请检查日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}, 缺陷数={len(defects)}") progress.setValue(90) # 7. 更新UI update_diff_display(diff_ratio, is_qualified, defects) # 显示详细结果 result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n" result_text += f"综合差异度: {diff_ratio*100:.2f}%\n" result_text += f"缺陷数量: {len(defects)}\n" result_text += f"阈值: {diff_threshold*100:.2f}%" if not is_qualified and defects: result_text += "\n\n缺陷详情:" for i, defect in enumerate(defects[:3]): # 最多显示3个主要缺陷 result_text += f"\n{i+1}. {defect['type']} (大小: {defect['area']}像素)" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() else: logging.warning("标记图像为空") # 8. 记录检测结果 detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'defects': defects, 'threshold': diff_threshold } detection_history.append(detection_result) update_history_display() progress.setValue(100) except Exception as e: logging.exception("印花检测失败") QMessageBox.critical(mainWindow, "检测错误", f"检测过程中发生错误: {str(e)}", QMessageBox.Ok) finally: progress.close() # 保存标准样本函数(增强版) def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path, sample_features if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"sample_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存标准样本图像", os.path.join(last_dir, default_filename), "BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: logging.info("用户取消了图像保存操作") return # 用户取消保存 # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "PNG" in selected_filter: file_path += ".png" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" else: # 默认使用BMP格式 file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 根据扩展名设置保存格式 format_mapping = { ".bmp": "bmp", ".png": "png", ".jpg": "jpg", ".jpeg": "jpg" } save_format = format_mapping.get(file_extension) if not save_format: QMessageBox.warning(mainWindow, "错误", "不支持的文件格式!", QMessageBox.Ok) return # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) logging.info(f"创建目录: {directory}") except OSError as e: error_msg = f"无法创建目录 {directory}: {str(e)}" QMessageBox.critical(mainWindow, "目录创建错误", error_msg, QMessageBox.Ok) return # 保存当前帧作为标准样本 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret != MV_OK: strError = f"保存样本图像失败: {hex(ret)}" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: success_msg = f"标准样本已保存至:\n{file_path}" QMessageBox.information(mainWindow, "成功", success_msg, QMessageBox.Ok) # 更新当前样本路径 current_sample_path = file_path update_sample_display() # 提取并保存样本特征 sample_img = cv2.imread(file_path) if sample_img is not None: sample_features[file_path] = extract_features(sample_img) logging.info(f"样本特征已提取并保存: {file_path}") # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) except Exception as e: error_msg = f"保存图像发生错误: {str(e)}" QMessageBox.critical(mainWindow, "异常错误", error_msg, QMessageBox.Ok) logging.exception("保存样本图像发生异常") # 预览当前样本 def preview_sample(): global current_sample_path if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return try: # 使用安全方法读取图像 img_data = np.fromfile(current_sample_path, dtype=np.uint8) sample_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR) if sample_img is None: raise Exception("无法加载图像") # 显示特征信息 if current_sample_path in sample_features: features = sample_features[current_sample_path] edge_density = features.get('edge_density', 0) feature_info = f"边缘密度: {edge_density:.4f}\n特征点: {len(features.get('multi_scale', []))}" cv2.putText(sample_img, feature_info, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) # 更新样本路径显示 def update_sample_display(): global current_sample_path if current_sample_path: ui.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") ui.lblSamplePath.setToolTip(current_sample_path) ui.bnPreviewSample.setEnabled(True) else: ui.lblSamplePath.setText("当前样本: 未设置样本") ui.bnPreviewSample.setEnabled(False) # 更新历史记录显示 def update_history_display(): global detection_history ui.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): # 显示最近10条记录 timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" defects = len(result.get('defects', [])) ui.cbHistory.addItem(f"[{timestamp}] {status} - 差异: {ratio} - 缺陷: {defects}") # ... [其余原有函数保持不变,如设备枚举、相机操作等] ... if __name__ == "__main__": # ... [原有初始化代码] ... # 初始化UI app = QApplication(sys.argv) mainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(mainWindow) # 扩大主窗口尺寸 mainWindow.resize(1400, 900) # 更宽的界面 # 创建工具栏 toolbar = mainWindow.addToolBar("检测工具") # 添加检测按钮 ui.bnCheckPrint = QPushButton("检测印花质量") toolbar.addWidget(ui.bnCheckPrint) # 添加保存样本按钮 ui.bnSaveSample = QPushButton("保存标准样本") toolbar.addWidget(ui.bnSaveSample) # 添加预览样本按钮 ui.bnPreviewSample = QPushButton("预览样本") toolbar.addWidget(ui.bnPreviewSample) # 添加历史记录下拉框 ui.cbHistory = QComboBox() ui.cbHistory.setMinimumWidth(350) toolbar.addWidget(QLabel("历史记录:")) toolbar.addWidget(ui.cbHistory) # 添加当前样本显示标签 ui.lblSamplePath = QLabel("当前样本: 未设置样本") status_bar = mainWindow.statusBar() status_bar.addPermanentWidget(ui.lblSamplePath) # === 增强版差异度调整面板 === # 创建右侧面板容器 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 创建差异度调整组 diff_group = QGroupBox("检测参数设置") diff_layout = QVBoxLayout(diff_group) # 差异度阈值控制 ui.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") ui.sliderDiffThreshold = QSlider(Qt.Horizontal) ui.sliderDiffThreshold.setRange(0, 100) # 0-100% ui.sliderDiffThreshold.setValue(int(app_config.get("threshold", 0.05) * 100)) ui.lblDiffValue = QLabel(f"{ui.sliderDiffThreshold.value()}%") # 当前差异度显示 ui.lblCurrentDiff = QLabel("当前差异度: -") ui.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") # 缺陷数量显示 ui.lblDefectCount = QLabel("缺陷数量: -") ui.lblDefectCount.setStyleSheet("font-size: 14px;") # 差异度状态指示器 ui.lblDiffStatus = QLabel("状态: 未检测") ui.lblDiffStatus.setStyleSheet("font-size: 12px;") # 添加分隔线 diff_layout.addWidget(QLabel(" ")) diff_layout.addWidget(QLabel("缺陷检测参数")) # 最小缺陷面积 ui.lblMinArea = QLabel("最小缺陷面积:") ui.sliderMinArea = QSlider(Qt.Horizontal) ui.sliderMinArea.setRange(10, 500) ui.sliderMinArea.setValue(app_config.get("min_defect_area", 50)) ui.lblMinAreaValue = QLabel(f"{ui.sliderMinArea.value()} px²") # 最大缺陷面积 ui.lblMaxArea = QLabel("最大缺陷面积:") ui.sliderMaxArea = QSlider(Qt.Horizontal) ui.sliderMaxArea.setRange(1000, 10000) ui.sliderMaxArea.setValue(app_config.get("max_defect_area", 5000)) ui.lblMaxAreaValue = QLabel(f"{ui.sliderMaxArea.value()} px²") # 颜色敏感度 ui.lblColorSensitivity = QLabel("颜色敏感度:") ui.sliderColorSensitivity = QSlider(Qt.Horizontal) ui.sliderColorSensitivity.setRange(0, 100) ui.sliderColorSensitivity.setValue(int(app_config.get("color_sensitivity", 0.8) * 100)) ui.lblColorSensitivityValue = QLabel(f"{ui.sliderColorSensitivity.value()}%") # 添加分隔线 diff_layout.addWidget(QLabel(" ")) diff_layout.addWidget(QLabel("高级分析选项")) # 纹理分析开关 ui.chkTextureAnalysis = QCheckBox("启用纹理分析") ui.chkTextureAnalysis.setChecked(app_config.get("texture_analysis", True)) # 边缘检测开关 ui.chkEdgeDetection = QCheckBox("启用边缘检测") ui.chkEdgeDetection.setChecked(app_config.get("edge_detection", True)) # 自适应阈值开关 ui.chkAdaptiveThreshold = QCheckBox("使用自适应阈值") ui.chkAdaptiveThreshold.setChecked(app_config.get("adaptive_threshold", True)) # 布局控件 diff_layout.addWidget(ui.lblDiffThreshold) diff_layout.addWidget(ui.sliderDiffThreshold) diff_layout.addWidget(ui.lblDiffValue) diff_layout.addWidget(ui.lblCurrentDiff) diff_layout.addWidget(ui.lblDefectCount) diff_layout.addWidget(ui.lblDiffStatus) diff_layout.addWidget(ui.lblMinArea) diff_layout.addWidget(ui.sliderMinArea) diff_layout.addWidget(ui.lblMinAreaValue) diff_layout.addWidget(ui.lblMaxArea) diff_layout.addWidget(ui.sliderMaxArea) diff_layout.addWidget(ui.lblMaxAreaValue) diff_layout.addWidget(ui.lblColorSensitivity) diff_layout.addWidget(ui.sliderColorSensitivity) diff_layout.addWidget(ui.lblColorSensitivityValue) diff_layout.addWidget(ui.chkTextureAnalysis) diff_layout.addWidget(ui.chkEdgeDetection) diff_layout.addWidget(ui.chkAdaptiveThreshold) # 添加拉伸项使控件靠上 diff_layout.addStretch(1) # 创建停靠窗口 dock = QDockWidget("检测参数面板", mainWindow) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) mainWindow.addDockWidget(Qt.RightDockWidgetArea, dock) # === 连接信号 === # 差异度阈值滑块 ui.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) # 最小缺陷面积滑块 ui.sliderMinArea.valueChanged.connect(update_min_defect_area) # 最大缺陷面积滑块 ui.sliderMaxArea.valueChanged.connect(update_max_defect_area) # 颜色敏感度滑块 ui.sliderColorSensitivity.valueChanged.connect(update_color_sensitivity) # 分析选项复选框 ui.chkTextureAnalysis.stateChanged.connect(toggle_texture_analysis) ui.chkEdgeDetection.stateChanged.connect(toggle_edge_detection) ui.chkAdaptiveThreshold.stateChanged.connect(toggle_adaptive_threshold) # 绑定按钮事件 ui.bnCheckPrint.clicked.connect(check_print) ui.bnSaveSample.clicked.connect(save_sample_image) ui.bnPreviewSample.clicked.connect(preview_sample) # ... [其余原有绑定代码] ... # 显示主窗口 mainWindow.show() # 执行应用 app.exec_() # 关闭设备 close_device() # ch:反初始化SDK | en: finalize SDK MvCamera.MV_CC_Finalize() sys.exit() 这个程序运行后出现了查找不到打不开相机同原有的差异度检查还有新增的功能都看不到了
最新发布
07-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值