CH343PT库使用<二>USB转串口设备描述符配置


前言

支持EEPROM配置的USB转串口设备型号有很多包括:CH340B、CH342F(USB转双串)、CH343P、CH344L/Q(USB转4串)、CH347T、CH348L/Q(USB转8串口)、CH9101U/R/Y、CH9102F、CH9103M(USB转双串)、CH‘9104L(USB转4串口)等型号。下面将使用CH343PT库中的接口去配置USB转串口设备CH9102F的EERPOM中的厂商字符串、产品字符串和Serial 字符串。


一、配置EEPROM接口函数介绍

1.1 芯片功能信息结构体

//芯片功能信息结构
typedef struct _USBSER_Property
{
	UCHAR    ChipType;              //芯片型号,USER_TYPE_CHxxx	
	CHAR     ChipTypeStr[32];       //芯片型号字符串
	CHAR     FwVerStr[32];          //固件版本字符串
	UCHAR    GpioCount;             //GPIO脚数,如果大于0,此型号有专用GPIO脚或复用脚;为0表示芯片不支持GPIO引脚
	BOOL     IsEmbbedEeprom;        //是否内置EEPROM,如支持,可进行设备信息的配置	
	BOOL     IsSupportMcuBootCtrl;  //是否支持Modem引脚作为103 MCU下载控制
	CHAR     ManufacturerString[64];//USB描述符内厂商字符串
	CHAR     ProductString[64];     //USB描述符内产品字符串
	USHORT   bcdDevice;             //USB描述符内bcdDevice值
	UCHAR    PortIndex;             //单串为0,如多串则为芯片第几个号串口
	BOOL     IsSupportGPIOInit;     //是否支持GPIO上电初始化设置
	CHAR     PortName[32];          //串口号
	ULONG    ResvD[8];              //预留数据
}ChipPropertyS,*pChipPropertyS;

如果想过滤CH9102F,可以使用该结构体中ChipType去判断芯片是否是CH9102F。代码实现如下:

BOOL IsCH9102F(HANDLE hCom, pChipPropertyS ChipProperty)
{
	CH343PT_GetChipProperty(hCom, ChipProperty);
	if(ChipProperty->ChipType == USER_TYPE_CH9102F)
		return TRUE;
	else
		return FALSE;
}

使用该函数会打开关闭未占用的串口(COM端口),如果想避免打开串口,可以用友好名称中CH9102F去过滤,如下图所示。
在这里插入图片描述

1.2 USB串口芯片配置EEPROM结构体

//除CH340和CH341之外的所有USB串口芯片配置EEPROM
typedef struct _USERCFG_343
{
	UCHAR SIG;      //对于 CH340B:内部配置信息有效标志,必须是 5BH。
	//对于 CH340H/S:外部配置芯片有效标志,必须是 53H。
	//对于 CH343??:外部配置芯片有效标志,必须是 53H。
	//其它值则配置无效
	UCHAR MODE;     //串口模式,必须是23H
	UCHAR CFG;      //芯片功能配置字节
	//BIT7:USB设备序列号字符串控制位;1=有效;0=无效;
	//BIT6:USB产品信息字符串控制位;1=有效;0=无效;
	//BIT5:USB厂商信息字符串控制位;1=有效;0=无效;
	//BIT4:CDC模式下是否启用硬件流控;1=启用;0=禁止;
	//BIT3:芯片引脚是否启用EEPROM的默认配置;如果启用则芯片上电后会根据EEPROM中的配置初始化引脚方向及电平状态,以及使能状态。具体配置值见后面说明。 1=启用;0=禁止;
	UCHAR WP;       //内部配置信息写保护标志,为 57H 则只读,否则可改写
	UCHAR Vid[2];   //Vendor ID,厂商识别码,高字节在后,任意值。
	UCHAR Pid[2];   //Product ID,产品识别码,高字节在后,任意值
	UCHAR BCD[2];   //高字节在后,任意值
	UCHAR Power;    //Max Power,USB电流配置字,以2mA为单位的最大电源电流
	UCHAR Attributes;//	USB供电方式、睡眠唤醒等功能配置信息
	//BIT7:必须设置成1;
	//BIT6:1= Self-Powered;
	//0=Bus-Powered;
	//BIT5:1= Remote Wakeup Enable;
	//0= Remote Wakeup Disable;
	//BIT4-BIT0:必须设置成0;
	UCHAR Reserved[4]; //保留字节00H或FFH
	CHAR  SN[24];    //USB设备序列号字符串.首字节是全部字节数(不超过16H),次字节开始为标准的USB字符串描述符格式(第1个字节为总长度,第2个字节固定为03H,第3个字节开始奇数为Unicode字符,偶数为0x00),如果不符合上述特征则使用厂商默认说明。
	UCHAR PROD[40];  //USB产品信息字符串.首字节是全部字节数(不超过28H),次字节开始为标准的USB字符串描述符格式(第1个字节为总长度,第2个字节固定为03H,第3个字节开始奇数为Unicode字符,偶数为0x00),如果不符合上述特征则使用厂商默认说明。
	UCHAR MAU[40];   //USB厂商信息字符串.首字节是全部字节数(不超过28H),次字节开始为标准的USB字符串描述符格式(第1个字节为总长度,第2个字节固定为03H,第3个字节开始奇数为Unicode字符,偶数为0x00),如果不符合上述特征则使用厂商默认说明。
}USERCFG_343,*pUSERCFG_343;

从结构体可以看出厂商字符串最多可以支持配置18个字节,产品字符串最多可以支持配置18个字节,Serial String最多可以支持配置10个字节。

1.3 进入芯片配置模式和退出芯片配置模式

调用CH343PT_EnterConfigMode函数成功后才能读取和写入EEPROM中的数据,成功调用CH343PT_ExitConfigMode函数后退出EEPROM配置模式。

//进入芯片配置模式:300bpsp;可进行非标准波特率配置和配置空间读写
BOOL WINAPI	CH343PT_EnterConfigMode(HANDLE iPortH);
//退出芯片配置模式:300bps
BOOL WINAPI	CH343PT_ExitConfigMode(HANDLE iPortH);

1.4 读取和写入芯片EEPROM中的信息

调用CH343PT_ReadDevConfig函数后可以读取芯片EEPROM中VID、PID、最大电流、厂商字符串、产品字符串、Serial字符串等信息。修改EEPROM中厂商字符串、产品字符串、Serial字符串需要调用CH343PT_WriteDevConfig函数。

//通过串口读取芯片EERPOM内设备配置数据
BOOL    WINAPI	 CH343PT_ReadDevConfig(HANDLE          iPortH,     // 指定CH341设备序号
									   PULONG          DataLen,    // 要读取的数据长度
									   PUCHAR          DataBuf);    // 数据缓冲区
//通过串口向芯片EERPOM内写入设备的配置数据,写入前需确保写位保护位已关闭。
BOOL    WINAPI	 CH343PT_WriteDevConfig(HANDLE          iPortH,
										ULONG           BufferSize,
										PUCHAR          DataBuf);

二、使用上述接口去配置CH9102F的EEPROM

2.1 进入EEPROM配置和退出EEPROM配置模式

代码实现如下:

//打开串口进入EEPROM配置模式
BOOL OpenCH34xCom(HANDLE hCom, LPCTSTR ComName)
{
	//打开串口	
	hCom = CreateFile(ComName, GENERIC_READ|GENERIC_WRITE,
			0,NULL,OPEN_EXISTING,
			NULL,
			NULL);
	if(hCom == INVALID_HANDLE_VALUE)
	{
		return FALSE;
	}
	CH343PT_EnterConfigMode(hCom); //进入EEPROM配置模式
	return TRUE;
}

//关闭串口退出EEPROM配置模式
BOOL CloseCH34xCom(HANDLE hCom)
{
	if (INVALID_HANDLE_VALUE != hCom)
	{
		CH343PT_ExitConfigMode(hCom); //退出EERPOM配置模式
		If(!CloseHandle(hCom)) //关闭串口
			return FALSE;
		hCom = INVALID_HANDLE_VALUE;		
	}
	return TRUE;
}

2.2 读写芯片中的EEPROM参数

//读EEPROM中厂商字符串
BOOL ReadEEPROMManuStr(USERCFG_343 *CH34XSetSerialStr, PCHAR ManuStrBuffer)
{
	UCHAR StrSize, StrType;

	//显示厂商字符串
	StrSize = CH34XSetSerialStr->MAU[0];
	StrType = CH34XSetSerialStr->MAU[2];
	if( ( StrSize>0 ) &&     //字符串长度
		( StrType == 0x03 ) )//固定字符串类型
		wcstombs(&ManuStrBuffer[0],(WCHAR *)&CH34XSetSerialStr->MAU[3],(CH34XSetSerialStr->MAU[0] - 0)/2);
	else //无效字符串
		return FALSE;
	return TRUE;
}

//读取EEPROM中的产品字符串
BOOL ReadEEPROMProdStr(USERCFG_343 *CH34XSetSerialStr, PCHAR ProdStrBuffer)
{
	UCHAR StrSize, StrType;
	//显示产品字符串
	StrSize = CH34XSetSerialStr->PROD[0];
	StrType = CH34XSetSerialStr->PROD[2];
	if( ( StrSize>0 ) &&     //字符串长度
		( StrType == 0x03 ) )//固定字符串类型	
		wcstombs(&ProdStrBuffer[0],(WCHAR *)&CH34XSetSerialStr->PROD[3],(CH34XSetSerialStr->PROD[0] - 2)/2);

	else //无效字符串
		return FALSE;
	return TRUE;
}

//读取EEPROM中的SN字符串
BOOL ReadEEPROMSNStr(USERCFG_343 *CH34XSetSerialStr, PCHAR SNStrBuffer)
{
	UCHAR StrSize, StrType;

	//获取EERPROM中的SN字符串
	StrSize = CH34XSetSerialStr->SN[0];
	StrType = CH34XSetSerialStr->SN[2];
	if( ( StrSize>0 ) &&     //字符串长度
		( StrType == 0x03 ) )//固定字符串类型	
		sprintf(SNStrBuffer,"%S",&CH34XSetSerialStr->SN[3]);
	else //无效字符串
		return FALSE;
	return TRUE;
}

//写EEPROM中的厂商字符串,产品字符串,SN字符串
BOOL WriteCH34xEEPROMStr(USERCFG_343 *CH34XSetSerialStr)
{
	ULONG CH34xEepromSize;
	
	CH34xEepromSize = sizeof(CH34XSetSerialStr); //获取配置空间大小
	if( !CH343PT_WriteDevConfig(hCom,CH34xEepromSize,(PUCHAR)&CH34XSetSerialStr) ) //写EEPROM
	{ 
		return FALSE;
	}
	return TRUE;
}

三、软件实现

使用VC6++创建一个对话框,界面如下。
在这里插入图片描述

3.1 获取界面中配置信息

//读取EDIT控件中的厂商字符串,并传入CH34XSetSerialStr结构体中
VOID ReadManuEditConfig(USERCFG_343 *CH34XSetSerialStr,PCHAR MauBuffer, ULONG ManuStrLength)
{
	//获取厂商字符串Manufacture String
	memset(CH34XSetSerialStr->MAU, 0, sizeof(CH34XSetSerialStr->MAU));
	mbstowcs((PWCHAR)(&CH34XSetSerialStr->MAU[3]),MauBuffer,ManuStrLength);
	CH34XSetSerialStr->MAU[0] = ManuStrLength*2+2; //字符个数
	CH34XSetSerialStr->MAU[1] = ManuStrLength*2+2; //字符个数
	CH34XSetSerialStr->MAU[2] = 0x03;        //字符串类型,固定03
	CH34XSetSerialStr->CFG |= 0x20; //设置序列号有效标志
	return;
}

//读取EDIT控件中的产品字符串,并传入CH34XSetSerialStr结构体中
VOID ReadProdEditConfig(USERCFG_343 *CH34XSetSerialStr,PCHAR ProdBuffer, ULONG ProdStrLength)
{
	//获取产品字符串Product String
	memset(CH34XSetSerialStr->PROD, 0, sizeof(CH34XSetSerialStr->PROD));
	mbstowcs((PWCHAR)(&CH34XSetSerialStr->PROD[3]),ProdBuffer,ProdStrLength);
	CH34XSetSerialStr->PROD[0] = ProdStrLength*2+2; //字符个数
	CH34XSetSerialStr->PROD[1] = ProdStrLength*2+2; //字符个数
	CH34XSetSerialStr->PROD[2] = 0x03;        //字符串类型,固定03
	CH34XSetSerialStr->CFG |= 0x40; //设置序列号有效标志
	return;
}

//读取EDIT控件中的serial string字符串,并传入CH34XSetSerialStr结构体中
VOID ReadSNEditConfig(USERCFG_343 *CH34XSetSerialStr, PCHAR SNBuffer, ULONG SNStrLength)
{
	//获取Serial String
	memset(CH34XSetSerialStr->SN, 0, sizeof(CH34XSetSerialStr->SN));
	mbstowcs((PWCHAR)(&CH34XSetSerialStr->SN[3]),SNBuffer,SNStrLength);
	CH34XSetSerialStr->SN[0] = SNStrLength*2+2; //字符个数
	CH34XSetSerialStr->SN[1] = SNStrLength*2+2; //字符个数
	CH34XSetSerialStr->SN[2] = 0x03; //字符串类型,固定为0x03
	CH34XSetSerialStr->CFG |= 0x80; //设置序列号有效标志
	return;
}
//读取界面中的厂商、产品和Serial字符串
VOID ReadEditConfig(USERCFG_343 *CH34XSetSerialStr)
{
	CHAR SerStr[1024] = {0};
	CHAR ProductStr[1024] = {0};
	CHAR MauBuffer[256] = ""; //需写入的厂商字符串
	CHAR ProdBuffer[256] = "";
	CHAR SNBuffer[256] = "";
	CHAR Temp[20] = {0};
	CHAR buf[512] ="";
	USHORT bcdDevice;
	ULONG ManuStrLength,ProdStrLength,SNStrLength;
	
	CH34XSetSerialStr->SIG = 0x53;
	CH34XSetSerialStr->MODE = 0x23;
	CH34XSetSerialStr->CFG = 0xE0;
	*(PUSHORT)CH34XSetSerialStr->Pid = 0x55D4;
	*(PUSHORT)CH34XSetSerialStr->Vid = 0x1A86;
	//*(PUSHORT)CH34XSetSerialStr->BCD = 0x0442;
	CH34XSetSerialStr->Power = 0x44;
	CH34XSetSerialStr->Attributes = 0xA0;
	
	//获取bcdDevice
	CH343PT_GetUsbComDevBcd(hCom,&bcdDevice);
	*(PUSHORT)CH34XSetSerialStr->BCD = bcdDevice;
	//获取厂商字符串Manufacture String
	memset(MauBuffer,0,sizeof(MauBuffer));
	GetDlgItemText(AfxMainHwnd,IDC_ManuString,MauBuffer,sizeof(MauBuffer));
	ManuStrLength = strlen(MauBuffer);
	ReadManuEditConfig(CH34XSetSerialStr,MauBuffer,ManuStrLength);
	
	//获取产品字符串Product String
	memset(ProdBuffer,0,sizeof(ProdBuffer));
	GetDlgItemText(AfxMainHwnd,IDC_ProductString,ProdBuffer,sizeof(ProdBuffer));
	ProdStrLength = strlen(ProdBuffer);	
	ReadProdEditConfig(CH34XSetSerialStr,ProdBuffer,ProdStrLength);

	//获取Serial String
	memset(SNBuffer,0,sizeof(SNBuffer));
	GetDlgItemText(AfxMainHwnd,IDC_SerialString,SNBuffer,sizeof(SNBuffer));
	SNStrLength = strlen(SNBuffer);
	ReadSNEditConfig(CH34XSetSerialStr,SNBuffer,SNStrLength);
	return;
}

3.2 在界面中显示读取EEPROM中的厂商、产品和Serial字符串

//读EEPROM中厂商字符串,产品字符串,SN字符串
BOOL ReadEEPROMStr(HANDLE hCom, USERCFG_343 *CH34XSetSerialStr, LPCTSTR ComName)
{
	CHAR TempStr[1024] = "";
	CHAR ManuStrBuffer[64] = "";
	CHAR ProdStrBuffer[256] = "";
	CHAR SNStrBuffer[64] = "";
	ULONG CH34xEepromSize,Retval;
	
	if(!OpenCH34xCom(hCom,ComName))
	{
		goto Exit;
	}
	
	CH34xEepromSize = sizeof(CH34XSetSerialStr);
	if( !CH343PT_ReadDevConfig(hCom,&CH34xEepromSize,(PUCHAR)&CH34XSetSerialStr) )
	{	
		goto Exit;
	}
	//读EEPROM中厂商字符串,产品字符串,SN字符串
	Retval = ReadEEPROMManuStr(CH34XSetSerialStr, ManuStrBuffer);
	if(Retval)
		SetDlgItemText(AfxMainHwnd,IDC_ManuString,ManuStrBuffer);
	else
		SetDlgItemText(AfxMainHwnd,IDC_ManuString,"");
	Retval = ReadEEPROMProdStr(CH34XSetSerialStr, ProdStrBuffer);
	if(Retval)
		SetDlgItemText(AfxMainHwnd,IDC_ProductString,ProdStrBuffer);
	else
		SetDlgItemText(AfxMainHwnd,IDC_ProductString,"");
	Retval = ReadEEPROMSNStr(CH34XSetSerialStr, SNStrBuffer);
	if(Retval)
		SetDlgItemText(AfxMainHwnd,IDC_SerialString,SNStrBuffer);
	else
		SetDlgItemText(AfxMainHwnd,IDC_SerialString,"");

	CloseCH34xCom(hCom); //关闭串口并退出配置模式
	return TRUE;
Exit:
	CloseCH34xCom(hCom); //关闭串口并退出配置模式
	return FALSE;
}

读取芯片默认的参数,如下图所示。
在这里插入图片描述

3.3 写入EEPROM配置

//写EEPROM中的厂商字符串,产品字符串,SN字符串
BOOL WriteEEPROMStr(USERCFG_343 CH34XSetSerialStr, LPCTSTR ComName)
{
	USERCFG_343 CH34XSetSerialStr;
	
	//打开串口进入EEPROM配置模式
	if(!OpenCH34xCom(hCom, ComName))
	{
		CloseCH34xCom(hCom); 
		return FALSE;
	}
	ReadEditConfig(CH34XSetSerialStr); //读取界面中的厂商、产品和Serial字符串
	WriteCH34xEEPROMStr(CH34XSetSerialStr)
	{
		CloseCH34xCom(hCom); 
		return FALSE;
	}
	CloseCH34xCom(hCom); //关闭串口并退出EEPROM配置模式 
	ResetFlag = TRUE;
	return TRUE;
}

写入成功可以用USB View工具查看是否修改成功,如下图所示。
写入配置界面
在这里插入图片描述
USBView界面可以看出修改后厂商、产品和Serial字符串已经配置成功了。
在这里插入图片描述
上述demo函数调用方法去配置芯片EEPROM参数,同样适应于支持EEPROM配置的芯片(CH342、CH343、CH344、CH347、CH348、CH9101、CH9102、CH9103、CH9104)。

# -*- 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'
最新发布
07-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PC技术小能手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值