vuer-cli 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,483 @@
1
+ """Visualize GLB model with camera frustums and optional point cloud."""
2
+
3
+ import warnings
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from ..utils import print_error
9
+
10
+ # Suppress numpy warnings for matrix operations
11
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
12
+
13
+
14
+ def create_camera_positions_around_center(radius=1.0, height=0.0):
15
+ """Create four camera positions around a center point at 90-degree intervals."""
16
+ import numpy as np
17
+
18
+ positions = []
19
+ for angle in [0, np.pi / 2, np.pi, 3 * np.pi / 2]:
20
+ x = radius * np.cos(angle)
21
+ y = radius * np.sin(angle)
22
+ z = height
23
+ positions.append([x, y, z])
24
+ return positions
25
+
26
+
27
+ def create_camera_rotations_looking_at_center():
28
+ """Create rotations for cameras to look at the center point."""
29
+ import numpy as np
30
+
31
+ rotations = [
32
+ [0, -np.pi / 2, np.pi / 2],
33
+ [np.pi / 2, 0, 0],
34
+ [-np.pi / 2, np.pi / 2, 0],
35
+ [np.pi / 2, -np.pi, np.pi],
36
+ ]
37
+ return rotations
38
+
39
+
40
+ def load_scene_pointcloud(
41
+ data_dir: str = "demcap_data",
42
+ camera_names: Optional[List[str]] = None,
43
+ max_frames: Optional[int] = None,
44
+ frame_step: int = 5,
45
+ pixel_step: int = 5,
46
+ max_depth: float = 10.0,
47
+ color_by_camera: bool = False,
48
+ ):
49
+ """Load and merge point clouds from multiple cameras into a single scene point cloud."""
50
+ import cv2
51
+ import numpy as np
52
+
53
+ from .ptc_utils import PointCloudGenerator, load_robot_poses, load_static_transforms
54
+
55
+ data_path = Path(data_dir)
56
+
57
+ if not data_path.exists():
58
+ return None, None
59
+
60
+ if camera_names is None:
61
+ camera_names = ["nav_front_d455", "nav_right_d455"]
62
+
63
+ try:
64
+ robot_pose_interpolator = load_robot_poses(data_path)
65
+ pose_time_min = robot_pose_interpolator.times[0]
66
+ pose_time_max = robot_pose_interpolator.times[-1]
67
+ except Exception:
68
+ return None, None
69
+
70
+ camera_color_map = {
71
+ "nav_front_d455": np.array([0.0, 0.0, 1.0], dtype=np.float32),
72
+ "nav_right_d455": np.array([1.0, 0.0, 0.0], dtype=np.float32),
73
+ "nav_left_d455": np.array([0.0, 1.0, 0.0], dtype=np.float32),
74
+ "nav_rear_d455": np.array([1.0, 1.0, 0.0], dtype=np.float32),
75
+ }
76
+
77
+ all_points = []
78
+ all_colors = []
79
+
80
+ reference_camera = camera_names[0]
81
+ first_camera_transform = np.eye(4)
82
+
83
+ for cam_name in camera_names:
84
+ try:
85
+ static_transform = load_static_transforms(data_path, cam_name)
86
+ generator = PointCloudGenerator(data_dir=str(data_path), camera_name=cam_name)
87
+
88
+ pairs = generator._find_rgb_depth_pairs()
89
+ pairs = [p for p in pairs if pose_time_min <= p["timestamp_s"] <= pose_time_max]
90
+
91
+ if max_frames:
92
+ pairs = pairs[:max_frames:frame_step]
93
+ else:
94
+ pairs = pairs[::frame_step]
95
+
96
+ if cam_name == reference_camera and len(pairs) > 0:
97
+ first_pair = pairs[0]
98
+ t_world_to_base = robot_pose_interpolator.interpolate_pose(
99
+ first_pair["timestamp_s"]
100
+ )
101
+ if t_world_to_base is not None:
102
+ t_base_to_camera = static_transform.get_transform()
103
+ first_camera_transform = t_world_to_base @ t_base_to_camera
104
+
105
+ for frame_idx, pair in enumerate(pairs):
106
+ rgb = cv2.imread(str(pair["rgb_file"]))
107
+ rgb = cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB)
108
+ depth = cv2.imread(str(pair["depth_file"]), cv2.IMREAD_UNCHANGED)
109
+
110
+ t_world_to_base = robot_pose_interpolator.interpolate_pose(pair["timestamp_s"])
111
+ if t_world_to_base is None:
112
+ continue
113
+
114
+ t_base_to_camera = static_transform.get_transform()
115
+ t_world_to_camera = t_world_to_base @ t_base_to_camera
116
+
117
+ points_cam, colors = generator.backproject_rgbd(
118
+ rgb, depth, max_depth, pixel_step
119
+ )
120
+
121
+ if len(points_cam) == 0:
122
+ continue
123
+
124
+ if color_by_camera:
125
+ camera_color = camera_color_map.get(
126
+ cam_name, np.array([0.5, 0.5, 0.5], dtype=np.float32)
127
+ )
128
+ colors = np.tile(camera_color, (len(points_cam), 1))
129
+
130
+ points_cam_h = np.hstack([points_cam, np.ones((len(points_cam), 1))])
131
+ points_world_h = (t_world_to_camera @ points_cam_h.T).T
132
+ points_ref_h = (np.linalg.inv(first_camera_transform) @ points_world_h.T).T
133
+
134
+ flip_xy = np.array([[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
135
+ points_ref_threejs_h = (flip_xy @ points_ref_h.T).T
136
+ points_ref = points_ref_threejs_h[:, :3]
137
+
138
+ all_points.append(points_ref.astype(np.float32))
139
+ all_colors.append(colors.astype(np.float32))
140
+
141
+ except Exception:
142
+ continue
143
+
144
+ if len(all_points) == 0:
145
+ return None, None
146
+
147
+ combined_points = np.vstack(all_points)
148
+ combined_colors = np.vstack(all_colors)
149
+
150
+ total_points_before = len(combined_points)
151
+
152
+ y_values = combined_points[:, 1]
153
+ y_threshold = np.percentile(y_values, 65)
154
+ mask = y_values < y_threshold
155
+ combined_points = combined_points[mask]
156
+ combined_colors = combined_colors[mask]
157
+
158
+ return combined_points, combined_colors
159
+
160
+
161
+ def create_proxie_group(
162
+ group_id: int,
163
+ initial_position=None,
164
+ initial_rotation=None,
165
+ opacity: float = 1.0,
166
+ glb_url: str = None,
167
+ ):
168
+ """Create a single group containing a GLB model with four camera frustums."""
169
+ from vuer.schemas import Frustum, Glb, Group
170
+
171
+ if initial_position is None:
172
+ initial_position = [0, 0, 0]
173
+ if initial_rotation is None:
174
+ initial_rotation = [0, 0, 0]
175
+
176
+ camera_positions = create_camera_positions_around_center(radius=0.1, height=1.625)
177
+ camera_rotations = create_camera_rotations_looking_at_center()
178
+
179
+ frustums = []
180
+ for i, (pos, rot) in enumerate(zip(camera_positions, camera_rotations, strict=False)):
181
+ if i == 3:
182
+ pos[0] += 0.185
183
+ pos[1] += 0.02
184
+ elif i == 0:
185
+ pos[0] += 0.20
186
+ pos[1] += 0.008
187
+ elif i == 1:
188
+ pos[0] += 0.17
189
+ pos[1] += -0.025
190
+ elif i == 2:
191
+ pos[0] += 0.18
192
+ pos[1] += 0.005
193
+ frustums.append(
194
+ Frustum(
195
+ key=f"frustum-{group_id}-{i}",
196
+ position=pos,
197
+ rotation=rot,
198
+ scale=0.1,
199
+ fov=60,
200
+ aspect=16 / 9,
201
+ near=0.1,
202
+ far=0.2,
203
+ showFrustum=True,
204
+ showImagePlane=True,
205
+ showFocalPlane=False,
206
+ colorFrustum="cyan",
207
+ )
208
+ )
209
+
210
+ initial_position[0] += 0
211
+ initial_position[1] += 2
212
+ initial_position[2] += 0
213
+ initial_rotation[0] += -1.57
214
+ initial_rotation[1] += 0
215
+ initial_rotation[2] += 0
216
+
217
+ return Group(
218
+ Glb(
219
+ src=glb_url,
220
+ position=[0, 0, 0],
221
+ rotation=[1.57, 0, 0],
222
+ scale=0.01,
223
+ opacity=opacity,
224
+ key=f"model-{group_id}",
225
+ ),
226
+ *frustums,
227
+ position=initial_position,
228
+ rotation=initial_rotation,
229
+ scale=1.0,
230
+ key=f"group-{group_id}",
231
+ )
232
+
233
+
234
+ async def main_viz(
235
+ sess,
236
+ show_pointcloud: bool = True,
237
+ data_dir: str = "demcap_data",
238
+ camera_names: Optional[List[str]] = None,
239
+ max_frames: Optional[int] = None,
240
+ frame_step: int = 5,
241
+ pixel_step: int = 5,
242
+ max_depth: float = 10.0,
243
+ point_size: float = 0.01,
244
+ color_by_camera: bool = False,
245
+ num_groups: int = 3,
246
+ group_spacing: float = 2.0,
247
+ base_position: Optional[List[float]] = None,
248
+ glb_url: str = None,
249
+ ):
250
+ """Main visualization function that creates multiple groups with GLB models and cameras."""
251
+ from vuer.schemas import DefaultScene, PointCloud
252
+
253
+ print("Loading scene...")
254
+
255
+ point_cloud_vertices = None
256
+ point_cloud_colors = None
257
+ if show_pointcloud:
258
+ print("Loading point cloud...")
259
+ point_cloud_vertices, point_cloud_colors = load_scene_pointcloud(
260
+ data_dir=data_dir,
261
+ camera_names=camera_names,
262
+ max_frames=max_frames,
263
+ frame_step=frame_step,
264
+ pixel_step=pixel_step,
265
+ max_depth=max_depth,
266
+ color_by_camera=color_by_camera,
267
+ )
268
+
269
+ if num_groups > 1:
270
+ opacity_values = [1.0 - (0.3 * i / (num_groups - 1)) for i in range(num_groups)]
271
+ else:
272
+ opacity_values = [1.0]
273
+
274
+ group_configs = [
275
+ {
276
+ "id": i,
277
+ "position": [
278
+ base_position[0],
279
+ base_position[1],
280
+ base_position[2] - i * group_spacing,
281
+ ],
282
+ "rotation": [0, 0, 0],
283
+ "opacity": opacity_values[i],
284
+ }
285
+ for i in range(num_groups)
286
+ ]
287
+
288
+ groups = [
289
+ create_proxie_group(
290
+ config["id"],
291
+ config["position"],
292
+ config.get("rotation"),
293
+ config.get("opacity", 1.0),
294
+ glb_url=glb_url,
295
+ )
296
+ for config in group_configs
297
+ ]
298
+
299
+ scene_elements = []
300
+ scene_elements.extend(groups)
301
+
302
+ if point_cloud_vertices is not None and point_cloud_colors is not None:
303
+ print(f"Point cloud: {len(point_cloud_vertices):,} points")
304
+ point_cloud = PointCloud(
305
+ key="scene_pointcloud",
306
+ vertices=point_cloud_vertices,
307
+ colors=point_cloud_colors,
308
+ size=point_size,
309
+ position=[0, 2, 0],
310
+ rotation=[0, 0, 0],
311
+ )
312
+ scene_elements.append(point_cloud)
313
+
314
+ print("Setting up scene...")
315
+ sess.set @ DefaultScene(
316
+ *scene_elements,
317
+ up=[0, 1, 0],
318
+ # show_helper=False,
319
+ # bgChildren=[OrbitControls(key="OrbitControls")],
320
+ )
321
+
322
+ print("Ready! Open browser to view.")
323
+ await sess.forever()
324
+
325
+
326
+ @dataclass
327
+ class VizPtcProxie:
328
+ """Visualize GLB robot model with camera frustums and optional point cloud.
329
+
330
+ DESCRIPTION
331
+ Creates an interactive 3D visualization combining a GLB robot model with
332
+ camera frustums and an optional RGB-D point cloud background. Useful for
333
+ visualizing robot poses within a reconstructed environment.
334
+
335
+ Features:
336
+ - Multiple robot model instances with configurable spacing
337
+ - Four camera frustums per robot showing sensor coverage
338
+ - Optional point cloud background from RGB-D data
339
+ - Opacity gradient for depth perception of robot poses
340
+
341
+ The visualization server runs at http://localhost:8012 by default.
342
+ Open this URL in a browser to view the 3D scene.
343
+
344
+ GLB MODEL
345
+ Place your robot GLB model at: vuer-cli/assets/proxie.glb
346
+ Or specify a custom URL with --glb-url
347
+
348
+ INPUT FORMAT
349
+ Requires extracted MCAP data (use 'vuer demcap' first):
350
+ <data_dir>/
351
+ ├── images/<camera>_color_image_*/ # RGB images
352
+ ├── depth/<camera>_*/ # Depth images
353
+ ├── transforms/transforms.csv # Robot poses
354
+ └── camera_intrinsics.json # Camera calibration
355
+
356
+ EXAMPLES
357
+ # Basic visualization with point cloud
358
+ vuer viz_ptc_proxie --data-dir /path/to/extracted_data
359
+
360
+ # Without point cloud (faster startup)
361
+ vuer viz_ptc_proxie --data-dir /path/to/data --show-pointcloud False
362
+
363
+ # Custom number of robot instances
364
+ vuer viz_ptc_proxie --data-dir /path/to/data --num-groups 5 --group-spacing 1.0
365
+
366
+ # Custom GLB model URL
367
+ vuer viz_ptc_proxie --data-dir /path/to/data --glb-url http://localhost:8012/static/my_robot.glb
368
+
369
+ # Specify cameras for point cloud
370
+ vuer viz_ptc_proxie --data-dir /path/to/data --cameras nav_front_d455,nav_right_d455
371
+
372
+ # Faster point cloud with sparser sampling
373
+ vuer viz_ptc_proxie --data-dir /path/to/data --frame-step 10 --pixel-step 10
374
+
375
+ DEPENDENCIES
376
+ Requires: pip install 'vuer-cli[viz]'
377
+ """
378
+
379
+ # Required: path to extracted MCAP data directory
380
+ data_dir: str
381
+
382
+ # Point cloud options
383
+ show_pointcloud: bool = True # Enable point cloud visualization
384
+ cameras: str = (
385
+ None # Comma-separated camera names (default: nav_front_d455,nav_right_d455)
386
+ )
387
+ max_frames: int = None # Max frames per camera (default: all)
388
+ frame_step: int = 5 # Process every Nth frame
389
+ pixel_step: int = 10 # Sample every Nth pixel
390
+ max_depth: float = 10.0 # Maximum depth in meters
391
+ point_size: float = 0.01 # Size of points
392
+
393
+ # Model options
394
+ assets_folder: str = (
395
+ None # Static assets folder for GLB model (default: vuer-cli/assets)
396
+ )
397
+ glb_url: str = "proxie.glb" # GLB model URL
398
+ num_groups: int = 10 # Number of proxie groups to create
399
+ group_spacing: float = 0.5 # Spacing between groups along Z-axis
400
+ base_position: str = "-2.0,-2.2,0" # Starting position [x,y,z] for first group
401
+
402
+ # Server options
403
+ host: str = "0.0.0.0" # Server host
404
+ port: int = 8012 # Server port
405
+
406
+ def __call__(self) -> int:
407
+ """Execute viz_ptc_proxie command."""
408
+ try:
409
+ import cv2
410
+ import numpy as np
411
+ import pandas as pd
412
+ from vuer import Vuer
413
+ except ImportError as e:
414
+ print_error(
415
+ f"Missing dependencies for viz_ptc_proxie command: {e}\n\n"
416
+ "Install with:\n"
417
+ " pip install 'vuer-cli[viz]'\n"
418
+ " # or: uv add 'vuer-cli[viz]'"
419
+ )
420
+ return 1
421
+
422
+ try:
423
+ data_path = Path(self.data_dir)
424
+ if not data_path.exists():
425
+ raise FileNotFoundError(f"Data directory {self.data_dir} does not exist")
426
+
427
+ # Parse camera names
428
+ camera_list = None
429
+ if self.cameras:
430
+ camera_list = [c.strip() for c in self.cameras.split(",")]
431
+
432
+ # Parse base_position
433
+ base_pos_list = [float(x.strip()) for x in self.base_position.split(",")]
434
+
435
+ # Create Vuer app - use default assets folder if not specified
436
+ assets_folder = self.assets_folder
437
+ if not assets_folder:
438
+ # Default to vuer-cli/assets folder
439
+ import vuer_cli
440
+
441
+ assets_folder = Path(vuer_cli.__file__).parent.parent.parent / "assets"
442
+ if not assets_folder.exists():
443
+ assets_folder = None
444
+
445
+ if assets_folder:
446
+ print(f"Using assets folder: {assets_folder}")
447
+ app = Vuer(host=self.host, port=self.port, workspace=assets_folder)
448
+ else:
449
+ app = Vuer(host=self.host, port=self.port)
450
+
451
+ print("Starting visualization server...")
452
+
453
+ @app.spawn(start=True)
454
+ async def main_with_args(sess):
455
+ print("Session started, loading data...")
456
+ await main_viz(
457
+ sess,
458
+ show_pointcloud=self.show_pointcloud,
459
+ data_dir=self.data_dir,
460
+ camera_names=camera_list,
461
+ max_frames=self.max_frames,
462
+ frame_step=self.frame_step,
463
+ pixel_step=self.pixel_step,
464
+ max_depth=self.max_depth,
465
+ point_size=self.point_size,
466
+ color_by_camera=False,
467
+ num_groups=self.num_groups,
468
+ group_spacing=self.group_spacing,
469
+ base_position=base_pos_list,
470
+ glb_url=self.glb_url,
471
+ )
472
+
473
+ return 0
474
+
475
+ except FileNotFoundError as e:
476
+ print_error(str(e))
477
+ return 1
478
+ except Exception as e:
479
+ print_error(f"Unexpected error: {e}")
480
+ import traceback
481
+
482
+ traceback.print_exc()
483
+ return 1