Export current scene as GLB point cloud with coloured camera frustums. Uses the same data pipeline as the web viewer so the result exactly matches what is displayed. Saves to {output_dir}/{source_dataset}/{view_density}/{scene_id}.glb and returns JSON with the output path and stats.
(
scene_id: str,
z_far: float = Query(10.0),
downsample: int = Query(1),
max_pts: int = Query(3_000_000),
depth_mask: bool = Query(True),
conf_threshold: float = Query(0.0),
output_dir: str = Query("glb_output"),
frustum_scale: float = Query(0.0),
)
| 1720 | |
| 1721 | @app.post("/api/scene/{scene_id}/export_glb") |
| 1722 | def export_scene_glb( |
| 1723 | scene_id: str, |
| 1724 | z_far: float = Query(10.0), |
| 1725 | downsample: int = Query(1), |
| 1726 | max_pts: int = Query(3_000_000), |
| 1727 | depth_mask: bool = Query(True), |
| 1728 | conf_threshold: float = Query(0.0), |
| 1729 | output_dir: str = Query("glb_output"), |
| 1730 | frustum_scale: float = Query(0.0), |
| 1731 | ): |
| 1732 | """Export current scene as GLB point cloud with coloured camera frustums. |
| 1733 | |
| 1734 | Uses the same data pipeline as the web viewer so the result exactly matches |
| 1735 | what is displayed. Saves to {output_dir}/{source_dataset}/{view_density}/{scene_id}.glb |
| 1736 | and returns JSON with the output path and stats. |
| 1737 | """ |
| 1738 | scene = _find_scene(scene_id) |
| 1739 | if scene is None: |
| 1740 | return JSONResponse(status_code=404, content={"error": f"Scene {scene_id!r} not found"}) |
| 1741 | |
| 1742 | source_dataset = scene["source_dataset"] |
| 1743 | density = scene.get("tags", {}).get("view_density", "unknown") |
| 1744 | |
| 1745 | out_dir = Path(output_dir) / source_dataset / density |
| 1746 | out_dir.mkdir(parents=True, exist_ok=True) |
| 1747 | out_path = out_dir / f"{scene_id}.glb" |
| 1748 | |
| 1749 | try: |
| 1750 | data = _load_scene_data_raw(scene, z_far, use_depth_mask=depth_mask, conf_threshold=conf_threshold) |
| 1751 | except Exception as e: |
| 1752 | return JSONResponse(status_code=500, content={"error": f"Failed to load data: {e}"}) |
| 1753 | |
| 1754 | all_pts, all_rgb = [], [] |
| 1755 | |
| 1756 | per_frame_max = max(1, max_pts // max(len(data["depths"]), 1)) |
| 1757 | for i in range(len(data["depths"])): |
| 1758 | depth = data["depths"][i] |
| 1759 | pose = data["extrinsics"][i] |
| 1760 | img = data["images"][i] |
| 1761 | K = data["intrinsics"][i] if "intrinsics" in data else data["K"] |
| 1762 | |
| 1763 | pts, vs, us = _unproject_frame(depth, K, pose, downsample, per_frame_max) |
| 1764 | if len(pts) == 0: |
| 1765 | continue |
| 1766 | |
| 1767 | vs_c = np.clip(vs, 0, img.shape[0] - 1).astype(int) |
| 1768 | us_c = np.clip(us, 0, img.shape[1] - 1).astype(int) |
| 1769 | all_pts.append(pts) |
| 1770 | all_rgb.append(img[vs_c, us_c]) |
| 1771 | |
| 1772 | if not all_pts: |
| 1773 | return JSONResponse(status_code=422, content={"error": "No valid points in scene"}) |
| 1774 | |
| 1775 | pts_all = np.concatenate(all_pts, axis=0) |
| 1776 | rgb_all = np.concatenate(all_rgb, axis=0).astype(np.float32) / 255.0 |
| 1777 | |
| 1778 | if len(pts_all) > max_pts: |
| 1779 | idx = np.random.choice(len(pts_all), max_pts, replace=False) |
nothing calls this directly
no test coverage detected