Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02edb186bb | |||
| 47c1e6040a | |||
| c8d9029df7 | |||
| 96c2495a8b | |||
| 03ab345e17 | |||
| d62b367b47 | |||
| bbdb7d3459 | |||
| fa84a29ab5 |
@@ -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()
|
||||
@@ -3,21 +3,26 @@ from PySide6.QtCore import Slot
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
from core.database import DatabaseManager
|
||||
from core.database import db_manager
|
||||
from core.media import MediaRepository
|
||||
from core.camera.camera_manager import CameraManager
|
||||
from ui.widgets.color_list_widget import ColorListWidget
|
||||
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
|
||||
from ui.widgets.split_view_widget import SplitView, CameraPlaceholder, ViewWithOverlay
|
||||
from core.camera.states import CameraState, NoCamerasState, ReadyToStreamState, StreamingState, DetectingState
|
||||
|
||||
|
||||
class MainController:
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
self.db = DatabaseManager()
|
||||
self.media_repo = MediaRepository(self.db)
|
||||
self.db = db_manager
|
||||
self.media_repo = MediaRepository()
|
||||
self.camera_manager = CameraManager()
|
||||
|
||||
# --- State ---
|
||||
self.selected_color_name: str | None = None
|
||||
self._latest_pixmap: QPixmap | None = None
|
||||
|
||||
# --- UI Widgets ---
|
||||
self.color_list: ColorListWidget = view.color_list_widget
|
||||
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
||||
@@ -31,7 +36,19 @@ class MainController:
|
||||
|
||||
self.db.connect()
|
||||
self.media_repo.sync_media()
|
||||
self.camera_manager.detect_cameras()
|
||||
|
||||
# Disable button by default
|
||||
self.photo_button.setEnabled(False)
|
||||
|
||||
# Initialize state machine
|
||||
self.state: CameraState = NoCamerasState()
|
||||
self.state.enter_state(self)
|
||||
|
||||
def transition_to(self, new_state: CameraState):
|
||||
"""Transitions the controller to a new state."""
|
||||
print(f"Transitioning to state: {new_state.__class__.__name__}")
|
||||
self.state = new_state
|
||||
self.state.enter_state(self)
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connects all signals to slots."""
|
||||
@@ -49,7 +66,7 @@ class MainController:
|
||||
|
||||
# UI control signals
|
||||
self.photo_button.clicked.connect(self.take_photo)
|
||||
self.welcome_view.camera_start_btn.clicked.connect(self.camera_detect)
|
||||
self.welcome_view.camera_start_btn.clicked.connect(self._on_start_button_clicked)
|
||||
self.live_view.rotateCW.connect(self.camera_manager.rotate_right)
|
||||
self.live_view.rotateCCW.connect(self.camera_manager.rotate_left)
|
||||
|
||||
@@ -67,6 +84,9 @@ class MainController:
|
||||
|
||||
@Slot(str)
|
||||
def on_color_selected(self, color_name: str):
|
||||
self.selected_color_name = color_name
|
||||
self.photo_button.setEnabled(True)
|
||||
|
||||
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)
|
||||
@@ -90,57 +110,49 @@ class MainController:
|
||||
|
||||
@Slot(list)
|
||||
def on_cameras_detected(self, cameras: list[dict]):
|
||||
"""Handles the list of detected cameras."""
|
||||
"""Handles the list of detected cameras and transitions state."""
|
||||
print("Detected cameras:", cameras)
|
||||
self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.")
|
||||
|
||||
self.welcome_view.camera_start_btn.clicked.disconnect()
|
||||
|
||||
if len(cameras) == 0:
|
||||
self.welcome_view.set_button_text("Wykryj kamery")
|
||||
self.welcome_view.camera_start_btn.clicked.connect(self.camera_detect)
|
||||
if cameras:
|
||||
self.transition_to(ReadyToStreamState())
|
||||
else:
|
||||
self.welcome_view.set_button_text("Uruchom kamere")
|
||||
self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview)
|
||||
|
||||
# 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'])
|
||||
self.transition_to(NoCamerasState())
|
||||
|
||||
@Slot(QPixmap)
|
||||
def on_frame_ready(self, pixmap: QPixmap):
|
||||
"""Displays a new frame from the camera."""
|
||||
"""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."""
|
||||
"""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):
|
||||
"""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)
|
||||
|
||||
"""Transitions to StreamingState when the camera starts."""
|
||||
self.transition_to(StreamingState())
|
||||
|
||||
@Slot()
|
||||
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)
|
||||
"""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):
|
||||
@@ -150,8 +162,6 @@ class MainController:
|
||||
self.on_camera_error("No cameras detected.")
|
||||
return
|
||||
|
||||
# For now, just start the first detected camera.
|
||||
# In a real app, you'd get the selected camera ID from the UI.
|
||||
camera_id = detected_cameras[0]['id']
|
||||
self.camera_manager.start_camera(camera_id)
|
||||
|
||||
@@ -161,7 +171,17 @@ class MainController:
|
||||
|
||||
def take_photo(self):
|
||||
"""Takes a photo with the active camera."""
|
||||
print("Taking photo...") # Placeholder
|
||||
# This needs to be implemented in CameraManager and called here.
|
||||
# e.g., self.camera_manager.take_photo()
|
||||
self.split_view.toggle_live_view() # This seems like a UI toggle, maybe rename?
|
||||
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)
|
||||
@@ -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
|
||||
57
core/camera/states.py
Normal file
57
core/camera/states.py
Normal 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()
|
||||
@@ -154,3 +154,6 @@ class DatabaseManager:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("DELETE FROM media WHERE color_id = ?", (color_id,))
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
db_manager = DatabaseManager()
|
||||
|
||||
@@ -1,13 +1,40 @@
|
||||
from pathlib import Path
|
||||
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")
|
||||
DEFAULT_ICON = Path("media/default_icon.jpg")
|
||||
|
||||
class MediaRepository:
|
||||
def __init__(self, db: DatabaseManager):
|
||||
self.db = db
|
||||
def __init__(self):
|
||||
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):
|
||||
disk_colors = {d.name for d in MEDIA_DIR.iterdir() if d.is_dir()}
|
||||
|
||||
72
review.md
Normal file
72
review.md
Normal 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
13
settings.py
Normal 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"
|
||||
@@ -2,6 +2,8 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout ,QVBoxLayout, QPushButton, QG
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import Qt, QSize, Signal
|
||||
|
||||
from settings import ICONS_DIR
|
||||
|
||||
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"]
|
||||
|
||||
@@ -19,13 +21,13 @@ class LabeledSpinSelector(QWidget):
|
||||
self.title_label = QLabel(title)
|
||||
|
||||
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.setIconSize(QSize(icon_size, icon_size))
|
||||
decrement_button.clicked.connect(self._decrement)
|
||||
|
||||
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.setIconSize(QSize(icon_size, icon_size))
|
||||
increment_button.clicked.connect(self._increment)
|
||||
|
||||
@@ -3,6 +3,7 @@ from PySide6.QtGui import QEnterEvent, QPixmap, QWheelEvent, QPainter, QBrush, Q
|
||||
from PySide6.QtCore import Qt, QSize, Signal, QEvent
|
||||
import sys
|
||||
from ui.widgets.placeholder_widget import PlaceholderWidget
|
||||
from settings import ICONS_DIR
|
||||
|
||||
|
||||
class ZoomableImageView(QGraphicsView):
|
||||
@@ -179,12 +180,12 @@ class ViewWithOverlay(QWidget):
|
||||
|
||||
def _create_top_right_buttons(self):
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -200,12 +201,12 @@ class ViewWithOverlay(QWidget):
|
||||
|
||||
def _create_top_left_buttons(self):
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -232,12 +233,11 @@ class ViewWithOverlay(QWidget):
|
||||
|
||||
def toggle_orientation(self, orientation):
|
||||
if orientation == Qt.Orientation.Vertical:
|
||||
self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
|
||||
self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
|
||||
self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-vertical-svgrepo-com.svg")))
|
||||
self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "horizontal-stacks-svgrepo-com.svg")))
|
||||
else:
|
||||
self.flip_btn.setIcon(QIcon("ui/icons/flip-horizontal-svgrepo-com.svg"))
|
||||
self.orient_btn.setIcon(QIcon("ui/icons/vertical-stacks-svgrepo-com.svg"))
|
||||
|
||||
self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-horizontal-svgrepo-com.svg")))
|
||||
self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "vertical-stacks-svgrepo-com.svg")))
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
if self.live:
|
||||
self.camera_btn.show()
|
||||
@@ -286,7 +286,7 @@ class SplitView(QSplitter):
|
||||
# self.widget_live.set_image(pixmap)
|
||||
|
||||
self.ref_image_rotate = 0
|
||||
self.ref_pixmap = None
|
||||
self.original_ref_pixmap = None
|
||||
|
||||
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
|
||||
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
|
||||
@@ -324,10 +324,9 @@ class SplitView(QSplitter):
|
||||
|
||||
def set_reference_image(self, path_image: str):
|
||||
"""Ustawienie obrazu referencyjnego"""
|
||||
self.ref_pixmap = QPixmap(path_image)
|
||||
if self.ref_image_rotate != 0:
|
||||
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(self.ref_image_rotate))
|
||||
self.widget_ref.set_image(self.ref_pixmap)
|
||||
self.original_ref_pixmap = QPixmap(path_image)
|
||||
self.ref_image_rotate = 0
|
||||
self.widget_ref.set_image(self.original_ref_pixmap)
|
||||
|
||||
def toggle_live_view(self):
|
||||
"""Przełączanie widoku na żywo"""
|
||||
@@ -337,15 +336,17 @@ class SplitView(QSplitter):
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
|
||||
def rotate_left(self):
|
||||
if not self.ref_pixmap:
|
||||
if not self.original_ref_pixmap:
|
||||
return
|
||||
self.ref_image_rotate = (self.ref_image_rotate - 90) % 360
|
||||
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(-90))
|
||||
self.widget_ref.set_image(self.ref_pixmap)
|
||||
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.ref_pixmap:
|
||||
if not self.original_ref_pixmap:
|
||||
return
|
||||
self.ref_image_rotate = (self.ref_image_rotate + 90) % 360
|
||||
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(90))
|
||||
self.widget_ref.set_image(self.ref_pixmap)
|
||||
transform = QTransform().rotate(self.ref_image_rotate)
|
||||
rotated_pixmap = self.original_ref_pixmap.transformed(transform)
|
||||
self.widget_ref.set_image(rotated_pixmap)
|
||||
Reference in New Issue
Block a user