diff --git a/app/main_window.py b/app/main_window.py index e981746..37efc26 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -6,12 +6,13 @@ from typing import Any import cv2 import numpy as np -from PySide6.QtCore import Qt, Slot +from PySide6.QtCore import Qt, QTimer, Slot from PySide6.QtGui import QAction, QImage, QPixmap from PySide6.QtWidgets import ( QApplication, QHBoxLayout, QLabel, + QFileDialog, QMainWindow, QMessageBox, QPushButton, @@ -42,6 +43,10 @@ class MainWindow(QMainWindow): self.fps_frame_count = 0 self.fps_last_time = time.monotonic() self.display_fps = 0.0 + self.video_capture: cv2.VideoCapture | None = None + self.video_timer = QTimer(self) + self.video_timer.timeout.connect(self._read_video_frame) + self.video_playing = False self.media_store = MediaStore(self.config, self.app_config) self.video_recorder = VideoRecorder(self.config, self.app_config) @@ -119,12 +124,19 @@ class MainWindow(QMainWindow): ) toolbar_layout = QHBoxLayout(self.toolbar) toolbar_layout.setContentsMargins(8, 6, 8, 6) + self.load_video_button = self._tool_button(QStyle.SP_DirOpenIcon, "Wczytaj film") + self.video_play_button = self._tool_button(QStyle.SP_MediaPlay, "Play/pauza filmu") 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.load_video_button) + toolbar_layout.addWidget(self.video_play_button) toolbar_layout.addWidget(self.photo_button) toolbar_layout.addWidget(self.record_button) toolbar_layout.addWidget(self.settings_button) + self.video_play_button.setEnabled(False) + self.load_video_button.clicked.connect(self.load_video) + self.video_play_button.clicked.connect(self.toggle_video_playback) self.photo_button.clicked.connect(self.take_photo) self.record_button.clicked.connect(self.toggle_recording) self.settings_button.clicked.connect(self.open_settings) @@ -158,9 +170,10 @@ class MainWindow(QMainWindow): def closeEvent(self, event: Any) -> None: if self.video_recorder.is_recording: self.video_recorder.stop(self.current_metadata("video")) - self.worker.stop() + self.video_timer.stop() + self._close_video_capture() + self._stop_camera_worker() self.detection_worker.stop() - self.worker.wait(2000) self.detection_worker.wait(2000) super().closeEvent(event) @@ -223,6 +236,41 @@ class MainWindow(QMainWindow): self.record_button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop)) self.statusBar().showMessage(f"Nagrywanie: {path}", 5000) + def load_video(self) -> None: + path, _ = QFileDialog.getOpenFileName( + self, + "Wczytaj film", + "", + "Filmy (*.mp4 *.avi *.mov *.mkv *.m4v);;Wszystkie pliki (*)", + ) + if not path: + return + + capture = cv2.VideoCapture(path) + if not capture.isOpened(): + QMessageBox.warning(self, "Film", "Nie mozna otworzyc pliku wideo") + capture.release() + return + + if self.video_recorder.is_recording: + self.video_recorder.stop(self.current_metadata("video")) + self.record_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) + + self._stop_camera_worker() + self._close_video_capture() + self.video_capture = capture + self.video_play_button.setEnabled(True) + self._set_video_playing(False) + self.overlay_result = None + self.last_detection = None + self.result_text.setPlainText(f"Wczytano film: {path}") + self._read_video_frame() + + def toggle_video_playback(self) -> None: + if self.video_capture is None: + return + self._set_video_playing(not self.video_playing) + def open_settings(self) -> None: dialog = SettingsDialog(self.config, self) dialog.settings_saved.connect(self.save_camera_settings) @@ -232,7 +280,54 @@ class MainWindow(QMainWindow): 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) + if self.worker is not None: + self.worker.update_camera_config(camera_config) + + def _read_video_frame(self) -> None: + if self.video_capture is None: + return + + ok, frame = self.video_capture.read() + if not ok or frame is None: + self._set_video_playing(False) + self.video_capture.set(cv2.CAP_PROP_POS_FRAMES, 0) + self.statusBar().showMessage("Koniec filmu", 3000) + return + + self.on_frame_ready(frame) + + def _set_video_playing(self, playing: bool) -> None: + self.video_playing = playing + if self.video_capture is None: + self.video_timer.stop() + self.video_play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) + self.video_play_button.setEnabled(False) + return + + if playing: + fps = self.video_capture.get(cv2.CAP_PROP_FPS) + if fps <= 0: + fps = float(self.config["camera"].get("fps", 30)) + interval_ms = max(1, int(round(1000 / fps))) + self.video_timer.start(interval_ms) + self.video_play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) + else: + self.video_timer.stop() + self.video_play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) + + def _close_video_capture(self) -> None: + self._set_video_playing(False) + if self.video_capture is not None: + self.video_capture.release() + self.video_capture = None + self.video_play_button.setEnabled(False) + + def _stop_camera_worker(self) -> None: + if self.worker is None: + return + self.worker.stop() + self.worker.wait(2000) + self.worker = None def _maybe_request_detection(self, frame: np.ndarray) -> None: if not self.detecting: @@ -277,10 +372,18 @@ class MainWindow(QMainWindow): lines.append(f"Komunikat: {result.error}") if result.confidence is not None: lines.append(f"YOLO confidence: {result.confidence:.3f}") + if result.ocr_engine: + lines.append(f"OCR: {result.ocr_engine}") + if result.ocr_confidence is not None: + lines.append(f"OCR confidence: {result.ocr_confidence:.3f}") + if result.ocr_elapsed_ms is not None: + lines.append(f"OCR czas: {result.ocr_elapsed_ms:.0f} ms") 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 '-'}") + color_score = _format_score(result.parsed.color_score) + model_score = _format_score(result.parsed.product_model_score) + lines.append(f"Kolor: {result.parsed.color_code or '-'}{color_score}") + lines.append(f"Model: {result.parsed.product_model or '-'}{model_score}") if result.raw_text: lines.append("") lines.append(result.raw_text) @@ -326,7 +429,7 @@ class MainWindow(QMainWindow): def _draw_fps(self, frame_bgr: np.ndarray) -> None: label = f"FPS: {self.display_fps:.1f}" - cv2.rectangle(frame_bgr, (12, 12), (122, 46), (0, 0, 0), -1) + cv2.rectangle(frame_bgr, (12, 12), (142, 46), (0, 0, 0), -1) cv2.putText( frame_bgr, label, @@ -344,3 +447,9 @@ def run_app(app_config: AppConfig) -> int: window = MainWindow(app_config) window.show() return app.exec() + + +def _format_score(score: float | None) -> str: + if score is None: + return "" + return f" ({score:.2f})" diff --git a/app_config.json b/app_config.json index 0c51d51..21c7da7 100644 --- a/app_config.json +++ b/app_config.json @@ -20,19 +20,32 @@ } }, "detection": { - "model_path": "models/best.pt", + "model_path": "models/best_v1.pt", "confidence_threshold": 0.25, "mode": "best", - "frame_stride": 5, + "frame_stride": 30, "image_size": 640, "device": "cpu" }, "ocr": { "enabled": true, + "engine": "paddle", "language": "eng", "tesseract_cmd": null, + "psm": 6, + "margin": 0, "threshold": true, - "scale": 2.0 + "paddle_threshold": false, + "scale": 2.0, + "config": "", + "use_angle_cls": true, + "paddle": { + "enable_mkldnn": false, + "lang": "en", + "use_doc_orientation_classify": false, + "use_doc_unwarping": false, + "use_textline_orientation": false + } }, "capture": { "photos_dir": "captures/photos", @@ -45,13 +58,33 @@ "show_fps": true }, "label_data": { + "model_min_score": 0.72, + "color_min_score": 0.72, "models": [ - "Regius", - "Duvell" + "Regius 6", + "Regius 7", + "Duvell 6", + "Duvell 7", + "Duvell Elite 6", + "Duvell Elite 7" ], "colors": [ "T-NF-BLK-OUT-BST-G", - "T-BLK-G" + "T-BLK-G", + "T-BLK-S", + "T-BLK-M", + "M-BLK-G", + "M-BLK-S", + "M-BLK-M", + "T-CST-G", + "T-CST-S", + "T-CST-M", + "T-ANTIQUE-G", + "T-ANTIQUE-S", + "T-ANTIQUE-M", + "T-NAT-G", + "T-NAT-S", + "T-NAT-M" ] } }