QDialog 的 width是获取的是显示状态的真实宽度吗

在Qt中设置UI样式时,代码设置与QSS(Qt样式表,语法类似CSS)的优先级及覆盖规律遵循以下核心原则,这些原则与CSS的层叠规则高度相似但有所简化:

一、优先级规则

  1. 内联样式(代码中直接设置)> 外部样式表(QSS文件)
    • 通过setStyleSheet()在代码中直接设置的样式优先级最高,会覆盖外部QSS文件中的相同属性。
    • 示例
      // 代码中设置(优先级最高)
      ui->pushButton->setStyleSheet("background-color: red;");
      
      // 外部QSS文件(可能被覆盖)
      QPushButton { background-color: blue; }
      按钮最终显示为红色。
  2. 选择器特异性(Specificity)
    • 特异性越高,优先级越高。Qt的选择器特异性计算规则为a-b-c,数值越大优先级越高:
      • a:ID选择器数量(如#myButton)。
      • b:类/属性/伪类选择器数量(如.warning:hover)。
      • c:元素类型选择器数量(如QPushButton)。
    • 示例
      /* 特异性: 1-0-1 (1个ID + 1个元素) */
      QWidget#mainWindow > QPushButton { background-color: green; }
      
      /* 特异性: 0-1-1 (1个类 + 1个元素) */
      QPushButton.warning { background-color: yellow; }
      若按钮同时匹配两个选择器,green会生效(因为ID选择器权重更高)。
  3. 代码顺序
    • 相同特异性时,后定义的样式覆盖先定义的
    • 示例
      /* 先定义(被覆盖) */
      QPushButton { background-color: blue; }
      
      /* 后定义(生效) */
      QPushButton { background-color: red; }
      按钮最终显示为红色。
  4. !important 强制覆盖
    • 使用!important可强制提升优先级,但需谨慎使用(易导致维护困难)。
    • 示例
      QPushButton { background-color: blue !important; }
      QPushButton#myButton { background-color: red; }
      按钮始终显示为蓝色,即使ID选择器特异性更高。

二、覆盖规律

  1. 子控件继承父控件样式
    • 父控件的样式默认会传递给子控件,除非子控件显式覆盖。
    • 示例
      /* 父控件样式 */
      QWidget#mainWindow { background-color: gray; }
      
      /* 子控件需显式覆盖 */
      QPushButton { background-color: white; }
  2. 避免过度嵌套选择器
    • 嵌套层级越深,特异性越高,但可能导致样式难以维护。
    • 推荐做法:直接使用控件ID或简洁的选择器。
    • 示例
      /* 不推荐:过度嵌套 */
      QWidget#mainWindow QFrame#content QToolButton { ... }
      
      /* 推荐:直接使用ID */
      QToolButton#myButton { ... }
  3. 伪状态(Pseudo-States)的优先级
    • 伪状态(如:hover:pressed)的样式优先级与普通状态相同,但需满足特异性规则。
    • 示例
      QPushButton { background-color: blue; }
      QPushButton:hover { background-color: red; } /* 鼠标悬停时生效 */

三、最佳实践

  1. 使用控件ID提升优先级
    • 为需要定制样式的控件分配唯一ID,并在选择器中直接使用。
    • C++代码示例
      // 设置控件ID
      ui->pushButton->setObjectName("myButton");
      
      // QSS中选择器
      QPushButton#myButton { background-color: green; }
  2. 慎用!important
    • 仅在必要时使用(如覆盖第三方库样式),并在注释中说明原因。
  3. 模块化样式管理
    • 将样式表内容写入外部QSS文件,通过setStyleSheet()加载,便于维护和调整。
    • 示例
      // 加载外部QSS文件
      QFile file(":/styles/main.qss");
      if (file.open(QFile::ReadOnly)) {
          QString styleSheet = QLatin1String(file.readAll());
          qApp->setStyleSheet(styleSheet);
      }
  4. 实时预览与测试
    • 使用Qt Designer的样式表编辑器实时预览效果,或通过代码打印控件的最终样式。
dorm_face_recognition_gui.py代码如下: import pickle import sys import os import cv2 import numpy as np import torch from PyQt5.QtWidgets import QListWidget, QProgressDialog from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QComboBox, QSlider, QMessageBox, QTextEdit, QGroupBox, QScrollArea, QDialog, QDialogButtonBox, QTableWidget, QTableWidgetItem, QHeaderView, QGridLayout) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont, QColor import joblib import logging import json from datetime import datetime 在 dorm_face_recognition_gui.py 顶部添加导入 from face_recognition import FaceRecognition 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(levelname)s - %(message)s’) logger = logging.getLogger(name) class FeedbackDialog(QDialog): “”“反馈对话框”“” def __init__(self, parent=None, last_results=None, dorm_members=None): super().__init__(parent) self.setWindowTitle("识别错误反馈") self.setFixedSize(500, 400) self.last_results = last_results or [] self.dorm_members = dorm_members or [] self.init_ui() def init_ui(self): layout = QVBoxLayout(self) # 添加当前识别结果 result_label = QLabel("当前识别结果:") layout.addWidget(result_label) # 使用表格显示结果 self.results_table = QTableWidget() self.results_table.setColumnCount(4) self.results_table.setHorizontalHeaderLabels(["ID", "识别结果", "置信度", "位置和大小"]) self.results_table.setSelectionBehavior(QTableWidget.SelectRows) self.results_table.setEditTriggers(QTableWidget.NoEditTriggers) # 填充表格数据 self.results_table.setRowCount(len(self.last_results)) for i, result in enumerate(self.last_results): x, y, w, h = result["box"] self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) self.results_table.setItem(i, 1, QTableWidgetItem(result["label"])) self.results_table.setItem(i, 2, QTableWidgetItem(f"{result['confidence']:.2f}")) self.results_table.setItem(i, 3, QTableWidgetItem(f"({x}, {y}) - {w}x{h}")) # 设置表格样式 self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.results_table.verticalHeader().setVisible(False) layout.addWidget(self.results_table) # 添加正确身份选择 correct_layout = QGridLayout() correct_label = QLabel("正确身份:") correct_layout.addWidget(correct_label, 0, 0) self.correct_combo = QComboBox() self.correct_combo.addItem("选择正确身份", None) for member in self.dorm_members: self.correct_combo.addItem(member, member) self.correct_combo.addItem("陌生人", "stranger") self.correct_combo.addItem("不在列表中", "unknown") correct_layout.addWidget(self.correct_combo, 0, 1) # 添加备注 note_label = QLabel("备注:") correct_layout.addWidget(note_label, 1, 0) self.note_text = QTextEdit() self.note_text.setPlaceholderText("可添加额外说明...") self.note_text.setMaximumHeight(60) correct_layout.addWidget(self.note_text, 1, 1) layout.addLayout(correct_layout) # 添加按钮 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def get_selected_result(self): """获取选择的识别结果""" selected_row = self.results_table.currentRow() if selected_row >= 0 and selected_row < len(self.last_results): return self.last_results[selected_row] return None def get_feedback_data(self): """获取反馈数据""" selected_result = self.get_selected_result() if not selected_result: return None return { "timestamp": datetime.now().isoformat(), "original_label": selected_result["label"], "correct_label": self.correct_combo.currentData(), "confidence": selected_result["confidence"], "box": selected_result["box"], # 保存完整的框信息 "note": self.note_text.toPlainText().strip() } class FaceRecognitionSystem(QMainWindow): def init(self): super().init() self.setWindowTitle(“寝室人脸识别系统”) self.setGeometry(100, 100, 1200, 800) # 初始化变量 self.model_loaded = False self.camera_active = False self.video_capture = None self.timer = QTimer() self.current_image = None self.last_results = [] # 存储上次识别结果 self.dorm_members = [] # 寝室成员列表 # 创建主界面 self.main_widget = QWidget() self.setCentralWidget(self.main_widget) self.layout = QHBoxLayout(self.main_widget) # 左侧控制面板 - 占40%宽度 self.control_panel = QWidget() self.control_layout = QVBoxLayout(self.control_panel) self.control_layout.setAlignment(Qt.AlignTop) self.control_panel.setMaximumWidth(400) self.layout.addWidget(self.control_panel, 40) # 40%宽度 # 右侧图像显示区域 - 占60%宽度 self.image_panel = QWidget() self.image_layout = QVBoxLayout(self.image_panel) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(800, 600) self.image_label.setStyleSheet("background-color: #333; border: 1px solid #555;") self.image_layout.addWidget(self.image_label) self.layout.addWidget(self.image_panel, 60) # 60%宽度 # 状态栏 self.status_bar = self.statusBar() self.status_bar.showMessage("系统初始化中...") # 初始化人脸识别器 - 关键修复 self.face_recognition = FaceRecognition() # 初始化UI组件 self.init_ui() # 添加工具栏(必须在UI初始化后) self.toolbar = self.addToolBar('工具栏') # 添加反馈按钮 self.add_feedback_button() # 初始化模型 self.init_models() def init_ui(self): """初始化用户界面组件""" # 标题 title_label = QLabel("寝室人脸识别系统") title_label.setFont(QFont("Arial", 18, QFont.Bold)) title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("color: #2c3e50; padding: 10px;") self.control_layout.addWidget(title_label) # 模型加载 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) self.load_model_btn = QPushButton("加载模型") self.load_model_btn.setIcon(QIcon.fromTheme("document-open")) self.load_model_btn.setStyleSheet("background-color: #3498db;") self.load_model_btn.clicked.connect(self.load_model) model_layout.addWidget(self.load_model_btn) self.model_status = QLabel("模型状态: 未加载") model_layout.addWidget(self.model_status) self.control_layout.addWidget(model_group) # 在模型设置部分添加重新训练按钮 self.retrain_btn = QPushButton("重新训练模型") self.retrain_btn.setIcon(QIcon.fromTheme("view-refresh")) self.retrain_btn.setStyleSheet("background-color: #f39c12;") self.retrain_btn.clicked.connect(self.retrain_model) self.retrain_btn.setEnabled(False) # 初始不可用 model_layout.addWidget(self.retrain_btn) # 识别设置 settings_group = QGroupBox("识别设置") settings_layout = QVBoxLayout(settings_group) # 置信度阈值 threshold_layout = QHBoxLayout() threshold_label = QLabel("置信度阈值:") threshold_layout.addWidget(threshold_label) self.threshold_slider = QSlider(Qt.Horizontal) self.threshold_slider.setRange(0, 100) self.threshold_slider.setValue(70) self.threshold_slider.valueChanged.connect(self.update_threshold) threshold_layout.addWidget(self.threshold_slider) self.threshold_value = QLabel("0.70") threshold_layout.addWidget(self.threshold_value) settings_layout.addLayout(threshold_layout) # 显示选项 display_layout = QHBoxLayout() display_label = QLabel("显示模式:") display_layout.addWidget(display_label) self.display_combo = QComboBox() self.display_combo.addItems(["原始图像", "检测框", "识别结果"]) self.display_combo.setCurrentIndex(2) display_layout.addWidget(self.display_combo) settings_layout.addLayout(display_layout) self.control_layout.addWidget(settings_group) # 识别功能 recognition_group = QGroupBox("识别功能") recognition_layout = QVBoxLayout(recognition_group) # 图片识别 self.image_recognition_btn = QPushButton("图片识别") self.image_recognition_btn.setIcon(QIcon.fromTheme("image-x-generic")) self.image_recognition_btn.setStyleSheet("background-color: #9b59b6;") self.image_recognition_btn.clicked.connect(self.open_image) self.image_recognition_btn.setEnabled(False) recognition_layout.addWidget(self.image_recognition_btn) # 摄像头识别 self.camera_recognition_btn = QPushButton("启动摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("camera-web")) self.camera_recognition_btn.setStyleSheet("background-color: #e74c3c;") self.camera_recognition_btn.clicked.connect(self.toggle_camera) self.camera_recognition_btn.setEnabled(False) recognition_layout.addWidget(self.camera_recognition_btn) self.control_layout.addWidget(recognition_group) # 结果展示区域 - 使用QTextEdit替代QLabel results_group = QGroupBox("识别结果") results_layout = QVBoxLayout(results_group) self.results_text = QTextEdit() self.results_text.setReadOnly(True) self.results_text.setFont(QFont("Microsoft YaHei", 12)) # 使用支持中文的字体 self.results_text.setStyleSheet("background-color: #f8f9fa; border: 1px solid #ddd; padding: 10px;") self.results_text.setPlaceholderText("识别结果将显示在这里") # 添加滚动区域 scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setWidget(self.results_text) results_layout.addWidget(scroll_area) self.control_layout.addWidget(results_group, 1) # 占据剩余空间 # 系统信息 info_group = QGroupBox("系统信息") info_layout = QVBoxLayout(info_group) self.device_label = QLabel(f"计算设备: {'GPU' if torch.cuda.is_available() else 'CPU'}") info_layout.addWidget(self.device_label) self.model_info = QLabel("加载模型以显示信息") info_layout.addWidget(self.model_info) self.control_layout.addWidget(info_group) # 退出按钮 exit_btn = QPushButton("退出系统") exit_btn.setIcon(QIcon.fromTheme("application-exit")) exit_btn.clicked.connect(self.close) exit_btn.setStyleSheet("background-color: #ff6b6b; color: white;") self.control_layout.addWidget(exit_btn) def add_feedback_button(self): """添加反馈按钮到界面""" # 创建反馈按钮 self.feedback_button = QPushButton("提供反馈", self) self.feedback_button.setFixedSize(120, 40) # 设置固定大小 self.feedback_button.setStyleSheet( "QPushButton {" " background-color: #4CAF50;" " color: white;" " border-radius: 5px;" " font-weight: bold;" "}" "QPushButton:hover {" " background-color: #45a049;" "}" ) # 连接按钮点击事件 self.feedback_button.clicked.connect(self.open_feedback_dialog) # 添加到工具栏 self.toolbar.addWidget(self.feedback_button) def open_feedback_dialog(self): """打开反馈对话框""" if not self.last_results: QMessageBox.warning(self, "无法反馈", "没有可反馈的识别结果") return dialog = FeedbackDialog( self, last_results=self.last_results, dorm_members=self.dorm_members ) if dialog.exec_() == QDialog.Accepted: feedback_data = dialog.get_feedback_data() if feedback_data: # 修复:调用 FaceRecognition 实例的 save_feedback 方法 selected_result = dialog.get_selected_result() if selected_result: # 获取检测框 detected_box = [ selected_result["box"][0], selected_result["box"][1], selected_result["box"][0] + selected_result["box"][2], selected_result["box"][1] + selected_result["box"][3] ] # 调用保存反馈方法 self.face_recognition.save_feedback( self.current_image, detected_box, feedback_data["original_label"], feedback_data["correct_label"] ) QMessageBox.information(self, "反馈提交", "感谢您的反馈!数据已保存用于改进模型") else: QMessageBox.warning(self, "反馈错误", "未选择要反馈的人脸结果") def init_models(self): """初始化模型组件""" # 设置设备 self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.device_label.setText(f"计算设备: {'GPU' if torch.cuda.is_available() else 'CPU'}") # 初始化人脸检测器 try: self.detector = MTCNN( keep_all=True, post_process=False, device=self.device ) self.status_bar.showMessage("MTCNN 检测器初始化完成") logger.info("MTCNN 检测器初始化完成") except Exception as e: self.status_bar.showMessage(f"MTCNN 初始化失败: {str(e)}") logger.error(f"MTCNN 初始化失败: {str(e)}") return # 初始化人脸特征提取器 try: self.embedder = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() self.status_bar.showMessage("FaceNet 特征提取器初始化完成") logger.info("FaceNet 特征提取器初始化完成") except Exception as e: self.status_bar.showMessage(f"FaceNet 初始化失败: {str(e)}") logger.error(f"FaceNet 初始化失败: {str(e)}") def load_model(self): """加载预训练的SVM分类器""" options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName( self, "选择模型文件", "", "模型文件 (*.pkl);;所有文件 (*)", options=options ) if file_path: try: # 加载模型 model_data = joblib.load(file_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.dorm_members = model_data['dorm_members'] # 启用重新训练按钮 self.retrain_btn.setEnabled(True) # 更新UI状态 self.model_loaded = True self.model_status.setText("模型状态: 已加载") self.model_info.setText(f"寝室成员: {', '.join(self.dorm_members)}") self.image_recognition_btn.setEnabled(True) self.camera_recognition_btn.setEnabled(True) # 状态栏消息 self.status_bar.showMessage(f"模型加载成功: {os.path.basename(file_path)}") # 显示成功消息 QMessageBox.information( self, "模型加载", f"模型加载成功!\n识别成员: {len(self.dorm_members)}人\n置信度阈值: {self.threshold_slider.value() / 100:.2f}" ) except Exception as e: QMessageBox.critical(self, "加载错误", f"模型加载失败: {str(e)}") self.status_bar.showMessage(f"模型加载失败: {str(e)}") def update_threshold(self, value): """更新置信度阈值""" threshold = value / 100 self.threshold_value.setText(f"{threshold:.2f}") self.status_bar.showMessage(f"置信度阈值更新为: {threshold:.2f}") def open_image(self): """打开图片文件进行识别""" if not self.model_loaded: QMessageBox.warning(self, "警告", "请先加载模型!") return options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName( self, "选择识别图片", "", "图片文件 (*.jpg *.jpeg *.png);;所有文件 (*)", options=options ) if file_path: # 读取图片 image = cv2.imread(file_path) if image is None: QMessageBox.critical(self, "错误", "无法读取图片文件!") return # 保存当前图片 self.current_image = image.copy() # 进行识别 self.recognize_faces(image) def toggle_camera(self): """切换摄像头状态""" if not self.model_loaded: QMessageBox.warning(self, "警告", "请先加载模型!") return if not self.camera_active: # 尝试打开摄像头 self.video_capture = cv2.VideoCapture(0) if not self.video_capture.isOpened(): QMessageBox.critical(self, "错误", "无法打开摄像头!") return # 启动摄像头 self.camera_active = True self.camera_recognition_btn.setText("停止摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("media-playback-stop")) self.timer.timeout.connect(self.process_camera_frame) self.timer.start(30) # 约33 FPS self.status_bar.showMessage("摄像头已启动") else: # 停止摄像头 self.camera_active = False self.camera_recognition_btn.setText("启动摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("camera-web")) self.timer.stop() if self.video_capture: self.video_capture.release() self.status_bar.showMessage("摄像头已停止") def process_camera_frame(self): """处理摄像头帧""" ret, frame = self.video_capture.read() if ret: # 保存当前帧 self.current_image = frame.copy() # 进行识别 self.recognize_faces(frame) def retrain_model(self): """使用反馈数据重新训练模型""" # 获取所有反馈数据 feedback_dir = os.path.join(os.getcwd(), "data", "feedback_data") # 修复1:支持多种文件扩展名 feedback_files = [] for f in os.listdir(feedback_dir): filepath = os.path.join(feedback_dir, f) if os.path.isfile(filepath) and (f.endswith('.pkl') or f.endswith('.json')): feedback_files.append(f) # 修复2:添加目录存在性检查 if not os.path.exists(feedback_dir): QMessageBox.warning(self, "目录不存在", f"反馈数据目录不存在: {feedback_dir}") return if not feedback_files: QMessageBox.information(self, "无反馈数据", "没有找到反馈数据,无法重新训练") return # 确认对话框 reply = QMessageBox.question( self, '确认重新训练', f"将使用 {len(feedback_files)} 条反馈数据重新训练模型。此操作可能需要几分钟时间,确定继续吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return try: # 创建进度对话框 progress = QProgressDialog("正在重新训练模型...", "取消", 0, len(feedback_files), self) progress.setWindowTitle("模型重新训练") progress.setWindowModality(Qt.WindowModal) progress.setMinimumDuration(0) progress.setValue(0) # 收集所有反馈数据 feedback_data = [] for i, filename in enumerate(feedback_files): filepath = os.path.join(feedback_dir, filename) # 修复3:根据文件扩展名使用不同的加载方式 if filename.endswith('.pkl'): with open(filepath, 'rb') as f: # 二进制模式读取 data = pickle.load(f) elif filename.endswith('.json'): with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) else: continue # 跳过不支持的文件类型 feedback_data.append(data) progress.setValue(i + 1) QApplication.processEvents() # 保持UI响应 if progress.wasCanceled(): return progress.setValue(len(feedback_files)) # 重新训练模型 self.status_bar.showMessage("正在重新训练模型...") # 修复4:添加详细的日志记录 logger.info(f"开始重新训练,使用 {len(feedback_data)} 条反馈数据") # 调用重新训练方法 success = self.face_recognition.retrain_with_feedback(feedback_data) if success: # 更新UI状态 self.model_status.setText("模型状态: 已重新训练") self.dorm_members = self.face_recognition.dorm_members self.model_info.setText(f"寝室成员: {', '.join(self.dorm_members)}") # 保存更新后的模型 model_path = os.path.join("models", "updated_model.pkl") self.face_recognition.save_updated_model(model_path) QMessageBox.information(self, "训练完成", "模型已成功使用反馈数据重新训练!") else: QMessageBox.warning(self, "训练失败", "重新训练过程中出现问题") except Exception as e: logger.error(f"重新训练失败: {str(e)}") QMessageBox.critical(self, "训练错误", f"重新训练模型时出错: {str(e)}") def recognize_faces(self, image): """识别人脸并在图像上标注结果""" # 清空上次结果 self.last_results = [] # 转换为 PIL 图像 pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) # 检测人脸 boxes, probs, _ = self.detector.detect(pil_image, landmarks=True) # 获取显示选项 display_mode = self.display_combo.currentIndex() # 准备显示图像 display_image = image.copy() # 如果没有检测到人脸 if boxes is None: if display_mode == 2: # 识别结果模式 cv2.putText(display_image, "未检测到人脸", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) self.results_text.setText("未检测到人脸") else: # 提取每个人脸 faces = [] for box in boxes: x1, y1, x2, y2 = box face = pil_image.crop((x1, y1, x2, y2)) faces.append(face) # 提取特征 embeddings = [] if faces and self.model_loaded: # 批量处理所有人脸 face_tensors = [self.preprocess_face(face) for face in faces] if face_tensors: face_tensors = torch.stack(face_tensors).to(self.device) with torch.no_grad(): embeddings = self.embedder(face_tensors).cpu().numpy() # 处理每个人脸 for i, (box, prob) in enumerate(zip(boxes, probs)): x1, y1, x2, y2 = box w, h = x2 - x1, y2 - y1 # 在图像上绘制结果 if display_mode == 0: # 原始图像 # 不绘制任何内容 pass elif display_mode == 1: # 检测框 # 绘制人脸框 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) elif display_mode == 2: # 识别结果 # 绘制人脸框 color = (0, 255, 0) # 绿色 # 如果有嵌入向量,则进行识别 if i < len(embeddings): # 预测 probabilities = self.classifier.predict_proba([embeddings[i]])[0] max_prob = np.max(probabilities) pred_class = self.classifier.predict([embeddings[i]])[0] pred_label = self.label_encoder.inverse_transform([pred_class])[0] # 获取置信度阈值 threshold = self.threshold_slider.value() / 100 # 判断是否为陌生人 if max_prob < threshold or pred_label == 'stranger': label = "陌生人" color = (0, 0, 255) # 红色 else: label = pred_label color = (0, 255, 0) # 绿色 # 保存结果用于文本显示 - 修复:保存完整的框信息 result = { "box": [int(x1), int(y1), int(x2 - x1), int(y2 - y1)], # [x, y, width, height] "label": label, "confidence": max_prob } self.last_results.append(result) # 绘制标签 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(display_image, f"{label} ({max_prob:.2f})", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) else: # 无法识别的处理 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 165, 255), 2) cv2.putText(display_image, "处理中...", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 165, 255), 2) # 更新结果文本 self.update_results_text() # 在图像上显示FPS(摄像头模式下) if self.camera_active: fps = self.timer.interval() if fps > 0: cv2.putText(display_image, f"FPS: {1000 / fps:.1f}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) # 显示图像 self.display_image(display_image) def update_results_text(self): """更新结果文本区域""" if not self.last_results: self.results_text.setText("未识别到任何人脸") return # 构建结果文本 result_text = "<h3>识别结果:</h3>" for i, result in enumerate(self.last_results, 1): x, y, w, h = result["box"] label = result["label"] confidence = result["confidence"] # 处理中文显示问题 if label in self.dorm_members: result_text += ( f"<p><b>人脸 #{i}:</b> " f"<span style='color:green;'>寝室成员 - {label}</span><br>" f"位置: ({x}, {y}), 大小: {w}x{h}, 置信度: {confidence:.2f}</p>" ) else: result_text += ( f"<p><b>人脸 #{i}:</b> " f"<span style='color:red;'>陌生人</span><br>" f"位置: ({x}, {y}), 大小: {w}x{h}, 置信度: {confidence:.2f}</p>" ) self.results_text.setHtml(result_text) def preprocess_face(self, face_img): """预处理人脸图像""" # 调整大小 face_img = face_img.resize((160, 160)) # 转换为张量并归一化 face_img = np.array(face_img).astype(np.float32) / 255.0 face_img = (face_img - 0.5) / 0.5 # 归一化到[-1, 1] face_img = torch.tensor(face_img).permute(2, 0, 1) # HWC to CHW return face_img def display_image(self, image): """在QLabel中显示图像""" # 将OpenCV图像转换为Qt格式 height, width, channel = image.shape bytes_per_line = 3 * width q_img = QImage(image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() # 缩放图像以适应标签 pixmap = QPixmap.fromImage(q_img) self.image_label.setPixmap(pixmap.scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) def closeEvent(self, event): """关闭事件处理""" if self.camera_active: self.timer.stop() if self.video_capture: self.video_capture.release() # 确认退出 reply = QMessageBox.question( self, '确认退出', "确定要退出系统吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: event.accept() else: event.ignore() if name == “main”: app = QApplication(sys.argv) # 设置全局异常处理 def handle_exception(exc_type, exc_value, exc_traceback): """全局异常处理""" import traceback error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) print(f"未捕获的异常:\n{error_msg}") # 记录到文件 with open("error.log", "a") as f: f.write(f"\n\n{datetime.now()}:\n{error_msg}") # 显示给用户 QMessageBox.critical(None, "系统错误", f"发生未处理的异常:\n{str(exc_value)}") sys.exit(1) sys.excepthook = handle_exception window = FaceRecognitionSystem() window.show() sys.exit(app.exec_()) face_model.py代码如下:import os os.environ[‘TF_CPP_MIN_LOG_LEVEL’] = ‘3’ # 禁用 TensorFlow 日志(如果仍有依赖) import cv2 import numpy as np import time import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchvision import transforms from sklearn.svm import SVC from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import joblib import logging import sys import glob from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image import gc 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(levelname)s - %(message)s’) logger = logging.getLogger(name) def check_gpu_environment(): “”“检查 GPU 环境”“” print(“=” * 60) print(“GPU 环境检查”) print(“=” * 60) # 检查 CUDA 是否可用 print(f"PyTorch 版本: {torch.__version__}") print(f"CUDA 可用: {torch.cuda.is_available()}") if torch.cuda.is_available(): print(f"GPU 数量: {torch.cuda.device_count()}") for i in range(torch.cuda.device_count()): print(f"GPU {i}: {torch.cuda.get_device_name(i)}") print(f" 显存总量: {torch.cuda.get_device_properties(i).total_memory / 1024 ** 3:.2f} GB") print("=" * 60) class FaceDataset(Dataset): “”“人脸数据集类”“” def __init__(self, data_dir, min_samples=10, transform=None): self.data_dir = data_dir self.transform = transform self.faces = [] self.labels = [] self.label_map = {} self.dorm_members = [] self._load_dataset(min_samples) def _load_dataset(self, min_samples): """加载数据集""" # 遍历每个成员文件夹 for member_dir in os.listdir(self.data_dir): member_path = os.path.join(self.data_dir, member_dir) if not os.path.isdir(member_path): continue # 记录寝室成员 self.dorm_members.append(member_dir) self.label_map[member_dir] = len(self.label_map) # 遍历成员的所有照片 member_faces = [] for img_file in os.listdir(member_path): img_path = os.path.join(member_path, img_file) try: # 使用 PIL 加载图像 img = Image.open(img_path).convert('RGB') member_faces.append(img) except Exception as e: logger.warning(f"无法加载图像 {img_path}: {str(e)}") # 确保每个成员有足够样本 if len(member_faces) < min_samples: logger.warning(f"{member_dir} 只有 {len(member_faces)} 个有效样本,至少需要 {min_samples} 个") continue # 添加成员数据 self.faces.extend(member_faces) self.labels.extend([self.label_map[member_dir]] * len(member_faces)) # 添加陌生人样本 stranger_faces = self._generate_stranger_samples(len(self.faces) // 4) self.faces.extend(stranger_faces) self.labels.extend([len(self.label_map)] * len(stranger_faces)) self.label_map['stranger'] = len(self.label_map) logger.info(f"数据集加载完成: {len(self.faces)} 个样本, {len(self.dorm_members)} 个成员") def _generate_stranger_samples(self, num_samples): """生成陌生人样本""" stranger_faces = [] # 使用公开数据集的人脸作为陌生人 # 这里使用 LFW 数据集作为示例(实际项目中应使用真实数据) for _ in range(num_samples): # 生成随机噪声图像(实际应用中应使用真实陌生人照片) random_face = Image.fromarray(np.uint8(np.random.rand(160, 160, 3) * 255)) stranger_faces.append(random_face) return stranger_faces def __len__(self): return len(self.faces) def __getitem__(self, idx): face = self.faces[idx] label = self.labels[idx] if self.transform: face = self.transform(face) return face, label class DormFaceRecognizer: “”“寝室人脸识别系统 (PyTorch 实现)”“” def __init__(self, threshold=0.7, device=None): # 设置设备 self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu') logger.info(f"使用设备: {self.device}") # 初始化人脸检测器 self.detector = MTCNN( keep_all=True, post_process=False, device=self.device ) logger.info("MTCNN 检测器初始化完成") # 初始化人脸特征提取器 self.embedder = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() # 设置为评估模式 logger.info("FaceNet 特征提取器初始化完成") # 初始化其他组件 self.classifier = None self.label_encoder = None self.threshold = threshold self.dorm_members = [] # 数据预处理 self.transform = transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) def create_dataset(self, data_dir, min_samples=10, batch_size=32, num_workers=4): """创建数据集""" dataset = FaceDataset( data_dir, min_samples=min_samples, transform=self.transform ) # 保存成员信息 self.dorm_members = dataset.dorm_members self.label_encoder = LabelEncoder().fit( list(dataset.label_map.keys()) + ['stranger'] ) # 创建数据加载器 dataloader = DataLoader( dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True ) return dataset, dataloader def extract_features(self, dataloader): """提取人脸特征向量""" embeddings = [] labels = [] logger.info("开始提取特征...") start_time = time.time() with torch.no_grad(): for batch_idx, (faces, batch_labels) in enumerate(dataloader): # 移动到设备 faces = faces.to(self.device) # 提取特征 batch_embeddings = self.embedder(faces) # 保存结果 embeddings.append(batch_embeddings.cpu().numpy()) labels.append(batch_labels.numpy()) # 每10个批次打印一次进度 if (batch_idx + 1) % 10 == 0: elapsed = time.time() - start_time logger.info(f"已处理 {batch_idx + 1}/{len(dataloader)} 批次, 耗时: {elapsed:.2f}秒") # 合并结果 embeddings = np.vstack(embeddings) labels = np.hstack(labels) logger.info(f"特征提取完成: {embeddings.shape[0]} 个样本, 耗时: {time.time() - start_time:.2f}秒") return embeddings, labels def train_classifier(self, embeddings, labels): """训练 SVM 分类器""" logger.info("开始训练分类器...") start_time = time.time() # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( embeddings, labels, test_size=0.2, random_state=42 ) # 创建并训练 SVM 分类器 self.classifier = SVC(kernel='linear', probability=True, C=1.0) self.classifier.fit(X_train, y_train) # 评估模型 y_pred = self.classifier.predict(X_test) accuracy = accuracy_score(y_test, y_pred) logger.info(f"分类器训练完成, 准确率: {accuracy:.4f}, 耗时: {time.time() - start_time:.2f}秒") return accuracy def recognize_face(self, image): """识别单张图像中的人脸""" # 转换为 PIL 图像 if isinstance(image, np.ndarray): image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) # 检测人脸 boxes, probs, landmarks = self.detector.detect(image, landmarks=True) recognitions = [] if boxes is not None: # 提取每个人脸 faces = [] for box in boxes: x1, y1, x2, y2 = box face = image.crop((x1, y1, x2, y2)) faces.append(face) # 预处理人脸 face_tensors = torch.stack([self.transform(face) for face in faces]).to(self.device) # 提取特征 with torch.no_grad(): embeddings = self.embedder(face_tensors).cpu().numpy() # 预测 probabilities = self.classifier.predict_proba(embeddings) pred_classes = self.classifier.predict(embeddings) for i, (box, prob) in enumerate(zip(boxes, probs)): max_prob = np.max(probabilities[i]) pred_label = self.label_encoder.inverse_transform([pred_classes[i]])[0] # 判断是否为陌生人 if max_prob < self.threshold or pred_label == 'stranger': recognitions.append(("陌生人", max_prob, box)) else: recognitions.append((pred_label, max_prob, box)) return recognitions def save_model(self, file_path): """保存模型""" model_data = { 'classifier': self.classifier, 'label_encoder': self.label_encoder, 'threshold': self.threshold, 'dorm_members': self.dorm_members } joblib.dump(model_data, file_path) logger.info(f"模型已保存至: {file_path}") def load_model(self, file_path): """加载模型""" model_data = joblib.load(file_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.threshold = model_data['threshold'] self.dorm_members = model_data['dorm_members'] logger.info(f"模型已加载,寝室成员: {', '.join(self.dorm_members)}") def main(): “”“主函数”“” print(f"[{time.strftime(‘%H:%M:%S’)}] 程序启动") # 检查 GPU 环境 check_gpu_environment() # 检查并创建必要的目录 os.makedirs('data/dorm_faces', exist_ok=True) # 初始化识别器 try: recognizer = DormFaceRecognizer(threshold=0.6) logger.info("人脸识别器初始化成功") except Exception as e: logger.error(f"初始化失败: {str(e)}") print("程序将在10秒后退出...") time.sleep(10) return # 数据集路径 data_dir = "data/dorm_faces" # 检查数据集是否存在 if not os.path.exists(data_dir) or not os.listdir(data_dir): logger.warning(f"数据集目录 '{data_dir}' 不存在或为空") print("请创建以下结构的目录:") print("dorm_faces/") print("├── 成员1/") print("│ ├── 照片1.jpg") print("│ ├── 照片2.jpg") print("│ └── ...") print("├── 成员2/") print("│ └── ...") print("└── ...") print("\n程序将在10秒后退出...") time.sleep(10) return # 步骤1: 创建数据集 try: dataset, dataloader = recognizer.create_dataset( data_dir, min_samples=10, batch_size=64, num_workers=4 ) except Exception as e: logger.error(f"数据集创建失败: {str(e)}") return # 步骤2: 提取特征 try: embeddings, labels = recognizer.extract_features(dataloader) except Exception as e: logger.error(f"特征提取失败: {str(e)}") return # 步骤3: 训练分类器 try: accuracy = recognizer.train_classifier(embeddings, labels) except Exception as e: logger.error(f"分类器训练失败: {str(e)}") return # 保存模型 model_path = "models/dorm_face_model_pytorch.pkl" try: recognizer.save_model(model_path) except Exception as e: logger.error(f"模型保存失败: {str(e)}") # 测试识别 test_image_path = "test_photo.jpg" if not os.path.exists(test_image_path): logger.warning(f"测试图片 '{test_image_path}' 不存在,跳过识别测试") else: logger.info(f"正在测试识别: {test_image_path}") try: test_image = cv2.imread(test_image_path) if test_image is None: logger.error(f"无法读取图片: {test_image_path}") else: recognitions = recognizer.recognize_face(test_image) if not recognitions: logger.info("未检测到人脸") else: # 在图像上绘制结果 for name, confidence, box in recognitions: x1, y1, x2, y2 = box label = f"{name} ({confidence:.2f})" color = (0, 255, 0) if name != "陌生人" else (0, 0, 255) # 绘制矩形框 cv2.rectangle(test_image, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) # 绘制标签 cv2.putText(test_image, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) # 显示结果 cv2.imshow("人脸识别结果", test_image) cv2.waitKey(0) cv2.destroyAllWindows() # 保存结果图像 result_path = "recognition_result_pytorch.jpg" cv2.imwrite(result_path, test_image) logger.info(f"识别结果已保存至: {result_path}") except Exception as e: logger.error(f"人脸识别失败: {str(e)}") logger.info("程序执行完成") if name == “main”: main() face_recognition.py代码如下:import json import cv2 import numpy as np import torch import insightface from insightface.app import FaceAnalysis from facenet_pytorch import InceptionResnetV1 from PIL import Image import joblib import os import pickle from datetime import datetime import random import torch.nn as nn import torch.optim as optim from sklearn.preprocessing import LabelEncoder from sklearn.svm import SVC from torch.utils.data import Dataset, DataLoader class FaceRecognition: def init(self, device=None): self.device = device or torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’) self.model_loaded = False self.training_data = {} # 初始化 training_data 属性 self.dorm_members = [] # 初始化 dorm_members 属性 self.label_encoder = LabelEncoder() # 初始化标签编码器 self.init_models() def init_models(self): """初始化人脸识别模型""" try: # 初始化ArcFace模型 - 使用正确的方法 self.arcface_model = FaceAnalysis(providers=['CPUExecutionProvider']) self.arcface_model.prepare(ctx_id=0, det_size=(640, 640)) # 初始化FaceNet模型作为备选 self.facenet_model = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() # 状态标记 self.models_initialized = True print("模型初始化完成") except Exception as e: print(f"模型初始化失败: {str(e)}") self.models_initialized = False def load_classifier(self, model_path): """加载分类器模型""" try: model_data = joblib.load(model_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.dorm_members = model_data['dorm_members'] # 确保加载training_data self.training_data = model_data.get('training_data', {}) self.model_loaded = True print(f"分类器加载成功,成员: {', '.join(self.dorm_members)}") print(f"训练数据包含 {len(self.training_data)} 个类别") return True except Exception as e: print(f"分类器加载失败: {str(e)}") self.model_loaded = False return False def extract_features(self, face_img): """使用ArcFace提取人脸特征""" try: if face_img.size == 0: print("错误:空的人脸图像") return None # 将图像从BGR转换为RGB rgb_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) faces = self.arcface_model.get(rgb_img) if faces: return faces[0].embedding print("未检测到人脸特征") return None except Exception as e: print(f"特征提取失败: {str(e)}") return None def extract_features_facenet(self, face_img): """使用FaceNet提取人脸特征(备选)""" try: # 转换为PIL图像并预处理 face_pil = Image.fromarray(cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)) face_tensor = self.preprocess_face(face_pil).to(self.device) with torch.no_grad(): features = self.facenet_model(face_tensor.unsqueeze(0)).cpu().numpy()[0] return features except Exception as e: print(f"FaceNet特征提取失败: {str(e)}") return None def preprocess_face(self, face_img): """预处理人脸图像""" # 调整大小 face_img = face_img.resize((160, 160)) # 转换为张量并归一化 face_img = np.array(face_img).astype(np.float32) / 255.0 face_img = (face_img - 0.5) / 0.5 # 归一化到[-1, 1] face_img = torch.tensor(face_img).permute(2, 0, 1) # HWC to CHW return face_img def retrain_with_feedback(self, feedback_data): """使用反馈数据重新训练模型""" # 检查是否有原始训练数据 if not self.training_data: print("错误:没有可用的原始训练数据") return False # 收集原始训练数据 original_features = [] original_labels = [] # 收集特征和标签 for member, embeddings in self.training_data.items(): for emb in embeddings: original_features.append(emb) original_labels.append(member) # 收集反馈数据 feedback_features = [] feedback_labels = [] for feedback in feedback_data: # 获取正确标签 correct_label = feedback.get("correct_label") if not correct_label or correct_label == "unknown": continue # 获取原始图像和人脸位置 image_path = feedback.get("image_path", "") if not image_path or not os.path.exists(image_path): print(f"图像路径无效: {image_path}") continue box = feedback.get("box", []) if len(box) != 4: print(f"无效的人脸框: {box}") continue # 处理图像 image = cv2.imread(image_path) if image is None: print(f"无法读取图像: {image_path}") continue # 裁剪人脸区域 x1, y1, x2, y2 = map(int, box) face_img = image[y1:y2, x1:x2] if face_img.size == 0: print(f"裁剪后的人脸图像为空: {image_path}") continue # 提取特征 embedding = self.extract_features(face_img) if embedding is None: print(f"无法提取特征: {image_path}") continue # 添加到训练数据 feedback_features.append(embedding) feedback_labels.append(correct_label) print(f"添加反馈数据: {correct_label} - {image_path}") # 检查是否有有效的反馈数据 if not feedback_features: print("错误:没有有效的反馈数据") return False # 合并数据 all_features = np.vstack([original_features, feedback_features]) all_labels = original_labels + feedback_labels # 重新训练分类器 self.classifier = SVC(kernel='linear', probability=True) self.classifier.fit(all_features, all_labels) # 更新标签编码器 self.label_encoder = LabelEncoder() self.label_encoder.fit(all_labels) # 更新寝室成员列表 self.dorm_members = list(self.label_encoder.classes_) # 更新训练数据 self.training_data = {} for label, feature in zip(all_labels, all_features): if label not in self.training_data: self.training_data[label] = [] self.training_data[label].append(feature) print(f"重新训练完成! 新模型包含 {len(self.dorm_members)} 个成员") return True def recognize(self, image, threshold=0.7): """识别人脸""" if not self.model_loaded or not self.models_initialized: return [], image.copy() # 使用ArcFace检测人脸 rgb_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) faces = self.arcface_model.get(rgb_img) results = [] display_img = image.copy() if faces: for face in faces: # 获取人脸框 x1, y1, x2, y2 = face.bbox.astype(int) # 提取特征 embedding = face.embedding # 预测 probabilities = self.classifier.predict_proba([embedding])[0] max_prob = np.max(probabilities) pred_class = self.classifier.predict([embedding])[0] pred_label = self.label_encoder.inverse_transform([pred_class])[0] # 判断是否为陌生人 if max_prob < threshold or pred_label == 'stranger': label = "陌生人" color = (0, 0, 255) # 红色 else: label = pred_label color = (0, 255, 0) # 绿色 # 保存结果 results.append({ "box": [x1, y1, x2, y2], "label": label, "confidence": max_prob }) # 在图像上绘制结果 cv2.rectangle(display_img, (x1, y1), (x2, y2), color, 2) cv2.putText(display_img, f"{label} ({max_prob:.2f})", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) return results, display_img def save_feedback(self, image, detected_box, incorrect_label, correct_label): """保存用户反馈数据 - 改进为保存图像路径而非完整图像""" feedback_dir = "data/feedback_data" os.makedirs(feedback_dir, exist_ok=True) # 创建唯一文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 保存人脸图像 face_img_dir = os.path.join(feedback_dir, "faces") os.makedirs(face_img_dir, exist_ok=True) face_img_path = os.path.join(face_img_dir, f"face_{timestamp}.jpg") # 裁剪并保存人脸区域 x1, y1, x2, y2 = map(int, detected_box) # 修复1:确保裁剪区域有效 if y2 > y1 and x2 > x1: face_img = image[y1:y2, x1:x2] if face_img.size > 0: cv2.imwrite(face_img_path, face_img) else: logger.warning(f"裁剪的人脸区域无效: {detected_box}") face_img_path = None else: logger.warning(f"无效的检测框: {detected_box}") face_img_path = None # 保存反馈元数据 filename = f"feedback_{timestamp}.json" # 修复2:使用JSON格式 filepath = os.path.join(feedback_dir, filename) # 准备数据 feedback_data = { "image_path": face_img_path, # 保存路径而非完整图像 "detected_box": detected_box, "incorrect_label": incorrect_label, "correct_label": correct_label, "timestamp": timestamp } # 修复3:使用JSON保存便于阅读和调试 with open(filepath, 'w', encoding='utf-8') as f: json.dump(feedback_data, f, ensure_ascii=False, indent=2) return True def save_updated_model(self, output_path): """保存更新后的模型""" model_data = { 'classifier': self.classifier, 'label_encoder': self.label_encoder, 'dorm_members': self.dorm_members, 'training_data': self.training_data # 包含训练数据 } joblib.dump(model_data, output_path) print(f"更新后的模型已保存到: {output_path}") class TripletFaceDataset(Dataset): “”“三元组人脸数据集”“” def __init__(self, embeddings, labels): self.embeddings = embeddings self.labels = labels self.label_to_indices = {} # 创建标签到索引的映射 for idx, label in enumerate(labels): if label not in self.label_to_indices: self.label_to_indices[label] = [] self.label_to_indices[label].append(idx) def __getitem__(self, index): anchor_label = self.labels[index] # 随机选择正样本 positive_idx = index while positive_idx == index: positive_idx = random.choice(self.label_to_indices[anchor_label]) # 随机选择负样本 negative_label = random.choice([l for l in set(self.labels) if l != anchor_label]) negative_idx = random.choice(self.label_to_indices[negative_label]) return ( self.embeddings[index], self.embeddings[positive_idx], self.embeddings[negative_idx] ) def __len__(self): return len(self.embeddings) class TripletLoss(nn.Module): “”“三元组损失函数”“” def __init__(self, margin=1.0): super(TripletLoss, self).__init__() self.margin = margin def forward(self, anchor, positive, negative): distance_positive = (anchor - positive).pow(2).sum(1) distance_negative = (anchor - negative).pow(2).sum(1) losses = torch.relu(distance_positive - distance_negative + self.margin) return losses.mean() def train_triplet_model(embeddings, labels, epochs=100): “”“训练三元组模型”“” dataset = TripletFaceDataset(embeddings, labels) dataloader = DataLoader(dataset, batch_size=32, shuffle=True) model = nn.Sequential( nn.Linear(embeddings.shape[1], 256), nn.ReLU(), nn.Linear(256, 128) ) criterion = TripletLoss(margin=0.5) optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(epochs): total_loss = 0.0 for anchor, positive, negative in dataloader: optimizer.zero_grad() anchor_embed = model(anchor) positive_embed = model(positive) negative_embed = model(negative) loss = criterion(anchor_embed, positive_embed, negative_embed) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch + 1}/{epochs}, Loss: {total_loss / len(dataloader):.4f}") return model main.py代码如下:import sys from dorm_face_recognition_gui import FaceRecognitionSystem from PyQt5.QtWidgets import QApplication if name == “main”: # 设置中文编码支持 if sys.platform == “win32”: import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(“dorm.face.recognition”) app = QApplication(sys.argv) app.setStyle("Fusion") # 使用Fusion风格 # 设置应用样式 app.setStyleSheet(""" QMainWindow { background-color: #ecf0f1; } QGroupBox { border: 1px solid #bdc3c7; border-radius: 8px; margin-top: 20px; padding: 10px; font-weight: bold; background-color: #ffffff; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 0 5px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 10px 15px; font-size: 14px; margin: 5px; border-radius: 5px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #1c6ea4; } QPushButton:disabled { background-color: #bdc3c7; } QLabel { font-size: 14px; padding: 3px; } QComboBox, QSlider { padding: 4px; background-color: #ffffff; } QTextEdit { font-family: "Microsoft YaHei"; font-size: 12px; } """) window = FaceRecognitionSystem() window.show() sys.exit(app.exec_()) ui.py代码如下:from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QComboBox, QSlider, QMessageBox, QTextEdit, QGroupBox, QScrollArea, QDialog, QListWidget) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont from face_recognition import FaceRecognition class FaceRecognitionSystem(QMainWindow): def init(self): super().init() # … 原有初始化代码 … # 初始化人脸识别器 self.face_recognition = FaceRecognition() # 添加反馈按钮 self.add_feedback_button() def add_feedback_button(self): """添加反馈按钮到界面""" self.feedback_btn = QPushButton("反馈识别错误") self.feedback_btn.setIcon(QIcon.fromTheme("dialog-warning")) self.feedback_btn.setStyleSheet("background-color: #f39c12;") self.feedback_btn.clicked.connect(self.handle_feedback) # 找到识别功能组并添加按钮 for i in range(self.control_layout.count()): widget = self.control_layout.itemAt(i).widget() if isinstance(widget, QGroupBox) and widget.title() == "识别功能": layout = widget.layout() layout.addWidget(self.feedback_btn) break def handle_feedback(self): """处理用户反馈""" if not hasattr(self, 'last_results') or not self.last_results: QMessageBox.warning(self, "警告", "没有可反馈的识别结果") return # 创建反馈对话框 dialog = QDialog(self) dialog.setWindowTitle("识别错误反馈") dialog.setFixedSize(400, 300) layout = QVBoxLayout(dialog) # 添加当前识别结果 result_label = QLabel("当前识别结果:") layout.addWidget(result_label) self.feedback_list = QListWidget() for i, result in enumerate(self.last_results, 1): label = result["label"] confidence = result["confidence"] self.feedback_list.addItem(f"人脸 #{i}: {label} (置信度: {confidence:.2f})") layout.addWidget(self.feedback_list) # 添加正确身份选择 correct_label = QLabel("正确身份:") layout.addWidget(correct_label) self.correct_combo = QComboBox() self.correct_combo.addItems(["选择正确身份"] + self.face_recognition.dorm_members + ["陌生人", "不在列表中"]) layout.addWidget(self.correct_combo) # 添加按钮 btn_layout = QHBoxLayout() submit_btn = QPushButton("提交反馈") submit_btn.clicked.connect(lambda: self.submit_feedback(dialog)) btn_layout.addWidget(submit_btn) cancel_btn = QPushButton("取消") cancel_btn.clicked.connect(dialog.reject) btn_layout.addWidget(cancel_btn) layout.addLayout(btn_layout) dialog.exec_() def submit_feedback(self, dialog): """提交反馈并更新模型""" selected_index = self.feedback_list.currentRow() if selected_index < 0: QMessageBox.warning(self, "警告", "请选择一个识别结果") return result = self.last_results[selected_index] correct_identity = self.correct_combo.currentText() if correct_identity == "选择正确身份": QMessageBox.warning(self, "警告", "请选择正确身份") return # 保存反馈数据 self.face_recognition.save_feedback( self.current_image.copy(), result["box"], result["label"], correct_identity ) QMessageBox.information(self, "反馈提交", "感谢您的反馈!数据已保存用于改进模型") dialog.accept() def recognize_faces(self, image): """识别人脸并在图像上标注结果""" # 使用人脸识别器进行识别 self.last_results, display_image = self.face_recognition.recognize( image, threshold=self.threshold_slider.value() / 100 ) # 更新结果文本 self.update_results_text() # 显示图像 self.display_image(display_image) def update_results_text(self): """更新结果文本区域""" if not self.last_results: self.results_text.setText("未识别到任何人脸") return # 构建结果文本 result_text = "<h3>识别结果:</h3>" for i, result in enumerate(self.last_results, 1): x1, y1, x2, y2 = result["box"] label = result["label"] confidence = result["confidence"] # 处理中文显示问题 if label in self.face_recognition.dorm_members: result_text += ( f"<p><b>人脸 #{i}:</b> " f"<span style='color:green;'>寝室成员 - {label}</span><br>" f"位置: ({x1}, {y1}), 置信度: {confidence:.2f}</p>" ) else: result_text += ( f"<p><b>人脸 #{i}:</b> " f"<span style='color:red;'>陌生人</span><br>" f"位置: ({x1}, {y1}), 置信度: {confidence:.2f}</p>" ) self.results_text.setHtml(result_text) # ... 其余原有方法 ... 需要把重新训练模型部分和反馈部分全部删除
06-26
这个程序里的Q_ARG一直是报错的情况该怎么解决 # -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np import math import time import logging import threading from collections import deque from PyQt5.QtWidgets import ( QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBox, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog, QLineEdit, QRadioButton, QGridLayout, QSpinBox, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QProgressBar ) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal, QTimer, QMetaObject, pyqtSlot from PyQt5.QtGui import QImage, QPixmap from CamOperation_class import CameraOperation from MvCameraControl_class import * import ctypes from ctypes import cast, POINTER from datetime import datetime import skimage import platform from CameraParams_header import ( MV_GIGE_DEVICE, MV_USB_DEVICE, MV_GENTL_CAMERALINK_DEVICE, MV_GENTL_CXP_DEVICE, MV_GENTL_XOF_DEVICE ) # ===== 全局配置 ===== # 模板匹配参数 MATCH_THRESHOLD = 0.75 # 降低匹配置信度阈值以提高灵敏度 MIN_MATCH_COUNT = 10 # 最小匹配特征点数量 MIN_FRAME_INTERVAL = 0.1 # 最小检测间隔(秒) # ===== 全局变量 ===== current_sample_path = "" detection_history = [] isGrabbing = False isOpen = False obj_cam_operation = None frame_monitor_thread = None template_matcher_thread = None MV_OK = 0 MV_E_CALLORDER = -2147483647 # ==================== 优化后的质量检测算法 ==================== def enhanced_check_print_quality(sample_image_path, test_image, threshold=0.05): # 不再使用传感器数据调整阈值 adjusted_threshold = threshold try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_GRAYSCALE) if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None if len(test_image.shape) == 3: test_image_gray = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) else: test_image_gray = test_image.copy() sample_image = cv2.GaussianBlur(sample_image, (5, 5), 0) test_image_gray = cv2.GaussianBlur(test_image_gray, (5, 5), 0) try: # 使用更鲁棒的SIFT特征检测器 sift = cv2.SIFT_create() keypoints1, descriptors1 = sift.detectAndCompute(sample_image, None) keypoints2, descriptors2 = sift.detectAndCompute(test_image_gray, None) if descriptors1 is None or descriptors2 is None: logging.warning("无法提取特征描述符,跳过配准") aligned_sample = sample_image else: # 使用FLANN匹配器提高匹配精度 FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) matches = flann.knnMatch(descriptors1, descriptors2, k=2) # 应用Lowe's比率测试筛选优质匹配 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) if len(good_matches) > MIN_MATCH_COUNT: src_pts = np.float32([keypoints1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([keypoints2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) if H is not None: aligned_sample = cv2.warpPerspective( sample_image, H, (test_image_gray.shape[1], test_image_gray.shape[0]) ) logging.info("图像配准成功,使用配准后样本") else: aligned_sample = sample_image logging.warning("无法计算单应性矩阵,使用原始样本") else: aligned_sample = sample_image logging.warning(f"特征点匹配不足({len(good_matches)}/{MIN_MATCH_COUNT}),跳过图像配准") except Exception as e: logging.error(f"图像配准失败: {str(e)}") aligned_sample = sample_image try: if aligned_sample.shape != test_image_gray.shape: test_image_gray = cv2.resize(test_image_gray, (aligned_sample.shape[1], aligned_sample.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None try: from skimage.metrics import structural_similarity as compare_ssim ssim_score, ssim_diff = compare_ssim( aligned_sample, test_image_gray, full=True, gaussian_weights=True, data_range=255 ) except ImportError: from skimage.measure import compare_ssim ssim_score, ssim_diff = compare_ssim( aligned_sample, test_image_gray, full=True, gaussian_weights=True ) except Exception as e: logging.error(f"SSIM计算失败: {str(e)}") abs_diff = cv2.absdiff(aligned_sample, test_image_gray) ssim_diff = abs_diff.astype(np.float32) / 255.0 ssim_score = 1.0 - np.mean(ssim_diff) ssim_diff = (1 - ssim_diff) * 255 abs_diff = cv2.absdiff(aligned_sample, test_image_gray) combined_diff = cv2.addWeighted(ssim_diff.astype(np.uint8), 0.7, abs_diff, 0.3, 0) _, thresholded = cv2.threshold(combined_diff, 30, 255, cv2.THRESH_BINARY) kernel = np.ones((3, 3), np.uint8) thresholded = cv2.morphologyEx(thresholded, cv2.MORPH_OPEN, kernel) thresholded = cv2.morphologyEx(thresholded, cv2.MORPH_CLOSE, kernel) diff_pixels = np.count_nonzero(thresholded) total_pixels = aligned_sample.size diff_ratio = diff_pixels / total_pixels is_qualified = diff_ratio <= adjusted_threshold marked_image = cv2.cvtColor(test_image_gray, cv2.COLOR_GRAY2BGR) marked_image[thresholded == 255] = [0, 0, 255] # 放大缺陷标记 scale_factor = 2.0 # 放大2倍 marked_image = cv2.resize(marked_image, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_LINEAR) labels = skimage.measure.label(thresholded) properties = skimage.measure.regionprops(labels) for prop in properties: if prop.area > 50: y, x = prop.centroid # 根据放大比例调整坐标 x_scaled = int(x * scale_factor) y_scaled = int(y * scale_factor) cv2.putText(marked_image, f"Defect", (x_scaled, y_scaled), cv2.FONT_HERSHEY_SIMPLEX, 0.5 * scale_factor, (0, 255, 255), int(scale_factor)) return is_qualified, diff_ratio, marked_image # ==================== 视觉触发的质量检测流程 ==================== def vision_controlled_check(capture_image=None, match_score=0.0): """修改为接受图像帧和匹配分数""" global current_sample_path, detection_history logging.info("视觉触发质量检测启动") # 如果没有提供图像,使用当前帧 if capture_image is None: frame = obj_cam_operation.get_current_frame() else: frame = capture_image if frame is None: QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: diff_threshold = mainWindow.sliderDiffThreshold.value() / 100.0 logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(30) is_qualified, diff_ratio, marked_image = enhanced_check_print_quality( current_sample_path, frame, threshold=diff_threshold ) progress.setValue(70) if is_qualified is None: QMessageBox.critical(mainWindow, "检测错误", "检测失败,请检查日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}") progress.setValue(90) update_diff_display(diff_ratio, is_qualified) result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n差异占比: {diff_ratio*100:.2f}%\n阈值: {diff_threshold*100:.2f}%" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: # 创建可调整大小的窗口 cv2.namedWindow("缺陷标记结果", cv2.WINDOW_NORMAL) cv2.resizeWindow("缺陷标记结果", 800, 600) # 初始大小 cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'threshold': diff_threshold, 'trigger_type': 'vision' if capture_image else 'manual' } 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 open_device(): global deviceList, nSelCamIndex, obj_cam_operation, isOpen, frame_monitor_thread, mainWindow if isOpen: QMessageBox.warning(mainWindow, "Error", '相机已打开!', QMessageBox.Ok) return MV_E_CALLORDER nSelCamIndex = mainWindow.ComboDevices.currentIndex() if nSelCamIndex < 0: QMessageBox.warning(mainWindow, "Error", '请选择相机!', QMessageBox.Ok) return MV_E_CALLORDER # 创建相机控制对象 cam = MvCamera() # 初始化相机操作对象 obj_cam_operation = CameraOperation(cam, deviceList, nSelCamIndex) ret = obj_cam_operation.open_device() if 0 != ret: strError = "打开设备失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) isOpen = False else: set_continue_mode() get_param() isOpen = True enable_controls() # 创建并启动帧监控线程 frame_monitor_thread = FrameMonitorThread(obj_cam_operation) frame_monitor_thread.frame_status.connect(mainWindow.statusBar().showMessage) frame_monitor_thread.start() def start_grabbing(): global obj_cam_operation, isGrabbing, template_matcher_thread ret = obj_cam_operation.start_grabbing(mainWindow.widgetDisplay.winId()) if ret != 0: strError = "开始取流失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = True enable_controls() # 等待第一帧到达 QThread.msleep(500) if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "警告", "开始取流后未接收到帧,请检查相机连接!", QMessageBox.Ok) # 如果启用了自动检测,启动检测线程 if mainWindow.chkContinuousMatch.isChecked(): toggle_template_matching(True) def stop_grabbing(): global obj_cam_operation, isGrabbing, template_matcher_thread ret = obj_cam_operation.Stop_grabbing() if ret != 0: strError = "停止取流失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = False enable_controls() # 停止模板匹配线程 if template_matcher_thread and template_matcher_thread.isRunning(): template_matcher_thread.stop() def close_device(): global isOpen, isGrabbing, obj_cam_operation, frame_monitor_thread, template_matcher_thread if frame_monitor_thread and frame_monitor_thread.isRunning(): frame_monitor_thread.stop() frame_monitor_thread.wait(2000) # 停止模板匹配线程 if template_matcher_thread and template_matcher_thread.isRunning(): template_matcher_thread.stop() template_matcher_thread.wait(2000) template_matcher_thread = None if isOpen and obj_cam_operation: obj_cam_operation.close_device() isOpen = False isGrabbing = False enable_controls() # ==================== 连续帧匹配检测器 ==================== class ContinuousFrameMatcher(QThread): frame_processed = pyqtSignal(np.ndarray, float, bool) # 处理后的帧, 匹配分数, 是否匹配 match_score_updated = pyqtSignal(float) # 匹配分数更新信号 match_success = pyqtSignal(np.ndarray, float) # 匹配成功信号 (帧, 匹配分数) def __init__(self, cam_operation, parent=None): super().__init__(parent) self.cam_operation = cam_operation self.running = True self.sample_template = None self.min_match_count = MIN_MATCH_COUNT self.match_threshold = MATCH_THRESHOLD self.sample_kp = None self.sample_des = None self.current_match_score = 0.0 self.last_match_time = 0 self.frame_counter = 0 self.consecutive_fail_count = 0 self.last_trigger_time = 0 # 上次触发时间 self.cool_down = 0.2 # 冷却时间(秒) # 特征检测器 - 使用SIFT self.sift = cv2.SIFT_create() # 特征匹配器 - 使用FLANN提高匹配精度 FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) self.flann = cv2.FlannBasedMatcher(index_params, search_params) # 性能监控 self.processing_times = deque(maxlen=100) self.frame_rates = deque(maxlen=100) # 黑白相机优化 self.clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) def preprocess_image(self, image): """增强黑白图像特征提取""" # 如果是单通道图像,转换为三通道 if len(image.shape) == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # 对比度增强 (LAB空间) lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) cl = self.clahe.apply(l) limg = cv2.merge((cl, a, b)) enhanced = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR) # 边缘增强 gray = cv2.cvtColor(enhanced, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) # 组合特征 return cv2.addWeighted(enhanced, 0.7, cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR), 0.3, 0) def set_sample(self, sample_img): """设置标准样本并提取特征""" # 保存样本图像 self.sample_img = sample_img # 预处理增强特征 processed_sample = self.preprocess_image(sample_img) # 提取样本特征点 self.sample_kp, self.sample_des = self.sift.detectAndCompute(processed_sample, None) if self.sample_des is None or len(self.sample_kp) < self.min_match_count: logging.warning("样本特征点不足") return False logging.info(f"样本特征提取成功: {len(self.sample_kp)}个关键点") return True def process_frame(self, frame): """处理帧:特征提取、匹配和可视化""" is_matched = False match_score = 0.0 processed_frame = frame.copy() # 检查是否已设置样本 if self.sample_kp is None or self.sample_des is None: return processed_frame, match_score, is_matched # 预处理当前帧 processed_frame = self.preprocess_image(frame) # 转换为灰度图像用于特征提取 gray_frame = cv2.cvtColor(processed_frame, cv2.COLOR_BGR2GRAY) try: # 提取当前帧的特征点 kp, des = self.sift.detectAndCompute(gray_frame, None) if des is None or len(kp) < 10: # 特征点不足 return processed_frame, match_score, is_matched # 匹配特征点 matches = self.flann.knnMatch(self.sample_des, des, k=2) # 应用Lowe's比率测试 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) # 计算匹配分数(匹配点数量占样本特征点数量的比例) if len(self.sample_kp) > 0: match_score = len(good_matches) / len(self.sample_kp) match_score = min(1.0, max(0.0, match_score)) # 确保在0-1范围内 else: match_score = 0.0 # 判断是否匹配成功 if len(good_matches) >= self.min_match_count and match_score >= self.match_threshold: is_matched = True # 在图像上绘制匹配结果 if len(gray_frame.shape) == 2: processed_frame = cv2.cvtColor(gray_frame, cv2.COLOR_GRAY2BGR) # 绘制匹配点 processed_frame = cv2.drawMatches( self.sample_img, self.sample_kp, processed_frame, kp, good_matches, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) # 在图像上显示匹配分数 cv2.putText(processed_frame, f"Match Score: {match_score:.2f}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # 更新当前匹配分数 self.current_match_score = match_score self.match_score_updated.emit(match_score) # 检查是否匹配成功且超过冷却时间 current_time = time.time() if is_matched and (current_time - self.last_trigger_time) > self.cool_down: self.last_trigger_time = current_time logging.info(f"匹配成功! 分数: {match_score:.2f}, 触发质量检测") # 发出匹配成功信号 (传递当前帧) self.match_success.emit(frame.copy(), match_score) except Exception as e: logging.error(f"帧处理错误: {str(e)}") return processed_frame, match_score, is_matched def set_threshold(self, threshold): """更新匹配阈值""" self.match_threshold = max(0.0, min(1.0, threshold)) logging.info(f"更新匹配阈值: {self.match_threshold:.2f}") def run(self): """主处理循环 - 连续处理每一帧""" logging.info("连续帧匹配线程启动") self.last_match_time = time.time() self.consecutive_fail_count = 0 while self.running: start_time = time.time() # 检查相机状态 if not self.cam_operation or not self.cam_operation.is_grabbing: if self.consecutive_fail_count % 10 == 0: logging.debug("相机未取流,等待...") time.sleep(0.1) self.consecutive_fail_count += 1 continue # 获取当前帧 frame = self.cam_operation.get_current_frame() if frame is None: self.consecutive_fail_count += 1 if self.consecutive_fail_count % 10 == 0: logging.warning(f"连续{self.consecutive_fail_count}次获取帧失败") time.sleep(0.05) continue self.consecutive_fail_count = 0 try: # 处理帧 processed_frame, match_score, is_matched = self.process_frame(frame) # 发送处理结果 self.frame_processed.emit(processed_frame, match_score, is_matched) except Exception as e: logging.error(f"帧处理错误: {str(e)}") # 控制处理频率 processing_time = time.time() - start_time sleep_time = max(0.01, MIN_FRAME_INTERVAL - processing_time) time.sleep(sleep_time) logging.info("连续帧匹配线程退出") # ==================== 模板匹配控制函数 ==================== def toggle_template_matching(state): global template_matcher_thread, current_sample_path logging.debug(f"切换连续匹配状态: {state}") if state == Qt.Checked and isGrabbing: # 确保已设置样本 if not current_sample_path: logging.warning("尝试启动连续匹配但未设置样本") QMessageBox.warning(mainWindow, "错误", "请先设置标准样本", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if template_matcher_thread is None: logging.info("创建新的连续帧匹配线程") template_matcher_thread = ContinuousFrameMatcher(obj_cam_operation) template_matcher_thread.frame_processed.connect(update_frame_display) template_matcher_thread.match_score_updated.connect(update_match_score_display) # 正确连接匹配成功信号到质量检测函数 template_matcher_thread.match_success.connect( lambda frame, score: vision_controlled_check(frame, score) ) # 加载样本图像 sample_img = cv2.imread(current_sample_path) if sample_img is None: logging.error("无法加载标准样本图像") QMessageBox.warning(mainWindow, "错误", "无法加载标准样本图像", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if not template_matcher_thread.set_sample(sample_img): logging.warning("标准样本特征不足") QMessageBox.warning(mainWindow, "错误", "标准样本特征不足", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if not template_matcher_thread.isRunning(): logging.info("启动连续帧匹配线程") template_matcher_thread.start() elif template_matcher_thread and template_matcher_thread.isRunning(): logging.info("停止连续帧匹配线程") template_matcher_thread.stop() # 重置匹配分数显示 update_match_score_display(0.0) # 重置帧显示 if obj_cam_operation and obj_cam_operation.is_frame_available(): frame = obj_cam_operation.get_current_frame() if frame is not None: display_frame = frame.copy() # 添加状态信息 cv2.putText(display_frame, "Continuous Matching Disabled", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) update_frame_display(display_frame, 0.0, False) def update_frame_display(frame, match_score, is_matched): """更新主显示窗口(线程安全)""" # 确保在GUI线程中执行 if QThread.currentThread() != QApplication.instance().thread(): QMetaObject.invokeMethod( mainWindow, "updateDisplay", Qt.QueuedConnection, Q_ARG(np.ndarray, frame), Q_ARG(float, match_score), Q_ARG(bool, is_matched) ) return # 如果已经在主线程,直接调用主窗口的更新方法 mainWindow.updateDisplay(frame, match_score, is_matched) def update_match_score_display(score): """更新匹配分数显示""" # 将分数转换为百分比显示 score_percent = score * 100 mainWindow.lblMatchScoreValue.setText(f"{score_percent:.1f}%") # 根据分数设置颜色 if score > 0.8: # 高于80%显示绿色 color = "green" elif score > 0.6: # 60%-80%显示黄色 color = "orange" else: # 低于60%显示红色 color = "red" mainWindow.lblMatchScoreValue.setStyleSheet(f"color: {color}; font-weight: bold;") def update_diff_display(diff_ratio, is_qualified): mainWindow.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") if is_qualified: mainWindow.lblDiffStatus.setText("状态: 合格") mainWindow.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: mainWindow.lblDiffStatus.setText("状态: 不合格") mainWindow.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") def update_diff_threshold(value): mainWindow.lblDiffValue.setText(f"{value}%") def update_sample_display(): global current_sample_path if current_sample_path: mainWindow.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") mainWindow.lblSamplePath.setToolTip(current_sample_path) mainWindow.bnPreviewSample.setEnabled(True) else: mainWindow.lblSamplePath.setText("当前样本: 未设置样本") mainWindow.bnPreviewSample.setEnabled(False) def update_history_display(): global detection_history mainWindow.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" trigger = "视觉" if result['trigger_type'] == 'vision' else "手动" mainWindow.cbHistory.addItem(f"[{trigger} {timestamp}] {status} - 差异: {ratio}") def update_match_threshold(value): """更新匹配阈值显示并应用到匹配器""" global template_matcher_thread # 更新UI显示 if mainWindow: mainWindow.lblThresholdValue.setText(f"{value}%") # 如果匹配线程存在,更新其匹配阈值 if template_matcher_thread: # 转换为0-1范围的浮点数 threshold = value / 100.0 template_matcher_thread.set_threshold(threshold) logging.debug(f"更新匹配阈值: {threshold:.2f}") # ==================== 主窗口类 ==================== class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("布料印花检测系统 - 连续匹配版") self.resize(1200, 800) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 设备枚举区域 device_layout = QHBoxLayout() self.ComboDevices = QComboBox() self.bnEnum = QPushButton("枚举设备") self.bnOpen = QPushButton("打开设备") self.bnClose = QPushButton("关闭设备") device_layout.addWidget(self.ComboDevices) device_layout.addWidget(self.bnEnum) device_layout.addWidget(self.bnOpen) device_layout.addWidget(self.bnClose) main_layout.addLayout(device_layout) # 取流控制组 self.groupGrab = QGroupBox("取流控制") grab_layout = QHBoxLayout(self.groupGrab) self.bnStart = QPushButton("开始取流") self.bnStop = QPushButton("停止取流") self.radioContinueMode = QRadioButton("连续模式") self.radioTriggerMode = QRadioButton("触发模式") self.bnSoftwareTrigger = QPushButton("软触发") grab_layout.addWidget(self.bnStart) grab_layout.addWidget(self.bnStop) grab_layout.addWidget(self.radioContinueMode) grab_layout.addWidget(self.radioTriggerMode) grab_layout.addWidget(self.bnSoftwareTrigger) main_layout.addWidget(self.groupGrab) # 参数设置组 self.paramgroup = QGroupBox("相机参数") param_layout = QGridLayout(self.paramgroup) self.edtExposureTime = QLineEdit() self.edtGain = QLineEdit() self.edtFrameRate = QLineEdit() self.bnGetParam = QPushButton("获取参数") self.bnSetParam = QPushButton("设置参数") self.bnSaveImage = QPushButton("保存图像") param_layout.addWidget(QLabel("曝光时间:"), 0, 0) param_layout.addWidget(self.edtExposureTime, 0, 1) param_layout.addWidget(self.bnGetParam, 0, 2) param_layout.addWidget(QLabel("增益:"), 1, 0) param_layout.addWidget(self.edtGain, 1, 1) param_layout.addWidget(self.bnSetParam, 1, 2) param_layout.addWidget(QLabel("帧率:"), 2, 0) param_layout.addWidget(self.edtFrameRate, 2, 1) param_layout.addWidget(self.bnSaveImage, 2, 2) main_layout.addWidget(self.paramgroup) # 图像显示区域 self.widgetDisplay = QLabel() self.widgetDisplay.setMinimumSize(640, 480) self.widgetDisplay.setStyleSheet("background-color: black;") self.widgetDisplay.setAlignment(Qt.AlignCenter) self.widgetDisplay.setText("相机预览区域") main_layout.addWidget(self.widgetDisplay, 1) # 创建自定义UI组件 self.setup_custom_ui() # 添加阈值自适应定时器 self.threshold_timer = QTimer() self.threshold_timer.timeout.connect(self.auto_adjust_threshold) self.threshold_timer.start(2000) # 每2秒调整一次 def auto_adjust_threshold(self): """根据环境亮度自动调整匹配阈值""" if not obj_cam_operation or not isGrabbing: return # 获取当前帧并计算平均亮度 frame = obj_cam_operation.get_current_frame() if frame is None: return gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) brightness = np.mean(gray) # 根据亮度动态调整阈值 (亮度低时降低阈值要求) if brightness < 50: # 暗环境 new_threshold = 40 # 40% elif brightness > 200: # 亮环境 new_threshold = 65 # 65% else: # 正常环境 new_threshold = 55 # 55% # 更新UI self.sliderThreshold.setValue(new_threshold) self.lblThresholdValue.setText(f"{new_threshold}%") # 更新匹配器阈值 update_match_threshold(new_threshold) # 状态显示调整信息 self.statusBar().showMessage(f"亮度: {brightness:.1f}, 自动调整阈值至: {new_threshold}%", 3000) def setup_custom_ui(self): # 工具栏 toolbar = self.addToolBar("检测工具") self.bnCheckPrint = QPushButton("手动检测") self.bnSaveSample = QPushButton("保存标准样本") self.bnPreviewSample = QPushButton("预览样本") self.cbHistory = QComboBox() self.cbHistory.setMinimumWidth(300) toolbar.addWidget(self.bnCheckPrint) toolbar.addWidget(self.bnSaveSample) toolbar.addWidget(self.bnPreviewSample) toolbar.addWidget(QLabel("历史记录:")) toolbar.addWidget(self.cbHistory) # 状态栏样本路径 self.lblSamplePath = QLabel("当前样本: 未设置样本") self.statusBar().addPermanentWidget(self.lblSamplePath) # 右侧面板 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 差异度调整组 diff_group = QGroupBox("差异度调整") diff_layout = QVBoxLayout(diff_group) self.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") self.sliderDiffThreshold = QSlider(Qt.Horizontal) self.sliderDiffThreshold.setRange(0, 100) self.sliderDiffThreshold.setValue(5) self.lblDiffValue = QLabel("5%") self.lblCurrentDiff = QLabel("当前差异度: -") self.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") self.lblDiffStatus = QLabel("状态: 未检测") self.lblDiffStatus.setStyleSheet("font-size: 12px;") diff_layout.addWidget(self.lblDiffThreshold) diff_layout.addWidget(self.sliderDiffThreshold) diff_layout.addWidget(self.lblDiffValue) diff_layout.addWidget(self.lblCurrentDiff) diff_layout.addWidget(self.lblDiffStatus) right_layout.addWidget(diff_group) # ===== 连续匹配面板 ===== match_group = QGroupBox("连续帧匹配") match_layout = QVBoxLayout(match_group) # 样本设置 sample_layout = QHBoxLayout() self.bnSetSample = QPushButton("设置标准样本") self.bnPreviewSample = QPushButton("预览样本") self.lblSampleStatus = QLabel("状态: 未设置样本") sample_layout.addWidget(self.bnSetSample) sample_layout.addWidget(self.bnPreviewSample) sample_layout.addWidget(self.lblSampleStatus) match_layout.addLayout(sample_layout) # 匹配参数 param_layout = QHBoxLayout() self.lblMatchThreshold = QLabel("匹配阈值:") self.sliderThreshold = QSlider(Qt.Horizontal) self.sliderThreshold.setRange(50, 100) self.sliderThreshold.setValue(75) # 降低默认阈值 self.lblThresholdValue = QLabel("75%") param_layout.addWidget(self.lblMatchThreshold) param_layout.addWidget(self.sliderThreshold) param_layout.addWidget(self.lblThresholdValue) match_layout.addLayout(param_layout) # 匹配分数显示 match_score_layout = QHBoxLayout() self.lblMatchScore = QLabel("实时匹配分数:") self.lblMatchScoreValue = QLabel("0.0%") self.lblMatchScoreValue.setStyleSheet("font-weight: bold;") match_score_layout.addWidget(self.lblMatchScore) match_score_layout.addWidget(self.lblMatchScoreValue) match_layout.addLayout(match_score_layout) # 连续匹配开关 self.chkContinuousMatch = QCheckBox("启用连续帧匹配") self.chkContinuousMatch.setChecked(False) match_layout.addWidget(self.chkContinuousMatch) right_layout.addWidget(match_group) right_layout.addStretch(1) # 停靠窗口 dock = QDockWidget("检测控制面板", self) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.addDockWidget(Qt.RightDockWidgetArea, dock) @pyqtSlot(np.ndarray, float, bool) def updateDisplay(self, frame, match_score, is_matched): """线程安全的显示更新方法(添加可视化仪表盘)""" # 创建可视化覆盖层 overlay = frame.copy() height, width = frame.shape[:2] # 绘制匹配度仪表盘 center_x, center_y = width - 100, 100 radius = 80 cv2.circle(overlay, (center_x, center_y), radius, (100, 100, 100), 3) # 绘制指针 (根据匹配度旋转) angle = 240 * match_score # 0-100% 对应 240度范围 end_x = center_x + int(radius * 0.8 * math.cos(math.radians(angle - 90))) end_y = center_y + int(radius * 0.8 * math.sin(math.radians(angle - 90))) cv2.line(overlay, (center_x, center_y), (end_x, end_y), (0, 200, 0), 3) # 添加文本标签 cv2.putText(overlay, f"匹配度: {match_score*100:.1f}%", (center_x - 70, center_y + radius + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0) if match_score > 0.8 else (0, 0, 255) if match_score < 0.6 else (0, 165, 255), 2) # 添加状态标签 status_text = "匹配成功!" if is_matched else "检测中..." cv2.putText(overlay, status_text, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0) if is_matched else (200, 200, 0), 2) # 将OpenCV图像转换为Qt图像 if len(overlay.shape) == 3: # 彩色图像 h, w, ch = overlay.shape bytes_per_line = ch * w q_img = QImage(overlay.data, w, h, bytes_per_line, QImage.Format_RGB888) q_img = q_img.rgbSwapped() # BGR -> RGB else: # 灰度图像 h, w = overlay.shape bytes_per_line = w q_img = QImage(overlay.data, w, h, bytes_per_line, QImage.Format_Grayscale8) # 创建QPixmap并缩放 pixmap = QPixmap.fromImage(q_img) scaled_pixmap = pixmap.scaled( self.widgetDisplay.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) # 更新显示 self.widgetDisplay.setPixmap(scaled_pixmap) self.widgetDisplay.setAlignment(Qt.AlignCenter) def closeEvent(self, event): logging.info("主窗口关闭,执行清理...") close_device() event.accept() # ===== 辅助函数 ===== def ToHexStr(num): if not isinstance(num, int): try: num = int(num) except: return f"<非整数:{type(num)}>" chaDic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'} hexStr = "" if num < 0: num = num + 2 ** 32 while num >= 16: digit = num % 16 hexStr = chaDic.get(digit, str(digit)) + hexStr num //= 16 hexStr = chaDic.get(num, str(num)) + hexStr return "0x" + hexStr def enum_devices(): global deviceList, obj_cam_operation n_layer_type = ( MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE ) # 创建设备列表 deviceList = MV_CC_DEVICE_INFO_LIST() # 枚举设备 ret = MvCamera.MV_CC_EnumDevices(n_layer_type, deviceList) if ret != MV_OK: error_msg = f"枚举设备失败! 错误码: 0x{ret:x}" logging.error(error_msg) QMessageBox.warning(mainWindow, "错误", error_msg, QMessageBox.Ok) return ret if deviceList.nDeviceNum == 0: QMessageBox.warning(mainWindow, "提示", "未找到任何设备", QMessageBox.Ok) return MV_OK logging.info(f"找到 {deviceList.nDeviceNum} 个设备") # 处理设备信息 devList = [] for i in range(deviceList.nDeviceNum): # 获取设备信息 mvcc_dev_info = ctypes.cast( deviceList.pDeviceInfo[i], ctypes.POINTER(MV_CC_DEVICE_INFO) ).contents # 根据设备类型提取信息 if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE: st_gige_info = mvcc_dev_info.SpecialInfo.stGigEInfo ip_addr = ( f"{(st_gige_info.nCurrentIp >> 24) & 0xFF}." f"{(st_gige_info.nCurrentIp >> 16) & 0xFF}." f"{(st_gige_info.nCurrentIp >> 8) & 0xFF}." f"{st_gige_info.nCurrentIp & 0xFF}" ) # 修复:将c_ubyte_Array_16转换为字节串再解码 user_defined_bytes = bytes(st_gige_info.chUserDefinedName) dev_name = f"GigE: {user_defined_bytes.decode('gbk', 'ignore')}" devList.append(f"[{i}] {dev_name} ({ip_addr})") elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE: st_usb_info = mvcc_dev_info.SpecialInfo.stUsb3VInfo serial = bytes(st_usb_info.chSerialNumber).decode('ascii', 'ignore').rstrip('\x00') # 修复:同样处理用户自定义名称 user_defined_bytes = bytes(st_usb_info.chUserDefinedName) dev_name = f"USB: {user_defined_bytes.decode('gbk', 'ignore')}" devList.append(f"[{i}] {dev_name} (SN: {serial})") else: devList.append(f"[{i}] 未知设备类型: {mvcc_dev_info.nTLayerType}") # 更新UI mainWindow.ComboDevices.clear() mainWindow.ComboDevices.addItems(devList) if devList: mainWindow.ComboDevices.setCurrentIndex(0) mainWindow.statusBar().showMessage(f"找到 {deviceList.nDeviceNum} 个设备", 3000) return MV_OK def set_continue_mode(): ret = obj_cam_operation.set_trigger_mode(False) if ret != 0: strError = "设置连续模式失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: mainWindow.radioContinueMode.setChecked(True) mainWindow.radioTriggerMode.setChecked(False) mainWindow.bnSoftwareTrigger.setEnabled(False) def set_software_trigger_mode(): ret = obj_cam_operation.set_trigger_mode(True) if ret != 0: strError = "设置触发模式失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: mainWindow.radioContinueMode.setChecked(False) mainWindow.radioTriggerMode.setChecked(True) mainWindow.bnSoftwareTrigger.setEnabled(isGrabbing) def trigger_once(): ret = obj_cam_operation.trigger_once() if ret != 0: strError = "软触发失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 尝试捕获当前帧 frame = obj_cam_operation.capture_frame() if frame is None: QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 确保图像有效 if frame.size == 0 or frame.shape[0] == 0 or frame.shape[1] == 0: 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: 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: file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 创建目录(如果不存在) directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) except OSError as e: QMessageBox.critical(mainWindow, "目录创建错误", f"无法创建目录 {directory}: {str(e)}", QMessageBox.Ok) return # 保存图像 try: # 使用OpenCV保存图像 if not cv2.imwrite(file_path, frame): raise Exception("OpenCV保存失败") # 更新状态 current_sample_path = file_path update_sample_display() settings.setValue("last_save_dir", os.path.dirname(file_path)) # 显示成功消息 QMessageBox.information(mainWindow, "成功", f"标准样本已保存至:\n{file_path}", QMessageBox.Ok) # 更新样本状态 mainWindow.lblSampleStatus.setText("状态: 样本已设置") mainWindow.lblSampleStatus.setStyleSheet("color: green;") except Exception as e: logging.error(f"保存图像失败: {str(e)}") QMessageBox.critical(mainWindow, "保存错误", f"保存图像时发生错误:\n{str(e)}", QMessageBox.Ok) 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: # 直接使用OpenCV加载图像 sample_img = cv2.imread(current_sample_path) if sample_img is None: raise Exception("无法加载图像") # 显示图像 cv2.namedWindow("标准样本预览", cv2.WINDOW_NORMAL) cv2.resizeWindow("标准样本预览", 800, 600) cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) def is_float(str): try: float(str) return True except ValueError: return False def get_param(): try: ret = obj_cam_operation.get_parameters() if ret != MV_OK: strError = "获取参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: mainWindow.edtExposureTime.setText("{0:.2f}".format(obj_cam_operation.exposure_time)) mainWindow.edtGain.setText("{0:.2f}".format(obj_cam_operation.gain)) mainWindow.edtFrameRate.setText("{0:.2f}".format(obj_cam_operation.frame_rate)) except Exception as e: error_msg = f"获取参数时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) def set_param(): frame_rate = mainWindow.edtFrameRate.text() exposure = mainWindow.edtExposureTime.text() gain = mainWindow.edtGain.text() if not (is_float(frame_rate) and is_float(exposure) and is_float(gain)): strError = "设置参数失败: 参数必须是有效的浮点数" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) return MV_E_PARAMETER try: ret = obj_cam_operation.set_param( frame_rate=float(frame_rate), exposure_time=float(exposure), gain=float(gain) ) if ret != MV_OK: strError = "设置参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) except Exception as e: error_msg = f"设置参数时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) def enable_controls(): global isGrabbing, isOpen mainWindow.groupGrab.setEnabled(isOpen) mainWindow.paramgroup.setEnabled(isOpen) mainWindow.bnOpen.setEnabled(not isOpen) mainWindow.bnClose.setEnabled(isOpen) mainWindow.bnStart.setEnabled(isOpen and (not isGrabbing)) mainWindow.bnStop.setEnabled(isOpen and isGrabbing) mainWindow.bnSoftwareTrigger.setEnabled(isGrabbing and mainWindow.radioTriggerMode.isChecked()) mainWindow.bnSaveImage.setEnabled(isOpen and isGrabbing) mainWindow.bnCheckPrint.setEnabled(isOpen and isGrabbing) mainWindow.bnSaveSample.setEnabled(isOpen and isGrabbing) mainWindow.bnPreviewSample.setEnabled(bool(current_sample_path)) # 连续匹配控制 mainWindow.chkContinuousMatch.setEnabled(bool(current_sample_path) and isGrabbing) # ===== 相机帧监控线程 ===== class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) # 用于发送状态消息的信号 def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True self.frame_count = 0 self.last_time = time.time() def run(self): """监控相机帧状态的主循环""" while self.running: try: if self.cam_operation and self.cam_operation.is_grabbing: # 获取帧统计信息 frame_info = self.get_frame_info() if frame_info: fps = frame_info.get('fps', 0) dropped = frame_info.get('dropped', 0) status = f"FPS: {fps:.1f} | 丢帧: {dropped}" self.frame_status.emit(status) else: self.frame_status.emit("取流中...") else: self.frame_status.emit("相机未取流") except Exception as e: self.frame_status.emit(f"监控错误: {str(e)}") # 每500ms检查一次 QThread.msleep(500) def stop(self): """停止监控线程""" self.running = False self.wait(1000) # 等待线程结束 def calculate_fps(self): """计算当前帧率""" current_time = time.time() elapsed = current_time - self.last_time if elapsed > 0: fps = self.frame_count / elapsed self.frame_count = 0 self.last_time = current_time return fps return 0 def get_frame_info(self): """获取帧信息""" try: # 更新帧计数 self.frame_count += 1 # 返回帧信息 return { 'fps': self.calculate_fps(), 'dropped': 0 # 实际应用中需要从相机获取真实丢帧数 } except Exception as e: logging.error(f"获取帧信息失败: {str(e)}") return None # ===== 主程序入口 ===== if __name__ == "__main__": # 配置日志系统 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("cloth_inspection_continuous.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统(连续匹配版)启动") app = QApplication(sys.argv) mainWindow = MainWindow() mainWindow.sliderThreshold.valueChanged.connect( lambda value: update_match_threshold(value) ) # 信号连接 mainWindow.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) mainWindow.bnCheckPrint.clicked.connect(lambda: vision_controlled_check(None)) mainWindow.bnSaveSample.clicked.connect(save_sample_image) mainWindow.bnPreviewSample.clicked.connect(preview_sample) mainWindow.bnEnum.clicked.connect(enum_devices) mainWindow.bnOpen.clicked.connect(open_device) mainWindow.bnClose.clicked.connect(close_device) mainWindow.bnStart.clicked.connect(start_grabbing) mainWindow.bnStop.clicked.connect(stop_grabbing) mainWindow.bnSoftwareTrigger.clicked.connect(trigger_once) mainWindow.radioTriggerMode.clicked.connect(set_software_trigger_mode) mainWindow.radioContinueMode.clicked.connect(set_continue_mode) mainWindow.bnGetParam.clicked.connect(get_param) mainWindow.bnSetParam.clicked.connect(set_param) mainWindow.bnSaveImage.clicked.connect(save_sample_image) # 连续匹配信号连接 mainWindow.sliderThreshold.valueChanged.connect(update_match_score_display) mainWindow.chkContinuousMatch.stateChanged.connect(toggle_template_matching) mainWindow.show() app.exec_() close_device() sys.exit()
07-17
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值