Record3D

Parse and stream Record3D captures. To get demo data, see ../assets/download_assets.sh.

Features:

  • viser.extras Record3D parser for RGBD data

  • Point cloud visualization from depth maps

  • Camera pose trajectory display

  • Temporal playback controls with scrubbing

Note

This example requires external assets. To download them, run:

git clone -b v1.0.28 https://github.com/viser-project/viser.git
cd viser/examples
./assets/download_assets.sh
python 04_demos/00_record3d_visualizer.py  # With viser installed.

Source: examples/04_demos/00_record3d_visualizer.py

Record3D

Code

  1import time
  2from pathlib import Path
  3
  4import numpy as np
  5import tyro
  6from tqdm.auto import tqdm
  7
  8import viser
  9import viser.extras
 10import viser.transforms as tf
 11
 12
 13def main(
 14    data_path: Path = Path(__file__).parent / "../assets/record3d_dance",
 15    downsample_factor: int = 4,
 16    max_frames: int = 100,
 17    share: bool = False,
 18) -> None:
 19    server = viser.ViserServer()
 20    server.initial_camera.position = (1.2, 1.2, 1.2)
 21    if share:
 22        server.request_share_url()
 23
 24    print("Loading frames!")
 25    loader = viser.extras.Record3dLoader(data_path)
 26    num_frames = min(max_frames, loader.num_frames())
 27
 28    # Initial camera pose.
 29    @server.on_client_connect
 30    def _(client: viser.ClientHandle) -> None:
 31        client.camera.position = (-1.554, -1.013, 1.142)
 32        client.camera.look_at = (-0.005, 2.283, -0.156)
 33
 34    # Add playback UI.
 35    with server.gui.add_folder("Playback"):
 36        gui_point_size = server.gui.add_slider(
 37            "Point size",
 38            min=0.001,
 39            max=0.02,
 40            step=1e-3,
 41            initial_value=0.01,
 42        )
 43        gui_timestep = server.gui.add_slider(
 44            "Timestep",
 45            min=0,
 46            max=num_frames - 1,
 47            step=1,
 48            initial_value=0,
 49            disabled=True,
 50        )
 51        gui_next_frame = server.gui.add_button("Next Frame", disabled=True)
 52        gui_prev_frame = server.gui.add_button("Prev Frame", disabled=True)
 53        gui_playing = server.gui.add_checkbox("Playing", True)
 54        gui_framerate = server.gui.add_slider(
 55            "FPS", min=1, max=60, step=0.1, initial_value=loader.fps
 56        )
 57        gui_framerate_options = server.gui.add_button_group(
 58            "FPS options", ("10", "20", "30", "60")
 59        )
 60
 61    # Use spacebar to toggle play/pause.
 62    play_pause = server.gui.add_command(label="Toggle Play / Pause", hotkey="space")
 63
 64    @play_pause.on_trigger
 65    def _(_) -> None:
 66        gui_playing.value = not gui_playing.value
 67
 68    # Frame step buttons.
 69    @gui_next_frame.on_click
 70    def _(_) -> None:
 71        gui_timestep.value = (gui_timestep.value + 1) % num_frames
 72
 73    @gui_prev_frame.on_click
 74    def _(_) -> None:
 75        gui_timestep.value = (gui_timestep.value - 1) % num_frames
 76
 77    # Disable frame controls when we're playing.
 78    @gui_playing.on_update
 79    def _(_) -> None:
 80        gui_timestep.disabled = gui_playing.value
 81        gui_next_frame.disabled = gui_playing.value
 82        gui_prev_frame.disabled = gui_playing.value
 83
 84    # Set the framerate when we click one of the options.
 85    @gui_framerate_options.on_click
 86    def _(_) -> None:
 87        gui_framerate.value = int(gui_framerate_options.value)
 88
 89    prev_timestep = gui_timestep.value
 90
 91    # Toggle frame visibility when the timestep slider changes.
 92    @gui_timestep.on_update
 93    def _(_) -> None:
 94        nonlocal prev_timestep
 95        current_timestep = gui_timestep.value
 96        with server.atomic():
 97            # Toggle visibility.
 98            frame_nodes[current_timestep].visible = True
 99            frame_nodes[prev_timestep].visible = False
100        prev_timestep = current_timestep
101        server.flush()  # Optional!
102
103    # Load in frames.
104    server.scene.add_frame(
105        "/frames",
106        wxyz=tf.SO3.exp(np.array([np.pi / 2.0, 0.0, 0.0])).wxyz,
107        position=(0, 0, 0),
108        show_axes=False,
109    )
110    frame_nodes: list[viser.FrameHandle] = []
111    point_nodes: list[viser.PointCloudHandle] = []
112    for i in tqdm(range(num_frames)):
113        frame = loader.get_frame(i)
114        position, color = frame.get_point_cloud(downsample_factor)
115
116        # Add base frame.
117        frame_nodes.append(server.scene.add_frame(f"/frames/t{i}", show_axes=False))
118
119        # Place the point cloud in the frame.
120        point_nodes.append(
121            server.scene.add_point_cloud(
122                name=f"/frames/t{i}/point_cloud",
123                points=position,
124                colors=color,
125                point_size=gui_point_size.value,
126                point_shape="rounded",
127            )
128        )
129
130        # Place the frustum.
131        fov = 2 * np.arctan2(frame.rgb.shape[0] / 2, frame.K[0, 0])
132        aspect = frame.rgb.shape[1] / frame.rgb.shape[0]
133        server.scene.add_camera_frustum(
134            f"/frames/t{i}/frustum",
135            fov=fov,
136            aspect=aspect,
137            scale=0.15,
138            image=frame.rgb[::downsample_factor, ::downsample_factor],
139            wxyz=tf.SO3.from_matrix(frame.T_world_camera[:3, :3]).wxyz,
140            position=frame.T_world_camera[:3, 3],
141        )
142
143        # Add some axes.
144        server.scene.add_frame(
145            f"/frames/t{i}/frustum/axes",
146            axes_length=0.05,
147            axes_radius=0.005,
148        )
149
150    # Hide all but the current frame.
151    for i, frame_node in enumerate(frame_nodes):
152        frame_node.visible = i == gui_timestep.value
153
154    # Playback update loop.
155    prev_timestep = gui_timestep.value
156    while True:
157        # Update the timestep if we're playing.
158        if gui_playing.value:
159            gui_timestep.value = (gui_timestep.value + 1) % num_frames
160
161        # Update point size of both this timestep and the next one! There's
162        # redundancy here, but this will be optimized out internally by viser.
163        #
164        # We update the point size for the next timestep so that it will be
165        # immediately available when we toggle the visibility.
166        point_nodes[gui_timestep.value].point_size = gui_point_size.value
167        point_nodes[
168            (gui_timestep.value + 1) % num_frames
169        ].point_size = gui_point_size.value
170
171        time.sleep(1.0 / gui_framerate.value)
172
173
174if __name__ == "__main__":
175    tyro.cli(main)