ZDPShift / engine.py
Jian Shi
Video tab: three temporal clips through the video matchers, mp4 outputs
4ed94f8
Raw
History Blame Contribute Delete
9.75 kB
"""Model zoo for the ZDPShift Space: six backbones, zero-shot and corrected.
Weights resolve from ZDP_WEIGHTS_ROOT if set (local development), otherwise
from the shijianjian/ZDPShift model repo via snapshot_download. Image models
are fine-tuned on translated SceneFlow; video models on translated SceneFlow +
Dynamic Replica, matching the data their released baselines were trained on.
The three image repos and BiDAStereo's vendored RAFT all ship a top-level
`core` package; each family's modules and sys.path entries are swapped in and
out when the active family changes, so all six coexist in one process.
"""
import os
import shutil
import sys
import numpy as np
import torch
HERE = os.path.dirname(os.path.abspath(__file__))
CODE = os.path.join(HERE, "zdp")
sys.path.insert(0, CODE)
sys.path.insert(0, os.path.join(CODE, "third_party"))
_REQUIRED = [f"{m}_{v}.pth" for m in ("raft", "igev", "fs", "dynamic", "bida", "sav")
for v in ("zs", "ours")] + ["fs_cfg.yaml", "video_depth_anything_vits.pth"]
def _weights_root() -> str:
"""env var > ./weights next to the app > hub download — validated."""
candidates = []
env = os.environ.get("ZDP_WEIGHTS_ROOT")
if env:
candidates.append(("ZDP_WEIGHTS_ROOT", env))
local = os.path.join(HERE, "weights")
if os.path.isdir(local):
candidates.append(("./weights", local))
for label, root in candidates:
missing = [f for f in _REQUIRED if not os.path.exists(os.path.join(root, f))]
if not missing:
return root
raise RuntimeError(f"weights root {label}={root} is missing: {missing}")
from huggingface_hub import snapshot_download
root = snapshot_download("shijianjian/ZDPShift", allow_patterns=_REQUIRED)
missing = [f for f in _REQUIRED if not os.path.exists(os.path.join(root, f))]
if missing:
raise RuntimeError(
f"hub repo shijianjian/ZDPShift is missing {missing} — "
"upload the demo weight set (see hf_weights_stage/upload.sh) or set "
"ZDP_WEIGHTS_ROOT / place a weights/ folder next to app.py")
return root
ROOT = _weights_root()
W = { # (zero-shot, corrected)
"raft": (f"{ROOT}/raft_zs.pth", f"{ROOT}/raft_ours.pth"),
"igev": (f"{ROOT}/igev_zs.pth", f"{ROOT}/igev_ours.pth"),
"fs": (f"{ROOT}/fs_zs.pth", f"{ROOT}/fs_ours.pth"),
"dynamic": (f"{ROOT}/dynamic_zs.pth", f"{ROOT}/dynamic_ours.pth"),
"bida": (f"{ROOT}/bida_zs.pth", f"{ROOT}/bida_ours.pth"),
"sav": (f"{ROOT}/sav_zs.pth", f"{ROOT}/sav_ours.pth"),
}
FS_CFG = f"{ROOT}/fs_cfg.yaml"
NAMES = {"raft": "RAFT-Stereo", "igev": "IGEV-Stereo", "fs": "FoundationStereo",
"dynamic": "DynamicStereo", "bida": "BiDAStereo", "sav": "StereoAnyVideo"}
IMAGE, VIDEO = ("raft", "igev", "fs"), ("dynamic", "bida", "sav")
# StereoAnyVideo constructs its VDA prior from a repo-relative path; the actual
# values are then overwritten by the checkpoint (the prior is frozen), but the
# file must exist where the constructor looks for it.
_VDA_DST = os.path.join(CODE, "third_party/StereoAnyVideo/models",
"Video-Depth-Anything/checkpoints/video_depth_anything_vits.pth")
def _ensure_vda():
if not os.path.exists(_VDA_DST):
os.makedirs(os.path.dirname(_VDA_DST), exist_ok=True)
shutil.copyfile(f"{ROOT}/video_depth_anything_vits.pth", _VDA_DST)
# ---- image-family namespace juggling ------------------------------------
_family_modules: dict[str, dict] = {}
_active_family: str | None = None
_CLASH = ("core", "models", "raft_stereo", "igev_stereo", "utils")
_FAMILY_DIRS = {
"raft": [os.path.join(CODE, "third_party/RAFT-Stereo"),
os.path.join(CODE, "third_party/RAFT-Stereo/core")],
"igev": [os.path.join(CODE, "third_party/IGEV/IGEV-Stereo"),
os.path.join(CODE, "third_party/IGEV/IGEV-Stereo/core")],
"fs": [os.path.join(CODE, "third_party/FoundationStereo")],
"bida": [os.path.join(CODE, "third_party/bidastereo/third_party/RAFT"),
os.path.join(CODE, "third_party/bidastereo/third_party/RAFT/core")],
}
def _swap_family(fam: str):
global _active_family
if fam == _active_family:
return
if _active_family is not None:
stash = {}
for name in list(sys.modules):
if name.split(".")[0] in _CLASH:
stash[name] = sys.modules.pop(name)
_family_modules[_active_family] = stash
for name, mod in _family_modules.pop(fam, {}).items():
sys.modules[name] = mod
others = {d for f, dirs in _FAMILY_DIRS.items() if f != fam for d in dirs}
sys.path[:] = [p for p in sys.path if p not in others]
for d in reversed(_FAMILY_DIRS[fam]):
if d in sys.path:
sys.path.remove(d)
sys.path.insert(0, d)
_active_family = fam
_cache: dict[tuple, object] = {}
_infer_fns: dict[str, object] = {}
def _dev() -> str:
"""Resolved per call: on ZeroGPU, CUDA exists only inside the GPU-decorated
handler; on a CPU Space it never does; locally it always does."""
return "cuda" if torch.cuda.is_available() else "cpu"
def _get_image(fam: str, variant: str):
key = (fam, variant)
if key not in _cache:
_swap_family(fam)
if fam == "raft":
import eval_raft_stereo as ev
net = ev.load_model(W[fam][variant == "ours"], symmetric=(variant == "ours"),
device=_dev())
elif fam == "igev":
import eval_igev_signed as ev
net = (ev.load_model(W[fam][1], 64, 192, device=_dev()) if variant == "ours"
else ev.load_model(W[fam][0], 0, 192, device=_dev()))
else:
import eval_foundation_stereo as ev
net = ev.load_model(W[fam][variant == "ours"], FS_CFG,
variant == "ours", 64, 192, device=_dev())
_infer_fns[fam] = ev.infer
_cache[key] = net
else:
_swap_family(fam)
return _cache[key]
def _get_video(fam: str, variant: str):
if fam == "bida":
_swap_family("bida")
if fam == "sav":
_ensure_vda()
key = (fam, variant)
if key not in _cache:
import vsm
_cache[key] = vsm.build(fam, W[fam][variant == "ours"], device=_dev())
return _cache[key]
# ---- public API ----------------------------------------------------------
def predict(fam: str, variant: str, left: np.ndarray, right: np.ndarray,
width: int = 960, iters: int = 24) -> np.ndarray:
"""Signed disparity [H,W] at input resolution; positive in front of the
screen plane, negative behind it."""
import cv2
if _dev() == "cpu": # keep CPU Spaces responsive
width, iters = min(width, 640), min(iters, 16)
H0, W0 = left.shape[:2]
s = width / W0
nw, nh = max(64, int(round(W0 * s)) // 32 * 32), max(64, int(round(H0 * s)) // 32 * 32)
L = cv2.resize(left, (nw, nh), interpolation=cv2.INTER_AREA).astype(np.float32)
R = cv2.resize(right, (nw, nh), interpolation=cv2.INTER_AREA).astype(np.float32)
if fam in IMAGE:
net = _get_image(fam, variant)
d = _infer_fns[fam](net, L, R, iters, device=_dev())
else:
import vsm
net = _get_video(fam, variant)
T = 5
video = np.stack([np.stack([L, R]).transpose(0, 3, 1, 2)] * T)
sgn = {"dynamic": -1.0, "bida": -1.0, "sav": 1.0}[fam]
d = sgn * vsm.infer_clip(fam, net, torch.from_numpy(video), iters=16,
max_width=width, device=_dev())[T // 2]
d = np.asarray(d)
d = cv2.resize(d, (W0, H0), interpolation=cv2.INTER_LINEAR) / (nw / W0)
return d
def colorize(d: np.ndarray, vmax: float | None = None) -> np.ndarray:
"""Signed disparity -> RdBu_r (red = in front, blue = behind screen)."""
hexes = ["053061", "2166ac", "4393c3", "92c5de", "d1e5f0", "f7f7f7",
"fddbc7", "f4a582", "d6604d", "b2182b", "67001f"]
anchors = np.array([[int(h[i:i + 2], 16) for i in (0, 2, 4)] for h in hexes],
dtype=np.float32)
if vmax is None:
vmax = max(8.0, float(np.percentile(np.abs(d), 99)))
x = (np.clip(d / vmax, -1, 1) * 0.5 + 0.5) * (len(hexes) - 1)
i = np.clip(x.astype(int), 0, len(hexes) - 2)
t = (x - i)[..., None]
return (anchors[i] * (1 - t) + anchors[i + 1] * t).astype(np.uint8)
def predict_clip(fam: str, variant: str, lefts: list, rights: list,
width: int = 960, iters: int = 16) -> np.ndarray:
"""Signed disparity [T,H,W] for a temporal clip (video matchers only)."""
import cv2
assert fam in VIDEO, "predict_clip is for the video matchers"
T = len(lefts)
if _dev() == "cpu": # keep CPU Spaces responsive
width, iters = min(width, 512), min(iters, 12)
lefts, rights = lefts[:8], rights[:8]
T = len(lefts)
H0, W0 = lefts[0].shape[:2]
s = width / W0
nw, nh = max(64, int(round(W0 * s)) // 32 * 32), max(64, int(round(H0 * s)) // 32 * 32)
frames = []
for L, R in zip(lefts, rights):
Ls = cv2.resize(L, (nw, nh), interpolation=cv2.INTER_AREA)
Rs = cv2.resize(R, (nw, nh), interpolation=cv2.INTER_AREA)
frames.append(np.stack([Ls, Rs]).transpose(0, 3, 1, 2))
video = np.stack(frames).astype(np.float32)
import vsm
net = _get_video(fam, variant)
sgn = {"dynamic": -1.0, "bida": -1.0, "sav": 1.0}[fam]
d = sgn * np.asarray(vsm.infer_clip(fam, net, torch.from_numpy(video),
iters=iters, max_width=width))
out = np.stack([cv2.resize(f, (W0, H0), interpolation=cv2.INTER_LINEAR)
for f in d]) / (nw / W0)
return out