Record3D¶
Parse and stream Record3D captures. To get demo data, see ../assets/download_assets.sh.
Features:
viser.extrasRecord3D parser for RGBD dataPoint 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
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)