diff --git a/dev/demo/demo_capture_volume_visualizer.py b/dev/demo/demo_capture_volume_visualizer.py index 8897a2f2e..e5c9f8ab7 100644 --- a/dev/demo/demo_capture_volume_visualizer.py +++ b/dev/demo/demo_capture_volume_visualizer.py @@ -9,13 +9,13 @@ import sys from pyxy3d.calibration.capture_volume.capture_volume import CaptureVolume import pickle -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.configurator import Configurator from pyxy3d.gui.vizualize.calibration.capture_volume_widget import CaptureVolumeWidget session_path = Path(__root__, "tests" , "sessions_copy_delete", "2_cam_set_origin_test") config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.load_estimated_capture_volume() diff --git a/dev/demo/demo_gui_calibrate_capture_volume.py b/dev/demo/demo_gui_calibrate_capture_volume.py index a03e53c83..e0175c071 100644 --- a/dev/demo/demo_gui_calibrate_capture_volume.py +++ b/dev/demo/demo_gui_calibrate_capture_volume.py @@ -5,7 +5,7 @@ from pyxy3d import __root__ from pathlib import Path from pyxy3d.configurator import Configurator -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode import toml from pyxy3d import __app_dir__ @@ -16,7 +16,7 @@ session_path = Path(recent_projects[recent_project_count-1]) config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.set_mode(SessionMode.ExtrinsicCalibration) app = QApplication(sys.argv) window = CalibrateCaptureVolumeWidget(session) diff --git a/dev/demo/demo_gui_capture_volume.py b/dev/demo/demo_gui_capture_volume.py index 1912e4d9c..c7c25149e 100644 --- a/dev/demo/demo_gui_capture_volume.py +++ b/dev/demo/demo_gui_capture_volume.py @@ -6,7 +6,7 @@ from pyxy3d import __root__ from pathlib import Path from pyxy3d.configurator import Configurator -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode import toml from pyxy3d import __app_dir__ @@ -17,7 +17,7 @@ session_path = Path(recent_projects[recent_project_count-1]) config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.load_estimated_capture_volume() # session.set_mode(SessionMode.ExtrinsicCalibration) app = QApplication(sys.argv) diff --git a/dev/demo/demo_gui_extrinsic_calibration.py b/dev/demo/demo_gui_extrinsic_calibration.py index 56f69c3fd..8b69f33f5 100644 --- a/dev/demo/demo_gui_extrinsic_calibration.py +++ b/dev/demo/demo_gui_extrinsic_calibration.py @@ -12,7 +12,7 @@ from pyxy3d.gui.main_widget import MainWindow from pyxy3d.configurator import Configurator from pyxy3d.gui.extrinsic_calibration_widget import ExtrinsicCalibrationWidget -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode app_settings = toml.load(Path(__app_dir__, "settings.toml")) @@ -21,7 +21,7 @@ recent_project_count = len(recent_projects) session_path = Path(recent_projects[recent_project_count - 1]) config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.set_mode(SessionMode.ExtrinsicCalibration) # while not hasattr(session.synchronizer, "current_sync_packet"): diff --git a/dev/demo/demo_gui_intrinsic_calibration.py b/dev/demo/demo_gui_intrinsic_calibration.py index 809ad7042..c63bf2a28 100644 --- a/dev/demo/demo_gui_intrinsic_calibration.py +++ b/dev/demo/demo_gui_intrinsic_calibration.py @@ -7,8 +7,8 @@ from pyxy3d import __app_dir__ from pyxy3d.gui.main_widget import MainWindow from pyxy3d.configurator import Configurator -from pyxy3d.gui.camera_config.intrinsic_calibration_widget import IntrinsicCalibrationWidget -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.gui.live_camera_config.intrinsic_calibration_widget import IntrinsicCalibrationWidget +from pyxy3d.session.session import LiveSession, SessionMode app_settings = toml.load(Path(__app_dir__, "settings.toml")) @@ -17,7 +17,7 @@ recent_project_count = len(recent_projects) session_path = Path(recent_projects[recent_project_count - 1]) config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.set_mode(SessionMode.IntrinsicCalibration) app = QApplication(sys.argv) diff --git a/dev/demo/demo_gui_main.py b/dev/demo/demo_gui_main.py index ea454a7ad..1cbdfedd4 100644 --- a/dev/demo/demo_gui_main.py +++ b/dev/demo/demo_gui_main.py @@ -4,7 +4,7 @@ from pathlib import Path import toml from pyxy3d import __app_dir__ -from pyxy3d.gui.single_main_widget import MainWindow +from pyxy3d.gui.main_widget import MainWindow app_settings = toml.load(Path(__app_dir__, "settings.toml")) recent_projects: list = app_settings["recent_projects"] diff --git a/dev/demo/demo_gui_postprocess.py b/dev/demo/demo_gui_postprocess.py index e1a1fd569..0a30daf59 100644 --- a/dev/demo/demo_gui_postprocess.py +++ b/dev/demo/demo_gui_postprocess.py @@ -10,7 +10,7 @@ from time import sleep from pyxy3d import __root__ from pyxy3d.configurator import Configurator -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pathlib import Path from pyxy3d import __app_dir__ @@ -24,7 +24,7 @@ logger.info(f"Launching post processing widget for {session_path}") config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) app = QApplication(sys.argv) diff --git a/dev/demo/demo_gui_recording.py b/dev/demo/demo_gui_recording.py index 2e8ab7801..4e36fd310 100644 --- a/dev/demo/demo_gui_recording.py +++ b/dev/demo/demo_gui_recording.py @@ -17,7 +17,7 @@ import toml from pyxy3d.gui.recording_widget import RecordingWidget -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d import __root__ from pyxy3d.helper import copy_contents @@ -31,7 +31,7 @@ session_path = Path(recent_projects[recent_project_count-1]) # copy_contents(session_origin_path,session_path) config = Configurator(session_path) -session = Session(config) +session = LiveSession(config) session.set_mode(SessionMode.Recording) App = QApplication(sys.argv) diff --git a/dev/demo/demo_playback_widget.py b/dev/demo/demo_playback_widget.py index 4ea57d683..e4b51b51d 100644 --- a/dev/demo/demo_playback_widget.py +++ b/dev/demo/demo_playback_widget.py @@ -12,7 +12,7 @@ from pyxy3d.recording.recorded_stream import RecordedStreamPool from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d.triangulate.sync_packet_triangulator import SyncPacketTriangulator -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.vizualize.playback_triangulation_widget import ( PlaybackTriangulationWidget, ) @@ -37,7 +37,7 @@ logger.info(f"Loading session {session_path}") -session = Session(config) +session = LiveSession(config) app = QApplication(sys.argv) recording_path = Path(session_path, "recording_1") diff --git a/dev/demo/demo_process_prerecorded.py b/dev/demo/demo_process_prerecorded.py index e92d8c755..77c5297e9 100644 --- a/dev/demo/demo_process_prerecorded.py +++ b/dev/demo/demo_process_prerecorded.py @@ -12,7 +12,7 @@ from pyxy3d.recording.recorded_stream import RecordedStreamPool from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d.triangulate.sync_packet_triangulator import SyncPacketTriangulator -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.vizualize.playback_triangulation_widget import ( PlaybackTriangulationWidget, ) @@ -53,7 +53,7 @@ logger.info(f"Loading session {session_path}") -session = Session(config) +session = LiveSession(config) app = QApplication(sys.argv) diff --git a/dev/demo/demo_real_time_triangulator_mediapipe_hands.py b/dev/demo/demo_real_time_triangulator_mediapipe_hands.py index 4a4906201..e305c6171 100644 --- a/dev/demo/demo_real_time_triangulator_mediapipe_hands.py +++ b/dev/demo/demo_real_time_triangulator_mediapipe_hands.py @@ -23,7 +23,7 @@ from pyxy3d.gui.vizualize.realtime_triangulation_widget import \ RealTimeTriangulationWidget from pyxy3d.interface import FramePacket, PointPacket, SyncPacket -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.trackers.charuco_tracker import Charuco, CharucoTracker from pyxy3d.trackers.hand_tracker import HandTracker from pyxy3d.triangulate.sync_packet_triangulator import SyncPacketTriangulator @@ -32,7 +32,7 @@ app = QApplication(sys.argv) session_path = Path(__root__,"dev", "sample_sessions", "low_res") -session = Session(session_path) +session = LiveSession(session_path) tracker = TrackerEnum.HAND session.load_stream_tools(tracker=tracker) diff --git a/dev/dev_playback_widget.py b/dev/dev_playback_widget.py new file mode 100644 index 000000000..48c3d17e0 --- /dev/null +++ b/dev/dev_playback_widget.py @@ -0,0 +1,90 @@ +import sys +import cv2 +from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QSlider, QLabel) +from PySide6.QtCore import (Qt, QTimer) +from PySide6.QtGui import (QPixmap, QImage) + +class VideoPlayer(QWidget): + def __init__(self, video_path, parent=None): + super().__init__(parent) + self.video_path = video_path + self.cap = cv2.VideoCapture(self.video_path) + self.frame_rate = int(self.cap.get(cv2.CAP_PROP_FPS)) + self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + self.timer = QTimer() + + self.place_widgets() + self.connect_widgets() + + def place_widgets(self): + self.layout = QVBoxLayout() + + self.image_label = QLabel(self) + self.layout.addWidget(self.image_label) + + self.play_button = QPushButton("Play", self) + self.layout.addWidget(self.play_button) + + self.slider = QSlider(Qt.Horizontal, self) + self.slider.setMaximum(self.total_frames) + self.layout.addWidget(self.slider) + + self.setLayout(self.layout) + + def connect_widgets(self): + self.play_button.clicked.connect(self.play_video) + self.slider.sliderMoved.connect(self.slider_moved) + self.timer.timeout.connect(self.next_frame) + + def play_video(self): + if self.timer.isActive(): + self.timer.stop() + self.play_button.setText("Play") + else: + self.timer.start(1000 // self.frame_rate) + self.play_button.setText("Pause") + + def next_frame(self): + ret, frame = self.cap.read() + if ret: + self.display_image(frame) + self.slider.setValue(self.slider.value() + 1) + else: + self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + self.slider.setValue(0) + + def slider_moved(self, position): + self.cap.set(cv2.CAP_PROP_POS_FRAMES, position) + ret, frame = self.cap.read() + if ret: + self.display_image(frame) + + def display_image(self, frame): + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + height, width, channel = frame.shape + bytes_per_line = 3 * width + q_img = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) + pixmap = QPixmap.fromImage(q_img) + self.image_label.setPixmap(pixmap.scaled(self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio)) + + def closeEvent(self, event): + self.cap.release() + super().closeEvent(event) + +class VideoWindow(QMainWindow): + def __init__(self, video_path, parent=None): + super().__init__(parent) + self.setWindowTitle("Video Player") + self.player = VideoPlayer(video_path, self) + self.setCentralWidget(self.player) + +if __name__ == "__main__": + app = QApplication(sys.argv) + # Define the input file path here. + input_file = r"C:\Users\Mac Prible\repos\pyxy3d\tests\sessions\4_cam_recording\calibration\extrinsic\port_0.mp4" + + + window = VideoWindow(input_file) + window.resize(800, 600) + window.show() + sys.exit(app.exec()) diff --git a/poetry.lock b/poetry.lock index ef9289e22..836a9d1e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "absl-py" @@ -1134,32 +1134,6 @@ files = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] -[[package]] -name = "mizani" -version = "0.9.3" -description = "Scales for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mizani-0.9.3-py3-none-any.whl", hash = "sha256:ac5d49b913de88dc2fb28d82141e9777b97407a6971a158f758093ad5bb820a1"}, - {file = "mizani-0.9.3.tar.gz", hash = "sha256:fb61339e9e4711850e902ca286b1ae75255f483823d891aa0515b426d56c606d"}, -] - -[package.dependencies] -matplotlib = ">=3.5.0" -numpy = ">=1.19.0" -pandas = ">=1.3.5" -scipy = ">=1.5.0" -tzdata = {version = "*", markers = "platform_system == \"Windows\" or platform_system == \"Emscripten\""} - -[package.extras] -all = ["mizani[build]", "mizani[dev]", "mizani[doc]", "mizani[lint]", "mizani[test]"] -build = ["build", "wheel"] -dev = ["notebook", "twine"] -doc = ["numpydoc (>=0.9.1)", "sphinx (>=6.1.0)"] -lint = ["black (>=23.1.0)", "ruff"] -test = ["pytest-cov"] - [[package]] name = "mkdocs" version = "1.5.3" @@ -1366,12 +1340,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, ] [[package]] @@ -1433,8 +1404,8 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] python-dateutil = ">=2.8.1" pytz = ">=2020.1" @@ -1468,24 +1439,6 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] -[[package]] -name = "patsy" -version = "0.5.3" -description = "A Python package for describing statistical models and for building design matrices." -optional = false -python-versions = "*" -files = [ - {file = "patsy-0.5.3-py2.py3-none-any.whl", hash = "sha256:7eb5349754ed6aa982af81f636479b1b8db9d5b1a6e957a6016ec0534b5c86b7"}, - {file = "patsy-0.5.3.tar.gz", hash = "sha256:bdc18001875e319bc91c812c1eb6a10be4bb13cb81eb763f466179dca3b67277"}, -] - -[package.dependencies] -numpy = ">=1.4" -six = "*" - -[package.extras] -test = ["pytest", "pytest-cov", "scipy"] - [[package]] name = "pexpect" version = "4.8.0" @@ -1593,36 +1546,6 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] -[[package]] -name = "plotnine" -version = "0.12.3" -description = "A Grammar of Graphics for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "plotnine-0.12.3-py3-none-any.whl", hash = "sha256:3868a538aecaf44505f7218e2ffedc5611a7b23c2cad4a1e28e1255b403f467b"}, - {file = "plotnine-0.12.3.tar.gz", hash = "sha256:a38dcb3607fc003c1e59ae0c9d535dae7817650d1cbc2e56e56e5b3de88dfe99"}, -] - -[package.dependencies] -matplotlib = ">=3.6.0" -mizani = ">0.9.0,<0.10.0" -numpy = ">=1.23.0" -pandas = ">=1.5.0" -patsy = ">=0.5.1" -scipy = ">=1.5.0" -statsmodels = ">=0.14.0" - -[package.extras] -all = ["plotnine[build]", "plotnine[dev]", "plotnine[doc]", "plotnine[extra]", "plotnine[lint]", "plotnine[test]"] -build = ["build", "wheel"] -dev = ["plotnine[typing]", "twine"] -doc = ["importlib-resources", "jupyter", "nbsphinx", "numpydoc (>=0.9.1)", "sphinx (>=6.1.0)"] -extra = ["adjustText", "geopandas", "scikit-learn", "scikit-misc (>=0.2.0)"] -lint = ["black (>=23.1.0)", "ruff"] -test = ["pytest-cov"] -typing = ["ipython", "pandas-stubs", "pyright"] - [[package]] name = "pluggy" version = "1.3.0" @@ -1638,38 +1561,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "polars" -version = "0.18.15" -description = "Blazingly fast DataFrame library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "polars-0.18.15-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:f7a4e4108efd2ab728249f792c89d2e7baffd65e0d6cd9f09b6c395363e3fbea"}, - {file = "polars-0.18.15-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:7dee57ecc6f6151f1f9b960f6baa5032ba5e967d3a0dc0cda830be20745be58c"}, - {file = "polars-0.18.15-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c713610c7c144f41987092e2ab2372937933fbdc494a65c08eea251af91b60f"}, - {file = "polars-0.18.15-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c48d248891cfe62ee58f852dabf54c6bd85d4fc83b3b61b759534be0d3b6ec81"}, - {file = "polars-0.18.15-cp38-abi3-win_amd64.whl", hash = "sha256:a2f1e3ad546b98601d06340606e90a8788c35e064b4d82d27301069ce744086e"}, - {file = "polars-0.18.15.tar.gz", hash = "sha256:0c483fc1cfb25d07443c0d51eff9a18b94ccdbf6b252212767524412667ca870"}, -] - -[package.extras] -adbc = ["adbc_driver_sqlite"] -all = ["polars[adbc,cloudpickle,connectorx,deltalake,fsspec,matplotlib,numpy,pandas,pyarrow,pydantic,sqlalchemy,timezone,xlsx2csv,xlsxwriter]"] -cloudpickle = ["cloudpickle"] -connectorx = ["connectorx"] -deltalake = ["deltalake (>=0.10.0)"] -fsspec = ["fsspec"] -matplotlib = ["matplotlib"] -numpy = ["numpy (>=1.16.0)"] -pandas = ["pandas", "pyarrow (>=7.0.0)"] -pyarrow = ["pyarrow (>=7.0.0)"] -pydantic = ["pydantic"] -sqlalchemy = ["pandas", "sqlalchemy"] -timezone = ["backports.zoneinfo", "tzdata"] -xlsx2csv = ["xlsx2csv (>=0.8.0)"] -xlsxwriter = ["xlsxwriter"] - [[package]] name = "prompt-toolkit" version = "3.0.39" @@ -1766,43 +1657,6 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "pyarrow" -version = "12.0.1" -description = "Python library for Apache Arrow" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyarrow-12.0.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d288029a94a9bb5407ceebdd7110ba398a00412c5b0155ee9813a40d246c5df"}, - {file = "pyarrow-12.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345e1828efdbd9aa4d4de7d5676778aba384a2c3add896d995b23d368e60e5af"}, - {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d6009fdf8986332b2169314da482baed47ac053311c8934ac6651e614deacd6"}, - {file = "pyarrow-12.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d3c4cbbf81e6dd23fe921bc91dc4619ea3b79bc58ef10bce0f49bdafb103daf"}, - {file = "pyarrow-12.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdacf515ec276709ac8042c7d9bd5be83b4f5f39c6c037a17a60d7ebfd92c890"}, - {file = "pyarrow-12.0.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:749be7fd2ff260683f9cc739cb862fb11be376de965a2a8ccbf2693b098db6c7"}, - {file = "pyarrow-12.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6895b5fb74289d055c43db3af0de6e16b07586c45763cb5e558d38b86a91e3a7"}, - {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1887bdae17ec3b4c046fcf19951e71b6a619f39fa674f9881216173566c8f718"}, - {file = "pyarrow-12.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c9cb8eeabbadf5fcfc3d1ddea616c7ce893db2ce4dcef0ac13b099ad7ca082"}, - {file = "pyarrow-12.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4aebdf412bd0eeb800d8e47db854f9f9f7e2f5a0220440acf219ddfddd4f63"}, - {file = "pyarrow-12.0.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:e0d8730c7f6e893f6db5d5b86eda42c0a130842d101992b581e2138e4d5663d3"}, - {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43364daec02f69fec89d2315f7fbfbeec956e0d991cbbef471681bd77875c40f"}, - {file = "pyarrow-12.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051f9f5ccf585f12d7de836e50965b3c235542cc896959320d9776ab93f3b33d"}, - {file = "pyarrow-12.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:be2757e9275875d2a9c6e6052ac7957fbbfc7bc7370e4a036a9b893e96fedaba"}, - {file = "pyarrow-12.0.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:cf812306d66f40f69e684300f7af5111c11f6e0d89d6b733e05a3de44961529d"}, - {file = "pyarrow-12.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:459a1c0ed2d68671188b2118c63bac91eaef6fc150c77ddd8a583e3c795737bf"}, - {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85e705e33eaf666bbe508a16fd5ba27ca061e177916b7a317ba5a51bee43384c"}, - {file = "pyarrow-12.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9120c3eb2b1f6f516a3b7a9714ed860882d9ef98c4b17edcdc91d95b7528db60"}, - {file = "pyarrow-12.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c780f4dc40460015d80fcd6a6140de80b615349ed68ef9adb653fe351778c9b3"}, - {file = "pyarrow-12.0.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a3c63124fc26bf5f95f508f5d04e1ece8cc23a8b0af2a1e6ab2b1ec3fdc91b24"}, - {file = "pyarrow-12.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b13329f79fa4472324f8d32dc1b1216616d09bd1e77cfb13104dec5463632c36"}, - {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb656150d3d12ec1396f6dde542db1675a95c0cc8366d507347b0beed96e87ca"}, - {file = "pyarrow-12.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6251e38470da97a5b2e00de5c6a049149f7b2bd62f12fa5dbb9ac674119ba71a"}, - {file = "pyarrow-12.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:3de26da901216149ce086920547dfff5cd22818c9eab67ebc41e863a5883bac7"}, - {file = "pyarrow-12.0.1.tar.gz", hash = "sha256:cce317fc96e5b71107bf1f9f184d5e54e2bd14bbf3f9a3d62819961f0af86fec"}, -] - -[package.dependencies] -numpy = ">=1.16.6" - [[package]] name = "pycparser" version = "2.21" @@ -2470,51 +2324,6 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] -[[package]] -name = "statsmodels" -version = "0.14.0" -description = "Statistical computations and models for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "statsmodels-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:16bfe0c96a53b20fa19067e3b6bd2f1d39e30d4891ea0d7bc20734a0ae95942d"}, - {file = "statsmodels-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a6a0a1a06ff79be8aa89c8494b33903442859add133f0dda1daf37c3c71682e"}, - {file = "statsmodels-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77b3cd3a5268ef966a0a08582c591bd29c09c88b4566c892a7c087935234f285"}, - {file = "statsmodels-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c64ebe9cf376cba0c31aed138e15ed179a1d128612dd241cdf299d159e5e882"}, - {file = "statsmodels-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb471f757fc45102a87e5d86e87dc2c8c78b34ad4f203679a46520f1d863b9da"}, - {file = "statsmodels-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:582f9e41092e342aaa04920d17cc3f97240e3ee198672f194719b5a3d08657d6"}, - {file = "statsmodels-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ebe885ccaa64b4bc5ad49ac781c246e7a594b491f08ab4cfd5aa456c363a6f6"}, - {file = "statsmodels-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b587ee5d23369a0e881da6e37f78371dce4238cf7638a455db4b633a1a1c62d6"}, - {file = "statsmodels-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef7fa4813c7a73b0d8a0c830250f021c102c71c95e9fe0d6877bcfb56d38b8c"}, - {file = "statsmodels-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:a6ad7b8aadccd4e4dd7f315a07bef1bca41d194eeaf4ec600d20dea02d242fce"}, - {file = "statsmodels-0.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3757542c95247e4ab025291a740efa5da91dc11a05990c033d40fce31c450dc9"}, - {file = "statsmodels-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de489e3ed315bdba55c9d1554a2e89faa65d212e365ab81bc323fa52681fc60e"}, - {file = "statsmodels-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e290f4718177bffa8823a780f3b882d56dd64ad1c18cfb4bc8b5558f3f5757"}, - {file = "statsmodels-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71054f9dbcead56def14e3c9db6f66f943110fdfb19713caf0eb0f08c1ec03fd"}, - {file = "statsmodels-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:d7fda067837df94e0a614d93d3a38fb6868958d37f7f50afe2a534524f2660cb"}, - {file = "statsmodels-0.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c7724ad573af26139a98393ae64bc318d1b19762b13442d96c7a3e793f495c3"}, - {file = "statsmodels-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b0a135f3bfdeec987e36e3b3b4c53e0bb87a8d91464d2fcc4d169d176f46fdb"}, - {file = "statsmodels-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce28eb1c397dba437ec39b9ab18f2101806f388c7a0cf9cdfd8f09294ad1c799"}, - {file = "statsmodels-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b1c768dd94cc5ba8398121a632b673c625491aa7ed627b82cb4c880a25563f"}, - {file = "statsmodels-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d1e3e10dfbfcd58119ba5a4d3c7d519182b970a2aebaf0b6f539f55ae16058d"}, - {file = "statsmodels-0.14.0.tar.gz", hash = "sha256:6875c7d689e966d948f15eb816ab5616f4928706b180cf470fd5907ab6f647a4"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.18", markers = "python_version != \"3.10\" or platform_system != \"Windows\" or platform_python_implementation == \"PyPy\""}, - {version = ">=1.22.3", markers = "python_version == \"3.10\" and platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""}, -] -packaging = ">=21.3" -pandas = ">=1.0" -patsy = ">=0.5.2" -scipy = ">=1.4,<1.9.2 || >1.9.2" - -[package.extras] -build = ["cython (>=0.29.26)"] -develop = ["colorama", "cython (>=0.29.26)", "cython (>=0.29.28,<3.0.0)", "flake8", "isort", "joblib", "matplotlib (>=3)", "oldest-supported-numpy (>=2022.4.18)", "pytest (>=7.0.1,<7.1.0)", "pytest-randomly", "pytest-xdist", "pywinpty", "setuptools-scm[toml] (>=7.0.0,<7.1.0)"] -docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"] - [[package]] name = "toml" version = "0.10.2" @@ -2583,17 +2392,6 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] -[[package]] -name = "tzdata" -version = "2023.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, -] - [[package]] name = "urllib3" version = "2.0.6" @@ -2664,4 +2462,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10, <3.12" -content-hash = "911cb80c4921da00530fd4d984fab2c7c6573c9a30a204f95cc8ab5e20733245" +content-hash = "6ac7d26219596b27d8eb7081dee641ecf3302612dd2e03fa7fe727986b17d145" diff --git a/pyproject.toml b/pyproject.toml index 75067445c..7fbbaf8fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,6 @@ numba = "^0.56.4" mediapipe = "0.10.1" pyside6 = "^6.5.2" pyqtdarktheme = "^2.1.0" -polars = "^0.18.11" -plotnine = "^0.12.2" -pyarrow = "^12.0.1" opencv-contrib-python = "^4.8.0.74" [tool.poetry.group.dev.dependencies] diff --git a/pyxy3d/__main__.py b/pyxy3d/__main__.py index 58ded531a..591aa9fcd 100644 --- a/pyxy3d/__main__.py +++ b/pyxy3d/__main__.py @@ -8,7 +8,7 @@ from pathlib import Path from pyxy3d.gui.recording_widget import launch_recording_widget -from pyxy3d.gui.single_main_widget import launch_main +from pyxy3d.gui.main_widget import launch_main def CLI_parser(): if len(sys.argv) == 1: diff --git a/pyxy3d/calibration/capture_volume/quality_controller.py b/pyxy3d/calibration/capture_volume/quality_controller.py index 8dbab8609..9f019af1b 100644 --- a/pyxy3d/calibration/capture_volume/quality_controller.py +++ b/pyxy3d/calibration/capture_volume/quality_controller.py @@ -350,7 +350,7 @@ def cartesian_product(*arrays): if __name__ == "__main__": # if True: - from pyxy3d.session.session import Session + from pyxy3d.session.session import LiveSession from pyxy3d import __root__ session_directory = Path(__root__, "tests", "217") @@ -362,7 +362,7 @@ def cartesian_product(*arrays): # charuco = get_charuco(config_path) # create QualityControl - session = Session(session_directory) + session = LiveSession(session_directory) session.load_estimated_capture_volume() quality_controller = QualityController(session.capture_volume) diff --git a/pyxy3d/calibration/capture_volume/set_origin_functions.py b/pyxy3d/calibration/capture_volume/set_origin_functions.py index bb24cd6d6..5235c198c 100644 --- a/pyxy3d/calibration/capture_volume/set_origin_functions.py +++ b/pyxy3d/calibration/capture_volume/set_origin_functions.py @@ -236,7 +236,7 @@ def get_board_origin_transform( if __name__ == "__main__": # - from pyxy3d.session.session import Session + from pyxy3d.session.session import LiveSession from pyxy3d.cameras.camera_array_initializer import CameraArrayInitializer from pyxy3d.calibration.capture_volume.capture_volume import CaptureVolume from pyxy3d.gui.vizualize.calibration.capture_volume_visualizer import ( @@ -270,7 +270,7 @@ def get_board_origin_transform( config_path = Path(session_directory, "config.toml") # need to get the charuco board that was used during the session for later - session = Session(session_directory) + session = LiveSession(session_directory) tracker = session.charuco REOPTIMIZE_CAPTURE_VOLUME = True diff --git a/pyxy3d/calibration/intrinsic_calibrator.py b/pyxy3d/calibration/intrinsic_calibrator.py new file mode 100644 index 000000000..01b7fb4b8 --- /dev/null +++ b/pyxy3d/calibration/intrinsic_calibrator.py @@ -0,0 +1,14 @@ +import pyxy3d.logger +from pyxy3d.recording.recorded_stream import RecordedStream +from pyxy3d.trackers.charuco_tracker import CharucoTracker + +logger = pyxy3d.logger.get(__name__) + + +class PreRecordedIntrinsicCalibrator: + """ + Takes a recorded stream and determines a CameraData object from it + Stream needs to have a charuco tracker assigned to it + """ + def __init__(self, stream: RecordedStream) -> None: + pass \ No newline at end of file diff --git a/pyxy3d/calibration/monocalibrator.py b/pyxy3d/calibration/monocalibrator.py index 5a116aaf8..af67c474e 100644 --- a/pyxy3d/calibration/monocalibrator.py +++ b/pyxy3d/calibration/monocalibrator.py @@ -1,8 +1,4 @@ - import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) -# import logging -# logger.setLevel(logging.DEBUG) import time from queue import Queue from threading import Thread, Event @@ -17,31 +13,26 @@ from pyxy3d.interface import FramePacket from pyxy3d.cameras.live_stream import LiveStream -class MonoCalibrator(): +logger = pyxy3d.logger.get(__name__) - def __init__( - self, stream:LiveStream, board_threshold=0.7, wait_time=0.5, fps = 6 - ): + +class MonoCalibrator: + def __init__(self, stream: LiveStream, board_threshold=0.7, wait_time=0.5, fps=6): self.stream = stream self.camera: Camera = stream.camera # reference needed to update params self.port = self.camera.port self.board_threshold = board_threshold - - # # this is strange but is done to allow the GUI an easy way to restore stream fps when switching between monocal/synchronizer - # # self.fps = fps # monocalibrator should not store fps...it's a stream prop. - # self.set_stream_fps(fps) self.wait_time = wait_time self.capture_corners = Event() - self.capture_corners.clear() # start out not doing anything + self.capture_corners.clear() # start out not doing anything self.stop_event = Event() - - self.frame_packet_in_q = Queue(-1) + + self.frame_packet_in_q = Queue(-1) self.subscribe_to_stream() self.grid_frame_ready_q = Queue() - self.initialize_grid_history() self.last_calibration_time = ( @@ -53,14 +44,6 @@ def __init__( logger.info(f"Beginning monocalibrator for port {self.port}") - # def set_stream_fps(self, fps_target:int=None): - # """ - # If new target, update monocal property, otherwise revert to previously stored - # """ - # if fps_target is not None: - # self.fps = fps_target - # self.stream.set_fps_target(self.fps) - @property def grid_count(self): """How many sets of corners have been collected up to this point""" @@ -81,17 +64,16 @@ def initialize_grid_history(self): self.all_ids = [] self.all_img_loc = [] self.all_obj_loc = [] - + def stop(self): self.stop_event.set() self.thread.join() def subscribe_to_stream(self): self.stream.subscribe(self.frame_packet_in_q) - - def unsubscribe_to_stream(self): + + def unsubscribe_to_stream(self): self.stream.unsubscribe(self.frame_packet_in_q) - def collect_corners(self): """ @@ -108,9 +90,8 @@ def collect_corners(self): self.connected_points = self.stream.tracker.get_connected_points() board_corner_count = len(self.stream.tracker.board.getChessboardCorners()) self.min_points_to_process = int(board_corner_count * self.board_threshold) - + while not self.stop_event.is_set(): - self.frame_packet: FramePacket = self.frame_packet_in_q.get() self.frame = self.frame_packet.frame @@ -129,7 +110,9 @@ def collect_corners(self): ) if enough_corners and enough_time_from_last_cal: - logger.debug(f"Points found and being processed for port {self.port}") + logger.debug( + f"Points found and being processed for port {self.port}" + ) # store the corners and IDs self.all_ids.append(self.ids) @@ -139,15 +122,16 @@ def collect_corners(self): self.last_calibration_time = time.perf_counter() self.update_grid_history() else: - logger.debug(f"No points collected for processing at port {self.port}") - - + logger.debug( + f"No points collected for processing at port {self.port}" + ) + if self.frame_packet.frame is not None: self.set_grid_frame() logger.info(f"Monocalibrator at port {self.port} successfully shutdown...") self.stream.push_to_out_q.clear() - + def update_grid_history(self): if len(self.ids) > 2: self.grid_capture_history = draw_charuco.grid_history( @@ -157,15 +141,12 @@ def update_grid_history(self): self.connected_points, ) - def set_grid_frame(self): - """Merges the current frame with the currently detected corners (red circles) + """Merges the current frame with the currently detected corners (red circles) and a history of the stored grid information.""" logger.debug(f"Frame Size is {self.frame.shape} at port {self.port}") - logger.debug( - f"camera resolution is {self.camera.size} at port {self.port}" - ) + logger.debug(f"camera resolution is {self.camera.size} at port {self.port}") # check to see if the camera resolution changed from the last round if ( @@ -173,7 +154,9 @@ def set_grid_frame(self): and self.frame.shape[1] == self.grid_capture_history.shape[1] ): self.grid_frame = self.frame_packet.frame_with_points - self.grid_frame = cv2.addWeighted(self.grid_frame, 1, self.grid_capture_history, 1, 0) + self.grid_frame = cv2.addWeighted( + self.grid_frame, 1, self.grid_capture_history, 1, 0 + ) self.grid_frame_ready_q.put("frame ready") @@ -221,7 +204,6 @@ def update_camera(self): if __name__ == "__main__": - from pyxy3d.cameras.camera import Camera from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d.cameras.live_stream import LiveStream @@ -231,7 +213,6 @@ def update_camera(self): ) charuco_tracker = CharucoTracker(charuco) - test_port = 0 cam = Camera(0) stream = LiveStream(cam, tracker=charuco_tracker) @@ -241,7 +222,7 @@ def update_camera(self): monocal = MonoCalibrator(stream) monocal.capture_corners.set() - + print("About to enter main loop") while True: # read_success, frame = cam.capture.read() @@ -263,9 +244,9 @@ def update_camera(self): monocal.stream.track_points.clear() else: monocal.stream.track_points.set() - + if key == ord("v"): - stream.change_resolution((1280,720)) + stream.change_resolution((1280, 720)) monocal.calibrate() monocal.update_camera() diff --git a/pyxy3d/cameras/synchronizer.py b/pyxy3d/cameras/synchronizer.py index 21e6fca1b..5bd34e15c 100644 --- a/pyxy3d/cameras/synchronizer.py +++ b/pyxy3d/cameras/synchronizer.py @@ -208,7 +208,7 @@ def average_fps(self): def synch_frames_worker(self): - logger.info(f"Waiting for all ports to begin harvesting corners...") + logger.info("Waiting for all ports to begin harvesting corners...") sync_index = 0 @@ -287,7 +287,7 @@ def synch_frames_worker(self): if self.current_sync_packet.sync_index % 100 == 0: logger.info(f"Placing new synched frames with index {self.current_sync_packet.sync_index}") else: - logger.info(f"signaling end of frames with `None` packet on subscriber queue.") + logger.info("signaling end of frames with `None` packet on subscriber queue.") for port, q in self.frame_packet_queues.items(): logger.info(f"Currently {q.qsize()} frame packets unprocessed for port {port}") diff --git a/pyxy3d/gui/calibrate_capture_volume_widget.py b/pyxy3d/gui/calibrate_capture_volume_widget.py index 9114d9329..8a37dd184 100644 --- a/pyxy3d/gui/calibrate_capture_volume_widget.py +++ b/pyxy3d/gui/calibrate_capture_volume_widget.py @@ -1,6 +1,5 @@ import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) import os import sys @@ -20,9 +19,9 @@ QStackedWidget, ) -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode from pyxy3d.gui.charuco_widget import CharucoWidget -from pyxy3d.gui.camera_config.intrinsic_calibration_widget import IntrinsicCalibrationWidget +from pyxy3d.gui.live_camera_config.intrinsic_calibration_widget import IntrinsicCalibrationWidget from pyxy3d import __root__, __app_dir__ from pyxy3d.trackers.charuco_tracker import CharucoTracker # from pyxy3d.gui.qt_logger import QtLogger @@ -33,6 +32,7 @@ from pyxy3d.gui.vizualize.calibration.capture_volume_widget import CaptureVolumeWidget from pyxy3d.configurator import Configurator +logger = pyxy3d.logger.get(__name__) class CalibrateCaptureVolumeWidget(QStackedWidget): """ @@ -41,7 +41,7 @@ class CalibrateCaptureVolumeWidget(QStackedWidget): """ cameras_connected = Signal() - def __init__(self, session:Session): + def __init__(self, session:LiveSession): super().__init__() self.CAMS_IN_PROCESS = False diff --git a/pyxy3d/gui/charuco_widget.py b/pyxy3d/gui/charuco_widget.py index 55d27899e..f90d98595 100644 --- a/pyxy3d/gui/charuco_widget.py +++ b/pyxy3d/gui/charuco_widget.py @@ -1,6 +1,5 @@ import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) import sys from pathlib import Path @@ -28,10 +27,11 @@ from pyxy3d import __app_dir__ from pyxy3d.calibration.charuco import ARUCO_DICTIONARIES, Charuco -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.navigation_bars import NavigationBarNext +logger = pyxy3d.logger.get(__name__) class CharucoWidget(QWidget): def __init__(self, session): super().__init__() @@ -282,7 +282,7 @@ def update_charuco(self): recent_project_count = len(recent_projects) session_path = Path(recent_projects[recent_project_count - 1]) config = Configurator(session_path) - session = Session(config) + session = LiveSession(config) app = QApplication(sys.argv) diff --git a/pyxy3d/gui/extrinsic_calibration_widget.py b/pyxy3d/gui/extrinsic_calibration_widget.py index 1552d8c0d..b06388622 100644 --- a/pyxy3d/gui/extrinsic_calibration_widget.py +++ b/pyxy3d/gui/extrinsic_calibration_widget.py @@ -30,7 +30,7 @@ ) # Append main repo to top of path to allow import of backend -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.frame_builders.paired_frame_builder import PairedFrameBuilder from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d import __root__ @@ -50,7 +50,7 @@ class ExtrinsicCalibrationWidget(QWidget): calibration_initiated = Signal() terminate = Signal() - def __init__(self,session:Session): + def __init__(self,session:LiveSession): super(ExtrinsicCalibrationWidget, self).__init__() self.session = session @@ -289,7 +289,7 @@ def cv2_to_qlabel(frame): session_path = Path(__root__, "dev","sample_sessions", "257") configurator = Configurator(session_path) - session = Session(configurator) + session = LiveSession(configurator) # session.load_cameras() tracker = CharucoTracker(session.charuco) session.load_stream_tools(tracker=tracker) diff --git a/pyxy3d/gui/camera_config/camera_config_dialogue.py b/pyxy3d/gui/live_camera_config/camera_config_dialogue.py similarity index 96% rename from pyxy3d/gui/camera_config/camera_config_dialogue.py rename to pyxy3d/gui/live_camera_config/camera_config_dialogue.py index 687f404be..65e8396dd 100644 --- a/pyxy3d/gui/camera_config/camera_config_dialogue.py +++ b/pyxy3d/gui/live_camera_config/camera_config_dialogue.py @@ -1,15 +1,10 @@ import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) - -import sys from pathlib import Path from threading import Thread -import time -import cv2 from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QImage, QPixmap, QIcon +from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QApplication, QWidget, @@ -27,14 +22,16 @@ ) # Append main repo to top of path to allow import of backend -from pyxy3d.gui.camera_config.frame_emitter import FrameEmitter +from pyxy3d.gui.live_camera_config.frame_emitter import FrameEmitter from pyxy3d.calibration.monocalibrator import MonoCalibrator from pyxy3d.cameras.camera import Camera from pyxy3d.cameras.live_stream import LiveStream -from pyxy3d.session.session import Session -from pyxy3d.gui.camera_config.camera_summary_widget import SummaryWidget +from pyxy3d.session.session import LiveSession +from pyxy3d.gui.live_camera_config.camera_summary_widget import SummaryWidget from pyxy3d import __root__ +logger = pyxy3d.logger.get(__name__) + class CameraConfigTab(QDialog): def __init__(self, session, port): @@ -123,10 +120,10 @@ class CalibrationControls(QGroupBox): signal_calibration_lock = Signal(bool) calibration_change = Signal() - def __init__(self, session: Session, port, frame_emitter: FrameEmitter): + def __init__(self, session: LiveSession, port, frame_emitter: FrameEmitter): super(CalibrationControls, self).__init__("Calibration Summary") - self.session: Session = session + self.session: LiveSession = session self.port = port self.monocal: MonoCalibrator = self.session.monocalibrators[port] self.stream: LiveStream = self.monocal.stream @@ -235,9 +232,9 @@ def update_camera_data(self): class AdvancedControls(QWidget): - def __init__(self, session: Session, port, frame_emitter: FrameEmitter): + def __init__(self, session: LiveSession, port, frame_emitter: FrameEmitter): super(AdvancedControls, self).__init__() - self.session: Session = session + self.session: LiveSession = session self.port = port self.monocal: MonoCalibrator = self.session.monocalibrators[port] self.stream: LiveStream = self.monocal.stream @@ -317,9 +314,9 @@ def FPSUpdateSlot(self, fps): class FrameControlWidget(QWidget): - def __init__(self, session: Session, port, frame_emitter: FrameEmitter): + def __init__(self, session: LiveSession, port, frame_emitter: FrameEmitter): super(FrameControlWidget, self).__init__() - self.session: Session = session + self.session: LiveSession = session self.monocal: MonoCalibrator = session.monocalibrators[port] self.port = port self.camera: Camera = self.monocal.stream.camera @@ -421,7 +418,6 @@ def save_camera(self): # to the signal more straightforward self.session.config.save_camera(self.camera) - def update_exposure(self, exp): self.monocal.camera.exposure = exp self.exposure_number.setText(str(exp)) @@ -452,4 +448,3 @@ def change_res_worker(new_res): daemon=True, ) self.change_res_thread.start() - diff --git a/pyxy3d/gui/camera_config/camera_summary_widget.py b/pyxy3d/gui/live_camera_config/camera_summary_widget.py similarity index 95% rename from pyxy3d/gui/camera_config/camera_summary_widget.py rename to pyxy3d/gui/live_camera_config/camera_summary_widget.py index 26bc06cc7..1dceb0d77 100644 --- a/pyxy3d/gui/camera_config/camera_summary_widget.py +++ b/pyxy3d/gui/live_camera_config/camera_summary_widget.py @@ -19,11 +19,6 @@ def __init__(self, camera:Camera, parent=None): self.setLayout(QVBoxLayout()) self.place_widgets() - # def clear_layout(self): - # while self.layout().count(): - # child = self.layout().takeAt(0) - # if child.widget(): - # child.widget().deleteLater() def place_widgets(self): # self.clear_layout() @@ -123,7 +118,7 @@ def place_widgets(self): from time import sleep from pyxy3d import __root__ from pathlib import Path - from pyxy3d.session.session import Session + from pyxy3d.session.session import LiveSession import toml from pyxy3d import __app_dir__ @@ -133,7 +128,7 @@ def place_widgets(self): recent_project_count = len(recent_projects) session_path = Path(recent_projects[recent_project_count - 1]) config = Configurator(session_path) - session = Session(config) + session = LiveSession(config) session.load_stream_tools() port = 2 diff --git a/pyxy3d/gui/camera_config/frame_emitter.py b/pyxy3d/gui/live_camera_config/frame_emitter.py similarity index 100% rename from pyxy3d/gui/camera_config/frame_emitter.py rename to pyxy3d/gui/live_camera_config/frame_emitter.py diff --git a/pyxy3d/gui/camera_config/intrinsic_calibration_widget.py b/pyxy3d/gui/live_camera_config/intrinsic_calibration_widget.py similarity index 94% rename from pyxy3d/gui/camera_config/intrinsic_calibration_widget.py rename to pyxy3d/gui/live_camera_config/intrinsic_calibration_widget.py index 4e889d011..b1723da18 100644 --- a/pyxy3d/gui/camera_config/intrinsic_calibration_widget.py +++ b/pyxy3d/gui/live_camera_config/intrinsic_calibration_widget.py @@ -13,15 +13,15 @@ QTabWidget, ) -from pyxy3d.gui.camera_config.camera_config_dialogue import CameraConfigTab -from pyxy3d.session.session import Session +from pyxy3d.gui.live_camera_config.camera_config_dialogue import CameraConfigTab +from pyxy3d.session.session import LiveSession from pyxy3d.gui.navigation_bars import NavigationBarBackNext class IntrinsicCalibrationWidget(QWidget): """ This is basically just the camera tabs plus the navigation bar """ - def __init__(self, session: Session): + def __init__(self, session: LiveSession): super(IntrinsicCalibrationWidget, self).__init__() self.setLayout(QVBoxLayout()) self.camera_tabs = CameraTabs(session) @@ -33,7 +33,7 @@ class CameraTabs(QTabWidget): stereoframe_ready = Signal(bool) - def __init__(self, session: Session): + def __init__(self, session: LiveSession): super(CameraTabs, self).__init__() self.session = session diff --git a/pyxy3d/gui/log_widget.py b/pyxy3d/gui/log_widget.py index 758c687c2..49bd080db 100644 --- a/pyxy3d/gui/log_widget.py +++ b/pyxy3d/gui/log_widget.py @@ -13,7 +13,7 @@ QVBoxLayout, ) -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pathlib import Path from threading import Thread from time import time diff --git a/pyxy3d/gui/main_widget.py b/pyxy3d/gui/main_widget.py index 7805f23f1..b408cd0b2 100644 --- a/pyxy3d/gui/main_widget.py +++ b/pyxy3d/gui/main_widget.py @@ -1,48 +1,34 @@ import pyxy3d.logger -import pyxy3d.logger - -logger = pyxy3d.logger.get(__name__) +from pathlib import Path -from PySide6.QtWidgets import QMainWindow, QStackedLayout, QFileDialog -logger = pyxy3d.logger.get(__name__) -from pathlib import Path +from PySide6.QtWidgets import QMainWindow, QFileDialog from threading import Thread import sys from PySide6.QtWidgets import ( QApplication, QMainWindow, - QStackedLayout, QWidget, QDockWidget, - QVBoxLayout, QMenu, - QMenuBar, - QTabWidget, ) import toml -from enum import Enum -from PySide6.QtGui import QIcon, QAction, QKeySequence, QShortcut +from PySide6.QtGui import QIcon, QAction from PySide6.QtCore import Qt -from pyxy3d import __root__, __settings_path__, __user_dir__ -from pyxy3d.session.session import Session, SessionMode +from pyxy3d import __root__, __settings_path__ +from pyxy3d.session.session import LiveSession, SessionMode from pyxy3d.gui.log_widget import LogWidget from pyxy3d.configurator import Configurator from pyxy3d.gui.charuco_widget import CharucoWidget -from pyxy3d.gui.camera_config.intrinsic_calibration_widget import ( +from pyxy3d.gui.live_camera_config.intrinsic_calibration_widget import ( IntrinsicCalibrationWidget, ) -from pyxy3d.gui.calibrate_capture_volume_widget import CalibrateCaptureVolumeWidget from pyxy3d.gui.recording_widget import RecordingWidget from pyxy3d.gui.post_processing_widget import PostProcessingWidget +from pyxy3d.gui.extrinsic_calibration_widget import ExtrinsicCalibrationWidget +from pyxy3d.gui.vizualize.calibration.capture_volume_widget import CaptureVolumeWidget - -class TabIndex(Enum): - Charuco = 0 - Cameras = 1 - CaptureVolume = 2 - Recording = 3 - Processing = 4 +logger = pyxy3d.logger.get(__name__) class MainWindow(QMainWindow): @@ -57,11 +43,10 @@ def __init__(self): # File Menu self.menu = self.menuBar() - self.file_menu = self.menu.addMenu("File") - # Open or New project (can just create a folder in the dialog in truly new) + # CREATE FILE MENU + self.file_menu = self.menu.addMenu("&File") self.open_project_action = QAction("New/Open Project", self) - self.open_project_action.triggered.connect(self.create_new_project_folder) self.file_menu.addAction(self.open_project_action) # Open Recent @@ -76,21 +61,37 @@ def __init__(self): self.exit_pyxy3d_action = QAction("Exit", self) self.file_menu.addAction(self.exit_pyxy3d_action) - self.cameras_menu = self.menu.addMenu("Cameras") - self.connect_cameras_action = QAction("Connect Cameras", self) + # CREATE CAMERA MENU + self.cameras_menu = self.menu.addMenu("&Cameras") + self.connect_cameras_action = QAction("Co&nnect Cameras", self) self.cameras_menu.addAction(self.connect_cameras_action) self.connect_cameras_action.setEnabled(False) - self.disconnect_cameras_action = QAction("Disconnect Cameras", self) + self.disconnect_cameras_action = QAction("&Disconnect Cameras", self) self.cameras_menu.addAction(self.disconnect_cameras_action) self.disconnect_cameras_action.setEnabled(False) - self.connect_menu_actions() + # CREATE MODE MENU + self.mode_menu = self.menu.addMenu("&Mode") + self.charuco_mode_select = QAction(SessionMode.Charuco.value) + self.intrinsic_mode_select = QAction(SessionMode.IntrinsicCalibration.value) + self.extrinsic_mode_select = QAction(SessionMode.ExtrinsicCalibration.value) + self.capture_volume_mode_select = QAction(SessionMode.CaptureVolumeOrigin.value) + self.recording_mode_select = QAction(SessionMode.Recording.value) + self.triangulate_mode_select = QAction(SessionMode.Triangulate.value) + self.mode_menu.addAction(self.charuco_mode_select) + self.mode_menu.addAction(self.intrinsic_mode_select) + self.mode_menu.addAction(self.extrinsic_mode_select) + self.mode_menu.addAction(self.capture_volume_mode_select) + self.mode_menu.addAction(self.recording_mode_select) + self.mode_menu.addAction(self.triangulate_mode_select) + + for action in self.mode_menu.actions(): + action.setEnabled(False) - # Set up layout (based on splitter) - self.tab_widget = QTabWidget() - # self.tab_widget = CentralTabWidget() - self.setCentralWidget(self.tab_widget) + self.connect_menu_actions() + self.blank_widget = QWidget() + self.setCentralWidget(self.blank_widget) # create log window which is fixed below main window self.docked_logger = QDockWidget("Log", self) @@ -101,214 +102,168 @@ def __init__(self): self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.docked_logger) - - def connect_menu_actions(self): + self.open_project_action.triggered.connect(self.create_new_project_folder) self.connect_cameras_action.triggered.connect(self.load_stream_tools) self.exit_pyxy3d_action.triggered.connect(QApplication.instance().quit) self.disconnect_cameras_action.triggered.connect(self.disconnect_cameras) - + + for action in self.mode_menu.actions(): + action.triggered.connect(self.mode_change_action) + + def mode_change_action(self): + action = self.sender() + + # create a reverse lookup dictionary to pull the mode enum that should be activated + SessionModeLookup = {mode.value: mode for mode in SessionMode} + mode = SessionModeLookup[action.text()] + logger.info(f"Attempting to set session mode to {mode.value}") + self.session.set_mode(mode) + logger.info(f"Successful change to {mode} Mode") + + def update_central_widget_mode(self): + """ + This will be triggered whenever the session successfully completes a mode change and emits + a signal to that effect. + """ + logger.info("Begin process of updating central widget") + + old_widget = self.centralWidget() + self.setCentralWidget(QWidget()) + old_widget.deleteLater() + + logger.info("Clearing events in emmitter threads to get them to wind down") + if type(old_widget) == RecordingWidget: + old_widget.thumbnail_emitter.keep_collecting.clear() + logger.info("Waiting for recording widget to wrap up") + old_widget.thumbnail_emitter.wait() + + if type(old_widget) == ExtrinsicCalibrationWidget: + old_widget.paired_frame_emitter.keep_collecting.clear() + logger.info("Waiting for extrinsic calibration widget to wrap up") + old_widget.paired_frame_emitter.wait() + + if type(old_widget) == IntrinsicCalibrationWidget: + for port, tab in old_widget.camera_tabs.tab_widgets.items(): + tab.frame_emitter.keep_collecting.clear() + + logger.info(f"Matching next tab to active session mode: {self.session.mode}") + # Create the new central widget based on the mode + match self.session.mode: + case SessionMode.Charuco: + new_widget = CharucoWidget(self.session) + case SessionMode.IntrinsicCalibration: + new_widget = IntrinsicCalibrationWidget(self.session) + case SessionMode.ExtrinsicCalibration: + logger.info("About to create extrinsic calibration widget") + new_widget = ExtrinsicCalibrationWidget(self.session) + case SessionMode.CaptureVolumeOrigin: + new_widget = CaptureVolumeWidget(self.session) + case SessionMode.Recording: + new_widget = RecordingWidget(self.session) + case SessionMode.Triangulate: + new_widget = PostProcessingWidget(self.session) + + self.setCentralWidget(new_widget) + + def switch_to_capture_volume(self): + """ + Once the extrinsic calibration is complete, the GUI should automatically switch over to the capture volume widget + """ + self.session.set_mode(SessionMode.CaptureVolumeOrigin) + + def update_enable_disable(self): + # note: if the cameras are connected,then you can peak + # into extrinsic/recording tabs, though cannot collect data + + # you can always look at a charuco board + self.charuco_mode_select.setEnabled(True) + + if self.session.is_camera_setup_eligible(): + self.intrinsic_mode_select.setEnabled(True) + self.extrinsic_mode_select.setEnabled(True) + self.recording_mode_select.setEnabled(True) + else: + self.intrinsic_mode_select.setEnabled(False) + self.extrinsic_mode_select.setEnabled(False) + self.recording_mode_select.setEnabled(False) + + if self.session.is_capture_volume_eligible(): + self.capture_volume_mode_select.setEnabled(True) + else: + self.capture_volume_mode_select.setEnabled(False) + + if self.session.is_triangulate_eligible(): + self.triangulate_mode_select.setEnabled(True) + else: + self.triangulate_mode_select.setEnabled(False) + def disconnect_cameras(self): - self.tab_widget.setCurrentWidget(self.charuco_widget) - self.tab_widget.setTabEnabled(TabIndex.Charuco.value,True) - self.tab_widget.setTabEnabled(TabIndex.Cameras.value,False) - self.tab_widget.setTabEnabled(TabIndex.CaptureVolume.value,False) - self.tab_widget.setTabEnabled(TabIndex.Recording.value,False) - self.tab_widget.setTabEnabled(TabIndex.Processing.value,True) - - self.camera_widget = QWidget() - self.calibrate_capture_volume_widget = QWidget() - self.recording_widget = QWidget() - self.session.set_mode(SessionMode.Charuco) - self.session.disconnect_cameras() + self.session.disconnect_cameras() self.disconnect_cameras_action.setEnabled(False) self.connect_cameras_action.setEnabled(True) + self.update_enable_disable() def pause_all_frame_reading(self): - logger.info("Pausing all frame reading at load of stream tools; should be on charuco tab right now") + logger.info( + "Pausing all frame reading at load of stream tools; should be on charuco tab right now" + ) self.session.pause_all_monocalibrators() - self.session.pause_synchronizer() + self.session.pause_synchronizer() def load_stream_tools(self): self.connect_cameras_action.setEnabled(False) self.disconnect_cameras_action.setEnabled(True) - self.session.qt_signaler.stream_tools_loaded_signal.connect(self.pause_all_frame_reading) + self.session.qt_signaler.stream_tools_loaded_signal.connect( + self.pause_all_frame_reading + ) self.thread = Thread( target=self.session.load_stream_tools, args=(), daemon=True ) self.thread.start() - - - def on_tab_changed(self, index): - logger.info(f"Switching main window to tab {index}") - match index: - case TabIndex.Charuco.value: - logger.info(f"Activating Charuco Widget") - # self.silence_extrinsic_cal_widget() - self.session.set_mode(SessionMode.Charuco) - case TabIndex.Cameras.value: - logger.info(f"Activating Camera Setup Widget") - # self.silence_extrinsic_cal_widget() - self.session.set_mode(SessionMode.IntrinsicCalibration) - case TabIndex.CaptureVolume.value: - logger.info(f"Activating Calibrate Capture Volume Widget") - - if self.session.is_capture_volume_eligible(): - logger.info(f"Session is eligible for setting of origin...activating capture volume origin widget") - self.calibrate_capture_volume_widget.activate_capture_volume_widget() - else: - logger.info(f"Session is not eligible for setting of origin...activating extrinsic calibration widget") - self.calibrate_capture_volume_widget.activate_extrinsic_calibration_widget() - - case TabIndex.Recording.value: - logger.info(f"Activate Recording Mode") - - try: - logger.info("Attempting to spin down the extrinsic calibration widget") - self.calibrate_capture_volume_widget.extrinsic_calibration_widget.shutdown_threads() - except: - logger.info("No extrinsic calibration calibration widget exists") - - self.session.set_mode(SessionMode.Recording) - case TabIndex.Processing.value: - logger.info(f"Activate Processing Mode") - # self.silence_extrinsic_cal_widget() - self.session.set_mode(SessionMode.PostProcessing) - # may have acquired new recordings - self.processing_widget.update_recording_folders() - def launch_session(self, path_to_folder: str): session_path = Path(path_to_folder) self.config = Configurator(session_path) logger.info(f"Launching session with config file stored in {session_path}") - self.session = Session(self.config) + self.session = LiveSession(self.config) # can always load charuco self.charuco_widget = CharucoWidget(self.session) + self.setCentralWidget(self.charuco_widget) - # launches without cameras connected, so just throw in placeholders - self.camera_widget = QWidget() - self.recording_widget = QWidget() - self.processing_widget = QWidget() - self.calibrate_capture_volume_widget = QWidget() - - self.tab_widget.addTab(self.charuco_widget, "Charuco") - self.tab_widget.addTab(self.camera_widget, "Cameras") - self.tab_widget.addTab(self.calibrate_capture_volume_widget, "CaptureVolume") - self.tab_widget.addTab(self.recording_widget, "Recording") - self.tab_widget.addTab(self.processing_widget, "Processing") - - # when tabs change, make sure session mode adjusts - self.tab_widget.currentChanged.connect(self.on_tab_changed) - - # Make sure file menu can allow camera connection action + # now connecting to cameras is an option self.connect_cameras_action.setEnabled(True) - - # based on session parameters, may be able to load more than the defualt tabs - # check on that now... - self.update_tabs() - - # might be able to do - old_index = self.tab_widget.currentIndex() - self.tab_widget.setCurrentIndex(old_index) + # but must exit and start over to launch a new session for now self.connect_session_signals() - def update_tabs(self): - """ - Tab updates occur primarily at 2 times: - 1. upon main window initiation when offline capacities - (capture volume and post-processing) may be available. - - 2. upon loading of stream tools when cameras/recording would be available - """ - - # can always modify charuco - self.tab_widget.setTabEnabled(TabIndex.Charuco.value, True) - - # if you are connected to comeras - if self.session.stream_tools_loaded: - # but haven't already loaded a non-placeholder widget - if type(self.camera_widget) != IntrinsicCalibrationWidget: - self.load_camera_widget() - - if type(self.recording_widget) != RecordingWidget: - self.load_recording_widget() - - if type(self.calibrate_capture_volume_widget) != CalibrateCaptureVolumeWidget: - self.load_capture_volume_widget() - - self.tab_widget.setTabEnabled(TabIndex.Cameras.value, True) - self.tab_widget.setTabEnabled(TabIndex.Recording.value, True) - self.tab_widget.setTabEnabled(TabIndex.CaptureVolume.value, True) - - - - else: - self.tab_widget.setTabEnabled(TabIndex.Cameras.value, False) - self.tab_widget.setTabEnabled(TabIndex.Recording.value, False) - self.tab_widget.setTabEnabled(TabIndex.CaptureVolume.value, False) - - # might be able to do post processing if recordings and calibration available - if self.session.is_post_processing_eligible(): - self.load_post_processing_widget() - self.tab_widget.setTabEnabled(TabIndex.Processing.value, True) - else: - self.tab_widget.setTabEnabled(TabIndex.Processing.value, False) - - + self.open_project_action.setEnabled(False) + self.open_recent_project_submenu.setEnabled(False) + self.update_enable_disable() def connect_session_signals(self): """ After launching a session, connect signals and slots. Much of these will be from the GUI to the session and vice-versa """ - self.session.qt_signaler.unlock_postprocessing.connect(self.load_post_processing_widget) - self.session.qt_signaler.stream_tools_loaded_signal.connect(self.update_tabs) - - def load_recording_widget(self): - # recording_index = self.tab_widget.indexOf(self.recording_widget) - self.tab_widget.removeTab(TabIndex.Recording.value) - self.recording_widget.deleteLater() - new_recording_widget = RecordingWidget(self.session) - self.tab_widget.insertTab( - TabIndex.Recording.value, new_recording_widget, TabIndex.Recording.name + self.session.qt_signaler.unlock_postprocessing.connect( + self.update_enable_disable ) - self.recording_widget = new_recording_widget - - def load_post_processing_widget(self): - self.tab_widget.removeTab(TabIndex.Processing.value) - self.processing_widget.deleteLater() - new_processing_widget = PostProcessingWidget(self.session) - self.tab_widget.insertTab( - TabIndex.Processing.value, new_processing_widget, TabIndex.Processing.name + self.session.qt_signaler.mode_change_success.connect( + self.update_central_widget_mode ) - self.processing_widget = new_processing_widget - - def load_capture_volume_widget(self): - self.tab_widget.removeTab(TabIndex.CaptureVolume.value) - self.calibrate_capture_volume_widget.deleteLater() - new_capture_volume_widget = CalibrateCaptureVolumeWidget(self.session) - self.tab_widget.insertTab( - TabIndex.CaptureVolume.value, - new_capture_volume_widget, - TabIndex.CaptureVolume.name, + self.session.qt_signaler.stream_tools_loaded_signal.connect( + self.update_enable_disable ) - self.calibrate_capture_volume_widget = new_capture_volume_widget - - def load_camera_widget(self): - self.tab_widget.removeTab(TabIndex.Cameras.value) - self.camera_widget.deleteLater() - new_camera_widget = IntrinsicCalibrationWidget(self.session) - self.tab_widget.insertTab( - TabIndex.Cameras.value, new_camera_widget, TabIndex.Cameras.name + self.session.qt_signaler.stream_tools_disconnected_signal.connect( + self.update_enable_disable + ) + self.session.qt_signaler.mode_change_success.connect(self.update_enable_disable) + self.session.qt_signaler.extrinsic_calibration_complete.connect( + self.switch_to_capture_volume ) - self.camera_widget = new_camera_widget - - # if fully calibrated, then make capture volume available - # self.camera_widget.camera_tabs.stereoframe_ready.connect(self.update_tabs) def add_to_recent_project(self, project_path: str): recent_project_action = QAction(project_path, self) @@ -327,7 +282,7 @@ def create_new_project_folder(self): path_to_folder = dialog.getExistingDirectory( parent=None, caption="Open Previous or Create New Project Directory", - directory=str(default_folder), + dir=str(default_folder), options=QFileDialog.Option.ShowDirsOnly, ) @@ -350,31 +305,13 @@ def update_app_settings(self): toml.dump(self.app_settings, f) -class CentralTabWidget(QTabWidget): - """ - Switching between tabs, particularly when system resource utilization is high, - is prone to result in segfault crashes. Working hypothesis is that this is due to mode - changes happening when the tab is changed and the GUI tries to render something it doesn't have - - This override slips the mode change between click and change to try to stabilize the mode switches. - - """ - - def __init__(self): - super(CentralTabWidget, self).__init__() - - def tabBarClicked(self, index): - # Emit a custom signal or perform any desired action before the tab changes - logger.info(f"Tab {index} clicked") - - # Uncomment the following line to allow the tab to change after the signal is emitted - super(CentralTabWidget, self).tabBarClicked(index) - def launch_main(): + import qdarktheme + app = QApplication(sys.argv) + qdarktheme.setup_theme("auto") window = MainWindow() window.show() - app.exec() diff --git a/pyxy3d/gui/post_processing_widget.py b/pyxy3d/gui/post_processing_widget.py index 4d7dc11ac..fcc6d45ff 100644 --- a/pyxy3d/gui/post_processing_widget.py +++ b/pyxy3d/gui/post_processing_widget.py @@ -39,7 +39,7 @@ from pyxy3d.post_processing.post_processor import PostProcessor from pyxy3d.post_processing.blender_tools import generate_metarig_config -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d import __root__ from pyxy3d.recording.video_recorder import VideoRecorder @@ -52,7 +52,7 @@ class PostProcessingWidget(QWidget): processing_complete = Signal() - def __init__(self, session:Session): + def __init__(self, session:LiveSession): super(PostProcessingWidget, self).__init__() self.session = session self.config = session.config diff --git a/pyxy3d/gui/prerecorded_calibration/video_playback.py b/pyxy3d/gui/prerecorded_calibration/video_playback.py new file mode 100644 index 000000000..990af922e --- /dev/null +++ b/pyxy3d/gui/prerecorded_calibration/video_playback.py @@ -0,0 +1,48 @@ +from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QFileDialog +from PySide6.QtMultimedia import QMediaPlayer +from PySide6.QtMultimediaWidgets import QVideoWidget + +class VideoPlayer(QWidget): + def __init__(self): + super().__init__() + + self.media_player = QMediaPlayer(self) + self.video_widget = QVideoWidget(self) + + self.play_button = QPushButton("Play", self) + self.play_button.clicked.connect(self.on_play) + + self.open_button = QPushButton("Open", self) + self.open_button.clicked.connect(self.on_open) + + self.layout = QVBoxLayout() + self.layout.addWidget(self.video_widget) + self.layout.addWidget(self.play_button) + self.layout.addWidget(self.open_button) + + self.setLayout(self.layout) + + def on_play(self): + if self.media_player.state() == QMediaPlayer.PlayingState: + self.media_player.pause() + self.play_button.setText("Play") + else: + self.media_player.play() + self.play_button.setText("Pause") + + def on_open(self): + file_dialog = QFileDialog(self) + file_dialog.setMimeTypeFilters(["video/mp4"]) + file_dialog.setViewMode(QFileDialog.List) + + if file_dialog.exec(): + video_file = file_dialog.selectedFiles()[0] + self.media_player.setMedia(video_file) + self.media_player.play() + self.play_button.setText("Pause") + +if __name__ == "__main__": + app = QApplication([]) + player = VideoPlayer() + player.show() + app.exec() diff --git a/pyxy3d/gui/recording_widget.py b/pyxy3d/gui/recording_widget.py index 234ccd5a6..6ea46225c 100644 --- a/pyxy3d/gui/recording_widget.py +++ b/pyxy3d/gui/recording_widget.py @@ -38,7 +38,7 @@ QVBoxLayout, ) -from pyxy3d.session.session import Session, SessionMode +from pyxy3d.session.session import LiveSession, SessionMode from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d import __root__ from pyxy3d.recording.video_recorder import VideoRecorder @@ -58,7 +58,7 @@ class NextRecordingActions(Enum): class RecordingWidget(QWidget): - def __init__(self,session:Session): + def __init__(self,session:LiveSession): super(RecordingWidget, self).__init__() self.session = session @@ -391,7 +391,7 @@ def cv2_to_qimage(frame): def launch_recording_widget(session_path): config = Configurator(session_path) - session = Session(config) + session = LiveSession(config) # session.load_stream_tools() # session._adjust_resolutions() session.set_mode(SessionMode.Recording) diff --git a/pyxy3d/gui/single_main_widget.py b/pyxy3d/gui/single_main_widget.py deleted file mode 100644 index b4d6ed3dd..000000000 --- a/pyxy3d/gui/single_main_widget.py +++ /dev/null @@ -1,318 +0,0 @@ -import pyxy3d.logger -from pathlib import Path -logger = pyxy3d.logger.get(__name__) - -from PySide6.QtWidgets import QMainWindow, QStackedLayout, QFileDialog -from threading import Thread -import sys -from PySide6.QtWidgets import ( - QApplication, - QMainWindow, - QStackedLayout, - QWidget, - QDockWidget, - QVBoxLayout, - QMenu, - QMenuBar, - QTabWidget, -) -import toml -from enum import Enum -from PySide6.QtGui import QIcon, QAction, QKeySequence, QShortcut -from PySide6.QtCore import Qt -from pyxy3d import __root__, __settings_path__, __user_dir__ -from pyxy3d.session.session import Session, SessionMode -from pyxy3d.gui.log_widget import LogWidget -from pyxy3d.configurator import Configurator -from pyxy3d.gui.charuco_widget import CharucoWidget -from pyxy3d.gui.camera_config.intrinsic_calibration_widget import ( - IntrinsicCalibrationWidget, -) -from pyxy3d.gui.calibrate_capture_volume_widget import CalibrateCaptureVolumeWidget -from pyxy3d.gui.recording_widget import RecordingWidget -from pyxy3d.gui.post_processing_widget import PostProcessingWidget -from pyxy3d.gui.extrinsic_calibration_widget import ExtrinsicCalibrationWidget -from pyxy3d.gui.vizualize.calibration.capture_volume_widget import CaptureVolumeWidget - - -class MainWindow(QMainWindow): - def __init__(self): - super(MainWindow, self).__init__() - - self.app_settings = toml.load(__settings_path__) - - self.setWindowTitle("Pyxy3D") - self.setWindowIcon(QIcon(str(Path(__root__, "pyxy3d/gui/icons/pyxy_logo.svg")))) - self.setMinimumSize(500, 500) - - # File Menu - self.menu = self.menuBar() - - # CREATE FILE MENU - self.file_menu = self.menu.addMenu("&File") - self.open_project_action = QAction("New/Open Project", self) - self.file_menu.addAction(self.open_project_action) - - # Open Recent - self.open_recent_project_submenu = QMenu("Recent Projects...", self) - # Populate the submenu with recent project paths; - # reverse so that last one appended is at the top of the list - for project_path in reversed(self.app_settings["recent_projects"]): - self.add_to_recent_project(project_path) - - self.file_menu.addMenu(self.open_recent_project_submenu) - - self.exit_pyxy3d_action = QAction("Exit", self) - self.file_menu.addAction(self.exit_pyxy3d_action) - - - # CREATE CAMERA MENU - self.cameras_menu = self.menu.addMenu("&Cameras") - self.connect_cameras_action = QAction("Co&nnect Cameras", self) - self.cameras_menu.addAction(self.connect_cameras_action) - self.connect_cameras_action.setEnabled(False) - - self.disconnect_cameras_action = QAction("&Disconnect Cameras", self) - self.cameras_menu.addAction(self.disconnect_cameras_action) - self.disconnect_cameras_action.setEnabled(False) - - # CREATE MODE MENU - self.mode_menu = self.menu.addMenu("&Mode") - self.charuco_mode_select = QAction(SessionMode.Charuco.value) - self.intrinsic_mode_select = QAction(SessionMode.IntrinsicCalibration.value) - self.extrinsic_mode_select = QAction(SessionMode.ExtrinsicCalibration.value) - self.capture_volume_mode_select = QAction(SessionMode.CaptureVolumeOrigin.value) - self.recording_mode_select = QAction(SessionMode.Recording.value) - self.processing_mode_select = QAction(SessionMode.PostProcessing.value) - self.mode_menu.addAction(self.charuco_mode_select) - self.mode_menu.addAction(self.intrinsic_mode_select) - self.mode_menu.addAction(self.extrinsic_mode_select) - self.mode_menu.addAction(self.capture_volume_mode_select) - self.mode_menu.addAction(self.recording_mode_select) - self.mode_menu.addAction(self.processing_mode_select) - - for action in self.mode_menu.actions(): - action.setEnabled(False) - - self.connect_menu_actions() - self.blank_widget = QWidget() - self.setCentralWidget(self.blank_widget) - - # create log window which is fixed below main window - self.docked_logger = QDockWidget("Log", self) - self.docked_logger.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable) - self.docked_logger.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea) - self.log_widget = LogWidget() - self.docked_logger.setWidget(self.log_widget) - - self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.docked_logger) - - def connect_menu_actions(self): - self.open_project_action.triggered.connect(self.create_new_project_folder) - self.connect_cameras_action.triggered.connect(self.load_stream_tools) - self.exit_pyxy3d_action.triggered.connect(QApplication.instance().quit) - self.disconnect_cameras_action.triggered.connect(self.disconnect_cameras) - - for action in self.mode_menu.actions(): - action.triggered.connect(self.mode_change_action) - - def mode_change_action(self): - action = self.sender() - - # create a reverse lookup dictionary to pull the mode enum that should be activated - SessionModeLookup = {mode.value: mode for mode in SessionMode} - mode = SessionModeLookup[action.text()] - logger.info(f"Attempting to set session mode to {mode.value}") - self.session.set_mode(mode) - - logger.info(f"Successful change to {mode} Mode") - - - def update_central_widget_mode(self): - """ - This will be triggered whenever the session successfully completes a mode change and emits - a signal to that effect. - """ - logger.info(f"Begin process of updating central widget") - - old_widget = self.centralWidget() - self.setCentralWidget(QWidget()) - old_widget.deleteLater() - - logger.info(f"Clearing events in emmitter threads to get them to wind down") - if type(old_widget) == RecordingWidget: - old_widget.thumbnail_emitter.keep_collecting.clear() - logger.info("Waiting for recording widget to wrap up") - old_widget.thumbnail_emitter.wait() - - if type(old_widget) == ExtrinsicCalibrationWidget: - old_widget.paired_frame_emitter.keep_collecting.clear() - logger.info("Waiting for extrinsic calibration widget to wrap up") - old_widget.paired_frame_emitter.wait() - - if type(old_widget) == IntrinsicCalibrationWidget: - for port, tab in old_widget.camera_tabs.tab_widgets.items(): - tab.frame_emitter.keep_collecting.clear() - - logger.info(f"Matching next tab to active session mode: {self.session.mode}") - # Create the new central widget based on the mode - match self.session.mode: - case SessionMode.Charuco: - new_widget = CharucoWidget(self.session) - case SessionMode.IntrinsicCalibration: - new_widget = IntrinsicCalibrationWidget(self.session) - case SessionMode.ExtrinsicCalibration: - logger.info(f"About to create extrinsic calibration widget") - new_widget = ExtrinsicCalibrationWidget(self.session) - case SessionMode.CaptureVolumeOrigin: - new_widget = CaptureVolumeWidget(self.session) - case SessionMode.Recording: - new_widget = RecordingWidget(self.session) - case SessionMode.PostProcessing: - new_widget = PostProcessingWidget(self.session) - - - self.setCentralWidget(new_widget) - - def switch_to_capture_volume(self): - """ - Once the extrinsic calibration is complete, the GUI should automatically switch over to the capture volume widget - """ - self.session.set_mode(SessionMode.CaptureVolumeOrigin) - - def update_enable_disable(self): - - # note: if the cameras are connected,then you can peak - # into extrinsic/recording tabs, though cannot collect data - - - # you can always look at a charuco board - self.charuco_mode_select.setEnabled(True) - - if self.session.is_camera_setup_eligible(): - self.intrinsic_mode_select.setEnabled(True) - self.extrinsic_mode_select.setEnabled(True) - self.recording_mode_select.setEnabled(True) - else: - self.intrinsic_mode_select.setEnabled(False) - self.extrinsic_mode_select.setEnabled(False) - self.recording_mode_select.setEnabled(False) - - - if self.session.is_capture_volume_eligible(): - self.capture_volume_mode_select.setEnabled(True) - else: - self.capture_volume_mode_select.setEnabled(False) - - - if self.session.is_post_processing_eligible(): - self.processing_mode_select.setEnabled(True) - else: - self.processing_mode_select.setEnabled(False) - - def disconnect_cameras(self): - - self.session.set_mode(SessionMode.Charuco) - self.session.disconnect_cameras() - self.disconnect_cameras_action.setEnabled(False) - self.connect_cameras_action.setEnabled(True) - self.update_enable_disable() - - def pause_all_frame_reading(self): - logger.info("Pausing all frame reading at load of stream tools; should be on charuco tab right now") - self.session.pause_all_monocalibrators() - self.session.pause_synchronizer() - - def load_stream_tools(self): - self.connect_cameras_action.setEnabled(False) - self.disconnect_cameras_action.setEnabled(True) - self.session.qt_signaler.stream_tools_loaded_signal.connect(self.pause_all_frame_reading) - self.thread = Thread( - target=self.session.load_stream_tools, args=(), daemon=True - ) - self.thread.start() - - - def launch_session(self, path_to_folder: str): - session_path = Path(path_to_folder) - self.config = Configurator(session_path) - logger.info(f"Launching session with config file stored in {session_path}") - self.session = Session(self.config) - - # can always load charuco - self.charuco_widget = CharucoWidget(self.session) - self.setCentralWidget(self.charuco_widget) - - # now connecting to cameras is an option - self.connect_cameras_action.setEnabled(True) - - # but must exit and start over to launch a new session for now - self.connect_session_signals() - - self.open_project_action.setEnabled(False) - self.open_recent_project_submenu.setEnabled(False) - self.update_enable_disable() - - def connect_session_signals(self): - """ - After launching a session, connect signals and slots. - Much of these will be from the GUI to the session and vice-versa - """ - self.session.qt_signaler.unlock_postprocessing.connect(self.update_enable_disable) - self.session.qt_signaler.mode_change_success.connect(self.update_central_widget_mode) - self.session.qt_signaler.stream_tools_loaded_signal.connect(self.update_enable_disable) - self.session.qt_signaler.stream_tools_disconnected_signal.connect(self.update_enable_disable) - self.session.qt_signaler.mode_change_success.connect(self.update_enable_disable) - self.session.qt_signaler.extrinsic_calibration_complete.connect(self.switch_to_capture_volume) - - def add_to_recent_project(self, project_path: str): - recent_project_action = QAction(project_path, self) - recent_project_action.triggered.connect(self.open_recent_project) - self.open_recent_project_submenu.addAction(recent_project_action) - - def open_recent_project(self): - action = self.sender() - project_path = action.text() - logger.info(f"Opening recent session stored at {project_path}") - self.launch_session(project_path) - - def create_new_project_folder(self): - default_folder = Path(self.app_settings["last_project_parent"]) - dialog = QFileDialog() - path_to_folder = dialog.getExistingDirectory( - parent=None, - caption="Open Previous or Create New Project Directory", - dir=str(default_folder), - options=QFileDialog.Option.ShowDirsOnly, - ) - - if path_to_folder: - logger.info(("Creating new project in :", path_to_folder)) - self.add_project_to_recent(path_to_folder) - self.launch_session(path_to_folder) - - def add_project_to_recent(self, folder_path): - if str(folder_path) in self.app_settings["recent_projects"]: - pass - else: - self.app_settings["recent_projects"].append(str(folder_path)) - self.app_settings["last_project_parent"] = str(Path(folder_path).parent) - self.update_app_settings() - self.add_to_recent_project(folder_path) - - def update_app_settings(self): - with open(__settings_path__, "w") as f: - toml.dump(self.app_settings, f) - -def launch_main(): - import qdarktheme - - app = QApplication(sys.argv) - qdarktheme.setup_theme('auto') - window = MainWindow() - window.show() - app.exec() - - -if __name__ == "__main__": - launch_main() diff --git a/pyxy3d/gui/vizualize/calibration/capture_volume_widget.py b/pyxy3d/gui/vizualize/calibration/capture_volume_widget.py index 1f82080d5..672e65859 100644 --- a/pyxy3d/gui/vizualize/calibration/capture_volume_widget.py +++ b/pyxy3d/gui/vizualize/calibration/capture_volume_widget.py @@ -29,13 +29,13 @@ QWidget, ) -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.vizualize.calibration.capture_volume_visualizer import CaptureVolumeVisualizer from pyxy3d.gui.navigation_bars import NavigationBarBack class CaptureVolumeWidget(QWidget): - def __init__(self, session: Session): + def __init__(self, session: LiveSession): super(CaptureVolumeWidget, self).__init__() self.session = session @@ -181,7 +181,7 @@ def update_board(self, sync_index): test_session_index = 0 session_path = test_sessions[test_session_index] logger.info(f"Loading session {session_path}") - session = Session(session_path) + session = LiveSession(session_path) session.load_estimated_capture_volume() diff --git a/pyxy3d/gui/vizualize/playback_triangulation_widget.py b/pyxy3d/gui/vizualize/playback_triangulation_widget.py index 285fceb13..0ff757afe 100644 --- a/pyxy3d/gui/vizualize/playback_triangulation_widget.py +++ b/pyxy3d/gui/vizualize/playback_triangulation_widget.py @@ -16,7 +16,7 @@ QVBoxLayout, QWidget, ) -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.vizualize.camera_mesh import CameraMesh, mesh_from_camera from pyxy3d.cameras.camera_array import CameraArray diff --git a/pyxy3d/gui/vizualize/realtime_triangulation_widget.py b/pyxy3d/gui/vizualize/realtime_triangulation_widget.py index fb32e0590..482b5d807 100644 --- a/pyxy3d/gui/vizualize/realtime_triangulation_widget.py +++ b/pyxy3d/gui/vizualize/realtime_triangulation_widget.py @@ -19,7 +19,7 @@ QVBoxLayout, QWidget, ) -from pyxy3d.session.session import Session +from pyxy3d.session.session import LiveSession from pyxy3d.gui.vizualize.camera_mesh import CameraMesh, mesh_from_camera from pyxy3d.interface import XYZPacket from pyxy3d.cameras.camera_array import CameraArray @@ -130,7 +130,7 @@ def display_points(self, xyz_packet:XYZPacket): test_session_index = 0 session_path = test_sessions[test_session_index] logger.info(f"Loading session {session_path}") - session = Session(session_path) + session = LiveSession(session_path) session.load_estimated_capture_volume() diff --git a/pyxy3d/recording/recorded_stream.py b/pyxy3d/recording/recorded_stream.py index 38a1a95e8..c6cb53463 100644 --- a/pyxy3d/recording/recorded_stream.py +++ b/pyxy3d/recording/recorded_stream.py @@ -8,9 +8,6 @@ import pyxy3d.logger import logging -logger = pyxy3d.logger.get(__name__) -logger.setLevel(logging.INFO) - from pathlib import Path from queue import Queue from threading import Thread, Event @@ -22,11 +19,13 @@ import numpy as np from pyxy3d.interface import FramePacket, Tracker, Stream -from pyxy3d.trackers.tracker_enum import TrackerEnum from pyxy3d.cameras.live_stream import Stream from pyxy3d.cameras.camera_array import CameraData from pyxy3d.configurator import Configurator +logger = pyxy3d.logger.get(__name__) +logger.setLevel(logging.INFO) + class RecordedStream(Stream): """ @@ -39,9 +38,9 @@ def __init__( self, directory: Path, port: int, - size, - rotation_count: int, - fps_target: int = 6, + size: tuple = None, + rotation_count: int = 0, + fps_target: int = None, tracker: Tracker = None, ): # self.port = port @@ -59,12 +58,22 @@ def __init__( video_path = str(Path(self.directory, f"port_{self.port}.mp4")) self.capture = cv2.VideoCapture(video_path) + # for playback, set the fps target to the actual + if fps_target is None: + fps_target = int(self.capture.get(cv2.CAP_PROP_FPS)) + + self.stop_event = Event() - self.subscribers = [] + ###################### This is going to be something that needs to be reconsidered + # I think that with a new framework there needs to be a tool to create the + # frame time history whenever video files are loaded in. + # these could be for an individual file or a group of files + # Don't ditch this just yet, Mac. Populate this info if it exists + # estimate based on FPS and frame count if it does not. synched_frames_history_path = str( - Path(self.directory, f"frame_time_history.csv") + Path(self.directory, "frame_time_history.csv") ) synched_frames_history = pd.read_csv(synched_frames_history_path) @@ -75,8 +84,12 @@ def __init__( self.port_history["frame_index"] = ( self.port_history["frame_time"].rank(method="min").astype(int) - 1 ) + self.start_frame_index = self.port_history["frame_index"].min() self.last_frame_index = self.port_history["frame_index"].max() + ##################### + + # initializing to something to avoid errors elsewhere self.frame_index = 0 @@ -165,7 +178,7 @@ def process_frames(self): logger.info(f"Spinlock initiated at port {self.port}") spinlock_looped = True sleep(0.5) - if spinlock_looped == True: + if spinlock_looped: logger.info(f"Spinlock released at port {self.port}") if self.milestones is not None: @@ -189,7 +202,6 @@ def process_frames(self): port=self.port, frame_time=self.frame_time, frame=self.frame, - # frame_index=self.frame_index, points=self.point_data, draw_instructions=draw_instructions, ) @@ -230,7 +242,12 @@ def __init__( rotation_count = camera.rotation_count size = camera.size self.streams[port] = RecordedStream( - directory, port,size, rotation_count, fps_target=fps_target, tracker=tracker + directory, + port, + size, + rotation_count, + fps_target=fps_target, + tracker=tracker, ) def play_videos(self): @@ -302,7 +319,9 @@ def get_configured_camera_data(config_path, intrinsics_only=True): from pyxy3d import __root__ - recording_directory = Path(__root__, "tests", "sessions", "post_monocal") + recording_directory = Path( + __root__, "tests", "sessions", "post_monocal", "calibration", "extrinsic" + ) charuco = Charuco( 4, 5, 11, 8.5, aruco_scale=0.75, square_size_overide_cm=5.25, inverted=True @@ -310,26 +329,30 @@ def get_configured_camera_data(config_path, intrinsics_only=True): tracker = CharucoTracker(charuco) - cameras = get_configured_camera_data(recording_directory) + config = Configurator(recording_directory) + cameras = get_configured_camera_data(Path(recording_directory, "config.toml")) recorded_stream_pool = RecordedStreamPool( - recording_directory, tracker_factory=tracker + directory=recording_directory, config=config, tracker=tracker ) syncr = Synchronizer(recorded_stream_pool.streams, fps_target=None) - recorded_stream_pool.play_videos() syncr.subscribe_to_streams() in_q = Queue(-1) syncr.subscribe_to_sync_packets(in_q) + recorded_stream_pool.play_videos() + + while True: + # logger.info("Pulling sync_packet from queue") + # sleep(0.3) - while not syncr.frames_complete: - sleep(0.03) sync_packet = in_q.get() + if sync_packet is None: + break + for port, frame_packet in sync_packet.frame_packets.items(): if frame_packet: - if frame_packet.frame_time == -1: - break # end of frames cv2.imshow(f"Port {port}", frame_packet.frame_with_points) key = cv2.waitKey(1) diff --git a/pyxy3d/session/session.py b/pyxy3d/session/session.py index f3f68ad11..029ed6289 100644 --- a/pyxy3d/session/session.py +++ b/pyxy3d/session/session.py @@ -1,21 +1,17 @@ # Environment for managing all created objects and the primary interface for the GUI. -import typing import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) - from PySide6.QtCore import QObject, Signal from concurrent.futures import ThreadPoolExecutor from pathlib import Path from time import sleep -from enum import Enum, auto +from enum import Enum from pyxy3d.trackers.charuco_tracker import CharucoTracker from pyxy3d.calibration.monocalibrator import MonoCalibrator from pyxy3d.cameras.camera import Camera from pyxy3d.cameras.synchronizer import Synchronizer from pyxy3d.cameras.camera_array_initializer import CameraArrayInitializer -from pyxy3d.interface import Tracker from pyxy3d.calibration.stereocalibrator import StereoCalibrator from pyxy3d.calibration.capture_volume.point_estimates import PointEstimates @@ -30,6 +26,8 @@ from pyxy3d.cameras.live_stream import LiveStream from pyxy3d.recording.video_recorder import VideoRecorder +logger = pyxy3d.logger.get(__name__) + # %% MAX_CAMERA_PORT_CHECK = 10 FILTERED_FRACTION = 0.025 # by default, 2.5% of image points with highest reprojection error are filtered out during calibration @@ -43,22 +41,22 @@ class SessionMode(Enum): ExtrinsicCalibration = "&Multicamera" CaptureVolumeOrigin = "Capture &Volume" Recording = "&Recording" - PostProcessing = "&Post-processing" + Triangulate = "&Triangulate" + class QtSignaler(QObject): stream_tools_loaded_signal = Signal() stream_tools_disconnected_signal = Signal() unlock_postprocessing = Signal() - recording_complete_signal = Signal() + recording_complete_signal = Signal() mode_change_success = Signal() extrinsic_calibration_complete = Signal() - def __init__(self) -> None: super(QtSignaler, self).__init__() - -class Session: + +class LiveSession: def __init__(self, config: Configurator): # need a way to let the GUI know when certain actions have been completed self.qt_signaler = QtSignaler() @@ -114,7 +112,7 @@ def disconnect_cameras(self): self.qt_signaler.stream_tools_disconnected_signal.emit() def is_camera_setup_eligible(self): - # assume true and prove false + # assume true and prove false eligible = True if len(self.cameras) == 0: @@ -200,19 +198,20 @@ def is_recording_eligible(self): return eligible - def is_post_processing_eligible(self): + def is_triangulate_eligible(self): """ - Post processing can only be performed if recordings (mp4 files) exist and extrinsics + Triangulation can only be performed if recordings (mp4 files) exist and extrinsics (config.toml) are calibrated in the 'record' directory """ - #assume false and prove otherwise + + # assume false and prove otherwise eligible = False for child in self.path.iterdir(): if child.is_dir(): - mp4_files = list(child.glob('*.mp4')) - config_file = child / 'config.toml' + mp4_files = list(child.glob("*.mp4")) + config_file = child / "config.toml" if mp4_files and config_file.exists(): - eligible=True + eligible = True return eligible @@ -231,7 +230,7 @@ def set_mode(self, mode: SessionMode): self.synchronizer.unsubscribe_from_streams() self.pause_all_monocalibrators() - case SessionMode.PostProcessing: + case SessionMode.Triangulate: if self.stream_tools_loaded: self.synchronizer.unsubscribe_from_streams() self.pause_all_monocalibrators() @@ -257,7 +256,7 @@ def set_mode(self, mode: SessionMode): self.pause_all_monocalibrators() self.set_streams_charuco() self.set_streams_tracking(True) - + self.synchronizer.subscribe_to_streams() case SessionMode.CaptureVolumeOrigin: @@ -274,15 +273,17 @@ def set_mode(self, mode: SessionMode): logger.info("Pausing monocals to enter recording mode") self.pause_all_monocalibrators() - + logger.info("Stop tracking for recording mode") self.set_streams_tracking(False) logger.info("Update stream fps to recording fps") self.update_streams_fps() - - logger.info("Subscribe synchronizer to streams so video recorder can manage") + + logger.info( + "Subscribe synchronizer to streams so video recorder can manage" + ) self.synchronizer.subscribe_to_streams() - + self.qt_signaler.mode_change_success.emit() def set_active_mode_fps(self, fps_target: int): @@ -290,11 +291,13 @@ def set_active_mode_fps(self, fps_target: int): Updates the FPS used by the currently active session mode This update includes the config.toml """ - logger.info(f"Updating streams fps to {fps_target} to align with {self.mode} mode") + logger.info( + f"Updating streams fps to {fps_target} to align with {self.mode} mode" + ) match self.mode: case SessionMode.Charuco: pass - case SessionMode.PostProcessing: + case SessionMode.Triangulate: pass case SessionMode.IntrinsicCalibration: self.fps_intrinsic_calibration = fps_target @@ -313,7 +316,7 @@ def get_active_mode_fps(self) -> int: match self.mode: case SessionMode.Charuco: pass - case SessionMode.PostProcessing: + case SessionMode.Triangulate: pass case SessionMode.IntrinsicCalibration: fps = self.fps_intrinsic_calibration @@ -425,8 +428,8 @@ def load_stream_tools(self): logger.info("Pausing stream tools since default loads to charuco") self.pause_all_monocalibrators() self.pause_synchronizer() - - logger.info(f"Signalling successful loading of stream tools") + + logger.info("Signalling successful loading of stream tools") self.qt_signaler.stream_tools_loaded_signal.emit() def _load_monocalibrators(self): @@ -458,7 +461,7 @@ def pause_all_monocalibrators(self): used when not actively on the camera calibration tab or when silencing all in preparation for activating only one """ - logger.info(f"Pausing all monocalibrator looping...") + logger.info("Pausing all monocalibrator looping...") for port, monocal in self.monocalibrators.items(): monocal.unsubscribe_to_stream() @@ -483,10 +486,10 @@ def stop_recording(self): self.is_recording = False - logger.info(f"Recording of frames is complete...signalling change in status") + logger.info("Recording of frames is complete...signalling change in status") self.qt_signaler.recording_complete_signal.emit() - if self.is_post_processing_eligible(): + if self.is_triangulate_eligible(): self.qt_signaler.unlock_postprocessing.emit() def _adjust_resolutions(self): @@ -565,4 +568,4 @@ def estimate_extrinsics(self): self.config.save_capture_volume(self.capture_volume) - self.qt_signaler.extrinsic_calibration_complete.emit() \ No newline at end of file + self.qt_signaler.extrinsic_calibration_complete.emit() diff --git a/tests/sessions/post_monocal/calibration/extrinsic/config.toml b/tests/sessions/post_monocal/calibration/extrinsic/config.toml new file mode 100644 index 000000000..845fd784b --- /dev/null +++ b/tests/sessions/post_monocal/calibration/extrinsic/config.toml @@ -0,0 +1,72 @@ +CreationDate = 2023-05-06T11:12:27.631355 + +[cam_0] +port = 0 +size = [ 1280, 720,] +rotation_count = 0 +error = 0.19 +matrix = [ [ 894.5288733178912, 0.0, 624.011791468827,], [ 0.0, 896.877811780321, 361.28189843593503,], [ 0.0, 0.0, 1.0,],] +distortions = [ -0.3383569013465177, 0.09672714756989992, -0.0013512226751077358, 0.003155760325647937, -0.0037489346582201062,] +translation = [ -0.6425176710599999, 0.2628680995431283, 1.4515383168392715,] +rotation = [ [ 0.028454458274723926, -0.8200005191342249, 0.571655046705523,], [ -0.9915778577174362, -0.0954406312087065, -0.0875467760627629,], [ 0.12634752031121108, -0.5643493904790591, -0.8158100695487417,],] +exposure = -7.0 +grid_count = 24 +ignore = false +verified_resolutions = [ [ 640, 480,], [ 1280, 720,], [ 1920, 1080,],] + +[cam_1] +port = 1 +size = [ 1280, 720,] +rotation_count = -1 +error = 0.469 +matrix = [ [ 703.9581082139392, 0.0, 626.0054656040467,], [ 0.0, 706.235022552987, 348.8635696574537,], [ 0.0, 0.0, 1.0,],] +distortions = [ 0.19214795710153681, -0.256426138563638, 0.0028024971240443184, -0.005231509572999095, 0.11538184615468566,] +translation = [ 0.00043375420342479525, -0.0019919458836723174, 0.0025865337259615363,] +rotation = [ [ 0.999994984884606, 0.0030591131003008653, -0.0008197759914263436,], [ -0.00305690929424111, 0.9999917465819073, 0.00267620508058602,], [ 0.0008279560394934837, -0.002673685678261328, 0.9999960829391735,],] +exposure = -6.0 +grid_count = 34 +ignore = false +verified_resolutions = [ [ 640, 480,], [ 1280, 720,], [ 1920, 1080,],] + +[cam_2] +port = 2 +size = [ 1280, 720,] +rotation_count = 1 +error = 0.656 +matrix = [ [ 637.2292860850233, 0.0, 657.4323077978876,], [ 0.0, 633.5719552845733, 378.1791029421547,], [ 0.0, 0.0, 1.0,],] +distortions = [ 0.1695125871364876, -0.24951331049151845, -0.0007512291961660638, 0.012510062186129272, 0.15631917743880638,] +translation = [ 0.038133706887563415, 0.6725515804052516, 1.5183602439910873,] +rotation = [ [ -0.9967644634943604, -0.07540940912751744, 0.027821310713882212,], [ -0.06836124744416018, 0.6132863527233845, -0.7868968098875017,], [ 0.04227699330224407, -0.7862526760388718, -0.6164571236177216,],] +exposure = -5 +grid_count = 28 +ignore = false +verified_resolutions = [ [ 640, 480,], [ 1280, 720,], [ 1920, 1080,],] + +[cam_3] +port = 3 +size = [ 1280, 720,] +rotation_count = 0 +error = 0.276 +matrix = [ [ 640.3566114270441, 0.0, 657.2609475214653,], [ 0.0, 643.5686701320052, 333.48626175632,], [ 0.0, 0.0, 1.0,],] +distortions = [ 0.19043785421231899, -0.28328273216892474, -0.0024158947669173716, 0.0028424628966545423, 0.1475260356665553,] +translation = [ -0.8510628562369814, 0.24570089951245075, 0.7997627191876137,] +rotation = [ [ 0.05823536030637522, 0.04404198557310357, 0.9973309111407134,], [ -0.9982739778759242, 0.01017116409484392, 0.057841270013155144,], [ -0.00759657197462299, -0.9989779031228541, 0.0445582895374147,],] +exposure = -6 +grid_count = 34 +ignore = false +verified_resolutions = [ [ 640, 480,], [ 1280, 720,], [ 1920, 1080,],] + +[capture_volume] +stage = 2 + +[charuco] +columns = 4 +rows = 5 +board_height = 11.0 +board_width = 8.5 +dictionary = "DICT_4X4_1000" +units = "inch" +aruco_scale = 0.75 +square_size_overide_cm = 5.4 +inverted = true + diff --git a/tests/test_calibration.py b/tests/test_calibration.py index 17dba5a0b..f71f79b1e 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -1,7 +1,5 @@ import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) - from time import sleep import shutil from pathlib import Path @@ -14,31 +12,21 @@ get_point_estimates, ) import pytest -from pyxy3d.calibration.charuco import Charuco, get_charuco from pyxy3d.trackers.charuco_tracker import CharucoTracker -from pyxy3d.calibration.monocalibrator import MonoCalibrator -from pyxy3d.cameras.camera import Camera from pyxy3d.cameras.synchronizer import Synchronizer -from pyxy3d.cameras.camera_array_initializer import CameraArrayInitializer - from pyxy3d.calibration.stereocalibrator import StereoCalibrator -from pyxy3d.calibration.capture_volume.point_estimates import PointEstimates -from pyxy3d.calibration.capture_volume.capture_volume import CaptureVolume from pyxy3d.calibration.capture_volume.quality_controller import QualityController -from pyxy3d.cameras.camera_array import CameraArray, CameraData -from pyxy3d.calibration.capture_volume.helper_functions.get_point_estimates import ( - get_point_estimates, -) -from pyxy3d.cameras.live_stream import LiveStream from pyxy3d.recording.video_recorder import VideoRecorder -from pyxy3d.recording.recorded_stream import RecordedStream, RecordedStreamPool +from pyxy3d.recording.recorded_stream import RecordedStreamPool from pyxy3d.session.session import FILTERED_FRACTION from pyxy3d.configurator import Configurator +logger = pyxy3d.logger.get(__name__) + TEST_SESSIONS = ["mediapipe_calibration"] @@ -92,26 +80,25 @@ def test_post_monocalibration(session_path): charuco_tracker = CharucoTracker(charuco) # create a synchronizer based off of these stream pools - logger.info(f"Creating RecordedStreamPool") + logger.info("Creating RecordedStreamPool") recording_path = Path(session_path, "calibration", "extrinsic") point_data_path = Path(recording_path, "xy.csv") stream_pool = RecordedStreamPool( - recording_path, - config=config, - fps_target=100, - tracker=charuco_tracker + recording_path, config=config, fps_target=100, tracker=charuco_tracker ) - + logger.info("Creating Synchronizer") syncr = Synchronizer(stream_pool.streams, fps_target=100) # video recorder needed to save out points.csv. - logger.info(f"Creating test video recorder to save out point data") + logger.info("Creating test video recorder to save out point data") video_recorder = VideoRecorder(syncr) - video_recorder.start_recording(recording_path, include_video=False, store_point_history=True) + video_recorder.start_recording( + recording_path, include_video=False, store_point_history=True + ) logger.info("Initiate playing stream pool videos...") stream_pool.play_videos() @@ -121,7 +108,7 @@ def test_post_monocalibration(session_path): logger.info("Waiting for point_data.csv to populate...") sleep(1) - logger.info(f"Waiting for video recorder to finish processing stream...") + logger.info("Waiting for video recorder to finish processing stream...") stereocalibrator = StereoCalibrator(config.config_toml_path, point_data_path) stereocalibrator.stereo_calibrate_all(boards_sampled=10) diff --git a/tests/test_intrinsic_calibrator.py b/tests/test_intrinsic_calibrator.py new file mode 100644 index 000000000..fce094581 --- /dev/null +++ b/tests/test_intrinsic_calibrator.py @@ -0,0 +1,40 @@ +from pathlib import Path +from queue import Queue + +from pyxy3d import __root__ +from pyxy3d.helper import copy_contents +from pyxy3d.calibration.charuco import Charuco +from pyxy3d.trackers.charuco_tracker import CharucoTracker +from pyxy3d.recording.recorded_stream import RecordedStream +import pyxy3d.logger + +logger = pyxy3d.logger.get(__name__) +def test_intrinsic_calibrator(): + + # use a general video file with a charuco for convenience + original_data_path= Path(__root__, "tests", "sessions", "4_cam_recording") + destination_path =Path(__root__, "tests", "sessions_copy_delete", "4_cam_recording") + copy_contents(original_data_path,destination_path) + + + recording_directory = Path( + __root__, "tests", "sessions", "post_monocal", "calibration", "extrinsic" + ) + + charuco = Charuco( + 4, 5, 11, 8.5, aruco_scale=0.75, square_size_overide_cm=5.25, inverted=True + ) + + charuco_tracker = CharucoTracker(charuco) + + stream = RecordedStream(recording_directory,port=1,rotation_count=0, tracker=charuco_tracker) + + frame_q = Queue() + stream.subscribe(frame_q) + + stream.play_video() + + +if __name__ == "__main__": + test_intrinsic_calibrator() + diff --git a/tests/test_real_time_triangulator.py b/tests/test_real_time_triangulator.py index 1a34d8e67..c1e614aa0 100644 --- a/tests/test_real_time_triangulator.py +++ b/tests/test_real_time_triangulator.py @@ -4,32 +4,26 @@ Hopefully I can keep things clean enough for that... """ -# %% import pyxy3d.logger -logger = pyxy3d.logger.get(__name__) from time import sleep from pyxy3d.cameras.synchronizer import Synchronizer -from pyxy3d.interface import PointPacket, FramePacket, SyncPacket from pyxy3d.triangulate.sync_packet_triangulator import SyncPacketTriangulator -from pyxy3d.cameras.camera_array import CameraArray, CameraData +from pyxy3d.cameras.camera_array import CameraArray from pyxy3d.recording.recorded_stream import RecordedStreamPool -from pyxy3d.calibration.charuco import Charuco, get_charuco +from pyxy3d.calibration.charuco import Charuco from pyxy3d.trackers.charuco_tracker import CharucoTracker from pyxy3d.configurator import Configurator import pytest import shutil from pathlib import Path -from numba import jit -from numba.typed import Dict, List import numpy as np -import cv2 import pandas as pd -from time import time from pyxy3d import __root__ +logger = pyxy3d.logger.get(__name__) TEST_SESSIONS = ["post_optimization"] @@ -83,7 +77,7 @@ def test_real_time_triangulator(session_path): camera_array: CameraArray = config.get_camera_array() - logger.info(f"Creating RecordedStreamPool based on calibration recordings") + logger.info("Creating RecordedStreamPool based on calibration recordings") recording_directory = Path(session_path, "calibration", "extrinsic") stream_pool = RecordedStreamPool( directory=recording_directory, @@ -106,7 +100,6 @@ def test_real_time_triangulator(session_path): while real_time_triangulator.running: sleep(1) - # %% # need to compare the output of the triangulator to the point_estimats # this is nice because it's two totally different processing pipelines # but sync indices will be different, so just compare mean positions @@ -126,7 +119,7 @@ def test_real_time_triangulator(session_path): logger.info(f"y: {round(triangulator_y_mean,4)} vs {round(config_y_mean,4)} ") logger.info(f"z: {round(triangulator_z_mean,4)} vs {round(config_z_mean,4)} ") - logger.info(f"Assert that mean positions are within 1.5 centimeters...") + logger.info("Assert that mean positions are within 1.5 centimeters...") assert abs(config_x_mean - triangulator_x_mean) < 0.015 assert abs(config_y_mean - triangulator_y_mean) < 0.015 assert abs(config_z_mean - triangulator_z_mean) < 0.015 diff --git a/tests/test_synchronizer.py b/tests/test_synchronizer.py index 9905fdf5f..dd41f06f9 100644 --- a/tests/test_synchronizer.py +++ b/tests/test_synchronizer.py @@ -2,24 +2,16 @@ import pyxy3d.logger import pandas as pd -import pytest -logger = pyxy3d.logger.get(__name__) from pyxy3d import __root__ -import pytest import shutil -import cv2 from pathlib import Path import time -from pyxy3d.trackers.hand_tracker import HandTracker from pyxy3d.cameras.synchronizer import Synchronizer -from pyxy3d.interface import PointPacket, FramePacket, SyncPacket -from pyxy3d.triangulate.sync_packet_triangulator import SyncPacketTriangulator -from pyxy3d.cameras.camera_array import CameraArray, CameraData from pyxy3d.recording.recorded_stream import RecordedStreamPool from pyxy3d.configurator import Configurator from pyxy3d.helper import copy_contents -from pyxy3d.trackers.tracker_enum import TrackerEnum from pyxy3d.recording.video_recorder import VideoRecorder +logger = pyxy3d.logger.get(__name__) # TEST_SESSIONS = ["mediapipe_calibration"] @@ -44,14 +36,12 @@ def test_synchronizer(): config = Configurator(session_path) - logger.info(f"Creating RecordedStreamPool") + logger.info("Creating RecordedStreamPool") recording_directory = Path(session_path, "recording_1") stream_pool = RecordedStreamPool( recording_directory, config=config, - # note taht recorded stream needs a tracker of some sort - # tracker=TrackerEnum.CHARUCO.value(config.get_charuco()), fps_target=100, ) logger.info("Creating Synchronizer") @@ -59,13 +49,6 @@ def test_synchronizer(): recorder = VideoRecorder(syncr, suffix="test") - #### Basic code for interfacing with in-progress RealTimeTriangulator - #### Just run off of saved point_data.csv for development/testing - # camera_array: CameraArray = config.get_camera_array() - # sync_packet_triangulator = SyncPacketTriangulator( - # camera_array, syncr, recording_directory=session_path - # ) - test_recordings = Path(session_path, "test_recording_1") recorder.start_recording(destination_folder=test_recordings, include_video=True,show_points=False, store_point_history=False) stream_pool.play_videos()