multimodalart's picture
multimodalart HF Staff
Upload app.py with huggingface_hub
1f77def verified
Raw
History Blame Contribute Delete
6.77 kB
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # MUST come before torch / any CUDA-touching import
import torch
import torch.nn.functional as F
import gradio as gr
import numpy as np
import cv2
import yaml
import sys
import tempfile
from pathlib import Path
from huggingface_hub import hf_hub_download
from omegaconf import OmegaConf
# Add the app root to sys.path so core.* and Utils are importable
CODE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, CODE_DIR)
from core.utils.utils import InputPadder
from Utils import vis_disparity, AMP_DTYPE
MODEL_REPO = "nvidia/c-fast-foundationstereo"
WEIGHTS_FILENAME = "model_best_bp2_serialize.pth"
CFG_FILENAME = "cfg.yaml"
def _load_model():
"""Download the checkpoint and cfg from the HF Hub, then torch.load the pickled model."""
weights_path = hf_hub_download(MODEL_REPO, WEIGHTS_FILENAME, repo_type="model")
cfg_path = hf_hub_download(MODEL_REPO, CFG_FILENAME, repo_type="model")
with open(cfg_path, "r") as f:
cfg: dict = yaml.safe_load(f)
cfg.setdefault("valid_iters", 8)
cfg.setdefault("max_disp", 192)
cfg.setdefault("normalize", True)
cfg.setdefault("cv_group", 8)
cfg.setdefault("volume_dim", 28)
args = OmegaConf.create(cfg)
# Monkey-patch torch.load to allow weights_only=False for this pickled model
_orig_load = torch.load
torch.load = lambda *a, **k: _orig_load(*a, **{**k, "weights_only": k.get("weights_only", False)})
model = _orig_load(weights_path, map_location="cpu", weights_only=False)
torch.load = _orig_load
model.args = args
model.cuda().eval()
return model, args
model, model_args = _load_model()
model = model.to("cuda")
@spaces.GPU(duration=60)
def predict_disparity(
left_image: np.ndarray,
right_image: np.ndarray,
valid_iters: int = 8,
max_disp: int = 192,
) -> tuple:
"""Estimate disparity from a rectified binocular stereo image pair.
Given a rectified left and right stereo image (RGB), produces a color-mapped
disparity visualization alongside the raw 16-bit disparity map.
Args:
left_image: Rectified left stereo image (RGB), as a numpy array (H, W, 3).
right_image: Rectified right stereo image (RGB), as a numpy array (H, W, 3).
valid_iters: Number of GRU refinement iterations (more = better quality, less = faster).
max_disp: Maximum disparity for volume encoding. 192 is sufficient for most scenes.
Returns:
A tuple of (color-mapped disparity visualization, 16-bit disparity map).
"""
if left_image is None or right_image is None:
raise ValueError("Both left and right images are required.")
# Ensure RGB
img0 = left_image[..., :3]
img1 = right_image[..., :3]
if img0.dtype != np.uint8:
img0 = (img0 * 255).astype(np.uint8) if img0.max() <= 1.0 else img0.astype(np.uint8)
img1 = (img1 * 255).astype(np.uint8) if img1.max() <= 1.0 else img1.astype(np.uint8)
H, W = img0.shape[:2]
img0_ori = img0.copy()
img1_ori = img1.copy()
# Resize right to match left if needed
if img1.shape[:2] != (H, W):
img1 = cv2.resize(img1, (W, H))
img0_t = torch.as_tensor(img0).cuda().float()[None].permute(0, 3, 1, 2)
img1_t = torch.as_tensor(img1).cuda().float()[None].permute(0, 3, 1, 2)
padder = InputPadder(img0_t.shape, divis_by=32, force_square=False)
img0_t, img1_t = padder.pad(img0_t, img1_t)
model.args.valid_iters = valid_iters
model.args.max_disp = max_disp
with torch.amp.autocast("cuda", enabled=True, dtype=AMP_DTYPE):
disp = model.forward(img0_t, img1_t, iters=valid_iters, test_mode=True, optimize_build_volume="pytorch1")
disp = padder.unpad(disp.float())
disp_np = disp.data.cpu().numpy().reshape(H, W).clip(0, None)
# Color-mapped visualization
vis = vis_disparity(disp_np, color_map=cv2.COLORMAP_TURBO)
# Side-by-side: left | right | disparity
combined = np.concatenate([img0_ori, img1_ori, vis], axis=1)
# Save 16-bit disparity to a temp file
disp_16 = (disp_np * 256).clip(0, 65535).astype(np.uint16)
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
cv2.imwrite(tmp.name, disp_16)
return combined, tmp.name
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown("# Fast-FoundationStereo: Real-Time Zero-Shot Stereo Disparity")
gr.Markdown(
"Upload a **rectified** binocular stereo image pair to estimate a disparity map. "
"This is [nvidia/c-fast-foundationstereo](https://huggingface.co/nvidia/c-fast-foundationstereo), "
"a 14.6M-parameter real-time stereo foundation model from NVIDIA (CVPR 2026)."
)
with gr.Row():
with gr.Column():
left_input = gr.Image(label="Left Image (rectified RGB)", type="numpy")
right_input = gr.Image(label="Right Image (rectified RGB)", type="numpy")
with gr.Column():
disparity_vis = gr.Image(label="Disparity Visualization (left | right | color-mapped disparity)")
disparity_raw = gr.Image(label="16-bit Disparity Map", type="filepath")
with gr.Accordion("Advanced Settings", open=False):
valid_iters_slider = gr.Slider(
minimum=2, maximum=16, value=8, step=1,
label="Valid Iterations (GRU refinement steps)",
info="More iterations = better quality but slower. 8 is default, 4 is faster."
)
max_disp_slider = gr.Slider(
minimum=64, maximum=416, value=192, step=32,
label="Max Disparity",
info="Maximum disparity for volume encoding. 192 is enough for most scenes."
)
run_btn = gr.Button("Estimate Disparity", variant="primary")
gr.Examples(
examples=[
["examples/left.png", "examples/right.png", 8, 192],
["examples/left.png", "examples/right.png", 4, 192],
],
inputs=[left_input, right_input, valid_iters_slider, max_disp_slider],
outputs=[disparity_vis, disparity_raw],
fn=predict_disparity,
cache_examples=True,
cache_mode="lazy",
)
run_btn.click(
fn=predict_disparity,
inputs=[left_input, right_input, valid_iters_slider, max_disp_slider],
outputs=[disparity_vis, disparity_raw],
)
if __name__ == "__main__":
demo.launch(mcp_server=True)