#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [ # "gradio>=5.49,<6", # "spaces", # "kernels", # "trimesh>=4.4", # "torch>=2.5.1", # "torchvision>=0.20.1", # "dvlt @ https://huggingface.co/spaces/blanchon/dvlt/resolve/main/packages/dvlt/wheels/dvlt-0.0.1-py3-none-any.whl", # ] # /// """Unofficial Gradio / ZeroGPU demo for Déjà View (DVLT). Déjà View (DVLT) — Looping Transformers for Multi-View 3D Reconstruction. DVLT loops a shared transformer block K times; we decode an intermediate point cloud every N steps so the viewer shows it converge. Run standalone with `uv run app.py` (deps in the PEP 723 header above), or as a Hugging Face ZeroGPU Space (deps in requirements.txt; torch from the image). """ from __future__ import annotations import os import sys import tempfile import time from dataclasses import dataclass, field import cv2 import gradio as gr import numpy as np import spaces import torch from accelerate import Accelerator from PIL import Image # Flash-Attention 3, prebuilt for ZeroGPU's Blackwell GPUs and exposed under the # module name DVLT imports (`from flash_attn_interface import flash_attn_func`). try: from kernels import get_kernel sys.modules["flash_attn_interface"] = get_kernel("kernels-community/flash-attn3", version=1) except Exception as exc: # local / no compatible kernel -> DVLT falls back to SDPA print(f"[dvlt] FA3 kernel unavailable ({exc}); using default attention.") from dvlt.common.constants import DataField, PredictionField from dvlt.common.geometry import depth_to_world_coords_points from dvlt.common.pose import to4x4 from dvlt.model.dvlt.model import DVLT, _slice_expand_flatten from dvlt.model_components import set_attn_backend from dvlt.util.preprocess import preprocess_images from dvlt.viz.depth import overlay_depth_map from dvlt.viz.glb import pointcloud_to_glb from dvlt.viz.pointcloud import zero_depths_on_pad # --- config (matched to the released nvidia/dvlt stage-2 checkpoint) --------- CHECKPOINT = os.environ.get("DVLT_CHECKPOINT", "nvidia/dvlt") IMG_SIZE = 504 PATCH_SIZE = 14 DEFAULT_STEPS = 12 # K — the model's default inference step count MAX_STEPS = 24 MAX_FRAMES = 16 # cap views so global attention stays within the ZeroGPU budget VIDEO_FPS_DEFAULT = 2.0 DECODE_EVERY_DEFAULT = 3 FRUSTUM_DEFAULT = 2.0 # % of scene radius CONF_DEFAULT = 50.0 MAX_POINTS_DEFAULT = 400_000 EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples") IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".bmp", ".JPG", ".JPEG", ".PNG") # --- model (placed on CUDA at module level per the ZeroGPU guidance) --------- # bf16 on GPU (the repo's inference precision); fp32 on CPU (bf16 breaks the # CPU pose solver). Autocast handles the cast; weights stay fp32. _ACCEL = Accelerator(mixed_precision="bf16" if torch.cuda.is_available() else "no") _MODEL: DVLT | None = None def load_model() -> DVLT: global _MODEL if _MODEL is None: try: set_attn_backend("fa3") except Exception as exc: # noqa: BLE001 print(f"[dvlt] fa3 unavailable ({exc}); using default attention.") model = DVLT(img_size=IMG_SIZE, depth_head_type="conv") model.load_pretrained(CHECKPOINT, strict=True) model.setup_test(_ACCEL) if torch.cuda.is_available(): model.model.to("cuda") _MODEL = model return _MODEL # --- point cloud / GLB / depth helpers --------------------------------------- def predictions_to_cloud(preds, batch, max_points, conf_threshold): """Depth-unproject + filter predictions into ``(points[N,3], colors[N,3] uint8)``.""" depths = preds[PredictionField.DEPTHS][0].float() cameras = preds[PredictionField.CAMERAS][0] extrinsics_c2w = to4x4(cameras.camera_to_worlds).float() intrinsics = cameras.get_intrinsics_matrices().float() world_points, _, valid_mask = depth_to_world_coords_points(depths, extrinsics_c2w, intrinsics) images = batch[DataField.IMAGES][0] colors = images.detach().float().cpu().permute(0, 2, 3, 1).numpy() * 255.0 pts = world_points.detach().float().cpu().numpy().reshape(-1, 3) cols = colors.reshape(-1, 3) mask = valid_mask.detach().cpu().numpy().reshape(-1) pad_ok = batch.get("gradio_valid_pixels", None) if pad_ok is not None: mask = mask & pad_ok[0].detach().cpu().numpy().reshape(-1) pts, cols = pts[mask], cols[mask] conf = preds.get(PredictionField.DEPTHS_CONF, None) if conf is None: conf = preds.get(PredictionField.WORLD_POINTS_DIRECT_CONF, None) if conf is not None and len(pts) > 0: conf_flat = conf[0].detach().float().cpu().numpy().reshape(-1)[mask] keep = conf_flat >= np.percentile(conf_flat, float(conf_threshold)) pts, cols = pts[keep], cols[keep] if max_points > 0 and len(pts) > max_points: idx = np.random.choice(len(pts), max_points, replace=False) pts, cols = pts[idx], cols[idx] return pts, cols.astype(np.uint8) def cameras_to_glb_inputs(preds, batch): cameras = preds[PredictionField.CAMERAS][0] c2ws = to4x4(cameras.camera_to_worlds).detach().float().cpu().numpy() intrinsics = cameras.get_intrinsics_matrices().detach().float().cpu().numpy() image_hw = tuple(int(v) for v in batch[DataField.IMAGES].shape[-2:]) return c2ws, intrinsics, image_hw def depth_overlays(preds, batch): depths = preds[PredictionField.DEPTHS][0].detach().float().cpu().numpy() depths = zero_depths_on_pad(depths, batch) images = batch[DataField.IMAGES][0].detach().float().cpu().permute(0, 2, 3, 1).numpy() return [overlay_depth_map(img, d) for img, d in zip(images, depths)] def cloud_to_glb(points, colors, c2ws=None, intrinsics=None, image_hw=None, show_cameras=True, frustum_frac=0.02): return pointcloud_to_glb( points, colors, cameras_c2w=c2ws if show_cameras else None, intrinsics=intrinsics, image_hw=image_hw, frustum_frac=float(frustum_frac) if show_cameras else 0.0, ) # --- streaming looped inference (the GPU function) --------------------------- @dataclass class StepResult: step: int total: int points: np.ndarray colors: np.ndarray c2ws: np.ndarray intrinsics: np.ndarray image_hw: tuple depth_overlays: list = field(default_factory=list) timings: str = "" # phase timings (logged by the main process for visibility) @spaces.GPU(duration=300) def infer(image_paths, num_steps, conf_threshold, max_points, decode_every): """Run the K-step looped inference in one GPU call; return a StepResult per decode. Non-generator on purpose: a lazily-consumed @spaces.GPU generator hangs on ZeroGPU. We do all GPU work here, return numpy results, and the CPU side reveals them progressively. """ model = load_model() m = model.model # DVLTModel # Move to the *real* GPU inside the @spaces.GPU function (vggt-omega pattern); # relying on module-level placement alone can leave compute on CPU here. device = torch.device("cuda" if torch.cuda.is_available() else "cpu") m.to(device) frames = [Image.open(p).convert("RGB") for p in image_paths] batch = preprocess_images(frames, IMG_SIZE, PATCH_SIZE, device) images = batch[DataField.IMAGES] B, S, _, H, W = images.shape K = int(np.clip(int(num_steps), 1, MAX_STEPS)) every = max(1, int(decode_every)) results = [] with torch.no_grad(), _ACCEL.autocast(): t0 = time.time() z_0 = m._encode_images(images) register_token = m.register_token.expand(B, S, -1, -1).reshape(B * S, m.num_register_tokens, -1) camera_token = _slice_expand_flatten(m.camera_token, B, S) x = torch.cat([camera_token, register_token, z_0], dim=1) rope_pos = m._get_rope_positions(B * S, H, W, device) ts = torch.linspace(0.0, 1.0, K).tolist() for i in range(K): t_next = ts[i + 1] if i + 1 < K else 1.0 x = m._interval_step(x, ts[i], t_next, rope_pos, B, S) is_last = i == K - 1 if not (is_last or (i + 1) % every == 0): continue preds = model._postprocess_predictions(batch, m._decode(x, H, W, B, S, rope_pos)) pts, rgb = predictions_to_cloud(preds, batch, int(max_points), float(conf_threshold)) c2ws, intrinsics, image_hw = cameras_to_glb_inputs(preds, batch) overlays = depth_overlays(preds, batch) if is_last else [] timings = ( f"dev={device} amp={_ACCEL.mixed_precision} xdtype={x.dtype} | " f"{S} views K={K} decode@{i + 1} at {time.time() - t0:.1f}s" ) results.append(StepResult(i + 1, K, pts, rgb, c2ws, intrinsics, image_hw, overlays, timings)) del preds return results # --- inputs: video -> frames -> image batch (the single source of truth) ----- def _cap(paths): """Evenly subsample to at most MAX_FRAMES.""" if len(paths) <= MAX_FRAMES: return paths idx = np.linspace(0, len(paths) - 1, MAX_FRAMES).round().astype(int) return [paths[i] for i in idx] def video_to_frames(video, video_fps): """Sample a video into frames and return their paths (to fill the image batch).""" if not video: return None out_dir = tempfile.mkdtemp(prefix="dvlt_frames_") cap = cv2.VideoCapture(video) src_fps = cap.get(cv2.CAP_PROP_FPS) stride = max(1, int(round((src_fps if src_fps and src_fps > 0 else 24.0) / max(float(video_fps), 0.1)))) paths, idx, saved = [], 0, 0 while True: ok, frame = cap.read() if not ok: break if idx % stride == 0: p = os.path.join(out_dir, f"frame_{saved:04d}.png") cv2.imwrite(p, frame) paths.append(p) saved += 1 idx += 1 cap.release() return _cap(sorted(paths)) def preview_images(image_paths): if not image_paths: return None, "Upload a video or images to begin." paths = _cap(sorted(image_paths)) return paths, f"Loaded {len(paths)} frame(s). Click **Reconstruct**." def _camera_info(n): return ( f"**{n} camera pose(s)** estimated. Each rainbow frustum is one input view " "(blue → red = frame order), linked along the camera trajectory." ) # --- reconstruction (streaming) + CPU-only re-render ------------------------- def reconstruct( image_paths, num_steps, conf_thres, max_points, decode_every, show_cam, frustum_scale, progress=gr.Progress(), ): if not image_paths: raise gr.Error("Upload a video or images first.") image_paths = _cap(sorted(image_paths)) frustum_frac = float(frustum_scale) / 100.0 print(f"[recon] {len(image_paths)} frames, K={num_steps} -> GPU", flush=True) results = infer(image_paths, int(num_steps), float(conf_thres), int(max_points), int(decode_every)) print(f"[recon] GPU returned {len(results)} step(s)", flush=True) for res in results: print(f"[recon] reveal step {res.step}/{res.total} ({res.timings})", flush=True) glb = cloud_to_glb( res.points, res.colors, c2ws=res.c2ws, intrinsics=res.intrinsics, image_hw=res.image_hw, show_cameras=bool(show_cam), frustum_frac=frustum_frac, ) is_final = res.step >= res.total progress(res.step / max(res.total, 1), desc=f"Refinement step {res.step}/{res.total}") status = ( f"✅ Done — {res.total} steps, {len(res.points):,} points, {len(res.c2ws)} cameras." if is_final else f"🔄 Step {res.step}/{res.total} — {len(res.points):,} points so far…" ) cloud = { "points": res.points, "colors": res.colors, "c2ws": res.c2ws, "intrinsics": res.intrinsics, "image_hw": res.image_hw, } depth_gallery = res.depth_overlays if (is_final and res.depth_overlays) else gr.update() yield glb, status, depth_gallery, _camera_info(len(res.c2ws)), cloud def update_view(cloud, max_points, show_cam, frustum_scale): """Re-render the cached cloud on CPU (density / cameras only).""" if not cloud: return None, "Nothing to update — run **Reconstruct** first." pts, cols = cloud["points"], cloud["colors"] if int(max_points) > 0 and len(pts) > int(max_points): idx = np.random.choice(len(pts), int(max_points), replace=False) pts, cols = pts[idx], cols[idx] glb = cloud_to_glb( pts, cols, c2ws=cloud["c2ws"], intrinsics=cloud["intrinsics"], image_hw=cloud["image_hw"], show_cameras=bool(show_cam), frustum_frac=float(frustum_scale) / 100.0, ) return glb, f"View updated — {len(pts):,} points. (Change confidence → re-run Reconstruct.)" # --- examples (videos only) -------------------------------------------------- def _build_examples(): rows = [] for vid, conf in [("conf_20_robot.mp4", 20.0), ("conf50.mp4", 50.0), ("conf_30.mp4", 30.0)]: p = os.path.join(EXAMPLES_DIR, vid) if os.path.isfile(p): rows.append([p, DEFAULT_STEPS, conf, MAX_POINTS_DEFAULT, True]) return rows _HEADER = """

🔁 Déjà View — DVLT

Looping Transformers for Multi-View 3D Reconstruction

📄 Paper  •  🌐 Project  •  🐙 Code  •  🤗 Model

⚠️ Unofficial demo, not affiliated with NVIDIA. Model weights are released under the NVIDIA non-commercial research license.

Upload a video (auto-sampled into frames) or a set of images, then hit Reconstruct. DVLT loops a shared transformer block K times to predict depth, camera poses and a 3D point cloud — the viewer updates after every step so you can watch it converge.

""" _CSS = ".dvlt-log * { font-size: 18px !important; font-weight: 600 !important; text-align: center !important; }" def build_demo() -> gr.Blocks: with gr.Blocks(theme=gr.themes.Ocean(), css=_CSS, title="DVLT 3D Demo") as demo: gr.HTML(_HEADER) cloud_state = gr.State() # cached final cloud for Update View with gr.Row(): with gr.Column(scale=2): input_video = gr.Video(label="Upload Video (sampled into frames)") video_fps = gr.Slider(0.5, 8.0, value=VIDEO_FPS_DEFAULT, step=0.5, label="Video sampling FPS") input_images = gr.File( file_count="multiple", file_types=["image"], type="filepath", label="Image batch (videos land here; or upload images directly)", ) gallery = gr.Gallery(label="Input frames", columns=4, height=240, object_fit="contain") with gr.Column(scale=4): status = gr.Markdown("Upload a video or images to begin.", elem_classes=["dvlt-log"]) model3d = gr.Model3D(label="Point cloud + camera poses", height=560, clear_color=[0, 0, 0, 0]) camera_info = gr.Markdown("") with gr.Row(): run_btn = gr.Button("Reconstruct", variant="primary", scale=2) view_btn = gr.Button("Update View", scale=1) gr.ClearButton([input_video, input_images, gallery, model3d, camera_info, cloud_state], scale=1) with gr.Accordion("Parameters", open=True): with gr.Row(): num_steps = gr.Slider( 1, MAX_STEPS, value=DEFAULT_STEPS, step=1, label="Refinement steps (K)", info="More steps = sharper geometry, more compute.", ) decode_every = gr.Slider( 1, 6, value=DECODE_EVERY_DEFAULT, step=1, label="Preview every N steps", info="How often to stream an intermediate cloud.", ) with gr.Row(): conf_thres = gr.Slider( 0, 99, value=CONF_DEFAULT, step=1, label="Confidence threshold (percentile)", info="Drops the least-confident points.", ) max_points = gr.Slider( 50_000, 2_000_000, value=MAX_POINTS_DEFAULT, step=50_000, label="Max points", ) with gr.Row(): frustum_scale = gr.Slider(0.0, 6.0, value=FRUSTUM_DEFAULT, step=0.1, label="Camera size (%)") show_cam = gr.Checkbox(value=True, label="Show cameras") depth_gallery = gr.Gallery(label="Depth maps", columns=4, height=240, object_fit="contain") recon_inputs = [input_images, num_steps, conf_thres, max_points, decode_every, show_cam, frustum_scale] recon_outputs = [model3d, status, depth_gallery, camera_info, cloud_state] examples = _build_examples() if examples: def run_example(video, k, conf, mp, show): paths = video_to_frames(video, VIDEO_FPS_DEFAULT) final = (None, "No output.", gr.update(), "", None) for out in reconstruct(paths, k, conf, mp, DECODE_EVERY_DEFAULT, show, FRUSTUM_DEFAULT): final = out glb, st, depth, cam, cloud = final return glb, st, depth, cam, cloud, paths gr.Markdown("### Examples  (click a row to load & reconstruct)") gr.Examples( examples=examples, inputs=[input_video, num_steps, conf_thres, max_points, show_cam], outputs=[model3d, status, depth_gallery, camera_info, cloud_state, input_images], fn=run_example, cache_examples=False, examples_per_page=6, ) # video -> frames -> image batch; the image batch drives the preview + reconstruction input_video.change(video_to_frames, [input_video, video_fps], [input_images]) input_images.change(preview_images, [input_images], [gallery, status]) run_btn.click(lambda: (None, "🔄 Reconstructing…"), None, [model3d, status]).then( reconstruct, recon_inputs, recon_outputs ) view_btn.click(update_view, [cloud_state, max_points, show_cam, frustum_scale], [model3d, status]) return demo # Place the model on CUDA at module level (ZeroGPU guidance). `infer` is already # defined above, so ZeroGPU detects the GPU function. if os.environ.get("DVLT_SKIP_AUTOLOAD") != "1": load_model() demo = build_demo() demo.queue(max_size=20) if __name__ == "__main__": # ssr_mode=False: the experimental Gradio SSR server can swallow streamed updates. demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True, ssr_mode=False)