diff --git a/draft/camera.py b/draft/camera.py new file mode 100644 index 0000000..e69de29 diff --git a/draft/pyqt_avif.py b/draft/pyqt_avif.py new file mode 100644 index 0000000..7bdb40c --- /dev/null +++ b/draft/pyqt_avif.py @@ -0,0 +1,131 @@ +import io +import sys +from pathlib import Path + +from PIL import Image +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QImage, QPixmap +from PyQt6.QtWidgets import ( + QApplication, + QGraphicsPixmapItem, + QGraphicsScene, + QGraphicsView, + QMainWindow, + QPushButton, + QVBoxLayout, + QWidget, +) + +# For PyQt5 users, change the imports above to this: +# from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QVBoxLayout, QWidget, QPushButton, QFileDialog +# from PyQt5.QtGui import QPixmap +# from PyQt5.QtCore import Qt + + +class ZoomableView(QGraphicsView): + """ + A custom QGraphicsView that provides zoom functionality with the mouse wheel. + """ + + def __init__(self, scene): + super().__init__(scene) + # Set anchor points for zooming and resizing + self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) + + def wheelEvent(self, event): + """ + Handles mouse wheel events to zoom in or out. + """ + # Get the amount of scroll + angle = event.angleDelta().y() + + if angle > 0: + # Zoom in + factor = 1.25 + else: + # Zoom out + factor = 0.8 + + self.scale(factor, factor) + + +class ImageViewer(QMainWindow): + """ + Main application window for viewing an image. + Includes loading, zooming, and panning functionality. + """ + + def __init__(self): + super().__init__() + + self.setWindowTitle("PyQt Image Viewer (Load, Zoom, Pan)") + self.setGeometry(100, 100, 800, 700) + + # Main widget and layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + # 1. Create the QGraphicsScene + # The scene is the container for all 2D graphical items + self.scene = QGraphicsScene() + + # 2. Create the custom QGraphicsView + # This is our custom view with zoom capabilities + self.view = ZoomableView(self.scene) + + # 3. Enable panning (drag the scene with the mouse) + # The hand cursor will appear when you click and drag. + self.view.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + + # Add a button to load an image + self.load_button = QPushButton("Load Image") + self.load_button.clicked.connect(self.load_image) + + # Add widgets to the layout + layout.addWidget(self.load_button) + layout.addWidget(self.view) + + # A variable to hold the currently displayed image item + self.pixmap_item = None + + def load_image(self): + file_path = Path( + "/home/lambda/Downloads/f546ed6d35ee05338b8403d57dda10103ac3b1b8.jpg@672w_378h_1c_!web-home-common-cover.avif" + ) + image_data = file_path.read_bytes() + im = Image.open(io.BytesIO(image_data)) + pixmap = QPixmap.fromImage( + QImage(im.tobytes(), im.size[0], im.size[1], QImage.Format.Format_RGB888) + ) + if pixmap.isNull(): + print(f"Error: Failed to load image from {file_path}") + return + # If an image is already loaded, remove the old one first + if self.pixmap_item: + self.scene.removeItem(self.pixmap_item) + + # 5. Create a QGraphicsPixmapItem to hold the image + self.pixmap_item = QGraphicsPixmapItem(pixmap) + + # 6. Add the item to the scene + self.scene.addItem(self.pixmap_item) + + # --- Optional: Improve the viewing experience --- + # Reset any previous transformations (like zoom/pan) + self.view.resetTransform() + # Fit the entire image within the view, maintaining aspect ratio + self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio) + + +if __name__ == "__main__": + # Create the application instance + app = QApplication(sys.argv) + + # Create and show the main window + viewer = ImageViewer() + viewer.show() + + # Start the application's event loop + sys.exit(app.exec()) diff --git a/draft/sync_time.py b/draft/sync_time.py index b6c5926..5220991 100644 --- a/draft/sync_time.py +++ b/draft/sync_time.py @@ -1,16 +1,15 @@ import json -from pathlib import Path import subprocess import tempfile +from pathlib import Path -import matplotlib.pyplot as plt import numpy as np import numpy.typing as npt from tqdm import tqdm +from flandre.utils.archive import to_zip from flandre.utils.RfFrame import b2t from flandre.utils.RfMeta import RfFrameMeta -from flandre.utils.archive import to_zip from flandre.utils.robot import rtsi_float2int diff --git a/flandre/launcher.py b/flandre/launcher.py index eb9c5b8..7ae0ab7 100644 --- a/flandre/launcher.py +++ b/flandre/launcher.py @@ -44,6 +44,7 @@ class LaunchComponent(Enum): Mi = auto() Web = auto() VideoQt = auto() + CameraQt = auto() def launch(arg: dict[LaunchComponent, dict]): diff --git a/flandre/nodes/CameraQt.py b/flandre/nodes/CameraQt.py new file mode 100644 index 0000000..88aa1ed --- /dev/null +++ b/flandre/nodes/CameraQt.py @@ -0,0 +1,133 @@ +import io +import logging +import sys +import threading +from pathlib import Path + +import pillow_avif # type: ignore +from PIL import Image +from PyQt6 import QtCore +from PyQt6.QtCore import QByteArray, Qt +from PyQt6.QtGui import QImage, QKeyEvent, QPixmap, QResizeEvent, QWheelEvent +from PyQt6.QtWidgets import ( + QApplication, + QGraphicsPixmapItem, + QGraphicsScene, + QMainWindow, +) + +from flandre.nodes.Node import Node +from flandre.pyqt.Image import Ui_MainWindow +from flandre.pyqt.ZMQReceiver import ZMQReceiver +from flandre.utils.archive import zip_to_bytes +from flandre.utils.Msg import KillMsg, Msg, RfFrameMsg +from flandre.utils.RfFrame import RfFrameFile + +pillow_avif.__all__ + +logger = logging.getLogger(__name__) + + +class Adv(QMainWindow, Ui_MainWindow): + ee2 = QtCore.pyqtSignal("QByteArray") + + def __init__(self, p: Node, parent=None): + super(Adv, self).__init__(parent) + self.p = p + self.setupUi(self) + zmq_receiver = ZMQReceiver(self) + zmq_receiver.zmq_event.connect(self.on_zmq_event) + zmq_receiver.start() + self.g = QGraphicsPixmapItem() + self.s = QGraphicsScene() + self.s.addItem(self.g) + self.graphicsView.setScene(self.s) + self.grey = False + self.scale = False + self.watermark = True + self.zoom = 1.0 + self.need_fit = False + self.frame1: RfFrameFile | None = None + self.ee2.connect(self.draw1) + threading.Thread(target=self.e1, daemon=True).start() + + def keyPressEvent(self, a0: QKeyEvent): + t = a0.text() + match t: + case "m": + pass + case _: + pass + + def resizeEvent(self, a0: QResizeEvent | None) -> None: + self.graphicsView.fitInView( + self.s.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio + ) + + def wheelEvent(self, a0: QWheelEvent | None) -> None: + if a0 is None: + return + if a0.angleDelta().y() > 0: + self.zoom += 0.1 + if a0.angleDelta().y() < 0: + self.zoom -= 0.1 + + def draw1(self, d: QByteArray): + image_data = d.data() + im = Image.open(io.BytesIO(image_data)) + pixmap = QPixmap.fromImage( + QImage(im.tobytes(), im.size[0], im.size[1], QImage.Format.Format_RGB888) + ) + self.g.setPixmap(pixmap) + # self.s.setSceneRect(0.0, 0.0, im.size[0], im.size[1]) + self.graphicsView.fitInView( + self.s.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio + ) + + def e1(self): + last_frame = self.frame1 + while True: + if last_frame != self.frame1 and self.frame1 is not None: + b = zip_to_bytes( + self.frame1.seq.path, + Path(self.frame1.filename).with_suffix(".avif").__str__(), + ) + self.ee2.emit(b) + last_frame = self.frame1 + + def on_zmq_event(self, raw_msg: QByteArray): + try: + msg: Msg = Msg.decode_msg(raw_msg.data()) + match msg: + case KillMsg(): + if msg.name == "": + self.close() + case RfFrameMsg(): + frame = msg.rf_frame + match frame: + case RfFrameFile(): + if frame.seq.type == "zip": + self.frame1 = frame + pass + case _: + pass + case _: + pass + + except Exception as e: + logger.warning(f"{e}") + + +class CameraQt(Node): + topics = [RfFrameMsg] + + def __init__(self): + super().__init__() + + def loop(self): + app = QApplication(sys.argv) + MainWindow = Adv(self) + # MainWindow.move(int(px), int(py)) + # MainWindow.resize(int(sx), int(sy)) + MainWindow.show() + app.exec() diff --git a/flandre/utils/Msg.py b/flandre/utils/Msg.py index 8b56e77..f3decac 100644 --- a/flandre/utils/Msg.py +++ b/flandre/utils/Msg.py @@ -353,7 +353,7 @@ class BMMsg(Msg): class RfFrameMsg(HeaderByteMsg): def __init__(self, sender: int, rf_frame: RfFrame): self.sender = sender - self.rf_frame = rf_frame + self.rf_frame: RfFrame = rf_frame if isinstance(rf_frame, RfFrameFile): super().__init__( dict( diff --git a/flandre/utils/RfFrame.py b/flandre/utils/RfFrame.py index 8483e60..59edec2 100644 --- a/flandre/utils/RfFrame.py +++ b/flandre/utils/RfFrame.py @@ -28,10 +28,10 @@ class RfFrameFile(RfFrame): return self.data match self.seq.type: case "zip": - from flandre.utils.archive import zip_to_bytes + from flandre.utils.archive import zip_to_rfmat if self.filename is not None: - return zip_to_bytes(self.seq.path, int(Path(self.filename).stem)) + return zip_to_rfmat(self.seq.path, int(Path(self.filename).stem)) case "dir": return (self.seq.path / self.filename).read_bytes() raise NotImplementedError() diff --git a/flandre/utils/archive.py b/flandre/utils/archive.py index 6e8b0af..4ee9b96 100644 --- a/flandre/utils/archive.py +++ b/flandre/utils/archive.py @@ -4,8 +4,8 @@ import subprocess import zipfile from pathlib import Path -from tqdm import tqdm import zstd +from tqdm import tqdm TEMP_FOLDER = Path("/mnt/16T/private_dataset/New Folder/temp") @@ -66,12 +66,12 @@ def to_zip( subprocess.run(["zip", "-0", "-j", "-r", zipdst, temp_dst]) -def zip_to_bytes(file: Path, name: int): - return zstd.loads(zipfile.ZipFile(file).open(f"{name}.zst").read()) +def zip_to_bytes(file: Path, name: str): + return zipfile.ZipFile(file).open(name).read() -def zip_to_bytes2(file: Path, name: str): - return zstd.loads(zipfile.ZipFile(file).open(name).read()) +def zip_to_rfmat(file: Path, frame_index: int): + return zstd.loads(zip_to_bytes(file, f"{frame_index}.zst")) if __name__ == "__main__": diff --git a/flandre/utils/robot.py b/flandre/utils/robot.py index 9361903..73d2159 100644 --- a/flandre/utils/robot.py +++ b/flandre/utils/robot.py @@ -1,6 +1,5 @@ from typing import Mapping - arg_map: dict[str, tuple[int, str]] = dict( x=(100000, "{v:.2f}"), y=(100000, "{v:.2f}"), diff --git a/pyproject.toml b/pyproject.toml index 0168b34..d1b7c25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "matplotlib>=3.10.1", "fastapi[standard]>=0.115.12", "websockets>=15.0.1", + "pillow-avif-plugin>=1.5.2", ] [tool.setuptools] # configuration specific to the `setuptools` build backend. diff --git a/uv.lock b/uv.lock index f44b2b4..47f2208 100644 --- a/uv.lock +++ b/uv.lock @@ -490,6 +490,7 @@ dependencies = [ { name = "matplotlib" }, { name = "mido", extra = ["ports-rtmidi"] }, { name = "opencv-python" }, + { name = "pillow-avif-plugin" }, { name = "platformdirs" }, { name = "pyjoystick" }, { name = "pyqt6" }, @@ -521,6 +522,7 @@ requires-dist = [ { name = "mido", extras = ["ports-rtmidi"], specifier = ">=1.3.3" }, { name = "nvidia-nvimgcodec-cu12", marker = "extra == 'to'", specifier = ">=0.5.0.13" }, { name = "opencv-python", specifier = ">=4.10.0.84" }, + { name = "pillow-avif-plugin", specifier = ">=1.5.2" }, { name = "platformdirs", specifier = ">=4.3.6" }, { name = "pyjoystick", specifier = ">=1.2.4" }, { name = "pyqt6", specifier = ">=6.8.0" }, @@ -1331,6 +1333,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, ] +[[package]] +name = "pillow-avif-plugin" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/32/a3bfad0537ba6f2accc6a8a2e53e09b266418347f58898f811ca2fb70bd9/pillow_avif_plugin-1.5.2.tar.gz", hash = "sha256:811e0dc8be1e44393d2e3865ec330a8a8a1194b94eb8cfca6fa778e3f476d649", size = 20571, upload-time = "2025-04-24T14:11:49.163Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/0ee9a2a55142183fd881d912bae912df36cf8fe84ba3be92c4530558732f/pillow_avif_plugin-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ec28185f536857965be2156e13231274d58a154504db3da4dcda772e391cf5f", size = 3900276, upload-time = "2025-04-24T14:10:32.128Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7e/f28e53b8c9e31e015c8923ff3dca4c577e38013d4dd5fb005182cd4631a8/pillow_avif_plugin-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e36b3cbf5e61b15d2fa213508cf348251e7830ebeff1d97e4e18c55cc0c3b784", size = 2805833, upload-time = "2025-04-24T14:10:33.774Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f8/3bf477c4d32470819f10c1163c79f3e0a14c9c3f3ffd20d2d051a6a1dd9b/pillow_avif_plugin-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8098ceed3f76c38b61a35221f0db0205d81d06ff87f4ec11175c90bc66f8c1d", size = 2991435, upload-time = "2025-04-24T14:10:35.06Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e3/c08f2346d04f0969fec9a6751af8c23dfffd02a55ca91715cb3ee86b53be/pillow_avif_plugin-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2075069ba6c00236cf7aed66c15863e9ea44b280c20e270aa6f56173b7a380c3", size = 6365698, upload-time = "2025-04-24T14:10:36.678Z" }, + { url = "https://files.pythonhosted.org/packages/90/96/7676079b7305a6cc31f284c7036c33f5131886fdc074f204b3b3cdef1886/pillow_avif_plugin-1.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:db7d4fc23d8f80ef4f9fdac8e0071856c4c8b9acfc3c4d5cb125d5299d6130b2", size = 3003962, upload-time = "2025-04-24T14:10:37.924Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/058a6e5e0794e2fa6e012a350097b989fb0aa9ecc704e9da2cafddbb7a22/pillow_avif_plugin-1.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8174501ed895508d5801d61fa9518252693125688dadac7fba79612f9bba623a", size = 4173892, upload-time = "2025-04-24T14:10:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/054bd0c363a5ff6e35d0e763c75cc64eb2408d19a7de34fc6448cdf57fbc/pillow_avif_plugin-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:25eeb443636a1c7f0bd0a6994436ad44de09e87648574a797cbd8620b8dcb52d", size = 3101308, upload-time = "2025-04-24T14:10:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/508f425b4e5e42e6c165b819b4953c482f05b38bc18958b1e11c3bfa7ebb/pillow_avif_plugin-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5004a099d52a9a23dfa5b19382a48c0f0930de6231deb013222867fea8948712", size = 4195525, upload-time = "2025-04-24T14:10:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/64/01/0f9cbc4fbb7345790379fb5321776150043d6b6e5674a196a4ba552b570c/pillow_avif_plugin-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:0bc8cec59b5d9c2020cdc6d218081b4d7b0dd60270a24f9174d6b91438a3aa5a", size = 9857569, upload-time = "2025-04-24T14:10:43.099Z" }, +] + [[package]] name = "platformdirs" version = "4.3.6"