# -*- coding: utf-8 -*-
import sys
import os
import cv2
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QWidget,
QVBoxLayout, QHBoxLayout, QMessageBox, QLabel,
QFileDialog, QToolBar, QComboBox, QStatusBar,
QGroupBox, QSlider, QDockWidget, QProgressDialog,
QLineEdit, QCheckBox, QGridLayout, QSpinBox, QRadioButton)
from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QPixmap, QImage, QFont
import time
import datetime
import logging
import platform
import random
from skimage.metrics import structural_similarity as ssim
import json
import threading
import ctypes
# 尝试导入海康SDK
try:
from MvCameraControl_class import *
except ImportError:
logging.error("未找到海康SDK库,请安装MVS SDK")
# 如果没有安装SDK,使用模拟模式
class MvCamera:
MV_CC_DEVICE_INFO_LIST = type('MV_CC_DEVICE_INFO_LIST', (object,), {})
MV_GIGE_DEVICE = 1
MV_USB_DEVICE = 4
MV_ACCESS_Exclusive = 1
@staticmethod
def MV_CC_EnumDevices(nTLayerType, stDeviceList):
return 0
@staticmethod
def MV_CC_CreateHandle(stDeviceInfo):
return 0
@staticmethod
def MV_CC_OpenDevice(stCamHandle, nAccessMode, nSwitchoverKey):
return 0
@staticmethod
def MV_CC_StartGrabbing(stCamHandle):
return 0
@staticmethod
def MV_CC_StopGrabbing(stCamHandle):
return 0
@staticmethod
def MV_CC_CloseDevice(stCamHandle):
return 0
@staticmethod
def MV_CC_DestroyHandle(stCamHandle):
return 0
@staticmethod
def MV_CC_RegisterImageCallBack(stCamHandle, cbOutput, pUser):
return 0
# 配置日志系统
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("cloth_inspection_debug.log"),
logging.StreamHandler()
]
)
logging.info("布料印花检测系统启动")
# 全局变量
current_sample_path = "" # 当前使用的样本路径
detection_history = [] # 检测历史记录
is_processing = False # 防止重复处理
discovered_cameras = [] # 存储发现的相机列表
# ====================== 虚拟传感器类 ======================
class VirtualSensor:
"""模拟真实传感器输入的虚拟传感器"""
def __init__(self):
self.state = False # 传感器状态(触发/未触发)
self.trigger_delay = 0.5 # 默认触发延迟(秒)
self.trigger_count = 0 # 触发计数器
self.sensor_type = "光电传感器" # 传感器类型
self.mock_mode = False # 模拟模式
def trigger(self):
"""模拟传感器触发"""
self.state = True
self.trigger_count += 1
logging.info(f"传感器触发 #{self.trigger_count}")
time.sleep(self.trigger_delay)
self.state = False
def set_delay(self, delay):
"""设置触发延迟时间"""
self.trigger_delay = max(0.1, min(delay, 5.0)) # 限制在0.1-5秒之间
def set_type(self, sensor_type):
"""设置传感器类型"""
self.sensor_type = sensor_type
def enable_mock(self, enable):
"""启用/禁用模拟模式"""
self.mock_mode = enable
if enable:
logging.info("传感器模拟模式已启用")
def mock_trigger(self):
"""模拟传感器触发(随机间隔)"""
if self.mock_mode:
interval = random.uniform(0.5, 3.0)
threading.Timer(interval, self.trigger).start()
# 创建虚拟传感器实例
virtual_sensor = VirtualSensor()
# ====================== 传感器信号处理线程 ======================
class SensorThread(QThread):
"""处理传感器信号的线程"""
sensor_triggered = pyqtSignal()
def __init__(self, sensor):
super().__init__()
self.sensor = sensor
self.running = True
self.mock_timer = QTimer()
self.mock_timer.timeout.connect(self.mock_sensor_check)
def run(self):
while self.running:
if self.sensor.state:
self.sensor_triggered.emit()
# 等待传感器复位
while self.sensor.state:
time.sleep(0.01)
time.sleep(0.05) # 减少CPU占用
def start_mock(self, interval=1000):
"""启动模拟传感器触发"""
self.mock_timer.start(interval)
def stop_mock(self):
"""停止模拟传感器触发"""
self.mock_timer.stop()
def mock_sensor_check(self):
"""检查并触发模拟传感器"""
if self.sensor.mock_mode:
self.sensor.trigger()
# ====================== 图像处理线程 ======================
class ImageProcessingThread(QThread):
"""图像处理线程,避免阻塞UI"""
processing_complete = pyqtSignal(bool, float, np.ndarray)
def __init__(self, sample_path, test_image, threshold, use_ssim):
super().__init__()
self.sample_path = sample_path
self.test_image = test_image
self.threshold = threshold
self.use_ssim = use_ssim
def run(self):
try:
# 执行检测
is_qualified, diff_ratio, marked_image = self.check_print_quality(
self.sample_path,
self.test_image,
self.threshold,
self.use_ssim
)
# 发出信号
self.processing_complete.emit(is_qualified, diff_ratio, marked_image)
except Exception as e:
logging.exception(f"图像处理线程错误: {str(e)}")
self.processing_complete.emit(None, None, None)
def check_print_quality(self, sample_image_path, test_image, threshold=0.05, use_ssim=True):
"""
优化的布料印花检测算法
:param sample_image_path: 合格样本图像路径
:param test_image: 测试图像 (numpy数组)
:param threshold: 差异阈值
:param use_ssim: 是否使用SSIM结构相似性指标
:return: 是否合格,差异值,标记图像
"""
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
# 确保测试图像是灰度图
if len(test_image.shape) == 3: # 如果是彩色图像
test_image_gray = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY)
else:
test_image_gray = test_image.copy()
# 图像配准 - 使用特征匹配解决轻微位移问题
aligned_image = self.align_images(sample_image, test_image_gray)
if aligned_image is None:
aligned_image = test_image_gray # 配准失败则使用原始图像
logging.warning("图像配准失败,使用原始图像")
# 确保两个图像大小一致
if aligned_image.shape != sample_image.shape:
aligned_image = cv2.resize(aligned_image, (sample_image.shape[1], sample_image.shape[0]))
# 方法1: 极速SSIM算法 (优化版)
if use_ssim:
# 使用优化的SSIM计算
score = self.fast_ssim(sample_image, aligned_image)
diff_极速响应ratio = 1.0 - score # 差异比例
# 计算绝对差异作为差异图
diff = cv2.absdiff(sample_image, aligned_image)
_, thresholded = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
else:
# 方法2: 传统绝对差异法
diff = cv2.absdiff(sample_image, aligned_image)
# 自适应阈值处理
thresholded = cv2.adaptiveThreshold(
diff, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
11, 2
)
# 计算差异比例
diff_pixels = np.count_nonzero(thresholded)
total_pixels = sample_image.size
diff_ratio = diff_pixels / total_pixels
# 形态学操作去除噪声
kernel = np.ones((3, 3), np.uint8)
thresholded = cv2.morphologyEx(thresholded, cv2.MORPH_OPEN, kernel)
thresholded = cv2.morphologyEx(thresholded, cv2.MORPH_CLOSE, kernel)
# 多尺度缺陷检测
marked_image = self.detect_defects(aligned_image, thresholded)
# 判断是否合格
is_qualified = diff_ratio <= threshold
return is_qualified, diff_ratio, marked_image
except Exception as e:
logging.exception(f"检测过程中发生错误: {str(e)}")
return None, None, None
def fast_ssim(self, img1, img2):
"""优化的SSIM计算,提高性能"""
# 图像下采样以提高速度
if img1.shape[0] > 512 or img1.shape[1] > 512:
scale = 0.5
img1 = cv2.resize(img1, (0, 0), fx=scale, fy=scale)
img2 = cv2.resize(img2, (0, 0), fx=scale, fy=scale)
# 计算SSIM
score = ssim(img1, img2, win_size=3, data_range=img1.max() - img1.min())
return max(0.0, min(1.0, score)) # 确保在0-1范围内
def align_images(self, image1, image2):
"""
使用特征匹配对齐两幅图像
:param image1: 参考图像
:param image2: 待对齐图像
:return: 对齐后的图像
"""
# 使用ORB检测器(比SIFT更快)
orb = cv2.ORB_create()
# 查找关键点和描述符
kp1, des1 = orb.detectAndCompute(image1, None)
kp2, des2 = orb.detectAndCompute(image2, None)
# 如果关键点不足,尝试使用SIFT
if des1 is None or des2 is None or len(des1) < 4 or len(des2) < 4:
sift = cv2.SIFT_create()
kp1, des1 = sift.detectAndCompute(image1, None)
kp2, des2 = sift.detectAndCompute(image2, None)
# 如果还是没有足够的关键点,返回None
if des1 is None or des2 is None or len(des1) < 4 or len(des2) < 4:
return None
# 使用BFMatcher进行特征匹配
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2)
# 至少需要4个点计算变换矩阵
if len(matches) < 4:
return None
# 提取匹配点坐标
src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
# 计算变换矩阵(使用RANSAC)
M, mask = cv2.findHomography(dst_pts, src_pts, cv2.RANSAC, 5.0)
# 应用变换
aligned_image = cv2.warpPerspective(
image2, M,
(image1.shape[1], image1.shape[0]),
flags=cv2.INTER_LINEAR
)
return aligned_image
def detect_defects(self, image, mask):
"""
多尺度缺陷检测和标记
:param image: 原始图像
:param mask: 差异掩码
:return: 标记后的图像
"""
# 创建彩色标记图像
marked_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# 查找轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 设置最小缺陷尺寸(避免标记小噪点)
min_defect_area = max(10, image.size * 0.0001) # 自适应最小面积
# 标记缺陷区域
defect_count = 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area > min_defect_area:
defect_count += 1
# 计算轮廓的边界框
x, y, w, h = cv2.boundingRect(cnt)
# 绘制边界框
cv2.rectangle(marked_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
# 在缺陷中心添加文本标签
cv2.putText(
marked_image, f"Defect {defect_count}: {area}px",
(x, y-10), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 0, 255), 1
)
# 添加缺陷统计信息
cv2.putText(
marked_image, f"Total Defects: {defect_count}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 0, 255), 2
)
return marked_image
# ====================== 网络配置检查 ======================
def check_network_configuration():
"""检查网络配置是否适合海康相机"""
global discovered_cameras # 声明使用全局变量
# 尝试使用海康SDK枚举设备
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret == 0 and device_list.nDeviceNum > 0:
discovered_cameras = []
for i in range(device_list.nDeviceNum):
device_info = device_list.pDeviceInfo[i]
if device_info.nTLayerType == MV_GIGE_DEVICE:
ip = ".".join(map(str, device_info.SpecialInfo.stGigEInfo.nCurrentIp))
model = device_info.SpecialInfo.stGigEInfo.chModelName.decode('utf-8', 'ignore')
serial = device_info.SpecialInfo.stGigEInfo.chSerialNumber.decode('utf-8', 'ignore')
discovered_cameras.append({"ip": ip, "model": model, "serial": serial})
elif device_info.nTLayerType == MV_USB_DEVICE:
model = device_info.SpecialInfo.stUsb3VInfo.chModelName.decode('utf-8', 'ignore')
serial = device_info.SpecialInfo.stUsb3VInfo.chSerialNumber.decode('utf-8', 'ignore')
discovered_cameras.append({"ip": "USB", "model": model, "serial": serial})
logging.info(f"发现 {len(discovered_cameras)} 台真实相机")
return True
else:
# 模拟相机发现
discovered_cameras = [
{"ip": "192.168.1.101", "model": "MV-CA016-10GC", "serial": "SN123456"},
{"ip": "192.168.1.102", "model": "MV-CA020-10GC", "serial": "SN789012"}
]
logging.info(f"使用模拟相机数据: {len(discovered_cameras)} 台网络相机")
return bool(discovered_cameras)
# ====================== 主窗口类 ======================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setup_ui()
self.setup_variables()
self.setup_connections()
def setup_ui(self):
"""设置用户界面"""
self.setWindowTitle("布料印花检测系统")
self.resize(1400, 900)
# 创建主窗口的中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QHBoxLayout(central_widget)
# 左侧布局(相机控制和图像显示)
left_layout = QVBoxLayout()
# 相机控制组
camera_group = QGroupBox("相机控制")
camera_layout = QGridLayout(camera_group)
# 相机控制按钮
self.bnEnum = QPushButton("枚举设备")
self.ComboDevices = QComboBox()
self.bnOpen = QPushButton("打开设备")
self.bnClose = QPushButton("关闭设备")
self.bnStart = QPushButton("开始取流")
self.bnStop = QPushButton("停止取流")
self.bnSaveImage = QPushButton("保存图像")
# 添加到布局
camera_layout.addWidget(self.bnEnum, 0, 0)
camera_layout.addWidget(self.ComboDevices, 0, 1, 1, 2)
camera_layout.addWidget(self.bnOpen, 1, 0)
camera_layout.addWidget(self.bnClose, 1, 1)
camera_layout.addWidget(self.bnStart, 2, 0)
camera_layout.addWidget(self.bnStop, 2, 1)
camera_layout.addWidget(self.bnSaveImage, 2, 2)
# 参数控制组
param_group = QGroupBox("相机参数")
param_layout = QGridLayout(param_group)
# 参数控件
self.lblExposure = QLabel("曝光时间(μs):")
self.edtExposureTime = QLineEdit("10000")
self.lblGain = QLabel("增益(dB):")
self.edtGain = QLineEdit("0")
self.lblFrameRate = QLabel("帧率(fps):")
self.edtFrameRate = QLineEdit("30")
self.bnGetParam = QPushButton("获取参数")
self.bnSetParam = QPushButton("设置参数")
# 添加到布局
param_layout.addWidget(self.lblExposure, 0, 0)
param_layout.addWidget(self.edtExposureTime, 0, 1)
param_layout.addWidget(self.lblGain, 1, 0)
param_layout.addWidget(self.edtGain, 1, 1)
param_layout.addWidget(self.lblFrameRate, 2, 0)
param_layout.addWidget(self.edtFrameRate, 2, 1)
param_layout.addWidget(self.bnGetParam, 3, 0)
param_layout.addWidget(self.bnSetParam, 3, 1)
# 触发模式组
trigger_group = QGroupBox("触发模式")
trigger_layout = QVBoxLayout(trigger_group)
self.radioContinueMode = QRadioButton("连续采集模式")
self.radioTriggerMode = QRadioButton("触发采集模式")
self.bnSoftwareTrigger = QPushButton("软触发")
trigger_layout.addWidget(self.radioContinueMode)
trigger_layout.addWidget(self.radioTriggerMode)
trigger_layout.addWidget(self.bnSoftwareTrigger)
self.radioContinueMode.setChecked(True)
# 图像显示区域
self.lblImageDisplay = QLabel()
self.lblImageDisplay.setAlignment(Qt.AlignCenter)
self.lblImageDisplay.setMinimumSize(640, 480)
self.lblImageDisplay.setStyleSheet("background-color: black;")
# 添加到左侧布局
left_layout.addWidget(camera_group)
left_layout.addWidget(param_group)
left_layout.addWidget(trigger_group)
left_layout.addWidget(self.lblImageDisplay, 1)
# 右侧布局(检测控制)
right_layout = QVBoxLayout()
# 差异度调整组
diff_group = QGroupBox("检测参数")
diff_layout = QGridLayout(diff_group)
# 差异度控件
self.lblDiffThreshold = QLabel("差异度阈值 (%):")
self.sliderDiffThreshold = QSlider(Qt.Horizontal)
self.sliderDiffThreshold.setRange(0, 100)
self.sliderDiffThreshold.setValue(5)
self.lblDiffValue = QLabel("5%")
self.lblDiffValue.setMinimumWidth(50)
# 算法选项
self.cbUseSSIM = QCheckBox("使用SSIM算法(更准确)")
self.cbUseSSIM.setChecked(True)
# 添加到布局
diff_layout.addWidget(self.lblDiffThreshold, 0, 0)
diff_layout.addWidget(self.sliderDiffThreshold, 0, 1)
diff_layout.addWidget(self.lblDiffValue, 0, 2)
diff_layout.addWidget(self.cbUseSSIM, 1, 0, 1, 3)
# 样本管理组
sample_group = QGroupBox("样本管理")
sample_layout = QGridLayout(sample_group)
# 样本控件
self.bnSaveSample = QPushButton("保存标准样本")
self.bnPreviewSample = QPushButton("预览样本")
self.lblSamplePath = QLabel("当前样本: 未设置样本")
self.lblSamplePath.setWordWrap(True)
# 添加到布局
sample_layout.addWidget(self.bnSaveSample, 0, 0)
sample_layout.addWidget(self.bnPreviewSample, 0, 1)
sample_layout.addWidget(self.lblSamplePath, 1, 0, 1, 2)
# 传感器控制组
sensor_group = QGroupBox("传感器控制")
sensor_layout = QGridLayout(sensor_group)
# 传感器控件
self.cbEnableSensor = QCheckBox("启用传感器触发")
self.cbEnableSensor.setChecked(True)
self.lblSensorType = QLabel("传感器类型:")
self.comboSensorType = QComboBox()
self.comboSensorType.addItems(["光电传感器", "接近传感器", "编码器"])
self.lblSensorDelay = QLabel("触发延迟 (秒):")
self.edtSensorDelay = QLineEdit("0.5")
self.bnSetSensorDelay = QPushButton("设置")
self.cbMockSensor = QCheckBox("模拟传感器")
self.spinMockInterval = QSpinBox()
self.spinMockInterval.setRange(1000, 10000)
self.spinMockInterval.setValue(3000)
self.spinMockInterval.setSuffix(" ms")
self.bnStartMock = QPushButton("启动模拟")
self.bnStopMock = QPushButton("停止模拟")
# 手动触发按钮
self.bnManualTrigger = QPushButton("手动触发")
# 添加到布局
sensor_layout.addWidget(self.cbEnableSensor, 0, 0, 1, 2)
sensor_layout.addWidget(self.lblSensorType, 1, 0)
sensor_layout.addWidget(self.comboSensorType, 1, 1)
sensor_layout.addWidget(self.lblSensorDelay, 2, 0)
sensor_layout.addWidget(self.edtSensorDelay, 2, 1)
sensor_layout.addWidget(self.bnSetSensorDelay, 2, 2)
sensor_layout.addWidget(self.cbMockSensor, 3, 0, 1, 2)
sensor_layout.addWidget(QLabel("模拟间隔:"), 4, 0)
sensor_layout.addWidget(self.spinMockInterval, 4, 1)
sensor_layout.addWidget(self.bnStartMock, 5, 0)
sensor_layout.addWidget(self.bnStopMock, 5, 1)
sensor_layout.addWidget(self.bnManualTrigger, 6, 0, 1, 3)
# 检测结果组
result_group = QGroupBox("检测结果")
result_layout = QVBoxLayout(result_group)
# 结果控件
self.lblCurrentDiff = QLabel("当前差异度: -")
self.lblCurrentDiff.setFont(QFont("Arial", 12, QFont.Bold))
self.lblDiffStatus = QLabel("状态极速响应: 未检测")
self.lblDiffStatus.setFont(QFont("Arial", 10))
self.bnCheckPrint = QPushButton("执行检测")
self.bnCheckPrint.setFont(QFont("Arial", 12, QFont.Bold))
self.bnCheckPrint.setStyleSheet("background-color: #4CAF50; color: white;")
# 历史记录
self.lblHistory = QLabel("历史记录:")
self.cbHistory = QComboBox()
# 添加到布局
result_layout.addWidget(self.lblCurrentDiff)
result_layout.addWidget(self.lblDiffStatus)
result_layout.addStretch(1)
result_layout.addWidget(self.bnCheckPrint)
result_layout.addStretch(1)
result_layout.addWidget(self.lblHistory)
result_layout.addWidget(self.cbHistory)
# 添加到右侧布局
right_layout.addWidget(diff_group)
right_layout.addWidget(sample_group)
right_layout.addWidget(sensor_group)
right_layout.addWidget(result_group, 1)
# 添加到主布局
main_layout.addLayout(left_layout, 3)
main_layout.addLayout(right_layout, 1)
# 状态栏
self.statusBar = QStatusBar()
self.setStatusBar(self.statusBar)
self.lblFrameStatus = QLabel("帧状态: 无帧")
self.statusBar.addPermanentWidget(self.lblFrameStatus)
# 设置样式
self.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 1px solid gray;
border-radius: 5px;
margin-top: 1ex;
}
QGroupBox::title {
subcontrol-origin: margin;
padding: 0 3px;
}
QLabel {
font-size: 10pt;
}
QPushButton {
font-size: 10pt;
padding: 5px;
}
""")
def setup_variables(self):
"""初始化变量"""
# 相机相关变量
self.isOpen = False
self.isGrabbing = False
self.cam = None # 海康相机实例
# 传感器线程
self.sensor_thread = SensorThread(virtual_sensor)
self.sensor_thread.sensor_triggered.connect(self.sensor_triggered)
# 图像处理线程
self.image_thread = None
# 加载设置
self.load_settings()
def setup_connections(self):
"""连接信号和槽"""
# 相机控制
self.bnEnum.clicked.connect(self.enum_devices)
self.bnOpen.clicked.connect(self.open_device)
self.bnClose.clicked.connect(self.close_device)
self.bnStart.clicked.connect(self.start_grabbing)
self.bnStop.clicked.connect(self.stop_grabbing)
self.bnSaveImage.clicked.connect(self.save_image_dialog)
# 参数控制
self.bnGetParam.clicked.connect(self.get_param)
self.bnSetParam.clicked.connect(self.set_param)
# 触发模式
self.radioContinueMode.clicked.connect(self.set_continue_mode)
self.radioTriggerMode.clicked.connect(self.set_software_trigger_mode)
self.bnSoftwareTrigger.clicked.connect(self.trigger_once)
# 检测控制
self.bnCheckPrint.clicked.connect(self.check_print)
self.bnSaveSample.clicked.connect(self.save_sample_image)
self.bnPreviewSample.clicked.connect(self.preview_sample)
self.sliderDiffThreshold.valueChanged.connect(self.update_diff_threshold)
# 传感器控制
self.bnSetSensorDelay.clicked.connect(self.set_sensor_delay)
self.bnManualTrigger.clicked.connect(self.manual_sensor_trigger)
self.comboSensorType.currentTextChanged.connect(self.set_sensor_type)
self.cbMockSensor.stateChanged.connect(self.enable_sensor_mock)
self.bnStartMock.clicked.connect(self.start_mock_sensor)
self.bnStopMock.clicked.connect(self.stop_mock_sensor)
def load_settings(self):
"""加载应用程序设置"""
self.settings = QSettings("ClothInspection", "CameraApp")
# 加载样本路径
sample_path = self.settings.value("current_sample_path", "")
if sample_path:
global current_sample_path
current_sample_path = sample_path
self.update_sample_display()
# 加载检测参数
diff_threshold = self.settings.value("diff_threshold", 5, type=int)
self.sliderDiffThreshold.setValue(diff_threshold)
self.update_diff_threshold(diff_threshold)
# 加载传感器设置
sensor_delay = self.settings.value("sensor_delay", 0.5, type=float)
self.edtSensorDelay.setText(str(sensor_delay))
virtual_sensor.set_delay(sensor_delay)
sensor_type = self.settings.value("sensor_type", "光电传感器")
self.comboSensorType.setCurrentText(sensor_type)
virtual_sensor.set_type(sensor_type)
def save_settings(self):
"""保存应用程序设置"""
# 保存样本路径
self.settings.setValue("current_sample_path", current_sample_path)
# 保存检测参数
self.settings.setValue("diff_threshold", self.sliderDiffThreshold.value())
# 保存传感器设置
self.settings.setValue("sensor_delay", float(self.edtSensorDelay.text()))
self.settings.setValue("sensor_type", self.comboSensorType.currentText())
# ====================== 相机操作函数 ======================
def enum_devices(self):
"""枚举真实海康设备"""
self.ComboDevices.clear()
# 枚举设备
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret != 0:
self.statusBar.showMessage(f"枚举设备失败! 错误码: {ret}", 3000)
return
if device_list.nDeviceNum == 0:
self.statusBar.showMessage("未找到设备", 3000)
return
# 将设备添加到下拉框
for i in range(device_list.nDeviceNum):
device_info = device_list.pDeviceInfo[i]
if device_info.nTLayerType == MV_GIGE_DEVICE:
# GigE设备
ip = ".".join(map(str, device_info.SpecialInfo.stGigEInfo.nCurrentIp))
model = device_info.SpecialInfo.stGigEInfo.chModelName.decode('utf-8', 'ignore')
device_str = f"[{i}]GigE: {model} ({ip})"
self.ComboDevices.addItem(device_str, i)
elif device_info.nTLayerType == MV_USB_DEVICE:
# USB设备
model = device_info.SpecialInfo.stUsb3VInfo.chModelName.decode('utf-8', 'ignore')
serial = device_info.SpecialInfo.stUsb3VInfo.chSerialNumber.decode('utf-8', 'ignore')
device_str = f"[{i}]USB: {model} (SN:{serial})"
self.ComboDevices.addItem(device_str, i)
self.statusBar.showMessage(f"已枚举到{device_list.nDeviceNum}个设备", 3000)
def open_device(self):
"""打开真实海康设备"""
if self.isOpen:
QMessageBox.warning(self, "错误", "设备已打开", QMessageBox.Ok)
return
if self.ComboDevices.currentIndex() < 0:
QMessageBox.warning(self, "错误", "请先选择设备", QMessageBox.Ok)
return
# 获取设备信息
device_index = self.ComboDevices.currentData()
# 枚举设备以获取设备信息
device_list = MV_CC_DEVICE_INFO_LIST()
ret = MvCamera.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, device_list)
if ret != 0 or device_list.nDeviceNum == 0:
QMessageBox.warning(self, "错误", "获取设备信息失败", QMessageBox.Ok)
return
device_info = device_list.pDeviceInfo[device_index]
# 创建相机实例
self.cam = MvCamera()
ret = self.cam.MV_CC_CreateHandle(device_info)
if ret != 0:
QMessageBox.warning(self, "错误", f"创建句柄失败! 错误码: {ret}", QMessageBox.Ok)
return
# 打开设备
ret = self.cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
if ret != 0:
QMessageBox.warning(self, "错误", f"打开设备失败! 错误码: {ret}", QMessageBox.Ok)
self.cam.MV_CC_DestroyHandle()
self.cam = None
return
# 注册图像回调函数
ret = self.cam.MV_CC_RegisterImageCallBack(self.image_callback, None)
if ret != 0:
logging.warning(f"注册图像回调失败! 错误码: {ret}")
self.isOpen = True
self.enable_controls()
self.statusBar.showMessage("设备已打开", 3000)
def close_device(self):
"""关闭真实设备"""
if not self.isOpen or self.cam is None:
return
# 停止取流
if self.isGrabbing:
self.stop_grabbing()
# 关闭设备
ret = self.cam.MV_CC_CloseDevice()
if ret != 0:
logging.error(f"关闭设备失败! 错误码: {ret}")
# 销毁句柄
self.cam.MV_CC_DestroyHandle()
self.cam = None
self.isOpen = False
self.isGrabbing = False
self.enable_controls()
self.statusBar.showMessage("设备已关闭", 3000)
def start_grabbing(self):
"""开始真实取流"""
if not self.isOpen:
QMessageBox.warning(self, "错误", "请先打开设备", QMessageBox.Ok)
return
# 开始取流
ret = self.cam.MV_CC_StartGrabbing()
if ret != 0:
QMessageBox.warning(self, "错误", f"开始取流失败! 错误码: {ret}", QMessageBox.Ok)
return
self.isGrabbing = True
self.enable_controls()
self.statusBar.showMessage("已开始取流", 3000)
def stop_grabbing(self):
"""停止真实取流"""
if not self.isGrabbing or self.cam is None:
return
# 停止取流
ret = self.cam.MV_CC_StopGrabbing()
if ret != 0:
logging.error(f"停止取流失败! 错误码: {ret}")
self.isGrabbing = False
self.enable_controls()
self.statusBar.showMessage("已停止取流", 3000)
def set_continue_mode(self):
"""设置连续采集模式"""
if self.isOpen:
# 实际设置相机为连续模式
if self.cam:
# 这里需要调用SDK设置触发模式为关闭
pass
self.bnSoftwareTrigger.setEnabled(False)
self.statusBar.showMessage("已设置为连续采集模式", 3000)
def set_software_trigger_mode(self):
"""设置触发采集模式"""
if self.isOpen:
# 实际设置相机为软件触发模式
if self.cam:
# 这里需要调用SDK设置触发模式为软件触发
pass
self.bnSoftwareTrigger.setEnabled(self.isGrabbing)
self.statusBar.showMessage("已设置为触发采集模式", 3000)
def trigger_once(self):
"""执行软触发"""
if self.isOpen and self.isGrabbing and self.cam:
# 执行软触发
ret = self.cam.MV_CC_SetCommandValue("TriggerSoftware")
if ret == 0:
self.statusBar.showMessage("已执行软触发", 3000)
else:
self.statusBar.showMessage(f"软触发失败! 错误码: {ret}", 3000)
def get_param(self):
"""获取相机参数"""
if not self.isOpen or self.cam is None:
QMessageBox.warning(self, "错误", "设备未打开", QMessageBox.Ok)
return
try:
# 获取曝光时间
exposure = ctypes.c_float()
ret = self.cam.MV_CC_GetFloatValue("ExposureTime", exposure)
if ret == 0:
self.edtExposureTime.setText(f"{exposure.value:.2f}")
# 获取增益
gain = ctypes.c_float()
ret = self.cam.MV_CC_GetFloatValue("Gain", gain)
if ret == 0:
self.edtGain.setText(f"{gain.value:.2f}")
# 获取帧率
frame_rate = ctypes.c_float()
ret = self.cam.MV_CC_GetFloatValue("AcquisitionFrameRate", frame_rate)
if ret == 0:
self.edtFrameRate.setText(f"{frame_rate.value:.2f}")
self.statusBar.showMessage("已获取相机参数", 3000)
except Exception as e:
logging.error(f"获取参数错误: {str(e)}")
self.statusBar.showMessage("获取参数失败", 3000)
def set_param(self):
"""设置相机参数"""
if not self.isOpen or self.cam is None:
QMessageBox.warning(self, "错误", "设备未打开", QMessageBox.Ok)
return
try:
exposure = float(self.edtExposureTime.text())
gain = float(self.edtGain.text())
frame_rate = float(self.edtFrameRate.text())
# 验证参数范围
if not (5000 <= exposure <= 20000):
raise ValueError("曝光时间应在5000-20000μs之间")
if not (0 <= gain <= 20):
raise ValueError("增益应在0-20dB之间")
if not (10 <= frame_rate <= 60):
raise ValueError("帧率应在10-60fps之间")
# 设置曝光时间
ret = self.cam.MV_CC_SetFloatValue("ExposureTime", exposure)
if ret != 0:
logging.error(f"设置曝光失败! 错误码: {ret}")
# 设置增益
ret = self.cam.MV_CC_SetFloatValue("Gain", gain)
if ret != 0:
logging.error(f"设置增益失败! 错误码: {ret}")
# 设置帧率
ret = self.cam.MV_CC_SetFloatValue("AcquisitionFrameRate", frame_rate)
if ret != 0:
logging.error(f"设置帧率失败! 错误码: {ret}")
self.statusBar.showMessage(f"已设置参数: 曝光={exposure}μs, 增益={gain}dB, 帧率={frame_rate}fps", 3000)
except ValueError as e:
QMessageBox.warning(self, "输入错误", str(e), QMessageBox.Ok)
def save_image_dialog(self):
"""保存图像对话框"""
if not self.isGrabbing:
QMessageBox.warning(self, "错误", "请先开始取流", QMessageBox.Ok)
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存图像",
os.path.join(os.getcwd(), "capture.bmp"),
"BMP Files (*.bmp);;All Files (*)"
)
if file_path:
# 在实际应用中这里会保存真实图像
# 需要从相机获取当前帧并保存
self.statusBar.showMessage(f"图像已保存至: {file_path}", 5000)
def enable_controls(self):
"""设置控件状态"""
# 相机控制
self.bnOpen.setEnabled(not self.isOpen)
self.bnClose.setEnabled(self.isOpen)
self.bnStart.setEnabled(self.isOpen and not self.isGrabbing)
self.bnStop.setEnabled(self.isOpen and self.isGrabbing)
self.bnSaveImage.setEnabled(self.isGrabbing)
self.bnSoftwareTrigger.setEnabled(self.isGrabbing and self.radioTriggerMode.isChecked())
# 检测控制
self.bnCheckPrint.setEnabled(self.isGrabbing and bool(current_sample_path))
self.bnSaveSample.setEnabled(self.isGrabbing)
self.bnPreviewSample.setEnabled(bool(current_sample_path))
# 参数控制
self.bnGetParam.setEnabled(self.isOpen)
self.bnSetParam.setEnabled(self.isOpen)
def image_callback(self, pData, pFrameInfo, pUser):
"""图像数据回调函数"""
try:
if pFrameInfo.contents.nFrameLen <= 0:
return
# 将原始数据转换为numpy数组
data = (ctypes.c_ubyte * pFrameInfo.contents.nFrameLen).from_address(pData)
image = np.frombuffer(data, dtype=np.uint8)
# 根据帧信息解码图像
if pFrameInfo.contents.enPixelType == PixelType_Gvsp_Mono8:
# 单通道灰度图
image = image.reshape(pFrameInfo.contents.nHeight, pFrameInfo.contents.nWidth)
elif pFrameInfo.contents.enPixelType == PixelType_Gvsp_RGB8_Packed:
# RGB24
image = image.reshape(pFrameInfo.contents.nHeight, pFrameInfo.contents.nWidth, 3)
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
else:
# 其他格式需要转换
image = cv2.imdecode(image, cv2.IMREAD_UNCHANGED)
# 显示图像
self.display_real_image(image)
# 保存当前帧用于检测
self.current_frame = image.copy()
# 更新帧状态
self.lblFrameStatus.setText(f"帧状态: {pFrameInfo.contents.nWidth}x{pFrameInfo.contents.nHeight}")
except Exception as e:
logging.exception(f"图像回调错误: {str(e)}")
def display_real_image(self, image):
"""显示真实相机图像"""
if len(image.shape) == 2:
# 灰度图
h, w = image.shape
bytes_per_line = w
q_img = QImage(image.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
elif len(image.shape) == 3:
# 彩色图
h, w, ch = image.shape
bytes_per_line = ch * w
q_img = QImage(image.data, w, h, bytes_per_line, QImage.Format_RGB888)
else:
return
pixmap = QPixmap.fromImage(q_img)
self.lblImageDisplay.setPixmap(pixmap.scaled(
self.lblImageDisplay.width(),
self.lblImageDisplay.height(),
Qt.KeepAspectRatio
))
# ====================== 检测相关函数 ======================
def update_diff_threshold(self, value):
"""更新差异度阈值显示"""
self.lblDiffValue.setText(f"{value}%")
def save_sample_image(self):
"""保存标准样本"""
if not self.isGrabbing:
QMessageBox.warning(self, "错误", "请先开始取流", QMessageBox.Ok)
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存标准样本",
os.path.join(os.getcwd(), "sample.bmp"),
"BMP Files (*.bmp);;All Files (*)"
)
if file_path:
global current_sample_path
current_sample_path = file_path
# 保存当前帧作为样本
if hasattr(self, 'current_frame') and self.current_frame is not None:
cv2.imwrite(file_path, self.current_frame)
self.statusBar.showMessage(f"标准样本已保存: {file_path}", 5000)
else:
self.statusBar.showMessage("无法保存样本: 无有效图像", 5000)
self.update_sample_display()
self.save_settings()
def preview_sample(self):
"""预览样本"""
global current_sample_path
if not current_sample_path or not os.path.exists(current_sample_path):
QMessageBox.warning(self, "错误", "请先设置有效的标准样本图像", QMessageBox.Ok)
return
# 显示样本图像
sample_image = cv2.imread(current_sample_path)
if sample_image is not None:
# 转换为QPixmap并显示
if len(sample_image.shape) == 2:
h, w = sample_image.shape
bytes_per_line = w
q_img = QImage(sample_image.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
else:
h, w, ch = sample_image.shape
bytes_per_line = ch * w
q_img = QImage(sample_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(q_img)
self.lblImageDisplay.setPixmap(pixmap.scaled(
self.lblImageDisplay.width(),
self.lblImageDisplay.height(),
Qt.KeepAspectRatio
))
self.statusBar.showMessage("正在预览样本图像", 3000)
else:
QMessageBox.warning(self, "错误", "无法加载样本图像", QMessageBox.Ok)
def update_sample_display(self):
"""更新样本路径显示"""
global current_sample_path
if current_sample_path:
self.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}")
self.lblSamplePath.setToolTip(current_sample_path)
else:
self.lblSamplePath.setText("当前样本: 未设置样本")
def update_history_display(self):
"""更新历史记录显示"""
global detection_history
self.cbHistory.clear()
for i, result in enumerate(detection_history[-10:]): # 显示最近10条记录
timestamp = result['timestamp'].strftime("%H:%M:%S")
status = "合格" if result['qualified'] else "不合格"
ratio = f"{result['diff_ratio']*100:.2f}%"
self.cbHistory.addItem(f"[{timestamp}] {status} - 差异: {ratio}")
def check_print(self):
"""执行检测"""
global is_processing, current_sample_path, detection_history
if is_processing:
return
is_processing = True
# 检查条件
if not self.isGrabbing:
QMessageBox.warning(self, "错误", "请先开始取流", QMessageBox.Ok)
is_processing = False
return
if not current_sample_path or not os.path.exists(current_sample_path):
QMessageBox.warning(self, "错误", "请先设置有效的标准样本图像", QMessageBox.Ok)
is_processing = False
return
# 获取当前帧
if not hasattr(self, 'current_frame') or self.current_frame is None:
QMessageBox.warning(self, "错误", "无有效图像可用于检测", QMessageBox.Ok)
is_processing = False
return
test_image = self.current_frame
# 显示进度对话框
self.progress = QProgressDialog("正在检测布料质量...", "取消", 0, 100, self)
self.progress.setWindowModality(Qt.WindowModal)
self.progress.setValue(30)
# 获取参数
diff_threshold = self.sliderDiffThreshold.value() / 100.0
use_ssim = self.cbUseSSIM.isChecked()
# 启动图像处理线程
self.image_thread = ImageProcessingThread(
current_sample_path, test_image, diff_threshold, use_ssim
)
self.image_thread.processing_complete.connect(self.handle_processing_result)
self.image_thread.start()
def handle_processing_result(self, is_qualified, diff_ratio, marked_image):
"""处理检测结果"""
global is_processing, detection_history
self.progress.setValue(100)
self.progress.close()
if is_qualified is None:
QMessageBox.critical(self, "检测错误", "检测过程中发生错误", QMessageBox.Ok)
is_processing = False
return
# 更新UI显示
self.update_diff_display(diff_ratio, is_qualified)
# 显示结果
result_text = f"布料印花 {'合格' if is_qualified else '不合格'}\n差异度: {diff_ratio*100:.2f}%\n阈值: {self.sliderDiffThreshold.value()}%"
QMessageBox.information(self, "检测结果", result_text, QMessageBox.Ok)
# 显示标记图像
if marked_image is not None:
# 转换为QPixmap并显示
if len(marked_image.shape) == 2:
h, w = marked_image.shape
bytes_per_line = w
q_img = QImage(marked_image.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
else:
h, w, ch = marked_image.shape
bytes_per_line = ch * w
q_img = QImage(marked_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(q_img)
self.lblImageDisplay.setPixmap(pixmap.scaled(
self.lblImageDisplay.width(),
self.lblImageDisplay.height(),
Qt.KeepAspectRatio
))
self.statusBar.showMessage("已显示缺陷标记图像", 5000)
# 记录检测结果
detection_result = {
'timestamp': datetime.datetime.now(),
'qualified': is_qualified,
'diff_ratio': diff_ratio,
'threshold': self.sliderDiffThreshold.value()
}
detection_history.append(detection_result)
self.update_history_display()
is_processing = False
def update_diff_display(self, diff_ratio, is_qualified):
"""更新差异度显示"""
self.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%")
if is_qualified:
self.lblDiffStatus.setText("状态: 合格")
self.lblDiffStatus.setStyleSheet("color: green;")
else:
self.lblDiffStatus.setText("状态: 不合格")
self.lblDiffStatus.setStyleSheet("color: red;")
# ====================== 传感器相关函数 ======================
def sensor_triggered(self):
"""传感器触发时执行检测"""
if not self.cbEnableSensor.isChecked():
return
if not self.isGrabbing:
logging.warning("传感器触发时相机未就绪")
return
# 在实际系统中,这里会确保布料移动到正确位置
self.statusBar.showMessage("传感器触发 - 开始检测", 3000)
self.check_print()
def manual_sensor_trigger(self):
"""手动触发传感器"""
virtual_sensor.trigger()
self.statusBar.showMessage("手动触发传感器", 3000)
def set_sensor_delay(self):
"""设置传感器触发延迟"""
try:
delay = float(self.edtSensorDelay.text())
virtual_sensor.set_delay(delay)
self.save_settings()
self.statusBar.showMessage(f"传感器延迟已设置为 {delay} 秒", 3000)
except ValueError:
QMessageBox.warning(self, "输入错误", "请输入有效的数字(0.1-5.0)", QMessageBox.Ok)
def set_sensor_type(self, sensor_type):
"""设置传感器类型"""
virtual_sensor.set_type(sensor_type)
self.save_settings()
self.statusBar.showMessage(f"传感器类型已设置为 {sensor_type}", 3000)
def enable_sensor_mock(self, state):
"""启用/禁用传感器模拟"""
virtual_sensor.enable_mock(state == Qt.Checked)
self.bnStartMock.setEnabled(state == Qt.Checked)
self.bnStopMock.setEnabled(state == Qt.Checked)
self.spinMockInterval.setEnabled(state == Qt.Checked)
def start_mock_sensor(self):
"""启动模拟传感器"""
interval = self.spinMockInterval.value()
self.sensor_thread.start_mock(interval)
self.statusBar.showMessage(f"传感器模拟已启动,间隔 {interval}ms", 3000)
def stop_mock_sensor(self):
"""停止模拟传感器"""
self.sensor_thread.stop_mock()
self.statusBar.showMessage("传感器模拟已停止", 3000)
def closeEvent(self, event):
"""关闭应用程序时执行清理"""
self.save_settings()
# 停止传感器线程
if self.sensor_thread.isRunning():
self.sensor_thread.stop_mock()
self.sensor_thread.quit()
self.sensor_thread.wait(2000)
# 关闭相机
if self.cam:
self.close_device()
event.accept()
# ====================== 主程序入口 ======================
if __name__ == "__main__":
# 首先检查网络配置
if not check_network_configuration():
# 创建临时QApplication用于显示错误消息
app_temp = QApplication(sys.argv)
error_msg = "网络配置检查失败,无法检测到海康相机。请检查:\n\n"
error_msg += "1. 相机是否已正确连接并上电\n"
error_msg += "2. 计算机和相机是否在同一子网\n"
error_msg += "3. 防火墙是否阻止了相机通信\n"
error_msg += "4. 网线连接是否正常\n\n"
# 添加发现的相机信息(如果有)
if discovered_cameras:
error_msg += "发现的相机:\n"
for cam in discovered_cameras:
error_msg += f"- {cam['model']} (IP: {cam['ip']}, SN: {cam['serial']})\n"
QMessageBox.critical(None, "网络错误", error_msg, QMessageBox.Ok)
sys.exit(1)
# 如果网络检查通过,继续运行主应用
app = QApplication(sys.argv)
# 设置应用程序样式
app.setStyle("Fusion")
# 创建主窗口
main_window = MainWindow()
# 启动传感器线程
main_window.sensor_thread.start()
# 显示主窗口
main_window.show()
# 执行应用程序
sys.exit(app.exec_())
这个程序出现了下面的问题
Traceback (most recent call last):
File "d:\海康\MVS\Development\Samples\Python\MvImport\2.py", line 1307, in <module>
if not check_network_configuration():
File "d:\海康\MVS\Development\Samples\Python\MvImport\2.py", line 380, in check_network_configuration
if device_info.nTLayerType == MV_GIGE_DEVICE:
AttributeError: 'LP__MV_CC_DEVICE_INFO_' object has no attribute 'nTLayerType'
最新发布