From 508930ae3951c607535003037e29180d4be3c0cf Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 08:38:26 +0200 Subject: [PATCH] feat: implement camera management with GPhotoCamera and CameraManager classes --- controllers/main_controller.py | 25 ++++++++-- core/camera/base_camera.py | 22 +++++++++ core/camera/camera_manager.py | 86 ++++++++++++++++++++++++++++++++++ core/camera/gphoto_camera.py | 46 ++++++++++++++++++ main.py | 2 + 5 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 core/camera/base_camera.py create mode 100644 core/camera/camera_manager.py create mode 100644 core/camera/gphoto_camera.py diff --git a/controllers/main_controller.py b/controllers/main_controller.py index b84a8c3..ff5a30a 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -7,6 +7,8 @@ from ui.widgets.thumbnail_list_widget import ThumbnailListWidget from ui.widgets.split_view_widget import SplitView from .camera_controller import CameraController +from core.camera.gphoto_camera import GPhotoCamera +from core.camera.camera_manager import CameraManager class MainController: def __init__(self, view): @@ -15,7 +17,11 @@ class MainController: self.media_repo = MediaRepository(self.db) self.media_repo.sync_media() - self.camera_controller = CameraController() + camera = GPhotoCamera() + self.manager = CameraManager(camera) + + + # self.camera_controller = CameraController() self.view = view self.color_list: ColorListWidget = view.color_list_widget @@ -29,9 +35,13 @@ class MainController: self.color_list.editColor.connect(self.on_edit_color) self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) - self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) - self.camera_controller.frameReady.connect(self.split_view.set_live_image) - self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) + # self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) + self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text) + # self.camera_controller.frameReady.connect(self.split_view.set_live_image) + self.manager.frame_ready.connect(self.split_view.set_live_image) + # self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) + self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview) + def start_camera(self): pass @@ -71,3 +81,10 @@ class MainController: def take_photo(self): print("Robienie zdjęcia...") self.split_view.toglle_live_view() + + def start_liveview(self): + self.manager.start_camera() + self.manager.start_stream() + + def shutdown(self): + self.manager.stop() \ No newline at end of file diff --git a/core/camera/base_camera.py b/core/camera/base_camera.py new file mode 100644 index 0000000..8e0b9f2 --- /dev/null +++ b/core/camera/base_camera.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from PySide6.QtCore import Signal + + +class BaseCamera(ABC): + def __init__(self) -> None: + self.error_msg = None + + @abstractmethod + def connect(self) -> bool: + raise NotImplementedError + + @abstractmethod + def disconnect(self) -> None: + raise NotImplementedError + + @abstractmethod + def get_frame(self): + raise NotImplementedError + + def get_error_msg(self): + return str(self.error_msg) diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py new file mode 100644 index 0000000..53345dd --- /dev/null +++ b/core/camera/camera_manager.py @@ -0,0 +1,86 @@ +from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot +from PySide6.QtGui import QImage, QPixmap +import cv2 + +from .base_camera import BaseCamera + + +class CameraManager(QThread): + frame_ready = Signal(QPixmap) + photo_ready = Signal(QPixmap) + error_occurred = Signal(str) + + def __init__(self, camera: BaseCamera, fps: int = 15, parent: QObject | None = None) -> None: + super().__init__(parent) + self.camera = camera + self.fps = fps + self.timer = None + self.is_streaming = False + + self.is_connected = False + + + self.start() + + def run(self) -> None: + self.timer = QTimer() + self.timer.setInterval(int(1000 / self.fps)) + self.timer.timeout.connect(self._update_frame) + self.exec() + + def start_camera(self) -> None: + if self.is_connected: + return + + if self.camera.connect(): + self.is_connected = True + else: + self.is_connected = False + self.error_occurred.emit(self.camera.get_error_msg()) + + def stop_camera(self) -> None: + self.is_streaming = False + self.is_connected = False + if self.timer: + self.timer.stop() + self.camera.disconnect() + + def start_stream(self): + if not self.is_connected: + return + + if self.is_streaming: + return + + if self.timer: + self.is_streaming = True + self.timer.start() + + def stop_stream(self) -> None: + if self.is_streaming: + self.is_streaming = False + if self.timer: + self.timer.stop() + + def _update_frame(self) -> None: + if not self.is_streaming or not self.is_connected: + return + + ret, frame = self.camera.get_frame() + + if not ret: + self.error_occurred.emit(self.camera.get_error_msg()) + return + + if frame is not None: + rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + h, w, ch = rgb_image.shape + qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) + pixmap = QPixmap.fromImage(qimg) + + self.frame_ready.emit(pixmap) + + def stop(self): + self.stop_camera() + self.quit() + self.wait() diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py new file mode 100644 index 0000000..54a5b71 --- /dev/null +++ b/core/camera/gphoto_camera.py @@ -0,0 +1,46 @@ +import gphoto2 as gp +import cv2 +import numpy as np + +from .base_camera import BaseCamera + +class GPhotoCamera(BaseCamera): + def __init__(self) -> None: + super().__init__() + self.camera = None + + def connect(self) -> bool: + self.error_msg = None + try: + self.camera = gp.Camera() # type: ignore + self.camera.init() + return True + except Exception as e: + self.error_msg = f"[GPHOTO2] {e}" + self.camera = None + return False + + + def disconnect(self) -> None: + if self.camera: + self.camera.exit() + self.camera = None + + def get_frame(self): + self.error_msg = None + + if self.camera is None: + self.error_msg = "[GPHOTO2] Camera is not initialized." + return (False, None) + + try: + file = self.camera.capture_preview() # type: ignore + data = file.get_data_and_size() + frame = np.frombuffer(data, dtype=np.uint8) + frame = cv2.imdecode(frame, cv2.IMREAD_COLOR) + + return (True, frame) + except Exception as e: + self.error_msg = f"[GPHOTO2] {e}" + return (False, None) + diff --git a/main.py b/main.py index 32761c1..ce79437 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,8 @@ def main(): window = MainWindow() controller = MainController(window) controller.load_colors() + + app.aboutToQuit.connect(controller.shutdown) window.show() sys.exit(app.exec())