feat: add camera window

This commit is contained in:
flandre 2025-06-11 20:13:53 +08:00
parent 1485c9446c
commit b6115bbb18
11 changed files with 295 additions and 12 deletions

0
draft/camera.py Normal file
View File

131
draft/pyqt_avif.py Normal file
View File

@ -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())

View File

@ -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

View File

@ -44,6 +44,7 @@ class LaunchComponent(Enum):
Mi = auto()
Web = auto()
VideoQt = auto()
CameraQt = auto()
def launch(arg: dict[LaunchComponent, dict]):

133
flandre/nodes/CameraQt.py Normal file
View File

@ -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()

View File

@ -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(

View File

@ -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()

View File

@ -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__":

View File

@ -1,6 +1,5 @@
from typing import Mapping
arg_map: dict[str, tuple[int, str]] = dict(
x=(100000, "{v:.2f}"),
y=(100000, "{v:.2f}"),

View File

@ -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.

19
uv.lock
View File

@ -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"