Spaces:
Running on Zero
Running on Zero
Compact flat-point GPU payload → frame cap 128 (denser); GLB built from points, verified == predictions_to_glb
Browse files- app.py +13 -14
- pipeline.py +138 -68
app.py
CHANGED
|
@@ -37,7 +37,7 @@ CKPT_REPO = "robbyant/lingbot-map"
|
|
| 37 |
CKPT_FILE = "lingbot-map-long.pt"
|
| 38 |
NUM_SCALE_FRAMES = 8 # initial bidirectional "scale" frames
|
| 39 |
KEYFRAME_INTERVAL = 1 # every frame is a keyframe — best quality for short clips
|
| 40 |
-
MAX_FRAMES_HARD =
|
| 41 |
GPU_DURATION = 120 # seconds requested per GPU call (tune to your tier)
|
| 42 |
|
| 43 |
# --------------------------------------------------------------------------------------
|
|
@@ -63,11 +63,10 @@ def gpu_reconstruct(images_cpu):
|
|
| 63 |
if not _AGG_BF16_DONE["done"] and getattr(MODEL, "aggregator", None) is not None:
|
| 64 |
MODEL.aggregator = MODEL.aggregator.to(dtype=torch.bfloat16)
|
| 65 |
_AGG_BF16_DONE["done"] = True
|
| 66 |
-
return P.
|
| 67 |
MODEL, images_cpu,
|
| 68 |
num_scale_frames=NUM_SCALE_FRAMES,
|
| 69 |
keyframe_interval=KEYFRAME_INTERVAL,
|
| 70 |
-
offload=True,
|
| 71 |
)
|
| 72 |
|
| 73 |
|
|
@@ -108,26 +107,26 @@ def reconstruct(image_files, video_file, fps, max_frames, conf_thres, show_cam):
|
|
| 108 |
images = P.preprocess_paths(paths) # [S, 3, H, W] on CPU
|
| 109 |
s, _, h, w = images.shape
|
| 110 |
|
| 111 |
-
|
| 112 |
t_infer = time.time() - t0
|
| 113 |
|
| 114 |
t1 = time.time()
|
| 115 |
-
glb = P.
|
| 116 |
t_export = time.time() - t1
|
| 117 |
|
| 118 |
-
kept, total = P.
|
| 119 |
status = _status_md(source, s, h, w, kept, total, t_infer, t_export, conf_thres)
|
| 120 |
-
return glb,
|
| 121 |
|
| 122 |
|
| 123 |
-
def reexport(
|
| 124 |
"""CPU-only re-render when the user nudges the confidence filter / camera toggle.
|
| 125 |
-
Uses the cached
|
| 126 |
-
if not
|
| 127 |
return None, "Run a reconstruction first."
|
| 128 |
t1 = time.time()
|
| 129 |
-
glb = P.
|
| 130 |
-
kept, total = P.
|
| 131 |
pct = (100.0 * kept / total) if total else 0.0
|
| 132 |
msg = (
|
| 133 |
f"Re-rendered (CPU). Points kept: **{kept:,} / {total:,}** ({pct:.0f}%) at "
|
|
@@ -163,8 +162,8 @@ def build_demo():
|
|
| 163 |
with gr.Accordion("Settings", open=True):
|
| 164 |
fps = gr.Slider(1, 12, value=6, step=1,
|
| 165 |
label="Video sampling FPS (frames/sec to extract)")
|
| 166 |
-
max_frames = gr.Slider(2, MAX_FRAMES_HARD, value=
|
| 167 |
-
label="Max frames (spread
|
| 168 |
conf_thres = gr.Slider(0, 95, value=60, step=1,
|
| 169 |
label="Confidence filter — drops the lowest-confidence % of points (raise to declutter)")
|
| 170 |
show_cam = gr.Checkbox(value=True, label="Show camera trajectory")
|
|
|
|
| 37 |
CKPT_FILE = "lingbot-map-long.pt"
|
| 38 |
NUM_SCALE_FRAMES = 8 # initial bidirectional "scale" frames
|
| 39 |
KEYFRAME_INTERVAL = 1 # every frame is a keyframe — best quality for short clips
|
| 40 |
+
MAX_FRAMES_HARD = 128 # absolute cap (~0.2s/frame → ~25s at 128, well under the 120s budget)
|
| 41 |
GPU_DURATION = 120 # seconds requested per GPU call (tune to your tier)
|
| 42 |
|
| 43 |
# --------------------------------------------------------------------------------------
|
|
|
|
| 63 |
if not _AGG_BF16_DONE["done"] and getattr(MODEL, "aggregator", None) is not None:
|
| 64 |
MODEL.aggregator = MODEL.aggregator.to(dtype=torch.bfloat16)
|
| 65 |
_AGG_BF16_DONE["done"] = True
|
| 66 |
+
return P.reconstruct_points(
|
| 67 |
MODEL, images_cpu,
|
| 68 |
num_scale_frames=NUM_SCALE_FRAMES,
|
| 69 |
keyframe_interval=KEYFRAME_INTERVAL,
|
|
|
|
| 70 |
)
|
| 71 |
|
| 72 |
|
|
|
|
| 107 |
images = P.preprocess_paths(paths) # [S, 3, H, W] on CPU
|
| 108 |
s, _, h, w = images.shape
|
| 109 |
|
| 110 |
+
rec = gpu_reconstruct(images) # GPU; returns a COMPACT flat point set
|
| 111 |
t_infer = time.time() - t0
|
| 112 |
|
| 113 |
t1 = time.time()
|
| 114 |
+
glb = P.build_glb_from_points(rec, conf_thres=conf_thres, show_cam=show_cam)
|
| 115 |
t_export = time.time() - t1
|
| 116 |
|
| 117 |
+
kept, total = P.count_points(rec, conf_thres)
|
| 118 |
status = _status_md(source, s, h, w, kept, total, t_infer, t_export, conf_thres)
|
| 119 |
+
return glb, rec, status
|
| 120 |
|
| 121 |
|
| 122 |
+
def reexport(rec, conf_thres, show_cam):
|
| 123 |
"""CPU-only re-render when the user nudges the confidence filter / camera toggle.
|
| 124 |
+
Uses the cached compact point set — no GPU, no re-inference."""
|
| 125 |
+
if not rec:
|
| 126 |
return None, "Run a reconstruction first."
|
| 127 |
t1 = time.time()
|
| 128 |
+
glb = P.build_glb_from_points(rec, conf_thres=conf_thres, show_cam=show_cam)
|
| 129 |
+
kept, total = P.count_points(rec, conf_thres)
|
| 130 |
pct = (100.0 * kept / total) if total else 0.0
|
| 131 |
msg = (
|
| 132 |
f"Re-rendered (CPU). Points kept: **{kept:,} / {total:,}** ({pct:.0f}%) at "
|
|
|
|
| 162 |
with gr.Accordion("Settings", open=True):
|
| 163 |
fps = gr.Slider(1, 12, value=6, step=1,
|
| 164 |
label="Video sampling FPS (frames/sec to extract)")
|
| 165 |
+
max_frames = gr.Slider(2, MAX_FRAMES_HARD, value=80, step=1,
|
| 166 |
+
label="Max frames — higher = denser & more complete (spread across the whole video)")
|
| 167 |
conf_thres = gr.Slider(0, 95, value=60, step=1,
|
| 168 |
label="Confidence filter — drops the lowest-confidence % of points (raise to declutter)")
|
| 169 |
show_cam = gr.Checkbox(value=True, label="Show camera trajectory")
|
pipeline.py
CHANGED
|
@@ -360,39 +360,86 @@ def reconstruct_predictions(
|
|
| 360 |
# GLB export (CPU only — no GPU needed)
|
| 361 |
# =============================================================================
|
| 362 |
|
| 363 |
-
def
|
| 364 |
-
"""
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
import trimesh
|
| 368 |
-
from scipy.spatial.transform import Rotation
|
| 369 |
-
import matplotlib
|
| 370 |
-
|
| 371 |
-
extrinsic = np.asarray(extrinsic, dtype=np.float64)
|
| 372 |
-
S = len(extrinsic)
|
| 373 |
-
if S < 2:
|
| 374 |
-
return scene
|
| 375 |
w2c = np.repeat(np.eye(4)[None], S, axis=0)
|
| 376 |
-
w2c[:, :3, :4] =
|
| 377 |
-
centers = np.linalg.inv(w2c)[:, :3, 3]
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
opengl = np.eye(4); opengl[1, 1] = -1.0; opengl[2, 2] = -1.0
|
| 380 |
align = np.eye(4); align[:3, :3] = Rotation.from_euler("y", 180, degrees=True).as_matrix()
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
radius = max(ext * 0.004, 1e-5)
|
| 385 |
cmap = matplotlib.colormaps.get_cmap("gist_rainbow")
|
| 386 |
tubes = []
|
| 387 |
for i in range(S - 1):
|
| 388 |
-
if np.linalg.norm(
|
| 389 |
continue
|
| 390 |
-
seg = trimesh.creation.cylinder(radius=radius, segment=
|
| 391 |
seg.visual.vertex_colors = tuple(int(255 * x) for x in cmap(i / (S - 1))[:3]) + (255,)
|
| 392 |
tubes.append(seg)
|
| 393 |
if tubes:
|
| 394 |
scene.add_geometry(trimesh.util.concatenate(tubes))
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
|
| 398 |
def export_glb(
|
|
@@ -403,66 +450,89 @@ def export_glb(
|
|
| 403 |
mask_sky: bool = False,
|
| 404 |
target_dir: Optional[str] = None,
|
| 405 |
) -> str:
|
| 406 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
-
The streaming model (GCTStream) is built with ``enable_point=False``, so it emits
|
| 409 |
-
**depth**, not a point map. We unproject depth → world points (``world_points_from_depth``,
|
| 410 |
-
via the camera w2c extrinsics + intrinsics) and use the exporter's *Depthmap* branch,
|
| 411 |
-
which filters by ``depth_conf``. If a point map is present (e.g. a future enable_point
|
| 412 |
-
build), we use it directly via the *Pointmap* branch instead.
|
| 413 |
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
"""
|
|
|
|
| 417 |
if out_path is None:
|
| 418 |
out_path = os.path.join(tempfile.mkdtemp(prefix="lingbot_glb_"), "scene.glb")
|
| 419 |
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
)
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
)
|
| 437 |
-
if show_cam
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
# Downsample the point cloud so the browser viewer (model-viewer) stays smooth — it
|
| 441 |
-
# bogs down past a few million points. Camera frustums are left intact.
|
| 442 |
-
import trimesh
|
| 443 |
-
display_max = 2_000_000
|
| 444 |
-
for _name in list(scene.geometry.keys()):
|
| 445 |
-
_g = scene.geometry[_name]
|
| 446 |
-
if isinstance(_g, trimesh.PointCloud) and len(_g.vertices) > display_max:
|
| 447 |
-
_sel = np.random.default_rng(0).choice(len(_g.vertices), display_max, replace=False)
|
| 448 |
-
_cols = np.asarray(_g.colors) if _g.colors is not None else None
|
| 449 |
-
_cols = _cols[_sel] if (_cols is not None and len(_cols) == len(_g.vertices)) else None
|
| 450 |
-
scene.geometry[_name] = trimesh.PointCloud(vertices=np.asarray(_g.vertices)[_sel], colors=_cols)
|
| 451 |
-
|
| 452 |
scene.export(out_path)
|
| 453 |
return out_path
|
| 454 |
|
| 455 |
|
| 456 |
-
def
|
| 457 |
-
"""(kept, total)
|
| 458 |
-
|
| 459 |
-
Handy for the status readout so users can see when conf_thres nukes the cloud.
|
| 460 |
-
"""
|
| 461 |
-
conf_key = "world_points_conf" if "world_points" in vis_predictions else "depth_conf"
|
| 462 |
-
conf = np.asarray(vis_predictions[conf_key]).reshape(-1)
|
| 463 |
total = conf.size
|
| 464 |
if total == 0:
|
| 465 |
return 0, 0
|
| 466 |
thr = np.percentile(conf, conf_thres) if conf_thres > 0 else 0.0
|
| 467 |
kept = int(np.count_nonzero((conf >= thr) & (conf > 1e-5)))
|
| 468 |
return kept, total
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
# GLB export (CPU only — no GPU needed)
|
| 361 |
# =============================================================================
|
| 362 |
|
| 363 |
+
def _camera_centers_world(extrinsic):
|
| 364 |
+
"""(camera centers in world coords [S,3], w2c 4x4 [S,4,4]) from w2c [S,3,4]."""
|
| 365 |
+
extr = np.asarray(extrinsic, dtype=np.float64)
|
| 366 |
+
S = len(extr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
w2c = np.repeat(np.eye(4)[None], S, axis=0)
|
| 368 |
+
w2c[:, :3, :4] = extr
|
| 369 |
+
centers = np.linalg.inv(w2c)[:, :3, 3]
|
| 370 |
+
return centers, w2c
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _alignment_transform(w2c):
|
| 374 |
+
"""The same scene alignment predictions_to_glb applies (orient to the first camera +
|
| 375 |
+
flip into the GLB/OpenGL convention). Applied to the WHOLE scene (points + trajectory).
|
| 376 |
+
Verified to match predictions_to_glb's apply_scene_alignment."""
|
| 377 |
+
from scipy.spatial.transform import Rotation
|
| 378 |
opengl = np.eye(4); opengl[1, 1] = -1.0; opengl[2, 2] = -1.0
|
| 379 |
align = np.eye(4); align[:3, :3] = Rotation.from_euler("y", 180, degrees=True).as_matrix()
|
| 380 |
+
return np.linalg.inv(w2c[0]) @ opengl @ align
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def _add_trajectory_tube(scene, centers_world):
|
| 384 |
+
"""Add a clean colored tube tracing the camera path (README "track" style) in WORLD
|
| 385 |
+
coords; the caller applies the scene alignment afterwards. gist_rainbow start→end."""
|
| 386 |
+
import trimesh
|
| 387 |
+
import matplotlib
|
| 388 |
+
S = len(centers_world)
|
| 389 |
+
if S < 2:
|
| 390 |
+
return
|
| 391 |
+
ext = float(np.linalg.norm(centers_world.max(0) - centers_world.min(0))) or 1.0
|
| 392 |
radius = max(ext * 0.004, 1e-5)
|
| 393 |
cmap = matplotlib.colormaps.get_cmap("gist_rainbow")
|
| 394 |
tubes = []
|
| 395 |
for i in range(S - 1):
|
| 396 |
+
if np.linalg.norm(centers_world[i + 1] - centers_world[i]) < 1e-9:
|
| 397 |
continue
|
| 398 |
+
seg = trimesh.creation.cylinder(radius=radius, segment=centers_world[i:i + 2], sections=8)
|
| 399 |
seg.visual.vertex_colors = tuple(int(255 * x) for x in cmap(i / (S - 1))[:3]) + (255,)
|
| 400 |
tubes.append(seg)
|
| 401 |
if tubes:
|
| 402 |
scene.add_geometry(trimesh.util.concatenate(tubes))
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def _flatten_vis(vis_predictions: dict) -> dict:
|
| 406 |
+
"""Flatten a vis dict → a compact flat point set in WORLD coords (the format the GPU
|
| 407 |
+
call returns and the GLB builder consumes). Unprojects depth at full resolution.
|
| 408 |
+
Returns {points (N,3) f32, colors (N,3) u8, conf (N,) f32, extrinsic (S,3,4) f32}.
|
| 409 |
+
"""
|
| 410 |
+
vis = vis_predictions
|
| 411 |
+
if "world_points" in vis:
|
| 412 |
+
world = np.asarray(vis["world_points"], np.float32)
|
| 413 |
+
conf = np.asarray(vis.get("world_points_conf"), np.float32)
|
| 414 |
+
else:
|
| 415 |
+
world = np.asarray(
|
| 416 |
+
unproject_depth_map_to_point_map(vis["depth"], vis["extrinsic"], vis["intrinsic"]),
|
| 417 |
+
np.float32,
|
| 418 |
+
)
|
| 419 |
+
conf = np.asarray(vis["depth_conf"], np.float32)
|
| 420 |
+
imgs = np.asarray(vis["images"])
|
| 421 |
+
if imgs.ndim == 4 and imgs.shape[1] == 3: # NCHW → NHWC
|
| 422 |
+
imgs = np.transpose(imgs, (0, 2, 3, 1))
|
| 423 |
+
return {
|
| 424 |
+
"points": world.reshape(-1, 3),
|
| 425 |
+
"colors": np.clip(imgs.reshape(-1, 3) * 255.0, 0, 255).astype(np.uint8),
|
| 426 |
+
"conf": conf.reshape(-1),
|
| 427 |
+
"extrinsic": np.asarray(vis["extrinsic"], np.float32),
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def _downsample_rec(rec: dict, max_points: int, seed: int = 0) -> dict:
|
| 432 |
+
"""Drop invalid points and randomly cap to max_points so the GPU→app payload stays
|
| 433 |
+
bounded and independent of frame count."""
|
| 434 |
+
pts, cols, conf = rec["points"], rec["colors"], rec["conf"]
|
| 435 |
+
valid = conf > 1e-5
|
| 436 |
+
pts, cols, conf = pts[valid], cols[valid], conf[valid]
|
| 437 |
+
n_total = int(pts.shape[0])
|
| 438 |
+
if n_total > max_points:
|
| 439 |
+
sel = np.random.default_rng(seed).choice(n_total, max_points, replace=False)
|
| 440 |
+
pts, cols, conf = pts[sel], cols[sel], conf[sel]
|
| 441 |
+
return {"points": pts, "colors": cols, "conf": conf,
|
| 442 |
+
"extrinsic": rec["extrinsic"], "n_total": n_total}
|
| 443 |
|
| 444 |
|
| 445 |
def export_glb(
|
|
|
|
| 450 |
mask_sky: bool = False,
|
| 451 |
target_dir: Optional[str] = None,
|
| 452 |
) -> str:
|
| 453 |
+
"""Convenience wrapper (smoke_test / local CPU runs): flatten a vis dict → build the GLB.
|
| 454 |
+
The Space uses reconstruct_points() + build_glb_from_points() directly (compact payload)."""
|
| 455 |
+
rec = _flatten_vis(vis_predictions)
|
| 456 |
+
return build_glb_from_points(rec, out_path=out_path, conf_thres=conf_thres, show_cam=show_cam)
|
| 457 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
+
def build_glb_from_points(
|
| 460 |
+
rec: dict,
|
| 461 |
+
out_path: Optional[str] = None,
|
| 462 |
+
conf_thres: float = 60.0,
|
| 463 |
+
show_cam: bool = True,
|
| 464 |
+
display_max: int = 2_000_000,
|
| 465 |
+
seed: int = 0,
|
| 466 |
+
) -> str:
|
| 467 |
+
"""Build the .glb from a flat point set: percentile-filter by confidence, downsample for
|
| 468 |
+
smooth in-browser rendering, add the camera-path line, and apply the scene alignment.
|
| 469 |
+
|
| 470 |
+
``conf_thres`` is a *percentile* — points below the conf_thres-th percentile of
|
| 471 |
+
confidence are dropped. 0 keeps everything; higher = fewer points.
|
| 472 |
"""
|
| 473 |
+
import trimesh
|
| 474 |
if out_path is None:
|
| 475 |
out_path = os.path.join(tempfile.mkdtemp(prefix="lingbot_glb_"), "scene.glb")
|
| 476 |
|
| 477 |
+
pts = np.asarray(rec["points"], np.float64)
|
| 478 |
+
cols = np.asarray(rec["colors"])
|
| 479 |
+
conf = np.asarray(rec["conf"], np.float64)
|
| 480 |
+
|
| 481 |
+
if conf.size:
|
| 482 |
+
thr = np.percentile(conf, conf_thres) if conf_thres > 0 else 0.0
|
| 483 |
+
mask = (conf >= thr) & (conf > 1e-5)
|
| 484 |
+
pts, cols = pts[mask], cols[mask]
|
| 485 |
+
if len(pts) > display_max: # browser viewer bogs down past a few M points
|
| 486 |
+
sel = np.random.default_rng(seed).choice(len(pts), display_max, replace=False)
|
| 487 |
+
pts, cols = pts[sel], cols[sel]
|
| 488 |
+
if len(pts) == 0: # degenerate guard
|
| 489 |
+
pts, cols = np.zeros((1, 3)), np.array([[255, 255, 255]])
|
| 490 |
+
|
| 491 |
+
scene = trimesh.Scene()
|
| 492 |
+
scene.add_geometry(trimesh.PointCloud(vertices=pts, colors=cols))
|
| 493 |
+
centers, w2c = _camera_centers_world(rec["extrinsic"])
|
| 494 |
+
if show_cam:
|
| 495 |
+
_add_trajectory_tube(scene, centers)
|
| 496 |
+
scene.apply_transform(_alignment_transform(w2c)) # align points + trajectory together
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
scene.export(out_path)
|
| 498 |
return out_path
|
| 499 |
|
| 500 |
|
| 501 |
+
def count_points(rec: dict, conf_thres: float = 60.0) -> Tuple[int, int]:
|
| 502 |
+
"""(kept, total) after the same percentile filter build_glb_from_points uses."""
|
| 503 |
+
conf = np.asarray(rec["conf"]).reshape(-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
total = conf.size
|
| 505 |
if total == 0:
|
| 506 |
return 0, 0
|
| 507 |
thr = np.percentile(conf, conf_thres) if conf_thres > 0 else 0.0
|
| 508 |
kept = int(np.count_nonzero((conf >= thr) & (conf > 1e-5)))
|
| 509 |
return kept, total
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def count_visible_points(vis_predictions: dict, conf_thres: float = 60.0) -> Tuple[int, int]:
|
| 513 |
+
"""Back-compat wrapper for the vis-dict form (smoke_test)."""
|
| 514 |
+
return count_points(_flatten_vis(vis_predictions), conf_thres)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
# =============================================================================
|
| 518 |
+
# GPU entry point that returns a COMPACT, frame-count-independent payload
|
| 519 |
+
# =============================================================================
|
| 520 |
+
|
| 521 |
+
def reconstruct_points(
|
| 522 |
+
model: GCTStream,
|
| 523 |
+
images: torch.Tensor,
|
| 524 |
+
num_scale_frames: int = 8,
|
| 525 |
+
keyframe_interval: int = 1,
|
| 526 |
+
max_return_points: int = 6_000_000,
|
| 527 |
+
) -> dict:
|
| 528 |
+
"""End-to-end GPU path → a COMPACT flat point set whose size is bounded (independent of
|
| 529 |
+
frame count). This is what lets the Space return many frames across the @spaces.GPU
|
| 530 |
+
boundary without a huge per-pixel payload.
|
| 531 |
+
|
| 532 |
+
Returns {points (M,3) f32, colors (M,3) u8, conf (M,) f32, extrinsic (S,3,4) f32, n_total}.
|
| 533 |
+
"""
|
| 534 |
+
vis = reconstruct_predictions(
|
| 535 |
+
model, images, num_scale_frames=num_scale_frames,
|
| 536 |
+
keyframe_interval=keyframe_interval, offload=True,
|
| 537 |
+
)
|
| 538 |
+
return _downsample_rec(_flatten_vis(vis), max_return_points)
|