from __future__ import annotations from datetime import datetime from typing import Any import cv2 import numpy as np from PySide6.QtCore import Qt, Slot from PySide6.QtGui import QAction, QImage, QPixmap from PySide6.QtWidgets import ( QApplication, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QPushButton, QTextEdit, QToolButton, QVBoxLayout, QWidget, QStyle, ) from app.camera import CameraWorker from app.config import AppConfig from app.detection import DetectionResult from app.media import MediaStore, VideoRecorder from app.settings_dialog import SettingsDialog class MainWindow(QMainWindow): def __init__(self, app_config: AppConfig) -> None: super().__init__() self.app_config = app_config self.config = app_config.data self.last_frame: np.ndarray | None = None self.overlay_result: DetectionResult | None = None self.last_detection: DetectionResult | None = None self.media_store = MediaStore(self.config, self.app_config) self.video_recorder = VideoRecorder(self.config, self.app_config) self.setWindowTitle("Duck Stain YOLO") self.resize(1280, 720) self._build_ui() self.worker = CameraWorker(self.config, self.app_config) self.worker.frame_ready.connect(self.on_frame_ready) self.worker.detection_ready.connect(self.on_detection_ready) self.worker.camera_error.connect(self.on_camera_error) self.worker.start() def _build_ui(self) -> None: self.stage = QWidget() self.setCentralWidget(self.stage) self.video_label = QLabel(self.stage) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet("background: #111; color: #ddd;") self.video_label.setText("Kamera") self.result_panel = QWidget(self.stage) self.result_panel.setObjectName("resultPanel") self.result_panel.setStyleSheet( """ QWidget#resultPanel { background: rgba(20, 20, 20, 170); border-radius: 8px; } QTextEdit { background: transparent; color: white; border: 0; font-size: 13px; } QPushButton { min-height: 28px; padding: 4px 12px; } """ ) panel_layout = QVBoxLayout(self.result_panel) self.result_text = QTextEdit() self.result_text.setReadOnly(True) self.result_text.setFixedHeight(118) panel_layout.addWidget(self.result_text) panel_buttons = QHBoxLayout() self.detect_button = QPushButton("wykryj") self.ok_button = QPushButton("ok") panel_buttons.addStretch(1) panel_buttons.addWidget(self.detect_button) panel_buttons.addWidget(self.ok_button) panel_layout.addLayout(panel_buttons) self.detect_button.clicked.connect(self.start_detection) self.ok_button.clicked.connect(self.accept_detection) self.toolbar = QWidget(self.stage) self.toolbar.setObjectName("bottomToolbar") self.toolbar.setStyleSheet( """ QWidget#bottomToolbar { background: rgba(20, 20, 20, 175); border-radius: 8px; } QToolButton { min-width: 44px; min-height: 38px; padding: 4px; } """ ) toolbar_layout = QHBoxLayout(self.toolbar) toolbar_layout.setContentsMargins(8, 6, 8, 6) self.photo_button = self._tool_button(QStyle.SP_DialogSaveButton, "Zrob zdjecie") self.record_button = self._tool_button(QStyle.SP_MediaPlay, "Start/stop nagrywania") self.settings_button = self._tool_button(QStyle.SP_FileDialogDetailedView, "Ustawienia obrazu") toolbar_layout.addWidget(self.photo_button) toolbar_layout.addWidget(self.record_button) toolbar_layout.addWidget(self.settings_button) self.photo_button.clicked.connect(self.take_photo) self.record_button.clicked.connect(self.toggle_recording) self.settings_button.clicked.connect(self.open_settings) quit_action = QAction("Zamknij", self) quit_action.triggered.connect(self.close) self.addAction(quit_action) def _tool_button(self, icon_id: QStyle.StandardPixmap, tooltip: str) -> QToolButton: button = QToolButton() button.setIcon(self.style().standardIcon(icon_id)) button.setToolTip(tooltip) return button def resizeEvent(self, event: Any) -> None: super().resizeEvent(event) self.video_label.setGeometry(self.stage.rect()) panel_width = min(420, max(280, self.stage.width() // 3)) self.result_panel.setGeometry(self.stage.width() - panel_width - 18, 18, panel_width, 190) self.toolbar.adjustSize() toolbar_size = self.toolbar.sizeHint() self.toolbar.setGeometry( (self.stage.width() - toolbar_size.width()) // 2, self.stage.height() - toolbar_size.height() - 18, toolbar_size.width(), toolbar_size.height(), ) def closeEvent(self, event: Any) -> None: if self.video_recorder.is_recording: self.video_recorder.stop(self.current_metadata("video")) self.worker.stop() self.worker.wait(2000) super().closeEvent(event) @Slot(object) def on_frame_ready(self, frame: np.ndarray) -> None: self.last_frame = frame.copy() if self.video_recorder.is_recording: self.video_recorder.write(frame) self._show_frame(frame) @Slot(object) def on_detection_ready(self, result: DetectionResult) -> None: self.last_detection = result self.overlay_result = result if result.xyxy else None self._update_result_text(result) @Slot(str) def on_camera_error(self, message: str) -> None: self.result_text.setPlainText(message) def start_detection(self) -> None: self.overlay_result = None self.result_text.setPlainText("Wykrywanie...") self.worker.start_detection() def accept_detection(self) -> None: self.worker.accept_detection() self.overlay_result = None if self.last_detection: self._update_result_text(self.last_detection, accepted=True) def take_photo(self) -> None: if self.last_frame is None: QMessageBox.warning(self, "Zdjecie", "Brak klatki z kamery") return path = self.media_store.save_photo(self.last_frame, self.current_metadata("photo")) self.statusBar().showMessage(f"Zapisano zdjecie: {path}", 5000) def toggle_recording(self) -> None: if self.last_frame is None: QMessageBox.warning(self, "Wideo", "Brak klatki z kamery") return if self.video_recorder.is_recording: path = self.video_recorder.stop(self.current_metadata("video")) self.record_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.statusBar().showMessage(f"Zapisano film: {path}", 5000) return try: path = self.video_recorder.start(self.last_frame) except RuntimeError as exc: QMessageBox.warning(self, "Wideo", str(exc)) return self.record_button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop)) self.statusBar().showMessage(f"Nagrywanie: {path}", 5000) def open_settings(self) -> None: dialog = SettingsDialog(self.config, self) dialog.settings_saved.connect(self.save_camera_settings) dialog.exec() @Slot(dict) def save_camera_settings(self, camera_config: dict[str, Any]) -> None: self.config["camera"] = camera_config self.app_config.save(self.config) self.worker.update_camera_config(camera_config) def current_metadata(self, media_type: str) -> dict[str, Any]: return { "media_type": media_type, "created_at": datetime.now().isoformat(timespec="seconds"), "detection": self.last_detection.to_metadata() if self.last_detection else None, "camera": { "width": self.config["camera"].get("width"), "height": self.config["camera"].get("height"), "fps": self.config["camera"].get("fps"), "properties": self.config["camera"].get("properties", {}), }, "detection_config": self.config.get("detection", {}), } def _update_result_text(self, result: DetectionResult, accepted: bool = False) -> None: status = "Zatwierdzono" if accepted else "Wynik" lines = [status] if result.error: lines.append(f"Komunikat: {result.error}") if result.confidence is not None: lines.append(f"YOLO confidence: {result.confidence:.3f}") if result.parsed: lines.append(f"Zamowienie: {result.parsed.order_number or '-'}") lines.append(f"Kolor: {result.parsed.color_code or '-'}") lines.append(f"Model: {result.parsed.product_model or '-'}") if result.raw_text: lines.append("") lines.append(result.raw_text) self.result_text.setPlainText("\n".join(lines)) def _show_frame(self, frame_bgr: np.ndarray) -> None: display_frame = frame_bgr.copy() if self.overlay_result is not None: self._draw_detection(display_frame, self.overlay_result) frame_rgb = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB) h, w, channels = frame_rgb.shape image = QImage(frame_rgb.data, w, h, channels * w, QImage.Format_RGB888).copy() pixmap = QPixmap.fromImage(image) self.video_label.setPixmap( pixmap.scaled(self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) ) def _draw_detection(self, frame_bgr: np.ndarray, result: DetectionResult) -> None: mode = self.config["detection"].get("mode", "best") boxes = result.all_boxes if mode == "all" else [result.to_metadata()] for item in boxes: xyxy = item.get("xyxy") or item.get("bbox_xyxy") if not xyxy: continue x1, y1, x2, y2 = [int(value) for value in xyxy] confidence = item.get("confidence") class_name = item.get("class_name") or "label" cv2.rectangle(frame_bgr, (x1, y1), (x2, y2), (0, 220, 0), 3) caption = f"{class_name} {confidence:.2f}" if confidence is not None else class_name cv2.putText( frame_bgr, caption, (x1, max(24, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 220, 0), 2, cv2.LINE_AA, ) def run_app(app_config: AppConfig) -> int: app = QApplication([]) window = MainWindow(app_config) window.show() return app.exec()