Compare commits
1 Commits
71a55843c1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d616fd15e |
@@ -5,96 +5,69 @@ from core.media import MediaRepository
|
||||
from ui.widgets.color_list_widget import ColorListWidget
|
||||
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
|
||||
from ui.widgets.split_view_widget import SplitView
|
||||
# from .camera_controller import CameraController
|
||||
from core.camera.camera_controller import CameraController
|
||||
from core.camera.camera_manager import CameraManager
|
||||
from .camera_controller import CameraController
|
||||
|
||||
|
||||
from core.camera.gphoto_camera import GPhotoCamera
|
||||
from core.camera.camera_controller import CameraController
|
||||
|
||||
class MainController:
|
||||
def __init__(self, view):
|
||||
self.db = DatabaseManager()
|
||||
self.db.connect()
|
||||
self.media_repo = MediaRepository(self.db)
|
||||
self.media_repo.sync_media()
|
||||
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.camera_controller = CameraController()
|
||||
self.view = view
|
||||
self.color_list: ColorListWidget = view.color_list_widget
|
||||
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
||||
self.split_view: SplitView = view.preview_widget
|
||||
|
||||
self.view = view
|
||||
self.color_list: ColorListWidget = view.color_list_widget
|
||||
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
||||
self.split_view: SplitView = view.preview_widget
|
||||
self.photo_button: QPushButton = view.photo_button
|
||||
self.photo_button.clicked.connect(self.take_photo)
|
||||
|
||||
self.photo_button: QPushButton = view.photo_button
|
||||
self.photo_button.clicked.connect(self.take_photo)
|
||||
self.color_list.colorSelected.connect(self.on_color_selected)
|
||||
self.color_list.editColor.connect(self.on_edit_color)
|
||||
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected)
|
||||
|
||||
self.record_button: QPushButton = view.record_button
|
||||
# self.record_button.clicked.connect(self.fun_test)
|
||||
self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text)
|
||||
self.camera_controller.frameReady.connect(self.split_view.set_live_image)
|
||||
self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start)
|
||||
|
||||
self.color_list.colorSelected.connect(self.on_color_selected)
|
||||
self.color_list.editColor.connect(self.on_edit_color)
|
||||
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected)
|
||||
def start_camera(self):
|
||||
pass
|
||||
|
||||
# self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text)
|
||||
# self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text)
|
||||
# self.camera_controller.frameReady.connect(self.split_view.set_live_image)
|
||||
# self.manager.frame_ready.connect(self.split_view.set_live_image)
|
||||
# self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start)
|
||||
self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview)
|
||||
def load_colors(self) -> None:
|
||||
colors = self.db.get_all_colors()
|
||||
print("Loaded colors:", colors)
|
||||
self.color_list.set_colors(colors)
|
||||
|
||||
|
||||
def start_camera(self):
|
||||
pass
|
||||
def on_color_selected(self, color_name: str):
|
||||
print(f"Wybrano kolor: {color_name}")
|
||||
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)
|
||||
print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items)
|
||||
|
||||
def load_colors(self) -> None:
|
||||
colors = self.db.get_all_colors()
|
||||
print("Loaded colors:", colors)
|
||||
self.color_list.set_colors(colors)
|
||||
self.thumbnail_list.list_widget.clear()
|
||||
for media in media_items:
|
||||
if media['file_type'] == 'photo':
|
||||
file_name = Path(media['media_path']).name
|
||||
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
|
||||
else:
|
||||
print(f"Nie znaleziono koloru o nazwie: {color_name}")
|
||||
|
||||
def on_edit_color(self, color_name: str):
|
||||
print(f"Edycja koloru: {color_name}")
|
||||
|
||||
def on_color_selected(self, color_name: str):
|
||||
print(f"Wybrano kolor: {color_name}")
|
||||
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)
|
||||
print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items)
|
||||
def on_thumbnail_selected(self, media_id: int):
|
||||
media = self.db.get_media(media_id)
|
||||
if media:
|
||||
print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}")
|
||||
self.split_view.set_reference_image(media['media_path'])
|
||||
else:
|
||||
print(f"Nie znaleziono mediów o ID: {media_id}")
|
||||
|
||||
self.thumbnail_list.list_widget.clear()
|
||||
for media in media_items:
|
||||
if media['file_type'] == 'photo':
|
||||
file_name = Path(media['media_path']).name
|
||||
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
|
||||
else:
|
||||
print(f"Nie znaleziono koloru o nazwie: {color_name}")
|
||||
|
||||
def on_edit_color(self, color_name: str):
|
||||
print(f"Edycja koloru: {color_name}")
|
||||
|
||||
def on_thumbnail_selected(self, media_id: int):
|
||||
media = self.db.get_media(media_id)
|
||||
if media:
|
||||
print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}")
|
||||
self.split_view.set_reference_image(media['media_path'])
|
||||
else:
|
||||
print(f"Nie znaleziono mediów o ID: {media_id}")
|
||||
|
||||
def take_photo(self):
|
||||
print("Robienie zdjęcia...")
|
||||
self.split_view.toglle_live_view()
|
||||
|
||||
def start_liveview(self):
|
||||
pass
|
||||
# self.manager.start_camera()
|
||||
# self.manager.start_stream()
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
# self.manager.stop()
|
||||
def take_photo(self):
|
||||
print("Robienie zdjęcia...")
|
||||
self.split_view.toglle_live_view()
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -29,65 +19,6 @@ class CameraFileMock:
|
||||
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):
|
||||
@@ -131,18 +62,3 @@ class Camera:
|
||||
|
||||
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
|
||||
18
core/base.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from
|
||||
|
||||
|
||||
class BaseImageSource(QObject):
|
||||
frameReady = Signal(QPixmap)
|
||||
errorOccurred = Signal(str)
|
||||
|
||||
def start(self): ...
|
||||
def stop(self): ...
|
||||
|
||||
|
||||
class BaseControlSource(QObject):
|
||||
errorOccurred = Signal(str)
|
||||
parameterChanged = Signal(str, object)
|
||||
|
||||
def set_parameter(self, name: str, value): ...
|
||||
def get_parameter(self, name: str): ...
|
||||
def list_parameters(self) -> dict: ...
|
||||
@@ -1,42 +1,49 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseCamera(ABC):
|
||||
def __init__(self) -> None:
|
||||
self.error_msg = None
|
||||
"""Interfejs wspólny dla wszystkich backendów kamer."""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def detect() -> dict:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def connect(self) -> bool:
|
||||
"""Nawiązuje połączenie z urządzeniem."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def disconnect(self):
|
||||
"""Zamyka połączenie z urządzeniem."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self) -> None:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def start_stream(self):
|
||||
"""Rozpocznij strumień wideo."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_frame(self):
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def stop_stream(self):
|
||||
"""Zatrzymaj strumień wideo."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_config_by_id(self, id: int) -> dict:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def get_frame(self):
|
||||
"""Pobierz jedną klatkę liveview."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_config_by_name(self, name: str) -> dict:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def capture_photo(self):
|
||||
"""Zrób zdjęcie."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set_config_by_id(self, id: int, value) -> None:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def record_video(self):
|
||||
"""Nagraj film."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set_config_by_name(self, name: str, value) -> None:
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def get_available_settings(self) -> dict:
|
||||
"""Zwraca słownik dostępnych ustawień i ich możliwych wartości."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_error_msg(self):
|
||||
return str(self.error_msg)
|
||||
@abstractmethod
|
||||
def set_setting(self, name: str, value) -> bool:
|
||||
"""Ustawia wybraną wartość dla danego ustawienia."""
|
||||
raise NotImplementedError
|
||||
@@ -1,115 +0,0 @@
|
||||
from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot, QMutex, QMutexLocker
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
import cv2
|
||||
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
|
||||
class CameraController(QThread):
|
||||
frame_ready = Signal(QPixmap)
|
||||
photo_ready = Signal(QPixmap)
|
||||
error_occurred = Signal(str)
|
||||
_enable_timer = Signal(bool)
|
||||
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.camera = None
|
||||
self.timer = None
|
||||
self.fps = 15
|
||||
self.is_streaming = False
|
||||
self.is_connected = False
|
||||
|
||||
self._camera_mutex = QMutex()
|
||||
self.start()
|
||||
|
||||
|
||||
def run(self) -> None:
|
||||
self.timer = QTimer()
|
||||
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()
|
||||
|
||||
def set_camera(self, camera: BaseCamera, fps: int = 15) -> None:
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
self.stop_stream()
|
||||
self.stop_camera()
|
||||
|
||||
self.camera = camera
|
||||
self.fps = fps
|
||||
|
||||
def start_camera(self) -> None:
|
||||
if self.camera is None or self.is_connected:
|
||||
return
|
||||
|
||||
if self.camera.connect():
|
||||
self.is_connected = True
|
||||
else:
|
||||
self.is_connected = False
|
||||
self.error_occurred.emit(self.camera.get_error_msg())
|
||||
|
||||
def stop_camera(self) -> None:
|
||||
if self.is_streaming:
|
||||
self.stop_stream()
|
||||
|
||||
if self.camera is not None:
|
||||
self.camera.disconnect()
|
||||
|
||||
self.is_connected = False
|
||||
|
||||
def start_stream(self):
|
||||
if not self.is_connected:
|
||||
return
|
||||
|
||||
if self.is_streaming:
|
||||
return
|
||||
|
||||
if self.timer:
|
||||
self.is_streaming = True
|
||||
# self.timer.start()
|
||||
self._enable_timer.emit(True)
|
||||
|
||||
def stop_stream(self) -> None:
|
||||
if self.is_streaming:
|
||||
self.is_streaming = False
|
||||
if self.timer:
|
||||
# self.timer.stop()
|
||||
self._enable_timer.emit(False)
|
||||
|
||||
def _update_frame(self) -> None:
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
if self.camera is None or not self.is_connected:
|
||||
return
|
||||
|
||||
if not self.is_streaming:
|
||||
return
|
||||
|
||||
ret, frame = self.camera.get_frame()
|
||||
|
||||
if not ret:
|
||||
self.error_occurred.emit(self.camera.get_error_msg())
|
||||
return
|
||||
|
||||
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.frame_ready.emit(pixmap)
|
||||
|
||||
def _set_timer(self, enable: bool):
|
||||
if not self.timer:
|
||||
return
|
||||
|
||||
if enable:
|
||||
self.timer.setInterval(int(1000 / self.fps))
|
||||
self.timer.start()
|
||||
else:
|
||||
self.timer.stop()
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
|
||||
from .gphoto_camera import GPhotoCamera
|
||||
from .opencv_camera import OpenCvCamera
|
||||
from .camera_controller import CameraController
|
||||
|
||||
|
||||
class CameraManager:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def detect_gphoto(self):
|
||||
camera_list = GPhotoCamera.detect()
|
||||
print(camera_list)
|
||||
return camera_list
|
||||
|
||||
def detect_opencv(self):
|
||||
camera_list = OpenCvCamera.detect()
|
||||
print(camera_list)
|
||||
return camera_list
|
||||
@@ -1,173 +1,35 @@
|
||||
from typing import Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
try:
|
||||
import gphoto2 as gp # type: ignore
|
||||
except:
|
||||
import controllers.mock_gphoto as gp
|
||||
class GPhotoBackend(BaseCamera):
|
||||
def __init__(self) -> None:
|
||||
self.camera = None
|
||||
self.context = None
|
||||
self._is_streaming = False
|
||||
|
||||
camera_widget_types = {
|
||||
gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore
|
||||
gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore
|
||||
gp.GP_WIDGET_TEXT: "GP_WIDGET_TEXT", # type: ignore
|
||||
gp.GP_WIDGET_RANGE: "GP_WIDGET_RANGE", # type: ignore
|
||||
gp.GP_WIDGET_TOGGLE: "GP_WIDGET_TOGGLE", # type: ignore
|
||||
gp.GP_WIDGET_RADIO: "GP_WIDGET_RADIO", # type: ignore
|
||||
gp.GP_WIDGET_MENU: "GP_WIDGET_MENU", # type: ignore
|
||||
gp.GP_WIDGET_BUTTON: "GP_WIDGET_BUTTON", # type: ignore
|
||||
gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore
|
||||
}
|
||||
def connect(self) -> bool:
|
||||
pass
|
||||
|
||||
operations = [
|
||||
("GP_OPERATION_NONE", gp.GP_OPERATION_NONE), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_IMAGE", gp.GP_OPERATION_CAPTURE_IMAGE), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_VIDEO", gp.GP_OPERATION_CAPTURE_VIDEO), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_AUDIO", gp.GP_OPERATION_CAPTURE_AUDIO), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_PREVIEW", gp.GP_OPERATION_CAPTURE_PREVIEW), # type: ignore
|
||||
("GP_OPERATION_CONFIG", gp.GP_OPERATION_CONFIG), # type: ignore
|
||||
("GP_OPERATION_TRIGGER_CAPTURE", gp.GP_OPERATION_TRIGGER_CAPTURE), # type: ignore
|
||||
]
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
def start_stream(self):
|
||||
pass
|
||||
|
||||
class GPhotoCamera(BaseCamera):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.camera = None
|
||||
self.configs: List[dict] = []
|
||||
self.camera_index = 0
|
||||
def stop_stream(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def detect() -> dict:
|
||||
cameras = gp.check_result(gp.gp_camera_autodetect()) # type: ignore
|
||||
# cameras = gp.Camera().autodetect()
|
||||
if not cameras or cameras.count() == 0: # type: ignore
|
||||
return {}
|
||||
def get_frame(self):
|
||||
pass
|
||||
|
||||
abilities_list = gp.CameraAbilitiesList() # type: ignore
|
||||
abilities_list.load()
|
||||
camera_list = {}
|
||||
for i in range(cameras.count()): # type: ignore
|
||||
name = cameras.get_name(i) # type: ignore
|
||||
port = cameras.get_value(i) # type: ignore
|
||||
def capture_photo(self):
|
||||
pass
|
||||
|
||||
abilities_index = abilities_list.lookup_model(name)
|
||||
abilities = abilities_list.get_abilities(abilities_index)
|
||||
abilities_name = []
|
||||
for name, bit in operations:
|
||||
if abilities.operations & bit: # type: ignore
|
||||
abilities_name.append(name)
|
||||
def record_video(self):
|
||||
pass
|
||||
|
||||
camera_list[i] = {"name": name, "port": port, "abilities": abilities_name}
|
||||
return camera_list
|
||||
def get_available_settings(self) -> dict:
|
||||
pass
|
||||
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
self.error_msg = None
|
||||
self.camera = gp.Camera() # type: ignore
|
||||
|
||||
try:
|
||||
if index:
|
||||
self.camera_index = index
|
||||
camera_list = GPhotoCamera.detect()
|
||||
port_info_list = gp.PortInfoList()
|
||||
port_info_list.load()
|
||||
|
||||
port_address = camera_list[index]["port"]
|
||||
port_index = port_info_list.lookup_path(port_address)
|
||||
|
||||
self.camera.set_port_info(port_info_list[port_index])
|
||||
|
||||
self.camera.init()
|
||||
config = self.camera.get_config()
|
||||
self.read_config(config)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.error_msg = f"[GPHOTO2] {e}"
|
||||
self.camera = None
|
||||
return False
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self.camera:
|
||||
self.camera.exit()
|
||||
self.camera = None
|
||||
self.configs.clear()
|
||||
|
||||
def get_frame(self):
|
||||
self.error_msg = None
|
||||
|
||||
if self.camera is None:
|
||||
self.error_msg = "[GPHOTO2] Camera is not initialized."
|
||||
return (False, None)
|
||||
|
||||
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)
|
||||
|
||||
return (True, frame)
|
||||
except Exception as e:
|
||||
self.error_msg = f"[GPHOTO2] {e}"
|
||||
return (False, None)
|
||||
|
||||
def get_config_by_id(self, id: int):
|
||||
return next(w for w in self.configs if w['id'] == id)
|
||||
|
||||
def get_config_by_name(self, name: str):
|
||||
return next(w for w in self.configs if w['name'] == name)
|
||||
|
||||
def set_config(self, config, value):
|
||||
if value not in config['choices']:
|
||||
return
|
||||
|
||||
config['widget'].set_value(value) # type: ignore
|
||||
if self._save_config(config):
|
||||
config['value'] = value
|
||||
|
||||
def set_config_by_id(self, id: int, value: str):
|
||||
config = self.get_config_by_id(id)
|
||||
|
||||
self.set_config(config, value)
|
||||
|
||||
def set_config_by_name(self, name: str, value: str):
|
||||
config = self.get_config_by_name(name)
|
||||
|
||||
self.set_config(config, value)
|
||||
|
||||
def _save_config(self, config):
|
||||
if not self.camera:
|
||||
return False
|
||||
|
||||
self.camera.set_single_config(config['name'], config['widget'])
|
||||
return True
|
||||
|
||||
def parse_config(self, config):
|
||||
new_config = {
|
||||
"id": config.get_id(),
|
||||
"name": config.get_name(),
|
||||
"label": config.get_label(),
|
||||
"type": camera_widget_types[config.get_type()],
|
||||
"widget": config
|
||||
}
|
||||
|
||||
try:
|
||||
new_config["value"] = config.get_value()
|
||||
except gp.GPhoto2Error:
|
||||
pass
|
||||
|
||||
try:
|
||||
new_config["choices"] = list(config.get_choices())
|
||||
except gp.GPhoto2Error:
|
||||
pass
|
||||
|
||||
return new_config
|
||||
|
||||
def read_config(self, config):
|
||||
self.configs.append(self.parse_config(config))
|
||||
|
||||
for i in range(config.count_children()):
|
||||
child = config.get_child(i)
|
||||
self.read_config(child)
|
||||
def set_setting(self, name: str, value) -> bool:
|
||||
pass
|
||||
@@ -1,122 +1,104 @@
|
||||
import cv2
|
||||
from cv2_enumerate_cameras import enumerate_cameras
|
||||
from typing import List
|
||||
# camera/opencv_camera.py
|
||||
|
||||
import cv2
|
||||
import time
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
class OpenCVCamera(BaseCamera):
|
||||
"""Implementacja kamery przy użyciu OpenCV."""
|
||||
|
||||
class OpenCvCamera(BaseCamera):
|
||||
"""Kamera oparta na cv2.VideoCapture"""
|
||||
def __init__(self, camera_index=0):
|
||||
self.camera_index = camera_index
|
||||
self.video_capture = None
|
||||
self._is_streaming = False
|
||||
# self._live_view_thread = None # Wewnętrzny wątek do pętli live view
|
||||
|
||||
config_map = {
|
||||
0: {"name": "frame_width", "cv_prop": cv2.CAP_PROP_FRAME_WIDTH, "default": 640},
|
||||
1: {"name": "frame_height", "cv_prop": cv2.CAP_PROP_FRAME_HEIGHT, "default": 480},
|
||||
2: {"name": "fps", "cv_prop": cv2.CAP_PROP_FPS, "default": 30},
|
||||
3: {"name": "brightness", "cv_prop": cv2.CAP_PROP_BRIGHTNESS, "default": 0.5},
|
||||
4: {"name": "contrast", "cv_prop": cv2.CAP_PROP_CONTRAST, "default": 0.5},
|
||||
5: {"name": "saturation", "cv_prop": cv2.CAP_PROP_SATURATION, "default": 0.5},
|
||||
}
|
||||
def connect(self) -> bool:
|
||||
self.video_capture = cv2.VideoCapture(self.camera_index)
|
||||
if not self.video_capture.isOpened():
|
||||
# self.error_occurred.emit(f"Nie można otworzyć kamery OpenCV o indeksie {self.camera_index}")
|
||||
self.video_capture = None
|
||||
return False
|
||||
# print("Kamera OpenCV połączona.")
|
||||
return True
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.cap = None
|
||||
self.configs: List[dict] = []
|
||||
self.camera_list = []
|
||||
self.camera_index = 0
|
||||
def disconnect(self):
|
||||
self.stop_stream()
|
||||
if self.video_capture:
|
||||
self.video_capture.release()
|
||||
self.video_capture = None
|
||||
# print("Kamera OpenCV rozłączona.")
|
||||
# self.camera_disconnected.emit()
|
||||
|
||||
@staticmethod
|
||||
def detect():
|
||||
camera_list = enumerate_cameras(cv2.CAP_ANY)
|
||||
result = {}
|
||||
def start_stream(self):
|
||||
if not self.video_capture or not self.video_capture.isOpened():
|
||||
# self.error_occurred.emit("Próba uruchomienia podglądu na niepodłączonej kamerze.")
|
||||
return
|
||||
|
||||
for camera in camera_list:
|
||||
cap = cv2.VideoCapture(camera.index, camera.backend)
|
||||
# ret, frame = cap.read()
|
||||
cap.release()
|
||||
if self._is_streaming:
|
||||
return # Już działa
|
||||
|
||||
# if ret and frame is not None and frame.size > 0:
|
||||
result[camera.index] = {
|
||||
"name": camera.name,
|
||||
"port": camera.path,
|
||||
"backend": camera.backend,
|
||||
}
|
||||
self._is_streaming = True
|
||||
# Uruchamiamy pętlę w metodzie, ponieważ cała klasa działa już w dedykowanym wątku
|
||||
# self._live_view_loop()
|
||||
|
||||
return result
|
||||
def stop_stream(self):
|
||||
self._is_streaming = False
|
||||
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
self.error_msg = None
|
||||
try:
|
||||
if index:
|
||||
self.camera_index = index
|
||||
|
||||
self.cap = cv2.VideoCapture(self.camera_index)
|
||||
def get_frame(self):
|
||||
if not self.video_capture:
|
||||
return None
|
||||
|
||||
if not self.cap.isOpened():
|
||||
self.error_msg = f"[CV2] Could not open camera {self.camera_index}"
|
||||
return False
|
||||
ret, frame = self.video_capture.read()
|
||||
if not ret:
|
||||
self.stop_stream()
|
||||
return None
|
||||
|
||||
self.configs.clear()
|
||||
for id, conf in self.config_map.items():
|
||||
value = self.cap.get(conf["cv_prop"])
|
||||
self.configs.append(
|
||||
{
|
||||
"id": id,
|
||||
"name": conf["name"],
|
||||
"label": conf["name"].capitalize(),
|
||||
"value": value,
|
||||
"choices": None, # brak predefiniowanych wyborów
|
||||
"cv_prop": conf["cv_prop"],
|
||||
}
|
||||
)
|
||||
return True
|
||||
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
h, w, ch = rgb_image.shape
|
||||
bytes_per_line = ch * w
|
||||
qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
|
||||
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
self.cap = None
|
||||
return False
|
||||
return qt_image
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
self.configs.clear()
|
||||
def capture_photo(self, save_path: str):
|
||||
if not self.video_capture or not self.video_capture.isOpened():
|
||||
# self.error_occurred.emit("Nie można zrobić zdjęcia, kamera nie jest podłączona.")
|
||||
return
|
||||
|
||||
def get_frame(self):
|
||||
self.error_msg = None
|
||||
if self.cap is None or not self.cap.isOpened():
|
||||
self.error_msg = "[CV2] Camera is not initialized."
|
||||
return (False, None)
|
||||
ret, frame = self.video_capture.read()
|
||||
if ret:
|
||||
try:
|
||||
cv2.imwrite(save_path, frame)
|
||||
print(f"Zdjęcie zapisane w: {save_path}")
|
||||
# self.photo_captured.emit(save_path)
|
||||
except Exception as e:
|
||||
# self.error_occurred.emit(f"Błąd zapisu zdjęcia: {e}")
|
||||
else:
|
||||
# self.error_occurred.emit("Nie udało się przechwycić klatki do zdjęcia.")
|
||||
|
||||
try:
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
self.error_msg = "[CV2] Failed to read frame."
|
||||
return (False, None)
|
||||
return (True, frame)
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
return (False, None)
|
||||
def get_available_settings(self) -> dict:
|
||||
# To jest uproszczona implementacja
|
||||
if not self.video_capture:
|
||||
return {}
|
||||
return {
|
||||
"brightness": self.video_capture.get(cv2.CAP_PROP_BRIGHTNESS),
|
||||
"contrast": self.video_capture.get(cv2.CAP_PROP_CONTRAST),
|
||||
"saturation": self.video_capture.get(cv2.CAP_PROP_SATURATION),
|
||||
}
|
||||
|
||||
def get_config_by_id(self, id: int):
|
||||
return next(w for w in self.configs if w["id"] == id)
|
||||
def set_setting(self, name: str, value) -> bool:
|
||||
if not self.video_capture:
|
||||
return False
|
||||
|
||||
def get_config_by_name(self, name: str):
|
||||
return next(w for w in self.configs if w["name"] == name)
|
||||
prop_map = {
|
||||
"brightness": cv2.CAP_PROP_BRIGHTNESS,
|
||||
"contrast": cv2.CAP_PROP_CONTRAST,
|
||||
"saturation": cv2.CAP_PROP_SATURATION,
|
||||
}
|
||||
|
||||
def set_config(self, config, value: float):
|
||||
if not self.cap:
|
||||
return
|
||||
try:
|
||||
self.cap.set(config["cv_prop"], value)
|
||||
config["value"] = self.cap.get(
|
||||
config["cv_prop"]) # sprawdz co ustawiło
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
|
||||
def set_config_by_id(self, id: int, value: float):
|
||||
config = self.get_config_by_id(id)
|
||||
self.set_config(config, value)
|
||||
|
||||
def set_config_by_name(self, name: str, value: float):
|
||||
config = self.get_config_by_name(name)
|
||||
self.set_config(config, value)
|
||||
if name in prop_map:
|
||||
return self.video_capture.set(prop_map[name], value)
|
||||
return False
|
||||
80
core/gphoto_adapter.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from PySide6.QtCore import QObject, QThread, Signal, QTimer
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .base import BaseControlSource, BaseImageSource
|
||||
|
||||
# try:
|
||||
# import gphoto2 as gp
|
||||
# except:
|
||||
from . import mock_gphoto as gp
|
||||
|
||||
class GPhotoImageSource(BaseImageSource):
|
||||
def __init__(self, camera: gp.Camera, fps=10, parent=None):
|
||||
super().__init__(parent)
|
||||
self.camera = camera
|
||||
self.fps = fps
|
||||
self.timer = None
|
||||
|
||||
def start(self):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self._grab_frame)
|
||||
self.timer.start(int(1000 / self.fps))
|
||||
|
||||
def _grab_frame(self):
|
||||
try:
|
||||
file = self.camera.capture_preview()
|
||||
data = file.get_data_and_size()
|
||||
frame = np.frombuffer(data, dtype=np.uint8)
|
||||
frame = cv2.imdecode(frame, cv2.IMREAD_COLOR)
|
||||
if frame is None:
|
||||
return
|
||||
|
||||
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
h, w, ch = rgb_image.shape
|
||||
qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888)
|
||||
pixmap = QPixmap.fromImage(qimg)
|
||||
self.frameReady.emit(pixmap)
|
||||
except gp.GPhoto2Error as e:
|
||||
self.errorOccurred.emit(f"GPhoto2 error: {e}")
|
||||
|
||||
def stop(self):
|
||||
if self.timer:
|
||||
self.timer.stop()
|
||||
|
||||
|
||||
|
||||
class GPhotoControlSource(BaseControlSource):
|
||||
def __init__(self, camera: gp.Camera, parent=None):
|
||||
super().__init__(parent)
|
||||
self.camera = camera
|
||||
|
||||
def set_parameter(self, name, value):
|
||||
try:
|
||||
config = self.camera.get_config()
|
||||
child = config.get_child_by_name(name)
|
||||
child.set_value(value)
|
||||
self.camera.set_config(config)
|
||||
self.parameterChanged.emit(name, value)
|
||||
except gp.GPhoto2Error as e:
|
||||
self.errorOccurred.emit(str(e))
|
||||
|
||||
def get_parameter(self, name):
|
||||
try:
|
||||
config = self.camera.get_config()
|
||||
child = config.get_child_by_name(name)
|
||||
return child.get_value()
|
||||
except gp.GPhoto2Error as e:
|
||||
self.errorOccurred.emit(str(e))
|
||||
return None
|
||||
|
||||
def list_parameters(self):
|
||||
params = {}
|
||||
try:
|
||||
config = self.camera.get_config()
|
||||
for child in config.get_children():
|
||||
params[child.get_name()] = child.get_value()
|
||||
except gp.GPhoto2Error as e:
|
||||
self.errorOccurred.emit(str(e))
|
||||
return params
|
||||
64
core/mock_gphoto.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
class GPhoto2Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CameraFileMock:
|
||||
"""Mock obiektu zwracanego przez gphoto2.Camera.capture_preview()"""
|
||||
|
||||
def __init__(self, frame: np.ndarray):
|
||||
# Kodowanie do JPEG, żeby symulować prawdziwe dane z kamery
|
||||
success, buf = cv2.imencode(".jpg", frame)
|
||||
if not success:
|
||||
raise GPhoto2Error("Nie udało się zakodować ramki testowej.")
|
||||
self._data = buf.tobytes()
|
||||
|
||||
def get_data_and_size(self):
|
||||
return self._data
|
||||
return self._data, len(self._data)
|
||||
|
||||
|
||||
class Camera:
|
||||
def __init__(self):
|
||||
self._frame_counter = 0
|
||||
self._running = False
|
||||
|
||||
def init(self):
|
||||
self._running = True
|
||||
print("[my_gphoto] Kamera MOCK zainicjalizowana")
|
||||
|
||||
def exit(self):
|
||||
self._running = False
|
||||
print("[my_gphoto] Kamera MOCK wyłączona")
|
||||
|
||||
def capture_preview(self):
|
||||
if not self._running:
|
||||
raise GPhoto2Error("Kamera MOCK nie jest uruchomiona")
|
||||
|
||||
# przykład 1: wczytaj stały obrazek z pliku
|
||||
# frame = cv2.imread("test_frame.jpg")
|
||||
# if frame is None:
|
||||
# raise GPhoto2Error("Nie znaleziono test_frame.jpg")
|
||||
|
||||
# przykład 2: wygeneruj kolorową planszę
|
||||
h, w = 480, 640
|
||||
color = (self._frame_counter % 255, 100, 200)
|
||||
frame = np.full((h, w, 3), color, dtype=np.uint8)
|
||||
|
||||
# dodanie napisu
|
||||
text = "OBRAZ TESTOWY"
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
scale = 1.5
|
||||
thickness = 3
|
||||
color_text = (255, 255, 255)
|
||||
|
||||
(text_w, text_h), _ = cv2.getTextSize(text, font, scale, thickness)
|
||||
x = (w - text_w) // 2
|
||||
y = (h + text_h) // 2
|
||||
cv2.putText(frame, text, (x, y), font, scale, color_text, thickness, cv2.LINE_AA)
|
||||
|
||||
|
||||
self._frame_counter += 1
|
||||
return CameraFileMock(frame)
|
||||
34
core/opencv_adapter.py
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class OpenCVControlSource(BaseControlSource):
|
||||
def __init__(self, cap: cv2.VideoCapture, parent=None):
|
||||
super().__init__(parent)
|
||||
self.cap = cap
|
||||
|
||||
def set_parameter(self, name, value):
|
||||
prop_id = getattr(cv2, name, None)
|
||||
if prop_id is None:
|
||||
self.errorOccurred.emit(f"Nieznany parametr {name}")
|
||||
return
|
||||
self.cap.set(prop_id, value)
|
||||
self.parameterChanged.emit(name, value)
|
||||
|
||||
def get_parameter(self, name):
|
||||
prop_id = getattr(cv2, name, None)
|
||||
if prop_id is None:
|
||||
self.errorOccurred.emit(f"Nieznany parametr {name}")
|
||||
return None
|
||||
return self.cap.get(prop_id)
|
||||
|
||||
def list_parameters(self):
|
||||
return {
|
||||
"CAP_PROP_BRIGHTNESS": self.cap.get(cv2.CAP_PROP_BRIGHTNESS),
|
||||
"CAP_PROP_CONTRAST": self.cap.get(cv2.CAP_PROP_CONTRAST),
|
||||
"CAP_PROP_SATURATION": self.cap.get(cv2.CAP_PROP_SATURATION),
|
||||
"CAP_PROP_GAIN": self.cap.get(cv2.CAP_PROP_GAIN),
|
||||
"CAP_PROP_EXPOSURE": self.cap.get(cv2.CAP_PROP_EXPOSURE),
|
||||
}
|
||||
6
main.py
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from ui.main_palette import set_dark_theme
|
||||
from ui.main_window import MainWindow
|
||||
from controllers.main_controller import MainController
|
||||
|
||||
@@ -9,13 +8,10 @@ from controllers.main_controller import MainController
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
set_dark_theme(app)
|
||||
# app.setStyle("Fusion")
|
||||
app.setStyle("Fusion")
|
||||
window = MainWindow()
|
||||
controller = MainController(window)
|
||||
controller.load_colors()
|
||||
|
||||
app.aboutToQuit.connect(controller.shutdown)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
cv2_enumerate_cameras==1.3.0
|
||||
gphoto2==2.6.2
|
||||
mypy==1.18.2
|
||||
mypy_extensions==1.1.0
|
||||
numpy==2.2.6
|
||||
opencv-python==4.12.0.88
|
||||
pathspec==0.12.1
|
||||
PySide6==6.9.2
|
||||
PySide6_Addons==6.9.2
|
||||
PySide6_Essentials==6.9.2
|
||||
shiboken6==6.9.2
|
||||
typing_extensions==4.15.0
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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="-4.5 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>arrow_left [#335]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-345.000000, -6679.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M299.633777,6519.29231 L299.633777,6519.29231 C299.228878,6518.90256 298.573377,6518.90256 298.169513,6519.29231 L289.606572,6527.55587 C288.797809,6528.33636 288.797809,6529.60253 289.606572,6530.38301 L298.231646,6538.70754 C298.632403,6539.09329 299.27962,6539.09828 299.685554,6538.71753 L299.685554,6538.71753 C300.100809,6538.32879 300.104951,6537.68821 299.696945,6537.29347 L291.802968,6529.67648 C291.398069,6529.28574 291.398069,6528.65315 291.802968,6528.26241 L299.633777,6520.70538 C300.038676,6520.31563 300.038676,6519.68305 299.633777,6519.29231" id="arrow_left-[#335]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,19 +0,0 @@
|
||||
<?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="-4.5 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>arrow_right [#336]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-305.000000, -6679.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M249.365851,6538.70769 L249.365851,6538.70769 C249.770764,6539.09744 250.426289,6539.09744 250.830166,6538.70769 L259.393407,6530.44413 C260.202198,6529.66364 260.202198,6528.39747 259.393407,6527.61699 L250.768031,6519.29246 C250.367261,6518.90671 249.720021,6518.90172 249.314072,6519.28247 L249.314072,6519.28247 C248.899839,6519.67121 248.894661,6520.31179 249.302681,6520.70653 L257.196934,6528.32352 C257.601847,6528.71426 257.601847,6529.34685 257.196934,6529.73759 L249.365851,6537.29462 C248.960938,6537.68437 248.960938,6538.31795 249.365851,6538.70769" id="arrow_right-[#336]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<?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 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.079 3.46209C15.3762 3.17355 15.851 3.18054 16.1396 3.47771L19.538 6.9777C19.8205 7.26871 19.8205 7.73162 19.538 8.02263L16.1396 11.5226C15.851 11.8198 15.3762 11.8268 15.079 11.5382C14.7819 11.2497 14.7749 10.7749 15.0634 10.4777L17.2263 8.25015L4.99989 8.25015C4.58567 8.25015 4.24989 7.91437 4.24989 7.50015C4.24989 7.08594 4.58567 6.75015 4.99989 6.75015L17.2263 6.75015L15.0634 4.52264C14.7749 4.22546 14.7819 3.75064 15.079 3.46209ZM8.92071 12.4618C9.21788 12.7504 9.22488 13.2252 8.93633 13.5224L6.77327 15.7501L18.9999 15.7501C19.4141 15.7501 19.7499 16.0859 19.7499 16.5001C19.7499 16.9143 19.4141 17.2501 18.9999 17.2501L6.77366 17.2501L8.93633 19.4774C9.22488 19.7746 9.21788 20.2494 8.92071 20.538C8.62353 20.8265 8.14871 20.8195 7.86016 20.5224L4.46177 17.0224C4.17922 16.7314 4.17922 16.2685 4.46177 15.9774L7.86016 12.4775C8.14871 12.1803 8.62353 12.1733 8.92071 12.4618Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +0,0 @@
|
||||
<?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 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.49976 4.25001C7.91398 4.25001 8.24976 4.5858 8.24976 5.00001L8.24976 17.2266L10.4775 15.0636C10.7747 14.775 11.2495 14.782 11.538 15.0792C11.8266 15.3763 11.8196 15.8512 11.5224 16.1397L8.02243 19.5381C7.73142 19.8207 7.26851 19.8207 6.9775 19.5381L3.47751 16.1397C3.18034 15.8512 3.17335 15.3763 3.4619 15.0792C3.75044 14.782 4.22527 14.775 4.52244 15.0636L6.74976 17.2262L6.74976 5.00001C6.74976 4.5858 7.08555 4.25001 7.49976 4.25001Z" fill="#000000"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9773 4.4619C16.2683 4.17934 16.7312 4.17934 17.0222 4.4619L20.5222 7.86029C20.8193 8.14884 20.8263 8.62366 20.5378 8.92083C20.2492 9.21801 19.7744 9.225 19.4772 8.93645L17.2497 6.7736L17.2497 19C17.2497 19.4142 16.9139 19.75 16.4997 19.75C16.0855 19.75 15.7497 19.4142 15.7497 19L15.7497 6.77358L13.5222 8.93645C13.225 9.225 12.7502 9.21801 12.4616 8.92083C12.1731 8.62366 12.1801 8.14884 12.4773 7.86029L15.9773 4.4619Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="horizontal-stacks-svgrepo-com.svg"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
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">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<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:zoom="0.98875"
|
||||
inkscape:cx="579.5196"
|
||||
inkscape:cy="342.35145"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M 13.5,14.82 V 1.18 C 13.5,0.53 12.97,0 12.32,0 H 9.8 C 9.15,0 8.62,0.53 8.62,1.18 V 14.82 C 8.62,15.47 9.15,16 9.8,16 h 2.52 c 0.65,0 1.18,-0.53 1.18,-1.18 z M 9.88,14.75 V 1.25 h 2.37 v 13.5 z m -2.5,0.07 V 1.18 C 7.38,0.53 6.85,0 6.2,0 H 3.68 C 3.03,0 2.5,0.53 2.5,1.18 V 14.82 C 2.5,15.47 3.03,16 3.68,16 H 6.2 c 0.65,0 1.18,-0.53 1.18,-1.18 z M 3.75,14.75 V 1.25 h 2.38 v 13.5 z"
|
||||
id="path1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,4 +0,0 @@
|
||||
<?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 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 20.5C17.1944 20.5 21 16.6944 21 12C21 7.30558 17.1944 3.5 12.5 3.5C7.80558 3.5 4 7.30558 4 12C4 13.5433 4.41128 14.9905 5.13022 16.238M1.5 15L5.13022 16.238M6.82531 12.3832L5.47107 16.3542L5.13022 16.238" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 533 B |
@@ -1,4 +0,0 @@
|
||||
<?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 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 20.5C6.80558 20.5 3 16.6944 3 12C3 7.30558 6.80558 3.5 11.5 3.5C16.1944 3.5 20 7.30558 20 12C20 13.5433 19.5887 14.9905 18.8698 16.238M22.5 15L18.8698 16.238M17.1747 12.3832L18.5289 16.3542L18.8698 16.238" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 534 B |
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.82 2.5H1.18C.53 2.5 0 3.03 0 3.68V6.2c0 .65.53 1.18 1.18 1.18h13.64c.65 0 1.18-.53 1.18-1.18V3.68c0-.65-.53-1.18-1.18-1.18zm-.07 3.62H1.25V3.75h13.5v2.37zm.07 2.5H1.18C.53 8.62 0 9.15 0 9.8v2.52c0 .65.53 1.18 1.18 1.18h13.64c.65 0 1.18-.53 1.18-1.18V9.8c0-.65-.53-1.18-1.18-1.18zm-.07 3.63H1.25V9.87h13.5v2.38z"/></svg>
|
||||
|
Before Width: | Height: | Size: 556 B |
@@ -1,48 +0,0 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QPalette, QColor
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
def set_dark_theme(app: QApplication):
|
||||
"""Definiuje i stosuje ciemną paletę kolorów do aplikacji."""
|
||||
|
||||
# 1. Upewnij się, że styl jest ustawiony na "Fusion"
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# 2. Definicja kolorów dla ciemnego motywu
|
||||
palette = QPalette()
|
||||
|
||||
# Kolory tła
|
||||
DARK_GRAY = QColor(45, 45, 45) # Ogólne tło okien i widżetów (Base, Window)
|
||||
LIGHT_GRAY = QColor(53, 53, 53) # Tło elementów, np. toolbara, menu (Window)
|
||||
VERY_DARK_GRAY = QColor(32, 32, 32) # Kolor tła dla kontrolek (Button)
|
||||
|
||||
# Kolory tekstu i obramowań
|
||||
WHITE = QColor(200, 200, 200) # Główny kolor tekstu (Text, WindowText)
|
||||
HIGHLIGHT = QColor(66, 135, 245) # Kolor podświetlenia (Highlight)
|
||||
|
||||
# Ustawienie głównej palety
|
||||
# palette.setColor(QPalette.ColorRole.Window, LIGHT_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.Window, VERY_DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Base, DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase, LIGHT_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.ToolTipBase, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.ToolTipText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Text, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Button, VERY_DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.ButtonText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
||||
palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(150, 150, 150))
|
||||
|
||||
# Kolory zaznaczenia/interakcji
|
||||
palette.setColor(QPalette.ColorRole.Highlight, HIGHLIGHT)
|
||||
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
|
||||
|
||||
# Kontrolki wyłączone (Disabled)
|
||||
# palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(127, 127, 127))
|
||||
# palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(127, 127, 127))
|
||||
|
||||
# 3. Zastosowanie palety do aplikacji
|
||||
app.setPalette(palette)
|
||||
@@ -10,7 +10,6 @@ from ui.widgets.placeholder_widget import PlaceholderWidget
|
||||
from ui.widgets.color_list_widget import ColorListWidget
|
||||
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
|
||||
from ui.widgets.split_view_widget import SplitView
|
||||
from ui.view_settings_dialog import ViewSettingsDialog
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -43,14 +42,6 @@ class MainWindow(QMainWindow):
|
||||
histogram_view = PlaceholderWidget("Histogram View", "#FF5733")
|
||||
histogram_view.setFixedHeight(200)
|
||||
|
||||
self.view_settings_button = QPushButton("Ustawienia widoku")
|
||||
self.view_settings_button.setMinimumHeight(40)
|
||||
self.view_settings_button.setStyleSheet("font-size: 12pt;")
|
||||
|
||||
self.view_settings_dialog = ViewSettingsDialog(self)
|
||||
self.view_settings_button.clicked.connect(self.view_settings_dialog.show)
|
||||
|
||||
|
||||
self.color_list_widget = ColorListWidget(self.control_widget)
|
||||
|
||||
self.record_button = QPushButton("Nagraj Wideo")
|
||||
@@ -62,7 +53,6 @@ class MainWindow(QMainWindow):
|
||||
self.photo_button.setStyleSheet("font-size: 12pt;")
|
||||
|
||||
control_layout.addWidget(histogram_view)
|
||||
control_layout.addWidget(self.view_settings_button)
|
||||
control_layout.addWidget(self.color_list_widget)
|
||||
control_layout.addWidget(self.record_button)
|
||||
control_layout.addWidget(self.photo_button)
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout ,QVBoxLayout, QPushButton, QGroupBox, QLabel, QRadioButton, QWidget, QToolButton, QSlider, QButtonGroup
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import Qt, QSize, Signal
|
||||
|
||||
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"]
|
||||
|
||||
class LabeledSpinSelector(QWidget):
|
||||
indexChanged = Signal(int)
|
||||
|
||||
def __init__(self, title: str, values: list[str], show_slider: bool = False, parent=None):
|
||||
super().__init__(parent)
|
||||
self.values = values
|
||||
self.current_index = 0
|
||||
self.show_slider = show_slider
|
||||
self._init_ui(title)
|
||||
|
||||
def _init_ui(self, title: str, button_size: int = 24, icon_size: int = 16):
|
||||
self.title_label = QLabel(title)
|
||||
|
||||
decrement_button = QToolButton()
|
||||
decrement_button.setIcon(QIcon("ui/icons/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.setFixedSize(button_size, button_size)
|
||||
increment_button.setIconSize(QSize(icon_size, icon_size))
|
||||
increment_button.clicked.connect(self._increment)
|
||||
|
||||
self.value_label = QLabel(self.values[self.current_index] if self.values else "N/A")
|
||||
self.value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.value_label.setStyleSheet("background-color: rgb(48, 48, 48);")
|
||||
self.value_label.setFixedHeight(button_size - 2)
|
||||
|
||||
spin_layout = QHBoxLayout()
|
||||
spin_layout.addWidget(decrement_button)
|
||||
spin_layout.addWidget(self.value_label)
|
||||
spin_layout.addWidget(increment_button)
|
||||
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.addWidget(self.title_label)
|
||||
top_layout.addLayout(spin_layout)
|
||||
|
||||
self.slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.slider.setRange(0, max(0, len(self.values) - 1))
|
||||
self.slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
||||
self.slider.valueChanged.connect(self._slider_changed)
|
||||
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.addLayout(top_layout)
|
||||
if self.show_slider:
|
||||
main_layout.addWidget(self.slider)
|
||||
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
def _increment(self):
|
||||
if not self.values:
|
||||
return
|
||||
new_index = min(self.current_index + 1, len(self.values) - 1)
|
||||
self.set_index(new_index)
|
||||
|
||||
def _decrement(self):
|
||||
if not self.values:
|
||||
return
|
||||
new_index = max(self.current_index - 1, 0)
|
||||
self.set_index(new_index)
|
||||
|
||||
def _slider_changed(self, index):
|
||||
if not self.values:
|
||||
return
|
||||
self.set_index(index)
|
||||
|
||||
def set_label(self, label: str):
|
||||
self.title_label.setText(label)
|
||||
|
||||
def set_index(self, index: int):
|
||||
if not self.values or not (0 <= index < len(self.values)):
|
||||
return
|
||||
if self.current_index != index:
|
||||
self.current_index = index
|
||||
self.value_label.setText(self.values[index])
|
||||
if self.show_slider:
|
||||
self.slider.setValue(index)
|
||||
self.indexChanged.emit(index)
|
||||
else:
|
||||
# Always update UI even if index is the same (for initial set)
|
||||
self.value_label.setText(self.values[index])
|
||||
if self.show_slider:
|
||||
self.slider.setValue(index)
|
||||
|
||||
class ViewSettingsDialog(QDialog):
|
||||
detectDevice = Signal(str)
|
||||
connectionChanged = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Ustawienia widoku")
|
||||
self.setFixedSize(640, 480)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
|
||||
|
||||
# self.setup_ui()
|
||||
camera_frame = self._create_devices_frame("camera")
|
||||
hdmi_frame = self._create_devices_frame("hdmi")
|
||||
conn_frame = self._create_connection_frame()
|
||||
camera_settings = self._create_settings_frame()
|
||||
hdmi_settings = self._create_settings_frame()
|
||||
dialog_buttons = self._create_dialog_buttons()
|
||||
|
||||
hdmi_settings.setEnabled(False)
|
||||
|
||||
settings_layout = QHBoxLayout()
|
||||
settings_layout.addWidget(camera_settings, 1)
|
||||
settings_layout.addWidget(hdmi_settings, 1)
|
||||
# main layout
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.addWidget(camera_frame)
|
||||
main_layout.addWidget(hdmi_frame)
|
||||
main_layout.addWidget(conn_frame)
|
||||
main_layout.addLayout(settings_layout)
|
||||
main_layout.addLayout(dialog_buttons)
|
||||
|
||||
main_layout.setStretch(3, 1)
|
||||
|
||||
self.last_choice = None
|
||||
|
||||
|
||||
def _create_dialog_buttons(self):
|
||||
ok_btn = QPushButton("OK")
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
|
||||
cancel_btn = QPushButton("Anuluj")
|
||||
cancel_btn.setDefault(True)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addStretch()
|
||||
layout.addWidget(ok_btn)
|
||||
layout.addWidget(cancel_btn)
|
||||
|
||||
return layout
|
||||
|
||||
def _create_devices_frame(self, name):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Wykryte aparaty")
|
||||
frame.setContentsMargins(6, 20, 6, 10)
|
||||
|
||||
device_label = QLabel("Nie wykryto podłączonych urządzeń.")
|
||||
detect_button = QPushButton("Wykryj...")
|
||||
detect_button.clicked.connect(lambda: self.detectDevice.emit(name))
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(device_label)
|
||||
layout.addStretch()
|
||||
layout.addWidget(detect_button)
|
||||
|
||||
frame.setLayout(layout)
|
||||
|
||||
return frame
|
||||
|
||||
def _create_connection_frame(self):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Wybór połączenia")
|
||||
frame.setContentsMargins(6, 20, 6, 10)
|
||||
|
||||
radio_usb = QRadioButton("USB")
|
||||
radio_hybrid = QRadioButton("USB + HDMI")
|
||||
radio_hdmi = QRadioButton("HDMI")
|
||||
|
||||
radio_hdmi.setEnabled(False)
|
||||
|
||||
radio_usb.clicked.connect(lambda: self.radio_toggle("usb"))
|
||||
radio_hybrid.clicked.connect(lambda: self.radio_toggle("hybrid"))
|
||||
radio_hdmi.clicked.connect(lambda: self.radio_toggle("hdmi"))
|
||||
|
||||
radio_layout = QHBoxLayout()
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_usb)
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_hybrid)
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_hdmi)
|
||||
radio_layout.addStretch()
|
||||
|
||||
frame.setLayout(radio_layout)
|
||||
|
||||
return frame
|
||||
|
||||
# def _create_settings_frame(self, settings: dict[str, list[str]]):
|
||||
def _create_settings_frame(self):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Ustawienia aparatu")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
# for key, value in settings.items():
|
||||
# layout.addWidget(LabeledSpinSelector(key, value))
|
||||
layout.addStretch()
|
||||
|
||||
frame.setLayout(layout)
|
||||
return frame
|
||||
|
||||
def radio_toggle(self, value):
|
||||
if self.last_choice != value:
|
||||
self.last_choice = value
|
||||
self.connectionChanged.emit(value)
|
||||
@@ -1,10 +1,9 @@
|
||||
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.QtCore import Qt, QSize, Signal, QEvent
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QStackedWidget, QPushButton, QLabel
|
||||
from PySide6.QtGui import QPixmap, QWheelEvent, QPainter, QBrush, QColor
|
||||
from PySide6.QtCore import Qt
|
||||
import sys
|
||||
from ui.widgets.placeholder_widget import PlaceholderWidget
|
||||
|
||||
|
||||
class ZoomableImageView(QGraphicsView):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -12,21 +11,18 @@ class ZoomableImageView(QGraphicsView):
|
||||
# Scena i element obrazu
|
||||
self._scene = QGraphicsScene(self)
|
||||
self.setScene(self._scene)
|
||||
self._scene.setBackgroundBrush(
|
||||
QBrush(QColor(20, 20, 20))) # ciemne tło
|
||||
self._scene.setBackgroundBrush(QBrush(QColor(20, 20, 20))) # ciemne tło
|
||||
|
||||
self._pixmap_item = QGraphicsPixmapItem()
|
||||
self._scene.addItem(self._pixmap_item)
|
||||
|
||||
# Ustawienia widoku
|
||||
# przesuwanie myszą
|
||||
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) # przesuwanie myszą
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
|
||||
# Wyłączenie suwaków
|
||||
self.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# Parametry zoomu
|
||||
self._zoom_factor = 1.25
|
||||
@@ -57,7 +53,6 @@ class ZoomableImageView(QGraphicsView):
|
||||
return
|
||||
super().wheelEvent(event) # normalne przewijanie
|
||||
|
||||
|
||||
class CameraPlaceholder(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -96,13 +91,11 @@ class CameraPlaceholder(QWidget):
|
||||
self.camera_start_btn.setStyleSheet(style_sheet)
|
||||
|
||||
self.info_label = QLabel("Kliknij, aby uruchomić kamerę")
|
||||
self.info_label.setStyleSheet(
|
||||
"background-color: transparent; color: #CECECE; font-size: 18px;")
|
||||
self.info_label.setStyleSheet("background-color: transparent; color: #CECECE; font-size: 18px;")
|
||||
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.camera_start_btn,
|
||||
alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.camera_start_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.info_label)
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
@@ -110,112 +103,6 @@ class CameraPlaceholder(QWidget):
|
||||
def set_info_text(self, text: str):
|
||||
self.info_label.setText(text)
|
||||
|
||||
|
||||
class ViewWithOverlay(QWidget):
|
||||
toggleOrientation = Signal()
|
||||
swapViews = Signal()
|
||||
rotateCW = Signal()
|
||||
rotateCCW = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.viewer = ZoomableImageView()
|
||||
layout.addWidget(self.viewer)
|
||||
|
||||
icon_size = QSize(32, 32)
|
||||
btn_size = (48, 48)
|
||||
btn_style = """
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
border: 2px solid #1f1f1f;
|
||||
"""
|
||||
|
||||
|
||||
self.cw_btn = QToolButton(self)
|
||||
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)
|
||||
self.ccw_btn.setIcon(QIcon("ui/icons/rotate-ccw-svgrepo-com.svg"))
|
||||
self.ccw_btn.setIconSize(icon_size)
|
||||
self.ccw_btn.setStyleSheet(btn_style)
|
||||
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.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
|
||||
self.flip_btn.setIconSize(icon_size)
|
||||
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.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
|
||||
self.orient_btn.setIconSize(icon_size)
|
||||
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.cw_btn.raise_()
|
||||
self.ccw_btn.raise_()
|
||||
self.flip_btn.raise_()
|
||||
self.orient_btn.raise_()
|
||||
|
||||
self.toggle_orientation(Qt.Orientation.Vertical)
|
||||
|
||||
def set_image(self, pixmap: QPixmap):
|
||||
self.viewer.set_image(pixmap)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
|
||||
move_x = self.cw_btn.width() + 10
|
||||
self.cw_btn.move(self.width() - move_x, 10)
|
||||
move_x += self.ccw_btn.width() + 10
|
||||
self.ccw_btn.move(self.width() - move_x, 10)
|
||||
move_x += self.flip_btn.width() + 10
|
||||
self.flip_btn.move(self.width() - move_x, 10)
|
||||
move_x += self.orient_btn.width() + 10
|
||||
self.orient_btn.move(self.width() - move_x, 10)
|
||||
|
||||
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"))
|
||||
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"))
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
self.orient_btn.show()
|
||||
self.flip_btn.show()
|
||||
self.ccw_btn.show()
|
||||
self.cw_btn.show()
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
self.orient_btn.hide()
|
||||
self.flip_btn.hide()
|
||||
self.ccw_btn.hide()
|
||||
self.cw_btn.hide()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
|
||||
class SplitView(QSplitter):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -223,11 +110,9 @@ class SplitView(QSplitter):
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
|
||||
self.widget_start = CameraPlaceholder()
|
||||
# self.widget_live = ZoomableImageView()
|
||||
self.widget_live = ViewWithOverlay()
|
||||
self.widget_live = ZoomableImageView()
|
||||
# self.widget_live = PlaceholderWidget("Camera View", "#750466")
|
||||
# self.widget_ref = ZoomableImageView()
|
||||
self.widget_ref = ViewWithOverlay()
|
||||
self.widget_ref = ZoomableImageView()
|
||||
# self.widget_ref = PlaceholderWidget("Image View", "#007981")
|
||||
|
||||
self.stack = QStackedWidget()
|
||||
@@ -244,31 +129,18 @@ class SplitView(QSplitter):
|
||||
# pixmap.fill(Qt.GlobalColor.lightGray)
|
||||
self.widget_live.set_image(pixmap)
|
||||
|
||||
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
|
||||
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
|
||||
self.widget_live.swapViews.connect(self.swap_views)
|
||||
self.widget_ref.swapViews.connect(self.swap_views)
|
||||
|
||||
def toggle_orientation(self):
|
||||
if self.orientation() == Qt.Orientation.Vertical:
|
||||
self.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.setSizes([self.width()//2, self.width()//2])
|
||||
self.widget_live.toggle_orientation(Qt.Orientation.Horizontal)
|
||||
self.widget_ref.toggle_orientation(Qt.Orientation.Horizontal)
|
||||
else:
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
self.setSizes([self.height()//2, self.height()//2])
|
||||
self.widget_live.toggle_orientation(Qt.Orientation.Vertical)
|
||||
self.widget_ref.toggle_orientation(Qt.Orientation.Vertical)
|
||||
|
||||
def swap_views(self):
|
||||
"""Zamiana widoków miejscami"""
|
||||
index_live = self.indexOf(self.stack)
|
||||
index_ref = self.indexOf(self.widget_ref)
|
||||
sizes = self.sizes()
|
||||
self.insertWidget(index_live, self.widget_ref)
|
||||
self.insertWidget(index_ref, self.stack)
|
||||
self.setSizes(sizes)
|
||||
# def set_live_image(self, path_image: str):
|
||||
# """Ustawienie obrazu na żywo"""
|
||||
# pixmap = QPixmap(path_image)
|
||||
# self.widget_live.set_image(pixmap)
|
||||
|
||||
def set_live_image(self, pixmap: QPixmap):
|
||||
"""Ustawienie obrazu na żywo"""
|
||||
@@ -287,3 +159,6 @@ class SplitView(QSplitter):
|
||||
self.stack.setCurrentWidget(self.widget_live)
|
||||
else:
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
|
||||
|
||||
|
||||
|
||||