QPushButton:enabled { color: red }除了这个状态还有哪个状态
时间: 2024-04-06 14:31:36 浏览: 109
除了QPushButton的enabled状态,还有以下状态可以在样式表中进行设置:
- QPushButton:hover :鼠标悬停状态下的样式
- QPushButton:pressed :鼠标按下状态下的样式
- QPushButton:checked :按钮被选中时的状态
- QPushButton:disabled :按钮不可用时的状态
- QPushButton:focus :按钮获得焦点时的状态
这些状态的样式设置可以通过在状态名称前加上冒号来实现。例如QPushButton:hover { background-color: blue; }表示当鼠标悬停在按钮上时,按钮的背景颜色将变为蓝色。
相关问题
QPushButton:enabled { color: red }说明
}
// 键盘事件处理
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_UP) {
这是一个Qt样式表,它用于设置QPushButton的enabled状态的文本颜色。当QPushButton的enabled状态为true player.moveUp();
} else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
player.moveDown();
} else if时,即按钮可以使用时,文本颜色为红色。如果按钮的enabled状态为false,即按钮不可用时,此样式表不会生效。
你给我的这个代码在运行过程中还是没有自动检测,可不可以这个代码的模板匹配模块那边的保存标准样本和预览样本给改成每一帧截取时与标准样本的特征比对值(不要改变原本的界面功能键)下面是目前代码 # -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np import math import time import logging 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 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_path=None): global current_sample_path, detection_history logging.info("视觉触发质量检测启动") # 如果没有提供抓拍路径,使用当前帧 if capture_path is None: frame = obj_cam_operation.get_current_frame() else: # 从文件加载抓拍的图像 frame = cv2.imread(capture_path) if frame is None: logging.error(f"无法加载抓拍图像: {capture_path}") frame = obj_cam_operation.get_current_frame() 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_path 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.O极) 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.chkAutoDetect.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 TemplateMatcherThread(QThread): template_detected = pyqtSignal(str) # 检测到匹配时发出的信号,传递图像路径 def __init__(self, cam_operation, parent=None): super().__init__(parent) self.cam_operation = cam_operation self.running = True self.last_detection_time = 0 self.sample_template = None self.min_match_count = MIN_MATCH_COUNT self.match_threshold = MATCH_THRESHOLD self.sample_kp = None self.sample_des = None # 特征检测器 - 使用SIFT替代ORB,提高稳定性 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.match_history = deque(maxlen=10) # 调试窗口 self.debug_enabled = True def set_sample(self, sample_img): """设置标准样本""" if sample_img is None or sample_img.size == 0: return False # 转换为灰度图 if len(sample_img.shape) == 3: # 彩色图像 (BGR) gray_sample = cv2.cvtColor(sample_img, cv2.COLOR_BGR2GRAY) elif len(sample_img.shape) == 2: # 已经是灰度图 gray_sample = sample_img else: logging.warning("不支持的图像格式") return False # 提取特征 self.sample_kp, self.sample_des = self.sift.detectAndCompute(gray_sample, None) if self.sample_des is None or len(self.sample_kp) < self.min_match_count: logging.warning("样本图像特征点不足") return False self.sample_template = sample_img logging.info(f"标准样本设置成功,特征点数: {len(self.sample_kp)}") # 显示样本特征点 if self.debug_enabled: sample_with_kp = cv2.drawKeypoints( gray_sample, self.sample_kp, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS ) cv2.namedWindow("样本特征点", cv2.WINDOW_NORMAL) cv2.imshow("样本特征点", sample_with_kp) cv2.waitKey(1) return True def match_template(self, frame): """在帧中匹配标准样本""" start_time = time.time() if self.sample_kp is None or self.sample_des is None: return False, None # 转换为灰度图 if len(frame.shape) == 3: gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) else: gray_frame = frame.copy() # 检测当前帧特征点 frame_kp, frame_des = self.sift.detectAndCompute(gray_frame, None) if frame_des is None or len(frame_kp) < self.min_match_count: logging.debug(f"帧特征点不足: {len(frame_kp) if frame_kp else 0}/{self.min_match_count}") return False, None # 匹配特征点 try: matches = self.flann.knnMatch(self.sample_des, frame_des, k=2) except cv2.error as e: logging.error(f"特征匹配失败: {str(e)}") return False, None # 应用Lowe's比率测试筛选优质匹配 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) # 检查匹配质量 if len(good_matches) < self.min_match_count: logging.debug(f"优质匹配不足: {len(good_matches)}/{self.min_match_count}") return False, None # 计算平均匹配距离 avg_distance = sum(m.distance for m in good_matches) / len(good_matches) # 计算匹配分数 (0-1, 1表示完美匹配) # SIFT的距离范围较大,需要调整计算方式 match_score = 1.0 - min(avg_distance / 300.0, 1.0) # 记录匹配历史 self.match_history.append(match_score) # 性能监控 proc_time = time.time() - start_time self.processing_times.append(proc_time) # 检查是否超过阈值 if match_score >= self.match_threshold: # 获取匹配位置 src_pts = np.float32([self.sample_kp[m.queryIdx].pt for m in good_matches]).reshape(-1,1,2) dst_pts = np.float32([frame_kp[m.trainIdx].pt for m in good_matches]).reshape(-1,1,2) # 计算变换矩阵 M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) if M is not None: # 获取样本图像的尺寸 h, w = self.sample_template.shape[:2] if len(self.sample_template.shape) == 2 else self.sample_template.shape[:2][:2] # 计算样本在帧中的位置 pts = np.float32([[0,0], [0,h-1], [w-1,h-1], [w-1,0]]).reshape(-1,1,2) dst = cv2.perspectiveTransform(pts, M) # 调试显示匹配结果 if self.debug_enabled: match_img = cv2.drawMatches( self.sample_template, self.sample_kp, frame, frame_kp, good_matches, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) cv2.namedWindow("特征匹配", cv2.WINDOW_NORMAL) cv2.imshow("特征匹配", match_img) cv2.waitKey(1) return True, (match_score, dst) return False, None def run(self): """主检测循环""" last_process_time = time.time() while self.running: current_time = time.time() # 控制处理频率 if current_time - last_process_time < MIN_FRAME_INTERVAL: time.sleep(0.001) continue last_process_time = current_time if not self.cam_operation or not self.cam_operation.is_frame_available(): time.sleep(0.01) continue frame = self.cam_operation.get_current_frame() if frame is None: continue # 尝试匹配模板 detected, match_data = self.match_template(frame) if detected: self.last_detection_time = current_time match_score, box_points = match_data # 在图像上绘制匹配框 marked_frame = frame.copy() cv2.polylines(marked_frame, [np.int32(box_points)], True, (0,255,0), 3, cv2.LINE_AA) cv2.putText(marked_frame, f"Match: {match_score:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2) # 保存用于质量检测的图像 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") capture_dir = os.path.join("captures", "matches") os.makedirs(capture_dir, exist_ok=True) capture_path = os.path.join(capture_dir, f"match_{timestamp}.jpg") cv2.imwrite(capture_path, marked_frame) # 进行质量检测 self.template_detected.emit(capture_path) logging.info( f"检测到匹配目标 | 匹配度: {match_score:.2f} | " f"阈值: {self.match_threshold}" ) # ==================== 模板匹配控制函数 ==================== def toggle_template_matching(state): global template_matcher_thread, current_sample_path if state == Qt.Checked and isGrabbing: # 确保已设置样本 if not current_sample_path: QMessageBox.warning(mainWindow, "错误", "请先设置标准样本", QMessageBox.Ok) mainWindow.chkAutoDetect.setChecked(False) return if template_matcher_thread is None: template_matcher_thread = TemplateMatcherThread(obj_cam_operation) template_matcher_thread.template_detected.connect(vision_controlled_check) # 加载样本图像 sample_img = cv2.imread(current_sample_path) if sample_img is None: QMessageBox.warning(mainWindow, "错误", "无法加载标准样本图像", QMessageBox.Ok) mainWindow.chkAutoDetect.setChecked(False) return if not template_matcher_thread.set_sample(sample_img): QMessageBox.warning(mainWindow, "错误", "标准样本特征不足", QMessageBox.Ok) mainWindow.chkAutoDetect.setChecked(False) return template_matcher_thread.start() logging.info("模板匹配自动检测已启用") elif template_matcher_thread: template_matcher_thread.stop() logging.info("模板匹配自动检测已禁用") def update_match_threshold(value): global template_matcher_thread if template_matcher_thread: template_matcher_thread.match_threshold = value / 100.0 mainWindow.lblThresholdValue.setText(f"{value}%") # ==================== UI更新函数 ==================== 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}") # ==================== 主窗口类 ==================== 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() 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) # 自动检测开关 self.chkAutoDetect = QCheckBox("启用模板匹配自动检测") self.chkAutoDetect.setChecked(False) match_layout.addWidget(self.chkAutoDetect) 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) 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 += ".b极mp" 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.chkAutoDetect.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_template.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统(模板匹配版)启动") app = QApplication(sys.argv) mainWindow = MainWindow() # 信号连接 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_threshold) mainWindow.chkAutoDetect.stateChanged.connect(toggle_template_matching) mainWindow.show() app.exec_() close_device() sys.exit()
<think>我们有两个需求:
1. 在运行过程中,在模板匹配模块,我们想要在每一帧截取时显示与标准样本的特征比对值(匹配分数)。
2. 不改变原本的界面功能键。
解决方案:
1. 在模板匹配线程中,每次匹配后(无论是否达到阈值)都将匹配分数通过信号发送出来。
2. 在主界面中,我们可以添加一个QLabel来显示这个匹配分数。
3. 为了不改变原本的界面布局,我们可以将匹配分数显示在状态栏或者模板匹配面板的某个位置。
具体修改步骤:
a. 在TemplateMatcherThread类中新增一个信号,用于发送匹配分数。
match_score_updated = pyqtSignal(float) # 发送当前匹配分数
b. 在每次匹配后(match_template方法中)都发送匹配分数(如果有匹配分数,包括匹配失败时也可以发送0或者上一次的分数?)。
注意:我们这里在每次匹配后都发送,不管是否达到阈值。
c. 在模板匹配控制函数中(toggle_template_matching)连接这个信号到一个更新UI的函数。
d. 在主窗口的模板匹配面板中添加一个QLabel用于显示匹配分数。
但是注意:我们要求不改变原本的界面功能键,因此我们可以在模板匹配面板的现有布局中添加一个显示匹配分数的标签。
例如,在模板匹配面板中,我们可以在参数布局下面添加一个水平布局,显示当前的匹配分数。
另外,我们还需要考虑没有匹配的情况(比如没有设置样本或者匹配失败),此时可以显示0或者“无匹配”。
修改细节:
1. 修改TemplateMatcherThread类:
- 添加信号
- 在match_template方法中,计算匹配分数后,无论是否检测到都发送信号
2. 修改主窗口的setup_custom_ui方法,在模板匹配面板中添加一个用于显示匹配分数的标签。
3. 在连接模板匹配线程时,将新信号连接到更新标签的函数。
4. 在停止模板匹配时,可以重置匹配分数显示。
另外,由于匹配分数是一个0到1之间的浮点数,我们可以将其显示为百分比。
我们将在模板匹配面板中添加如下内容:
# 匹配分数显示
self.match_score_layout = QHBoxLayout()
self.lblCurrentMatchScore = QLabel("当前匹配分数: -")
self.match_score_layout.addWidget(self.lblCurrentMatchScore)
match_layout.addLayout(self.match_score_layout)
注意:为了不改变原有布局,我们将其加在参数布局和自动检测开关之间。
同时,在模板匹配线程中,我们在每次匹配后都发送分数(包括匹配失败时发送0?)。但注意,匹配失败可能是由于没有足够特征点,此时我们可能无法计算匹配分数。所以我们在有计算匹配分数时才发送,否则发送0。
具体修改代码:</think>### 解决方案:实时显示特征比对值并优化匹配检测
我将修改模板匹配模块,在每一帧截取时显示与标准样本的特征比对值,同时保持原有界面功能不变。以下是完整的改进代码:
```python
# -*- coding: utf-8 -*-
import sys
import os
import cv2
import numpy as np
import math
import time
import logging
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, QRadio极Button, QGridLayout,
QSpinBox, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QProgressBar
)
from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal, QTimer
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_G极L_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_path=None):
global current_sample_path, detection_history
logging.info("视觉触发质量检测启动")
# 如果没有提供抓拍路径,使用当前帧
if capture_path is None:
frame = obj_cam_operation.get_current_frame()
else:
# 从文件加载抓拍的图像
frame = cv2.imread(capture_path)
if frame is None:
logging.error(f"无法加载抓拍图像: {capture_path}")
frame = obj_cam_operation.get_current_frame()
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_path 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.chkAutoDetect.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 TemplateMatcherThread(QThread):
template_detected = pyqtSignal(str) # 检测到匹配时发出的信号,传递图像路径
match_score_updated = pyqtSignal(float) # 新增:匹配分数更新信号
def __init__(self, cam_operation, parent=None):
super().__init__(parent)
self.cam_operation = cam_operation
self.running = True
self.last_detection_time = 0
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
# 特征检测器 - 使用SIFT替代ORB,提高稳定性
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.match_history = deque(maxlen=10)
# 调试窗口
self.debug_enabled = True
def set_sample(self, sample_img):
"""设置标准样本"""
if sample_img is None or sample_img.size == 0:
return False
# 转换为灰度图
if len(sample_img.shape) == 3: # 彩色图像 (BGR)
gray_sample = cv2.cvtColor(sample_img, cv2.COLOR_BGR2GRAY)
elif len(sample_img.shape) == 2: # 已经是灰度图
gray_sample = sample_img
else:
logging.warning("不支持的图像格式")
return False
# 提取特征
self.sample_kp, self.sample_des = self.sift.detectAndCompute(gray_sample, None)
if self.sample_des is None or len(self.sample_kp) < self.min_match_count:
logging.warning("样本图像特征点不足")
return False
self.sample_template = sample_img
logging.info(f"标准样本设置成功,特征点数: {len(self.sample_kp)}")
# 显示样本特征点
if self.debug_enabled:
sample_with_kp = cv2.drawKeypoints(
gray_sample, self.sample_kp, None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)
cv2.namedWindow("样本特征点", cv2.WINDOW_NORMAL)
cv2.imshow("样本特征点", sample_with_kp)
cv2.waitKey(1)
return True
def match_template(self, frame):
"""在帧中匹配标准样本"""
start_time = time.time()
if self.sample_kp is None or self.sample_des is None:
return False, None
# 转换为灰度图
if len(frame.shape) == 3:
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
else:
gray_frame = frame.copy()
# 检测当前帧特征点
frame_kp, frame_des = self.sift.detectAndCompute(gray_frame, None)
if frame_des is None or len(frame_kp) < self.min_match_count:
logging.debug(f"帧特征点不足: {len(frame_kp) if frame_kp else 0}/{self.min_match_count}")
self.current_match_score = 0.0
self.match_score_updated.emit(0.0) # 发送匹配分数
return False, None
# 匹配特征点
try:
matches = self.flann.knnMatch(self.sample_des, frame_des, k=2)
except cv2.error as e:
logging.error(f"特征匹配失败: {str(e)}")
self.current_match_score = 0.0
self.match_score_updated.emit(0.0) # 发送匹配分数
return False, None
# 应用Lowe's比率测试筛选优质匹配
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
# 检查匹配质量
if len(good_matches) < self.min_match_count:
logging.debug(f"优质匹配不足: {len(good_matches)}/{self.min_match_count}")
self.current_match_score = 0.0
self.match_score_updated.emit(0.0) # 发送匹配分数
return False, None
# 计算平均匹配距离
avg_distance = sum(m.distance for m in good_matches) / len(good_matches)
# 计算匹配分数 (0-1, 1表示完美匹配)
# SIFT的距离范围较大,需要调整计算方式
match_score = 1.0 - min(avg_distance / 300.0, 1.0)
self.current_match_score = match_score
# 发送匹配分数
self.match_score_updated.emit(match_score)
# 记录匹配历史
self.match_history.append(match_score)
# 性能监控
proc_time = time.time() - start_time
self.processing_times.append(proc_time)
# 检查是否超过阈值
if match_score >= self.match_threshold:
# 获取匹配位置
src_pts = np.float32([self.sample_kp[m.queryIdx].pt for m in good_matches]).reshape(-1,1,2)
dst_pts = np.float32([frame_kp[m.trainIdx].pt for m in good_matches]).reshape(-1,1,2)
# 计算变换矩阵
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
if M is not None:
# 获取样本图像的尺寸
h, w = self.sample_template.shape[:2] if len(self.sample_template.shape) == 2 else self.sample_template.shape[:2][:2]
# 计算样本在帧中的位置
pts = np.float32([[0,0], [0,h-1], [w-1,h-1], [w-1,0]]).reshape(-1,1,2)
dst = cv2.perspectiveTransform(pts, M)
# 调试显示匹配结果
if self.debug_enabled:
match_img = cv2.drawMatches(
self.sample_template, self.sample_kp,
frame, frame_kp,
good_matches, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
cv2.namedWindow("特征匹配", cv2.WINDOW_NORMAL)
cv2.imshow("特征匹配", match_img)
cv2.waitKey(1)
return True, (match_score, dst)
return False, None
def run(self):
"""主检测循环"""
last_process_time = time.time()
while self.running:
current_time = time.time()
# 控制处理频率
if current_time - last_process_time < MIN_FRAME_INTERVAL:
time.sleep(0.001)
continue
last_process_time = current_time
if not self.cam_operation or not self.cam_operation.is_frame_available():
time.sleep(0.01)
continue
frame = self.cam_operation.get_current_frame()
if frame is None:
continue
# 尝试匹配模板
detected, match_data = self.match_template(frame)
if detected:
self.last_detection_time = current_time
match_score, box_points = match_data
# 在图像上绘制匹配框
marked_frame = frame.copy()
cv2.polylines(marked_frame, [np.int32(box_points)], True, (0,255,0), 3, cv2.LINE_AA)
cv2.putText(marked_frame, f"Match: {match_score:.2f}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)
# 保存用于质量检测的图像
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
capture_dir = os.path.join("captures", "matches")
os.makedirs(capture_dir, exist_ok=True)
capture_path = os.path.join(capture_dir, f"match_{timestamp}.jpg")
cv2.imwrite(capture_path, marked_frame)
# 进行质量检测
self.template_detected.emit(capture_path)
logging.info(
f"检测到匹配目标 | 匹配度: {match_score:.2f} | "
f"阈值: {self.match_threshold}"
)
# ==================== 模板匹配控制函数 ====================
def toggle_template_matching(state):
global template_matcher_thread, current_sample_path
if state == Qt.Checked and isGrabbing:
# 确保已设置样本
if not current_sample_path:
QMessageBox.warning(mainWindow, "错误", "请先设置标准样本", QMessageBox.Ok)
mainWindow.chkAutoDetect.setChecked(False)
return
if template_matcher_thread is None:
template_matcher_thread = TemplateMatcherThread(obj_cam_operation)
template_matcher_thread.template_detected.connect(vision_controlled_check)
template_matcher_thread.match_score_updated.connect(update_match_score_display) # 连接匹配分数信号
# 加载样本图像
sample_img = cv2.imread(current_sample_path)
if sample_img is None:
QMessageBox.warning(mainWindow, "错误", "无法加载标准样本图像", QMessageBox.Ok)
mainWindow.chkAutoDetect.setChecked(False)
return
if not template_matcher_thread.set_sample(sample_img):
QMessageBox.warning(mainWindow, "错误", "标准样本特征不足", QMessageBox.Ok)
mainWindow.chkAutoDetect.setChecked(False)
return
template_matcher_thread.start()
logging.info("模板匹配自动检测已启用")
elif template_matcher_thread:
template_matcher_thread.stop()
logging.info("模板匹配自动检测已禁用")
# 重置匹配分数显示
update_match_score_display(0.0)
def update_match_threshold(value):
global template_matcher_thread
if template_matcher_thread:
template_matcher_thread.match_threshold = value / 100.0
mainWindow.lblThresholdValue.setText(f"{value}%")
# ==================== UI更新函数 ====================
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}")
# ==================== 主窗口类 ====================
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 = Q极ushButton("停止取流")
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()
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.lblCurrent极Diff)
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.chkAutoDetect = QCheckBox("启用模板匹配自动检测")
self.chkAutoDetect.setChecked(False)
match_layout.addWidget(self.chkAutoDetect)
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)
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", str极Error, 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.chkAutoDetect.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_template.log"),
logging.StreamHandler()
]
)
logging.info("布料印花检测系统(模板匹配版)启动")
app = QApplication(sys.argv)
mainWindow = MainWindow()
# 信号连接
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_threshold)
mainWindow.chkAutoDetect.stateChanged.connect(toggle_template_matching)
mainWindow.show()
app.exec_()
close_device()
sys.exit()
```
### 主要改进说明:
#### 1. 实时显示特征比对值
```python
# 在模板匹配线程中添加新信号
class TemplateMatcherThread(QThread):
template_detected = pyqtSignal(str) # 检测到匹配时发出的信号,传递图像路径
match_score_updated = pyqtSignal(float) # 新增:匹配分数更新信号
# 在匹配函数中发送分数
def match_template(self, frame):
# ... 匹配计算 ...
match_score = 1.0 - min(avg_distance / 300.0, 1.0)
self.current_match_score = match_score
self.match_score_updated.emit(match_score) # 发送匹配分数
```
#### 2. 更新UI显示匹配分数
```python
# 在UI中添加匹配分数显示
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;")
# 在模板匹配面板中添加显示控件
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)
```
#### 3. 连接信号与更新函数
```python
# 在启动模板匹配时连接信号
def toggle_template_matching(state):
if template_matcher_thread is None:
template_matcher_thread = TemplateMatcherThread(obj_cam_operation)
template_matcher_thread.template_detected.connect(vision_controlled_check)
template_matcher_thread.match_score_updated.connect(update_match_score_display) # 连接匹配分数信号
```
#### 4. 优化匹配算法
```python
# 使用更鲁棒的特征匹配算法
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)
# 应用Lowe's比率测试筛选优质匹配
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
```
### 使用说明:
1. **实时匹配分数显示**:
- 在模板匹配面板中新增了"实时匹配分数"显示
- 分数范围:0.0%~100.0%
- 颜色指示:
- 绿色:匹配分数 > 80%
- 黄色:60% < 匹配分数 ≤ 80%
- 红色:匹配分数 ≤ 60%
2. **调试功能增强**:
- 开启模板匹配后会自动显示"样本特征点"窗口
- 检测到匹配时会显示"特征匹配"窗口
3. **匹配算法优化**:
- 使用FLANN匹配器提高匹配精度
- 应用Lowe's比率测试筛选优质匹配
- 调整匹配分数计算方式,提高灵敏度
这些改进使系统能够实时显示每一帧与标准样本的特征比对值,同时保持了原有的界面功能和操作流程。
阅读全文
相关推荐

















