# -*- coding: utf-8 -*-
import sys
import os
import cv2
import numpy as np
import time
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout,
QMessageBox, QLabel, QFileDialog, QToolBar, QComboBox, QStatusBar, QGroupBox,
QSlider, QDockWidget, QProgressDialog, QLineEdit, QRadioButton, QGridLayout, QSpinBox
)
from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal
from CamOperation_class import CameraOperation
sys.path.append("D:\\海康\\MVS\\Development\\Samples\\Python\\BasicDemo")
import ctypes
from ctypes import cast, POINTER
from datetime import datetime
import logging
import socket
import serial
import skimage
import platform
from CameraConstants import *
import threading
import time
import sys
class ManagedThread(threading.Thread):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._stop_event = threading.Event()
# 设置为非守护线程
self.daemon = False
def stop(self):
"""安全停止线程"""
self._stop_event.set()
def should_stop(self):
"""检查是否应该停止"""
return self._stop_event.is_set()
def worker():
"""线程工作函数"""
try:
while not threading.current_thread().should_stop():
# 模拟工作
time.sleep(1)
# 安全输出
sys.stdout.write("Working...\n")
except Exception as e:
# 避免在关闭时使用stderr
pass
def main():
# 创建并启动线程
threads = []
for _ in range(3):
t = ManagedThread(target=worker)
t.start()
threads.append(t)
try:
# 主程序逻辑
time.sleep(5)
finally:
# 安全停止所有线程
for t in threads:
t.stop()
for t in threads:
t.join(timeout=2.0) # 设置超时避免无限等待
# 确保所有输出完成
sys.stdout.flush()
sys.stderr.flush()
# 在导入部分添加
from CameraParams_header import (
MV_GIGE_DEVICE,
MV_USB_DEVICE,
MV_GENTL_CAMERALINK_DEVICE,
MV_GENTL_CXP_DEVICE,
MV_GENTL_XOF_DEVICE
)
# ===== 路径修复 =====
def fix_sdk_path():
"""修复海康SDK的加载路径"""
if getattr(sys, 'frozen', False): # 打包模式
base_path = sys._MEIPASS
mvimport_path = os.path.join(base_path, "MvImport")
if mvimport_path not in sys.path:
sys.path.insert(0, mvimport_path)
if sys.platform == 'win32':
os.environ['PATH'] = base_path + os.pathsep + os.environ['PATH']
try:
ctypes.WinDLL(os.path.join(base_path, "MvCamCtrldll.dll"))
except OSError as e:
print(f"核心DLL加载失败: {e}")
sys.exit(1)
else: # 开发模式
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 立即执行路径修复
fix_sdk_path()
# ===== 正确导入SDK模块 =====
try:
from MvImport.MvCameraControl_class import MvCamera
print("成功导入MvCamera类")
from CameraParams_header import *
from MvErrorDefine_const import *
except ImportError as e:
print(f"SDK导入失败: {e}")
sys.exit(1)
# 配置日志系统
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 = []
isGrabbing = False
isOpen = False
obj_cam_operation = None
frame_monitor_thread = None
sensor_monitor_thread = None
sensor_controller = None
MV_OK = 0
MV_E_CALLORDER = -2147483647
# ==================== 传感器通讯模块 ====================
class SensorController:
def __init__(self):
self.connected = False
self.running = False
self.connection = None
def connect(self, config):
try:
if config['type'] == 'serial':
self.connection = serial.Serial(
port=config['port'],
baudrate=config['baudrate'],
timeout=config.get('timeout', 1.0)
)
else:
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connection.connect((config['ip'], config['port']))
self.connection.settimeout(config.get('timeout', 1.0))
self.connected = True
self.running = True
logging.info(f"传感器连接成功: {config}")
return True
except Exception as e:
logging.error(f"传感器连接失败: {str(e)}")
return False
def disconnect(self):
if self.connection:
try:
self.connection.close()
except:
pass
self.connection = None
self.connected = False
self.running = False
logging.info("传感器已断开")
def read_data(self):
if not self.connected:
return None
return {
'tension': np.random.uniform(10.0, 20.0),
'speed': np.random.uniform(1.0, 5.0),
'temperature': np.random.uniform(20.0, 30.0),
'humidity': np.random.uniform(40.0, 60.0)
}
def wait_for_material(self, delay_seconds=0):
if not self.connected:
logging.warning("未连接传感器,跳过等待")
return False
logging.info(f"等待布料到达,延迟 {delay_seconds} 秒")
start_time = time.time()
while time.time() - start_time < delay_seconds:
QThread.msleep(100)
if not self.running:
return False
logging.info("布料已到位,准备拍摄")
return True
class SensorMonitorThread(QThread):
data_updated = pyqtSignal(dict)
def __init__(self, sensor_controller):
super().__init__()
self.sensor_controller = sensor_controller
self.running = True
def run(self):
while self.running:
if self.sensor_controller and self.sensor_controller.connected:
try:
data = self.sensor_controller.read_data()
if data:
self.data_updated.emit(data)
except Exception as e:
logging.error(f"传感器数据读取错误: {str(e)}")
QThread.msleep(500)
def stop(self):
self.running = False
self.wait(2000)
def wait_for_material(self, delay_seconds):
return self.sensor_controller.wait_for_material(delay_seconds)
# ==================== 相机帧监控线程 ====================
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
# ==================== 优化后的检测算法 ====================
def enhanced_check_print_quality(sample_image_path, test_image, threshold=0.05, sensor_data=None):
if sensor_data:
speed_factor = min(1.0 + sensor_data['speed'] * 0.1, 1.5)
env_factor = 1.0 + abs(sensor_data['temperature'] - 25) * 0.01 + abs(sensor_data['humidity'] - 50) * 0.005
adjusted_threshold = threshold * speed_factor * env_factor
logging.info(f"根据传感器数据调整阈值: 原始={threshold:.4f}, 调整后={adjusted_threshold:.4f}")
else:
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:
orb = cv2.ORB_create(nfeatures=200)
keypoints1, descriptors1 = orb.detectAndCompute(sample_image, None)
keypoints2, descriptors2 = orb.detectAndCompute(test_image_gray, None)
if descriptors1 is None or descriptors2 is None:
logging.warning("无法提取特征描述符,跳过配准")
aligned_sample = sample_image
else:
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(descriptors1, descriptors2)
matches = sorted(matches, key=lambda x: x.distance)
if len(matches) > 10:
src_pts = np.float32([keypoints1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
dst_pts = np.float32([keypoints2[m.trainIdx].pt for m in 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("特征点匹配不足,跳过图像配准")
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]
labels = skimage.measure.label(thresholded)
properties = skimage.measure.regionprops(labels)
for prop in properties:
if prop.area > 50:
y, x = prop.centroid
cv2.putText(marked_image, f"Defect", (int(x), int(y)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
return is_qualified, diff_ratio, marked_image
# ==================== 传感器控制的质量检测流程 ====================
def sensor_controlled_check():
global isGrabbing, obj_cam_operation, current_sample_path, detection_history, sensor_controller
logging.info("质量检测启动")
sensor_data = None
if sensor_controller and sensor_controller.connected:
sensor_data = sensor_controller.read_data()
if not sensor_data:
QMessageBox.warning(mainWindow, "传感器警告", "无法读取传感器数据,将使用默认参数", QMessageBox.Ok)
else:
logging.info("未连接传感器,使用默认参数检测")
check_print_with_sensor(sensor_data)
def check_print_with_sensor(sensor_data=None):
global isGrabbing, obj_cam_operation, current_sample_path, detection_history
logging.info("检测印花质量按钮按下")
if not isGrabbing:
QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok)
return
if not obj_cam_operation:
QMessageBox.warning(mainWindow, "错误", "相机未正确初始化!", QMessageBox.Ok)
return
if not current_sample_path or not os.path.exists(current_sample_path):
QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok)
return
progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow)
progress.setWindowModality(Qt.WindowModal)
progress.setValue(10)
try:
test_image = obj_cam_operation.get_current_frame()
progress.setValue(30)
if test_image is None:
QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok)
return
diff_threshold = mainWindow.sliderDiffThreshold.value() / 100.0
logging.info(f"使用差异度阈值: {diff_threshold}")
progress.setValue(50)
is_qualified, diff_ratio, marked_image = enhanced_check_print_quality(
current_sample_path,
test_image,
threshold=diff_threshold,
sensor_data=sensor_data
)
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.imshow("缺陷标记结果", marked_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
detection_result = {
'timestamp': datetime.now(),
'qualified': is_qualified,
'diff_ratio': diff_ratio,
'threshold': diff_threshold,
'sensor_data': sensor_data if sensor_data else {}
}
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 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 save_sample_image():
global isGrabbing, obj_cam_operation, current_sample_path
if not isGrabbing:
QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok)
return
# 检查是否有可用帧
if not obj_cam_operation.is_frame_available():
QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok)
return
settings = QSettings("ClothInspection", "CameraApp")
last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures"))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_filename = f"sample_{timestamp}"
file_path, selected_filter = QFileDialog.getSaveFileName(
mainWindow,
"保存标准样本图像",
os.path.join(last_dir, default_filename),
"BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)",
options=QFileDialog.DontUseNativeDialog
)
if not file_path:
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()
format_mapping = {".bmp": "bmp", ".png": "png", ".jpg": "jpg", ".jpeg": "jpg"}
save_format = format_mapping.get(file_extension)
if not save_format:
QMessageBox.warning(mainWindow, "错误", "不支持的文件格式!", QMessageBox.Ok)
return
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
try:
os.makedirs(directory, exist_ok=True)
except OSError as e:
QMessageBox.critical(mainWindow, "目录创建错误", f"无法创建目录 {directory}: {str(e)}", QMessageBox.Ok)
return
try:
ret = obj_cam_operation.save_image(file_path, save_format)
if ret != MV_OK:
strError = f"保存样本图像失败: {hex(ret)}"
QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok)
else:
QMessageBox.information(mainWindow, "成功", f"标准样本已保存至:\n{file_path}", QMessageBox.Ok)
current_sample_path = file_path
update_sample_display()
settings.setValue("last_save_dir", os.path.dirname(file_path))
except Exception as e:
QMessageBox.critical(mainWindow, "异常错误", f"保存图像时发生错误: {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:
img_data = np.fromfile(current_sample_path, dtype=np.uint8)
sample_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
if sample_img is None:
raise Exception("无法加载图像")
cv2.imshow("标准样本预览", sample_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
except Exception as e:
QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok)
def update_sample_display():
global current_sample_path
if current_sample_path:
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}%"
mainWindow.cbHistory.addItem(f"[极客{timestamp}] {status} - 差异: {ratio}")
def TxtWrapBy(start_str, end, all):
start = all.find(start_str)
if start >= 0:
start += len(start_str)
end = all.find(end, start)
if end >= 0:
return all[start:end].strip()
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 decoding_char(c_ubyte_value):
c_char_p_value = ctypes.cast(c_ubyte_value, ctypes.c_char_p)
try:
decode_str = c_char_p_value.value.decode('gbk')
except UnicodeDecodeError:
decode_str = str(c_char_p_value.value)
return decode_str
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 != 0:
strError = "Enum devices fail! ret = :" + ToHexStr(ret)
QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok)
return ret
if deviceList.nDeviceNum == 0:
QMessageBox.warning(mainWindow, "Info", "Find no device", QMessageBox.Ok)
return ret
print(f"Find {deviceList.nDeviceNum} devices!")
# 处理设备信息
devList = []
for i in range(0, deviceList.nDeviceNum):
# 获取设备信息指针
device_info = deviceList.pDeviceInfo[i]
# 转换为正确的结构体类型
mvcc_dev_info = cast(device_info, POINTER(MV_CC_DEVICE_INFO)).contents
# 根据设备类型提取信息 - 关键修复:使用正确的结构体名称
if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE:
# 处理GigE设备信息 - 使用正确的stGigEInfo
try:
# 尝试访问新版SDK的结构体
st_gige_info = mvcc_dev_info.SpecialInfo.stGigEInfo
except AttributeError:
try:
# 尝试访问旧版SDK的结构体
st_gige_info = mvcc_dev_info.SpecialInfo.stGEInfo
except AttributeError:
# 如果两种结构体都不存在,使用默认值
devList.append(f"[{i}]GigE: Unknown Device")
continue
user_defined_name = decoding_char(st_gige_info.chUserDefinedName)
model_name = decoding_char(st_gige_info.chModelName)
nip1 = ((st_gige_info.nCurrentIp & 0xff000000) >> 24)
nip2 = ((st_gige_info.nCurrentIp & 0x00ff0000) >> 16)
nip3 = ((st_gige_info.nCurrentIp & 0x0000ff00) >> 8)
nip4 = (st_gige_info.nCurrentIp & 0x000000ff)
devList.append(f"[{i}]GigE: {user_defined_name} {model_name}({nip1}.{nip2}.{nip3}.{nip4})")
elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE:
# 处理USB设备信息
try:
st_usb_info = mvcc_dev_info.SpecialInfo.stUsb3VInfo
except AttributeError:
try:
st_usb_info = mvcc_dev_info.SpecialInfo.stUsbInfo
except AttributeError:
devList.append(f"[{i}]USB: Unknown Device")
continue
user_defined_name = decoding_char(st_usb_info.chUserDefinedName)
model_name = decoding_char(st_usb_info.chModelName)
strSerialNumber = ""
for per in st_usb_info.chSerialNumber:
if per == 0:
break
strSerialNumber = strSerialNumber + chr(per)
devList.append(f"[{i}]USB: {user_defined_name} {model_name}({strSerialNumber})")
else:
# 处理其他类型设备
devList.append(f"[{i}]Unknown Device Type: {mvcc_dev_info.nTLayerType}")
# 更新UI
mainWindow.ComboDevices.clear()
mainWindow.ComboDevices.addItems(devList)
mainWindow.ComboDevices.setCurrentIndex(0)
# 更新状态栏
mainWindow.statusBar().showMessage(f"找到 {deviceList.nDeviceNum} 个设备", 3000)
return MV_OK
# ===== 关键改进:相机操作函数 =====
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
# 关键改进:添加相机状态检查
if not obj_cam_operation or not hasattr(obj_cam_operation, 'cam') or not obj_cam_operation.cam:
QMessageBox.warning(mainWindow, "Error", "相机对象未正确初始化", QMessageBox.Ok)
return
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)
def stop_grabbing():
global obj_cam_operation, isGrabbing
# 关键改进:添加相机状态检查
if not obj_cam_operation or not hasattr(obj_cam_operation, 'cam') or not obj_cam_operation.cam:
QMessageBox.warning(mainWindow, "Error", "相机对象未正确初始化", QMessageBox.Ok)
return
# 关键改进:添加连接状态检查
if not hasattr(obj_cam_operation, 'connected') or not obj_cam_operation.connected:
QMessageBox.warning(mainWindow, "Error", "相机未连接", QMessageBox.Ok)
return
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()
def close_device():
global isOpen, isGrabbing, obj_cam_operation, frame_monitor_thread
if frame_monitor_thread and frame_monitor_thread.isRunning():
frame_monitor_thread.stop()
frame_monitor_thread.wait(2000)
if isOpen and obj_cam_operation:
# 关键改进:确保相机对象存在
if hasattr(obj_cam_operation, 'cam') and obj_cam_operation.cam:
obj_cam_operation.close_device()
isOpen = False
isGrabbing = False
enable_controls()
def set_continue_mode():
# 关键改进:添加相机状态检查
if not obj_cam_operation or not hasattr(obj_cam_operation, 'cam') or not obj_cam_operation.cam:
return
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():
# 关键改进:添加相机状态检查
if not obj_cam_operation or not hasattr(obj_cam_operation, 'cam') or not obj_cam_operation.cam:
return
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():
# 关键改进:添加相机状态检查
if not obj_cam_operation or not hasattr(obj_cam_operation, 'cam') or not obj_cam_operation.cam:
return
ret = obj_cam_operation.trigger_once()
if ret != 0:
strError = "软触发失败 ret:" + ToHexStr(ret)
QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok)
def save_sample_image():
global isGrabbing, obj_cam_operation, current_sample_path
if not isGrabbing:
QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok)
return
# 尝试捕获当前帧
frame = obj_cam_operation.capture_frame()
if frame is None:
QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok)
return
# 确保图像有效
if frame.size == 0 or frame.shape[0] == 0 or frame.shape[1] == 0:
QMessageBox.warning(mainWindow, "无效图像", "捕获的图像无效,请检查相机设置!", QMessageBox.Ok)
return
settings = QSettings("ClothInspection", "CameraApp")
last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures"))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_filename = f"sample_{timestamp}"
file_path, selected_filter = QFileDialog.getSaveFileName(
mainWindow,
"保存标准样本图像",
os.path.join(last_dir, default_filename),
"BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)",
options=QFileDialog.DontUseNativeDialog
)
if not file_path:
return
# 确保文件扩展名正确
file_extension = os.path.splitext(file_path)[1].lower()
if not file_extension:
if "BMP" in selected_filter:
file_path += ".bmp"
elif "PNG" in selected_filter:
file_path += ".png"
elif "JPEG" in selected_filter or "JPG" in selected_filter:
file_path += ".jpg"
else:
file_path += ".bmp"
file_extension = os.path.splitext(file_path)[1].lower()
# 创建目录(如果不存在)
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
try:
os.makedirs(directory, exist_ok=True)
except OSError as e:
QMessageBox.critical(mainWindow, "目录创建错误", f"无法创建目录 {directory}: {str(e)}", QMessageBox.Ok)
return
# 保存图像
try:
# 使用OpenCV保存图像
if not cv2.imwrite(file_path, frame):
raise Exception("OpenCV保存失败")
# 更新状态
current_sample_path = file_path
update_sample_display()
settings.setValue("last_save_dir", os.path.dirname(file_path))
# 显示成功消息
QMessageBox.information(mainWindow, "成功", f"标准样本已保存至:\n{file_path}", QMessageBox.Ok)
# 可选:自动预览样本
preview_sample()
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.imshow("标准样本预览", sample_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
except Exception as e:
QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok)
def start_grabbing():
global obj_cam_operation, isGrabbing
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)
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))
def update_sensor_display(data):
if not data:
return
text = (f"张力: {data['tension']:.2f}N | "
f"速度: {data['speed']:.2f}m/s | "
f"温度: {data['temperature']:.1f}°C | "
f"湿度: {data['humidity']:.1f}%")
mainWindow.lblSensorData.setText(text)
def connect_sensor():
global sensor_monitor_thread, sensor_controller
sensor_type = mainWindow.cbSensorType.currentText()
if sensor_controller is None:
sensor_controller = SensorController()
if sensor_type == "串口":
config = {
'type': 'serial',
'port': mainWindow.cbComPort.currentText(),
'baudrate': int(mainWindow.cbBaudrate.currentText()),
'timeout': 1.0
}
else:
config = {
'type': 'ethernet',
'ip': mainWindow.edtIP.text(),
'port': int(mainWindow.edtPort.text()),
'timeout': 1.0
}
if sensor_controller.connect(config):
mainWindow.bnConnectSensor.setEnabled(False)
mainWindow.bnDisconnectSensor.setEnabled(True)
sensor_monitor_thread = SensorMonitorThread(sensor_controller)
sensor_monitor_thread.data_updated.connect(update_sensor_display)
sensor_monitor_thread.start()
def disconnect_sensor():
global sensor_monitor_thread
if sensor_controller:
sensor_controller.disconnect()
mainWindow.bnConnectSensor.setEnabled(True)
mainWindow.bnDisconnectSensor.setEnabled(False)
if sensor_monitor_thread and sensor_monitor_thread.isRunning():
sensor_monitor_thread.stop()
sensor_monitor_thread.wait(2000)
sensor_monitor_thread = None
mainWindow.lblSensorData.setText("传感器数据: 未连接")
def update_sensor_ui(index):
mainWindow.serialGroup.setVisible(index == 0)
mainWindow.ethernetGroup.setVisible(index == 1)
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)
# 状态栏
#self.statusBar = QStatusBar()
#self.setStatusBar(self.statusBar)
# 创建自定义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)
# 传感器控制面板
sensor_panel = QGroupBox("传感器控制")
sensor_layout = QVBoxLayout(sensor_panel)
sensor_type_layout = QHBoxLayout()
self.lblSensorType = QLabel("传感器类型:")
self.cbSensorType = QComboBox()
self.cbSensorType.addItems(["串口", "以太网"])
sensor_type_layout.addWidget(self.lblSensorType)
sensor_type_layout.addWidget(self.cbSensorType)
sensor_layout.addLayout(sensor_type_layout)
# 串口参数
self.serialGroup = QGroupBox("串口参数")
serial_layout = QVBoxLayout(self.serialGroup)
self.lblComPort = QLabel("端口:")
self.cbComPort = QComboBox()
if platform.system() == 'Windows':
ports = [f"COM{i}" for i in range(1, 21)]
else:
ports = [f"/dev/ttyS{i}" for i in range(0, 4)] + [f"/dev/ttyUSB{i}" for i in range(0, 4)]
self.cbComPort.addItems(ports)
self.lblBaudrate = QLabel("波特率:")
self.cbBaudrate = QComboBox()
self.cbBaudrate.addItems(["96000", "19200", "38400", "57600", "115200"])
self.cbBaudrate.setCurrentText("115200")
serial_layout.addWidget(self.lblComPort)
serial_layout.addWidget(self.cbComPort)
serial_layout.addWidget(self.lblBaudrate)
serial_layout.addWidget(self.cbBaudrate)
sensor_layout.addWidget(self.serialGroup)
# 以太网参数
self.ethernetGroup = QGroupBox("以太网参数")
ethernet_layout = QVBoxLayout(self.ethernetGroup)
self.lblIP = QLabel("IP地址:")
self.edtIP = QLineEdit("192.168.1.100")
self.lblPort = QLabel("端口:")
self.edtPort = QLineEdit("502")
ethernet_layout.addWidget(self.lblIP)
ethernet_layout.addWidget(self.edtIP)
ethernet_layout.addWidget(self.lblPort)
ethernet_layout.addWidget(self.edtPort)
sensor_layout.addWidget(self.ethernetGroup)
# 连接按钮
self.bnConnectSensor = QPushButton("连接传感器")
self.bnDisconnectSensor = QPushButton("断开传感器")
self.bnDisconnectSensor.setEnabled(False)
sensor_layout.addWidget(self.bnConnectSensor)
sensor_layout.addWidget(self.bnDisconnectSensor)
# 延迟设置
delay_layout = QHBoxLayout()
self.lblDelay = QLabel("触发延迟(秒):")
self.spinDelay = QSpinBox()
self.spinDelay.setRange(0, 60)
self.spinDelay.setValue(0)
self.spinDelay.setToolTip("传感器检测到布料后延迟拍摄的时间")
delay_layout.addWidget(self.lblDelay)
delay_layout.addWidget(self.spinDelay)
sensor_layout.addLayout(delay_layout)
# 传感器数据
self.lblSensorData = QLabel("传感器数据: 未连接")
self.lblSensorData.setStyleSheet("font-size: 10pt;")
sensor_layout.addWidget(self.lblSensorData)
right_layout.addWidget(sensor_panel)
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()
disconnect_sensor()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWindow = MainWindow()
# 信号连接
mainWindow.cbSensorType.currentIndexChanged.connect(update_sensor_ui)
update_sensor_ui(0)
mainWindow.bnConnectSensor.clicked.connect(connect_sensor)
mainWindow.bnDisconnectSensor.clicked.connect(disconnect_sensor)
mainWindow.sliderDiffThreshold.valueChanged.connect(update_diff_threshold)
mainWindow.bnCheckPrint.clicked.connect(sensor_controlled_check)
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)
main()
mainWindow.show()
app.exec_()
close_device()
disconnect_sensor()
sys.exit()
我想把这个程序打包成exe文件我该怎么做