AttributeError: 'QComboBox' object has no attribute 'setFirstItemMovable'

时间: 2023-06-23 14:18:47 浏览: 204
抱歉,setFirstItemMovable() 方法是 Qt 4 版本中的方法,已经在 Qt 5 中被移除。在 Qt 5 中,可以使用 setItemDelegate() 方法来设置 QComboBox 下拉列表项的委托,然后在委托中实现自定义的复选框控件。 以下是一个示例代码: ```python from PyQt5.QtWidgets import QApplication, QComboBox, QStyledItemDelegate, QCheckBox from PyQt5.QtCore import Qt app = QApplication([]) # 创建 QComboBox 控件 combo = QComboBox() # 创建自定义的委托类,并重写 createEditor() 方法 class CheckBoxDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): # 创建 QCheckBox 控件,并将其设置为编辑器 editor = QCheckBox(parent) editor.setStyleSheet('margin-right: 5px;') return editor # 设置下拉列表项的委托为自定义的委托类 combo.setItemDelegate(CheckBoxDelegate()) # 向 QComboBox 添加 5 个选项 for i in range(5): combo.addItem('Option {}'.format(i)) # 定义 currentItemChanged 信号的处理函数 def on_current_item_changed(index): # 获取当前选项的状态,并设置为选项的选中状态 state = combo.itemData(index, Qt.CheckStateRole) combo.setItemData(index, state == Qt.Checked, Qt.UserRole) # 将 currentItemChanged 信号连接到处理函数 combo.currentIndexChanged.connect(on_current_item_changed) # 显示 QComboBox combo.show() app.exec_() ``` 在这个例子中,我们创建了一个自定义的委托类 CheckBoxDelegate,并重写了 createEditor() 方法,在其中创建了 QCheckBox 控件并将其设置为编辑器。然后,我们使用 setItemDelegate() 方法将下拉列表项的委托设置为自定义的委托类。 在 currentItemChanged 信号的处理函数中,我们获取当前选项的状态,并将其设置为选项的选中状态。同时,我们将选项的选中状态保存在 Qt.UserRole 角色中,以便后续的操作。 这个例子演示了如何使用自定义的委托类来实现一个带有复选框的下拉框,你可以根据需要修改代码来适应自己的需求。
阅读全文

相关推荐

这是一个企业用简易的进销存系统中的部分代码,现在这个模块出现问题,在新建订单添加物料时点击ok添加无效,随即程序崩溃,两项测试输出均正确,出现“加载订单错误: 'UserDB' object has no attribute 'get_purchase_orders'”的提示,请你帮助我在程序中添加更多调试输出,帮我定位问题,再尝试解决问题 from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView, QLineEdit, QLabel, QFormLayout, QDialog, QDialogButtonBox, QMessageBox, QTabWidget, QComboBox, QDateEdit, QAbstractItemView, QSplitter, QSizePolicy ) from PyQt5.QtCore import Qt, QDate from PyQt5.QtGui import QBrush, QColor from purchase_manager.purchase_db import PurchaseDB from purchase_manager.purchase_models import PurchaseOrder, PurchaseOrderItem from basicData_manager.basic_data_db import BasicDataDB class PurchaseOrderFormDialog(QDialog): def __init__(self, parent=None, order=None): super().__init__(parent) self.setWindowTitle("编辑采购订单" if order else "新建采购订单") self.setMinimumSize(900, 650) self.db = BasicDataDB() self.purchase_db = PurchaseDB() self.order = order self.items = [] layout = QVBoxLayout() # 表单区域 form_layout = QFormLayout() # 订单编号(自动生成或只读) self.order_number_input = QLineEdit() self.order_number_input.setReadOnly(bool(order)) if not order: self.order_number_input.setText(self.generate_order_number()) # 供应商选择 self.supplier_combo = QComboBox() self.populate_suppliers() # 日期选择 self.order_date_edit = QDateEdit(QDate.currentDate()) self.order_date_edit.setCalendarPopup(True) self.expected_delivery_edit = QDateEdit(QDate.currentDate().addDays(7)) self.expected_delivery_edit.setCalendarPopup(True) form_layout.addRow("订单编号:", self.order_number_input) form_layout.addRow("供应商:", self.supplier_combo) form_layout.addRow("订单日期:", self.order_date_edit) form_layout.addRow("预计交货日期:", self.expected_delivery_edit) # 物料明细表格 self.items_table = QTableWidget() self.items_table.setColumnCount(9) self.items_table.setHorizontalHeaderLabels([ "ID", "物料编码", "物料名称", "规格", "单位", "数量", "税前单价", "税率", "金额" ]) self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.items_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.items_table.setSelectionBehavior(QTableWidget.SelectRows) self.items_table.setEditTriggers(QTableWidget.DoubleClicked) # 允许编辑数量 # 添加物料按钮 add_item_btn = QPushButton("添加物料") add_item_btn.clicked.connect(self.add_item) # 删除物料按钮 remove_item_btn = QPushButton("删除选中物料") remove_item_btn.clicked.connect(self.remove_selected_items) button_layout = QHBoxLayout() button_layout.addWidget(add_item_btn) button_layout.addWidget(remove_item_btn) button_layout.addStretch() # 合计金额 self.total_amount_label = QLabel("0.00") form_layout.addRow("合计金额:", self.total_amount_label) layout.addLayout(form_layout) layout.addWidget(QLabel("订单明细:")) layout.addWidget(self.items_table) layout.addLayout(button_layout) # 初始化数据 if order: self.load_order_data(order) else: # 初始加载空表格 self.items_table.setRowCount(0) # 按钮区域 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.validate_and_accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) self.items_table.itemChanged.connect(self.on_item_changed) self.setLayout(layout) def generate_order_number(self): """生成唯一的订单编号""" from datetime import datetime return f"{datetime.now().strftime('%Y%m%d%H%M%S')}" def populate_suppliers(self): """填充已审核的供应商""" try: suppliers = self.db.get_suppliers(status='approved') self.supplier_combo.clear() for supplier in suppliers: self.supplier_combo.addItem(f"{supplier[1]} - {supplier[2]}", supplier[0]) except Exception as e: print(f"加载供应商错误: {str(e)}") def load_order_data(self, order): """加载订单数据""" self.order_number_input.setText(order.order_number) self.supplier_combo.setCurrentText(order.supplier_name) self.order_date_edit.setDate(QDate.fromString(order.order_date, "yyyy-MM-dd")) self.expected_delivery_edit.setDate(QDate.fromString(order.expected_delivery_date, "yyyy-MM-dd")) # 加载订单明细 order_items = self.purchase_db.get_purchase_order_details(order.id) self.items = [] self.items_table.setRowCount(len(order_items)) for row, item in enumerate(order_items): self.items.append({ "product_id": item['product_id'], "code": item['product_code'], "name": item['product_name'], "specification": item['specification'], "unit": item['unit'], "quantity": item['quantity'], "unit_price": item['unit_price'], "tax_rate": item['tax_rate'] }) self.items_table.setItem(row, 0, self.create_readonly_item(str(item['id']))) self.items_table.setItem(row, 1, self.create_readonly_item(item['product_code'])) self.items_table.setItem(row, 2, self.create_readonly_item(item['product_name'])) self.items_table.setItem(row, 3, self.create_readonly_item(item['specification'])) self.items_table.setItem(row, 4, self.create_readonly_item(item['unit'])) self.items_table.setItem(row, 5, QTableWidgetItem(str(item['quantity']))) self.items_table.setItem(row, 6, QTableWidgetItem(f"{item['unit_price']:.2f}")) self.items_table.setItem(row, 7, QTableWidgetItem(f"{item['tax_rate'] * 100:.0f}%")) self.items_table.setItem(row, 8, self.create_readonly_item(f"{item['total_price']:.2f}")) self.update_total_amount() def add_item(self): """打开物料选择对话框""" dialog = ProductSelectionDialog(self) if dialog.exec_() == QDialog.Accepted: # 测试输出 print("02 - Selected Products to Add:") print(dialog.selected_products) for product in dialog.selected_products: # 检查是否已添加过该物料(通过ID检查) existing = next((item for item in self.items if item.get("id") == product["id"]), None) if existing: # 如果已存在,增加数量 row_index = self.items.index(existing) current_qty = int(self.items_table.item(row_index, 5).text()) new_qty = current_qty + 1 # 更新表格 self.items_table.item(row_index, 5).setText(str(new_qty)) # 更新内存中的数据 self.items[row_index]["quantity"] = new_qty else: # 添加新物料 self.add_item_to_table(product) def add_item_to_table(self, product): """添加物料到表格""" row = self.items_table.rowCount() self.items_table.insertRow(row) # 创建物料数据对象(包含所有必要属性) item_data = { "id": product["id"], "code": product["code"], "name": product["name"], "specification": product["specification"], "unit": product["unit"], "quantity": 1, # 默认数量为1 "unit_price": product["purchase_price"], "tax_rate": product["tax_rate"] } self.items.append(item_data) # 设置表格项 self.items_table.setItem(row, 0, self.create_readonly_item(str(product["id"]))) self.items_table.setItem(row, 1, self.create_readonly_item(product["code"])) self.items_table.setItem(row, 2, self.create_readonly_item(product["name"])) self.items_table.setItem(row, 3, self.create_readonly_item(product["specification"])) self.items_table.setItem(row, 4, self.create_readonly_item(product["unit"])) self.items_table.setItem(row, 5, QTableWidgetItem("1")) # 数量 self.items_table.setItem(row, 6, QTableWidgetItem(f"{product['purchase_price']:.2f}")) self.items_table.setItem(row, 7, QTableWidgetItem(f"{product['tax_rate'] * 100:.0f}%")) # 计算并设置总价 total_price = product['purchase_price'] * (1 + product['tax_rate']) self.items_table.setItem(row, 8, self.create_readonly_item(f"{total_price:.2f}")) self.update_total_amount() def create_readonly_item(self, text): """创建只读表格项""" item = QTableWidgetItem(text) item.setFlags(item.flags() & ~Qt.ItemIsEditable) return item def remove_selected_items(self): """删除选中物料""" selected_rows = sorted(set(index.row() for index in self.items_table.selectedIndexes())) if not selected_rows: QMessageBox.warning(self, "选择错误", "请先选择要删除的物料行") return for row in reversed(selected_rows): self.items_table.removeRow(row) del self.items[row] self.update_total_amount() def update_total_amount(self): """更新总金额""" total = 0.0 for row in range(self.items_table.rowCount()): quantity = float(self.items_table.item(row, 5).text() or 0) unit_price = float(self.items_table.item(row, 6).text() or 0) tax_rate = float(self.items_table.item(row, 7).text().rstrip('%')) / 100 total += quantity * unit_price * (1 + tax_rate) self.total_amount_label.setText(f"{total:.2f}") def validate_and_accept(self): """验证表单并接受""" if not self.items: QMessageBox.warning(self, "验证错误", "订单必须包含至少一个物料") return if self.supplier_combo.currentIndex() == -1: QMessageBox.warning(self, "验证错误", "请选择供应商") return self.accept() def get_order_data(self): """获取订单数据""" order_data = { "order_number": self.order_number_input.text(), "supplier_id": self.supplier_combo.currentData(), "order_date": self.order_date_edit.date().toString("yyyy-MM-dd"), "expected_delivery_date": self.expected_delivery_edit.date().toString("yyyy-MM-dd"), "total_amount": float(self.total_amount_label.text()), "items": [] } for row in range(self.items_table.rowCount()): item = { "product_id": int(self.items_table.item(row, 0).text()), "quantity": int(self.items_table.item(row, 5).text()), "unit_price": float(self.items_table.item(row, 6).text()), "tax_rate": float(self.items_table.item(row, 7).text().rstrip('%')) / 100 } order_data["items"].append(item) return order_data def on_item_changed(self, item): """当物料数量改变时更新金额""" if item.column() in (5, 6, 7): # 数量、单价或税率列改变 row = item.row() try: quantity = float(self.items_table.item(row, 5).text() or 0) unit_price = float(self.items_table.item(row, 6).text() or 0) tax_rate = float(self.items_table.item(row, 7).text().rstrip('%') or 0) / 100 total_price = quantity * unit_price * (1 + tax_rate) # 更新金额列 self.items_table.item(row, 8).setText(f"{total_price:.2f}") # 更新总金额 self.update_total_amount() except ValueError: # 输入无效时跳过 pass class ProductSelectionDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("选择物料") self.setFixedSize(800, 500) self.selected_product = None self.db = BasicDataDB() self.selected_products = [] layout = QVBoxLayout() # 搜索区域 search_layout = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText("输入物料名称或编码...") self.search_input.textChanged.connect(self.load_products) search_layout.addWidget(QLabel("搜索:")) search_layout.addWidget(self.search_input) layout.addLayout(search_layout) # 物料表格 self.products_table = QTableWidget() self.products_table.setColumnCount(8) self.products_table.setHorizontalHeaderLabels([ "选择","ID", "物料编码", "物料名称", "规格", "单位", "税前进价", "税率" ])# 拓展新列 ——税率 self.products_table.verticalHeader().setVisible(False) self.products_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.products_table.setSelectionMode(QAbstractItemView.SingleSelection) self.products_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.products_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.products_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch) self.products_table.setSelectionMode(QAbstractItemView.MultiSelection) self.products_table.setSelectionBehavior(QAbstractItemView.SelectRows) layout.addWidget(self.products_table) # 按钮区域 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.on_accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) self.setLayout(layout) # 初始加载数据 self.load_products() def load_products(self, search_term=None): """加载物料数据并应用搜索过滤""" try: products = self.db.get_products(status='approved') # 应用搜索过滤 if search_term: search_term = search_term.lower().strip() products = [p for p in products if search_term in p[1].lower() or # 物料编码 search_term in p[2].lower()] # 物料名称 self.products_table.setRowCount(len(products)) for row, product in enumerate(products): # 选择框 select_item = QTableWidgetItem() select_item.setCheckState(Qt.Unchecked) self.products_table.setItem(row, 0, select_item) # 物料数据 self.products_table.setItem(row, 1, QTableWidgetItem(str(product[0]))) # ID self.products_table.setItem(row, 2, QTableWidgetItem(product[1])) # 编码 self.products_table.setItem(row, 3, QTableWidgetItem(product[2])) # 名称 self.products_table.setItem(row, 4, QTableWidgetItem(product[4])) # 规格 self.products_table.setItem(row, 5, QTableWidgetItem(product[5])) # 单位 self.products_table.setItem(row, 6, QTableWidgetItem(f"{product[6]:.2f}")) # 进价 self.products_table.setItem(row, 7, QTableWidgetItem(f"{product[7]:.2f}")) # 税率 # 设置整行选择 self.products_table.setSelectionBehavior(QAbstractItemView.SelectRows) except Exception as e: print(f"加载物料错误: {str(e)}") def on_accept(self): """处理确定按钮点击""" self.selected_products = [] for row in range(self.products_table.rowCount()): if self.products_table.item(row, 0).checkState() == Qt.Checked: product = { "id": int(self.products_table.item(row, 1).text()), # 索引1 "code": self.products_table.item(row, 2).text(), # 索引2 "name": self.products_table.item(row, 3).text(), # 索引3 "specification": self.products_table.item(row, 4).text(), # 索引4 "unit": self.products_table.item(row, 5).text(), # 索引5 "purchase_price": float(self.products_table.item(row, 6).text()), # 索引6 "tax_rate": float(self.products_table.item(row, 7).text()) # 索引7 } self.selected_products.append(product) # 测试输出 - 可以保留用于调试 print("01 - Selected Products:") print(self.selected_products) if not self.selected_products: QMessageBox.warning(self, "选择错误", "请至少选择一个物料") return self.accept() class PurchaseOrderManager(QWidget): def __init__(self, current_user, db, parent=None): super().__init__(parent) self.current_user = current_user self.db = db or PurchaseDB() self.basic_db = BasicDataDB() self.init_ui() self.load_orders() def init_ui(self): layout = QVBoxLayout() # 顶部按钮区域 button_layout = QHBoxLayout() self.new_btn = QPushButton("新建订单") self.new_btn.clicked.connect(self.show_new_form) self.edit_btn = QPushButton("修改订单") self.edit_btn.clicked.connect(self.show_edit_form) self.submit_btn = QPushButton("提交订单") self.submit_btn.clicked.connect(self.submit_order) self.approve_btn = QPushButton("审核订单") self.approve_btn.clicked.connect(self.approve_order) self.refresh_btn = QPushButton("刷新") self.refresh_btn.clicked.connect(self.load_orders) button_layout.addWidget(self.new_btn) button_layout.addWidget(self.edit_btn) button_layout.addWidget(self.submit_btn) button_layout.addWidget(self.approve_btn) button_layout.addStretch() button_layout.addWidget(self.refresh_btn) # 搜索区域 search_layout = QHBoxLayout() self.status_combo = QComboBox() self.status_combo.addItem("全部状态", "") self.status_combo.addItem("草稿", "draft") self.status_combo.addItem("已提交", "submitted") self.status_combo.addItem("已审核", "approved") self.status_combo.addItem("已收货", "received") self.status_combo.addItem("已取消", "canceled") self.status_combo.currentIndexChanged.connect(self.load_orders) self.supplier_combo = QComboBox() self.populate_suppliers() self.supplier_combo.currentIndexChanged.connect(self.load_orders) self.date_from = QDateEdit(QDate.currentDate().addMonths(-1)) self.date_from.setCalendarPopup(True) self.date_from.dateChanged.connect(self.load_orders) self.date_to = QDateEdit(QDate.currentDate()) self.date_to.setCalendarPopup(True) self.date_to.dateChanged.connect(self.load_orders) search_layout.addWidget(QLabel("状态:")) search_layout.addWidget(self.status_combo) search_layout.addWidget(QLabel("供应商:")) search_layout.addWidget(self.supplier_combo) search_layout.addWidget(QLabel("日期从:")) search_layout.addWidget(self.date_from) search_layout.addWidget(QLabel("到:")) search_layout.addWidget(self.date_to) # 订单表格 self.orders_table = QTableWidget() self.orders_table.setColumnCount(8) self.orders_table.setHorizontalHeaderLabels([ "ID", "订单编号", "供应商", "订单日期", "交货日期", "总金额", "状态", "创建人" ]) self.orders_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.orders_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.orders_table.setSelectionBehavior(QTableWidget.SelectRows) self.orders_table.setEditTriggers(QTableWidget.NoEditTriggers) self.orders_table.doubleClicked.connect(self.show_order_details) # 订单明细表格 self.items_table = QTableWidget() self.items_table.setColumnCount(7) self.items_table.setHorizontalHeaderLabels([ "物料编码", "物料名称", "规格", "单位", "数量", "单价", "金额" ]) self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.items_table.setEditTriggers(QTableWidget.NoEditTriggers) # 使用分割器 splitter = QSplitter(Qt.Vertical) splitter.addWidget(self.orders_table) splitter.addWidget(self.items_table) splitter.setSizes([300, 200]) layout.addLayout(button_layout) layout.addLayout(search_layout) layout.addWidget(splitter) self.setLayout(layout) # 根据用户角色设置按钮可见性 if self.current_user.role != "root": self.approve_btn.setVisible(False) def populate_suppliers(self): """填充供应商下拉框""" try: suppliers = self.basic_db.get_suppliers(status='approved') self.supplier_combo.clear() self.supplier_combo.addItem("全部供应商", -1) for supplier in suppliers: self.supplier_combo.addItem(f"{supplier[1]} - {supplier[2]}", supplier[0]) except Exception as e: print(f"加载供应商错误: {str(e)}") def load_orders(self): """加载采购订单""" try: status = self.status_combo.currentData() supplier_id = self.supplier_combo.currentData() if supplier_id == -1: supplier_id = None start_date = self.date_from.date().toString("yyyy-MM-dd") end_date = self.date_to.date().toString("yyyy-MM-dd") orders = self.db.get_purchase_orders( status=status, supplier_id=supplier_id, start_date=start_date, end_date=end_date ) self.orders_table.setRowCount(len(orders)) for row, order in enumerate(orders): self.orders_table.setItem(row, 0, QTableWidgetItem(str(order[0]))) # ID self.orders_table.setItem(row, 1, QTableWidgetItem(order[1])) # 订单编号 self.orders_table.setItem(row, 2, QTableWidgetItem(order[11])) # 供应商名称 self.orders_table.setItem(row, 3, QTableWidgetItem(order[3])) # 订单日期 self.orders_table.setItem(row, 4, QTableWidgetItem(order[4] or "")) # 交货日期 self.orders_table.setItem(row, 5, QTableWidgetItem(f"{order[5]:.2f}")) # 总金额 # 状态列 status_item = QTableWidgetItem(order[6]) if order[6] == "draft": status_item.setForeground(QBrush(QColor("gray"))) elif order[6] == "submitted": status_item.setForeground(QBrush(QColor("blue"))) elif order[6] == "approved": status_item.setForeground(QBrush(QColor("green"))) elif order[6] == "received": status_item.setForeground(QBrush(QColor("darkgreen"))) elif order[6] == "canceled": status_item.setForeground(QBrush(QColor("red"))) self.orders_table.setItem(row, 6, status_item) self.orders_table.setItem(row, 7, QTableWidgetItem(order[8])) # 创建人 # 清除明细表格 self.items_table.setRowCount(0) except Exception as e: print(f"加载订单错误: {str(e)}") QMessageBox.critical(self, "错误", f"加载订单失败: {str(e)}") def get_selected_order_id(self): """获取选中的订单ID""" selected = self.orders_table.selectedItems() if not selected: return None return int(self.orders_table.item(selected[0].row(), 0).text()) def show_new_form(self): """显示新建订单表单""" dialog = PurchaseOrderFormDialog(self) if dialog.exec_() == QDialog.Accepted: try: order_data = dialog.get_order_data() order_id = self.db.create_purchase_order(order_data, self.current_user.username) QMessageBox.information(self, "成功", f"采购订单 #{order_id} 创建成功!") self.load_orders() except Exception as e: QMessageBox.critical(self, "错误", f"创建采购订单失败: {str(e)}") def show_edit_form(self): """显示编辑订单表单""" order_id = self.get_selected_order_id() if not order_id: QMessageBox.warning(self, "选择错误", "请先选择一个订单") return # 获取订单详情 orders = self.db.get_purchase_orders() order_data = next((o for o in orders if o[0] == order_id), None) if not order_data: QMessageBox.warning(self, "错误", "找不到选中的订单") return # 检查订单状态 if order_data[6] not in ["draft", "submitted"]: QMessageBox.warning(self, "操作受限", "只能编辑草稿或已提交状态的订单") return # 创建订单对象 order = PurchaseOrder( id=order_data[0], order_number=order_data[1], supplier_id=order_data[2], supplier_name=order_data[11], order_date=order_data[3], expected_delivery_date=order_data[4], total_amount=order_data[5], status=order_data[6], created_by=order_data[8], created_at=order_data[9] ) dialog = PurchaseOrderFormDialog(self, order) if dialog.exec_() == QDialog.Accepted: try: update_data = dialog.get_order_data() self.db.update_purchase_order(order_id, update_data, self.current_user.username) QMessageBox.information(self, "成功", "采购订单更新成功!") self.load_orders() except Exception as e: QMessageBox.critical(self, "错误", f"更新采购订单失败: {str(e)}") def submit_order(self): """提交订单""" order_id = self.get_selected_order_id() if not order_id: QMessageBox.warning(self, "选择错误", "请先选择一个订单") return # 检查订单状态 orders = self.db.get_purchase_orders() order_data = next((o for o in orders if o[0] == order_id), None) if not order_data: QMessageBox.warning(self, "错误", "找不到选中的订单") return if order_data[6] != "draft": QMessageBox.warning(self, "操作受限", "只能提交草稿状态的订单") return try: self.db.submit_purchase_order(order_id, self.current_user.username) QMessageBox.information(self, "成功", "采购订单提交成功!") self.load_orders() except Exception as e: QMessageBox.critical(self, "错误", f"提交采购订单失败: {str(e)}") def approve_order(self): """审核订单""" order_id = self.get_selected_order_id() if not order_id: QMessageBox.warning(self, "选择错误", "请先选择一个订单") return # 检查订单状态 orders = self.db.get_purchase_orders() order_data = next((o for o in orders if o[0] == order_id), None) if not order_data: QMessageBox.warning(self, "错误", "找不到选中的订单") return if order_data[6] != "submitted": QMessageBox.warning(self, "操作受限", "只能审核已提交状态的订单") return try: self.db.approve_purchase_order(order_id, self.current_user.username) QMessageBox.information(self, "成功", "采购订单审核通过!") self.load_orders() except Exception as e: QMessageBox.critical(self, "错误", f"审核采购订单失败: {str(e)}") def show_order_details(self, index): """显示订单明细""" row = index.row() order_id = int(self.orders_table.item(row, 0).text()) try: items = self.db.get_purchase_order_details(order_id) self.items_table.setRowCount(len(items)) for row, item in enumerate(items): self.items_table.setItem(row, 0, QTableWidgetItem(item['product_code'])) self.items_table.setItem(row, 1, QTableWidgetItem(item['product_name'])) self.items_table.setItem(row, 2, QTableWidgetItem(item['specification'])) self.items_table.setItem(row, 3, QTableWidgetItem(item['unit'])) self.items_table.setItem(row, 4, QTableWidgetItem(str(item['quantity']))) self.items_table.setItem(row, 5, QTableWidgetItem(f"{item['unit_price']:.2f}")) self.items_table.setItem(row, 6, QTableWidgetItem(f"{item['total_price']:.2f}")) except Exception as e: print(f"加载订单明细错误: {str(e)}")

这个程序运行后我点了调试运行之后显示特征点都一样为什么并没有自动触发检测而且我这个以后肯定是要它自动匹配好,自动质量检测的 # -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np import math import time import logging import threading from collections import deque from PyQt5.QtWidgets import ( QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBox, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog, QLineEdit, QRadioButton, QGridLayout, QSpinBox, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QProgressBar, ) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal, QTimer, QMetaObject, pyqtSlot,Q_ARG from PyQt5.QtGui import QImage, QPixmap from CamOperation_class import CameraOperation from MvCameraControl_class import * import ctypes from ctypes import cast, POINTER from datetime import datetime import skimage import platform from CameraParams_header import ( MV_GIGE_DEVICE, MV_USB_DEVICE, MV_GENTL_CAMERALINK_DEVICE, MV_GENTL_CXP_DEVICE, MV_GENTL_XOF_DEVICE ) # ===== 全局配置 ===== # 模板匹配参数 MATCH_THRESHOLD = 0.75 # 降低匹配置信度阈值以提高灵敏度 MIN_MATCH_COUNT = 10 # 最小匹配特征点数量 MIN_FRAME_INTERVAL = 0.1 # 最小检测间隔(秒) # ===== 全局变量 ===== current_sample_path = "" detection_history = [] isGrabbing = False isOpen = False obj_cam_operation = None frame_monitor_thread = None template_matcher_thread = None MV_OK = 0 MV_E_CALLORDER = -2147483647 # ==================== 优化后的质量检测算法 ==================== def enhanced_check_print_quality(sample_image_path, test_image, threshold=0.05): # 不再使用传感器数据调整阈值 adjusted_threshold = threshold try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_GRAYSCALE) if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None if len(test_image.shape) == 3: test_image_gray = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) else: test_image_gray = test_image.copy() sample_image = cv2.GaussianBlur(sample_image, (5, 5), 0) test_image_gray = cv2.GaussianBlur(test_image_gray, (5, 5), 0) try: # 使用更鲁棒的SIFT特征检测器 sift = cv2.SIFT_create() keypoints1, descriptors1 = sift.detectAndCompute(sample_image, None) keypoints2, descriptors2 = sift.detectAndCompute(test_image_gray, None) if descriptors1 is None or descriptors2 is None: logging.warning("无法提取特征描述符,跳过配准") aligned_sample = sample_image else: # 使用FLANN匹配器提高匹配精度 FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) matches = flann.knnMatch(descriptors1, descriptors2, k=2) # 应用Lowe's比率测试筛选优质匹配 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) if len(good_matches) > MIN_MATCH_COUNT: src_pts = np.float32([keypoints1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([keypoints2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) if H is not None: aligned_sample = cv2.warpPerspective( sample_image, H, (test_image_gray.shape[1], test_image_gray.shape[0]) ) logging.info("图像配准成功,使用配准后样本") else: aligned_sample = sample_image logging.warning("无法计算单应性矩阵,使用原始样本") else: aligned_sample = sample_image logging.warning(f"特征点匹配不足({len(good_matches)}/{MIN_MATCH_COUNT}),跳过图像配准") except Exception as e: logging.error(f"图像配准失败: {str(e)}") aligned_sample = sample_image try: if aligned_sample.shape != test_image_gray.shape: test_image_gray = cv2.resize(test_image_gray, (aligned_sample.shape[1], aligned_sample.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None try: from skimage.metrics import structural_similarity as compare_ssim ssim_score, ssim_diff = compare_ssim( aligned_sample, test_image_gray, full=True, gaussian_weights=True, data_range=255 ) except ImportError: from skimage.measure import compare_ssim ssim_score, ssim_diff = compare_ssim( aligned_sample, test_image_gray, full=True, gaussian_weights=True ) except Exception as e: logging.error(f"SSIM计算失败: {str(e)}") abs_diff = cv2.absdiff(aligned_sample, test_image_gray) ssim_diff = abs_diff.astype(np.float32) / 255.0 ssim_score = 1.0 - np.mean(ssim_diff) ssim_diff = (1 - ssim_diff) * 255 abs_diff = cv2.absdiff(aligned_sample, test_image_gray) combined_diff = cv2.addWeighted(ssim_diff.astype(np.uint8), 0.7, abs_diff, 0.3, 0) _, thresholded = cv2.threshold(combined_diff, 30, 255, cv2.THRESH_BINARY) kernel = np.ones((3, 3), np.uint8) thresholded = cv2.morphologyEx(thresholded, cv2.MORPH_OPEN, kernel) threshold极光 = cv2.morphologyEx(thresholded, cv2.MORPH_CLOSE, kernel) diff_pixels = np.count_nonzero(thresholded) total_pixels = aligned_sample.size diff_ratio = diff_pixels / total_pixels is_qualified = diff_ratio <= adjusted_threshold marked_image = cv2.cvtColor(test_image_gray, cv2.COLOR_GRAY2BGR) marked_image[thresholded == 255] = [0, 0, 255] # 放大缺陷标记 scale_factor = 2.0 # 放大2倍 marked_image = cv2.resize(marked_image, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_LINEAR) labels = skimage.measure.label(thresholded) properties = skimage.measure.regionprops(labels) for prop in properties: if prop.area > 50: y, x = prop.centroid # 根据放大比例调整坐标 x_scaled = int(x * scale_factor) y_scaled = int(y * scale_factor) cv2.putText(marked_image, f"Defect", (x_scaled, y_scaled), cv2.FONT_HERSHEY_SIMPLEX, 0.5 * scale_factor, (0, 255, 255), int(scale_factor)) return is_qualified, diff_ratio, marked_image # ==================== 视觉触发的质量检测流程 ==================== def vision_controlled_check(capture_image=None, match_score=0.0): """修改为接受图像帧和匹配分数""" global current_sample_path, detection_history logging.info("视觉触发质量检测启动") # 如果没有提供图像,使用当前帧 if capture_image is None: frame = obj_cam_operation.get_current_frame() else: frame = capture_image if frame is None: QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: diff_threshold = mainWindow.sliderDiffThreshold.value() / 100.0 logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(30) is_qualified, diff_ratio, marked_image = enhanced_check_print_quality( current_sample_path, frame, threshold=diff_threshold ) progress.setValue(70) if is_qualified is None: QMessageBox.critical(mainWindow, "检测错误", "检测失败,请极光日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}") progress.setValue(90) update_diff_display(diff_ratio, is_qualified) result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n差异占比: {diff_ratio*100:.2f}%\n阈值: {diff_threshold*100:.2f}%" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: # 创建可调整大小的窗口 cv2.namedWindow("缺陷标记结果", cv2.WINDOW_NORMAL) cv2.resizeWindow("缺陷标记结果", 800, 600) # 初始大小 cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'threshold': diff_threshold, 'trigger_type': 'vision' if capture_image else 'manual' } detection_history.append(detection_result) update_history_display() progress.setValue(100) except Exception as e: logging.exception("印花检测失败") QMessageBox.critical(mainWindow, "检测错误", f"检测过程中发生错误: {str(e)}", QMessageBox.Ok) finally: progress.close() # ==================== 相机操作函数 ==================== def open_device(): global deviceList, nSelCamIndex, obj_cam_operation, isOpen, frame_monitor_thread, mainWindow if isOpen: QMessageBox.warning(mainWindow, "Error", '相机已打开!', QMessageBox.Ok) return MV_E_CALLORDER nSelCamIndex = mainWindow.ComboDevices.currentIndex() if nSelCamIndex < 0: QMessageBox.warning(mainWindow, "Error", '请选择相机!', QMessageBox.Ok) return MV_E_CALLORDER # 创建相机控制对象 cam = MvCamera() # 初始化相机操作对象 obj_cam_operation = CameraOperation(cam, deviceList, nSelCamIndex) ret = obj_cam_operation.open_device() if 0 != ret: strError = "打开设备失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) isOpen = False else: set_continue_mode() get_param() isOpen = True enable_controls() # 创建并启动帧监控线程 frame_monitor_thread = FrameMonitorThread(obj_cam_operation) frame_monitor_thread.frame_status.connect(mainWindow.statusBar().showMessage) frame_monitor_thread.start() def start_grabbing(): global obj_cam_operation, isGrabbing, template_matcher_thread ret = obj_cam_operation.start_grabbing(mainWindow.widgetDisplay.winId()) if ret != 0: strError = "开始取流失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = True enable_controls() # 等待第一帧到达 QThread.msleep(500) if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "警告", "开始取流后未接收到帧,请检查相机连接!", QMessageBox.Ok) # 如果启用了自动检测,启动检测线程 if mainWindow.chkContinuousMatch.isChecked(): toggle_template_matching(True) def stop_grabbing(): global obj_cam_operation, isGrabbing, template_matcher_thread ret = obj_cam_operation.Stop_grabbing() if ret != 0: strError = "停止取流失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = False enable_controls() # 停止模板匹配线程 if template_matcher_thread and template_matcher_thread.isRunning(): template_matcher_thread.stop() def close_device(): global isOpen, isGrabbing, obj_cam_operation, frame_monitor_thread, template_matcher_thread if frame_monitor_thread and frame_monitor_thread.isRunning(): frame_monitor_thread.stop() frame_monitor_thread.wait(2000) # 停止模板匹配线程 if template_matcher_thread and template_matcher_thread.isRunning(): template_matcher_thread.stop() template_matcher_thread.wait(2000) template_matcher_thread = None if isOpen and obj_cam_operation: obj_cam_operation.close_device() isOpen = False isGrabbing = False enable_controls() # ==================== 连续帧匹配检测器 ==================== class ContinuousFrameMatcher(QThread): frame_processed = pyqtSignal(np.ndarray, float, bool) # 处理后的帧, 匹配分数, 是否匹配 match_score_updated = pyqtSignal(float) # 匹配分数更新信号 match_success = pyqtSignal(np.ndarray, float) # 匹配成功信号 (帧, 匹配分数) trigger_activated = pyqtSignal(bool) # 新增信号:触发状态变化 def __init__(self, cam_operation, parent=None): super().__init__(parent) self.cam_operation = cam_operation self.running = True self.sample_template = None self.min_match_count = MIN_MATCH_COUNT self.match_threshold = MATCH_THRESHOLD self.sample_kp = None self.sample_des = None self.current_match_score = 0.0 self.last_match_time = 0 self.frame_counter = 0 self.consecutive_fail_count = 0 self.last_trigger_time = 0 # 上次触发时间 self.cool_down = 0.2 # 冷却时间(秒) self.trigger_threshold = 0.5 # 默认触发阈值 # 特征检测器 - 使用SIFT self.sift = cv2.SIFT_create() # 特征匹配器 - 使用FLANN提高匹配精度 FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) self.flann = cv2.FlannBasedMatcher(index_params, search_params) # 性能监控 self.processing_times = deque(maxlen=100) self.frame_rates = deque(maxlen=100) # 黑白相机优化 self.clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) def calculate_match_score(self, good_matches): """更合理的匹配分数计算方法""" if not good_matches: return 0.0 # 使用匹配质量(距离的倒数)计算分数 total_quality = 0.0 for match in good_matches: # 避免除零错误 distance = max(match.distance, 1e-5) total_quality += 1.0 / distance # 归一化处理 max_quality = len(good_matches) * (1.0 / 0.01) # 假设最小距离0.01 match_score = min(1.0, max(0.0, total_quality / max_quality)) return match_score def preprocess_image(self, image): """更保守的图像预处理""" if len(image.shape) == 2: # 已经是灰度图,直接返回 return image # 仅转换为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 可选:轻度直方图均衡化 gray = cv2.equalizeHist(gray) return gray def set_sample(self, sample_img): """设置标准样本并提取特征""" # 保存样本图像 self.sample_img = sample_img # 预处理增强特征 processed_sample = self.preprocess_image(sample_img) # 提取样本特征点 self.sample_kp, self.sample_des = self.sift.detectAndCompute(processed_sample, None) if self.sample_des is None or len(self.sample_kp) < 100: # 增加最小特征点要求 logging.warning(f"样本特征点不足({len(self.sample_kp)}个),需要至少100个") return False logging.info(f"样本特征提取成功: {len(self.sample_kp)}个关键点") return True def set_threshold(self, threshold): """更新匹配阈值""" self.match_threshold = max(0.0, min(1.0, threshold)) logging.info(f"更新匹配阈值: {self.match_threshold:.2f}") def set_trigger_threshold(self, threshold): """更新触发阈值""" self.trigger_threshold = max(0.0, min(1.0, threshold)) logging.info(f"更新触发阈值: {self.trigger_threshold:.2f}") def process_frame(self, frame): """处理帧:特征提取、匹配和可视化""" is_matched = False match_score = 0.0 processed_frame = frame.copy() # 检查是否已设置样本 if self.sample_kp is None or self.sample_des is None: return processed_frame, match_score, is_matched # 预处理当前帧 processed_frame = self.preprocess_image(frame) # 转换为灰度图像用于特征提取 gray_frame = cv2.cvtColor(processed_frame, cv2.COLOR_BGR2GRAY) try: # 提取当前帧的特征点 kp, des = self.sift.detectAndCompute(gray_frame, None) # 即使特征点不足也继续处理 if des is None or len(kp) < 5: # 更新匹配分数为0 self.current_match_score = 0.0 self.match_score_updated.emit(0.0) return processed_frame, 0.0, False # 匹配特征点 matches = self.flann.knnMatch(self.sample_des, des, k=2) # 应用Lowe's比率测试 good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) # 使用更科学的匹配分数计算方法 match_score = self.calculate_match_score(good_matches) # 添加调试日志 logging.debug(f"匹配结果: 样本特征点={len(self.sample_kp)}, " f"当前帧特征点={len(kp)}, " f"良好匹配={len(good_matches)}, " f"匹配分数={match_score:.4f}") # 判断是否匹配成功(用于UI显示) if len(good_matches) >= self.min_match_count and match_score >= self.match_threshold: is_matched = True # 在图像上绘制匹配结果 if len(gray_frame.shape) == 2: processed_frame = cv2.cvtColor(gray_frame, cv2.COLOR_GRAY2BGR) # 绘制匹配点 processed_frame = cv2.drawMatches( self.sample_img, self.s极光, processed_frame, kp, good_matches, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) # 在图像上显示匹配分数 cv2.putText(processed_frame, f"Match Score: {match_score:.2f}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # 更新当前匹配分数 self.current_match_score = match_score self.match_score_updated.emit(match_score) # 检查是否达到触发条件(50%以上)并且超过冷却时间 trigger_active = False current_time = time.time() if match_score >= self.trigger_threshold and (current_time - self.last_trigger_time) > self.cool_down: self.last_trigger_time = current_time trigger_active = True logging.info(f"匹配分数达到触发阈值! 分数: {match_score:.2f}, 触发质量检测") # 发出匹配成功信号 (传递当前帧) self.match_success.emit(frame.copy(), match_score) # 发送触发状态信号 self.trigger_activated.emit(trigger_active) except Exception as e: logging.error(f"帧处理错误: {str(e)}") return processed_frame, match_score, is_matched def run(self): """主处理循环 - 连续处理每一帧""" logging.info("连续帧匹配线程启动") self.last_match_time = time.time() self.consecutive_fail_count = 0 while self.running: start_time = time.time() # 检查相机状态 if not self.cam_operation or not self.cam_operation.is_grabbing: if self.consecutive_fail_count % 10 == 0: logging.debug("相机未取流,等待...") time.sleep(0.1) self.consecutive_fail_count += 1 continue # 获取当前帧 frame = self.cam_operation.get_current_frame() if frame is None: self.consecutive_fail_count += 1 if self.consecutive_fail_count % 10 == 0: logging.warning(f"连续{self.consecutive_fail_count}次获取帧失败") time.sleep(0.05) continue self.consecutive_fail_count = 0 try: # 处理帧 processed_frame, match_score, is_matched = self.process_frame(frame) # 发送处理结果 self.frame_processed.emit(processed_frame, match_score, is_matched) except Exception as e: logging.error(f"帧处理错误: {str(e)}") # 控制处理频率 processing_time = time.time() - start_time sleep_time = max(0.01, MIN_FRAME_INTERVAL - processing_time) time.sleep(sleep_time) logging.info("连续帧匹配线程退出") # ==================== 模板匹配控制函数 ==================== def toggle_template_matching(state): global template_matcher_thread, current_sample_path logging.debug(f"切换连续匹配状态: {state}") if state == Qt.Checked and isGrabbing: # 确保已设置样本 if not current_sample_path: logging.warning("尝试启动连续匹配但未设置样本") QMessageBox.warning(mainWindow, "错误", "请先设置标准样本", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if template_matcher_thread is None: logging.info("创建新的连续帧匹配线程") template_matcher_thread = ContinuousFrameMatcher(obj_cam_operation) template_matcher_thread.frame_processed.connect(update_frame_display) template_matcher_thread.match_score_updated.connect(update_match_score_display) template_matcher_thread.trigger_activated.connect( lambda active: mainWindow.update_trigger_indicator(active) ) # 正确连接匹配成功信号到质量检测函数 template_matcher_thread.match_success.connect( lambda frame, score: vision_controlled_check(frame, score) ) # 加载样本图像 sample_img = cv2.imread(current_sample_path) if sample_img is None: logging.error("无法加载标准样本图像") QMessageBox.warning(mainWindow, "错误", "无法加载标准样本图像", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if not template_matcher_thread.set_sample(sample_img): logging.warning("标准样本特征不足") QMessageBox.warning(mainWindow, "错误", "标准样本特征不足", QMessageBox.Ok) mainWindow.chkContinuousMatch.setChecked(False) return if not template_matcher_thread.isRunning(): logging.info("启动连续帧匹配线程") template_matcher_thread.start() elif template_matcher_thread and template_matcher_thread.isRunning(): logging.info("停止连续帧匹配线程") template_matcher_thread.stop() # 重置匹配分数显示 update_match_score_display(0.0) # 重置帧显示 if obj_cam_operation and obj_cam_operation.is_frame_available(): frame = obj_cam_operation.get_current_frame() if frame is not None: display_frame = frame.copy() # 添加状态信息 cv2.putText(display_frame, "Continuous Matching Disabled", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) update_frame_display(display_frame, 0.0, False) # 添加类型转换函数 def numpy_to_qimage(np_array): """将 numpy 数组转换为 QImage""" if np_array is None: return QImage() height, width, channel = np_array.shape bytes_per_line = 3 * width # 确保数据是连续的 if not np_array.flags['C_CONTIGUOUS']: np_array = np.ascontiguousarray(np_array) # 转换 BGR 到 RGB rgb_image = cv2.cvtColor(np_array, cv2.COLOR_BGR2RGB) # 创建 QImage qimg = QImage( rgb_image.data, width, height, bytes_per_line, QImage.Format_RGB888 ) # 复制数据以避免内存问题 return qimg.copy() # 修改 update_frame_display 函数 def update_frame_display(frame, match_score, is_matched): """更新主显示窗口(线程安全)""" # 确保在GUI线程中执行 if QThread.currentThread() != QApplication.instance().thread(): # 转换为 QImage 再传递 qimg = numpy_to_qimage(frame) QMetaObject.invokeMethod( mainWindow, "updateDisplay", Qt.QueuedConnection, Q_ARG(QImage, qimg), Q_ARG(float, match_score), Q_ARG(bool, is_matched) ) return # 如果已经在主线程,直接调用主窗口的更新方法 mainWindow.updateDisplay(frame, match_score, is_matched) def update_match_score_display(score): """更新匹配分数显示""" # 将分数转换为百分比显示 score_percent = score * 100 mainWindow.lblMatchScoreValue.setText(f"{score_percent:.1f}%") # 根据分数设置颜色 if score > 0.8: # 高于80%显示绿色 color = "green" elif score > 0.6: # 60%-80%显示黄色 color = "orange" else: # 低于60%显示红色 color = "red" mainWindow.lblMatchScoreValue.setStyleSheet(f"color: {color}; font-weight: bold;") def update_diff_display(diff_ratio, is_qualified): mainWindow.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") if is_qualified: mainWindow.lblDiffStatus.setText("状态: 合格") mainWindow.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: mainWindow.lblDiffStatus.setText("状态: 不合格") mainWindow.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") def update_diff_threshold(value): mainWindow.lblDiffValue.setText(f"{value}%") def update_sample_display(): global current_sample_path if current_sample_path: mainWindow.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") mainWindow.lblSamplePath.setToolTip(current_sample_path) mainWindow.bnPreviewSample.setEnabled(True) else: mainWindow.lblSamplePath.setText("当前样本: 未设置样本") mainWindow.bnPreviewSample.setEnabled(False) def update_history_display(): global detection_history mainWindow.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" trigger = "视觉" if result['trigger_type'] == 'vision' else "手动" mainWindow.cbHistory.addItem(f"[{trigger} {timestamp}] {status} - 差异: {ratio}") def update_match_threshold(value): """更新匹配阈值显示并应用到匹配器""" global template_matcher_thread # 更新UI显示 if mainWindow: mainWindow.lblThresholdValue.setText(f"{value}%") # 如果匹配线程存在,更新其匹配阈值 if template_matcher_thread: # 转换为0-1范围的浮点数 threshold = value / 100.0 template_matcher_thread.set_threshold(threshold) logging.debug(f"更新匹配阈值: {threshold:.2f}") # 新增函数:保存当前帧 def save_current_frame(): if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return frame = obj_cam_operation.get_current_frame() if frame is None: 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"capture_{timestamp}" file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存当前帧", os.path.join(last_dir, default_filename), "PNG Files (*.png);;BMP Files (*.bmp);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: return # 确保文件扩展名正确 if not os.path.splitext(file_path)[1]: if "PNG" in selected_filter: file_path += ".png" elif "BMP" in selected_filter: file_path += ".bmp" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" # 保存图像 try: if cv2.imwrite(file_path, frame): QMessageBox.information(mainWindow, "成功", f"当前帧已保存至:\n{file_path}", QMessageBox.Ok) else: raise Exception("OpenCV保存失败") except Exception as e: QMessageBox.critical(mainWindow, "保存错误", f"保存图像时发生错误:\n{str(e)}", QMessageBox.Ok) # ==================== 主窗口类 ==================== class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("布料印花检测系统 - 连续匹配版") self.resize(1200, 800) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 设备枚举区域 device_layout = QHBoxLayout() self.ComboDevices = QComboBox() self.bnEnum = QPushButton("枚举设备") self.bnOpen = QPushButton("打开设备") self.bnClose = QPushButton("关闭设备") device_layout.addWidget(self.ComboDevices) device_layout.addWidget(self.bnEnum) device_layout.addWidget(self.bnOpen) device_layout.addWidget(self.bnClose) main_layout.addLayout(device_layout) # 取流控制组 self.groupGrab = QGroupBox("取流控制") grab_layout = QHBoxLayout(self.groupGrab) self.bnStart = QPushButton("开始取流") self.bnStop = QPushButton("停止取流") self.radioContinueMode = QRadioButton("连续模式") self.radioTriggerMode = QRadioButton("触发模式") self.bnSoftwareTrigger = QPushButton("软触发") grab_layout.addWidget(self.bnStart) grab_layout.addWidget(self.bnStop) grab_layout.addWidget(self.radioContinueMode) grab_layout.addWidget(self.radioTriggerMode) grab_layout.addWidget(self.bnSoftwareTrigger) main_layout.addWidget(self.groupGrab) # 参数设置组 self.paramgroup = QGroupBox("相机参数") param_layout = QGridLayout(self.paramgroup) self.edtExposureTime = QLineEdit() self.edtGain = QLineEdit() self.edtFrameRate = QLineEdit() self.bnGetParam = QPushButton("获取参数") self.bnSetParam = QPushButton("设置参数") self.bnSaveImage = QPushButton("保存图像") param_layout.addWidget(QLabel("曝光时间:"), 0, 0) param_layout.addWidget(self.edtExposureTime, 0, 1) param_layout.addWidget(self.bnGetParam, 0, 2) param_layout.addWidget(QLabel("增益:"), 1, 0) param_layout.addWidget(self.edtGain, 1, 1) param_layout.addWidget(self.bnSetParam, 1, 2) param_layout.addWidget(QLabel("帧率:"), 2, 0) param_layout.addWidget(self.edtFrameRate, 2, 1) param_layout.addWidget(self.bnSaveImage, 2, 2) main_layout.addWidget(self.paramgroup) # 图像显示区域 self.widgetDisplay = QLabel() self.widgetDisplay.setMinimumSize(640, 480) self.widgetDisplay.setStyleSheet("background-color: black;") self.widgetDisplay.setAlignment(Qt.AlignCenter) self.widgetDisplay.setText("相机预览区域") main_layout.addWidget(self.widgetDisplay, 1) # 创建自定义UI组件 self.setup_custom_ui() # 添加阈值自适应定时器 self.threshold_timer = QTimer() self.threshold_timer.timeout.connect(self.auto_adjust_threshold) self.threshold_timer.start(2000) # 每2秒调整一次 def auto_adjust_threshold(self): """根据环境亮度自动调整匹配阈值""" if not obj_cam_operation or not isGrabbing: return # 获取当前帧 frame = obj_cam_operation.get_current_frame() if frame is None: return # 处理不同通道数的图像 if len(frame.shape) == 2 or frame.shape[2] == 1: # 已经是灰度图 gray = frame elif frame.shape[2] == 3: # 三通道彩色图 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) elif frame.shape[2] == 4: # 四通道图(带alpha) gray = cv2.cvtColor(frame, cv2.COLOR_BGRA2GRAY) else: # 其他通道数,无法处理 logging.warning(f"无法处理的图像格式: {frame.shape}") return # 计算平均亮度 try: brightness = np.mean(gray) except Exception as e: logging.error(f"计算亮度失败: {str(e)}") return # 根据亮度动态调整阈值 (亮度低时降低阈值要求) if brightness < 50: # 暗环境 new_threshold = 40 # 40% elif brightness > 200: # 亮环境 new_threshold = 65 # 65% else: # 正常环境 new_threshold = 55 # 55% # 更新UI self.sliderThreshold.setValue(new_threshold) self.lblThresholdValue.setText(f"{new_threshold}%") # 更新匹配器阈值 update_match_threshold(new_threshold) # 状态栏显示调整信息 self.statusBar().showMessage(f"亮度: {brightness:.1f}, 自动调整阈值至: {new_threshold}%", 3000) def update_trigger_indicator(self, active): """更新触发指示灯状态""" if active: self.triggerIndicator.setStyleSheet("background-color: green; border-radius: 10px;") else: self.triggerIndicator.setStyleSheet("background-color: gray; border-radius: 10px;") def update_trigger_threshold(self, value): """更新触发阈值显示并应用到匹配器""" self.lblTriggerValue.setText(f"{value}%") threshold = value / 100.0 if template_matcher_thread: template_matcher_thread.set_trigger_threshold(threshold) def setup_custom_ui(self): # 工具栏 toolbar = self.addToolBar("检测工具") self.bnCheckPrint = QPushButton("手动检测") toolbar.addWidget(self.bnCheckPrint) # 添加分隔标签 toolbar.addWidget(QLabel("图像保存:")) self.bnSaveCurrentFrame = QPushButton("保存当前帧") toolbar.addWidget(self.bnSaveCurrentFrame) self.bnSaveSample = QPushButton("保存标准样本") toolbar.addWidget(self.bnSaveSample) self.bnPreviewSample = QPushButton("预览样本") toolbar.addWidget(self.bnPreviewSample) # 添加触发指示灯 self.triggerIndicator = QLabel() self.triggerIndicator.setFixedSize(20, 20) self.triggerIndicator.setStyleSheet("background-color: gray; border-radius: 10px;") toolbar.addWidget(QLabel("触发状态:")) toolbar.addWidget(self.triggerIndicator) # 历史记录 toolbar.addWidget(QLabel("历史记录:")) self.cbHistory = QComboBox() self.cbHistory.setMinimumWidth(300) toolbar.addWidget(self.cbHistory) # 状态栏样本路径 self.lblSamplePath = QLabel("当前样本: 未设置样本") self.statusBar().addPermanentWidget(self.lblSamplePath) # 右侧面板 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 差异度调整组 diff_group = QGroupBox("差异度调整") diff_layout = QVBoxLayout(diff_group) self.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") self.sliderDiffThreshold = QSlider(Qt.Horizontal) self.sliderDiffThreshold.setRange(0, 100) self.sliderDiffThreshold.setValue(5) self.lblDiffValue = QLabel("5%") self.lblCurrentDiff = QLabel("当前差异度: -") self.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") self.lblDiffStatus = QLabel("状态: 未检测") self.lblDiffStatus.setStyleSheet("font-size: 12px;") diff_layout.addWidget(self.lblDiffThreshold) diff_layout.addWidget(self.sliderDiffThreshold) diff_layout.addWidget(self.lblDiffValue) diff_layout.addWidget(self.lblCurrentDiff) diff_layout.addWidget(self.lblDiffStatus) right_layout.addWidget(diff_group) # ===== 连续匹配面板 ===== match_group = QGroupBox("连续帧匹配") match_layout = QVBoxLayout(match_group) # 样本设置 sample_layout = QHBoxLayout() self.bnSetSample = QPushButton("设置标准样本") self.bnPreviewSample = QPushButton("预览样本") self.lblSampleStatus = QLabel("状态: 未设置样本") sample_layout.addWidget(self.bnSetSample) sample_layout.addWidget(self.bnPreviewSample) sample_layout.addWidget(self.lblSampleStatus) match_layout.addLayout(sample_layout) # 匹配参数 param_layout = QHBoxLayout() self.lblMatchThreshold = QLabel("匹配阈值:") self.sliderThreshold = QSlider(Qt.Horizontal) self.sliderThreshold.setRange(50, 100) self.sliderThreshold.setValue(75) # 降低默认阈值 self.lblThresholdValue = QLabel("75%") param_layout.addWidget(self.lblMatchThreshold) param_layout.addWidget(self.sliderThreshold) param_layout.addWidget(self.lblThresholdValue) match_layout.addLayout(param_layout) # 触发阈值调整 trigger_threshold_layout = QHBoxLayout() self.lblTriggerThreshold = QLabel("触发阈值(%):") self.sliderTriggerThreshold = QSlider(Qt.Horizontal) self.sliderTriggerThreshold.setRange(0, 100) self.sliderTriggerThreshold.setValue(50) # 默认50% self.lblTriggerValue = QLabel("50%") trigger_threshold_layout.addWidget(self.lblTriggerThreshold) trigger_threshold_layout.addWidget(self.sliderTriggerThreshold) trigger_threshold_layout.addWidget(self.lblTriggerValue) match_layout.addLayout(trigger_threshold_layout) # 匹配分数显示 match_score_layout = QHBoxLayout() self.lblMatchScore = QLabel("实时匹配分数:") self.lblMatchScoreValue = QLabel("0.0%") self.lblMatchScoreValue.setStyleSheet("font-weight: bold;") match_score_layout.addWidget(self.lblMatchScore) match_score_layout.addWidget(self.lblMatchScoreValue) match_layout.addLayout(match_score_layout) # 连续匹配开关 self.chkContinuousMatch = QCheckBox("启用连续帧匹配") self.chkContinuousMatch.setChecked(False) match_layout.addWidget(self.chkContinuousMatch) right_layout.addWidget(match_group) right_layout.addStretch(1) # 添加调试按钮 self.bnDebug = QPushButton("调试匹配") toolbar.addWidget(self.bnDebug) # 连接信号 self.bnDebug.clicked.connect(self.debug_matching) # 停靠窗口 dock = QDockWidget("检测控制面板", self) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.addDockWidget(Qt.RightDockWidgetArea, dock) @pyqtSlot(QImage, float, bool) def updateDisplay(self, qimg, match_score, is_matched): """线程安全的显示更新方法(只接收 QImage)""" if qimg.isNull(): return # 创建QPixmap并缩放 pixmap = QPixmap.fromImage(qimg) scaled_pixmap = pixmap.scaled( self.widgetDisplay.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) # 更新显示 self.widgetDisplay.setPixmap(scaled_pixmap) self.widgetDisplay.setAlignment(Qt.AlignCenter) def closeEvent(self, event): logging.info("主窗口关闭,执行清理...") close_device() event.accept() def debug_matching(self): """保存当前帧和匹配结果用于调试""" global isGrabbing, current_sample_path, obj_cam_operation # 检查必要状态 if not isGrabbing: QMessageBox.warning(self, "错误", "请先开始相机取流!") return if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(self, "错误", "请先设置有效的标准样本图像!") return try: # 获取当前帧 frame = obj_cam_operation.get_current_frame() if frame is None: QMessageBox.warning(self, "无有效图像", "未获取到有效帧") return # 创建调试目录 debug_dir = "debug_match" os.makedirs(debug_dir, exist_ok=True) # 保存当前帧 current_time = datetime.now().strftime("%Y%m%d_%H%M%S") frame_path = os.path.join(debug_dir, f"current_frame_{current_time}.png") cv2.imwrite(frame_path, frame) # 保存样本图像 sample_img = cv2.imread(current_sample_path) sample_path = os.path.join(debug_dir, f"sample_{os.path.basename(current_sample_path)}") cv2.imwrite(sample_path, sample_img) # 安全预处理:检查图像通道数 def safe_preprocess(img): """安全的图像预处理函数""" # 处理不同通道数的图像 if len(img.shape) == 2 or img.shape[2] == 1: # 已经是灰度图 return img elif img.shape[2] == 3: # 三通道彩色图 return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) elif img.shape[2] == 4: # 四通道图(带alpha) return cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY) else: # 其他通道数,返回原图 logging.warning(f"无法处理的图像格式: {img.shape}") return img # 应用安全预处理 if template_matcher_thread: processed_frame = template_matcher_thread.preprocess_image(frame) processed_sample = template_matcher_thread.preprocess_image(sample_img) else: processed_frame = safe_preprocess(frame) processed_sample = safe_preprocess(sample_img) # 提取特征点 sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(processed_sample, None) kp2, des2 = sift.detectAndCompute(processed_frame, None) # 绘制特征点 - 处理单通道图像 def draw_keypoints_safe(img, keypoints): """安全的特征点绘制函数""" if len(img.shape) == 2: # 单通道转三通道用于彩色绘制 return cv2.drawKeypoints( cv2.cvtColor(img, cv2.COLOR_GRAY2BGR), keypoints, None, color=(0, 255, 0) ) else: return cv2.drawKeypoints(img, keypoints, None, color=(0, 255, 0)) sample_with_kp = draw_keypoints_safe(processed_sample, kp1) frame_with_kp = draw_keypoints_safe(processed_frame, kp2) # 保存带特征点的图像 cv2.imwrite(os.path.join(debug_dir, "sample_keypoints.png"), sample_with_kp) cv2.imwrite(os.path.join(debug_dir, "frame_keypoints.png"), frame_with_kp) # 进行匹配(仅当有足够的特征点时) if des1 is not None and des2 is not None and len(des1) > 0 and len(des2) > 0: FLANN_INDEX_KDTREE = 1 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5) search_params = dict(checks=50) flann = cv2.FlannBasedMatcher(index_params, search_params) matches = flann.knnMatch(des1, des2, k=2) good_matches = [] for m, n in matches: if m.distance < 0.7 * n.distance: good_matches.append(m) # 绘制匹配结果 match_img = cv2.drawMatches( sample_img, kp1, frame, kp2, good_matches, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS ) # 保存匹配结果 cv2.imwrite(os.path.join(debug_dir, "matches.png"), match_img) QMessageBox.information(self, "调试完成", f"调试文件已保存到: {debug_dir}\n" f"样本特征点: {len(kp1)}\n" f"当前帧特征点: {len(kp2)}\n" f"良好匹配数: {len(good_matches)}") else: QMessageBox.warning(self, "特征不足", f"无法进行匹配:\n" f"样本特征点: {len(kp1) if kp1 else 0}\n" f"当前帧特征点: {len(kp2) if kp2 else 0}") except Exception as e: logging.exception("调试匹配失败") QMessageBox.critical(self, "调试错误", f"调试过程中发生错误:\n{str(e)}") # ===== 辅助函数 ===== def ToHexStr(num): if not isinstance(num, int): try: num = int(num) except: return f"<非整数:{type(num)}>" chaDic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'} hexStr = "" if num < 0: num = num + 2 ** 32 while num >= 16: digit = num % 16 hexStr = chaDic.get(digit, str(digit)) + hexStr num //= 16 hexStr = chaDic.get(num, str(num)) + hexStr return "0x" + hexStr def enum_devices(): global deviceList, obj_cam_operation n_layer_type = ( MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE ) # 创建设备列表 deviceList = MV_CC_DEVICE_INFO_LIST() # 枚举设备 ret = MvCamera.MV_CC_EnumDevices(n_layer_type, deviceList) if ret != MV_OK: error_msg = f"枚举设备失败! 错误码: 0x{ret:x}" logging.error(error_msg) QMessageBox.warning(mainWindow, "错误", error_msg, QMessageBox.Ok) return ret if deviceList.nDeviceNum == 0: QMessageBox.warning(mainWindow, "提示", "未找到任何设备", QMessageBox.Ok) return MV_OK logging.info(f"找到 {deviceList.nDeviceNum} 个设备") # 处理设备信息 devList = [] for i in range(deviceList.nDeviceNum): # 获取设备信息 mvcc_dev_info = ctypes.cast( deviceList.pDeviceInfo[i], ctypes.POINTER(MV_CC_DEVICE_INFO) ).contents # 根据设备类型提取信息 if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE: st_gige_info = mvcc_dev_info.SpecialInfo.stGigEInfo ip_addr = ( f"{(st_gige_info.nCurrentIp >> 24) & 0xFF}." f"{(st_gige_info.nCurrentIp >> 16) & 0xFF}." f"{(st_gige_info.nCurrentIp >> 8) & 0xFF}." f"{st_gige_info.nCurrentIp & 0xFF}" ) # 修复:将c_ubyte_Array_16转换为字节串再解码 user_defined_bytes = bytes(st_gige_info.chUserDefinedName) dev_name = f"GigE: {user_defined_bytes.decode('gbk', 'ignore')}" devList.append(f"[{i}] {dev_name} ({ip_addr})") elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE: st_usb_info = mvcc_dev_info.SpecialInfo.stUsb3VInfo serial = bytes(st_usb_info.chSerialNumber).decode('ascii', 'ignore').rstrip('\x00') # 修复:同样处理用户自定义名称 user_defined_bytes = bytes(st_usb_info.chUserDefinedName) dev_name = f"USB: {user_defined_bytes.decode('gbk', 'ignore')}" devList.append(f"[{i}] {dev_name} (SN: {serial})") else: devList.append(f"[{i}] 未知设备类型: {mvcc_dev_info.nTLayerType}") # 更新UI mainWindow.ComboDevices.clear() mainWindow.ComboDevices.addItems(devList) if devList: mainWindow.ComboDevices.setCurrentIndex(0) mainWindow.statusBar().showMessage(f"找到 {deviceList.nDeviceNum} 个设备", 300) return MV_OK def set_continue_mode(): ret = obj_cam_operation.set_trigger_mode(False) if ret != 0: strError = "设置连续模式失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: mainWindow.radioContinueMode.setChecked(True) mainWindow.radioTriggerMode.setChecked(False) mainWindow.bnSoftwareTrigger.setEnabled(False) def set_software_trigger_mode(): ret = obj_cam_operation.set_trigger_mode(True) if ret != 0: strError = "设置触发模式失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: mainWindow.radioContinueMode.setChecked(False) mainWindow.radioTriggerMode.setChecked(True) mainWindow.bnSoftwareTrigger.setEnabled(isGrabbing) def trigger_once(): ret = obj_cam_operation.trigger_once() if ret != 0: strError = "软触发失败 ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 尝试捕获当前帧 frame = obj_cam_operation.capture_frame() if frame is None: QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 确保图像有效 if frame.size == 0 or frame.shape[0] == 0 or frame.shape[1] == 0: QMessageBox.warning(mainWindow, "无效图像", "捕获的图像无效,请检查相机设置!", QMessageBox.Ok) return settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"sample_{timestamp}" file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存标准样本图像", os.path.join(last_dir, default_filename), "BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: return # 确保文件扩展名正确 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: if "BMP" in selected_filter: file_path += ".bmp" elif "PNG" in selected_filter: file_path += ".png" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" else: file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 创建目录(如果不存在) directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) except OSError as e: QMessageBox.critical(mainWindow, "目录创建错误", f"无法创建目录 {directory}: {str(e)}", QMessageBox.Ok) return # 保存图像 try: # 使用OpenCV保存图像 if not cv2.imwrite(file_path, frame): raise Exception("OpenCV保存失败") # 更新状态 current_sample_path = file_path update_sample_display() settings.setValue("last_save_dir", os.path.dirname(file_path)) # 显示成功消息 QMessageBox.information(mainWindow, "成功", f"标准样本已保存至:\n{file_path}", QMessageBox.Ok) # 更新样本状态 mainWindow.lblSampleStatus.setText("状态: 样本已设置") mainWindow.lblSampleStatus.setStyleSheet("color: green;") except Exception as e: logging.error(f"保存图像失败: {str(e)}") QMessageBox.critical(mainWindow, "保存错误", f"保存图像时发生错误:\n{str(e)}", QMessageBox.Ok) def preview_sample(): global current_sample_path if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return try: # 直接使用OpenCV加载图像 sample_img = cv2.imread(current_sample_path) if sample_img is None: raise Exception("无法加载图像") # 显示图像 cv2.namedWindow("标准样本预览", cv2.WINDOW_NORMAL) cv2.resizeWindow("标准样本预览", 800, 600) cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) def is_float(str): try: float(str) return True except ValueError: return False def get_param(): try: ret = obj_cam_operation.get_parameters() if ret != MV_OK: strError = "获取参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: mainWindow.edtExposureTime.setText("{0:.2f}".format(obj_cam_operation.exposure_time)) mainWindow.edtGain.setText("{0:.2f}".format(obj_cam_operation.gain)) mainWindow.edtFrameRate.setText("{0:.2f}".format(obj_cam_operation.frame_rate)) except Exception as e: error_msg = f"获取参数时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) def set_param(): frame_rate = mainWindow.edtFrameRate.text() exposure = mainWindow.edtExposureTime.text() gain = mainWindow.edtGain.text() if not (is_float(frame_rate) and is_float(exposure) and is_float(gain)): strError = "设置参数失败: 参数必须是有效的浮点数" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) return MV_E_PARAMETER try: ret = obj_cam_operation.set_param( frame_rate=float(frame_rate), exposure_time=float(exposure), gain=float(gain) ) if ret != MV_OK: strError = "设置参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) except Exception as e: error_msg = f"设置参数时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) def enable_controls(): global isGrabbing, isOpen mainWindow.groupGrab.setEnabled(isOpen) mainWindow.paramgroup.setEnabled(isOpen) mainWindow.bnOpen.setEnabled(not isOpen) mainWindow.bnClose.setEnabled(isOpen) mainWindow.bnStart.setEnabled(isOpen and (not isGrabbing)) mainWindow.bnStop.setEnabled(isOpen and isGrabbing) mainWindow.bnSoftwareTrigger.setEnabled(isGrabbing and mainWindow.radioTriggerMode.isChecked()) mainWindow.bnSaveImage.setEnabled(isOpen and isGrabbing) mainWindow.bnCheckPrint.setEnabled(isOpen and isGrabbing) mainWindow.bnSaveSample.setEnabled(isOpen and isGrabbing) mainWindow.bnPreviewSample.setEnabled(bool(current_sample_path)) mainWindow.bnSaveCurrentFrame.setEnabled(isOpen and isGrabbing) # 连续匹配控制 mainWindow.chkContinuousMatch.setEnabled(bool(current_sample_path) and isGrabbing) # ===== 相机帧监控线程 ===== class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) # 用于发送状态消息的信号 def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True self.frame_count = 0 self.last_time = time.time() def run(self): """监控相机帧状态的主循环""" while self.running: try: if self.cam_operation and self.cam_operation.is_grabbing: # 获取帧统计信息 frame_info = self.get_frame_info() if frame_info: fps = frame_info.get('fps', 0) dropped = frame_info.get('dropped', 0) status = f"FPS: {fps:.1f} | 丢帧: {dropped}" self.frame_status.emit(status) else: self.frame_status.emit("取流中...") else: self.frame_status.emit("相机未取流") except Exception as e: self.frame_status.emit(f"监控错误: {str(e)}") # 每500ms检查一次 QThread.msleep(500) def stop(self): """停止监控线程""" self.running = False self.wait(1000) # 等待线程结束 def calculate_fps(self): """计算当前帧率""" current_time = time.time() elapsed = current_time - self.last_time if elapsed > 0: fps = self.frame_count / elapsed self.frame_count = 0 self.last_time = current_time return fps return 0 def get_frame_info(self): """获取帧信息""" try: # 更新帧计数 self.frame_count += 1 # 返回帧信息 return { 'fps': self.calculate_fps(), 'dropped': 0 # 实际应用中需要从相机获取真实丢帧数 } except Exception as e: logging.error(f"获取帧信息失败: {str(e)}") return None # ===== 主程序入口 ===== if __name__ == "__main__": # 配置日志系统 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("cloth_inspection_continuous.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统(连续匹配版)启动") app = QApplication(sys.argv) mainWindow = MainWindow() # 信号连接 mainWindow.sliderThreshold.valueChanged.connect(update_match_threshold) mainWindow.sliderTriggerThreshold.valueChanged.connect( mainWindow.update_trigger_threshold ) # 其他信号连接 mainWindow.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) mainWindow.bnCheckPrint.clicked.connect(lambda: vision_controlled_check(None)) mainWindow.bnSaveSample.clicked.connect(save_sample_image) mainWindow.bnPreviewSample.clicked.connect(preview_sample) mainWindow.bnEnum.clicked.connect(enum_devices) mainWindow.bnOpen.clicked.connect(open_device) mainWindow.bnClose.clicked.connect(close_device) mainWindow.bnStart.clicked.connect(start_grabbing) mainWindow.bnStop.clicked.connect(stop_grabbing) mainWindow.bnSoftwareTrigger.clicked.connect(trigger_once) mainWindow.radioTriggerMode.clicked.connect(set_software_trigger_mode) mainWindow.radioContinueMode.clicked.connect(set_continue_mode) mainWindow.bnGetParam.clicked.connect(get_param) mainWindow.bnSetParam.clicked.connect(set_param) mainWindow.bnSaveImage.clicked.connect(save_current_frame) mainWindow.bnSaveCurrentFrame.clicked.connect(save_current_frame) # 连续匹配信号连接 mainWindow.sliderThreshold.valueChanged.connect(update_match_score_display) mainWindow.chkContinuousMatch.stateChanged.connect(toggle_template_matching) mainWindow.show() app.exec_() close_device() sys.exit()

最新推荐

recommend-type

基于QT的黑白棋游戏程序设计与实现(1).docx

基于QT的黑白棋游戏程序设计与实现(1).docx
recommend-type

互联网公司财务风险与管控建议研究论述(1).doc

互联网公司财务风险与管控建议研究论述(1).doc
recommend-type

软件开发过程规范(1).doc

软件开发过程规范(1).doc
recommend-type

langchain4j-document-loader-azure-storage-blob-0.27.0.jar中文文档.zip

1、压缩文件中包含: 中文文档、jar包下载地址、Maven依赖、Gradle依赖、源代码下载地址。 2、使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 3、特殊说明: (1)本文档为人性化翻译,精心制作,请放心使用; (2)只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; (3)不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 4、温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件。 5、本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册。
recommend-type

《河北省土地整治项目管理办法》.pdf

《河北省土地整治项目管理办法》.pdf
recommend-type

C++实现的DecompressLibrary库解压缩GZ文件

根据提供的文件信息,我们可以深入探讨C++语言中关于解压缩库(Decompress Library)的使用,特别是针对.gz文件格式的解压过程。这里的“lib”通常指的是库(Library),是软件开发中用于提供特定功能的代码集合。在本例中,我们关注的库是用于处理.gz文件压缩包的解压库。 首先,我们要明确一个概念:.gz文件是一种基于GNU zip压缩算法的压缩文件格式,广泛用于Unix、Linux等操作系统上,对文件进行压缩以节省存储空间或网络传输时间。要解压.gz文件,开发者需要使用到支持gzip格式的解压缩库。 在C++中,处理.gz文件通常依赖于第三方库,如zlib或者Boost.IoStreams。codeproject.com是一个提供编程资源和示例代码的网站,程序员可以在该网站上找到现成的C++解压lib代码,来实现.gz文件的解压功能。 解压库(Decompress Library)提供的主要功能是读取.gz文件,执行解压缩算法,并将解压缩后的数据写入到指定的输出位置。在使用这些库时,我们通常需要链接相应的库文件,这样编译器在编译程序时能够找到并使用这些库中定义好的函数和类。 下面是使用C++解压.gz文件时,可能涉及的关键知识点: 1. Zlib库 - zlib是一个用于数据压缩的软件库,提供了许多用于压缩和解压缩数据的函数。 - zlib库支持.gz文件格式,并且在多数Linux发行版中都预装了zlib库。 - 在C++中使用zlib库,需要包含zlib.h头文件,同时链接z库文件。 2. Boost.IoStreams - Boost是一个提供大量可复用C++库的组织,其中的Boost.IoStreams库提供了对.gz文件的压缩和解压缩支持。 - Boost库的使用需要下载Boost源码包,配置好编译环境,并在编译时链接相应的Boost库。 3. C++ I/O操作 - 解压.gz文件需要使用C++的I/O流操作,比如使用ifstream读取.gz文件,使用ofstream输出解压后的文件。 - 对于流操作,我们常用的是std::ifstream和std::ofstream类。 4. 错误处理 - 解压缩过程中可能会遇到各种问题,如文件损坏、磁盘空间不足等,因此进行适当的错误处理是必不可少的。 - 正确地捕获异常,并提供清晰的错误信息,对于调试和用户反馈都非常重要。 5. 代码示例 - 从codeproject找到的C++解压lib很可能包含一个或多个源代码文件,这些文件会包含解压.gz文件所需的函数或类。 - 示例代码可能会展示如何初始化库、如何打开.gz文件、如何读取并处理压缩数据,以及如何释放资源等。 6. 库文件的链接 - 编译使用解压库的程序时,需要指定链接到的库文件,这在不同的编译器和操作系统中可能略有不同。 - 通常,在编译命令中加入-l参数,比如使用g++的话可能是`g++ -o DecompressLibrary DecompressLibrary.cpp -lz`,其中`-lz`表示链接zlib库。 7. 平台兼容性 - 在不同平台上使用解压库可能需要考虑平台兼容性问题。 - Windows系统可能需要额外的配置和库文件,因为zlib或其他库可能不是默认预装的。 根据以上知识点,我们可以得出,在C++中解压.gz文件主要涉及到对zlib或类似库的使用,以及熟悉C++的I/O操作。正确使用这些库,能够有效地对压缩文件进行解压,并处理可能出现的错误情况。如果从codeproject获取到的C++解压lib确实是针对.gz文件格式的,那么它很可能已经封装好了大部分的操作细节,让开发者能够以更简单的方式实现解压功能。
recommend-type

【数据融合技术】:甘肃土壤类型空间分析中的专业性应用

# 摘要 数据融合技术作为一种集成多源数据信息的方法,在土壤类型空间分析中发挥着关键作用。本文介绍了数据融合技术的基本概念及其理论基础,阐述了数据预处理、同步整合及冲突解决等关键技术,并详细描述了甘肃土壤类型数据准备的流程,包括数据采集、质量评估、空间化处理及融合实践准备。通过具体案例分析,展示了数据融合在土壤类型空间分布分析、土壤质量评估及土壤保护规划中的应用。同时,文
recommend-type

VM ware如何查看软件版本信息

<think>首先,用户的问题是:“我想查找VMware软件版本信息的查看方法请问如何在VMware中查看软件版本信息”。用户参考了之前的引用,但那些引用主要是关于查看VMware版本的一般方法,现在用户具体想知道在VMware软件中如何查看版本信息。回顾之前的对话:-用户第一次问的是VSCode的版本查看方法。-现在用户问的是VMware的版本查看方法。-系统级指令要求使用中文回答,正确格式化数学表达式(如果需要),但这里可能不需要数学表达式。-指令还要求生成相关问题,并在回答中引用段落时添加引用标识。用户提供的引用[1]到[5]是关于VMware版本的查看方法、下载等,但用户特别强调“参考
recommend-type

数据库课程设计报告:常用数据库综述

数据库是现代信息管理的基础,其技术广泛应用于各个领域。在高等教育中,数据库课程设计是一个重要环节,它不仅是学习理论知识的实践,也是培养学生综合运用数据库技术解决问题能力的平台。本知识点将围绕“经典数据库课程设计报告”展开,详细阐述数据库的基本概念、课程设计的目的和内容,以及在设计报告中常用的数据库技术。 ### 1. 数据库基本概念 #### 1.1 数据库定义 数据库(Database)是存储在计算机存储设备中的数据集合,这些数据集合是经过组织的、可共享的,并且可以被多个应用程序或用户共享访问。数据库管理系统(DBMS)提供了数据的定义、创建、维护和控制功能。 #### 1.2 数据库类型 数据库按照数据模型可以分为关系型数据库(如MySQL、Oracle)、层次型数据库、网状型数据库、面向对象型数据库等。其中,关系型数据库因其简单性和强大的操作能力而广泛使用。 #### 1.3 数据库特性 数据库具备安全性、完整性、一致性和可靠性等重要特性。安全性指的是防止数据被未授权访问和破坏。完整性指的是数据和数据库的结构必须符合既定规则。一致性保证了事务的执行使数据库从一个一致性状态转换到另一个一致性状态。可靠性则保证了系统发生故障时数据不会丢失。 ### 2. 课程设计目的 #### 2.1 理论与实践结合 数据库课程设计旨在将学生在课堂上学习的数据库理论知识与实际操作相结合,通过完成具体的数据库设计任务,加深对数据库知识的理解。 #### 2.2 培养实践能力 通过课程设计,学生能够提升分析问题、设计解决方案以及使用数据库技术实现这些方案的能力。这包括需求分析、概念设计、逻辑设计、物理设计、数据库实现、测试和维护等整个数据库开发周期。 ### 3. 课程设计内容 #### 3.1 需求分析 在设计报告的开始,需要对项目的目标和需求进行深入分析。这涉及到确定数据存储需求、数据处理需求、数据安全和隐私保护要求等。 #### 3.2 概念设计 概念设计阶段要制定出数据库的E-R模型(实体-关系模型),明确实体之间的关系。E-R模型的目的是确定数据库结构并形成数据库的全局视图。 #### 3.3 逻辑设计 基于概念设计,逻辑设计阶段将E-R模型转换成特定数据库系统的逻辑结构,通常是关系型数据库的表结构。在此阶段,设计者需要确定各个表的属性、数据类型、主键、外键以及索引等。 #### 3.4 物理设计 在物理设计阶段,针对特定的数据库系统,设计者需确定数据的存储方式、索引的具体实现方法、存储过程、触发器等数据库对象的创建。 #### 3.5 数据库实现 根据物理设计,实际创建数据库、表、视图、索引、触发器和存储过程等。同时,还需要编写用于数据录入、查询、更新和删除的SQL语句。 #### 3.6 测试与维护 设计完成之后,需要对数据库进行测试,确保其满足需求分析阶段确定的各项要求。测试过程包括单元测试、集成测试和系统测试。测试无误后,数据库还需要进行持续的维护和优化。 ### 4. 常用数据库技术 #### 4.1 SQL语言 SQL(结构化查询语言)是数据库管理的国际标准语言。它包括数据查询、数据操作、数据定义和数据控制四大功能。SQL语言是数据库课程设计中必备的技能。 #### 4.2 数据库设计工具 常用的数据库设计工具包括ER/Studio、Microsoft Visio、MySQL Workbench等。这些工具可以帮助设计者可视化地设计数据库结构,提高设计效率和准确性。 #### 4.3 数据库管理系统 数据库管理系统(DBMS)是用于创建和管理数据库的软件。关系型数据库管理系统如MySQL、PostgreSQL、Oracle、SQL Server等是数据库课程设计中的核心工具。 #### 4.4 数据库安全 数据库安全涉及用户认证、授权、数据加密、审计日志记录等方面,以确保数据的完整性和保密性。设计报告中应考虑如何通过DBMS内置的机制或额外的安全措施来保护数据。 ### 5. 结语 综上所述,一个经典数据库课程设计报告包含了从需求分析到数据库安全的全过程,涵盖了数据库设计的各个方面。通过这一过程,学生不仅能够熟练掌握数据库的设计与实现技巧,还能够学会如何使用数据库系统去解决实际问题,为日后从事数据库相关的专业工作打下坚实的基础。
recommend-type

【空间分布规律】:甘肃土壤类型与农业生产的关联性研究

# 摘要 本文对甘肃土壤类型及其在农业生产中的作用进行了系统性研究。首先概述了甘肃土壤类型的基础理论,并探讨了土壤类型与农业生产的理论联系。通过GIS技术分析,本文详细阐述了甘肃土壤的空间分布规律,并对其特征和影响因素进行了深入分析。此外,本文还研究了甘肃土壤类型对农业生产实际影响,包括不同区域土壤改良和作物种植案例,以及土壤养分、水分管理对作物生长周期和产量的具体影响。最后,提出了促进甘肃土壤与农业可持续发展的策略,包括土壤保护、退化防治对策以及土壤类型优化与农业创新的结合。本文旨在为