Download src/depth.py from ayzeksalimli/hand-tracking-drawing: direct link, hf CLI and curl.
- Browser
- Download file 6.42 kB
-
https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/depth.py
- Command line
-
hf download hf://ayzeksalimli/hand-tracking-drawing/src/depth.py
-
curl -L -o depth.py https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/depth.py
6.42 kB
| """Monocular depth estimation.""" | |
| from __future__ import annotations | |
| import threading | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| def _pick_device() -> str: | |
| """Best available inference device.""" | |
| try: | |
| import torch | |
| except ImportError: | |
| return "cpu" | |
| try: | |
| if torch.cuda.is_available(): | |
| return "cuda" | |
| if torch.backends.mps.is_available(): | |
| return "mps" | |
| except (AttributeError, RuntimeError): | |
| return "cpu" | |
| return "cpu" | |
| class DepthEstimator: | |
| """Monocular depth from a YOLO depth model.""" | |
| def __init__(self, model_path: str | Path, imgsz: int = 384) -> None: | |
| from ultralytics import YOLO | |
| model_path = Path(model_path) | |
| if not model_path.exists(): | |
| raise FileNotFoundError(f"Depth model not found: {model_path}") | |
| self.device = _pick_device() | |
| self.model = YOLO(str(model_path)) | |
| self.imgsz = imgsz | |
| self._lo: float | None = None | |
| self._hi: float | None = None | |
| self._bounds_alpha = 0.08 | |
| def __call__(self, frame: np.ndarray) -> np.ndarray: | |
| """Raw depth map, same size as frame.""" | |
| result = self.model.predict( | |
| frame, imgsz=self.imgsz, device=self.device, verbose=False | |
| )[0] | |
| depth = result.depth.data | |
| if hasattr(depth, "cpu"): | |
| depth = depth.cpu().numpy() | |
| depth = np.asarray(depth, dtype=np.float32) | |
| if depth.ndim == 3: | |
| depth = depth[0] | |
| if depth.shape[:2] != frame.shape[:2]: | |
| depth = cv2.resize(depth, (frame.shape[1], frame.shape[0]), | |
| interpolation=cv2.INTER_LINEAR) | |
| return depth | |
| def normalize(self, depth: np.ndarray) -> np.ndarray: | |
| """Stable 0..1 map, 0 near, 1 far.""" | |
| finite = depth[np.isfinite(depth)] | |
| if finite.size == 0: | |
| return np.full(depth.shape, 0.5, dtype=np.float32) | |
| lo, hi = float(np.percentile(finite, 2.0)), float(np.percentile(finite, 98.0)) | |
| if self._lo is None or self._hi is None: | |
| self._lo, self._hi = lo, hi | |
| else: | |
| self._lo += self._bounds_alpha * (lo - self._lo) | |
| self._hi += self._bounds_alpha * (hi - self._hi) | |
| span = max(self._hi - self._lo, 1e-6) | |
| norm = np.clip((depth - self._lo) / span, 0.0, 1.0) | |
| return np.nan_to_num(norm, nan=0.5).astype(np.float32) | |
| def colorize(depth_norm: np.ndarray) -> np.ndarray: | |
| """Turbo colormap of a normalized depth map.""" | |
| u8 = (np.clip(depth_norm, 0.0, 1.0) * 255.0).astype(np.uint8) | |
| return cv2.applyColorMap(255 - u8, cv2.COLORMAP_TURBO) | |
| def sample(depth_map: np.ndarray, x: float, y: float, radius: int = 9, | |
| percentile: float = 20.0, default: float = 0.0) -> float: | |
| """Nearest surface around a point.""" | |
| h, w = depth_map.shape[:2] | |
| xi = int(np.clip(round(float(x)), 0, w - 1)) | |
| yi = int(np.clip(round(float(y)), 0, h - 1)) | |
| x0, x1 = max(0, xi - radius), min(w, xi + radius + 1) | |
| y0, y1 = max(0, yi - radius), min(h, yi + radius + 1) | |
| patch = depth_map[y0:y1, x0:x1] | |
| patch = patch[np.isfinite(patch)] | |
| if patch.size == 0: | |
| return default | |
| return float(np.percentile(patch, percentile)) | |
| class DepthWorker: | |
| """Background depth estimation thread.""" | |
| def __init__(self, model_path: str | Path, imgsz: int = 384, | |
| input_width: int = 640) -> None: | |
| self.estimator = DepthEstimator(model_path, imgsz) | |
| self.device = self.estimator.device | |
| self.input_width = input_width | |
| self._pending: np.ndarray | None = None | |
| self._target: tuple[int, int] | None = None | |
| self._metric: np.ndarray | None = None | |
| self._norm: np.ndarray | None = None | |
| self._seq = 0 | |
| self._lock = threading.Lock() | |
| self._wake = threading.Event() | |
| self._stop = threading.Event() | |
| self._thread = threading.Thread(target=self._loop, daemon=True) | |
| self._thread.start() | |
| def submit(self, frame: np.ndarray) -> None: | |
| """Queue the newest frame.""" | |
| h, w = frame.shape[:2] | |
| if w > self.input_width: | |
| k = self.input_width / float(w) | |
| small = cv2.resize(frame, (self.input_width, max(1, int(round(h * k)))), | |
| interpolation=cv2.INTER_AREA) | |
| else: | |
| small = frame.copy() | |
| with self._lock: | |
| self._pending = small | |
| self._target = (w, h) | |
| self._wake.set() | |
| def _loop(self) -> None: | |
| """Consume frames until stopped.""" | |
| while not self._stop.is_set(): | |
| self._wake.wait(0.1) | |
| self._wake.clear() | |
| with self._lock: | |
| frame, target = self._pending, self._target | |
| self._pending = None | |
| if frame is None or target is None: | |
| continue | |
| try: | |
| metric = self.estimator(frame) | |
| norm = self.estimator.normalize(metric) | |
| except Exception as exc: # keep the app alive on backend errors | |
| print(f"[!] depth failed: {exc}", flush=True) | |
| self._stop.set() | |
| return | |
| if (metric.shape[1], metric.shape[0]) != target: | |
| metric = cv2.resize(metric, target, interpolation=cv2.INTER_LINEAR) | |
| norm = cv2.resize(norm, target, interpolation=cv2.INTER_LINEAR) | |
| with self._lock: | |
| self._metric = metric | |
| self._norm = norm | |
| self._seq += 1 | |
| def latest(self) -> np.ndarray | None: | |
| """Newest metric depth map.""" | |
| with self._lock: | |
| return self._metric | |
| def latest_norm(self) -> np.ndarray | None: | |
| """Newest normalized depth map.""" | |
| with self._lock: | |
| return self._norm | |
| def ready(self) -> bool: | |
| """A depth map is available.""" | |
| return self.latest is not None | |
| def frames(self) -> int: | |
| """Number of finished estimations.""" | |
| with self._lock: | |
| return self._seq | |
| def close(self) -> None: | |
| """Stop the worker thread.""" | |
| self._stop.set() | |
| self._wake.set() | |
| self._thread.join(timeout=1.0) | |