diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 9f7f806fa1..a26789ace8 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -97,3 +97,6 @@ and `logo_dark`. Keep cache-removal documentation focused on migration impact. Omit asset-fetch implementation walkthroughs and alternative override instructions. + +When rebasing example package moves, check upstream-added `:code-file:` references +against the new paths; Git can merge these references without detecting stale paths. diff --git a/docs/source/references/camera_streaming.rst b/docs/source/references/camera_streaming.rst index 35c7d0ce41..cbb6fc1d67 100644 --- a/docs/source/references/camera_streaming.rst +++ b/docs/source/references/camera_streaming.rst @@ -344,7 +344,7 @@ environment with ``isaaccapture``) and run:: python -m isaaccapture.cloudxr.service status python -m isaaccapture.cloudxr.service stop -Run ``camera_viz.py --help`` for the rest (install dir, env-config file, WSS proxy toggle). +Run ``camera_viz.sh run CONFIG --help`` for the rest (install dir, env-config file, WSS proxy toggle). Runtime settings themselves are declared in the config rather than exported. The viewer ships defaults (pose wait off, runtime foveation on), and ``display.cloudxr`` overrides them per @@ -376,7 +376,7 @@ runtime resolved, read the settings dump at the top of the newest Split mode — robot → workstation over RTP ----------------------------------------- -Split mode runs the capture side on the robot (``camera_streamer.py``) and ships RTP H.264 to +Split mode runs the capture side on the robot (the ``camera_streamer`` module) and ships RTP H.264 to the viewer on the workstation (``source: rtp``). .. warning:: @@ -521,28 +521,29 @@ Televiz as the compositor at the end of the chain: camera_viz/ ├── camera_viz.sh — CLI: setup / loopback / run / deploy / service-* - ├── camera_viz.py — receiver / viewer (drives a Televiz VizSession) - ├── camera_streamer.py — robot-side RTP sender (per-camera supervisor) - ├── pipeline/ — source ABC + threaded runner - ├── placements/ — XR lock-mode strategies (world / head / lazy / gimbal) - ├── sources/ — V4L2 / OAK-D / ZED / video replay / synthetic / rtp_h264 - ├── transports/ — RTP sender + receiver (native + GStreamer) - ├── codec/ — native NVENC / NVDEC pybind module ├── configs/ — one YAML per source kind ├── test_data/ — sample replay clip (Git LFS) + ├── python/isaaccapture_examples/camera_viz/ + │ ├── camera_viz.py — receiver / viewer (drives a Televiz VizSession) + │ ├── camera_streamer.py — robot-side RTP sender (per-camera supervisor) + │ ├── pipeline/ — source ABC + threaded runner + │ ├── placements/ — XR lock-mode strategies (world / head / lazy / gimbal) + │ ├── sources/ — V4L2 / OAK-D / ZED / video replay / synthetic / rtp_h264 + │ ├── transports/ — RTP sender + receiver (native + GStreamer) + │ └── codec/ — native NVENC / NVDEC pybind module └── scripts/ — installer + systemd unit template -- **Sources** (:code-dir:`sources/ `) implement a common source ABC and - hand frames to a threaded runner in :code-dir:`pipeline/ `. Each +- **Sources** (:code-dir:`sources/ `) implement a common source ABC and + hand frames to a threaded runner in :code-dir:`pipeline/ `. Each source produces GPU frames where possible — e.g. the ZED source uses ``retrieve_image(MEM.GPU)`` so BGRA8 stays in VRAM and a CUDA kernel channel-swaps into contiguous RGBA with no host round-trip. -- **The viewer** (:code-file:`camera_viz.py `) creates a +- **The viewer** (:code-file:`camera_viz.py `) creates a ``VizSession`` and adds one ``QuadLayer`` per enabled camera, then submits each frame to its layer and calls ``render()`` once per frame. Stereo cameras submit both eyes. -- **Transport** (:code-dir:`transports/ `) carries the split mode: +- **Transport** (:code-dir:`transports/ `) carries the split mode: an RTP H.264 sender on the robot and a receiver on the workstation, with a native NVENC/NVDEC codec - module (:code-dir:`codec/ `) or a GStreamer fallback. -- **Placement** (:code-dir:`placements/ `) holds the XR lock-mode + module (:code-dir:`codec/ `) or a GStreamer fallback. +- **Placement** (:code-dir:`placements/ `) holds the XR lock-mode strategies. Placement is application policy — Televiz only renders a layer at whatever pose the app sets each frame. diff --git a/examples/camera_viz/README.md b/examples/camera_viz/README.md index f16f519512..b96a52ea4c 100644 --- a/examples/camera_viz/README.md +++ b/examples/camera_viz/README.md @@ -22,7 +22,7 @@ SPDX-License-Identifier: Apache-2.0 | `zed` | ZED 2 / Mini / X One; mono or `stereo: true` (per-eye SDK retrieve, zero-copy GPU) | | `video` | Video-file replay (anything OpenCV/FFmpeg reads) — preview / testing without a camera. Loops by default; `stereo: true` splits side-by-side files into eyes (viewer only) | -In XR mode the viewer **attaches to the CloudXR runtime + WSS proxy**, starting a background service if none is serving — nothing to start separately (`--accept-eula` for the first run; CloudXR.js is hosted at `https://:48322/client/` by default when this run starts the service — `--no-host-client` only applies then; stop and restart the service to change hosting; `camera_viz.py --help` for the rest). Output: XR headset (default) or desktop window (`run CONFIG --mode window`); one surface per camera — a flat plane (default), a cylinder arc, or an equirect sphere (`placements..shape`, XR only for the curved shapes). Stereo cameras render true SBS in XR; window mode shows the left eye. XR placements: `world` / `head` / `lazy` / `gimbal`. +In XR mode the viewer **attaches to the CloudXR runtime + WSS proxy**, starting a background service if none is serving — nothing to start separately (`--accept-eula` for the first run; CloudXR.js is hosted at `https://:48322/client/` by default when this run starts the service — `--no-host-client` only applies then; stop and restart the service to change hosting; `camera_viz.sh run CONFIG --help` for the rest). Output: XR headset (default) or desktop window (`run CONFIG --mode window`); one surface per camera — a flat plane (default), a cylinder arc, or an equirect sphere (`placements..shape`, XR only for the curved shapes). Stereo cameras render true SBS in XR; window mode shows the left eye. XR placements: `world` / `head` / `lazy` / `gimbal`. --- @@ -323,20 +323,21 @@ camera_viz xr · local · 1 camera ``` camera_viz/ ├── camera_viz.sh — CLI: setup / loopback / run / deploy / service-* -├── camera_viz.py — receiver / viewer (entrypoint + wiring) -├── camera_streamer.py — robot-side RTP sender (per-camera supervisor) -├── config.py — YAML → SourceEntry: parse + validate, no allocation -├── display.py — VizSession + one layer per surface -├── cloudxr_env.py — display.cloudxr → the runtime's env file -├── dashboard.py — terminal status panel -├── controls/ — XR controller bindings, shapes, stereo geometry, HUD -├── pipeline/ — source ABC + threaded runner -├── placements/ — XR lock-mode strategies -├── sources/ — V4L2 / OAK-D / ZED / synthetic / video replay / rtp_h264 -├── transports/ — RTP sender + receiver, native + GStreamer -├── codec/ — native NVENC/NVDEC pybind module ├── configs/ — one YAML per camera kind ├── test_data/ — sample replay clip (Git LFS) +├── python/isaaccapture_examples/camera_viz/ +│ ├── camera_viz.py — receiver / viewer (entrypoint + wiring) +│ ├── camera_streamer.py — robot-side RTP sender (per-camera supervisor) +│ ├── config.py — YAML → SourceEntry: parse + validate, no allocation +│ ├── display.py — VizSession + one layer per surface +│ ├── cloudxr_env.py — display.cloudxr → the runtime's env file +│ ├── dashboard.py — terminal status panel +│ ├── controls/ — XR controller bindings, shapes, stereo geometry, HUD +│ ├── pipeline/ — source ABC + threaded runner +│ ├── placements/ — XR lock-mode strategies +│ ├── sources/ — V4L2 / OAK-D / ZED / synthetic / video replay / rtp_h264 +│ ├── transports/ — RTP sender + receiver, native + GStreamer +│ └── codec/ — native NVENC/NVDEC pybind module └── scripts/ ├── _install_deps.sh — installer (setup + deploy) └── camera-streamer.service.in — systemd unit template diff --git a/examples/camera_viz/camera_viz.sh b/examples/camera_viz/camera_viz.sh index 065be329d7..47b572150a 100755 --- a/examples/camera_viz/camera_viz.sh +++ b/examples/camera_viz/camera_viz.sh @@ -198,7 +198,7 @@ PY } # ────────────────────────────────────────────────────────────────────── -# run (the viewer; args after CONFIG forward to camera_viz.py, e.g. --mode xr) +# run (the viewer; args after CONFIG forward to the viewer, e.g. --mode xr) # ────────────────────────────────────────────────────────────────────── cmd_run() { @@ -209,7 +209,7 @@ cmd_run() { _require_rtp_deps fi log_step "Starting camera_viz — Ctrl-C to exit" - "$LOCAL_VENV/bin/python" "$HERE/camera_viz.py" "$@" + "$LOCAL_VENV/bin/python" -m isaaccapture_examples.camera_viz "$@" } # ────────────────────────────────────────────────────────────────────── @@ -240,11 +240,11 @@ cmd_loopback() { trap '_loopback_cleanup' EXIT log_step "Starting camera_streamer → 127.0.0.1 (background)" - "$LOCAL_VENV/bin/python" "$HERE/camera_streamer.py" "$1" --host 127.0.0.1 & + "$LOCAL_VENV/bin/python" -m isaaccapture_examples.camera_viz.camera_streamer "$1" --host 127.0.0.1 & _LOOPBACK_SENDER_PID=$! log_step "Starting camera_viz (foreground) — Ctrl-C to exit" - "$LOCAL_VENV/bin/python" "$HERE/camera_viz.py" "$_LOOPBACK_RECV_CONFIG" + "$LOCAL_VENV/bin/python" -m isaaccapture_examples.camera_viz "$_LOOPBACK_RECV_CONFIG" } # ────────────────────────────────────────────────────────────────────── @@ -256,7 +256,7 @@ cmd_deploy() { # anything we don't recognize would land in REMOTE_REST (where the # positional CONFIG lives). local no_service=false - # ``--streaming-host`` → injected as ``--host`` on camera_streamer.py's + # ``--streaming-host`` → injected as ``--host`` on the streamer's # CLI inside the rendered systemd unit. Lets you keep the YAML at # 127.0.0.1 for loopback and override only when deploying to a robot. local streaming_host="${STREAMING_HOST:-}" @@ -304,10 +304,10 @@ cmd_deploy() { local manual_host_flag="" if [[ -n "$streaming_host" ]]; then manual_host_flag=" --host $streaming_host" - log_info "(--streaming-host has no effect in --no-service mode — pass it to camera_streamer.py yourself.)" + log_info "(--streaming-host has no effect in --no-service mode — pass it to the streamer yourself.)" fi log_info "Run manually with:" - log_info " ssh $REMOTE_USER@$REMOTE_HOST 'cd ~/camera_viz && .venv/bin/python camera_streamer.py $config$manual_host_flag'" + log_info " ssh $REMOTE_USER@$REMOTE_HOST 'cd ~/camera_viz && .venv/bin/python -m isaaccapture_examples.camera_viz.camera_streamer $config$manual_host_flag'" log_info "Re-run without --no-service when you're ready to install the systemd unit." return 0 fi @@ -441,9 +441,9 @@ REMOTE (Jetson robot) deploy [--host H --user U [--password P]] [--streaming-host IP] [--no-service] CONFIG rsync source, install deps, install + start - systemd user service running camera_streamer.py. + systemd user service running the streamer. --no-service stops after deps so you can run - camera_streamer.py by hand first. + the streamer by hand first. --streaming-host injects ``--host IP`` into the unit's ExecStart so the sender streams there regardless of streaming.host in the YAML. Same diff --git a/examples/camera_viz/pyproject.toml b/examples/camera_viz/pyproject.toml index c2dd8a13da..075f1c9d9c 100644 --- a/examples/camera_viz/pyproject.toml +++ b/examples/camera_viz/pyproject.toml @@ -7,8 +7,14 @@ # ``camera_viz.sh setup``; pass ``setup --wheel `` to use a locally # built wheel instead. +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] -name = "camera-viz" +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaaccapture-examples-camera-viz" version = "0.0.0" description = "Televiz camera-feed visualizer" requires-python = ">=3.11,<3.14" @@ -38,13 +44,25 @@ rtp = [ # setup script apt-installs them and uses --system-site-packages), # not pip — PyGObject from PyPI requires a source-build with cairo # + girepository dev headers. pybind11 stays in the venv as a build- - # time dep for the codec at ./codec/. + # time dep for the native codec. "pybind11>=2.11", ] -# Native NVENC/NVDEC codec lives in ./codec/. Build with -# ``codec/build.sh``; ``./camera_viz.sh setup`` does this automatically. +# Native NVENC/NVDEC codec lives in ./python/isaaccapture_examples/camera_viz/codec/. +# Build with +# its build.sh; ``./camera_viz.sh setup`` does this automatically. # Requires CUDA toolkit + NVIDIA driver (libnvidia-encode + libnvcuvid). +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `camera_viz/`. +# +# The native codec's .so is built in place by codec/build.sh, so it is picked up +# from the source tree rather than shipped by this build. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/camera_viz"] +sources = ["python"] + [tool.uv] python-preference = "only-managed" diff --git a/examples/camera_viz/python/isaaccapture_examples/camera_viz/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/camera_viz/python/isaaccapture_examples/camera_viz/__main__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/__main__.py new file mode 100644 index 0000000000..afc8d7fbf8 --- /dev/null +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/__main__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from .camera_viz import main + +sys.exit(main()) diff --git a/examples/camera_viz/camera_streamer.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_streamer.py similarity index 99% rename from examples/camera_viz/camera_streamer.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_streamer.py index f0d1b2e7f8..eae755f19c 100755 --- a/examples/camera_viz/camera_streamer.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_streamer.py @@ -29,14 +29,14 @@ import yaml -from pipeline import FrameSource -from sources import ( +from .pipeline import FrameSource +from .sources import ( PairedFrameSource, build_local_camera, resolve_video_paths, set_verbose, ) -from transports import RtpH264Sender, make_encoder +from .transports import RtpH264Sender, make_encoder logger = logging.getLogger("isaaccapture.camera_streamer") diff --git a/examples/camera_viz/camera_viz.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_viz.py similarity index 98% rename from examples/camera_viz/camera_viz.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_viz.py index e0b739510d..ccf53b78f4 100755 --- a/examples/camera_viz/camera_viz.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/camera_viz.py @@ -30,18 +30,16 @@ from isaaccapture import logging_config from isaaccapture.cloudxr import CloudXRLauncher -import cloudxr_env -import config -import display -from controls import ( +from . import cloudxr_env, config, display +from .controls import ( ControllerControls, ControlTarget, controls_config_from_yaml, make_hud, ) -from dashboard import Dashboard -from pipeline import VizRunner -from sources import resolve_video_paths, set_notify_sink, set_verbose +from .dashboard import Dashboard +from .pipeline import VizRunner +from .sources import resolve_video_paths, set_notify_sink, set_verbose def _parse_args(argv: Optional[list[str]]): @@ -390,7 +388,3 @@ def _on_signal(signum, frame): # actually in force inspectable while the runtime is up. settings_stack.close() return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/examples/camera_viz/cloudxr_env.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/cloudxr_env.py similarity index 100% rename from examples/camera_viz/cloudxr_env.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/cloudxr_env.py diff --git a/examples/camera_viz/codec/CMakeLists.txt b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/CMakeLists.txt similarity index 100% rename from examples/camera_viz/codec/CMakeLists.txt rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/CMakeLists.txt diff --git a/examples/camera_viz/codec/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/__init__.py similarity index 100% rename from examples/camera_viz/codec/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/__init__.py diff --git a/examples/camera_viz/codec/build.sh b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/build.sh similarity index 100% rename from examples/camera_viz/codec/build.sh rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/build.sh diff --git a/examples/camera_viz/codec/codec_bindings.cpp b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/codec_bindings.cpp similarity index 100% rename from examples/camera_viz/codec/codec_bindings.cpp rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/codec_bindings.cpp diff --git a/examples/camera_viz/codec/h264_decoder.cpp b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/h264_decoder.cpp similarity index 100% rename from examples/camera_viz/codec/h264_decoder.cpp rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/h264_decoder.cpp diff --git a/examples/camera_viz/codec/h264_encoder.cpp b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/h264_encoder.cpp similarity index 100% rename from examples/camera_viz/codec/h264_encoder.cpp rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/h264_encoder.cpp diff --git a/examples/camera_viz/codec/inc/h264_decoder.hpp b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/inc/h264_decoder.hpp similarity index 100% rename from examples/camera_viz/codec/inc/h264_decoder.hpp rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/inc/h264_decoder.hpp diff --git a/examples/camera_viz/codec/inc/h264_encoder.hpp b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/inc/h264_encoder.hpp similarity index 100% rename from examples/camera_viz/codec/inc/h264_encoder.hpp rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/inc/h264_encoder.hpp diff --git a/examples/camera_viz/codec/nv12_to_rgba.cu b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/nv12_to_rgba.cu similarity index 100% rename from examples/camera_viz/codec/nv12_to_rgba.cu rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/nv12_to_rgba.cu diff --git a/examples/camera_viz/codec/nv12_to_rgba.cuh b/examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/nv12_to_rgba.cuh similarity index 100% rename from examples/camera_viz/codec/nv12_to_rgba.cuh rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/codec/nv12_to_rgba.cuh diff --git a/examples/camera_viz/config.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/config.py similarity index 98% rename from examples/camera_viz/config.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/config.py index abbd7df97b..5a890168e6 100644 --- a/examples/camera_viz/config.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/config.py @@ -18,9 +18,9 @@ from dataclasses import dataclass from typing import List, Optional, Tuple -from pipeline import FrameSource -from placements import PlacementConfig, PlacementStrategy, build as build_placement -from sources import PairedFrameSource, RtpH264Source, build_local_camera +from .pipeline import FrameSource +from .placements import PlacementConfig, PlacementStrategy, build as build_placement +from .sources import PairedFrameSource, RtpH264Source, build_local_camera @dataclass diff --git a/examples/camera_viz/controls/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/__init__.py similarity index 100% rename from examples/camera_viz/controls/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/__init__.py diff --git a/examples/camera_viz/controls/bindings.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/bindings.py similarity index 99% rename from examples/camera_viz/controls/bindings.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/bindings.py index 6a6fe9b72a..bcee9766a6 100644 --- a/examples/camera_viz/controls/bindings.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/bindings.py @@ -39,7 +39,7 @@ from . import shapes, stereo from .hud import split_message -from placements import ( +from ..placements import ( PlacementConfig, PlacementStrategy, build as build_placement, diff --git a/examples/camera_viz/controls/hud.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/hud.py similarity index 99% rename from examples/camera_viz/controls/hud.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/hud.py index 6555d454d3..42e52850ac 100644 --- a/examples/camera_viz/controls/hud.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/hud.py @@ -17,7 +17,7 @@ import numpy as np from PIL import Image, ImageDraw, ImageFont -from placements import HeadLocked, PlacementConfig +from ..placements import HeadLocked, PlacementConfig _W, _H = 1280, 160 _PAD, _BAR = 36, 12 # left text inset, accent-bar width diff --git a/examples/camera_viz/controls/shapes.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/shapes.py similarity index 99% rename from examples/camera_viz/controls/shapes.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/shapes.py index 9366d65a36..275dec47cd 100644 --- a/examples/camera_viz/controls/shapes.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/shapes.py @@ -21,7 +21,7 @@ from dataclasses import replace from typing import List, Optional, Tuple -from placements import yaw_quat +from ..placements import yaw_quat def _clamp(value: float, limits: Tuple[float, float]) -> float: diff --git a/examples/camera_viz/controls/stereo.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/stereo.py similarity index 100% rename from examples/camera_viz/controls/stereo.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/controls/stereo.py diff --git a/examples/camera_viz/dashboard.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/dashboard.py similarity index 100% rename from examples/camera_viz/dashboard.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/dashboard.py diff --git a/examples/camera_viz/display.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/display.py similarity index 98% rename from examples/camera_viz/display.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/display.py index 8662f85822..7056b9c373 100644 --- a/examples/camera_viz/display.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/display.py @@ -14,8 +14,8 @@ import isaaccapture.viz as viz -from config import SourceEntry, VALID_SHAPES -from placements import yaw_quat +from .config import SourceEntry, VALID_SHAPES +from .placements import yaw_quat # ImageLayerBase::kSlotCount (kMaxFramesInFlight + 2). Only used to # report the VRAM that shape switching adds. diff --git a/examples/camera_viz/pipeline/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/__init__.py similarity index 100% rename from examples/camera_viz/pipeline/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/__init__.py diff --git a/examples/camera_viz/pipeline/interface.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/interface.py similarity index 100% rename from examples/camera_viz/pipeline/interface.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/interface.py diff --git a/examples/camera_viz/pipeline/runner.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/runner.py similarity index 99% rename from examples/camera_viz/pipeline/runner.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/runner.py index 924fb85d98..b25d0b218d 100644 --- a/examples/camera_viz/pipeline/runner.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/pipeline/runner.py @@ -23,7 +23,7 @@ import isaaccapture.viz as viz -from dashboard import CameraRow, Dashboard, Snapshot +from ..dashboard import CameraRow, Dashboard, Snapshot from .interface import FrameSource diff --git a/examples/camera_viz/placements/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/__init__.py similarity index 100% rename from examples/camera_viz/placements/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/__init__.py diff --git a/examples/camera_viz/placements/_math.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/_math.py similarity index 100% rename from examples/camera_viz/placements/_math.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/_math.py diff --git a/examples/camera_viz/placements/lock_modes.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/lock_modes.py similarity index 100% rename from examples/camera_viz/placements/lock_modes.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/placements/lock_modes.py diff --git a/examples/camera_viz/sources/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/__init__.py similarity index 99% rename from examples/camera_viz/sources/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/__init__.py index 89132a9c81..b43b02fc8a 100644 --- a/examples/camera_viz/sources/__init__.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import List -from pipeline import FrameSource +from ..pipeline import FrameSource from ._helpers import PairedFrameSource, set_notify_sink, set_verbose from .oakd import OakdSource diff --git a/examples/camera_viz/sources/_helpers.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_helpers.py similarity index 99% rename from examples/camera_viz/sources/_helpers.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_helpers.py index 93cf6efba9..cabdd13bb8 100644 --- a/examples/camera_viz/sources/_helpers.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_helpers.py @@ -43,7 +43,7 @@ import numpy as np -from pipeline import Frame, FrameSource, SourceSpec +from ..pipeline import Frame, FrameSource, SourceSpec _SINK = None diff --git a/examples/camera_viz/sources/_nv_decode.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_nv_decode.py similarity index 98% rename from examples/camera_viz/sources/_nv_decode.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_nv_decode.py index 3bcc84ec84..3cacc0e557 100644 --- a/examples/camera_viz/sources/_nv_decode.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/_nv_decode.py @@ -36,7 +36,7 @@ def _ensure_initialized(self) -> None: if self._decoder is not None: return try: - import codec + from .. import codec except ImportError as e: raise RuntimeError( "NvH264Decoder requires the native codec. Run " diff --git a/examples/camera_viz/sources/oakd.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/oakd.py similarity index 99% rename from examples/camera_viz/sources/oakd.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/oakd.py index 405ea1488b..dc6e20bcf6 100644 --- a/examples/camera_viz/sources/oakd.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/oakd.py @@ -34,7 +34,7 @@ import numpy as np -from pipeline import Frame, FrameSource, SourceSpec +from ..pipeline import Frame, FrameSource, SourceSpec from ._helpers import alloc_pinned_host, notify, notify_verbose logger = logging.getLogger("isaaccapture.camera_viz.sources.oakd") diff --git a/examples/camera_viz/sources/rtp_h264.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/rtp_h264.py similarity index 99% rename from examples/camera_viz/sources/rtp_h264.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/rtp_h264.py index 4b957f9db1..9086c45bd1 100644 --- a/examples/camera_viz/sources/rtp_h264.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/rtp_h264.py @@ -19,8 +19,8 @@ import time from typing import Optional -from pipeline import Frame, FrameSource, SourceSpec -from transports import RtpH264Receiver +from ..pipeline import Frame, FrameSource, SourceSpec +from ..transports import RtpH264Receiver from ._nv_decode import NvH264Decoder diff --git a/examples/camera_viz/sources/synthetic.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/synthetic.py similarity index 99% rename from examples/camera_viz/sources/synthetic.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/synthetic.py index e0c30fd03f..f2ab1102a1 100644 --- a/examples/camera_viz/sources/synthetic.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/synthetic.py @@ -17,7 +17,7 @@ import time from typing import Optional -from pipeline import Frame, FrameSource, SourceSpec +from ..pipeline import Frame, FrameSource, SourceSpec class SyntheticSource(FrameSource): diff --git a/examples/camera_viz/sources/v4l2.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/v4l2.py similarity index 100% rename from examples/camera_viz/sources/v4l2.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/v4l2.py diff --git a/examples/camera_viz/sources/video_file.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/video_file.py similarity index 100% rename from examples/camera_viz/sources/video_file.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/video_file.py diff --git a/examples/camera_viz/sources/zed.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/zed.py similarity index 99% rename from examples/camera_viz/sources/zed.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/zed.py index e649d569a3..6013c43b8c 100644 --- a/examples/camera_viz/sources/zed.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/sources/zed.py @@ -27,7 +27,7 @@ from dataclasses import dataclass, field from typing import List, Optional -from pipeline import Frame, FrameSource, SourceSpec +from ..pipeline import Frame, FrameSource, SourceSpec from ._helpers import notify, notify_verbose diff --git a/examples/camera_viz/transports/__init__.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/__init__.py similarity index 100% rename from examples/camera_viz/transports/__init__.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/__init__.py diff --git a/examples/camera_viz/transports/_encoder_factory.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_encoder_factory.py similarity index 98% rename from examples/camera_viz/transports/_encoder_factory.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_encoder_factory.py index a301893ee4..cd1a7e0142 100644 --- a/examples/camera_viz/transports/_encoder_factory.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_encoder_factory.py @@ -24,7 +24,7 @@ def _has_native_codec() -> bool: # Both ImportError (no package) and RuntimeError (raised by # codec/__init__.py on missing .so) fall through to GStreamer. try: - import codec # noqa: F401 + from .. import codec # noqa: F401 return True except (ImportError, RuntimeError): diff --git a/examples/camera_viz/transports/_nv12_convert.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv12_convert.py similarity index 100% rename from examples/camera_viz/transports/_nv12_convert.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv12_convert.py diff --git a/examples/camera_viz/transports/_nv_encode.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv_encode.py similarity index 98% rename from examples/camera_viz/transports/_nv_encode.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv_encode.py index 6ba6ef1812..adb503787f 100644 --- a/examples/camera_viz/transports/_nv_encode.py +++ b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv_encode.py @@ -45,7 +45,7 @@ def _ensure_initialized(self, rgba_device_id: int) -> None: return try: - import codec + from .. import codec except ImportError as e: raise RuntimeError( "NvH264Encoder requires the native codec. Run " diff --git a/examples/camera_viz/transports/_nv_encode_gst.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv_encode_gst.py similarity index 100% rename from examples/camera_viz/transports/_nv_encode_gst.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/_nv_encode_gst.py diff --git a/examples/camera_viz/transports/rtp_h264_receiver.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/rtp_h264_receiver.py similarity index 100% rename from examples/camera_viz/transports/rtp_h264_receiver.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/rtp_h264_receiver.py diff --git a/examples/camera_viz/transports/rtp_h264_sender.py b/examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/rtp_h264_sender.py similarity index 100% rename from examples/camera_viz/transports/rtp_h264_sender.py rename to examples/camera_viz/python/isaaccapture_examples/camera_viz/transports/rtp_h264_sender.py diff --git a/examples/camera_viz/scripts/_install_deps.sh b/examples/camera_viz/scripts/_install_deps.sh index a1adfb3caf..c8471defcf 100755 --- a/examples/camera_viz/scripts/_install_deps.sh +++ b/examples/camera_viz/scripts/_install_deps.sh @@ -569,6 +569,13 @@ else uv pip install --python "$PY" --upgrade "${PKGS[@]}" fi +# The example itself, so `python -m isaaccapture_examples.camera_viz` resolves +# without the source directory on sys.path. Editable, so edits need no +# reinstall -- and the native codec .so, built in place below, is picked up +# from the source tree either way. +step "installing camera_viz" +uv pip install --python "$PY" -e "$CAMERA_VIZ_DIR" --no-deps + # ZED SDK ships get_python_api.py which downloads a matching pyzed wheel # and then tries ``pip install`` it (which fails in uv venvs, no pip). # We let that fail and install the wheel ourselves. @@ -595,7 +602,7 @@ fi # Native NVENC/NVDEC codec. Failures are non-fatal: the runtime falls # back to the GStreamer encoder when the native ``.so`` isn't importable. if $WITH_RTP; then - CODEC_DIR="$CAMERA_VIZ_DIR/codec" + CODEC_DIR="$CAMERA_VIZ_DIR/python/isaaccapture_examples/camera_viz/codec" if [[ -d "$CODEC_DIR" ]]; then step "building native NVENC/NVDEC codec" # shellcheck disable=SC1091 @@ -609,7 +616,7 @@ fi # Smoke imports. ``gi`` is in the list under RTP to confirm PyGObject # built and installed cleanly into the venv. -SMOKE_MODS="cupy yaml scipy.spatial.transform" +SMOKE_MODS="isaaccapture_examples.camera_viz cupy yaml scipy.spatial.transform" # ``websockets`` comes from the cloudxr extra and is what the XR default mode # needs to launch the runtime; check it here so a missing extra fails setup # rather than the first ``run --mode xr``. diff --git a/examples/camera_viz/scripts/camera-streamer.service.in b/examples/camera_viz/scripts/camera-streamer.service.in index cea9c2ebd3..616eeb177a 100644 --- a/examples/camera_viz/scripts/camera-streamer.service.in +++ b/examples/camera_viz/scripts/camera-streamer.service.in @@ -13,7 +13,7 @@ Wants=network-online.target Type=simple WorkingDirectory={{WORKDIR}} Environment=PATH={{VENV}}/bin:/usr/local/cuda/bin:/usr/bin:/bin -ExecStart={{VENV}}/bin/python {{WORKDIR}}/camera_streamer.py {{CONFIG}}{{EXTRA_ARGS}} +ExecStart={{VENV}}/bin/python -m isaaccapture_examples.camera_viz.camera_streamer {{CONFIG}}{{EXTRA_ARGS}} Restart=always RestartSec=2 StandardOutput=journal diff --git a/examples/noitom/README.md b/examples/noitom/README.md index b9d6027f93..81c0457150 100644 --- a/examples/noitom/README.md +++ b/examples/noitom/README.md @@ -45,10 +45,11 @@ MCAP script because the recording uses the standard `full_body` channel. ![Noitom full-body recording](assets/record.gif) -Record the Noitom full-body stream: +Install the example once, then record the Noitom full-body stream: ```bash -uv run python examples/noitom/record_noitom_full_body.py \ +uv pip install -e ./examples/noitom +python -m isaaccapture_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap ``` @@ -70,14 +71,22 @@ python -m isaaccapture_examples.mcap_record_replay.replay_full_body \ Run Isaac Lab with the external task registration callback. The task launches `noitom_mocap_plugin` through IsaacTeleop's plugin manager by default. +The callback runs inside Isaac Lab's own interpreter, so install the example +into *that* environment rather than a venv of your own — this replaces the +`PYTHONPATH=` prefix these commands used to carry: + +```bash +cd ~/dependence/IsaacLab3-0 +./isaaclab.sh -p -m pip install -e ~/IsaacTeleop/examples/noitom +``` + ```bash cd ~/dependence/IsaacLab3-0 -PYTHONPATH=~/IsaacTeleop/examples/noitom:$PYTHONPATH \ - ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ +./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \ --visualizer kit \ --xr \ - --external_callback noitom_tasks.register_tasks + --external_callback isaaccapture_examples.noitom.noitom_tasks.register_tasks ``` For advanced manual plugin control, start the plugin yourself and disable @@ -87,12 +96,11 @@ auto-launch in the Isaac Lab terminal: ./install/plugins/noitom_mocap/noitom_mocap_plugin NOITOM_MOCAP_AUTO_LAUNCH=0 \ -PYTHONPATH=~/IsaacTeleop/examples/noitom:$PYTHONPATH \ - ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ +./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \ --visualizer kit \ --xr \ - --external_callback noitom_tasks.register_tasks + --external_callback isaaccapture_examples.noitom.noitom_tasks.register_tasks ``` If you run a dedicated CloudXR runtime yourself, source its environment before diff --git a/examples/noitom/pyproject.toml b/examples/noitom/pyproject.toml new file mode 100644 index 0000000000..881601cffc --- /dev/null +++ b/examples/noitom/pyproject.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaaccapture-examples-noitom" +version = "0.0.0" # Internal example - not versioned +description = "Noitom mocap suit examples: retargeting, Isaac Lab task, MCAP recording" +requires-python = ">=3.11,<3.14" +# isaaclab and gymnasium are deliberately absent: noitom_tasks.py runs inside an +# Isaac Lab environment that already provides them, and neither installs from +# PyPI. Install this example into that interpreter, not a fresh venv. +dependencies = [ + "isaaccapture", + "numpy>=1.23.0", +] + +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `noitom/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/noitom"] +sources = ["python"] diff --git a/examples/noitom/python/isaaccapture_examples/noitom/__init__.py b/examples/noitom/python/isaaccapture_examples/noitom/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/noitom/python/isaaccapture_examples/noitom/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/noitom/noitom_reference_draw.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py similarity index 99% rename from examples/noitom/noitom_reference_draw.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py index 8e389f76dc..5bf0f178fe 100644 --- a/examples/noitom/noitom_reference_draw.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py @@ -13,7 +13,7 @@ from isaaccapture.schema import BodyJoint -from noitom_retargeting import noitom_position_to_isaac +from .noitom_retargeting import noitom_position_to_isaac # G1_29DOF_CFG spawn rot (0,0,0.7071,0.7071): pelvis +X aligns with world +Y (table forward). G1_ROBOT_FORWARD_XY = np.array([0.0, 1.0], dtype=np.float64) diff --git a/examples/noitom/noitom_retargeting.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py similarity index 99% rename from examples/noitom/noitom_retargeting.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py index e7295b1dc4..768a17e66f 100644 --- a/examples/noitom/noitom_retargeting.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py @@ -1456,7 +1456,7 @@ def _aligned_skeleton_positions( settings: NoitomRetargetingSettings, calib_view: NoitomCalibrationView, ) -> dict[int, np.ndarray]: - from noitom_reference_draw import ( + from .noitom_reference_draw import ( ReferenceSkeletonLengths, aligned_reference_skeleton_from_frame, ) diff --git a/examples/noitom/noitom_tasks.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py similarity index 99% rename from examples/noitom/noitom_tasks.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py index 776d9579fb..0eacf837f6 100644 --- a/examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py @@ -37,13 +37,13 @@ from isaaccapture.retargeting_engine.tensor_types import DLDataType, NDArrayType from isaaccapture.teleop_session_manager import PluginConfig -from noitom_retargeting import ( +from .noitom_retargeting import ( ArmIkTargets, NoitomG1Retargeter, NoitomRetargetingSettings, noitom_position_to_isaac, ) -from noitom_reference_draw import ( +from .noitom_reference_draw import ( ReferenceSkeletonLengths, aligned_reference_skeleton_from_frame, ) @@ -168,7 +168,9 @@ def _noitom_settings_from_env() -> NoitomG1Settings: def _plugin_search_paths() -> list[Path]: - base = Path(__file__).resolve().parents[2] + # Six levels up is the repo root -- or the install prefix, which has the + # same shape under examples//python/isaaccapture_examples//. + base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", base / "install" / "plugins", diff --git a/examples/noitom/record_noitom_full_body.py b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py similarity index 93% rename from examples/noitom/record_noitom_full_body.py rename to examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py index 74f77e9f1e..d1cf959149 100644 --- a/examples/noitom/record_noitom_full_body.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py @@ -37,7 +37,9 @@ def _plugin_search_paths() -> list[Path]: - base = Path(__file__).resolve().parents[2] + # Six levels up is the repo root -- or the install prefix, which has the + # same shape under examples//python/isaaccapture_examples//. + base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", base / "install" / "plugins", @@ -70,7 +72,9 @@ def _resolve_output(path_arg: str | None) -> Path: if path_arg: path = Path(path_arg) else: - out_dir = Path(__file__).resolve().parent / "recordings" + # CWD-relative, matching mcap_record_replay: anchoring to __file__ would + # write inside the package, and into site-packages once installed. + out_dir = Path.cwd() / "recordings" path = out_dir / f"noitom_full_body_{datetime.now():%Y%m%d_%H%M%S}.mcap" path.parent.mkdir(parents=True, exist_ok=True) return path diff --git a/examples/oglo_tactile/README.md b/examples/oglo_tactile/README.md index 6779aa2214..89a104c8cf 100644 --- a/examples/oglo_tactile/README.md +++ b/examples/oglo_tactile/README.md @@ -31,10 +31,11 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. ```bash sudo apt install libdbus-1-dev # BlueZ for the BLE plugin sudo apt install pkg-config glslang-tools libvulkan-dev # BUILD_VIZ=ON below - pip install pillow numpy # heatmap renderer - pip install cupy-cuda12x # headset overlay GPU upload (match your CUDA) + uv pip install -e ./examples/oglo_tactile # heatmap renderer + deps + uv pip install -e './examples/oglo_tactile[gpu]' # adds the CUDA overlay upload ``` - `cupy` is only needed for the in-headset overlay; recording works without it. + The `gpu` extra is only needed for the in-headset overlay; recording works + without it. Match your CUDA major version if you edit the pin. 2. **Build** (plugin + viz + python wheel) @@ -76,8 +77,7 @@ python -m isaaccapture.cloudxr.service start --accept-eula # flag needed on fi cd ~/Documents/IsaacTeleop source scripts/setup_cloudxr_env.sh source ~/.cloudxr/run/cloudxr.env # path printed by step above -cd examples/oglo_tactile -python oglo_teleop_record.py +python -m isaaccapture_examples.oglo_tactile.oglo_teleop_record ``` The script: connects both gloves over BLE, starts recording to `oglo_teleop_.mcap`, and shows the **two-hand tactile heatmap** in the diff --git a/examples/oglo_tactile/pyproject.toml b/examples/oglo_tactile/pyproject.toml new file mode 100644 index 0000000000..d2a9ddd0ad --- /dev/null +++ b/examples/oglo_tactile/pyproject.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaaccapture-examples-oglo-tactile" +version = "0.0.0" # Internal example - not versioned +description = "OGLO tactile glove: heatmap renderer and teleop recording" +requires-python = ">=3.11,<3.14" +dependencies = [ + "isaaccapture[cloudxr]", + "numpy>=1.23.0", + "pillow", +] + +[project.optional-dependencies] +# The in-headset overlay uploads the heatmap through CUDA; match your CUDA +# major version. Without it the renderer still writes PNGs. +gpu = ["cupy-cuda12x"] + +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `oglo_tactile/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/oglo_tactile"] +sources = ["python"] diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/oglo_tactile/oglo_heatmap.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py similarity index 100% rename from examples/oglo_tactile/oglo_heatmap.py rename to examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py diff --git a/examples/oglo_tactile/oglo_teleop_record.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py similarity index 97% rename from examples/oglo_tactile/oglo_teleop_record.py rename to examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py index 8f34c15710..035dfb680c 100755 --- a/examples/oglo_tactile/oglo_teleop_record.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py @@ -36,7 +36,7 @@ import isaaccapture.deviceio as deviceio import isaaccapture.viz as viz -from oglo_heatmap import NUM_TAXELS, Normalizer, TactileHeatmapRenderer +from .oglo_heatmap import NUM_TAXELS, Normalizer, TactileHeatmapRenderer OVERLAY_W, OVERLAY_H = 1024, 512 COLLECTION_PREFIX = "oglo" @@ -50,8 +50,12 @@ def _on_signal(signum: int, frame: FrameType | None) -> None: def _default_plugin_bin() -> str: + # Only meaningful from a source checkout: examples/oglo_tactile/python/ + # isaaccapture_examples/oglo_tactile/ is five levels below the repo root. An + # installed copy lands in site-packages, where this finds nothing and the + # PATH fallback below takes over. here = Path(__file__).resolve() - root = here.parents[2] # repo root + root = here.parents[5] for cand in ( root / "build/src/plugins/oglo_tactile/oglo_tactile_plugin", root / "install/plugins/oglo_tactile/oglo_tactile_plugin", diff --git a/src/plugins/noitom_mocap/README.md b/src/plugins/noitom_mocap/README.md index 8c3e9dd4b1..d564ac4f78 100644 --- a/src/plugins/noitom_mocap/README.md +++ b/src/plugins/noitom_mocap/README.md @@ -60,7 +60,8 @@ Record through the Noitom example wrapper and replay with the shared MCAP script: ```bash -uv run python examples/noitom/record_noitom_full_body.py \ +uv pip install -e ./examples/noitom +python -m isaaccapture_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap uv pip install -e ./examples/mcap_record_replay diff --git a/tests/python/examples/camera_viz/conftest.py b/tests/python/examples/camera_viz/conftest.py index 2c1ec96f82..62370f3018 100644 --- a/tests/python/examples/camera_viz/conftest.py +++ b/tests/python/examples/camera_viz/conftest.py @@ -1,10 +1,12 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Tests live under tests/python/examples/camera_viz/ but import from -# examples/camera_viz/ (the example's pipeline / sources modules). -# Prepend the parent dir so those imports resolve against the in-tree -# source rather than any installed copy. +# Resolve `isaaccapture_examples.camera_viz` against the in-tree source, and with +# it the codec .so built in place by codec/build.sh. +# +# python/, not python/isaaccapture_examples/: that is a PEP 420 namespace. Do not +# add an __init__.py to make an import work -- it breaks the installed wheel's +# ability to share the namespace. import sys from pathlib import Path @@ -15,4 +17,4 @@ from repo_paths import repo_root # noqa: E402 -sys.path.insert(0, str(repo_root() / "examples" / "camera_viz")) +sys.path.insert(0, str(repo_root() / "examples" / "camera_viz" / "python")) diff --git a/tests/python/examples/camera_viz/test_cloudxr_env.py b/tests/python/examples/camera_viz/test_cloudxr_env.py index 2b242f5ed9..2dc6e162b9 100644 --- a/tests/python/examples/camera_viz/test_cloudxr_env.py +++ b/tests/python/examples/camera_viz/test_cloudxr_env.py @@ -10,7 +10,7 @@ import pytest -import cloudxr_env +from isaaccapture_examples.camera_viz import cloudxr_env # ── YAML -> env ─────────────────────────────────────────────────────── diff --git a/tests/python/examples/camera_viz/test_config.py b/tests/python/examples/camera_viz/test_config.py index ca323d4465..4595795006 100644 --- a/tests/python/examples/camera_viz/test_config.py +++ b/tests/python/examples/camera_viz/test_config.py @@ -6,7 +6,7 @@ import pytest -import config +from isaaccapture_examples.camera_viz import config def test_shape_config_carries_the_equirect_heading(): diff --git a/tests/python/examples/camera_viz/test_controls.py b/tests/python/examples/camera_viz/test_controls.py index dc5d41660b..d0157c07a0 100644 --- a/tests/python/examples/camera_viz/test_controls.py +++ b/tests/python/examples/camera_viz/test_controls.py @@ -14,15 +14,15 @@ import pytest -from controls import ( +from isaaccapture_examples.camera_viz.controls import ( LOCK_MODE_CYCLE, SHAPE_CYCLE, ControllerControls, ControlsConfig, ControlTarget, + controls_config_from_yaml, ) -from controls import controls_config_from_yaml -from placements import ( +from isaaccapture_examples.camera_viz.placements import ( PlacementConfig, build as build_placement, heading_deg, @@ -278,7 +278,7 @@ def test_offset_can_never_reach_divergent_parallax(): must diverge, which they physically cannot do. The ceiling is derived from the measured IPD, so a stick held to the stop stays clear of it however generous the configured limit is.""" - from controls import MAX_OFFSET_FRACTION_OF_IPD + from isaaccapture_examples.camera_viz.controls import MAX_OFFSET_FRACTION_OF_IPD target = _stereo_target(plane_distance=0.0) controls, _ = _make( @@ -712,14 +712,14 @@ def test_held_stick_log_is_throttled(capsys): def test_summarize_says_a_shared_value_once(): """Bindings apply to every camera at once, so values normally agree — repeating them per camera is what overflowed the HUD bar.""" - from controls import summarize + from isaaccapture_examples.camera_viz.controls import summarize assert summarize([("zed", "52.0 mm")]) == "52.0 mm" assert summarize([("a", "52.0 mm"), ("b", "52.0 mm")]) == "52.0 mm (2 cameras)" def test_summarize_lists_cameras_that_disagree(): - from controls import summarize + from isaaccapture_examples.camera_viz.controls import summarize out = summarize([("front", "52.0 mm"), ("rear", "10.0 mm")]) assert "front 52.0 mm" in out and "rear 10.0 mm" in out @@ -727,14 +727,18 @@ def test_summarize_lists_cameras_that_disagree(): def test_summarize_handles_nothing_changing(): - from controls import summarize + from isaaccapture_examples.camera_viz.controls import summarize assert summarize([]) == "" def test_multi_camera_baseline_message_stays_one_line(capsys): """End-to-end: three cameras must not produce three repeated values.""" - from controls.hud import _TEXT_W, _TITLE, split_message + from isaaccapture_examples.camera_viz.controls.hud import ( + _TEXT_W, + _TITLE, + split_message, + ) targets = [_stereo_target(n) for n in ("front", "left", "right")] controls, _ = _make(targets, ControlsConfig(deadzone=0.0)) @@ -757,13 +761,13 @@ def test_multi_camera_baseline_message_stays_one_line(capsys): def test_suggestion_comes_from_the_plane_distance_and_ipd(): """d = ipd * (1 - Z / FAR_TARGET_M): a 1 m plane at a 63 mm IPD suggests 6.3 * 0.9 = 5.67 cm.""" - from controls import FAR_TARGET_M + from isaaccapture_examples.camera_viz.controls import FAR_TARGET_M target = _stereo_target() target.placement_config = PlacementConfig(distance=1.0) controls, _ = _make([target]) controls.step(0.0, 63.0) - from controls import PLANE_DISTANCE_STEP_CM + from isaaccapture_examples.camera_viz.controls import PLANE_DISTANCE_STEP_CM raw = 6.3 * (1.0 - 1.0 / FAR_TARGET_M) expected = round(raw / PLANE_DISTANCE_STEP_CM) * PLANE_DISTANCE_STEP_CM @@ -798,7 +802,7 @@ def test_suggestion_never_exceeds_the_divergence_ceiling(): target.placement_config = PlacementConfig(distance=0.05) # absurdly near controls, _ = _make([target]) controls.step(0.0, 63.0) - from controls import PLANE_DISTANCE_STEP_CM + from isaaccapture_examples.camera_viz.controls import PLANE_DISTANCE_STEP_CM # Rounding to a step can land half a step above the raw ceiling. assert controls._suggested_plane_distance_cm(target) <= ( @@ -845,7 +849,7 @@ def test_stick_still_moves_at_display_frame_rates(): def test_the_layer_only_ever_sees_whole_steps(): - from controls import PLANE_DISTANCE_STEP_CM + from isaaccapture_examples.camera_viz.controls import PLANE_DISTANCE_STEP_CM target = _stereo_target(plane_distance=0.0) controls, _ = _make( @@ -912,7 +916,7 @@ def test_equirect_gap_is_skipped_because_it_does_nothing(): def test_quad_and_cylinder_axes_do_different_things(): """The point of the rework: each shape's two axes must not collapse into one control.""" - from controls import SHAPE_PARAMS + from isaaccapture_examples.camera_viz.controls import SHAPE_PARAMS for shape, (x_name, y_name) in SHAPE_PARAMS.items(): assert x_name != y_name, shape @@ -926,7 +930,7 @@ def test_height_change_moves_the_plane_now_not_at_the_next_resnap(lock_mode): """world cached the finished placement and ignored retuning outright; lazy only recomputed on a re-snap, so a height change sat unapplied until you happened to look away.""" - from placements import PlacementConfig, build + from isaaccapture_examples.camera_viz.placements import PlacementConfig, build head_pos, head_q = (0.0, 1.5, 0.0), (1.0, 0.0, 0.0, 0.0) cfg = PlacementConfig(distance=1.0) @@ -940,7 +944,7 @@ def test_height_change_moves_the_plane_now_not_at_the_next_resnap(lock_mode): @pytest.mark.parametrize("lock_mode", ["world", "head", "gimbal", "lazy"]) def test_size_change_lands_immediately_too(lock_mode): - from placements import PlacementConfig, build + from isaaccapture_examples.camera_viz.placements import PlacementConfig, build head_pos, head_q = (0.0, 1.5, 0.0), (1.0, 0.0, 0.0, 0.0) cfg = PlacementConfig(distance=1.0) @@ -953,7 +957,7 @@ def test_size_change_lands_immediately_too(lock_mode): def test_world_lock_still_pins_the_plane_across_a_retune(): """Retuning must not become an excuse to re-place a world-locked plane.""" - from placements import PlacementConfig, build + from isaaccapture_examples.camera_viz.placements import PlacementConfig, build cfg = PlacementConfig(distance=1.0) strategy = build("world", cfg) diff --git a/tests/python/examples/camera_viz/test_dashboard.py b/tests/python/examples/camera_viz/test_dashboard.py index e89b1ef9e8..114cd84146 100644 --- a/tests/python/examples/camera_viz/test_dashboard.py +++ b/tests/python/examples/camera_viz/test_dashboard.py @@ -8,7 +8,7 @@ import os import re -from dashboard import CameraRow, Dashboard, Snapshot +from isaaccapture_examples.camera_viz.dashboard import CameraRow, Dashboard, Snapshot ESCAPE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") diff --git a/tests/python/examples/camera_viz/test_hud.py b/tests/python/examples/camera_viz/test_hud.py index bafc68a462..2de18f3f8d 100644 --- a/tests/python/examples/camera_viz/test_hud.py +++ b/tests/python/examples/camera_viz/test_hud.py @@ -7,8 +7,8 @@ import numpy as np -from controls import hud -from controls.hud import ( +from isaaccapture_examples.camera_viz.controls import hud +from isaaccapture_examples.camera_viz.controls.hud import ( _BODY, _H, _TITLE, diff --git a/tests/python/examples/camera_viz/test_paired_frame_source_race.py b/tests/python/examples/camera_viz/test_paired_frame_source_race.py index 4a353be8ce..e90416d86b 100644 --- a/tests/python/examples/camera_viz/test_paired_frame_source_race.py +++ b/tests/python/examples/camera_viz/test_paired_frame_source_race.py @@ -17,8 +17,8 @@ import time from typing import Optional -from pipeline import Frame, FrameSource, SourceSpec -from sources._helpers import PairedFrameSource +from isaaccapture_examples.camera_viz.pipeline import Frame, FrameSource, SourceSpec +from isaaccapture_examples.camera_viz.sources._helpers import PairedFrameSource class FakeSource(FrameSource): diff --git a/tests/python/examples/camera_viz/test_placements.py b/tests/python/examples/camera_viz/test_placements.py index fd937cb4a4..4bd471da90 100644 --- a/tests/python/examples/camera_viz/test_placements.py +++ b/tests/python/examples/camera_viz/test_placements.py @@ -14,8 +14,8 @@ import pytest -from placements import PlacementConfig, build -from placements._math import rotate_vec +from isaaccapture_examples.camera_viz.placements import PlacementConfig, build +from isaaccapture_examples.camera_viz.placements._math import rotate_vec MODES = ("world", "head", "gimbal", "lazy") diff --git a/tests/python/examples/camera_viz/test_runner_controls.py b/tests/python/examples/camera_viz/test_runner_controls.py index dfd2f0a5aa..8e99fddeaf 100644 --- a/tests/python/examples/camera_viz/test_runner_controls.py +++ b/tests/python/examples/camera_viz/test_runner_controls.py @@ -10,7 +10,7 @@ from __future__ import annotations -from pipeline.runner import VizRunner +from isaaccapture_examples.camera_viz.pipeline.runner import VizRunner class FakeLayer: @@ -115,7 +115,7 @@ def test_the_panel_is_drawn_off_the_submit_thread(): that stops the video feed, so the panel gets its own thread.""" import inspect - from pipeline.runner import VizRunner + from isaaccapture_examples.camera_viz.pipeline.runner import VizRunner submit_src = inspect.getsource(VizRunner._submit_loop_inner) assert "_print_stats" not in submit_src @@ -127,7 +127,7 @@ def test_a_broken_panel_write_does_not_kill_the_stats_thread(monkeypatch): panel is decoration; losing it must not take the stats thread with it.""" import threading - from pipeline import runner as runner_mod + from isaaccapture_examples.camera_viz.pipeline import runner as runner_mod monkeypatch.setattr(runner_mod, "LIVE_STATS_PERIOD_S", 0.005) diff --git a/tests/python/examples/camera_viz/test_runner_health.py b/tests/python/examples/camera_viz/test_runner_health.py index 903e1ad781..f5816de2da 100644 --- a/tests/python/examples/camera_viz/test_runner_health.py +++ b/tests/python/examples/camera_viz/test_runner_health.py @@ -6,7 +6,7 @@ import pytest -from pipeline import VizRunner +from isaaccapture_examples.camera_viz.pipeline import VizRunner class _AliveThread: diff --git a/tests/python/examples/camera_viz/test_synthetic_offscreen.py b/tests/python/examples/camera_viz/test_synthetic_offscreen.py index 1eea586a6a..7a99d2214e 100644 --- a/tests/python/examples/camera_viz/test_synthetic_offscreen.py +++ b/tests/python/examples/camera_viz/test_synthetic_offscreen.py @@ -17,7 +17,7 @@ import isaaccapture.viz as viz -from sources import build_local_camera +from isaaccapture_examples.camera_viz.sources import build_local_camera def _gpu_and_cupy_available() -> bool: diff --git a/tests/python/examples/camera_viz/test_video_file_source.py b/tests/python/examples/camera_viz/test_video_file_source.py index 0b1186218c..52b750d2c5 100644 --- a/tests/python/examples/camera_viz/test_video_file_source.py +++ b/tests/python/examples/camera_viz/test_video_file_source.py @@ -16,7 +16,10 @@ cv2 = pytest.importorskip("cv2") -from sources import build_local_camera, resolve_video_paths # noqa: E402 +from isaaccapture_examples.camera_viz.sources import ( # noqa: E402 + build_local_camera, + resolve_video_paths, +) def _cuda_available() -> bool: