From 508930ae3951c607535003037e29180d4be3c0cf Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 08:38:26 +0200 Subject: [PATCH 01/13] 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()) From 19e2c7977c3099609498574337c63f60e612fc28 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 18:46:38 +0200 Subject: [PATCH 02/13] feat: read gphoto config --- controllers/main_controller.py | 5 +- core/camera/camera_manager.py | 10 ++-- core/camera/gphoto_camera.py | 88 ++++++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/controllers/main_controller.py b/controllers/main_controller.py index ff5a30a..5bfd6d5 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -31,6 +31,9 @@ class MainController: self.photo_button: QPushButton = view.photo_button self.photo_button.clicked.connect(self.take_photo) + self.record_button: QPushButton = view.record_button + # self.record_button.clicked.connect(self.fun_test) + self.color_list.colorSelected.connect(self.on_color_selected) self.color_list.editColor.connect(self.on_edit_color) self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) @@ -84,7 +87,7 @@ class MainController: def start_liveview(self): self.manager.start_camera() - self.manager.start_stream() + # self.manager.start_stream() def shutdown(self): self.manager.stop() \ No newline at end of file diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index 53345dd..31f7616 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -19,13 +19,15 @@ class CameraManager(QThread): 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.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: diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index 54a5b71..a18e617 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -1,30 +1,63 @@ +from typing import Optional, List +from dataclasses import dataclass, field import gphoto2 as gp import cv2 import numpy as np from .base_camera import BaseCamera +camera_widget_types = { + gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore + gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore + gp.GP_WIDGET_TEXT: "GP_WIDGET_TEXT", # type: ignore + gp.GP_WIDGET_RANGE: "GP_WIDGET_RANGE", # type: ignore + gp.GP_WIDGET_TOGGLE: "GP_WIDGET_TOGGLE", # type: ignore + gp.GP_WIDGET_RADIO: "GP_WIDGET_RADIO", # type: ignore + gp.GP_WIDGET_MENU: "GP_WIDGET_MENU", # type: ignore + gp.GP_WIDGET_BUTTON: "GP_WIDGET_BUTTON", # type: ignore + gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore +} + + +@dataclass +class CameraWidget: + id: int + name: str + label: str + type: str + widget: object + value: Optional[str] = None + choices: List[str] = field(default_factory=list) + + def __str__(self) -> str: + return f"[{self.id} - {self.type}] '{self.name}' {self.label}\n\tvalue = {self.value} | choices = {self.choices}" + + class GPhotoCamera(BaseCamera): def __init__(self) -> None: super().__init__() self.camera = None + self.widgets: List[CameraWidget] = [] def connect(self) -> bool: self.error_msg = None try: self.camera = gp.Camera() # type: ignore self.camera.init() + self.read_config() + widget = self.get_setting_by_name("iso") + self.set_setting_by_id(widget.id, "100") 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 + self.widgets.clear() def get_frame(self): self.error_msg = None @@ -32,15 +65,62 @@ class GPhotoCamera(BaseCamera): 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) - + + def get_setting_by_id(self, id: int) -> CameraWidget: + return next(w for w in self.widgets if w.id == id) + + def get_setting_by_name(self, name: str) -> CameraWidget: + return next(w for w in self.widgets if w.name == name) + + def set_setting_by_id(self, id: int, value: str): + widget = self.get_setting_by_id(id) + if value in widget.choices: + widget.widget.set_value(value) # type: ignore + self.camera.set_single_config(widget.name, widget.widget) # type: ignore + + def read_config(self): + if not self.camera: + return + + self.widgets.clear() + + def parse_widget(widget): + temp_widget = CameraWidget( + id=widget.get_id(), + name=widget.get_name(), + label=widget.get_label(), + type=camera_widget_types[widget.get_type()], + widget=widget + ) + + try: + temp_widget.value = widget.get_value() + except gp.GPhoto2Error: + pass + + try: + temp_widget.choices = list(widget.get_choices()) + except gp.GPhoto2Error: + pass + + self.widgets.append(temp_widget) + + for i in range(widget.count_children()): + parse_widget(widget.get_child(i)) + + config = self.camera.get_config() + parse_widget(config) + + for wid in self.widgets: + print(wid) From 35576986c9794a7983449f316f36369829cc5da0 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 20:51:37 +0200 Subject: [PATCH 03/13] refactor gphoto_camera --- core/camera/gphoto_camera.py | 85 +++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index a18e617..30ee416 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -25,7 +25,7 @@ class CameraWidget: name: str label: str type: str - widget: object + config: object value: Optional[str] = None choices: List[str] = field(default_factory=list) @@ -44,9 +44,15 @@ class GPhotoCamera(BaseCamera): try: self.camera = gp.Camera() # type: ignore self.camera.init() - self.read_config() + config = self.camera.get_config() + self.read_config(config) + for widget in self.widgets: + print(widget) widget = self.get_setting_by_name("iso") + self.set_setting_by_id(widget.id, "400") self.set_setting_by_id(widget.id, "100") + self.set_setting_by_id(widget.id, "1600") + return True except Exception as e: self.error_msg = f"[GPHOTO2] {e}" @@ -83,44 +89,55 @@ class GPhotoCamera(BaseCamera): def get_setting_by_name(self, name: str) -> CameraWidget: return next(w for w in self.widgets if w.name == name) + def set_setting(self, widget:CameraWidget, value): + if value not in widget.choices: + return + + widget.config.set_value(value) # type: ignore + self.camera.set_single_config(widget.name, widget.config) # type: ignore + widget.value = value + new_config = self.camera.get_single_config(widget.name) + print(f"old: {widget}") + print(f"new: {new_config}") + def set_setting_by_id(self, id: int, value: str): widget = self.get_setting_by_id(id) - if value in widget.choices: - widget.widget.set_value(value) # type: ignore - self.camera.set_single_config(widget.name, widget.widget) # type: ignore - def read_config(self): - if not self.camera: + if value not in widget.choices: return + + widget.config.set_value(value) # type: ignore + self.camera.set_single_config(widget.name, widget.config) # type: ignore + # widget.value = value + new_config = self.parse_widget( self.camera.get_single_config(widget.name) ) + print(f"old: {widget}") + print(f"new: {new_config}") - self.widgets.clear() + + def parse_widget(self, config): + temp_widget = CameraWidget( + id=config.get_id(), + name=config.get_name(), + label=config.get_label(), + type=camera_widget_types[config.get_type()], + config=config + ) - def parse_widget(widget): - temp_widget = CameraWidget( - id=widget.get_id(), - name=widget.get_name(), - label=widget.get_label(), - type=camera_widget_types[widget.get_type()], - widget=widget - ) + try: + temp_widget.value = config.get_value() + except gp.GPhoto2Error: + pass - try: - temp_widget.value = widget.get_value() - except gp.GPhoto2Error: - pass + try: + temp_widget.choices = list(config.get_choices()) + except gp.GPhoto2Error: + pass - try: - temp_widget.choices = list(widget.get_choices()) - except gp.GPhoto2Error: - pass + return temp_widget + + def read_config(self, config): + self.widgets.append(self.parse_widget(config)) - self.widgets.append(temp_widget) - - for i in range(widget.count_children()): - parse_widget(widget.get_child(i)) - - config = self.camera.get_config() - parse_widget(config) - - for wid in self.widgets: - print(wid) + for i in range(config.count_children()): + child = config.get_child(i) + self.read_config(child) From abc07fd08de5e4e76e45084acf570c78424ceffb Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 21:43:44 +0200 Subject: [PATCH 04/13] refactor: replace CameraWidget with dictionary-based config handling in GPhotoCamera --- core/camera/gphoto_camera.py | 108 +++++++++++++++-------------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index 30ee416..bb925ac 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -19,25 +19,11 @@ camera_widget_types = { } -@dataclass -class CameraWidget: - id: int - name: str - label: str - type: str - config: object - value: Optional[str] = None - choices: List[str] = field(default_factory=list) - - def __str__(self) -> str: - return f"[{self.id} - {self.type}] '{self.name}' {self.label}\n\tvalue = {self.value} | choices = {self.choices}" - - class GPhotoCamera(BaseCamera): def __init__(self) -> None: super().__init__() self.camera = None - self.widgets: List[CameraWidget] = [] + self.configs: List[dict] = [] def connect(self) -> bool: self.error_msg = None @@ -46,13 +32,13 @@ class GPhotoCamera(BaseCamera): self.camera.init() config = self.camera.get_config() self.read_config(config) - for widget in self.widgets: - print(widget) - widget = self.get_setting_by_name("iso") - self.set_setting_by_id(widget.id, "400") - self.set_setting_by_id(widget.id, "100") - self.set_setting_by_id(widget.id, "1600") - + for config in self.configs: + print(config) + config_iso = self.get_config_by_name("iso") + self.set_config_by_id(config_iso['id'], "400") + self.set_config_by_id(config_iso['id'], "100") + self.set_config_by_id(config_iso['id'], "1600") + return True except Exception as e: self.error_msg = f"[GPHOTO2] {e}" @@ -63,7 +49,7 @@ class GPhotoCamera(BaseCamera): if self.camera: self.camera.exit() self.camera = None - self.widgets.clear() + self.configs.clear() def get_frame(self): self.error_msg = None @@ -83,60 +69,60 @@ class GPhotoCamera(BaseCamera): self.error_msg = f"[GPHOTO2] {e}" return (False, None) - def get_setting_by_id(self, id: int) -> CameraWidget: - return next(w for w in self.widgets if w.id == id) + def get_config_by_id(self, id: int): + return next(w for w in self.configs if w['id'] == id) - def get_setting_by_name(self, name: str) -> CameraWidget: - return next(w for w in self.widgets if w.name == name) + def get_config_by_name(self, name: str): + return next(w for w in self.configs if w['name'] == name) - def set_setting(self, widget:CameraWidget, value): - if value not in widget.choices: + def set_config(self, config, value): + if value not in config['choices']: return - - widget.config.set_value(value) # type: ignore - self.camera.set_single_config(widget.name, widget.config) # type: ignore - widget.value = value - new_config = self.camera.get_single_config(widget.name) - print(f"old: {widget}") - print(f"new: {new_config}") - def set_setting_by_id(self, id: int, value: str): - widget = self.get_setting_by_id(id) + config['config'].set_value(value) # type: ignore + if self._save_config(config): + config['value'] = value - if value not in widget.choices: - return - - widget.config.set_value(value) # type: ignore - self.camera.set_single_config(widget.name, widget.config) # type: ignore - # widget.value = value - new_config = self.parse_widget( self.camera.get_single_config(widget.name) ) - print(f"old: {widget}") - print(f"new: {new_config}") + def set_config_by_id(self, id: int, value: str): + config = self.get_config_by_id(id) - - def parse_widget(self, config): - temp_widget = CameraWidget( - id=config.get_id(), - name=config.get_name(), - label=config.get_label(), - type=camera_widget_types[config.get_type()], - config=config - ) + self.set_config(config, value) + + def set_config_by_name(self, name: str): + config = self.get_config_by_name(name) + + self.set_config(config, name) + + def _save_config(self, config): + if not self.camera: + return False + + self.camera.set_single.config(config['name'], config['widget']) + return True + + def parse_config(self, config): + new_config = { + "id": config.get_id(), + "name": config.get_name(), + "label": config.get_label(), + "type": camera_widget_types[config.get_type()], + "widget": config + } try: - temp_widget.value = config.get_value() + new_config["value"] = config.get_value() except gp.GPhoto2Error: pass try: - temp_widget.choices = list(config.get_choices()) + new_config["choices"] = list(config.get_choices()) except gp.GPhoto2Error: pass - return temp_widget - + return new_config + def read_config(self, config): - self.widgets.append(self.parse_widget(config)) + self.configs.append(self.parse_config(config)) for i in range(config.count_children()): child = config.get_child(i) From 373e01310e4ece07e80a25eb0cc0ca798182cb6c Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 21 Sep 2025 22:01:46 +0200 Subject: [PATCH 05/13] refactor: update GPhotoCamera configuration methods for consistency --- core/camera/base_camera.py | 19 +++++++++++++++++-- core/camera/gphoto_camera.py | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/camera/base_camera.py b/core/camera/base_camera.py index 8e0b9f2..8c2f5b1 100644 --- a/core/camera/base_camera.py +++ b/core/camera/base_camera.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from PySide6.QtCore import Signal class BaseCamera(ABC): @@ -17,6 +16,22 @@ class BaseCamera(ABC): @abstractmethod def get_frame(self): raise NotImplementedError - + + @abstractmethod + def get_config_by_id(self, id: int) -> dict: + raise NotImplementedError + + @abstractmethod + def get_config_by_name(self, name: str) -> dict: + raise NotImplementedError + + @abstractmethod + def set_config_by_id(self, id: int, value: str): + raise NotImplementedError + + @abstractmethod + def set_config_by_name(self, name: str, value: str): + raise NotImplementedError + def get_error_msg(self): return str(self.error_msg) diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index bb925ac..adb2c22 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -88,10 +88,10 @@ class GPhotoCamera(BaseCamera): self.set_config(config, value) - def set_config_by_name(self, name: str): + def set_config_by_name(self, name: str, value: str): config = self.get_config_by_name(name) - self.set_config(config, name) + self.set_config(config, value) def _save_config(self, config): if not self.camera: From 1ff5091250714c5db022931886ac127b6e7f7ac2 Mon Sep 17 00:00:00 2001 From: bartool Date: Sat, 27 Sep 2025 12:26:43 +0200 Subject: [PATCH 06/13] refactor: update set_config methods to specify return type as None feat: implement CvCamera class for OpenCV camera handling --- core/camera/base_camera.py | 4 +- core/camera/gphoto_camera.py | 7 --- core/camera/opencv_camera.py | 98 ++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 core/camera/opencv_camera.py diff --git a/core/camera/base_camera.py b/core/camera/base_camera.py index 8c2f5b1..fb8961d 100644 --- a/core/camera/base_camera.py +++ b/core/camera/base_camera.py @@ -26,11 +26,11 @@ class BaseCamera(ABC): raise NotImplementedError @abstractmethod - def set_config_by_id(self, id: int, value: str): + def set_config_by_id(self, id: int, value) -> None: raise NotImplementedError @abstractmethod - def set_config_by_name(self, name: str, value: str): + def set_config_by_name(self, name: str, value) -> None: raise NotImplementedError def get_error_msg(self): diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index adb2c22..84fa640 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -32,13 +32,6 @@ class GPhotoCamera(BaseCamera): self.camera.init() config = self.camera.get_config() self.read_config(config) - for config in self.configs: - print(config) - config_iso = self.get_config_by_name("iso") - self.set_config_by_id(config_iso['id'], "400") - self.set_config_by_id(config_iso['id'], "100") - self.set_config_by_id(config_iso['id'], "1600") - return True except Exception as e: self.error_msg = f"[GPHOTO2] {e}" diff --git a/core/camera/opencv_camera.py b/core/camera/opencv_camera.py new file mode 100644 index 0000000..07816c5 --- /dev/null +++ b/core/camera/opencv_camera.py @@ -0,0 +1,98 @@ +import cv2 +from typing import List + +from .base_camera import BaseCamera + + + + +class CvCamera(BaseCamera): + """Kamera oparta na cv2.VideoCapture""" + + config_map = { + 0: {"name": "frame_width", "cv_prop": cv2.CAP_PROP_FRAME_WIDTH, "default": 640}, + 1: {"name": "frame_height", "cv_prop": cv2.CAP_PROP_FRAME_HEIGHT, "default": 480}, + 2: {"name": "fps", "cv_prop": cv2.CAP_PROP_FPS, "default": 30}, + 3: {"name": "brightness", "cv_prop": cv2.CAP_PROP_BRIGHTNESS, "default": 0.5}, + 4: {"name": "contrast", "cv_prop": cv2.CAP_PROP_CONTRAST, "default": 0.5}, + 5: {"name": "saturation", "cv_prop": cv2.CAP_PROP_SATURATION, "default": 0.5}, + } + + def __init__(self, device_index: int = 0) -> None: + super().__init__() + self.device_index = device_index + self.cap = None + self.configs: List[dict] = [] + + def connect(self) -> bool: + self.error_msg = None + try: + self.cap = cv2.VideoCapture(self.device_index) + if not self.cap.isOpened(): + self.error_msg = f"[CV2] Could not open camera {self.device_index}" + return False + + self.configs.clear() + for id, conf in self.config_map.items(): + value = self.cap.get(conf["cv_prop"]) + self.configs.append( + { + "id": id, + "name": conf["name"], + "label": conf["name"].capitalize(), + "value": value, + "choices": None, # brak predefiniowanych wyborów + "cv_prop": conf["cv_prop"], + } + ) + return True + + except Exception as e: + self.error_msg = f"[CV2] {e}" + self.cap = None + return False + + def disconnect(self) -> None: + if self.cap: + self.cap.release() + self.cap = None + self.configs.clear() + + def get_frame(self): + self.error_msg = None + if self.cap is None or not self.cap.isOpened(): + self.error_msg = "[CV2] Camera is not initialized." + return (False, None) + + try: + ret, frame = self.cap.read() + if not ret: + self.error_msg = "[CV2] Failed to read frame." + return (False, None) + return (True, frame) + except Exception as e: + self.error_msg = f"[CV2] {e}" + return (False, None) + + def get_config_by_id(self, id: int): + return next(w for w in self.configs if w["id"] == id) + + def get_config_by_name(self, name: str): + return next(w for w in self.configs if w["name"] == name) + + def set_config(self, config, value: float): + if not self.cap: + return + try: + self.cap.set(config["cv_prop"], value) + config["value"] = self.cap.get(config["cv_prop"]) # sprawdz co ustawiło + except Exception as e: + self.error_msg = f"[CV2] {e}" + + def set_config_by_id(self, id: int, value: float): + config = self.get_config_by_id(id) + self.set_config(config, value) + + def set_config_by_name(self, name: str, value: float): + config = self.get_config_by_name(name) + self.set_config(config, value) From dcf4ef0f0a1f0f274f0c0efdc685d4904e7b6a06 Mon Sep 17 00:00:00 2001 From: bartool Date: Sat, 27 Sep 2025 12:26:57 +0200 Subject: [PATCH 07/13] refactor: implement set_dark_theme function for application theming --- main.py | 4 +++- ui/main_palette.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 ui/main_palette.py diff --git a/main.py b/main.py index ce79437..86989f2 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import sys from PySide6.QtWidgets import QApplication +from ui.main_palette import set_dark_theme from ui.main_window import MainWindow from controllers.main_controller import MainController @@ -8,7 +9,8 @@ from controllers.main_controller import MainController def main(): app = QApplication(sys.argv) - app.setStyle("Fusion") + set_dark_theme(app) + # app.setStyle("Fusion") window = MainWindow() controller = MainController(window) controller.load_colors() diff --git a/ui/main_palette.py b/ui/main_palette.py new file mode 100644 index 0000000..9bb95e0 --- /dev/null +++ b/ui/main_palette.py @@ -0,0 +1,48 @@ +import sys +from PySide6.QtWidgets import QApplication +from PySide6.QtGui import QPalette, QColor +from PySide6.QtCore import Qt + +def set_dark_theme(app: QApplication): + """Definiuje i stosuje ciemną paletę kolorów do aplikacji.""" + + # 1. Upewnij się, że styl jest ustawiony na "Fusion" + app.setStyle('Fusion') + + # 2. Definicja kolorów dla ciemnego motywu + palette = QPalette() + + # Kolory tła + DARK_GRAY = QColor(45, 45, 45) # Ogólne tło okien i widżetów (Base, Window) + LIGHT_GRAY = QColor(53, 53, 53) # Tło elementów, np. toolbara, menu (Window) + VERY_DARK_GRAY = QColor(32, 32, 32) # Kolor tła dla kontrolek (Button) + + # Kolory tekstu i obramowań + WHITE = QColor(200, 200, 200) # Główny kolor tekstu (Text, WindowText) + HIGHLIGHT = QColor(66, 135, 245) # Kolor podświetlenia (Highlight) + + # Ustawienie głównej palety + # palette.setColor(QPalette.ColorRole.Window, LIGHT_GRAY) + palette.setColor(QPalette.ColorRole.Window, VERY_DARK_GRAY) + palette.setColor(QPalette.ColorRole.WindowText, WHITE) + palette.setColor(QPalette.ColorRole.Base, DARK_GRAY) + palette.setColor(QPalette.ColorRole.AlternateBase, LIGHT_GRAY) + palette.setColor(QPalette.ColorRole.ToolTipBase, WHITE) + palette.setColor(QPalette.ColorRole.ToolTipText, WHITE) + palette.setColor(QPalette.ColorRole.Text, WHITE) + palette.setColor(QPalette.ColorRole.Button, VERY_DARK_GRAY) + palette.setColor(QPalette.ColorRole.ButtonText, WHITE) + palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) + palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218)) + palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(150, 150, 150)) + + # Kolory zaznaczenia/interakcji + palette.setColor(QPalette.ColorRole.Highlight, HIGHLIGHT) + palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black) + + # Kontrolki wyłączone (Disabled) + # palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(127, 127, 127)) + # palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(127, 127, 127)) + + # 3. Zastosowanie palety do aplikacji + app.setPalette(palette) From e2c8352c44f4e62b93b5c2ff4427238c8b0e5e6d Mon Sep 17 00:00:00 2001 From: bartool Date: Tue, 30 Sep 2025 18:56:47 +0200 Subject: [PATCH 08/13] fix: correct config attribute from 'config' to 'widget' in set_config method --- core/camera/gphoto_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index 84fa640..aac0f10 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -72,7 +72,7 @@ class GPhotoCamera(BaseCamera): if value not in config['choices']: return - config['config'].set_value(value) # type: ignore + config['widget'].set_value(value) # type: ignore if self._save_config(config): config['value'] = value From 196eff7fd8378f9ef2764e04ab111eb590a95ca5 Mon Sep 17 00:00:00 2001 From: bartool Date: Tue, 30 Sep 2025 18:58:56 +0200 Subject: [PATCH 09/13] refacot: change name from CameraManager to CameraController. Add set_camera nad cleanup code. --- ...camera_manager.py => camera_controller.py} | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) rename core/camera/{camera_manager.py => camera_controller.py} (54%) diff --git a/core/camera/camera_manager.py b/core/camera/camera_controller.py similarity index 54% rename from core/camera/camera_manager.py rename to core/camera/camera_controller.py index 31f7616..bb8b3c9 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_controller.py @@ -1,39 +1,52 @@ -from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot +from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot, QMutex, QMutexLocker from PySide6.QtGui import QImage, QPixmap import cv2 from .base_camera import BaseCamera -class CameraManager(QThread): +class CameraController(QThread): frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) + _enable_timer = Signal(bool) - def __init__(self, camera: BaseCamera, fps: int = 15, parent: QObject | None = None) -> None: + + def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) - self.camera = camera - self.fps = fps + self.camera = None self.timer = None + self.fps = 15 self.is_streaming = False - self.is_connected = False - self.timer = QTimer() - self.timer.setInterval(int(1000 / self.fps)) - self.timer.timeout.connect(self._update_frame) + self._camera_mutex = QMutex() self.start() + def run(self) -> None: - # self.timer = QTimer() - # self.timer.setInterval(int(1000 / self.fps)) - # self.timer.timeout.connect(self._update_frame) + self.timer = QTimer() + self.timer.timeout.connect(self._update_frame) + self._enable_timer.connect(self._set_timer) self.exec() + + def stop(self): + self.stop_camera() + self.quit() + self.wait() + + def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: + with QMutexLocker(self._camera_mutex): + self.stop_stream() + self.stop_camera() + + self.camera = camera + self.fps = fps def start_camera(self) -> None: - if self.is_connected: + if self.camera is None or self.is_connected: return - + if self.camera.connect(): self.is_connected = True else: @@ -41,38 +54,46 @@ class CameraManager(QThread): self.error_occurred.emit(self.camera.get_error_msg()) def stop_camera(self) -> None: - self.is_streaming = False + if self.is_streaming: + self.stop_stream() + + if self.camera is not None: + self.camera.disconnect() + 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() + # self.timer.start() + self._enable_timer.emit(True) def stop_stream(self) -> None: if self.is_streaming: self.is_streaming = False if self.timer: - self.timer.stop() + # self.timer.stop() + self._enable_timer.emit(False) def _update_frame(self) -> None: - if not self.is_streaming or not self.is_connected: - return + with QMutexLocker(self._camera_mutex): + if self.camera is None or not self.is_connected: + return + + if not self.is_streaming: + return - ret, frame = self.camera.get_frame() + ret, frame = self.camera.get_frame() - if not ret: - self.error_occurred.emit(self.camera.get_error_msg()) - return + 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) @@ -82,7 +103,13 @@ class CameraManager(QThread): self.frame_ready.emit(pixmap) - def stop(self): - self.stop_camera() - self.quit() - self.wait() + def _set_timer(self, enable: bool): + if not self.timer: + return + + if enable: + self.timer.setInterval(int(1000 / self.fps)) + self.timer.start() + else: + self.timer.stop() + From 324ab2e016c49b5f94d79e959753e503e5ab14f0 Mon Sep 17 00:00:00 2001 From: bartool Date: Tue, 30 Sep 2025 21:50:30 +0200 Subject: [PATCH 10/13] refactor: update camera classes to improve initialization and connection handling --- core/camera/base_camera.py | 50 ++++----- core/camera/gphoto_camera.py | 198 ++++++++++++++++++++--------------- core/camera/opencv_camera.py | 171 ++++++++++++++++-------------- 3 files changed, 229 insertions(+), 190 deletions(-) diff --git a/core/camera/base_camera.py b/core/camera/base_camera.py index fb8961d..acaea6b 100644 --- a/core/camera/base_camera.py +++ b/core/camera/base_camera.py @@ -2,36 +2,36 @@ from abc import ABC, abstractmethod class BaseCamera(ABC): - def __init__(self) -> None: - self.error_msg = None + def __init__(self) -> None: + self.error_msg = None - @abstractmethod - def connect(self) -> bool: - raise NotImplementedError + @abstractmethod + def connect(self, index: int | None = None) -> bool: + raise NotImplementedError - @abstractmethod - def disconnect(self) -> None: - raise NotImplementedError + @abstractmethod + def disconnect(self) -> None: + raise NotImplementedError - @abstractmethod - def get_frame(self): - raise NotImplementedError + @abstractmethod + def get_frame(self): + raise NotImplementedError - @abstractmethod - def get_config_by_id(self, id: int) -> dict: - raise NotImplementedError + @abstractmethod + def get_config_by_id(self, id: int) -> dict: + raise NotImplementedError - @abstractmethod - def get_config_by_name(self, name: str) -> dict: - raise NotImplementedError + @abstractmethod + def get_config_by_name(self, name: str) -> dict: + raise NotImplementedError - @abstractmethod - def set_config_by_id(self, id: int, value) -> None: - raise NotImplementedError + @abstractmethod + def set_config_by_id(self, id: int, value) -> None: + raise NotImplementedError - @abstractmethod - def set_config_by_name(self, name: str, value) -> None: - raise NotImplementedError + @abstractmethod + def set_config_by_name(self, name: str, value) -> None: + raise NotImplementedError - def get_error_msg(self): - return str(self.error_msg) + def get_error_msg(self): + return str(self.error_msg) diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index aac0f10..a6ceb2c 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -7,116 +7,140 @@ import numpy as np from .base_camera import BaseCamera camera_widget_types = { - gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore - gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore - gp.GP_WIDGET_TEXT: "GP_WIDGET_TEXT", # type: ignore - gp.GP_WIDGET_RANGE: "GP_WIDGET_RANGE", # type: ignore - gp.GP_WIDGET_TOGGLE: "GP_WIDGET_TOGGLE", # type: ignore - gp.GP_WIDGET_RADIO: "GP_WIDGET_RADIO", # type: ignore - gp.GP_WIDGET_MENU: "GP_WIDGET_MENU", # type: ignore - gp.GP_WIDGET_BUTTON: "GP_WIDGET_BUTTON", # type: ignore - gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore + gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore + gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore + gp.GP_WIDGET_TEXT: "GP_WIDGET_TEXT", # type: ignore + gp.GP_WIDGET_RANGE: "GP_WIDGET_RANGE", # type: ignore + gp.GP_WIDGET_TOGGLE: "GP_WIDGET_TOGGLE", # type: ignore + gp.GP_WIDGET_RADIO: "GP_WIDGET_RADIO", # type: ignore + gp.GP_WIDGET_MENU: "GP_WIDGET_MENU", # type: ignore + gp.GP_WIDGET_BUTTON: "GP_WIDGET_BUTTON", # type: ignore + gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore } class GPhotoCamera(BaseCamera): - def __init__(self) -> None: - super().__init__() - self.camera = None - self.configs: List[dict] = [] + def __init__(self) -> None: + super().__init__() + self.camera = None + self.configs: List[dict] = [] + self.camera_list = [] - def connect(self) -> bool: - self.error_msg = None - try: - self.camera = gp.Camera() # type: ignore - self.camera.init() - config = self.camera.get_config() - self.read_config(config) - return True - except Exception as e: - self.error_msg = f"[GPHOTO2] {e}" - self.camera = None - return False + def detect(self) -> list: + self.camera_list.clear() + cameras = gp.check_result(gp.gp_camera_autodetect()) # type: ignore + # cameras = gp.Camera().autodetect() + if not cameras or cameras.count() == 0: # type: ignore + return [] + + for i in range(cameras.count()): # type: ignore + name = cameras.get_name(i) # type: ignore + port = cameras.get_value(i) # type: ignore + self.camera_list.append({"name": name, "port": port}) + return self.camera_list + + def connect(self, index: int | None = None) -> bool: + self.error_msg = None + self.camera = gp.Camera() # type: ignore + + try: + if index: + port_info_list = gp.PortInfoList() + port_info_list.load() - def disconnect(self) -> None: - if self.camera: - self.camera.exit() - self.camera = None - self.configs.clear() + port_address = self.camera_list[index]["port"] + port_index = port_info_list.lookup_path(port_address) + + self.camera.set_port_info(port_info_list[port_index]) + + self.camera.init() + config = self.camera.get_config() + self.read_config(config) + return True + except Exception as e: + self.error_msg = f"[GPHOTO2] {e}" + self.camera = None + return False - def get_frame(self): - self.error_msg = None + def disconnect(self) -> None: + if self.camera: + self.camera.exit() + self.camera = None + self.configs.clear() - if self.camera is None: - self.error_msg = "[GPHOTO2] Camera is not initialized." - return (False, None) + def get_frame(self): + self.error_msg = 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) + if self.camera is None: + self.error_msg = "[GPHOTO2] Camera is not initialized." + return (False, None) - return (True, frame) - except Exception as e: - self.error_msg = f"[GPHOTO2] {e}" - 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) - def get_config_by_id(self, id: int): - return next(w for w in self.configs if w['id'] == id) + return (True, frame) + except Exception as e: + self.error_msg = f"[GPHOTO2] {e}" + return (False, None) - def get_config_by_name(self, name: str): - return next(w for w in self.configs if w['name'] == name) + def get_config_by_id(self, id: int): + return next(w for w in self.configs if w['id'] == id) - def set_config(self, config, value): - if value not in config['choices']: - return + def get_config_by_name(self, name: str): + return next(w for w in self.configs if w['name'] == name) - config['widget'].set_value(value) # type: ignore - if self._save_config(config): - config['value'] = value + def set_config(self, config, value): + if value not in config['choices']: + return - def set_config_by_id(self, id: int, value: str): - config = self.get_config_by_id(id) + config['widget'].set_value(value) # type: ignore + if self._save_config(config): + config['value'] = value - self.set_config(config, value) + def set_config_by_id(self, id: int, value: str): + config = self.get_config_by_id(id) - def set_config_by_name(self, name: str, value: str): - config = self.get_config_by_name(name) + self.set_config(config, value) - self.set_config(config, value) + def set_config_by_name(self, name: str, value: str): + config = self.get_config_by_name(name) - def _save_config(self, config): - if not self.camera: - return False + self.set_config(config, value) - self.camera.set_single.config(config['name'], config['widget']) - return True + def _save_config(self, config): + if not self.camera: + return False - def parse_config(self, config): - new_config = { - "id": config.get_id(), - "name": config.get_name(), - "label": config.get_label(), - "type": camera_widget_types[config.get_type()], - "widget": config - } + self.camera.set_single.config(config['name'], config['widget']) + return True - try: - new_config["value"] = config.get_value() - except gp.GPhoto2Error: - pass + def parse_config(self, config): + new_config = { + "id": config.get_id(), + "name": config.get_name(), + "label": config.get_label(), + "type": camera_widget_types[config.get_type()], + "widget": config + } - try: - new_config["choices"] = list(config.get_choices()) - except gp.GPhoto2Error: - pass + try: + new_config["value"] = config.get_value() + except gp.GPhoto2Error: + pass - return new_config + try: + new_config["choices"] = list(config.get_choices()) + except gp.GPhoto2Error: + pass - def read_config(self, config): - self.configs.append(self.parse_config(config)) + return new_config - for i in range(config.count_children()): - child = config.get_child(i) - self.read_config(child) + def read_config(self, config): + self.configs.append(self.parse_config(config)) + + for i in range(config.count_children()): + child = config.get_child(i) + self.read_config(child) diff --git a/core/camera/opencv_camera.py b/core/camera/opencv_camera.py index 07816c5..a6a20b5 100644 --- a/core/camera/opencv_camera.py +++ b/core/camera/opencv_camera.py @@ -1,98 +1,113 @@ import cv2 +from cv2_enumerate_cameras import enumerate_cameras from typing import List from .base_camera import BaseCamera - - class CvCamera(BaseCamera): - """Kamera oparta na cv2.VideoCapture""" + """Kamera oparta na cv2.VideoCapture""" - config_map = { - 0: {"name": "frame_width", "cv_prop": cv2.CAP_PROP_FRAME_WIDTH, "default": 640}, - 1: {"name": "frame_height", "cv_prop": cv2.CAP_PROP_FRAME_HEIGHT, "default": 480}, - 2: {"name": "fps", "cv_prop": cv2.CAP_PROP_FPS, "default": 30}, - 3: {"name": "brightness", "cv_prop": cv2.CAP_PROP_BRIGHTNESS, "default": 0.5}, - 4: {"name": "contrast", "cv_prop": cv2.CAP_PROP_CONTRAST, "default": 0.5}, - 5: {"name": "saturation", "cv_prop": cv2.CAP_PROP_SATURATION, "default": 0.5}, - } + config_map = { + 0: {"name": "frame_width", "cv_prop": cv2.CAP_PROP_FRAME_WIDTH, "default": 640}, + 1: {"name": "frame_height", "cv_prop": cv2.CAP_PROP_FRAME_HEIGHT, "default": 480}, + 2: {"name": "fps", "cv_prop": cv2.CAP_PROP_FPS, "default": 30}, + 3: {"name": "brightness", "cv_prop": cv2.CAP_PROP_BRIGHTNESS, "default": 0.5}, + 4: {"name": "contrast", "cv_prop": cv2.CAP_PROP_CONTRAST, "default": 0.5}, + 5: {"name": "saturation", "cv_prop": cv2.CAP_PROP_SATURATION, "default": 0.5}, + } - def __init__(self, device_index: int = 0) -> None: - super().__init__() - self.device_index = device_index - self.cap = None - self.configs: List[dict] = [] + def __init__(self) -> None: + super().__init__() + self.cap = None + self.configs: List[dict] = [] + self.camera_list = [] + self.camera_index = 0 - def connect(self) -> bool: - self.error_msg = None - try: - self.cap = cv2.VideoCapture(self.device_index) - if not self.cap.isOpened(): - self.error_msg = f"[CV2] Could not open camera {self.device_index}" - return False + def detect(self) -> list: + self.camera_list.clear() + self.camera_list = enumerate_cameras(cv2.CAP_ANY) - self.configs.clear() - for id, conf in self.config_map.items(): - value = self.cap.get(conf["cv_prop"]) - self.configs.append( - { - "id": id, - "name": conf["name"], - "label": conf["name"].capitalize(), - "value": value, - "choices": None, # brak predefiniowanych wyborów - "cv_prop": conf["cv_prop"], - } - ) - return True + result = [] + for camera in self.camera_list: + result.append({"name": camera.name, "port": camera.path}) - except Exception as e: - self.error_msg = f"[CV2] {e}" - self.cap = None - return False + return result - def disconnect(self) -> None: - if self.cap: - self.cap.release() - self.cap = None - self.configs.clear() + def connect(self, index: int | None = None) -> bool: + self.error_msg = None + try: + if index: + self.camera_index = self.camera_list[index].index - def get_frame(self): - self.error_msg = None - if self.cap is None or not self.cap.isOpened(): - self.error_msg = "[CV2] Camera is not initialized." - return (False, None) + self.cap = cv2.VideoCapture(self.camera_index) - try: - ret, frame = self.cap.read() - if not ret: - self.error_msg = "[CV2] Failed to read frame." - return (False, None) - return (True, frame) - except Exception as e: - self.error_msg = f"[CV2] {e}" - return (False, None) + if not self.cap.isOpened(): + self.error_msg = f"[CV2] Could not open camera {self.camera_index}" + return False - def get_config_by_id(self, id: int): - return next(w for w in self.configs if w["id"] == id) + self.configs.clear() + for id, conf in self.config_map.items(): + value = self.cap.get(conf["cv_prop"]) + self.configs.append( + { + "id": id, + "name": conf["name"], + "label": conf["name"].capitalize(), + "value": value, + "choices": None, # brak predefiniowanych wyborów + "cv_prop": conf["cv_prop"], + } + ) + return True - def get_config_by_name(self, name: str): - return next(w for w in self.configs if w["name"] == name) + except Exception as e: + self.error_msg = f"[CV2] {e}" + self.cap = None + return False - def set_config(self, config, value: float): - if not self.cap: - return - try: - self.cap.set(config["cv_prop"], value) - config["value"] = self.cap.get(config["cv_prop"]) # sprawdz co ustawiło - except Exception as e: - self.error_msg = f"[CV2] {e}" + def disconnect(self) -> None: + if self.cap: + self.cap.release() + self.cap = None + self.configs.clear() - def set_config_by_id(self, id: int, value: float): - config = self.get_config_by_id(id) - self.set_config(config, value) + def get_frame(self): + self.error_msg = None + if self.cap is None or not self.cap.isOpened(): + self.error_msg = "[CV2] Camera is not initialized." + return (False, None) - def set_config_by_name(self, name: str, value: float): - config = self.get_config_by_name(name) - self.set_config(config, value) + try: + ret, frame = self.cap.read() + if not ret: + self.error_msg = "[CV2] Failed to read frame." + return (False, None) + return (True, frame) + except Exception as e: + self.error_msg = f"[CV2] {e}" + return (False, None) + + def get_config_by_id(self, id: int): + return next(w for w in self.configs if w["id"] == id) + + def get_config_by_name(self, name: str): + return next(w for w in self.configs if w["name"] == name) + + def set_config(self, config, value: float): + if not self.cap: + return + try: + self.cap.set(config["cv_prop"], value) + config["value"] = self.cap.get( + config["cv_prop"]) # sprawdz co ustawiło + except Exception as e: + self.error_msg = f"[CV2] {e}" + + def set_config_by_id(self, id: int, value: float): + config = self.get_config_by_id(id) + self.set_config(config, value) + + def set_config_by_name(self, name: str, value: float): + config = self.get_config_by_name(name) + self.set_config(config, value) From dea17b8b265f2b5ac36cefea481ece46a3b1cb32 Mon Sep 17 00:00:00 2001 From: bartool Date: Wed, 1 Oct 2025 18:26:02 +0200 Subject: [PATCH 11/13] refactor: update detect method in camera classes to return dictionaries instead of lists --- core/camera/base_camera.py | 5 +++++ core/camera/gphoto_camera.py | 17 ++++++++++------- core/camera/opencv_camera.py | 31 +++++++++++++++++++++++-------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/core/camera/base_camera.py b/core/camera/base_camera.py index acaea6b..56b92b1 100644 --- a/core/camera/base_camera.py +++ b/core/camera/base_camera.py @@ -5,6 +5,11 @@ class BaseCamera(ABC): def __init__(self) -> None: self.error_msg = None + @staticmethod + @abstractmethod + def detect() -> dict: + raise NotImplementedError + @abstractmethod def connect(self, index: int | None = None) -> bool: raise NotImplementedError diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index a6ceb2c..46b1f88 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -24,20 +24,21 @@ class GPhotoCamera(BaseCamera): super().__init__() self.camera = None self.configs: List[dict] = [] - self.camera_list = [] + self.camera_index = 0 - def detect(self) -> list: - self.camera_list.clear() + @staticmethod + def detect() -> dict: cameras = gp.check_result(gp.gp_camera_autodetect()) # type: ignore # cameras = gp.Camera().autodetect() if not cameras or cameras.count() == 0: # type: ignore - return [] + return {} + camera_list = {} for i in range(cameras.count()): # type: ignore name = cameras.get_name(i) # type: ignore port = cameras.get_value(i) # type: ignore - self.camera_list.append({"name": name, "port": port}) - return self.camera_list + camera_list[i] = {"name": name, "port": port} + return camera_list def connect(self, index: int | None = None) -> bool: self.error_msg = None @@ -45,10 +46,12 @@ class GPhotoCamera(BaseCamera): try: if index: + self.camera_index = index + camera_list = GPhotoCamera.detect() port_info_list = gp.PortInfoList() port_info_list.load() - port_address = self.camera_list[index]["port"] + port_address = camera_list[index]["port"] port_index = port_info_list.lookup_path(port_address) self.camera.set_port_info(port_info_list[port_index]) diff --git a/core/camera/opencv_camera.py b/core/camera/opencv_camera.py index a6a20b5..edcb678 100644 --- a/core/camera/opencv_camera.py +++ b/core/camera/opencv_camera.py @@ -5,7 +5,7 @@ from typing import List from .base_camera import BaseCamera -class CvCamera(BaseCamera): +class OpenCvCamera(BaseCamera): """Kamera oparta na cv2.VideoCapture""" config_map = { @@ -24,13 +24,28 @@ class CvCamera(BaseCamera): self.camera_list = [] self.camera_index = 0 - def detect(self) -> list: - self.camera_list.clear() - self.camera_list = enumerate_cameras(cv2.CAP_ANY) + @staticmethod + def detect(): + camera_list = enumerate_cameras(cv2.CAP_ANY) + result = {} + seen_ports = set() - result = [] - for camera in self.camera_list: - result.append({"name": camera.name, "port": camera.path}) + for camera in camera_list: + # unikamy duplikatów tego samego /dev/videoX albo tej samej ścieżki na Windows/macOS + if camera.path in seen_ports: + continue + + cap = cv2.VideoCapture(camera.index, camera.backend) # próbujemy otworzyć + ret, frame = cap.read() + cap.release() + + if ret and frame is not None and frame.size > 0: + result[camera.index] = { + "name": camera.name, + "port": camera.path, + "backend": camera.backend, + } + seen_ports.add(camera.path) return result @@ -38,7 +53,7 @@ class CvCamera(BaseCamera): self.error_msg = None try: if index: - self.camera_index = self.camera_list[index].index + self.camera_index = index self.cap = cv2.VideoCapture(self.camera_index) From ca25b06f992d6ce62c846e1ddd7248f44f0a2bdf Mon Sep 17 00:00:00 2001 From: bartool Date: Wed, 1 Oct 2025 18:26:41 +0200 Subject: [PATCH 12/13] refactor: implement CameraManager class with methods for detecting GPhoto and OpenCV cameras --- controllers/main_controller.py | 149 +++++++++++++++++---------------- core/camera/camera_manager.py | 20 +++++ 2 files changed, 98 insertions(+), 71 deletions(-) create mode 100644 core/camera/camera_manager.py diff --git a/controllers/main_controller.py b/controllers/main_controller.py index 5bfd6d5..6453a84 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -5,89 +5,96 @@ from core.media import MediaRepository from ui.widgets.color_list_widget import ColorListWidget 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 .camera_controller import CameraController +from core.camera.camera_controller import CameraController from core.camera.camera_manager import CameraManager + +from core.camera.gphoto_camera import GPhotoCamera +from core.camera.camera_controller import CameraController + class MainController: - def __init__(self, view): - self.db = DatabaseManager() - self.db.connect() - self.media_repo = MediaRepository(self.db) - self.media_repo.sync_media() + def __init__(self, view): + self.db = DatabaseManager() + self.db.connect() + self.media_repo = MediaRepository(self.db) + self.media_repo.sync_media() - camera = GPhotoCamera() - self.manager = CameraManager(camera) + # camera = GPhotoCamera() + # self.manager = CameraController(camera) + manager = CameraManager() + manager.detect_gphoto() + manager.detect_opencv() + + # self.camera_controller = CameraController() + + self.view = view + self.color_list: ColorListWidget = view.color_list_widget + self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget + self.split_view: SplitView = view.preview_widget + + self.photo_button: QPushButton = view.photo_button + self.photo_button.clicked.connect(self.take_photo) + + self.record_button: QPushButton = view.record_button + # self.record_button.clicked.connect(self.fun_test) + + self.color_list.colorSelected.connect(self.on_color_selected) + 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.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) - # self.camera_controller = CameraController() + def start_camera(self): + pass - self.view = view - self.color_list: ColorListWidget = view.color_list_widget - self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget - self.split_view: SplitView = view.preview_widget - - self.photo_button: QPushButton = view.photo_button - self.photo_button.clicked.connect(self.take_photo) - - self.record_button: QPushButton = view.record_button - # self.record_button.clicked.connect(self.fun_test) - - self.color_list.colorSelected.connect(self.on_color_selected) - 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.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 load_colors(self) -> None: + colors = self.db.get_all_colors() + print("Loaded colors:", colors) + self.color_list.set_colors(colors) - def start_camera(self): - pass + def on_color_selected(self, color_name: str): + print(f"Wybrano kolor: {color_name}") + color_id = self.db.get_color_id(color_name) + if color_id is not None: + media_items = self.db.get_media_for_color(color_id) + print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items) - def load_colors(self) -> None: - colors = self.db.get_all_colors() - print("Loaded colors:", colors) - self.color_list.set_colors(colors) + self.thumbnail_list.list_widget.clear() + for media in media_items: + if media['file_type'] == 'photo': + file_name = Path(media['media_path']).name + self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) + else: + print(f"Nie znaleziono koloru o nazwie: {color_name}") + def on_edit_color(self, color_name: str): + print(f"Edycja koloru: {color_name}") - def on_color_selected(self, color_name: str): - print(f"Wybrano kolor: {color_name}") - color_id = self.db.get_color_id(color_name) - if color_id is not None: - media_items = self.db.get_media_for_color(color_id) - print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items) + def on_thumbnail_selected(self, media_id: int): + media = self.db.get_media(media_id) + if media: + print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}") + self.split_view.set_reference_image(media['media_path']) + else: + print(f"Nie znaleziono mediów o ID: {media_id}") - self.thumbnail_list.list_widget.clear() - for media in media_items: - if media['file_type'] == 'photo': - file_name = Path(media['media_path']).name - self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) - else: - print(f"Nie znaleziono koloru o nazwie: {color_name}") + def take_photo(self): + print("Robienie zdjęcia...") + self.split_view.toglle_live_view() - def on_edit_color(self, color_name: str): - print(f"Edycja koloru: {color_name}") + def start_liveview(self): + pass + # self.manager.start_camera() + # self.manager.start_stream() - def on_thumbnail_selected(self, media_id: int): - media = self.db.get_media(media_id) - if media: - print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}") - self.split_view.set_reference_image(media['media_path']) - else: - print(f"Nie znaleziono mediów o ID: {media_id}") - - 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 + def shutdown(self): + pass + # self.manager.stop() \ No newline at end of file diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py new file mode 100644 index 0000000..5fbea5c --- /dev/null +++ b/core/camera/camera_manager.py @@ -0,0 +1,20 @@ + + +from .gphoto_camera import GPhotoCamera +from .opencv_camera import OpenCvCamera +from .camera_controller import CameraController + + +class CameraManager: + def __init__(self) -> None: + pass + + def detect_gphoto(self): + camera_list = GPhotoCamera.detect() + print(camera_list) + return camera_list + + def detect_opencv(self): + camera_list = OpenCvCamera.detect() + print(camera_list) + return camera_list From c815762f7266f01f1a3a876eaeebe14fb6c7dd1f Mon Sep 17 00:00:00 2001 From: bartool Date: Thu, 9 Oct 2025 18:47:17 +0200 Subject: [PATCH 13/13] refactor: enhance mock camera classes and update camera detection logic --- controllers/mock_gphoto.py | 84 ++++++++++++++++++++++++++++++++++++ core/camera/gphoto_camera.py | 30 +++++++++++-- core/camera/opencv_camera.py | 22 ++++------ 3 files changed, 119 insertions(+), 17 deletions(-) diff --git a/controllers/mock_gphoto.py b/controllers/mock_gphoto.py index bbe6bc5..832b102 100644 --- a/controllers/mock_gphoto.py +++ b/controllers/mock_gphoto.py @@ -1,6 +1,16 @@ import cv2 import numpy as np +GP_WIDGET_WINDOW = 0 +GP_WIDGET_SECTION = 0 +GP_WIDGET_TEXT = 0 +GP_WIDGET_RANGE = 0 +GP_WIDGET_TOGGLE = 0 +GP_WIDGET_RADIO = 0 +GP_WIDGET_MENU = 0 +GP_WIDGET_BUTTON = 0 +GP_WIDGET_DATE = 0 + class GPhoto2Error(Exception): pass @@ -19,6 +29,65 @@ class CameraFileMock: return self._data return self._data, len(self._data) +class CameraListMock: + def count(self): + return 1 + + def get_name(self, idx): + return f"mock_name {idx}" + + def get_value(self, idx): + return f"mock_value {idx}" + +class MockPortInfo: + def __init__(self, address): + self.address = address + +class PortInfoList: + def __init__(self): + self._ports = [] + + def load(self): + # Dodaj przykładowe porty + self._ports = [MockPortInfo("usb:001,002"), MockPortInfo("usb:001,003")] + + def lookup_path(self, port_address): + for idx, port in enumerate(self._ports): + if port.address == port_address: + return idx + raise ValueError("Port not found") + + def __getitem__(self, idx): + return self._ports[idx] + +class ConfigMock: + def get_id(self): + return 0 + def get_name(self): + return "name" + def get_label(self): + return "label" + def get_type(self): + return 0 + def get_value(self): + return "value" + def get_choices(self): + return [] + def count_children(self): + return 0 + def get_child(self): + return ConfigMock() + + +class CameraAbilitiesList: + def __init__(self) -> None: + self.abilities = [] + def load(self): + return + def lookup_model(self, name): + return 1 + def get_abilities(self, abilities_index): + return 0 class Camera: def __init__(self): @@ -62,3 +131,18 @@ class Camera: self._frame_counter += 1 return CameraFileMock(frame) + + def set_port_info(self, obj): + return False + + def get_config(self): + return ConfigMock() + + def set_single_config(self, name, widget): + return True + +def gp_camera_autodetect(): + return CameraListMock() + +def check_result(obj): + return obj \ No newline at end of file diff --git a/core/camera/gphoto_camera.py b/core/camera/gphoto_camera.py index 46b1f88..003bfd5 100644 --- a/core/camera/gphoto_camera.py +++ b/core/camera/gphoto_camera.py @@ -1,11 +1,15 @@ from typing import Optional, List from dataclasses import dataclass, field -import gphoto2 as gp import cv2 import numpy as np from .base_camera import BaseCamera +try: + import gphoto2 as gp # type: ignore +except: + import controllers.mock_gphoto as gp + camera_widget_types = { gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore @@ -18,6 +22,16 @@ camera_widget_types = { gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore } +operations = [ + ("GP_OPERATION_NONE", gp.GP_OPERATION_NONE), # type: ignore + ("GP_OPERATION_CAPTURE_IMAGE", gp.GP_OPERATION_CAPTURE_IMAGE), # type: ignore + ("GP_OPERATION_CAPTURE_VIDEO", gp.GP_OPERATION_CAPTURE_VIDEO), # type: ignore + ("GP_OPERATION_CAPTURE_AUDIO", gp.GP_OPERATION_CAPTURE_AUDIO), # type: ignore + ("GP_OPERATION_CAPTURE_PREVIEW", gp.GP_OPERATION_CAPTURE_PREVIEW), # type: ignore + ("GP_OPERATION_CONFIG", gp.GP_OPERATION_CONFIG), # type: ignore + ("GP_OPERATION_TRIGGER_CAPTURE", gp.GP_OPERATION_TRIGGER_CAPTURE), # type: ignore +] + class GPhotoCamera(BaseCamera): def __init__(self) -> None: @@ -33,11 +47,21 @@ class GPhotoCamera(BaseCamera): if not cameras or cameras.count() == 0: # type: ignore return {} + abilities_list = gp.CameraAbilitiesList() # type: ignore + abilities_list.load() camera_list = {} for i in range(cameras.count()): # type: ignore name = cameras.get_name(i) # type: ignore port = cameras.get_value(i) # type: ignore - camera_list[i] = {"name": name, "port": port} + + abilities_index = abilities_list.lookup_model(name) + abilities = abilities_list.get_abilities(abilities_index) + abilities_name = [] + for name, bit in operations: + if abilities.operations & bit: # type: ignore + abilities_name.append(name) + + camera_list[i] = {"name": name, "port": port, "abilities": abilities_name} return camera_list def connect(self, index: int | None = None) -> bool: @@ -117,7 +141,7 @@ class GPhotoCamera(BaseCamera): if not self.camera: return False - self.camera.set_single.config(config['name'], config['widget']) + self.camera.set_single_config(config['name'], config['widget']) return True def parse_config(self, config): diff --git a/core/camera/opencv_camera.py b/core/camera/opencv_camera.py index edcb678..06f9016 100644 --- a/core/camera/opencv_camera.py +++ b/core/camera/opencv_camera.py @@ -28,24 +28,18 @@ class OpenCvCamera(BaseCamera): def detect(): camera_list = enumerate_cameras(cv2.CAP_ANY) result = {} - seen_ports = set() for camera in camera_list: - # unikamy duplikatów tego samego /dev/videoX albo tej samej ścieżki na Windows/macOS - if camera.path in seen_ports: - continue - - cap = cv2.VideoCapture(camera.index, camera.backend) # próbujemy otworzyć - ret, frame = cap.read() + cap = cv2.VideoCapture(camera.index, camera.backend) + # ret, frame = cap.read() cap.release() - if ret and frame is not None and frame.size > 0: - result[camera.index] = { - "name": camera.name, - "port": camera.path, - "backend": camera.backend, - } - seen_ports.add(camera.path) + # if ret and frame is not None and frame.size > 0: + result[camera.index] = { + "name": camera.name, + "port": camera.path, + "backend": camera.backend, + } return result