# 제품 등록 페이지 — 기준 이미지 캡처 import cv2 import numpy as np from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import ( QWidget, QHBoxLayout, QVBoxLayout, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QLabel, QMessageBox, QScrollArea, QFrame, ) _GRP_STYLE = ( "QGroupBox {" " background:#222222; border:1px solid #333333; border-radius:6px;" " margin-top:14px; padding:14px 12px 12px 12px;" "}" "QGroupBox::title { color:#aaaaaa; subcontrol-origin:margin; left:10px; padding:0 4px; }" ) class RegisterPage(QWidget): def __init__(self, insight_cam, matcher=None, db_client=None, parent=None): super().__init__(parent) self._insight = insight_cam self._db_client = db_client self._db_items = [] self._selected = None self._captured_img = None self._build_ui() # ================================================================== # # UI 구성 # ================================================================== # def _build_ui(self): root = QHBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) root.addWidget(self._build_left_panel(), stretch=2) root.addWidget(self._build_right_panel(), stretch=3) def _build_left_panel(self) -> QWidget: w = QWidget() w.setStyleSheet("background:#1a1a1a;") layout = QVBoxLayout(w) layout.setContentsMargins(16, 16, 8, 16) layout.setSpacing(10) self._btn_mes = QPushButton("MES 불러오기") self._btn_mes.setEnabled(False) self._btn_mes.setFixedHeight(56) self._btn_mes.setToolTip("DB 연결 후 사용 가능") self._btn_mes.setStyleSheet( "background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;" ) self._btn_mes.clicked.connect(self._on_load_from_db) layout.addWidget(self._btn_mes) lbl = QLabel("제품 목록") lbl.setStyleSheet("font-size:13px; color:#777777;") layout.addWidget(lbl) self._list = QListWidget() self._list.setStyleSheet(""" QListWidget { background:#222222; border:1px solid #333333; border-radius:4px; outline:none; font-size:15px; } QListWidget::item { padding:0px 14px; border-bottom:1px solid #2a2a2a; color:#dddddd; } QListWidget::item:selected { background:#185FA5; color:#ffffff; } QListWidget::item:hover:!selected { background:#2d2d2d; } """) self._list.currentRowChanged.connect(self._on_select) layout.addWidget(self._list, stretch=1) return w def _build_right_panel(self) -> QScrollArea: scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QScrollArea.NoFrame) scroll.setStyleSheet("background:#1a1a1a;") inner = QWidget() inner.setStyleSheet("background:#1a1a1a;") layout = QVBoxLayout(inner) layout.setContentsMargins(8, 16, 16, 16) layout.setSpacing(0) layout.addWidget(self._build_detail_section()) layout.addWidget(_divider()) layout.addWidget(self._build_capture_section()) layout.addStretch() scroll.setWidget(inner) return scroll # ── 섹션 1: 제품 상세 ────────────────────────────────────────────── def _build_detail_section(self) -> QGroupBox: g = QGroupBox("제품 상세 정보") g.setStyleSheet(_GRP_STYLE) layout = QVBoxLayout(g) layout.setSpacing(8) self._lbl_name = _info_value("—") self._lbl_model = _info_value("—") self._lbl_type = _info_value("—") layout.addLayout(_info_row("카테고리", self._lbl_name)) layout.addLayout(_info_row("모델명", self._lbl_model)) layout.addLayout(_info_row("Type", self._lbl_type)) self._arrow_lbl = QLabel("") self._arrow_lbl.setAlignment(Qt.AlignCenter) self._arrow_lbl.setFixedHeight(80) self._arrow_lbl.setStyleSheet("font-size:60px; background:transparent;") layout.addWidget(self._arrow_lbl) return g # ── 섹션 2: 캡처 ────────────────────────────────────────────────── def _build_capture_section(self) -> QGroupBox: g = QGroupBox("기준 이미지 캡처") g.setStyleSheet(_GRP_STYLE) layout = QVBoxLayout(g) layout.setSpacing(12) btn = QPushButton("캡처 (In-Sight 트리거)") btn.setFixedHeight(56) btn.setStyleSheet( "background:#1a3a5c; color:#ffffff; border:none; border-radius:4px;" "font-size:15px; font-weight:bold;" ) btn.clicked.connect(self._on_capture) self._preview = QLabel() self._preview.setAlignment(Qt.AlignCenter) self._preview.setFixedSize(400, 300) self._preview.setStyleSheet( "background:#111111; color:#555555; border:1px solid #333333;" "border-radius:4px; font-size:14px;" ) self._preview.setText("캡처 이미지 없음") layout.addWidget(btn) layout.addWidget(self._preview, alignment=Qt.AlignHCenter) return g # ================================================================== # # 슬롯 # ================================================================== # def _on_select(self, row: int): if row < 0: return item = self._list.item(row) if item is None: return article_id = item.data(Qt.UserRole) if article_id is None: return db_item = next( (x for x in self._db_items if x["article_id"] == article_id), None ) r = { "id": article_id, "name": item.text(), "model": db_item.get("buyer_article_no", "") if db_item else "", "type": "", } self._selected = r self._captured_img = None self._lbl_name.setText(r["name"]) self._lbl_model.setText(r["model"]) t = r.get("type", "") self._lbl_type.setText(t if t else "—") if t == "RH": self._arrow_lbl.setText("→") self._arrow_lbl.setStyleSheet("font-size:60px; color:#4488ff; background:transparent;") elif t == "LH": self._arrow_lbl.setText("←") self._arrow_lbl.setStyleSheet("font-size:60px; color:#ff8844; background:transparent;") else: self._arrow_lbl.setText("") self._reset_preview() def _on_capture(self): if self._selected is None: QMessageBox.warning(self, "경고", "먼저 제품을 선택하세요.") return if not self._insight.is_connected(): QMessageBox.critical(self, "오류", "Cognex 카메라가 연결되어 있지 않습니다.") return raw = self._insight.trigger_and_get_image() if not raw: QMessageBox.critical(self, "캡처 실패", "이미지를 수신하지 못했습니다.\n카메라 연결 상태를 확인하세요.") return arr = np.frombuffer(raw, dtype=np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED) if img is None: QMessageBox.critical(self, "캡처 실패", "이미지 디코딩에 실패했습니다.") return self._captured_img = img self._show_ndarray(img) def update_db(self, db_client): """MainWindow에서 DB 연결/해제 시 호출.""" self._db_client = db_client enabled = db_client is not None and db_client.is_connected() self._btn_mes.setEnabled(enabled) self._btn_mes.setStyleSheet( "background:#1a3a5c; color:#ffffff; border:none; border-radius:4px; font-size:15px;" if enabled else "background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;" ) def _on_load_from_db(self): if not self._db_client or not self._db_client.is_connected(): QMessageBox.warning(self, "경고", "DB를 먼저 연결해주세요.") return items = self._db_client.get_reflector_list() if not items: QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.") return self._list.clear() self._db_items = items for item in items: li = QListWidgetItem(item['article']) li.setSizeHint(QSize(0, 52)) li.setData(Qt.UserRole, item["article_id"]) self._list.addItem(li) QMessageBox.information( self, "완료", f"{len(items)}개 제품을 불러왔습니다." ) # ================================================================== # # 헬퍼 # ================================================================== # def _show_ndarray(self, img: np.ndarray): h_img, w_img = img.shape[:2] if img.ndim == 2: qimg = QImage(img.data, w_img, h_img, w_img, QImage.Format_Grayscale8) else: rgb = np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) qimg = QImage(rgb.data, w_img, h_img, w_img * 3, QImage.Format_RGB888) pix = QPixmap.fromImage(qimg).scaled(400, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) self._preview.setPixmap(pix) self._preview.setText("") def _reset_preview(self): self._preview.clear() self._preview.setText("캡처 이미지 없음") # ================================================================== # # 모듈 수준 유틸리티 # ================================================================== # def _divider() -> QFrame: f = QFrame() f.setFrameShape(QFrame.HLine) f.setFixedHeight(1) f.setStyleSheet("background:#333333; border:none;") return f def _info_value(text: str) -> QLabel: lbl = QLabel(text) lbl.setStyleSheet("color:#ffffff; font-size:16px; font-weight:bold;") return lbl def _info_row(label: str, value_widget: QLabel) -> QHBoxLayout: row = QHBoxLayout() key = QLabel(label + ":") key.setFixedWidth(80) key.setStyleSheet("color:#888888; font-size:14px;") row.addWidget(key) row.addWidget(value_widget, stretch=1) return row