Add example
Browse files- app.py +95 -14
- examples/dl3dv_scene139.jpg +0 -0
- examples/dl3dv_scene9.jpg +0 -0
- examples/mipnerf360_garden.jpg +0 -0
- examples/mipnerf360_kitchen.jpg +0 -0
- examples/tandt_truck.jpg +0 -0
- viser_app/recon_scene.py +6 -3
- viser_app/render.py +5 -3
app.py
CHANGED
|
@@ -71,6 +71,30 @@ BENCH_WIDTHS = [int(x) for x in os.environ.get("BENCH_WIDTHS", "512 768").split(
|
|
| 71 |
ARTIFACTS_DIR = Path(tempfile.gettempdir()) / "dvsm_artifacts"
|
| 72 |
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# ---------------------------------------------------------------------------
|
| 75 |
# Module-level model load: download + keep on CPU (ZeroGPU convention).
|
| 76 |
# Real `.cuda()` happens inside @spaces.GPU workers.
|
|
@@ -150,9 +174,14 @@ class _SessionArgs:
|
|
| 150 |
@spaces.GPU(duration=SESSION_SECONDS)
|
| 151 |
def _live_session_worker(artifact_path: str, port: int):
|
| 152 |
"""Generator running in the GPU worker. Builds the renderer, starts a viser
|
| 153 |
-
server on `port`, then holds the GPU
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
import torch
|
| 157 |
import viser
|
| 158 |
from dvsm.models.pose_preprocessor import PosePreprocessor
|
|
@@ -160,6 +189,7 @@ def _live_session_worker(artifact_path: str, port: int):
|
|
| 160 |
from viser_app.viewer import install_viser_handlers
|
| 161 |
from viser_app.viewer_optim import install_optimizations
|
| 162 |
|
|
|
|
| 163 |
model = DVSM_MODEL_CPU.cuda().eval()
|
| 164 |
artifact = torch.load(artifact_path, weights_only=False, map_location="cpu")
|
| 165 |
renderer = build_renderer(model, artifact)
|
|
@@ -172,24 +202,25 @@ def _live_session_worker(artifact_path: str, port: int):
|
|
| 172 |
renderer.pose_preprocessor = None
|
| 173 |
pp_scale = float(pp_cpu.s.reshape(-1)[0])
|
| 174 |
|
|
|
|
| 175 |
renderer = install_optimizations(
|
| 176 |
renderer, use_fast_plucker=True, use_cuda_graph=True,
|
| 177 |
n_warmup=5, max_graphs=len(BENCH_WIDTHS) + 2,
|
| 178 |
)
|
| 179 |
|
|
|
|
| 180 |
server = viser.ViserServer(host="0.0.0.0", port=port)
|
| 181 |
server.scene.set_up_direction("+y")
|
| 182 |
install_viser_handlers(server, renderer, scene_meta, pp_cpu, pp_scale,
|
| 183 |
model, _SessionArgs())
|
| 184 |
|
| 185 |
deadline = time.monotonic() + SESSION_SECONDS - 5 # margin before hard kill
|
| 186 |
-
|
| 187 |
while True:
|
| 188 |
remaining = int(deadline - time.monotonic())
|
| 189 |
if remaining <= 0:
|
| 190 |
break
|
| 191 |
-
yield
|
| 192 |
-
first = False
|
| 193 |
time.sleep(1.0)
|
| 194 |
try:
|
| 195 |
server.stop()
|
|
@@ -232,6 +263,21 @@ def _viewer_message(msg: str, color: str = "var(--body-text-color-subdued)") ->
|
|
| 232 |
)
|
| 233 |
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
# ---------------------------------------------------------------------------
|
| 236 |
# Gradio callbacks (main process)
|
| 237 |
# ---------------------------------------------------------------------------
|
|
@@ -339,13 +385,15 @@ def live_session(artifact_path):
|
|
| 339 |
_state["viser_port"] = port
|
| 340 |
_state["session_alive"] = True
|
| 341 |
try:
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
| 346 |
yield (_iframe_html(port), _session_bar(SESSION_SECONDS, SESSION_SECONDS))
|
| 347 |
-
else:
|
| 348 |
-
yield (gr.update(), _session_bar(int(
|
| 349 |
finally:
|
| 350 |
_state["session_alive"] = False
|
| 351 |
_state["viser_port"] = None
|
|
@@ -489,6 +537,11 @@ input[type='range']::-moz-range-thumb { background: #76B900 !important; }
|
|
| 489 |
sit flush at the column top (kept aligned) and the viewer fills its frame
|
| 490 |
edge-to-edge (no padding gap around the iframe). */
|
| 491 |
.flush-block { padding: 0 !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
"""
|
| 493 |
|
| 494 |
with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
|
|
@@ -533,7 +586,7 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
|
|
| 533 |
)
|
| 534 |
# Below the text: 1 (inputs + buttons) : 3 (viser viewer).
|
| 535 |
with gr.Row():
|
| 536 |
-
with gr.Column(scale=1, min_width=
|
| 537 |
inputs = gr.File(
|
| 538 |
file_count="multiple",
|
| 539 |
file_types=["image", "video", ".mp4", ".mov", ".mkv",
|
|
@@ -548,7 +601,19 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
|
|
| 548 |
recon_btn = gr.Button("Reconstruct", variant="primary")
|
| 549 |
session_btn = gr.Button("Start / Continue Live Session",
|
| 550 |
variant="primary", interactive=False)
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
# Viewer first so its top edge aligns with the upload box on the
|
| 553 |
# left (both flush via .flush-block). Recon progress/status overlay
|
| 554 |
# the viewer box itself (see reconstruct_cb); only the live-session
|
|
@@ -568,6 +633,22 @@ with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
|
|
| 568 |
[viewer_html, session_status],
|
| 569 |
show_progress="hidden")
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
# ---------------------------------------------------------------------------
|
| 573 |
# Launch Gradio (which starts uvicorn) and attach the viser reverse-proxy routes
|
|
|
|
| 71 |
ARTIFACTS_DIR = Path(tempfile.gettempdir()) / "dvsm_artifacts"
|
| 72 |
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 73 |
|
| 74 |
+
# Pre-built example scenes are auto-discovered from examples/: every
|
| 75 |
+
# "<stem>.recon.pt" (baked with THIS model) that has a matching "<stem>.jpg|png"
|
| 76 |
+
# thumbnail becomes a gallery entry. Drop files in / remove them to change the
|
| 77 |
+
# gallery — no code edit needed. Clicking a thumbnail skips reconstruction and
|
| 78 |
+
# opens the live session directly on the artifact.
|
| 79 |
+
EXAMPLES_DIR = PROJECT_ROOT / "examples"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _discover_examples():
|
| 83 |
+
"""Every "<stem>.recon.pt" with a matching "<stem>.jpg|jpeg|png|webp"
|
| 84 |
+
thumbnail. The gallery shows thumbnails only (no labels)."""
|
| 85 |
+
scenes = []
|
| 86 |
+
for recon in sorted(EXAMPLES_DIR.glob("*.recon.pt")):
|
| 87 |
+
stem = recon.name[: -len(".recon.pt")]
|
| 88 |
+
thumb = next((EXAMPLES_DIR / f"{stem}{ext}"
|
| 89 |
+
for ext in (".jpg", ".jpeg", ".png", ".webp")
|
| 90 |
+
if (EXAMPLES_DIR / f"{stem}{ext}").exists()), None)
|
| 91 |
+
if thumb is not None:
|
| 92 |
+
scenes.append({"thumb": thumb, "recon": recon})
|
| 93 |
+
return scenes
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
EXAMPLE_SCENES = _discover_examples()
|
| 97 |
+
|
| 98 |
# ---------------------------------------------------------------------------
|
| 99 |
# Module-level model load: download + keep on CPU (ZeroGPU convention).
|
| 100 |
# Real `.cuda()` happens inside @spaces.GPU workers.
|
|
|
|
| 174 |
@spaces.GPU(duration=SESSION_SECONDS)
|
| 175 |
def _live_session_worker(artifact_path: str, port: int):
|
| 176 |
"""Generator running in the GPU worker. Builds the renderer, starts a viser
|
| 177 |
+
server on `port`, then holds the GPU until the session budget expires.
|
| 178 |
+
|
| 179 |
+
Yields tagged tuples so the UI can show a prep progress bar then a live
|
| 180 |
+
countdown:
|
| 181 |
+
("prep", frac, desc) during setup
|
| 182 |
+
("ready", port, None) viser is up and serving
|
| 183 |
+
("alive", remaining, None) per-second heartbeat while holding the GPU
|
| 184 |
+
"""
|
| 185 |
import torch
|
| 186 |
import viser
|
| 187 |
from dvsm.models.pose_preprocessor import PosePreprocessor
|
|
|
|
| 189 |
from viser_app.viewer import install_viser_handlers
|
| 190 |
from viser_app.viewer_optim import install_optimizations
|
| 191 |
|
| 192 |
+
yield ("prep", 0.15, "Loading scene…")
|
| 193 |
model = DVSM_MODEL_CPU.cuda().eval()
|
| 194 |
artifact = torch.load(artifact_path, weights_only=False, map_location="cpu")
|
| 195 |
renderer = build_renderer(model, artifact)
|
|
|
|
| 202 |
renderer.pose_preprocessor = None
|
| 203 |
pp_scale = float(pp_cpu.s.reshape(-1)[0])
|
| 204 |
|
| 205 |
+
yield ("prep", 0.4, "Loading scene…")
|
| 206 |
renderer = install_optimizations(
|
| 207 |
renderer, use_fast_plucker=True, use_cuda_graph=True,
|
| 208 |
n_warmup=5, max_graphs=len(BENCH_WIDTHS) + 2,
|
| 209 |
)
|
| 210 |
|
| 211 |
+
yield ("prep", 0.6, "Loading scene…")
|
| 212 |
server = viser.ViserServer(host="0.0.0.0", port=port)
|
| 213 |
server.scene.set_up_direction("+y")
|
| 214 |
install_viser_handlers(server, renderer, scene_meta, pp_cpu, pp_scale,
|
| 215 |
model, _SessionArgs())
|
| 216 |
|
| 217 |
deadline = time.monotonic() + SESSION_SECONDS - 5 # margin before hard kill
|
| 218 |
+
yield ("ready", port, None)
|
| 219 |
while True:
|
| 220 |
remaining = int(deadline - time.monotonic())
|
| 221 |
if remaining <= 0:
|
| 222 |
break
|
| 223 |
+
yield ("alive", remaining, None)
|
|
|
|
| 224 |
time.sleep(1.0)
|
| 225 |
try:
|
| 226 |
server.stop()
|
|
|
|
| 263 |
)
|
| 264 |
|
| 265 |
|
| 266 |
+
def _prep_overlay(frac: float, desc: str) -> str:
|
| 267 |
+
"""Determinate green progress bar centered in the viewer box, shown while
|
| 268 |
+
the viser session is being prepared (renderer build + warmup + bench)."""
|
| 269 |
+
pct = max(0, min(100, round(100 * frac)))
|
| 270 |
+
return (
|
| 271 |
+
"<div style='width:100%;aspect-ratio:16/9;overflow:hidden;display:flex;"
|
| 272 |
+
"flex-direction:column;align-items:center;justify-content:center;gap:10px;"
|
| 273 |
+
"padding:0 12%;color:var(--body-text-color-subdued)'>"
|
| 274 |
+
f"<div style='font-weight:600;font-size:1.05em'>{desc}</div>"
|
| 275 |
+
"<div style='width:100%;height:8px;background:#333;border-radius:4px;overflow:hidden'>"
|
| 276 |
+
f"<div style='height:100%;width:{pct}%;background:#76B900;"
|
| 277 |
+
"transition:width .3s linear'></div></div></div>"
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
# ---------------------------------------------------------------------------
|
| 282 |
# Gradio callbacks (main process)
|
| 283 |
# ---------------------------------------------------------------------------
|
|
|
|
| 385 |
_state["viser_port"] = port
|
| 386 |
_state["session_alive"] = True
|
| 387 |
try:
|
| 388 |
+
for tag, a, _b in _live_session_worker(artifact_path, port):
|
| 389 |
+
if tag == "prep":
|
| 390 |
+
# a = fraction, _b = description -> prep bar overlaid in the viewer
|
| 391 |
+
yield (_prep_overlay(a, _b), gr.update())
|
| 392 |
+
elif tag == "ready":
|
| 393 |
+
# viser is up -> swap in the iframe + start the countdown bar
|
| 394 |
yield (_iframe_html(port), _session_bar(SESSION_SECONDS, SESSION_SECONDS))
|
| 395 |
+
else: # "alive": a = seconds remaining
|
| 396 |
+
yield (gr.update(), _session_bar(int(a), SESSION_SECONDS))
|
| 397 |
finally:
|
| 398 |
_state["session_alive"] = False
|
| 399 |
_state["viser_port"] = None
|
|
|
|
| 537 |
sit flush at the column top (kept aligned) and the viewer fills its frame
|
| 538 |
edge-to-edge (no padding gap around the iframe). */
|
| 539 |
.flush-block { padding: 0 !important; }
|
| 540 |
+
/* Proportional columns: flex-basis 0 so the row width is always split 1:3 and
|
| 541 |
+
BOTH columns scale with the window (default Gradio scale only distributes the
|
| 542 |
+
leftover space, leaving the left column looking fixed). */
|
| 543 |
+
.col-left { flex: 1 1 0 !important; min-width: 260px; }
|
| 544 |
+
.col-right { flex: 3 1 0 !important; min-width: 360px; }
|
| 545 |
"""
|
| 546 |
|
| 547 |
with gr.Blocks(title="TTB-DVSM (ZeroGPU)") as demo:
|
|
|
|
| 586 |
)
|
| 587 |
# Below the text: 1 (inputs + buttons) : 3 (viser viewer).
|
| 588 |
with gr.Row():
|
| 589 |
+
with gr.Column(scale=1, min_width=260, elem_classes=["col-left"]):
|
| 590 |
inputs = gr.File(
|
| 591 |
file_count="multiple",
|
| 592 |
file_types=["image", "video", ".mp4", ".mov", ".mkv",
|
|
|
|
| 601 |
recon_btn = gr.Button("Reconstruct", variant="primary")
|
| 602 |
session_btn = gr.Button("Start / Continue Live Session",
|
| 603 |
variant="primary", interactive=False)
|
| 604 |
+
if EXAMPLE_SCENES:
|
| 605 |
+
gr.Markdown("### Example")
|
| 606 |
+
example_gallery = gr.Gallery(
|
| 607 |
+
# No captions. Fixed box height -> vertical scrollbar when
|
| 608 |
+
# there are more thumbnails than fit; width still scales with
|
| 609 |
+
# the (resizable) column since it's 3 columns.
|
| 610 |
+
value=[str(s["thumb"]) for s in EXAMPLE_SCENES],
|
| 611 |
+
columns=3, height=200, object_fit="cover",
|
| 612 |
+
show_label=False, allow_preview=False,
|
| 613 |
+
)
|
| 614 |
+
else:
|
| 615 |
+
example_gallery = None
|
| 616 |
+
with gr.Column(scale=3, elem_classes=["col-right"]):
|
| 617 |
# Viewer first so its top edge aligns with the upload box on the
|
| 618 |
# left (both flush via .flush-block). Recon progress/status overlay
|
| 619 |
# the viewer box itself (see reconstruct_cb); only the live-session
|
|
|
|
| 633 |
[viewer_html, session_status],
|
| 634 |
show_progress="hidden")
|
| 635 |
|
| 636 |
+
# Clicking an example thumbnail loads its prebuilt artifact and opens the
|
| 637 |
+
# live session directly (no reconstruction).
|
| 638 |
+
if example_gallery is not None:
|
| 639 |
+
def _on_example_select(evt: gr.SelectData):
|
| 640 |
+
idx = int(evt.index)
|
| 641 |
+
if 0 <= idx < len(EXAMPLE_SCENES):
|
| 642 |
+
return str(EXAMPLE_SCENES[idx]["recon"]), gr.update(interactive=True)
|
| 643 |
+
return "", gr.update(interactive=False)
|
| 644 |
+
|
| 645 |
+
example_gallery.select(
|
| 646 |
+
_on_example_select, None, [artifact_state, session_btn],
|
| 647 |
+
).then(
|
| 648 |
+
live_session, [artifact_state], [viewer_html, session_status],
|
| 649 |
+
show_progress="hidden",
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
|
| 653 |
# ---------------------------------------------------------------------------
|
| 654 |
# Launch Gradio (which starts uvicorn) and attach the viser reverse-proxy routes
|
examples/dl3dv_scene139.jpg
ADDED
|
examples/dl3dv_scene9.jpg
ADDED
|
examples/mipnerf360_garden.jpg
ADDED
|
examples/mipnerf360_kitchen.jpg
ADDED
|
examples/tandt_truck.jpg
ADDED
|
viser_app/recon_scene.py
CHANGED
|
@@ -210,14 +210,17 @@ def reconstruct(ckpt_root: Path | None, ckpt_name: str, scene_data: dict,
|
|
| 210 |
|
| 211 |
|
| 212 |
def _move_kv_to_cpu(kv):
|
|
|
|
|
|
|
|
|
|
| 213 |
if kv is None:
|
| 214 |
return None
|
|
|
|
|
|
|
| 215 |
if isinstance(kv, dict):
|
| 216 |
-
return {k: (v
|
| 217 |
if isinstance(kv, (tuple, list)):
|
| 218 |
return type(kv)(_move_kv_to_cpu(v) for v in kv)
|
| 219 |
-
if isinstance(kv, torch.Tensor):
|
| 220 |
-
return kv.cpu()
|
| 221 |
return kv
|
| 222 |
|
| 223 |
|
|
|
|
| 210 |
|
| 211 |
|
| 212 |
def _move_kv_to_cpu(kv):
|
| 213 |
+
"""Move baked fast-weights to CPU, storing floating tensors as fp16 to halve
|
| 214 |
+
the artifact size on disk (upcast back to fp32 at load time, see
|
| 215 |
+
render._kv_to_cuda)."""
|
| 216 |
if kv is None:
|
| 217 |
return None
|
| 218 |
+
if isinstance(kv, torch.Tensor):
|
| 219 |
+
return kv.half().cpu() if kv.is_floating_point() else kv.cpu()
|
| 220 |
if isinstance(kv, dict):
|
| 221 |
+
return {k: _move_kv_to_cpu(v) for k, v in kv.items()}
|
| 222 |
if isinstance(kv, (tuple, list)):
|
| 223 |
return type(kv)(_move_kv_to_cpu(v) for v in kv)
|
|
|
|
|
|
|
| 224 |
return kv
|
| 225 |
|
| 226 |
|
viser_app/render.py
CHANGED
|
@@ -68,14 +68,16 @@ def load_model(ckpt_root: Path | None = None, ckpt_name: str = "latest.pt",
|
|
| 68 |
|
| 69 |
|
| 70 |
def _kv_to_cuda(kv):
|
|
|
|
|
|
|
| 71 |
if kv is None:
|
| 72 |
return None
|
|
|
|
|
|
|
| 73 |
if isinstance(kv, dict):
|
| 74 |
-
return {k: (v
|
| 75 |
if isinstance(kv, (tuple, list)):
|
| 76 |
return type(kv)(_kv_to_cuda(v) for v in kv)
|
| 77 |
-
if isinstance(kv, torch.Tensor):
|
| 78 |
-
return kv.cuda()
|
| 79 |
return kv
|
| 80 |
|
| 81 |
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
def _kv_to_cuda(kv):
|
| 71 |
+
"""Move baked fast-weights to CUDA, upcasting fp16 (how they're stored on
|
| 72 |
+
disk, see recon_scene._move_kv_to_cpu) back to fp32 for rendering."""
|
| 73 |
if kv is None:
|
| 74 |
return None
|
| 75 |
+
if isinstance(kv, torch.Tensor):
|
| 76 |
+
return (kv.float() if kv.dtype == torch.float16 else kv).cuda()
|
| 77 |
if isinstance(kv, dict):
|
| 78 |
+
return {k: _kv_to_cuda(v) for k, v in kv.items()}
|
| 79 |
if isinstance(kv, (tuple, list)):
|
| 80 |
return type(kv)(_kv_to_cuda(v) for v in kv)
|
|
|
|
|
|
|
| 81 |
return kv
|
| 82 |
|
| 83 |
|