From 3841b44a0abb1ad240ee1c2992d587a717dbe6f9 Mon Sep 17 00:00:00 2001 From: bartool Date: Thu, 18 Sep 2025 20:18:50 +0200 Subject: [PATCH] temporary commit --- controllers/camera_controller.py | 96 +++++--------------------------- controllers/main_controller.py | 11 +++- core/base.py | 26 +++++++++ core/camera_manager.py | 69 +++++++++++++++++++++++ core/gphoto_adapter.py | 84 ++++++++++++++++++++++++++++ core/mock_gphoto.py | 64 +++++++++++++++++++++ core/opencv_adapter.py | 83 +++++++++++++++++++++++++++ 7 files changed, 349 insertions(+), 84 deletions(-) create mode 100644 core/base.py create mode 100644 core/camera_manager.py create mode 100644 core/gphoto_adapter.py create mode 100644 core/mock_gphoto.py create mode 100644 core/opencv_adapter.py diff --git a/controllers/camera_controller.py b/controllers/camera_controller.py index ed186c0..ddc5bb6 100644 --- a/controllers/camera_controller.py +++ b/controllers/camera_controller.py @@ -1,97 +1,31 @@ -# import gphoto2 as gp -import numpy as np -import cv2 - from PySide6.QtCore import QObject, QThread, Signal -from PySide6.QtGui import QImage, QPixmap - -# try: - # import gphoto2 as gp -# except: -from . import mock_gphoto as gp - -class CameraWorker(QObject): - frameReady = Signal(QPixmap) - errorOccurred = Signal(str) - - def __init__(self, fps: int = 15, parent=None): - super().__init__(parent) - self.fps = fps - self.running = False - self.camera = None - - def start_camera(self): - """Uruchom kamerę i zacznij pobierać klatki""" - try: - self.camera = gp.Camera() # type: ignore - self.camera.init() - self.running = True - self._capture_loop() - except gp.GPhoto2Error as e: - self.errorOccurred.emit(f"Błąd inicjalizacji kamery: {e}") - - def stop_camera(self): - """Zatrzymaj pobieranie""" - self.running = False - if self.camera: - try: - self.camera.exit() - except gp.GPhoto2Error: - pass - self.camera = None - - def _capture_loop(self): - """Pętla odczytu klatek w osobnym wątku""" - import time - delay = 1.0 / self.fps - - while self.running: - 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 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.frameReady.emit(pixmap) - - except gp.GPhoto2Error as e: - self.errorOccurred.emit(f"Błąd odczytu LiveView: {e}") - break - except Exception as e: - self.errorOccurred.emit(f"Nieoczekiwany błąd: {e}") - break - - time.sleep(delay) +from ..core.base import BaseImageSource, BaseControlSource class CameraController(QObject): - frameReady = Signal(QPixmap) + new_frame = Signal(object) errorOccurred = Signal(str) - def __init__(self, fps: int = 15, parent=None): + def __init__(self, image_source: BaseImageSource, control_source: BaseControlSource, parent=None): super().__init__(parent) - self.camera_thread = QThread() - self.worker = CameraWorker(fps) - self.worker.moveToThread(self.camera_thread ) + self.image_source = image_source + self.control_source = control_source - # sygnały z workera - self.worker.frameReady.connect(self.frameReady) - self.worker.errorOccurred.connect(self.errorOccurred) + self.camera_thread = QThread() + self.moveToThread(self.camera_thread) - # sygnały start/stop - self.camera_thread.started.connect(self.worker.start_camera) + self.image_source.moveToThread(self.camera_thread) + self.control_source.moveToThread(self.camera_thread) + + self.image_source.new_frame.connect(self.new_frame) + self.image_source.errorOccurred.connect(self.errorOccurred) + self.control_source.errorOccurred.connect(self.errorOccurred) def start(self): - """Start kamery w osobnym wątku""" self.camera_thread.start() + self.image_source.start() def stop(self): - """Stop kamery i zakończenie wątku""" - self.worker.stop_camera() + self.image_source.stop() self.camera_thread.quit() self.camera_thread.wait() diff --git a/controllers/main_controller.py b/controllers/main_controller.py index b84a8c3..fa1c131 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -6,7 +6,8 @@ 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.gphoto_adapter import GPhotoImageSource, GPhotoControlSource +import gphoto2 as gp class MainController: def __init__(self, view): @@ -15,7 +16,11 @@ class MainController: self.media_repo = MediaRepository(self.db) self.media_repo.sync_media() - self.camera_controller = CameraController() + camera = gp.Camera() + camera.init() + stream = GPhotoImageSource(camera=camera, fps=15) + controll = GPhotoControlSource(camera=camera) + self.camera_controller = CameraController(stream, controll) self.view = view self.color_list: ColorListWidget = view.color_list_widget @@ -30,7 +35,7 @@ class MainController: 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.camera_controller.new_frame.connect(self.split_view.set_live_image) self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) def start_camera(self): diff --git a/core/base.py b/core/base.py new file mode 100644 index 0000000..2bb2088 --- /dev/null +++ b/core/base.py @@ -0,0 +1,26 @@ +from PySide6.QtCore import QObject, Signal +from PySide6.QtGui import QPixmap + + +class BaseImageSource(QObject): + new_frame = Signal(QPixmap) + errorOccurred = Signal(str) + + def start(self): + raise NotImplementedError + + def stop(self): + raise NotImplementedError + +class BaseControlSource(QObject): + errorOccurred = Signal(str) + parameterChanged = Signal(str, object) + + def set_parameter(self, name: str, value): + raise NotImplementedError + + def get_parameter(self, name: str): + raise NotImplementedError + + def list_parameters(self) -> dict: + raise NotImplementedError \ No newline at end of file diff --git a/core/camera_manager.py b/core/camera_manager.py new file mode 100644 index 0000000..c60d5a7 --- /dev/null +++ b/core/camera_manager.py @@ -0,0 +1,69 @@ +import cv2 +import gphoto2 as gp + +from controllers.camera_controller import CameraController +from .gphoto_adapter import GPhotoImageSource, GPhotoControlSource +from .opencv_adapter import OpenCVImageSource, OpenCVControlSource + + +class CameraManager: + def __init__(self): + self.devices = [] # lista wykrytych kamer + + def detect_devices(self): + self.devices.clear() + + # --- Wykrywanie webcamów / grabberów HDMI + for index in range(5): # sprawdź kilka indeksów + cap = cv2.VideoCapture(index) + if cap.isOpened(): + self.devices.append({ + "id": f"opencv:{index}", + "name": f"Webcam / HDMI Grabber #{index}", + "type": "opencv", + "index": index + }) + cap.release() + + # --- Wykrywanie kamer gphoto2 + cameras = gp.Camera.autodetect() # type: ignore + for i, (name, addr) in enumerate(cameras): + self.devices.append({ + "id": f"gphoto:{i}", + "name": f"{name} ({addr})", + "type": "gphoto", + "addr": addr + }) + + return self.devices + + def create_controller(self, device_id, hybrid_with=None): + """ + Tworzy CameraController na podstawie id urządzenia. + Można podać hybrid_with="opencv" albo "gphoto" żeby zbudować hybrydę. + """ + device = next((d for d in self.devices if d["id"] == device_id), None) + if not device: + raise ValueError(f"Nie znaleziono urządzenia {device_id}") + + # Webcam / grabber + if device["type"] == "opencv": + cap = cv2.VideoCapture(device["index"]) + img = OpenCVImageSource(device["index"]) + ctrl = OpenCVControlSource(cap) + return CameraController(img, ctrl) + + # GPhoto camera + elif device["type"] == "gphoto": + cam = gp.Camera() # type: ignore + cam.init() + img = GPhotoImageSource(cam) + ctrl = GPhotoControlSource(cam) + return CameraController(img, ctrl) + + # Hybrydowy tryb + elif device["type"] == "hybrid": + raise NotImplementedError("Tu możesz połączyć OpenCV + GPhoto w hybrydę") + + else: + raise ValueError(f"Nieobsługiwany typ urządzenia: {device['type']}") diff --git a/core/gphoto_adapter.py b/core/gphoto_adapter.py new file mode 100644 index 0000000..2c34114 --- /dev/null +++ b/core/gphoto_adapter.py @@ -0,0 +1,84 @@ +import numpy as np +import cv2 + +from PySide6.QtCore import QObject, QThread, Signal, QTimer +from PySide6.QtGui import QImage, QPixmap + +from .base import BaseImageSource, BaseControlSource + +import gphoto2 as gp + +# try: +# import gphoto2 as gp +# except: +# from . import mock_gphoto as gp + +class GPhotoImageSource(BaseImageSource): + + def __init__(self, camera: gp.Camera, fps=10, parent=None): # type: ignore + super().__init__(parent) + self.camera = camera + self.fps = fps + self.timer = None + + def start(self): + self.timer = QTimer() + self.timer.timeout.connect(self._grab_frame) + self.timer.start(int(1000 / self.fps)) + + def stop(self): + if self.timer: + self.timer.stop() + + def _grab_frame(self): + try: + file = self.camera.capture_preview() + data = file.get_data_and_size() + frame = np.frombuffer(data, dtype=np.uint8) + frame = cv2.imdecode(frame, cv2.IMREAD_COLOR) + if frame is None: + return + + 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.new_frame.emit(pixmap) + except gp.GPhoto2Error as e: + self.errorOccurred.emit(f"GPhoto2 error: {e}") + + +class GPhotoControlSource(BaseControlSource): + + def __init__(self, camera: gp.Camera, parent=None): # type: ignore + super().__init__(parent) + self.camera = camera + + def set_parameter(self, name, value): + try: + config = self.camera.get_config() + child = config.get_child_by_name(name) + child.set_value(value) + self.camera.set_config(config) + self.parameterChanged.emit(name, value) + except gp.GPhoto2Error as e: + self.errorOccurred.emit(str(e)) + + def get_parameter(self, name): + try: + config = self.camera.get_config() + child = config.get_child_by_name(name) + return child.get_value() + except gp.GPhoto2Error as e: + self.errorOccurred.emit(str(e)) + return None + + def list_parameters(self): + params = {} + try: + config = self.camera.get_config() + for child in config.get_children(): + params[child.get_name()] = child.get_value() + except gp.GPhoto2Error as e: + self.errorOccurred.emit(str(e)) + return params \ No newline at end of file diff --git a/core/mock_gphoto.py b/core/mock_gphoto.py new file mode 100644 index 0000000..bbe6bc5 --- /dev/null +++ b/core/mock_gphoto.py @@ -0,0 +1,64 @@ +import cv2 +import numpy as np + +class GPhoto2Error(Exception): + pass + + +class CameraFileMock: + """Mock obiektu zwracanego przez gphoto2.Camera.capture_preview()""" + + def __init__(self, frame: np.ndarray): + # Kodowanie do JPEG, żeby symulować prawdziwe dane z kamery + success, buf = cv2.imencode(".jpg", frame) + if not success: + raise GPhoto2Error("Nie udało się zakodować ramki testowej.") + self._data = buf.tobytes() + + def get_data_and_size(self): + return self._data + return self._data, len(self._data) + + +class Camera: + def __init__(self): + self._frame_counter = 0 + self._running = False + + def init(self): + self._running = True + print("[my_gphoto] Kamera MOCK zainicjalizowana") + + def exit(self): + self._running = False + print("[my_gphoto] Kamera MOCK wyłączona") + + def capture_preview(self): + if not self._running: + raise GPhoto2Error("Kamera MOCK nie jest uruchomiona") + + # przykład 1: wczytaj stały obrazek z pliku + # frame = cv2.imread("test_frame.jpg") + # if frame is None: + # raise GPhoto2Error("Nie znaleziono test_frame.jpg") + + # przykład 2: wygeneruj kolorową planszę + h, w = 480, 640 + color = (self._frame_counter % 255, 100, 200) + frame = np.full((h, w, 3), color, dtype=np.uint8) + + # dodanie napisu + text = "OBRAZ TESTOWY" + font = cv2.FONT_HERSHEY_SIMPLEX + scale = 1.5 + thickness = 3 + color_text = (255, 255, 255) + + (text_w, text_h), _ = cv2.getTextSize(text, font, scale, thickness) + x = (w - text_w) // 2 + y = (h + text_h) // 2 + cv2.putText(frame, text, (x, y), font, scale, color_text, thickness, cv2.LINE_AA) + + + self._frame_counter += 1 + return CameraFileMock(frame) diff --git a/core/opencv_adapter.py b/core/opencv_adapter.py new file mode 100644 index 0000000..2c57c49 --- /dev/null +++ b/core/opencv_adapter.py @@ -0,0 +1,83 @@ +from PySide6.QtCore import QObject, Signal, QTimer +from PySide6.QtGui import QImage, QPixmap +import cv2 +import numpy as np + + +from .base import BaseImageSource, BaseControlSource + + + + + +class OpenCVImageSource(BaseImageSource): + + def __init__(self, device_index=0, fps=30, parent=None): + super().__init__(parent) + self.device_index = device_index + self.fps = fps + self.cap = None + self.timer = None + + def start(self): + self.cap = cv2.VideoCapture(self.device_index) + if not self.cap.isOpened(): + self.errorOccurred.emit(f"Nie mogę otworzyć kamery {self.device_index}") + return + + self.timer = QTimer() + self.timer.timeout.connect(self._grab_frame) + self.timer.start(int(1000 / self.fps)) + + def stop(self): + if self.timer: + self.timer.stop() + if self.cap: + self.cap.release() + + def _grab_frame(self): + if self.cap is None: + self.errorOccurred.emit(f"Kamera niezaincjalizowana!") + return + + ret, frame = self.cap.read() + if not ret: + self.errorOccurred.emit("Brak obrazu z kamery OpenCV") + return + + 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.new_frame.emit(pixmap) + + +class OpenCVControlSource(BaseControlSource): + + def __init__(self, cap: cv2.VideoCapture, parent=None): + super().__init__(parent) + self.cap = cap + + def set_parameter(self, name, value): + prop_id = getattr(cv2, name, None) + if prop_id is None: + self.errorOccurred.emit(f"Nieznany parametr {name}") + return + self.cap.set(prop_id, value) + self.parameterChanged.emit(name, value) + + def get_parameter(self, name): + prop_id = getattr(cv2, name, None) + if prop_id is None: + self.errorOccurred.emit(f"Nieznany parametr {name}") + return None + return self.cap.get(prop_id) + + def list_parameters(self): + return { + "CAP_PROP_BRIGHTNESS": self.cap.get(cv2.CAP_PROP_BRIGHTNESS), + "CAP_PROP_CONTRAST": self.cap.get(cv2.CAP_PROP_CONTRAST), + "CAP_PROP_SATURATION": self.cap.get(cv2.CAP_PROP_SATURATION), + "CAP_PROP_GAIN": self.cap.get(cv2.CAP_PROP_GAIN), + "CAP_PROP_EXPOSURE": self.cap.get(cv2.CAP_PROP_EXPOSURE), + } \ No newline at end of file