Save point cloud + optional camera frustums to a GLB file. Args: points: (M, 3) world-coordinate points colors: (M, 3) RGB colors [0, 1] output_path: output .glb path extrinsics: (N, 3, 4) cam2world; when provided, draw camera frustums intrinsics: (N, 3,
(points, colors, output_path,
extrinsics=None, intrinsics=None, image_size=None,
frustum_scale=0.1, max_pts=500_000,
frustums_as_mesh=True)
| 198 | # ============================================================ |
| 199 | |
| 200 | def save_pointcloud_glb(points, colors, output_path, |
| 201 | extrinsics=None, intrinsics=None, image_size=None, |
| 202 | frustum_scale=0.1, max_pts=500_000, |
| 203 | frustums_as_mesh=True): |
| 204 | """Save point cloud + optional camera frustums to a GLB file. |
| 205 | |
| 206 | Args: |
| 207 | points: (M, 3) world-coordinate points |
| 208 | colors: (M, 3) RGB colors [0, 1] |
| 209 | output_path: output .glb path |
| 210 | extrinsics: (N, 3, 4) cam2world; when provided, draw camera frustums |
| 211 | intrinsics: (N, 3, 3) intrinsics |
| 212 | image_size: (W, H) image size |
| 213 | frustum_scale: frustum size scale |
| 214 | max_pts: maximum number of points (randomly downsampled when exceeded) |
| 215 | frustums_as_mesh: True = export frustums as triangle meshes (stable in MeshLab), |
| 216 | False = export as Path3D/LINES (clearer in three.js/Web, |
| 217 | but may crash MeshLab) |
| 218 | """ |
| 219 | try: |
| 220 | import trimesh |
| 221 | except ImportError: |
| 222 | print(f" [WARN] trimesh not installed, skipping GLB export: {output_path}") |
| 223 | return |
| 224 | |
| 225 | points = np.asarray(points, dtype=np.float32) |
| 226 | colors = np.asarray(colors, dtype=np.float32) |
| 227 | |
| 228 | # Filter NaN/Inf (one of the common causes of MeshLab crashes) |
| 229 | if len(points) > 0: |
| 230 | finite = np.isfinite(points).all(axis=1) |
| 231 | if not finite.all(): |
| 232 | points = points[finite] |
| 233 | colors = colors[finite] |
| 234 | |
| 235 | if len(points) == 0: |
| 236 | print(f" [WARN] no finite points to export: {output_path}") |
| 237 | return |
| 238 | |
| 239 | if len(points) > max_pts: |
| 240 | idx = np.random.choice(len(points), max_pts, replace=False) |
| 241 | points = points[idx] |
| 242 | colors = colors[idx] |
| 243 | |
| 244 | colors_uint8 = (np.clip(colors, 0, 1) * 255).astype(np.uint8) |
| 245 | rgba = np.concatenate([colors_uint8, np.full((len(colors_uint8), 1), 255, dtype=np.uint8)], axis=1) |
| 246 | |
| 247 | cloud = trimesh.PointCloud(vertices=points, colors=rgba) |
| 248 | |
| 249 | # If camera parameters are provided, add frustums |
| 250 | if extrinsics is not None and intrinsics is not None and image_size is not None: |
| 251 | scene = trimesh.Scene([cloud]) |
| 252 | if frustums_as_mesh: |
| 253 | frustums = build_frustum_mesh_geometries( |
| 254 | extrinsics, intrinsics, image_size, frustum_scale, |
| 255 | ) |
| 256 | else: |
| 257 | frustums = build_frustum_geometries( |
no test coverage detected