diff --git a/app/config.py b/app/config.py index 7dd74d1..e67a1cf 100644 --- a/app/config.py +++ b/app/config.py @@ -53,6 +53,9 @@ DEFAULT_CONFIG: dict[str, Any] = { "video_extension": "mp4", "video_codec": "mp4v", }, + "display": { + "show_fps": True, + }, "label_data": {"models": ["Regius", "Duvell"], "colors": ["T-NF-BLK-OUT-BST-G", "T-BLK-G"]}, } diff --git a/app/main_window.py b/app/main_window.py index 4233e0e..e981746 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from datetime import datetime from typing import Any @@ -38,6 +39,9 @@ class MainWindow(QMainWindow): self.last_detection: DetectionResult | None = None self.detecting = False self.detection_frame_count = 0 + self.fps_frame_count = 0 + self.fps_last_time = time.monotonic() + self.display_fps = 0.0 self.media_store = MediaStore(self.config, self.app_config) self.video_recorder = VideoRecorder(self.config, self.app_config) @@ -162,6 +166,7 @@ class MainWindow(QMainWindow): @Slot(object) def on_frame_ready(self, frame: np.ndarray) -> None: + self._update_fps() self.last_frame = frame.copy() if self.video_recorder.is_recording: self.video_recorder.write(frame) @@ -240,6 +245,17 @@ class MainWindow(QMainWindow): self.detection_worker.request_detection(frame) + def _update_fps(self) -> None: + self.fps_frame_count += 1 + now = time.monotonic() + elapsed = now - self.fps_last_time + if elapsed < 1.0: + return + + self.display_fps = self.fps_frame_count / elapsed + self.fps_frame_count = 0 + self.fps_last_time = now + def current_metadata(self, media_type: str) -> dict[str, Any]: return { "media_type": media_type, @@ -274,6 +290,8 @@ class MainWindow(QMainWindow): display_frame = frame_bgr.copy() if self.overlay_result is not None: self._draw_detection(display_frame, self.overlay_result) + if self.config.get("display", {}).get("show_fps", True): + self._draw_fps(display_frame) frame_rgb = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB) h, w, channels = frame_rgb.shape @@ -306,6 +324,20 @@ class MainWindow(QMainWindow): cv2.LINE_AA, ) + 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.putText( + frame_bgr, + label, + (20, 36), + cv2.FONT_HERSHEY_SIMPLEX, + 0.7, + (255, 255, 255), + 2, + cv2.LINE_AA, + ) + def run_app(app_config: AppConfig) -> int: app = QApplication([]) diff --git a/app/media.py b/app/media.py index 52657bb..ee620ec 100644 --- a/app/media.py +++ b/app/media.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import time from datetime import datetime from pathlib import Path from typing import Any @@ -54,6 +55,10 @@ class VideoRecorder: self.path: Path | None = None self.writer: cv2.VideoWriter | None = None self.started_at: str | None = None + self.started_monotonic: float | None = None + self.fps = 0.0 + self.frames_written = 0 + self.last_frame: np.ndarray | None = None @property def is_recording(self) -> bool: @@ -66,24 +71,37 @@ class VideoRecorder: capture_cfg = self.config["capture"] self.path = MediaStore(self.config, self.app_config).video_path() h, w = frame_bgr.shape[:2] - fps = float(self.config["camera"].get("fps", 30)) + self.fps = float(self.config["camera"].get("fps", 30)) codec = str(capture_cfg.get("video_codec", "mp4v")) fourcc = cv2.VideoWriter_fourcc(*codec[:4]) - self.writer = cv2.VideoWriter(str(self.path), fourcc, fps, (w, h)) + self.writer = cv2.VideoWriter(str(self.path), fourcc, self.fps, (w, h)) if not self.writer.isOpened(): self.writer = None raise RuntimeError("Nie mozna uruchomic zapisu wideo") self.started_at = datetime.now().isoformat(timespec="seconds") + self.started_monotonic = time.monotonic() + self.frames_written = 0 + self.last_frame = None self.write(frame_bgr) return self.path def write(self, frame_bgr: np.ndarray) -> None: - if self.writer is not None: - self.writer.write(frame_bgr) + if self.writer is None or self.started_monotonic is None: + return + + elapsed = max(0.0, time.monotonic() - self.started_monotonic) + target_frames = max(1, int(elapsed * self.fps) + 1) + frame = frame_bgr.copy() + while self.frames_written < target_frames: + self.writer.write(frame) + self.frames_written += 1 + self.last_frame = frame def stop(self, metadata: dict[str, Any]) -> Path | None: if self.writer is None: return None + if self.last_frame is not None: + self.write(self.last_frame) self.writer.release() self.writer = None path = self.path @@ -93,9 +111,15 @@ class VideoRecorder: "recording": { "started_at": self.started_at, "stopped_at": datetime.now().isoformat(timespec="seconds"), + "fps": self.fps, + "frames_written": self.frames_written, }, } write_metadata(path, metadata) self.path = None self.started_at = None + self.started_monotonic = None + self.fps = 0.0 + self.frames_written = 0 + self.last_frame = None return path diff --git a/app_config.json b/app_config.json index 956d52b..0c51d51 100644 --- a/app_config.json +++ b/app_config.json @@ -41,6 +41,9 @@ "video_extension": "mp4", "video_codec": "mp4v" }, + "display": { + "show_fps": true + }, "label_data": { "models": [ "Regius",