"""Monocular depth estimation (MiDaS) with a synthetic fallback. Real depth (when the ML stack is present) is used as inpaint/ControlNet conditioning and to help recover the floor plane. Without ML we synthesize a plausible vertical gradient (far near the top/horizon, near at the bottom), which is enough for conditioning and a UI preview and is consistent with the heuristic floor model. """ from __future__ import annotations import functools from pathlib import Path import cv2 import numpy as np from ..config import settings from .ml_runtime import ml_available @functools.lru_cache(maxsize=1) def _load_midas(): import torch model = torch.hub.load("intel-isl/MiDaS", settings.MIDAS_MODEL) transforms = torch.hub.load("intel-isl/MiDaS", "transforms") tf = ( transforms.small_transform if "small" in settings.MIDAS_MODEL.lower() else transforms.dpt_transform ) model.eval() return model, tf def estimate_depth(image_path: Path) -> np.ndarray: """Return a HxW float32 depth map normalized to [0, 1] (1 = nearest).""" img = cv2.imread(str(image_path)) if img is None: raise ValueError(f"Could not read image for depth: {image_path}") h, w = img.shape[:2] if not ml_available(): return _synthetic_depth(h, w) try: import torch model, tf = _load_midas() rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) batch = tf(rgb) with torch.no_grad(): pred = model(batch) pred = torch.nn.functional.interpolate( pred.unsqueeze(1), size=(h, w), mode="bicubic", align_corners=False ).squeeze().cpu().numpy() d = pred.astype("float32") d = (d - d.min()) / (float(np.ptp(d)) + 1e-6) return d except Exception: return _synthetic_depth(h, w) def _synthetic_depth(h: int, w: int) -> np.ndarray: col = np.linspace(0.0, 1.0, h, dtype="float32").reshape(h, 1) return np.repeat(col, w, axis=1) def depth_to_image(depth: np.ndarray) -> np.ndarray: """Convert a [0, 1] depth map to an 8-bit BGR image for saving/preview.""" vis = (np.clip(depth, 0.0, 1.0) * 255).astype("uint8") return cv2.applyColorMap(vis, cv2.COLORMAP_INFERNO)