Compare commits

...

12 Commits

5 changed files with 387 additions and 177 deletions

View File

@@ -1,100 +1,154 @@
from PySide6.QtWidgets import QPushButton
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Slot
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QPushButton
from core.database import DatabaseManager from core.database import DatabaseManager
from core.media import MediaRepository from core.media import MediaRepository
from core.camera.camera_manager import CameraManager
from ui.widgets.color_list_widget import ColorListWidget from ui.widgets.color_list_widget import ColorListWidget
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
from ui.widgets.split_view_widget import SplitView from ui.widgets.split_view_widget import SplitView, CameraPlaceholder
# 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: class MainController:
def __init__(self, view): def __init__(self, view):
self.db = DatabaseManager() self.view = view
self.db.connect() self.db = DatabaseManager()
self.media_repo = MediaRepository(self.db) self.media_repo = MediaRepository(self.db)
self.media_repo.sync_media() self.camera_manager = CameraManager()
# camera = GPhotoCamera() # --- UI Widgets ---
# self.manager = CameraController(camera) self.color_list: ColorListWidget = view.color_list_widget
manager = CameraManager() self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
manager.detect_gphoto() self.split_view: SplitView = view.preview_widget
manager.detect_opencv() self.welcome_view: CameraPlaceholder = self.split_view.widget_start
self.photo_button: QPushButton = view.photo_button
self.record_button: QPushButton = view.record_button
# self.camera_controller = CameraController() self._connect_signals()
self.view = view self.db.connect()
self.color_list: ColorListWidget = view.color_list_widget self.media_repo.sync_media()
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget self.camera_manager.detect_cameras()
self.split_view: SplitView = view.preview_widget
self.photo_button: QPushButton = view.photo_button def _connect_signals(self):
self.photo_button.clicked.connect(self.take_photo) """Connects all signals to slots."""
# Database and media signals
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.record_button: QPushButton = view.record_button # Camera signals
# self.record_button.clicked.connect(self.fun_test) self.camera_manager.cameras_detected.connect(self.on_cameras_detected)
self.camera_manager.frame_ready.connect(self.on_frame_ready)
self.camera_manager.error_occurred.connect(self.on_camera_error)
self.camera_manager.camera_started.connect(self.on_camera_started)
self.camera_manager.camera_stopped.connect(self.on_camera_stopped)
self.color_list.colorSelected.connect(self.on_color_selected) # UI control signals
self.color_list.editColor.connect(self.on_edit_color) self.photo_button.clicked.connect(self.take_photo)
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) # self.record_button.clicked.connect(self.toggle_record) # Placeholder
self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview)
# You will need a way to select a camera, e.g., a combobox.
# self.view.camera_combobox.currentIndexChanged.connect(self.on_camera_selected_in_ui)
# self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) def load_colors(self) -> None:
# self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text) """Loads colors from the database and populates the list."""
# self.camera_controller.frameReady.connect(self.split_view.set_live_image) colors = self.db.get_all_colors()
# self.manager.frame_ready.connect(self.split_view.set_live_image) self.color_list.set_colors(colors)
# 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 shutdown(self):
"""Cleans up resources before application exit."""
self.camera_manager.shutdown()
self.db.disconnect()
# --- Slots for Database/Media ---
@Slot(str)
def on_color_selected(self, color_name: str):
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)
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'])
@Slot(str)
def on_edit_color(self, color_name: str):
print(f"Edycja koloru: {color_name}") # Placeholder
@Slot(int)
def on_thumbnail_selected(self, media_id: int):
media = self.db.get_media(media_id)
if media:
self.split_view.set_reference_image(media['media_path'])
# --- Slots for CameraManager ---
@Slot(list)
def on_cameras_detected(self, cameras: list[dict]):
"""Handles the list of detected cameras."""
print("Detected cameras:", cameras)
self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.")
# Populate a combobox in the UI here
# self.view.camera_combobox.clear()
# for camera in cameras:
# self.view.camera_combobox.addItem(camera['name'], userData=camera['id'])
@Slot(QPixmap)
def on_frame_ready(self, pixmap: QPixmap):
"""Displays a new frame from the camera."""
self.split_view.set_live_image(pixmap)
@Slot(str)
def on_camera_error(self, error_message: str):
"""Shows an error message from the camera."""
print(f"Camera Error: {error_message}")
self.welcome_view.set_error_text(error_message)
@Slot()
def on_camera_started(self):
"""Updates UI when the camera stream starts."""
self.split_view.toggle_live_view()
self.welcome_view.set_button_text("Stop Camera")
# Re-route button click to stop the camera
self.welcome_view.camera_start_btn.clicked.disconnect()
self.welcome_view.camera_start_btn.clicked.connect(self.stop_liveview)
def start_camera(self): @Slot()
pass def on_camera_stopped(self):
"""Updates UI when the camera stream stops."""
# self.split_view.show_placeholder()
self.welcome_view.set_button_text("Start Camera")
# Re-route button click to start the camera
self.welcome_view.camera_start_btn.clicked.disconnect()
self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview)
def load_colors(self) -> None: # --- UI Actions ---
colors = self.db.get_all_colors()
print("Loaded colors:", colors)
self.color_list.set_colors(colors)
def start_liveview(self):
"""Starts the camera feed."""
detected_cameras = self.camera_manager.get_detected_cameras()
if not detected_cameras:
self.on_camera_error("No cameras detected.")
return
def on_color_selected(self, color_name: str): # For now, just start the first detected camera.
print(f"Wybrano kolor: {color_name}") # In a real app, you'd get the selected camera ID from the UI.
color_id = self.db.get_color_id(color_name) camera_id = detected_cameras[0]['id']
if color_id is not None: self.camera_manager.start_camera(camera_id)
media_items = self.db.get_media_for_color(color_id)
print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items)
self.thumbnail_list.list_widget.clear() def stop_liveview(self):
for media in media_items: """Stops the camera feed."""
if media['file_type'] == 'photo': self.camera_manager.stop_camera()
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): def take_photo(self):
print(f"Edycja koloru: {color_name}") """Takes a photo with the active camera."""
print("Taking photo...") # Placeholder
def on_thumbnail_selected(self, media_id: int): # This needs to be implemented in CameraManager and called here.
media = self.db.get_media(media_id) # e.g., self.camera_manager.take_photo()
if media: self.split_view.toggle_live_view() # This seems like a UI toggle, maybe rename?
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):
pass
# self.manager.start_camera()
# self.manager.start_stream()
def shutdown(self):
pass
# self.manager.stop()

View File

@@ -1,115 +1,162 @@
from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot, QMutex, QMutexLocker from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker, QThread
from PySide6.QtGui import QImage, QPixmap from PySide6.QtGui import QImage, QPixmap
import cv2 import cv2
from .base_camera import BaseCamera from .base_camera import BaseCamera
class CameraController(QThread): class CameraWorker(QObject):
frame_ready = Signal(QPixmap) frame_ready = Signal(QPixmap)
photo_ready = Signal(QPixmap) photo_ready = Signal(QPixmap)
error_occurred = Signal(str) error_occurred = Signal(str)
_enable_timer = Signal(bool) camera_ready = Signal(bool)
def __init__(self, parent: QObject | None = None) -> None: def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent) super().__init__(parent)
self.camera = None self.camera: BaseCamera | None = None
self.timer = None self.timer: QTimer | None = None
self.fps = 15 self.fps = 15
self.is_streaming = False self.is_streaming = False
self.is_connected = False self.is_connected = False
self._camera_mutex = QMutex() self._camera_mutex = QMutex()
self.start()
@Slot()
def run(self) -> None: def initialize_worker(self):
"""Initializes the timer in the worker's thread."""
self.timer = QTimer() self.timer = QTimer()
self.timer.timeout.connect(self._update_frame) 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()
@Slot(BaseCamera, int)
def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: def set_camera(self, camera: BaseCamera, fps: int = 15) -> None:
with QMutexLocker(self._camera_mutex): with QMutexLocker(self._camera_mutex):
self.stop_stream() if self.is_streaming:
self.stop_camera() self.stop_stream()
if self.is_connected:
self.stop_camera()
self.camera = camera self.camera = camera
self.fps = fps self.fps = fps
@Slot()
def start_camera(self) -> None: def start_camera(self) -> None:
if self.camera is None or 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:
if self.is_streaming:
self.stop_stream()
if self.camera is not None:
self.camera.disconnect()
self.is_connected = False
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._enable_timer.emit(True)
def stop_stream(self) -> None:
if self.is_streaming:
self.is_streaming = False
if self.timer:
# self.timer.stop()
self._enable_timer.emit(False)
def _update_frame(self) -> None:
with QMutexLocker(self._camera_mutex): with QMutexLocker(self._camera_mutex):
if self.camera is None or not self.is_connected: if self.camera is None or self.is_connected:
return return
if not self.is_streaming: if self.camera.connect():
self.is_connected = True
self.camera_ready.emit(True)
else:
self.is_connected = False
self.camera_ready.emit(False)
self.error_occurred.emit(self.camera.get_error_msg())
@Slot()
def stop_camera(self) -> None:
with QMutexLocker(self._camera_mutex):
if self.is_streaming:
self.stop_stream()
if self.camera is not None and self.is_connected:
self.camera.disconnect()
self.is_connected = False
@Slot()
def start_stream(self):
if not self.is_connected or self.is_streaming or self.timer is None:
return
self.is_streaming = True
self.timer.setInterval(int(1000 / self.fps))
self.timer.start()
@Slot()
def stop_stream(self) -> None:
if self.is_streaming and self.timer is not None:
self.is_streaming = False
self.timer.stop()
@Slot()
def _update_frame(self) -> None:
# This method is called by the timer, which is in the same thread.
# A mutex is still good practice for accessing the shared camera object.
with QMutexLocker(self._camera_mutex):
if self.camera is None or not self.is_connected or not self.is_streaming:
return return
ret, frame = self.camera.get_frame() ret, frame = self.camera.get_frame()
if not ret: if not ret:
self.error_occurred.emit(self.camera.get_error_msg()) error_msg = self.camera.get_error_msg()
if error_msg:
self.error_occurred.emit(error_msg)
return return
if frame is not None: if frame is not None:
# Process the frame and emit it.
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape h, w, ch = rgb_image.shape
qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888)
pixmap = QPixmap.fromImage(qimg) pixmap = QPixmap.fromImage(qimg)
self.frame_ready.emit(pixmap) self.frame_ready.emit(pixmap)
def _set_timer(self, enable: bool):
if not self.timer:
return
if enable:
self.timer.setInterval(int(1000 / self.fps)) class CameraController(QObject):
self.timer.start() frame_ready = Signal(QPixmap)
else: photo_ready = Signal(QPixmap)
self.timer.stop() error_occurred = Signal(str)
camera_ready = Signal(bool)
# Signals to command the worker
_set_camera_requested = Signal(BaseCamera, int)
_start_camera_requested = Signal()
_stop_camera_requested = Signal()
_start_stream_requested = Signal()
_stop_stream_requested = Signal()
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._thread = QThread()
self._worker = CameraWorker()
self._worker.moveToThread(self._thread)
# Connect worker signals to controller signals
self._worker.frame_ready.connect(self.frame_ready)
self._worker.photo_ready.connect(self.photo_ready)
self._worker.error_occurred.connect(self.error_occurred)
self._worker.camera_ready.connect(self.camera_ready)
# Connect controller's command signals to worker's slots
self._set_camera_requested.connect(self._worker.set_camera)
self._start_camera_requested.connect(self._worker.start_camera)
self._stop_camera_requested.connect(self._worker.stop_camera)
self._start_stream_requested.connect(self._worker.start_stream)
self._stop_stream_requested.connect(self._worker.stop_stream)
# Initialize worker when thread starts
self._thread.started.connect(self._worker.initialize_worker)
self._thread.start()
def stop(self):
self._thread.quit()
self._thread.wait()
def set_camera(self, camera: BaseCamera, fps: int = 15) -> None:
self._set_camera_requested.emit(camera, fps)
def start_camera(self) -> None:
self._start_camera_requested.emit()
def stop_camera(self) -> None:
self._stop_camera_requested.emit()
def start_stream(self):
self._start_stream_requested.emit()
def stop_stream(self) -> None:
self._stop_stream_requested.emit()

View File

@@ -1,20 +1,121 @@
from PySide6.QtCore import QObject, Signal
from PySide6.QtGui import QPixmap
from .camera_controller import CameraController
from .gphoto_camera import GPhotoCamera from .gphoto_camera import GPhotoCamera
from .opencv_camera import OpenCvCamera from .opencv_camera import OpenCvCamera
from .camera_controller import CameraController from .base_camera import BaseCamera
class CameraManager: class CameraManager(QObject):
def __init__(self) -> None: """
pass Zarządza wszystkimi operacjami związanymi z kamerami,
stanowiąc fasadę dla reszty aplikacji.
"""
frame_ready = Signal(QPixmap)
error_occurred = Signal(str)
cameras_detected = Signal(list)
camera_started = Signal()
camera_stopped = Signal()
def detect_gphoto(self): def __init__(self, parent: QObject | None = None) -> None:
camera_list = GPhotoCamera.detect() super().__init__(parent)
print(camera_list) self._camera_controller = CameraController()
return camera_list self._detected_cameras: list[dict] = []
self._active_camera: BaseCamera | None = None
self._active_camera_info: dict | None = None
def detect_opencv(self): # Przekazywanie sygnałów z kontrolera kamery na zewnątrz
camera_list = OpenCvCamera.detect() self._camera_controller.frame_ready.connect(self.frame_ready)
print(camera_list) self._camera_controller.error_occurred.connect(self.error_occurred)
return camera_list self._camera_controller.camera_ready.connect(self.start_liveview)
def detect_cameras(self) -> None:
"""Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV)."""
self._detected_cameras.clear()
# Wykryj kamery GPhoto
try:
gphoto_cameras = GPhotoCamera.detect()
for index, info in gphoto_cameras.items():
self._detected_cameras.append({
"id": f"gphoto_{index}",
"name": f"{info['name']} ({info['port']})",
"type": "gphoto",
"index": index
})
except Exception as e:
self.error_occurred.emit(
f"Błąd podczas wykrywania kamer GPhoto: {e}")
# Wykryj kamery OpenCV
try:
opencv_cameras = OpenCvCamera.detect()
for index, info in opencv_cameras.items():
self._detected_cameras.append({
"id": f"opencv_{index}",
"name": f"OpenCV: {info['name']}",
"type": "opencv",
"index": index
})
except Exception as e:
self.error_occurred.emit(
f"Błąd podczas wykrywania kamer OpenCV: {e}")
self.cameras_detected.emit(self._detected_cameras)
def get_detected_cameras(self) -> list[dict]:
return self._detected_cameras
def start_camera(self, camera_id: str, fps: int = 15) -> None:
"""Uruchamia wybraną kamerę."""
if self._active_camera:
self.stop_camera()
camera_info = next(
(c for c in self._detected_cameras if c['id'] == camera_id), None)
if not camera_info:
self.error_occurred.emit(
f"Nie znaleziono kamery o ID: {camera_id}")
return
camera_type = camera_info['type']
camera_index = camera_info['index']
if camera_type == "gphoto":
self._active_camera = GPhotoCamera()
elif camera_type == "opencv":
self._active_camera = OpenCvCamera()
else:
self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}")
return
self._active_camera_info = camera_info
self._camera_controller.set_camera(self._active_camera, fps)
self._camera_controller.start_camera()
def start_liveview(self, connected):
if connected:
self._camera_controller.start_stream()
self.camera_started.emit()
else:
self._active_camera = None
self._active_camera_info = None
def stop_camera(self) -> None:
"""Zatrzymuje aktywną kamerę."""
if self._active_camera:
self._camera_controller.stop_camera()
self._active_camera = None
self._active_camera_info = None
self.camera_stopped.emit()
def get_active_camera_info(self) -> dict | None:
return self._active_camera_info
def shutdown(self) -> None:
"""Zamyka kontroler kamery i jego wątek."""
self.stop_camera()
self._camera_controller.stop()

View File

@@ -57,9 +57,9 @@ class GPhotoCamera(BaseCamera):
abilities_index = abilities_list.lookup_model(name) abilities_index = abilities_list.lookup_model(name)
abilities = abilities_list.get_abilities(abilities_index) abilities = abilities_list.get_abilities(abilities_index)
abilities_name = [] abilities_name = []
for name, bit in operations: for operation, bit in operations:
if abilities.operations & bit: # type: ignore if abilities.operations & bit: # type: ignore
abilities_name.append(name) abilities_name.append(operation)
camera_list[i] = {"name": name, "port": port, "abilities": abilities_name} camera_list[i] = {"name": name, "port": port, "abilities": abilities_name}
return camera_list return camera_list

View File

@@ -100,16 +100,28 @@ class CameraPlaceholder(QWidget):
"background-color: transparent; color: #CECECE; font-size: 18px;") "background-color: transparent; color: #CECECE; font-size: 18px;")
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.error_label = QLabel()
self.error_label.setStyleSheet(
"background-color: transparent; color: #CECECE; font-size: 18px; font-style: italic;")
self.error_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addStretch() layout.addStretch()
layout.addWidget(self.camera_start_btn, layout.addWidget(self.camera_start_btn,
alignment=Qt.AlignmentFlag.AlignCenter) alignment=Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.info_label) layout.addWidget(self.info_label)
layout.addWidget(self.error_label)
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
def set_info_text(self, text: str): def set_info_text(self, text: str):
self.info_label.setText(text) self.info_label.setText(text)
def set_error_text(self, text: str):
self.error_label.setText(text)
def set_button_text(self, text:str):
self.camera_start_btn.setText(text)
class ViewWithOverlay(QWidget): class ViewWithOverlay(QWidget):
cameraConnection = Signal() cameraConnection = Signal()
@@ -252,12 +264,8 @@ class SplitView(QSplitter):
self.setOrientation(Qt.Orientation.Vertical) self.setOrientation(Qt.Orientation.Vertical)
self.widget_start = CameraPlaceholder() self.widget_start = CameraPlaceholder()
# self.widget_live = ZoomableImageView() self.widget_live = ViewWithOverlay(live=True)
self.widget_live = ViewWithOverlay()
# self.widget_live = PlaceholderWidget("Camera View", "#750466")
# self.widget_ref = ZoomableImageView()
self.widget_ref = ViewWithOverlay() self.widget_ref = ViewWithOverlay()
# self.widget_ref = PlaceholderWidget("Image View", "#007981")
self.stack = QStackedWidget() self.stack = QStackedWidget()
self.stack.addWidget(self.widget_start) self.stack.addWidget(self.widget_start)
@@ -269,9 +277,9 @@ class SplitView(QSplitter):
self.setSizes([self.height(), 0]) self.setSizes([self.height(), 0])
pixmap = QPixmap("media/empty_guitar_h.jpg") # pixmap = QPixmap("media/empty_guitar_h.jpg")
# pixmap.fill(Qt.GlobalColor.lightGray) # pixmap.fill(Qt.GlobalColor.lightGray)
self.widget_live.set_image(pixmap) # self.widget_live.set_image(pixmap)
self.widget_live.toggleOrientation.connect(self.toggle_orientation) self.widget_live.toggleOrientation.connect(self.toggle_orientation)
self.widget_ref.toggleOrientation.connect(self.toggle_orientation) self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
@@ -302,15 +310,15 @@ class SplitView(QSplitter):
def set_live_image(self, pixmap: QPixmap): def set_live_image(self, pixmap: QPixmap):
"""Ustawienie obrazu na żywo""" """Ustawienie obrazu na żywo"""
self.widget_live.set_image(pixmap) self.widget_live.set_image(pixmap)
if self.stack.currentWidget() != self.widget_live: # if self.stack.currentWidget() != self.widget_live:
self.stack.setCurrentWidget(self.widget_live) # self.stack.setCurrentWidget(self.widget_live)
def set_reference_image(self, path_image: str): def set_reference_image(self, path_image: str):
"""Ustawienie obrazu referencyjnego""" """Ustawienie obrazu referencyjnego"""
pixmap = QPixmap(path_image) pixmap = QPixmap(path_image)
self.widget_ref.set_image(pixmap) self.widget_ref.set_image(pixmap)
def toglle_live_view(self): def toggle_live_view(self):
"""Przełączanie widoku na żywo""" """Przełączanie widoku na żywo"""
if self.stack.currentWidget() == self.widget_start: if self.stack.currentWidget() == self.widget_start:
self.stack.setCurrentWidget(self.widget_live) self.stack.setCurrentWidget(self.widget_live)