32c7336bf6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
"""
|
|
Estimación de profundidad monocular a partir de una sola imagen con Depth-Anything-V2.
|
|
|
|
Función del registry (grupo de capacidad `img-to-3d`, dominio `datascience`). Promovida desde
|
|
la app `img_to_3d_webapp` para que cualquier artefacto pueda estimar un mapa de profundidad sin
|
|
reimplementar la carga del modelo HuggingFace ni la normalización del resultado.
|
|
|
|
Impura: descarga/carga pesos de un modelo de transformers, usa GPU si está disponible y mantiene
|
|
una caché de pipelines a nivel de proceso para no recargar en cada llamada.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
# El pipeline de transformers es caro de instanciar (carga de pesos). Se cachea por
|
|
# (modelo, device) a nivel de módulo para que un servicio no recargue en cada request.
|
|
# Es estado mutable de PROCESO: documentado como impureza (ver .md "Gotchas"). Se puede
|
|
# desactivar por llamada con use_cache=False.
|
|
_PIPE_CACHE: dict = {}
|
|
|
|
|
|
def _resolve_device(device: str) -> int:
|
|
"""Resuelve el índice de device para transformers.pipeline (0=GPU0, -1=CPU)."""
|
|
import torch
|
|
|
|
if device == "cpu":
|
|
return -1
|
|
if device == "auto":
|
|
return 0 if torch.cuda.is_available() else -1
|
|
# device explícito tipo "cuda:0" o un índice
|
|
try:
|
|
return int(device)
|
|
except ValueError:
|
|
return 0 if device.startswith("cuda") else -1
|
|
|
|
|
|
def _build_pipe(model_name: str, device: str):
|
|
from transformers import pipeline
|
|
|
|
return pipeline("depth-estimation", model=model_name, device=_resolve_device(device))
|
|
|
|
|
|
def _get_pipe(model_name: str, device: str, use_cache: bool):
|
|
if not use_cache:
|
|
return _build_pipe(model_name, device)
|
|
key = (model_name, device)
|
|
pipe = _PIPE_CACHE.get(key)
|
|
if pipe is None:
|
|
pipe = _build_pipe(model_name, device)
|
|
_PIPE_CACHE[key] = pipe
|
|
return pipe
|
|
|
|
|
|
def estimate_image_depth(
|
|
image_path: str,
|
|
model_name: str = "depth-anything/Depth-Anything-V2-Small-hf",
|
|
device: str = "auto",
|
|
use_cache: bool = True,
|
|
) -> dict:
|
|
"""
|
|
Estima un mapa de profundidad monocular a partir de una única imagen.
|
|
|
|
Parámetros:
|
|
image_path: ruta a la imagen de entrada (cualquier formato que PIL abra).
|
|
model_name: id de modelo HuggingFace de estimación de profundidad.
|
|
device: "auto" (GPU si hay), "cpu", o índice/cadena cuda explícita ("cuda:0", "0").
|
|
use_cache: si True (default) reutiliza el pipeline cacheado por (modelo, device) a
|
|
nivel de proceso; si False construye uno nuevo y no toca la caché.
|
|
|
|
Devuelve (dict, nunca lanza):
|
|
Éxito: {"status": "ok", "depth": ndarray HxW float32 normalizado a [0,1]
|
|
(1 = más cerca de la cámara), "image": PIL.Image RGB original,
|
|
"height": H, "width": W, "model": model_name, "device": device}.
|
|
Error: {"status": "error", "error": str} (ruta inválida, modelo no disponible,
|
|
device inválido, fallo de inferencia).
|
|
"""
|
|
try:
|
|
image = Image.open(image_path).convert("RGB")
|
|
pipe = _get_pipe(model_name, device, use_cache)
|
|
result = pipe(image)
|
|
depth = np.asarray(result["depth"], dtype=np.float32)
|
|
|
|
# Normalizar a [0,1]. Depth-Anything devuelve disparidad relativa (mayor = más cerca).
|
|
d = depth - depth.min()
|
|
peak = d.max()
|
|
if peak > 0:
|
|
d = d / peak
|
|
|
|
H, W = d.shape
|
|
return {
|
|
"status": "ok",
|
|
"depth": d,
|
|
"image": image,
|
|
"height": int(H),
|
|
"width": int(W),
|
|
"model": model_name,
|
|
"device": device,
|
|
}
|
|
except Exception as e: # noqa: BLE001
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Demo runner para `fn run estimate_image_depth_py_datascience <image_path> [model] [device]`.
|
|
# Imprime un resumen JSON-serializable (el ndarray y la PIL.Image no se serializan).
|
|
import json
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print(json.dumps({"status": "error", "error": "uso: <image_path> [model_name] [device]"}))
|
|
sys.exit(1)
|
|
|
|
path = sys.argv[1]
|
|
model = sys.argv[2] if len(sys.argv) > 2 else "depth-anything/Depth-Anything-V2-Small-hf"
|
|
dev = sys.argv[3] if len(sys.argv) > 3 else "auto"
|
|
|
|
res = estimate_image_depth(path, model_name=model, device=dev)
|
|
if res["status"] == "ok":
|
|
depth = res["depth"]
|
|
summary = {
|
|
"status": "ok",
|
|
"height": res["height"],
|
|
"width": res["width"],
|
|
"depth_min": float(depth.min()),
|
|
"depth_max": float(depth.max()),
|
|
"depth_mean": round(float(depth.mean()), 4),
|
|
"model": res["model"],
|
|
"device": res["device"],
|
|
}
|
|
print(json.dumps(summary))
|
|
else:
|
|
print(json.dumps(res))
|
|
sys.exit(1)
|