diff --git a/app/camera.py b/app/camera.py index cf56954..c99db8a 100644 --- a/app/camera.py +++ b/app/camera.py @@ -50,38 +50,20 @@ def rotate_frame(frame: np.ndarray, degrees: int) -> np.ndarray: class CameraWorker(QThread): frame_ready = Signal(object) - detection_ready = Signal(object) camera_error = Signal(str) def __init__(self, config: dict[str, Any], app_config: Any) -> None: super().__init__() self.config = config self.app_config = app_config - self.pipeline = DetectionPipeline(config, app_config) self._running = threading.Event() self._running.set() - self._detecting = False - self._accepted = False - self._frame_count = 0 self._capture: cv2.VideoCapture | None = None self._lock = threading.Lock() def stop(self) -> None: self._running.clear() - @Slot() - def start_detection(self) -> None: - with self._lock: - self._detecting = True - self._accepted = False - self._frame_count = 0 - - @Slot() - def accept_detection(self) -> None: - with self._lock: - self._detecting = False - self._accepted = True - @Slot(dict) def update_camera_config(self, camera_config: dict[str, Any]) -> None: with self._lock: @@ -115,7 +97,6 @@ class CameraWorker(QThread): rotation_degrees = int(self.config["camera"].get("rotation_degrees", 0)) frame = rotate_frame(frame, rotation_degrees) self.frame_ready.emit(frame) - self._maybe_detect(frame) finally: capture.release() self._capture = None @@ -131,15 +112,53 @@ class CameraWorker(QThread): continue capture.set(CV_CAP_PROPS[name], float(value)) - def _maybe_detect(self, frame: np.ndarray) -> None: + +class DetectionWorker(QThread): + detection_ready = Signal(object) + + def __init__(self, config: dict[str, Any], app_config: Any) -> None: + super().__init__() + self.config = config + self.app_config = app_config + self.pipeline: DetectionPipeline | None = None + self._running = threading.Event() + self._running.set() + self._pending = threading.Event() + self._lock = threading.Lock() + self._pending_frame: np.ndarray | None = None + self._busy = False + + def stop(self) -> None: + self._running.clear() + self._pending.set() + + def request_detection(self, frame: np.ndarray) -> bool: with self._lock: - detecting = self._detecting and not self._accepted - frame_stride = max(1, int(self.config["detection"].get("frame_stride", 5))) - self._frame_count += 1 - should_detect = detecting and self._frame_count % frame_stride == 0 + if self._busy or self._pending_frame is not None: + return False + self._pending_frame = frame.copy() + self._pending.set() + return True - if not should_detect: - return + def run(self) -> None: + while self._running.is_set(): + self._pending.wait(0.2) + if not self._running.is_set(): + break - result: DetectionResult = self.pipeline.process(frame) - self.detection_ready.emit(result) + with self._lock: + frame = self._pending_frame + self._pending_frame = None + self._pending.clear() + if frame is None: + continue + self._busy = True + + try: + if self.pipeline is None: + self.pipeline = DetectionPipeline(self.config, self.app_config) + result: DetectionResult = self.pipeline.process(frame) + self.detection_ready.emit(result) + finally: + with self._lock: + self._busy = False diff --git a/app/main_window.py b/app/main_window.py index 219a9d4..4233e0e 100644 --- a/app/main_window.py +++ b/app/main_window.py @@ -21,7 +21,7 @@ from PySide6.QtWidgets import ( QStyle, ) -from app.camera import CameraWorker +from app.camera import CameraWorker, DetectionWorker from app.config import AppConfig from app.detection import DetectionResult from app.media import MediaStore, VideoRecorder @@ -36,6 +36,8 @@ class MainWindow(QMainWindow): self.last_frame: np.ndarray | None = None self.overlay_result: DetectionResult | None = None self.last_detection: DetectionResult | None = None + self.detecting = False + self.detection_frame_count = 0 self.media_store = MediaStore(self.config, self.app_config) self.video_recorder = VideoRecorder(self.config, self.app_config) @@ -45,10 +47,13 @@ class MainWindow(QMainWindow): 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() + self.detection_worker = DetectionWorker(self.config, self.app_config) + self.detection_worker.detection_ready.connect(self.on_detection_ready) + self.detection_worker.start() + def _build_ui(self) -> None: self.stage = QWidget() self.setCentralWidget(self.stage) @@ -150,7 +155,9 @@ class MainWindow(QMainWindow): if self.video_recorder.is_recording: self.video_recorder.stop(self.current_metadata("video")) self.worker.stop() + self.detection_worker.stop() self.worker.wait(2000) + self.detection_worker.wait(2000) super().closeEvent(event) @Slot(object) @@ -158,10 +165,13 @@ class MainWindow(QMainWindow): self.last_frame = frame.copy() if self.video_recorder.is_recording: self.video_recorder.write(frame) + self._maybe_request_detection(frame) self._show_frame(frame) @Slot(object) def on_detection_ready(self, result: DetectionResult) -> None: + if not self.detecting: + return self.last_detection = result self.overlay_result = result if result.xyxy else None self._update_result_text(result) @@ -172,11 +182,12 @@ class MainWindow(QMainWindow): def start_detection(self) -> None: self.overlay_result = None + self.detecting = True + self.detection_frame_count = 0 self.result_text.setPlainText("Wykrywanie...") - self.worker.start_detection() def accept_detection(self) -> None: - self.worker.accept_detection() + self.detecting = False self.overlay_result = None if self.last_detection: self._update_result_text(self.last_detection, accepted=True) @@ -218,6 +229,17 @@ class MainWindow(QMainWindow): self.app_config.save(self.config) self.worker.update_camera_config(camera_config) + def _maybe_request_detection(self, frame: np.ndarray) -> None: + if not self.detecting: + return + + frame_stride = max(1, int(self.config["detection"].get("frame_stride", 5))) + self.detection_frame_count += 1 + if self.detection_frame_count % frame_stride != 0: + return + + self.detection_worker.request_detection(frame) + def current_metadata(self, media_type: str) -> dict[str, Any]: return { "media_type": media_type,