-
Notifications
You must be signed in to change notification settings - Fork 107
Migrate viewport recording to Isaac Lab #1221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Copyright (c) 2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). | ||
| # All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from types import SimpleNamespace | ||
|
|
||
| from isaaclab_arena.video.video_recording import VideoRecordingCfg, configure_env_for_video | ||
|
|
||
|
|
||
| def _env_cfg(): | ||
| return SimpleNamespace( | ||
| episode_length_s=10.0, | ||
| decimation=2, | ||
| sim=SimpleNamespace(dt=0.1), | ||
| video_recorders=[], | ||
| ) | ||
|
|
||
|
|
||
| def test_configure_env_for_viewport_video_uses_native_recorder(tmp_path): | ||
| env_cfg = _env_cfg() | ||
|
|
||
| configure_env_for_video( | ||
| env_cfg, | ||
| VideoRecordingCfg(record_viewport_video=True, video_base_dir=str(tmp_path)), | ||
| num_steps=25, | ||
| num_episodes=None, | ||
| ) | ||
|
|
||
| assert len(env_cfg.video_recorders) == 1 | ||
| recorder_cfg = env_cfg.video_recorders[0] | ||
| assert recorder_cfg.source == "visualizer:kit" | ||
| assert recorder_cfg.output_dir == str(tmp_path) | ||
| assert recorder_cfg.output_filename_prefix == "viewport" | ||
| assert recorder_cfg.video_length == 25 | ||
|
|
||
|
|
||
| def test_configure_env_for_viewport_video_sizes_episode_rollout(tmp_path): | ||
| env_cfg = _env_cfg() | ||
|
|
||
| configure_env_for_video( | ||
| env_cfg, | ||
| VideoRecordingCfg(record_viewport_video=True, video_base_dir=str(tmp_path)), | ||
| num_steps=None, | ||
| num_episodes=3, | ||
| ) | ||
|
|
||
| assert env_cfg.video_recorders[0].video_length == 150 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||
|
|
||||||
| import dataclasses | ||||||
| import datetime | ||||||
| import math | ||||||
| import os | ||||||
|
|
||||||
|
|
||||||
|
|
@@ -31,11 +32,6 @@ def enabled(self) -> bool: | |||||
| """Whether any recorder is requested.""" | ||||||
| return self.record_viewport_video or self.record_camera_video | ||||||
|
|
||||||
| @property | ||||||
| def render_mode(self) -> str | None: | ||||||
| """The ``render_mode`` the env must be built with to capture the viewport video.""" | ||||||
| return "rgb_array" if self.record_viewport_video else None | ||||||
|
|
||||||
|
|
||||||
| def timestamped_run_dir(base_dir: str) -> str: | ||||||
| """Append a reverse-dated subdirectory to ``base_dir``, e.g. ``base_dir/2026-06-16_14-42-54``. | ||||||
|
|
@@ -47,15 +43,42 @@ def timestamped_run_dir(base_dir: str) -> str: | |||||
| return os.path.join(base_dir, timestamp) | ||||||
|
|
||||||
|
|
||||||
| def _resolve_video_length(env, num_steps: int | None, num_episodes: int | None) -> int: | ||||||
| """Number of env steps to record: the step budget, or one episode's worth per episode. | ||||||
| def _resolve_video_length(env_cfg, num_steps: int | None, num_episodes: int | None) -> int: | ||||||
| """Number of env steps to record from the rollout limit and environment config. | ||||||
|
|
||||||
| ``max_episode_length`` is in environment steps, which matches the rollout cadence. | ||||||
| ``episode_length_s / (sim.dt * decimation)`` is the configured maximum episode | ||||||
| length in environment steps. | ||||||
| """ | ||||||
| if num_steps is not None: | ||||||
| return num_steps | ||||||
| assert num_episodes is not None, "Cannot determine video length: both num_steps and num_episodes are None." | ||||||
| return num_episodes * env.unwrapped.max_episode_length | ||||||
| max_episode_length = math.ceil(env_cfg.episode_length_s / (env_cfg.sim.dt * env_cfg.decimation)) | ||||||
| return num_episodes * max_episode_length | ||||||
|
|
||||||
|
|
||||||
| def configure_env_for_video( | ||||||
| env_cfg, | ||||||
| video_cfg: VideoRecordingCfg, | ||||||
| num_steps: int | None, | ||||||
| num_episodes: int | None, | ||||||
| ) -> None: | ||||||
| """Add requested native video recorders to an Isaac Lab environment config.""" | ||||||
| if not video_cfg.record_viewport_video: | ||||||
| return | ||||||
|
|
||||||
| from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg | ||||||
|
|
||||||
| os.makedirs(video_cfg.video_base_dir, exist_ok=True) | ||||||
| video_length = _resolve_video_length(env_cfg, num_steps, num_episodes) | ||||||
| env_cfg.video_recorders.append( | ||||||
| VideoRecorderCfg( | ||||||
| source="visualizer:kit", | ||||||
| output_dir=video_cfg.video_base_dir, | ||||||
| output_filename_prefix="viewport", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Rebuilds overwrite each other's viewport video The prefix is fixed, and Isaac Lab's recorder restarts its clip index at 0 for each new env (
Suggested change
|
||||||
| video_length=video_length, | ||||||
| ) | ||||||
| ) | ||||||
| print(f"Recording {video_length}-step viewport video to: {video_cfg.video_base_dir}") | ||||||
|
|
||||||
|
|
||||||
| def wrap_env_for_video( | ||||||
|
|
@@ -64,36 +87,22 @@ def wrap_env_for_video( | |||||
| num_steps: int | None, | ||||||
| num_episodes: int | None, | ||||||
| ): | ||||||
| """Wrap ``env`` with the recorders enabled in ``video_cfg`` and return the wrapped env. | ||||||
| """Wrap ``env`` with the camera-observation recorder when requested. | ||||||
|
|
||||||
| Returns ``env`` unchanged when no recorder is requested. ``num_steps`` and ``num_episodes`` | ||||||
| are mutually exclusive and size the viewport video. | ||||||
| Viewport recording is configured natively before environment construction by | ||||||
| :func:`configure_env_for_video`. | ||||||
|
|
||||||
| Args: | ||||||
| env: The env to wrap. | ||||||
| video_cfg: The video recording configuration struct. | ||||||
| num_steps: Step budget for the rollout, or ``None`` when episode-driven. | ||||||
| num_episodes: Episode budget for the rollout, or ``None`` when step-driven. | ||||||
| num_steps: Unused; retained for call-site compatibility. | ||||||
| num_episodes: Unused; retained for call-site compatibility. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 Leftovers from the old viewport path Three bits are dead now: these two parameters (all three call sites are in-repo, so they can just be dropped), the |
||||||
| """ | ||||||
| if not video_cfg.enabled: | ||||||
| if not video_cfg.record_camera_video: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 This breaks the sim-preview viewport recording
|
||||||
| return env | ||||||
|
|
||||||
| os.makedirs(video_cfg.video_base_dir, exist_ok=True) | ||||||
|
|
||||||
| # Record the kit viewport (via env.render()). | ||||||
| if video_cfg.record_viewport_video: | ||||||
| from gymnasium.wrappers import RecordVideo | ||||||
|
|
||||||
| video_length = _resolve_video_length(env, num_steps, num_episodes) | ||||||
| env = RecordVideo( | ||||||
| env, | ||||||
| video_folder=video_cfg.video_base_dir, | ||||||
| step_trigger=lambda step: step == 0, | ||||||
| video_length=video_length, | ||||||
| disable_logger=True, | ||||||
| ) | ||||||
| print(f"Recording {video_length}-step viewport video to: {video_cfg.video_base_dir}") | ||||||
|
|
||||||
| # Record the embodiment-mounted cameras (from obs["camera_obs"]), | ||||||
| # flushed at each episode reset rather than after a fixed number of steps. | ||||||
| if video_cfg.record_camera_video: | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Comment doesn't match the line below it
This line builds the arena builder, not the policy — the policy is built at line 220. The ordering the comment describes belongs up by that call.