Compare commits
26 Commits
71a55843c1
...
review
| Author | SHA1 | Date | |
|---|---|---|---|
| 02edb186bb | |||
| 47c1e6040a | |||
| c8d9029df7 | |||
| 96c2495a8b | |||
| 03ab345e17 | |||
| d62b367b47 | |||
| bbdb7d3459 | |||
| fa84a29ab5 | |||
| 2cc496a2b2 | |||
| 0bc6c01e7e | |||
| 4d00f83083 | |||
| 2c1233f304 | |||
| feebc5153a | |||
| 49a1405340 | |||
| c6345c569d | |||
| 73b51c696e | |||
| 511e668cb3 | |||
| ce1b864b17 | |||
| 46734208e7 | |||
| bbdc2af605 | |||
| 2a5f570e5e | |||
| d63d616675 | |||
| eb72b25fdb | |||
| 86b9cc70a6 | |||
| 5b345e6641 | |||
| cc37d7054c |
@@ -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()
|
|
||||||
@@ -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.db.connect()
|
|
||||||
self.media_repo = MediaRepository(self.db)
|
|
||||||
self.media_repo.sync_media()
|
|
||||||
|
|
||||||
# camera = GPhotoCamera()
|
|
||||||
# self.manager = CameraController(camera)
|
|
||||||
manager = CameraManager()
|
|
||||||
manager.detect_gphoto()
|
|
||||||
manager.detect_opencv()
|
|
||||||
|
|
||||||
# self.camera_controller = CameraController()
|
|
||||||
|
|
||||||
self.view = view
|
self.view = view
|
||||||
|
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.color_list: ColorListWidget = view.color_list_widget
|
||||||
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
||||||
self.split_view: SplitView = view.preview_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.photo_button: QPushButton = view.photo_button
|
||||||
self.photo_button.clicked.connect(self.take_photo)
|
|
||||||
|
|
||||||
self.record_button: QPushButton = view.record_button
|
self.record_button: QPushButton = view.record_button
|
||||||
# self.record_button.clicked.connect(self.fun_test)
|
|
||||||
|
|
||||||
|
self._connect_signals()
|
||||||
|
|
||||||
|
self.db.connect()
|
||||||
|
self.media_repo.sync_media()
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
# Database and media signals
|
||||||
self.color_list.colorSelected.connect(self.on_color_selected)
|
self.color_list.colorSelected.connect(self.on_color_selected)
|
||||||
self.color_list.editColor.connect(self.on_edit_color)
|
self.color_list.editColor.connect(self.on_edit_color)
|
||||||
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected)
|
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected)
|
||||||
|
|
||||||
# self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text)
|
# Camera signals
|
||||||
# self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text)
|
self.camera_manager.cameras_detected.connect(self.on_cameras_detected)
|
||||||
# self.camera_controller.frameReady.connect(self.split_view.set_live_image)
|
self.camera_manager.frame_ready.connect(self.on_frame_ready)
|
||||||
# self.manager.frame_ready.connect(self.split_view.set_live_image)
|
self.camera_manager.error_occurred.connect(self.on_camera_error)
|
||||||
# self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start)
|
self.camera_manager.camera_started.connect(self.on_camera_started)
|
||||||
self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview)
|
self.camera_manager.camera_stopped.connect(self.on_camera_stopped)
|
||||||
|
|
||||||
|
# UI control signals
|
||||||
def start_camera(self):
|
self.photo_button.clicked.connect(self.take_photo)
|
||||||
pass
|
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)
|
||||||
|
|
||||||
def load_colors(self) -> None:
|
def load_colors(self) -> None:
|
||||||
|
"""Loads colors from the database and populates the list."""
|
||||||
colors = self.db.get_all_colors()
|
colors = self.db.get_all_colors()
|
||||||
print("Loaded colors:", colors)
|
|
||||||
self.color_list.set_colors(colors)
|
self.color_list.set_colors(colors)
|
||||||
|
|
||||||
|
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):
|
def on_color_selected(self, color_name: str):
|
||||||
print(f"Wybrano kolor: {color_name}")
|
self.selected_color_name = color_name
|
||||||
|
self.photo_button.setEnabled(True)
|
||||||
|
|
||||||
color_id = self.db.get_color_id(color_name)
|
color_id = self.db.get_color_id(color_name)
|
||||||
if color_id is not None:
|
if color_id is not None:
|
||||||
media_items = self.db.get_media_for_color(color_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()
|
self.thumbnail_list.list_widget.clear()
|
||||||
for media in media_items:
|
for media in media_items:
|
||||||
if media['file_type'] == 'photo':
|
if media['file_type'] == 'photo':
|
||||||
file_name = Path(media['media_path']).name
|
file_name = Path(media['media_path']).name
|
||||||
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
|
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
|
||||||
else:
|
|
||||||
print(f"Nie znaleziono koloru o nazwie: {color_name}")
|
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
def on_edit_color(self, color_name: str):
|
def on_edit_color(self, color_name: str):
|
||||||
print(f"Edycja koloru: {color_name}")
|
print(f"Edycja koloru: {color_name}") # Placeholder
|
||||||
|
|
||||||
|
@Slot(int)
|
||||||
def on_thumbnail_selected(self, media_id: int):
|
def on_thumbnail_selected(self, media_id: int):
|
||||||
media = self.db.get_media(media_id)
|
media = self.db.get_media(media_id)
|
||||||
if media:
|
if media:
|
||||||
print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}")
|
|
||||||
self.split_view.set_reference_image(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):
|
# --- Slots for CameraManager ---
|
||||||
print("Robienie zdjęcia...")
|
|
||||||
self.split_view.toglle_live_view()
|
@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):
|
def start_liveview(self):
|
||||||
pass
|
"""Starts the camera feed."""
|
||||||
# self.manager.start_camera()
|
detected_cameras = self.camera_manager.get_detected_cameras()
|
||||||
# self.manager.start_stream()
|
if not detected_cameras:
|
||||||
|
self.on_camera_error("No cameras detected.")
|
||||||
|
return
|
||||||
|
|
||||||
def shutdown(self):
|
camera_id = detected_cameras[0]['id']
|
||||||
pass
|
self.camera_manager.start_camera(camera_id)
|
||||||
# self.manager.stop()
|
|
||||||
|
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)
|
||||||
@@ -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
|
|
||||||
@@ -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):
|
||||||
|
if self.is_streaming:
|
||||||
self.stop_stream()
|
self.stop_stream()
|
||||||
|
if self.is_connected:
|
||||||
self.stop_camera()
|
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:
|
||||||
|
with QMutexLocker(self._camera_mutex):
|
||||||
if self.camera is None or self.is_connected:
|
if self.camera is None or self.is_connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.camera.connect():
|
if self.camera.connect():
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
|
self.camera_ready.emit(True)
|
||||||
else:
|
else:
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
self.camera_ready.emit(False)
|
||||||
self.error_occurred.emit(self.camera.get_error_msg())
|
self.error_occurred.emit(self.camera.get_error_msg())
|
||||||
|
|
||||||
|
@Slot()
|
||||||
def stop_camera(self) -> None:
|
def stop_camera(self) -> None:
|
||||||
|
with QMutexLocker(self._camera_mutex):
|
||||||
if self.is_streaming:
|
if self.is_streaming:
|
||||||
self.stop_stream()
|
self.stop_stream()
|
||||||
|
|
||||||
if self.camera is not None:
|
if self.camera is not None and self.is_connected:
|
||||||
self.camera.disconnect()
|
self.camera.disconnect()
|
||||||
|
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|
||||||
|
@Slot()
|
||||||
def start_stream(self):
|
def start_stream(self):
|
||||||
if not self.is_connected:
|
if not self.is_connected or self.is_streaming or self.timer is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.is_streaming:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.timer:
|
|
||||||
self.is_streaming = True
|
self.is_streaming = True
|
||||||
# self.timer.start()
|
self.timer.setInterval(int(1000 / self.fps))
|
||||||
self._enable_timer.emit(True)
|
self.timer.start()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
def stop_stream(self) -> None:
|
def stop_stream(self) -> None:
|
||||||
if self.is_streaming:
|
if self.is_streaming and self.timer is not None:
|
||||||
self.is_streaming = False
|
self.is_streaming = False
|
||||||
if self.timer:
|
self.timer.stop()
|
||||||
# self.timer.stop()
|
|
||||||
self._enable_timer.emit(False)
|
|
||||||
|
|
||||||
|
@Slot()
|
||||||
def _update_frame(self) -> None:
|
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):
|
with QMutexLocker(self._camera_mutex):
|
||||||
if self.camera is None or not self.is_connected:
|
if self.camera is None or not self.is_connected or not self.is_streaming:
|
||||||
return
|
|
||||||
|
|
||||||
if 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:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._rotation_index == 1:
|
||||||
|
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
||||||
|
elif self._rotation_index == 2:
|
||||||
|
frame = cv2.rotate(frame, cv2.ROTATE_180)
|
||||||
|
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)
|
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):
|
@Slot()
|
||||||
if not self.timer:
|
def rotate_left(self):
|
||||||
return
|
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()
|
||||||
|
|
||||||
if enable:
|
|
||||||
self.timer.setInterval(int(1000 / self.fps))
|
|
||||||
self.timer.start()
|
|
||||||
else:
|
|
||||||
self.timer.stop()
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .base_camera import BaseCamera
|
|||||||
try:
|
try:
|
||||||
import gphoto2 as gp # type: ignore
|
import gphoto2 as gp # type: ignore
|
||||||
except:
|
except:
|
||||||
import controllers.mock_gphoto as gp
|
import core.camera.mock_gphoto as gp
|
||||||
|
|
||||||
camera_widget_types = {
|
camera_widget_types = {
|
||||||
gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore
|
gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore
|
||||||
@@ -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
|
||||||
|
|||||||
204
core/camera/mock_gphoto.py
Normal file
204
core/camera/mock_gphoto.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
"""
|
||||||
|
Mock gphoto2 module for Windows testing and development.
|
||||||
|
Simulates gphoto2 API behavior to allow the app to run without a camera.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
# --- Constants (simulate gphoto2 enums) ---
|
||||||
|
|
||||||
|
GP_WIDGET_WINDOW = 0
|
||||||
|
GP_WIDGET_SECTION = 1
|
||||||
|
GP_WIDGET_TEXT = 2
|
||||||
|
GP_WIDGET_RANGE = 3
|
||||||
|
GP_WIDGET_TOGGLE = 4
|
||||||
|
GP_WIDGET_RADIO = 5
|
||||||
|
GP_WIDGET_MENU = 6
|
||||||
|
GP_WIDGET_BUTTON = 7
|
||||||
|
GP_WIDGET_DATE = 8
|
||||||
|
|
||||||
|
GP_OPERATION_NONE = 0x00
|
||||||
|
GP_OPERATION_CAPTURE_IMAGE = 0x01
|
||||||
|
GP_OPERATION_CAPTURE_VIDEO = 0x02
|
||||||
|
GP_OPERATION_CAPTURE_AUDIO = 0x04
|
||||||
|
GP_OPERATION_CAPTURE_PREVIEW = 0x08
|
||||||
|
GP_OPERATION_CONFIG = 0x10
|
||||||
|
GP_OPERATION_TRIGGER_CAPTURE = 0x20
|
||||||
|
|
||||||
|
|
||||||
|
# --- Error class ---
|
||||||
|
class GPhoto2Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mock camera configuration widget ---
|
||||||
|
|
||||||
|
class MockWidget:
|
||||||
|
def __init__(self, name, label, wtype, value=None, choices=None):
|
||||||
|
self._name = name
|
||||||
|
self._label = label
|
||||||
|
self._type = wtype
|
||||||
|
self._value = value
|
||||||
|
self._choices = choices or []
|
||||||
|
self._children = []
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def get_label(self):
|
||||||
|
return self._label
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return self._type
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
if self._choices and value not in self._choices:
|
||||||
|
raise GPhoto2Error(f"Invalid value '{value}' for widget '{self._name}'")
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
def get_choices(self):
|
||||||
|
return self._choices
|
||||||
|
|
||||||
|
def count_children(self):
|
||||||
|
return len(self._children)
|
||||||
|
|
||||||
|
def get_child(self, i):
|
||||||
|
return self._children[i]
|
||||||
|
|
||||||
|
def add_child(self, child):
|
||||||
|
self._children.append(child)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mock classes for detection / abilities ---
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MockCameraInfo:
|
||||||
|
name: str
|
||||||
|
port: str
|
||||||
|
|
||||||
|
|
||||||
|
class MockCameraList:
|
||||||
|
def __init__(self):
|
||||||
|
self._cameras = [
|
||||||
|
MockCameraInfo("Mock Camera 1", "usb:001,002"),
|
||||||
|
MockCameraInfo("Mock Camera 2", "usb:001,003")
|
||||||
|
]
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return len(self._cameras)
|
||||||
|
|
||||||
|
def get_name(self, i):
|
||||||
|
return self._cameras[i].name
|
||||||
|
|
||||||
|
def get_value(self, i):
|
||||||
|
return self._cameras[i].port
|
||||||
|
|
||||||
|
|
||||||
|
class CameraAbilities:
|
||||||
|
def __init__(self, operations):
|
||||||
|
self.operations = operations
|
||||||
|
|
||||||
|
|
||||||
|
class CameraAbilitiesList:
|
||||||
|
def load(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lookup_model(self, name):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_abilities(self, index):
|
||||||
|
return CameraAbilities(
|
||||||
|
GP_OPERATION_CAPTURE_IMAGE
|
||||||
|
| GP_OPERATION_CAPTURE_PREVIEW
|
||||||
|
| GP_OPERATION_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PortInfoList:
|
||||||
|
def load(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lookup_path(self, path):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return f"MockPortInfo({index})"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mock Camera class ---
|
||||||
|
|
||||||
|
class Camera:
|
||||||
|
def __init__(self):
|
||||||
|
self.initialized = False
|
||||||
|
self.port_info = None
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def exit(self):
|
||||||
|
self.initialized = False
|
||||||
|
|
||||||
|
def set_port_info(self, info):
|
||||||
|
self.port_info = info
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
# Simulate config tree
|
||||||
|
root = MockWidget("root", "Root", GP_WIDGET_WINDOW)
|
||||||
|
|
||||||
|
iso = MockWidget("iso", "ISO", GP_WIDGET_MENU, "100", ["100", "200", "400", "800"])
|
||||||
|
shutter = MockWidget("shutter", "Shutter Speed", GP_WIDGET_MENU, "1/60", ["1/30", "1/60", "1/125"])
|
||||||
|
wb = MockWidget("whitebalance", "White Balance", GP_WIDGET_RADIO, "Auto", ["Auto", "Daylight", "Tungsten"])
|
||||||
|
|
||||||
|
root.add_child(iso)
|
||||||
|
root.add_child(shutter)
|
||||||
|
root.add_child(wb)
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
def set_single_config(self, name, widget):
|
||||||
|
# Simulate saving a setting
|
||||||
|
print(f"[mock_gphoto] Setting '{name}' = '{widget.get_value()}'")
|
||||||
|
|
||||||
|
def capture_preview(self):
|
||||||
|
# Generate a fake image (OpenCV compatible)
|
||||||
|
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||||
|
cv2.putText(frame, "Mock Preview", (150, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||||
|
_, buf = cv2.imencode(".jpg", frame)
|
||||||
|
|
||||||
|
class MockFile:
|
||||||
|
def get_data_and_size(self_inner):
|
||||||
|
return buf.tobytes()
|
||||||
|
|
||||||
|
return MockFile()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mock detection functions ---
|
||||||
|
|
||||||
|
def gp_camera_autodetect():
|
||||||
|
return MockCameraList()
|
||||||
|
|
||||||
|
|
||||||
|
def check_result(value):
|
||||||
|
# gphoto2.check_result usually raises error if return < 0
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# --- API aliases to match gphoto2 ---
|
||||||
|
|
||||||
|
GP_ERROR = -1
|
||||||
|
|
||||||
|
gp_camera_autodetect = gp_camera_autodetect
|
||||||
|
check_result = check_result
|
||||||
|
Camera = Camera
|
||||||
|
CameraAbilitiesList = CameraAbilitiesList
|
||||||
|
PortInfoList = PortInfoList
|
||||||
|
GPhoto2Error = GPhoto2Error
|
||||||
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 = 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()
|
||||||
|
|||||||
@@ -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
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"
|
||||||
64
ui/icons/camera_hdmi.svg
Normal file
64
ui/icons/camera_hdmi.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="800"
|
||||||
|
height="800"
|
||||||
|
viewBox="0 0 800 800"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||||
|
sodipodi:docname="camera_hdmi.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="1.0805556"
|
||||||
|
inkscape:cx="-35.167095"
|
||||||
|
inkscape:cy="455.78406"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<rect
|
||||||
|
x="88.843188"
|
||||||
|
y="123.08483"
|
||||||
|
width="808.84319"
|
||||||
|
height="255.42416"
|
||||||
|
id="rect1" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Warstwa 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
id="text1"
|
||||||
|
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="-24.242525"
|
||||||
|
y="518.46466"
|
||||||
|
id="text4"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan4"
|
||||||
|
x="-24.242525"
|
||||||
|
y="518.46466"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
64
ui/icons/camera_usb.svg
Normal file
64
ui/icons/camera_usb.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="800"
|
||||||
|
height="800"
|
||||||
|
viewBox="0 0 800 800"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||||
|
sodipodi:docname="camera_usb.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="1.0805556"
|
||||||
|
inkscape:cx="-35.167095"
|
||||||
|
inkscape:cy="455.78406"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<rect
|
||||||
|
x="88.843188"
|
||||||
|
y="123.08483"
|
||||||
|
width="808.84319"
|
||||||
|
height="255.42416"
|
||||||
|
id="rect1" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Warstwa 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
id="text1"
|
||||||
|
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="46.868595"
|
||||||
|
y="518.38385"
|
||||||
|
id="text4"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan4"
|
||||||
|
x="46.868595"
|
||||||
|
y="518.38385"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
75
ui/icons/camera_usb_hdmi.svg
Normal file
75
ui/icons/camera_usb_hdmi.svg
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="800"
|
||||||
|
height="800"
|
||||||
|
viewBox="0 0 800 800"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||||
|
sodipodi:docname="camera_usb_hdmi.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="1.0805556"
|
||||||
|
inkscape:cx="-35.167095"
|
||||||
|
inkscape:cy="455.78406"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<rect
|
||||||
|
x="88.843188"
|
||||||
|
y="123.08483"
|
||||||
|
width="808.84319"
|
||||||
|
height="255.42416"
|
||||||
|
id="rect1" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Warstwa 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
id="text1"
|
||||||
|
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="-24.243189"
|
||||||
|
y="721.06696"
|
||||||
|
id="text3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3"
|
||||||
|
x="-24.243189"
|
||||||
|
y="721.06696"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="46.868595"
|
||||||
|
y="298.13632"
|
||||||
|
id="text4"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan4"
|
||||||
|
x="46.868595"
|
||||||
|
y="298.13632"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
2
ui/icons/error-16-svgrepo-com.svg
Normal file
2
ui/icons/error-16-svgrepo-com.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.493 0.015 C 7.442 0.021,7.268 0.039,7.107 0.055 C 5.234 0.242,3.347 1.208,2.071 2.634 C 0.660 4.211,-0.057 6.168,0.009 8.253 C 0.124 11.854,2.599 14.903,6.110 15.771 C 8.169 16.280,10.433 15.917,12.227 14.791 C 14.017 13.666,15.270 11.933,15.771 9.887 C 15.943 9.186,15.983 8.829,15.983 8.000 C 15.983 7.171,15.943 6.814,15.771 6.113 C 14.979 2.878,12.315 0.498,9.000 0.064 C 8.716 0.027,7.683 -0.006,7.493 0.015 M8.853 1.563 C 9.967 1.707,11.010 2.136,11.944 2.834 C 12.273 3.080,12.920 3.727,13.166 4.056 C 13.727 4.807,14.142 5.690,14.330 6.535 C 14.544 7.500,14.544 8.500,14.330 9.465 C 13.916 11.326,12.605 12.978,10.867 13.828 C 10.239 14.135,9.591 14.336,8.880 14.444 C 8.456 14.509,7.544 14.509,7.120 14.444 C 5.172 14.148,3.528 13.085,2.493 11.451 C 2.279 11.114,1.999 10.526,1.859 10.119 C 1.618 9.422,1.514 8.781,1.514 8.000 C 1.514 6.961,1.715 6.075,2.160 5.160 C 2.500 4.462,2.846 3.980,3.413 3.413 C 3.980 2.846,4.462 2.500,5.160 2.160 C 6.313 1.599,7.567 1.397,8.853 1.563 M7.706 4.290 C 7.482 4.363,7.355 4.491,7.293 4.705 C 7.257 4.827,7.253 5.106,7.259 6.816 C 7.267 8.786,7.267 8.787,7.325 8.896 C 7.398 9.033,7.538 9.157,7.671 9.204 C 7.803 9.250,8.197 9.250,8.329 9.204 C 8.462 9.157,8.602 9.033,8.675 8.896 C 8.733 8.787,8.733 8.786,8.741 6.816 C 8.749 4.664,8.749 4.662,8.596 4.481 C 8.472 4.333,8.339 4.284,8.040 4.276 C 7.893 4.272,7.743 4.278,7.706 4.290 M7.786 10.530 C 7.597 10.592,7.410 10.753,7.319 10.932 C 7.249 11.072,7.237 11.325,7.294 11.495 C 7.388 11.780,7.697 12.000,8.000 12.000 C 8.303 12.000,8.612 11.780,8.706 11.495 C 8.763 11.325,8.751 11.072,8.681 10.932 C 8.616 10.804,8.460 10.646,8.333 10.580 C 8.217 10.520,7.904 10.491,7.786 10.530 " stroke="none" fill-rule="evenodd" fill="#000000"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
17
ui/icons/settings-svgrepo-com.svg
Normal file
17
ui/icons/settings-svgrepo-com.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||||
|
|
||||||
|
<title>settings</title>
|
||||||
|
<desc>Created with Sketch Beta.</desc>
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||||
|
<g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-101.000000, -360.000000)" fill="#000000">
|
||||||
|
<path d="M128.52,381.134 L127.528,382.866 C127.254,383.345 126.648,383.508 126.173,383.232 L123.418,381.628 C122.02,383.219 120.129,384.359 117.983,384.799 L117.983,387 C117.983,387.553 117.54,388 116.992,388 L115.008,388 C114.46,388 114.017,387.553 114.017,387 L114.017,384.799 C111.871,384.359 109.98,383.219 108.582,381.628 L105.827,383.232 C105.352,383.508 104.746,383.345 104.472,382.866 L103.48,381.134 C103.206,380.656 103.369,380.044 103.843,379.769 L106.609,378.157 C106.28,377.163 106.083,376.106 106.083,375 C106.083,373.894 106.28,372.838 106.609,371.843 L103.843,370.232 C103.369,369.956 103.206,369.345 103.48,368.866 L104.472,367.134 C104.746,366.656 105.352,366.492 105.827,366.768 L108.582,368.372 C109.98,366.781 111.871,365.641 114.017,365.201 L114.017,363 C114.017,362.447 114.46,362 115.008,362 L116.992,362 C117.54,362 117.983,362.447 117.983,363 L117.983,365.201 C120.129,365.641 122.02,366.781 123.418,368.372 L126.173,366.768 C126.648,366.492 127.254,366.656 127.528,367.134 L128.52,368.866 C128.794,369.345 128.631,369.956 128.157,370.232 L125.391,371.843 C125.72,372.838 125.917,373.894 125.917,375 C125.917,376.106 125.72,377.163 125.391,378.157 L128.157,379.769 C128.631,380.044 128.794,380.656 128.52,381.134 L128.52,381.134 Z M130.008,378.536 L127.685,377.184 C127.815,376.474 127.901,375.749 127.901,375 C127.901,374.252 127.815,373.526 127.685,372.816 L130.008,371.464 C130.957,370.912 131.281,369.688 130.733,368.732 L128.75,365.268 C128.203,364.312 126.989,363.983 126.041,364.536 L123.694,365.901 C122.598,364.961 121.352,364.192 119.967,363.697 L119.967,362 C119.967,360.896 119.079,360 117.983,360 L114.017,360 C112.921,360 112.033,360.896 112.033,362 L112.033,363.697 C110.648,364.192 109.402,364.961 108.306,365.901 L105.959,364.536 C105.011,363.983 103.797,364.312 103.25,365.268 L101.267,368.732 C100.719,369.688 101.044,370.912 101.992,371.464 L104.315,372.816 C104.185,373.526 104.099,374.252 104.099,375 C104.099,375.749 104.185,376.474 104.315,377.184 L101.992,378.536 C101.044,379.088 100.719,380.312 101.267,381.268 L103.25,384.732 C103.797,385.688 105.011,386.017 105.959,385.464 L108.306,384.099 C109.402,385.039 110.648,385.809 112.033,386.303 L112.033,388 C112.033,389.104 112.921,390 114.017,390 L117.983,390 C119.079,390 119.967,389.104 119.967,388 L119.967,386.303 C121.352,385.809 122.598,385.039 123.694,384.099 L126.041,385.464 C126.989,386.017 128.203,385.688 128.75,384.732 L130.733,381.268 C131.281,380.312 130.957,379.088 130.008,378.536 L130.008,378.536 Z M116,378 C114.357,378 113.025,376.657 113.025,375 C113.025,373.344 114.357,372 116,372 C117.643,372 118.975,373.344 118.975,375 C118.975,376.657 117.643,378 116,378 L116,378 Z M116,370 C113.261,370 111.042,372.238 111.042,375 C111.042,377.762 113.261,380 116,380 C118.739,380 120.959,377.762 120.959,375 C120.959,372.238 118.739,370 116,370 L116,370 Z" id="settings" sketch:type="MSShapeGroup">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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)
|
||||||
|
|||||||
@@ -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,31 +105,61 @@ 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()
|
||||||
|
cameraSettings = Signal()
|
||||||
toggleOrientation = Signal()
|
toggleOrientation = Signal()
|
||||||
swapViews = Signal()
|
swapViews = Signal()
|
||||||
rotateCW = Signal()
|
rotateCW = Signal()
|
||||||
rotateCCW = Signal()
|
rotateCCW = Signal()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, live: bool = False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.live = live
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.viewer = ZoomableImageView()
|
self.viewer = ZoomableImageView()
|
||||||
layout.addWidget(self.viewer)
|
layout.addWidget(self.viewer)
|
||||||
|
|
||||||
|
self._create_top_right_buttons()
|
||||||
|
if self.live:
|
||||||
|
self._create_top_left_buttons()
|
||||||
|
|
||||||
|
self.resize(self.size())
|
||||||
|
|
||||||
|
# self.cw_btn.raise_()
|
||||||
|
# self.ccw_btn.raise_()
|
||||||
|
# self.flip_btn.raise_()
|
||||||
|
# self.orient_btn.raise_()
|
||||||
|
|
||||||
|
self.toggle_orientation(Qt.Orientation.Vertical)
|
||||||
|
|
||||||
|
def _create_tool_button(self, callback, icon_path: str | None):
|
||||||
icon_size = QSize(32, 32)
|
icon_size = QSize(32, 32)
|
||||||
btn_size = (48, 48)
|
btn_size = (48, 48)
|
||||||
btn_style = """
|
btn_style = """
|
||||||
@@ -133,50 +168,47 @@ class ViewWithOverlay(QWidget):
|
|||||||
border: 2px solid #1f1f1f;
|
border: 2px solid #1f1f1f;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
btn = QToolButton(self)
|
||||||
|
if icon_path:
|
||||||
|
btn.setIcon(QIcon(icon_path))
|
||||||
|
btn.setIconSize(icon_size)
|
||||||
|
btn.setStyleSheet(btn_style)
|
||||||
|
btn.setFixedSize(*btn_size)
|
||||||
|
btn.clicked.connect(callback)
|
||||||
|
|
||||||
self.cw_btn = QToolButton(self)
|
return btn
|
||||||
self.cw_btn.setIcon(QIcon("ui/icons/rotate-cw-svgrepo-com.svg"))
|
|
||||||
self.cw_btn.setIconSize(icon_size)
|
|
||||||
self.cw_btn.setStyleSheet(btn_style)
|
|
||||||
self.cw_btn.setFixedSize(*btn_size)
|
|
||||||
move_x = self.cw_btn.width() + 10
|
|
||||||
self.cw_btn.move(self.width() - move_x, 10)
|
|
||||||
self.cw_btn.clicked.connect(self.rotateCW)
|
|
||||||
|
|
||||||
self.ccw_btn = QToolButton(self)
|
def _create_top_right_buttons(self):
|
||||||
self.ccw_btn.setIcon(QIcon("ui/icons/rotate-ccw-svgrepo-com.svg"))
|
self.cw_btn = self._create_tool_button(
|
||||||
self.ccw_btn.setIconSize(icon_size)
|
icon_path=str(ICONS_DIR / "rotate-cw-svgrepo-com.svg"),
|
||||||
self.ccw_btn.setStyleSheet(btn_style)
|
callback=self.rotateCW,
|
||||||
self.ccw_btn.setFixedSize(*btn_size)
|
)
|
||||||
move_x += self.ccw_btn.width() + 10
|
|
||||||
self.ccw_btn.move(self.width() - move_x, 10)
|
|
||||||
self.ccw_btn.clicked.connect(self.rotateCCW)
|
|
||||||
|
|
||||||
self.flip_btn = QToolButton(self)
|
self.ccw_btn = self._create_tool_button(
|
||||||
# self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
|
icon_path=str(ICONS_DIR / "rotate-ccw-svgrepo-com.svg"),
|
||||||
self.flip_btn.setIconSize(icon_size)
|
callback=self.rotateCCW,
|
||||||
self.flip_btn.setStyleSheet(btn_style)
|
)
|
||||||
self.flip_btn.setFixedSize(*btn_size)
|
|
||||||
move_x += self.flip_btn.width() + 10
|
|
||||||
self.flip_btn.move(self.width() - move_x, 10)
|
|
||||||
self.flip_btn.clicked.connect(self.swapViews)
|
|
||||||
|
|
||||||
self.orient_btn = QToolButton(self)
|
self.flip_btn = self._create_tool_button(
|
||||||
# self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
|
icon_path=None,
|
||||||
self.orient_btn.setIconSize(icon_size)
|
callback=self.swapViews,
|
||||||
self.orient_btn.setStyleSheet(btn_style)
|
)
|
||||||
self.orient_btn.setFixedSize(*btn_size)
|
|
||||||
move_x += self.orient_btn.width() + 10
|
|
||||||
self.orient_btn.move(self.width() - move_x, 10)
|
|
||||||
self.orient_btn.clicked.connect(self.toggleOrientation)
|
|
||||||
|
|
||||||
|
self.orient_btn = self._create_tool_button(
|
||||||
|
icon_path=None,
|
||||||
|
callback=self.toggleOrientation,
|
||||||
|
)
|
||||||
|
|
||||||
self.cw_btn.raise_()
|
def _create_top_left_buttons(self):
|
||||||
self.ccw_btn.raise_()
|
self.camera_btn = self._create_tool_button(
|
||||||
self.flip_btn.raise_()
|
icon_path=str(ICONS_DIR / "settings-svgrepo-com.svg"),
|
||||||
self.orient_btn.raise_()
|
callback=self.cameraConnection
|
||||||
|
)
|
||||||
|
|
||||||
self.toggle_orientation(Qt.Orientation.Vertical)
|
self.settings_btn = self._create_tool_button(
|
||||||
|
icon_path=str(ICONS_DIR / "error-16-svgrepo-com.svg"),
|
||||||
|
callback=self.cameraSettings
|
||||||
|
)
|
||||||
|
|
||||||
def set_image(self, pixmap: QPixmap):
|
def set_image(self, pixmap: QPixmap):
|
||||||
self.viewer.set_image(pixmap)
|
self.viewer.set_image(pixmap)
|
||||||
@@ -184,24 +216,33 @@ class ViewWithOverlay(QWidget):
|
|||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
|
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
|
||||||
move_x = self.cw_btn.width() + 10
|
if self.live:
|
||||||
self.cw_btn.move(self.width() - move_x, 10)
|
left_corner = 10
|
||||||
move_x += self.ccw_btn.width() + 10
|
self.camera_btn.move(left_corner, 10)
|
||||||
self.ccw_btn.move(self.width() - move_x, 10)
|
left_corner += self.camera_btn.width() + 10
|
||||||
move_x += self.flip_btn.width() + 10
|
self.settings_btn.move(left_corner, 10)
|
||||||
self.flip_btn.move(self.width() - move_x, 10)
|
|
||||||
move_x += self.orient_btn.width() + 10
|
right_corner = self.cw_btn.width() + 10
|
||||||
self.orient_btn.move(self.width() - move_x, 10)
|
self.cw_btn.move(self.width() - right_corner, 10)
|
||||||
|
right_corner += self.ccw_btn.width() + 10
|
||||||
|
self.ccw_btn.move(self.width() - right_corner, 10)
|
||||||
|
right_corner += self.flip_btn.width() + 10
|
||||||
|
self.flip_btn.move(self.width() - right_corner, 10)
|
||||||
|
right_corner += self.orient_btn.width() + 10
|
||||||
|
self.orient_btn.move(self.width() - right_corner, 10)
|
||||||
|
|
||||||
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:
|
||||||
|
self.camera_btn.show()
|
||||||
|
self.settings_btn.show()
|
||||||
|
|
||||||
self.orient_btn.show()
|
self.orient_btn.show()
|
||||||
self.flip_btn.show()
|
self.flip_btn.show()
|
||||||
self.ccw_btn.show()
|
self.ccw_btn.show()
|
||||||
@@ -209,6 +250,10 @@ class ViewWithOverlay(QWidget):
|
|||||||
return super().enterEvent(event)
|
return super().enterEvent(event)
|
||||||
|
|
||||||
def leaveEvent(self, event: QEvent) -> None:
|
def leaveEvent(self, event: QEvent) -> None:
|
||||||
|
if self.live:
|
||||||
|
self.camera_btn.hide()
|
||||||
|
self.settings_btn.hide()
|
||||||
|
|
||||||
self.orient_btn.hide()
|
self.orient_btn.hide()
|
||||||
self.flip_btn.hide()
|
self.flip_btn.hide()
|
||||||
self.ccw_btn.hide()
|
self.ccw_btn.hide()
|
||||||
@@ -223,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)
|
||||||
@@ -240,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:
|
||||||
@@ -273,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)
|
||||||
Reference in New Issue
Block a user