24 Commits

Author SHA1 Message Date
02edb186bb feat: Implement photo capture functionality
Implement the logic for capturing and saving photos from the live preview stream.

- Add `save_photo` method to `MediaRepository` to handle file saving and database updates.
- `MainController` now tracks the selected color and the latest camera frame.
- The "Take Photo" button is enabled only when a color is selected.
- Pressing the button saves the current preview frame to the correct media folder and refreshes the thumbnail list.
- Fixes indentation issues in `main_controller.py` caused by previous faulty replacements.
2025-10-14 10:04:01 +02:00
47c1e6040a feat: Implement State pattern for camera logic
Refactor camera control logic in `MainController` to use the State design pattern.

- Create a new `core/camera/states.py` module with state classes (`NoCamerasState`, `DetectingState`, `ReadyToStreamState`, `StreamingState`).
- `MainController` now acts as a context, delegating actions to the current state object.
- This replaces complex conditional logic with a robust, scalable, and maintainable state machine, making it easier to manage camera behavior and add new states in the future.
2025-10-14 09:03:19 +02:00
c8d9029df7 refactor: Implement Singleton pattern for DatabaseManager
Refactor the database handling to use a Singleton pattern for `DatabaseManager`.

- A single, module-level instance `db_manager` is created in `core/database.py` to ensure one database connection is used throughout the application.
- `MediaRepository` and `MainController` are updated to use this shared instance instead of creating their own.
- This simplifies dependency injection and prevents potential issues with multiple database connections.
- Also, update `review.md` to reflect the progress.
2025-10-14 08:55:45 +02:00
96c2495a8b refactor(MainController): Simplify camera button signal handling
Refactor the signal handling for the camera start/stop button in `MainController` to simplify logic and improve reliability.

- Replaced the dynamic connect/disconnect pattern with a single, persistent signal connection to `_on_start_button_clicked`.
- Centralized UI state updates (e.g., button text) into a new `_update_start_button_state` method.
- This eliminates potential errors from mismatched signal connections and makes the control flow easier to follow.
- Also fixes major indentation errors caused by a previous faulty replacement.
2025-10-14 08:49:20 +02:00
03ab345e17 refactor(SplitView): Improve image rotation logic
Refactor the image rotation mechanism in the `SplitView` widget to prevent image quality degradation.

- The original reference pixmap is now stored in `self.original_ref_pixmap`.
- Rotations are always applied to the original, unmodified pixmap, using a cumulative rotation angle.
- This avoids sequential transformations that caused gradual quality loss.
- Also fixes indentation issues caused by previous automated replacements.
2025-10-14 08:41:47 +02:00
d62b367b47 refactor: Centralize resource path management
Create a new `settings.py` file to define and manage global application paths.

Modify UI components (`split_view_widget.py`, `view_settings_dialog.py`) to use the centralized `ICONS_DIR` path constant instead of hardcoded relative paths for icons. This improves maintainability and makes the application independent of the working directory.
2025-10-14 08:33:08 +02:00
bbdb7d3459 delete unused files 2025-10-14 08:27:40 +02:00
fa84a29ab5 review concept 2025-10-14 08:26:41 +02:00
2cc496a2b2 refactor: enhance SplitView to support image rotation and handle null pixmap in ZoomableImageView 2025-10-13 22:22:35 +02:00
0bc6c01e7e refactor: enhance camera rotation functionality and integrate with live view 2025-10-13 20:54:04 +02:00
4d00f83083 refactor: update camera start button behavior to handle camera detection and live view 2025-10-13 19:39:34 +02:00
2c1233f304 refactor: initialize QTimer with parent and ensure proper thread cleanup on stop 2025-10-13 19:36:09 +02:00
feebc5153a refactor: update SplitView to use ViewWithOverlay for live and reference images 2025-10-13 05:17:59 +02:00
49a1405340 Merge branch 'feature/camera-manager' 2025-10-13 05:15:27 +02:00
c6345c569d refactor: update camera control signals and improve live view handling 2025-10-13 05:14:27 +02:00
73b51c696e Merge branch 'feature/camera-worker' into feature/camera-manager 2025-10-12 19:31:28 +02:00
511e668cb3 feat: add isConnected method to CameraWorker and is_connected method to CameraController 2025-10-12 19:29:23 +02:00
ce1b864b17 refactor: streamline MainController initialization and signal connections 2025-10-12 19:02:53 +02:00
46734208e7 refactor. new qobjct thread approuch insted qthread 2025-10-12 18:51:37 +02:00
bbdc2af605 refactor: change CameraController to inherit from QObject and manage threading in CameraManager 2025-10-12 13:41:13 +02:00
2a5f570e5e feat: implement CameraDetectionWorker for asynchronous camera detection 2025-10-12 13:08:40 +02:00
d63d616675 working on camera-manager 2025-10-12 10:31:40 +02:00
eb72b25fdb feat: add error label and update related methods in CameraPlaceholder 2025-10-12 10:28:53 +02:00
86b9cc70a6 fix: correct variable name in abilities extraction loop 2025-10-12 10:28:22 +02:00
13 changed files with 679 additions and 443 deletions

View File

@@ -1,97 +0,0 @@
# 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)
class CameraController(QObject):
frameReady = Signal(QPixmap)
errorOccurred = Signal(str)
def __init__(self, fps: int = 15, parent=None):
super().__init__(parent)
self.camera_thread = QThread()
self.worker = CameraWorker(fps)
self.worker.moveToThread(self.camera_thread )
# sygnały z workera
self.worker.frameReady.connect(self.frameReady)
self.worker.errorOccurred.connect(self.errorOccurred)
# sygnały start/stop
self.camera_thread.started.connect(self.worker.start_camera)
def start(self):
"""Start kamery w osobnym wątku"""
self.camera_thread.start()
def stop(self):
"""Stop kamery i zakończenie wątku"""
self.worker.stop_camera()
self.camera_thread.quit()
self.camera_thread.wait()

View File

@@ -1,100 +1,187 @@
from PySide6.QtWidgets import QPushButton
from pathlib import Path from pathlib import Path
from core.database import DatabaseManager from PySide6.QtCore import Slot
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QPushButton
from core.database import db_manager
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, ViewWithOverlay
# from .camera_controller import CameraController from core.camera.states import CameraState, NoCamerasState, ReadyToStreamState, StreamingState, DetectingState
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 = db_manager
self.media_repo = MediaRepository(self.db) self.media_repo = MediaRepository()
self.media_repo.sync_media() self.camera_manager = CameraManager()
# camera = GPhotoCamera() # --- State ---
# self.manager = CameraController(camera) self.selected_color_name: str | None = None
manager = CameraManager() self._latest_pixmap: QPixmap | None = None
manager.detect_gphoto()
manager.detect_opencv()
# self.camera_controller = CameraController() # --- UI Widgets ---
self.color_list: ColorListWidget = view.color_list_widget
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
self.split_view: SplitView = view.preview_widget
self.welcome_view: CameraPlaceholder = self.split_view.widget_start
self.live_view: ViewWithOverlay = self.split_view.widget_live
self.photo_button: QPushButton = view.photo_button
self.record_button: QPushButton = view.record_button
self.view = view self._connect_signals()
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.db.connect()
self.photo_button.clicked.connect(self.take_photo) self.media_repo.sync_media()
self.record_button: QPushButton = view.record_button # Disable button by default
# self.record_button.clicked.connect(self.fun_test) self.photo_button.setEnabled(False)
self.color_list.colorSelected.connect(self.on_color_selected) # Initialize state machine
self.color_list.editColor.connect(self.on_edit_color) self.state: CameraState = NoCamerasState()
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) self.state.enter_state(self)
# self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) def transition_to(self, new_state: CameraState):
# self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text) """Transitions the controller to a new state."""
# self.camera_controller.frameReady.connect(self.split_view.set_live_image) print(f"Transitioning to state: {new_state.__class__.__name__}")
# self.manager.frame_ready.connect(self.split_view.set_live_image) self.state = new_state
# self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) self.state.enter_state(self)
self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview)
def _connect_signals(self):
"""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)
def start_camera(self): # Camera signals
pass 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)
def load_colors(self) -> None: # UI control signals
colors = self.db.get_all_colors() self.photo_button.clicked.connect(self.take_photo)
print("Loaded colors:", colors) self.welcome_view.camera_start_btn.clicked.connect(self._on_start_button_clicked)
self.color_list.set_colors(colors) self.live_view.rotateCW.connect(self.camera_manager.rotate_right)
self.live_view.rotateCCW.connect(self.camera_manager.rotate_left)
def load_colors(self) -> None:
"""Loads colors from the database and populates the list."""
colors = self.db.get_all_colors()
self.color_list.set_colors(colors)
def on_color_selected(self, color_name: str): def shutdown(self):
print(f"Wybrano kolor: {color_name}") """Cleans up resources before application exit."""
color_id = self.db.get_color_id(color_name) self.camera_manager.shutdown()
if color_id is not None: self.db.disconnect()
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() # --- Slots for Database/Media ---
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): @Slot(str)
print(f"Edycja koloru: {color_name}") def on_color_selected(self, color_name: str):
self.selected_color_name = color_name
self.photo_button.setEnabled(True)
def on_thumbnail_selected(self, media_id: int): color_id = self.db.get_color_id(color_name)
media = self.db.get_media(media_id) if color_id is not None:
if media: media_items = self.db.get_media_for_color(color_id)
print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}") self.thumbnail_list.list_widget.clear()
self.split_view.set_reference_image(media['media_path']) for media in media_items:
else: if media['file_type'] == 'photo':
print(f"Nie znaleziono mediów o ID: {media_id}") file_name = Path(media['media_path']).name
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
def take_photo(self): @Slot(str)
print("Robienie zdjęcia...") def on_edit_color(self, color_name: str):
self.split_view.toglle_live_view() print(f"Edycja koloru: {color_name}") # Placeholder
def start_liveview(self): @Slot(int)
pass def on_thumbnail_selected(self, media_id: int):
# self.manager.start_camera() media = self.db.get_media(media_id)
# self.manager.start_stream() if media:
self.split_view.set_reference_image(media['media_path'])
def shutdown(self): # --- Slots for CameraManager ---
pass
# self.manager.stop() @Slot(list)
def on_cameras_detected(self, cameras: list[dict]):
"""Handles the list of detected cameras and transitions state."""
print("Detected cameras:", cameras)
if cameras:
self.transition_to(ReadyToStreamState())
else:
self.transition_to(NoCamerasState())
@Slot(QPixmap)
def on_frame_ready(self, pixmap: QPixmap):
"""Displays a new frame from the camera and stores it."""
self._latest_pixmap = pixmap
self.split_view.set_live_image(pixmap)
@Slot(str)
def on_camera_error(self, error_message: str):
"""Shows an error message from the camera and transitions to a safe state."""
print(f"Camera Error: {error_message}")
self.welcome_view.set_error_text(error_message)
self.transition_to(NoCamerasState())
@Slot()
def on_camera_started(self):
"""Transitions to StreamingState when the camera starts."""
self.transition_to(StreamingState())
@Slot()
def on_camera_stopped(self):
"""Transitions to a post-streaming state."""
self.split_view.toggle_live_view()
if self.camera_manager.get_detected_cameras():
self.transition_to(ReadyToStreamState())
else:
self.transition_to(NoCamerasState())
# --- UI Actions ---
def _on_start_button_clicked(self):
"""Delegates the button click to the current state."""
self.state.handle_start_button(self)
def camera_detect(self):
"""Initiates camera detection and transitions to DetectingState."""
self.transition_to(DetectingState())
self.camera_manager.detect_cameras()
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
camera_id = detected_cameras[0]['id']
self.camera_manager.start_camera(camera_id)
def stop_liveview(self):
"""Stops the camera feed."""
self.camera_manager.stop_camera()
def take_photo(self):
"""Takes a photo with the active camera."""
if self.selected_color_name is None:
print("Cannot take photo: No color selected.")
# Optionally: show a message to the user in the UI
return
if self._latest_pixmap is None or self._latest_pixmap.isNull():
print("Cannot take photo: No frame available.")
return
print(f"Taking photo for color: {self.selected_color_name}")
self.media_repo.save_photo(self._latest_pixmap, self.selected_color_name)
# Refresh thumbnail list
self.on_color_selected(self.selected_color_name)

View File

@@ -1,148 +0,0 @@
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
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 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):
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)
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

View File

@@ -1,115 +1,194 @@
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._rotation_index = 0
self._camera_mutex = QMutex() self._camera_mutex = QMutex()
self.start()
@Slot()
def run(self) -> None: def initialize_worker(self):
self.timer = QTimer() """Initializes the timer in the worker's thread."""
self.timer = QTimer(self)
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 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 _set_timer(self, enable: bool):
if not self.timer:
return return
if enable: if self._rotation_index == 1:
self.timer.setInterval(int(1000 / self.fps)) frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
self.timer.start() elif self._rotation_index == 2:
else: frame = cv2.rotate(frame, cv2.ROTATE_180)
self.timer.stop() elif self._rotation_index == 3:
frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
# Process the frame and emit it.
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)
@Slot()
def rotate_left(self):
self._rotation_index = (self._rotation_index - 1) % 4
@Slot()
def rotate_right(self):
self._rotation_index = (self._rotation_index + 1) % 4
class CameraController(QObject):
frame_ready = Signal(QPixmap)
photo_ready = Signal(QPixmap)
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()
_rotate_left_requested = Signal()
_rotate_right_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)
self._rotate_left_requested.connect(self._worker.rotate_left)
self._rotate_right_requested.connect(self._worker.rotate_right)
# Initialize worker when thread starts
self._thread.started.connect(self._worker.initialize_worker)
self._thread.finished.connect(self._worker.stop_camera)
self._thread.start()
def stop(self):
if self._thread.isRunning():
self._thread.quit()
self._thread.wait()
self._thread.deleteLater()
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()
def rotate_left(self):
self._rotate_left_requested.emit()
def rotate_right(self):
self._rotate_right_requested.emit()

View File

@@ -1,20 +1,127 @@
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 rotate_left(self):
self._camera_controller.rotate_left()
def rotate_right(self):
self._camera_controller.rotate_right()
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

57
core/camera/states.py Normal file
View File

@@ -0,0 +1,57 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from controllers.main_controller import MainController
class CameraState(ABC):
"""Abstract base class for all camera states."""
def enter_state(self, controller: MainController):
"""Called upon entering the state, e.g., to update the UI."""
pass
@abstractmethod
def handle_start_button(self, controller: MainController):
"""Handles the main camera button click."""
pass
class NoCamerasState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykryj kamery")
controller.welcome_view.set_info_text("Nie wykryto kamer. Kliknij, aby rozpocząć.")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.camera_detect()
class DetectingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykrywanie...")
controller.welcome_view.set_info_text("Trwa wykrywanie kamer...")
controller.welcome_view.camera_start_btn.setEnabled(False)
def handle_start_button(self, controller: MainController):
# Do nothing while detecting
pass
class ReadyToStreamState(CameraState):
def enter_state(self, controller: MainController):
cameras = controller.camera_manager.get_detected_cameras()
controller.welcome_view.set_button_text("Uruchom kamerę")
controller.welcome_view.set_info_text(f"Wykryto {len(cameras)} kamer(y).")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.start_liveview()
class StreamingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Zatrzymaj kamerę")
controller.welcome_view.camera_start_btn.setEnabled(True)
if controller.split_view.stack.currentWidget() != controller.live_view:
controller.split_view.toggle_live_view()
def handle_start_button(self, controller: MainController):
controller.stop_liveview()

View File

@@ -154,3 +154,6 @@ class DatabaseManager:
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute("DELETE FROM media WHERE color_id = ?", (color_id,)) cur.execute("DELETE FROM media WHERE color_id = ?", (color_id,))
self.conn.commit() self.conn.commit()
db_manager = DatabaseManager()

View File

@@ -1,13 +1,40 @@
from pathlib import Path from pathlib import Path
import shutil import shutil
from core.database import DatabaseManager from datetime import datetime
from PySide6.QtGui import QPixmap
from core.database import db_manager
MEDIA_DIR = Path("media") MEDIA_DIR = Path("media")
DEFAULT_ICON = Path("media/default_icon.jpg") DEFAULT_ICON = Path("media/default_icon.jpg")
class MediaRepository: class MediaRepository:
def __init__(self, db: DatabaseManager): def __init__(self):
self.db = db self.db = db_manager
def save_photo(self, pixmap: QPixmap, color_name: str):
"""Saves a pixmap to the media directory for the given color."""
color_id = self.db.get_color_id(color_name)
if color_id is None:
print(f"Error: Could not find color ID for {color_name}")
return
# Create directory if it doesn't exist
color_dir = MEDIA_DIR / color_name
color_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filename = f"photo_{timestamp}.jpg"
file_path = color_dir / filename
# Save the pixmap
if not pixmap.save(str(file_path), "JPG", 95):
print(f"Error: Failed to save pixmap to {file_path}")
return
# Add to database
self.db.add_media(color_id, str(file_path), 'photo')
print(f"Saved photo to {file_path}")
def sync_media(self): def sync_media(self):
disk_colors = {d.name for d in MEDIA_DIR.iterdir() if d.is_dir()} disk_colors = {d.name for d in MEDIA_DIR.iterdir() if d.is_dir()}

72
review.md Normal file
View File

@@ -0,0 +1,72 @@
Przejrzałem kod Twojej aplikacji. Gratuluję, to bardzo dobrze zorganizowany projekt, zwłaszcza jak na aplikację, która jest jeszcze w trakcie rozwoju. Widać dużą dbałość o architekturę i separację poszczególnych warstw.
Poniżej znajdziesz moje uwagi podzielone na kategorie, o które prosiłeś.
### 1. Architektura Aplikacji
Twoja architektura jest największym plusem projektu.
* **Wzorzec MVC/Controller:** Podział na foldery `core` (Model), `ui` (View) i `controllers` (Controller) jest świetnym zastosowaniem tego wzorca. `MainController` dobrze pełni rolę pośrednika, oddzielając logikę biznesową od interfejsu użytkownika.
* **Podsystem kamery (`core/camera`):** To jest wzorowo zaprojektowana część aplikacji.
* **Fasada (`CameraManager`):** Użycie `CameraManager` jako fasady, która ukrywa skomplikowaną logikę wyboru i zarządzania kamerą, jest doskonałym pomysłem. Upraszcza to reszcie aplikacji interakcję z kamerami.
* **Strategia (`BaseCamera`):** Abstrakcyjna klasa `BaseCamera` i jej konkretne implementacje (`GPhotoCamera`, `OpenCvCamera`) to świetne użycie wzorca strategii. Pozwala to na łatwe dodawanie nowych typów kamer w przyszłości.
* **Wątkowość:** Przeniesienie obsługi kamery do osobnego wątku (`CameraWorker` i `QThread`) jest kluczowe dla aplikacji Qt i zostało zrobione poprawnie. Dzięki temu interfejs użytkownika nie zawiesza się podczas przechwytywania obrazu.
* **Problem do rozwiązania:**
* **Zduplikowane pliki:** Zauważyłem, że w projekcie istnieją dwa pliki o nazwie `camera_controller.py` (w `controllers/` i `core/camera/`) oraz dwa pliki `mock_gphoto.py`. Wygląda na to, że te w folderze `controllers/` są starszymi, nieużywanymi wersjami. **Sugeruję ich usunięcie**, aby uniknąć pomyłek w przyszłości. Nowsze wersje w `core/camera/` są znacznie bardziej rozbudowane i bezpieczniejsze (np. używają `QMutex`).
### 2. Co można napisać lepiej? (Sugestie i Ulepszenia)
* **Zarządzanie ścieżkami i zasobami:**
* W kodzie UI (np. w `ui/widgets/split_view_widget.py`) ścieżki do ikon są wpisane na stałe (np. `"ui/icons/rotate-cw-svgrepo-com.svg"`). To może powodować problemy, jeśli uruchomisz aplikację z innego katalogu.
* **Sugestia:** Użyj biblioteki `pathlib` w połączeniu z globalną stałą, aby tworzyć absolutne ścieżki do zasobów. Możesz stworzyć plik `settings.py` lub `config.py`, w którym zdefiniujesz główny katalog aplikacji i podkatalogi z zasobami.
* **Logika rotacji obrazu referencyjnego:**
* W `SplitView.rotate_left()` i `rotate_right()`, transformujesz pixmapę, która mogła być już wcześniej obrócona (`self.ref_pixmap = self.ref_pixmap.transformed(...)`). Każda taka operacja może prowadzić do utraty jakości obrazu.
* **Sugestia:** Przechowuj oryginalny, niezmodyfikowany `QPixmap` w osobnej zmiennej. Przy każdej rotacji transformuj ten oryginalny obraz o łączny kąt obrotu, a nie wynik poprzedniej transformacji.
* **Upraszczanie logiki sygnałów w `MainController`:**
* W metodzie `on_cameras_detected` i innych, wielokrotnie rozłączasz i podłączasz sygnały do slotów przycisku (`camera_start_btn.clicked.disconnect()`). Jest to podejście podatne na błędy.
* **Sugestia:** Zamiast tego, użyj jednego slotu, który w środku sprawdza aktualny stan aplikacji (np. `if self.camera_manager.is_camera_active(): ...`). Możesz też po prostu zmieniać tekst i stan (`setEnabled`) przycisków w zależności od kontekstu.
* **Obsługa błędów:**
* W niektórych miejscach (np. `GPhotoCamera.detect`) używasz szerokiego `except Exception as e:`. Warto łapać bardziej specyficzne wyjątki (np. `gp.GPhoto2Error`), aby lepiej reagować na konkretne problemy.
### 3. Wzorce projektowe do rozważenia
Używasz już wielu dobrych wzorców (Fasada, Strategia, Obserwator przez sygnały/sloty, Worker Thread). Oto kilka dodatkowych, które mogłyby się przydać:
* **Singleton:** Klasa `DatabaseManager` jest idealnym kandydatem na Singletona. W całej aplikacji potrzebujesz tylko jednego połączenia z bazą danych. Można to zrealizować tworząc jedną, globalną instancję w module `database.py`, którą inne części aplikacji będą importować.
* **State (Stan):** Logika związana ze stanem kamery (brak kamery, wykryto, działa, zatrzymana) w `MainController` mogłaby zostać zamknięta we wzorcu Stan. Miałbyś obiekty reprezentujące każdy stan (np. `NoCameraState`, `StreamingState`), a każdy z nich inaczej obsługiwałby te same akcje (np. kliknięcie przycisku "Start"). To oczyściłoby kod z wielu instrukcji `if/else`.
### Podsumowanie i konkretne kroki
To bardzo obiecujący projekt z solidnymi fundamentami. Moje sugestie mają na celu głównie "doszlifowanie" istniejącego kodu.
**Zalecane działania (w kolejności od najważniejszych):**
1. **Wyczyść projekt:** Usuń zduplikowane pliki `controllers/camera_controller.py` i `controllers/mock_gphoto.py`, aby uniknąć nieporozumień.
2. **Zrefaktoryzuj ścieżki:** Popraw sposób zarządzania ścieżkami do ikon i innych zasobów, aby uniezależnić się od bieżącego katalogu roboczego.
3. **Popraw rotację obrazu:** Zmodyfikuj logikę w `SplitView`, aby uniknąć degradacji jakości obrazu przy wielokrotnym obracaniu.
4. **Uprość logikę UI:** Zastanów się nad refaktoryzacją obsługi stanu przycisków w `MainController`, aby kod był bardziej czytelny i mniej podatny na błędy.
Świetna robota! Jeśli masz więcej pytań lub chciałbyś, żebym przyjrzał się jakiemuś konkretnemu fragmentowi, daj znać.
---
### Postęp Prac (14.10.2025)
Na podstawie powyższej recenzji, wspólnie wprowadziliśmy następujące zmiany:
* **Zrefaktoryzowano ścieżki do zasobów (Zrealizowano):**
* Utworzono plik `settings.py` do centralnego zarządzania ścieżkami.
* Zaktualizowano komponenty UI (`split_view_widget.py`, `view_settings_dialog.py`), aby korzystały ze scentralizowanych ścieżek, co uniezależniło aplikację od katalogu roboczego.
* **Poprawiono logikę rotacji obrazu (Zrealizowano):**
* Zmieniono mechanizm obracania obrazu referencyjnego w `SplitView`, aby operacje były wykonywane na oryginalnym obrazie. Zapobiega to stopniowej utracie jakości przy wielokrotnych rotacjach.
* **Uproszczono logikę sygnałów w `MainController` (Zrealizowano):**
* Zastąpiono dynamiczne łączenie i rozłączanie sygnałów przycisku kamery jednym, stałym połączeniem i centralną metodą obsługi. Zwiększyło to czytelność i niezawodność kodu.
* **Wyczyszczono projekt (Zrealizowano):**
* Użytkownik potwierdził usunięcie zduplikowanych plików (`camera_controller.py` i `mock_gphoto.py`) z katalogu `controllers`.

13
settings.py Normal file
View File

@@ -0,0 +1,13 @@
from pathlib import Path
# Absolutna ścieżka do głównego katalogu projektu
ROOT_DIR = Path(__file__).parent.resolve()
# Ścieżka do katalogu UI
UI_DIR = ROOT_DIR / "ui"
# Ścieżka do katalogu z ikonami
ICONS_DIR = UI_DIR / "icons"
# Ścieżka do katalogu z mediami
MEDIA_DIR = ROOT_DIR / "media"

View File

@@ -2,6 +2,8 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout ,QVBoxLayout, QPushButton, QG
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtCore import Qt, QSize, Signal from PySide6.QtCore import Qt, QSize, Signal
from settings import ICONS_DIR
ISO_ARR = ["AUTO","100", "200", "400", "800", "1600", "3200"] ISO_ARR = ["AUTO","100", "200", "400", "800", "1600", "3200"]
SPEED_ARR = ["30", "25", "20", "15", "13", "10.3", "8", "6.3", "5", "4", "3.2", "2.5", "2", "1.6", "1.3", "1", "0.8", "0.6", "0.5", "0.4", "0.3", "1/4", "1/5", "1/6", "1/8", "1/10", "1/13", "1/15", "1/20", "1/25", "1/30", "1/40", "1/50", "1/60", "1/80", "1/100", "1/125", "1/160", "1/200", "1/250", "1/320", "1/400", "1/500", "1/640", "1/800", "1/1000", "1/1250", "1/1600", "1/2000", "1/2500", "1/3200", "1/4000"] SPEED_ARR = ["30", "25", "20", "15", "13", "10.3", "8", "6.3", "5", "4", "3.2", "2.5", "2", "1.6", "1.3", "1", "0.8", "0.6", "0.5", "0.4", "0.3", "1/4", "1/5", "1/6", "1/8", "1/10", "1/13", "1/15", "1/20", "1/25", "1/30", "1/40", "1/50", "1/60", "1/80", "1/100", "1/125", "1/160", "1/200", "1/250", "1/320", "1/400", "1/500", "1/640", "1/800", "1/1000", "1/1250", "1/1600", "1/2000", "1/2500", "1/3200", "1/4000"]
@@ -19,13 +21,13 @@ class LabeledSpinSelector(QWidget):
self.title_label = QLabel(title) self.title_label = QLabel(title)
decrement_button = QToolButton() decrement_button = QToolButton()
decrement_button.setIcon(QIcon("ui/icons/arrow-left-335-svgrepo-com.svg")) decrement_button.setIcon(QIcon(str(ICONS_DIR / "arrow-left-335-svgrepo-com.svg")))
decrement_button.setFixedSize(button_size, button_size) decrement_button.setFixedSize(button_size, button_size)
decrement_button.setIconSize(QSize(icon_size, icon_size)) decrement_button.setIconSize(QSize(icon_size, icon_size))
decrement_button.clicked.connect(self._decrement) decrement_button.clicked.connect(self._decrement)
increment_button = QToolButton() increment_button = QToolButton()
increment_button.setIcon(QIcon("ui/icons/arrow-right-336-svgrepo-com.svg")) increment_button.setIcon(QIcon(str(ICONS_DIR / "arrow-right-336-svgrepo-com.svg")))
increment_button.setFixedSize(button_size, button_size) increment_button.setFixedSize(button_size, button_size)
increment_button.setIconSize(QSize(icon_size, icon_size)) increment_button.setIconSize(QSize(icon_size, icon_size))
increment_button.clicked.connect(self._increment) increment_button.clicked.connect(self._increment)

View File

@@ -1,8 +1,9 @@
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QStackedWidget, QPushButton, QLabel, QToolButton from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QStackedWidget, QPushButton, QLabel, QToolButton
from PySide6.QtGui import QEnterEvent, QPixmap, QWheelEvent, QPainter, QBrush, QColor, QIcon from PySide6.QtGui import QEnterEvent, QPixmap, QWheelEvent, QPainter, QBrush, QColor, QIcon, QImage, QTransform
from PySide6.QtCore import Qt, QSize, Signal, QEvent from PySide6.QtCore import Qt, QSize, Signal, QEvent
import sys import sys
from ui.widgets.placeholder_widget import PlaceholderWidget from ui.widgets.placeholder_widget import PlaceholderWidget
from settings import ICONS_DIR
class ZoomableImageView(QGraphicsView): class ZoomableImageView(QGraphicsView):
@@ -33,10 +34,14 @@ class ZoomableImageView(QGraphicsView):
self._current_scale = 1.0 self._current_scale = 1.0
def set_image(self, pixmap: QPixmap): def set_image(self, pixmap: QPixmap):
# pixmap = QPixmap(image_path) if pixmap.isNull():
return
self._pixmap_item.setPixmap(pixmap) self._pixmap_item.setPixmap(pixmap)
self._scene.setSceneRect(pixmap.rect()) self._scene.setSceneRect(pixmap.rect())
# self.reset_transform() if self._current_scale == 1.0:
self.fitInView(self._pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
self._first_image = False
def reset_transform(self): def reset_transform(self):
"""Resetuje skalowanie i ustawia 1:1""" """Resetuje skalowanie i ustawia 1:1"""
@@ -100,16 +105,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()
@@ -163,12 +180,12 @@ class ViewWithOverlay(QWidget):
def _create_top_right_buttons(self): def _create_top_right_buttons(self):
self.cw_btn = self._create_tool_button( self.cw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-cw-svgrepo-com.svg", icon_path=str(ICONS_DIR / "rotate-cw-svgrepo-com.svg"),
callback=self.rotateCW, callback=self.rotateCW,
) )
self.ccw_btn = self._create_tool_button( self.ccw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-ccw-svgrepo-com.svg", icon_path=str(ICONS_DIR / "rotate-ccw-svgrepo-com.svg"),
callback=self.rotateCCW, callback=self.rotateCCW,
) )
@@ -184,12 +201,12 @@ class ViewWithOverlay(QWidget):
def _create_top_left_buttons(self): def _create_top_left_buttons(self):
self.camera_btn = self._create_tool_button( self.camera_btn = self._create_tool_button(
icon_path="ui/icons/settings-svgrepo-com.svg", icon_path=str(ICONS_DIR / "settings-svgrepo-com.svg"),
callback=self.cameraConnection callback=self.cameraConnection
) )
self.settings_btn = self._create_tool_button( self.settings_btn = self._create_tool_button(
icon_path="ui/icons/error-16-svgrepo-com.svg", icon_path=str(ICONS_DIR / "error-16-svgrepo-com.svg"),
callback=self.cameraSettings callback=self.cameraSettings
) )
@@ -216,12 +233,11 @@ class ViewWithOverlay(QWidget):
def toggle_orientation(self, orientation): def toggle_orientation(self, orientation):
if orientation == Qt.Orientation.Vertical: if orientation == Qt.Orientation.Vertical:
self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg")) self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-vertical-svgrepo-com.svg")))
self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg")) self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "horizontal-stacks-svgrepo-com.svg")))
else: else:
self.flip_btn.setIcon(QIcon("ui/icons/flip-horizontal-svgrepo-com.svg")) self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-horizontal-svgrepo-com.svg")))
self.orient_btn.setIcon(QIcon("ui/icons/vertical-stacks-svgrepo-com.svg")) self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "vertical-stacks-svgrepo-com.svg")))
def enterEvent(self, event: QEnterEvent) -> None: def enterEvent(self, event: QEnterEvent) -> None:
if self.live: if self.live:
self.camera_btn.show() self.camera_btn.show()
@@ -252,12 +268,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,14 +281,19 @@ 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.ref_image_rotate = 0
self.original_ref_pixmap = None
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)
self.widget_live.swapViews.connect(self.swap_views) self.widget_live.swapViews.connect(self.swap_views)
self.widget_ref.swapViews.connect(self.swap_views) self.widget_ref.swapViews.connect(self.swap_views)
self.widget_ref.rotateCCW.connect(self.rotate_left)
self.widget_ref.rotateCW.connect(self.rotate_right)
def toggle_orientation(self): def toggle_orientation(self):
if self.orientation() == Qt.Orientation.Vertical: if self.orientation() == Qt.Orientation.Vertical:
@@ -302,17 +319,34 @@ 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) self.original_ref_pixmap = QPixmap(path_image)
self.widget_ref.set_image(pixmap) self.ref_image_rotate = 0
self.widget_ref.set_image(self.original_ref_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)
else: else:
self.stack.setCurrentWidget(self.widget_start) self.stack.setCurrentWidget(self.widget_start)
def rotate_left(self):
if not self.original_ref_pixmap:
return
self.ref_image_rotate = (self.ref_image_rotate - 90) % 360
transform = QTransform().rotate(self.ref_image_rotate)
rotated_pixmap = self.original_ref_pixmap.transformed(transform)
self.widget_ref.set_image(rotated_pixmap)
def rotate_right(self):
if not self.original_ref_pixmap:
return
self.ref_image_rotate = (self.ref_image_rotate + 90) % 360
transform = QTransform().rotate(self.ref_image_rotate)
rotated_pixmap = self.original_ref_pixmap.transformed(transform)
self.widget_ref.set_image(rotated_pixmap)