feat(datascience): promover remove_background al registry + mask en depth_to_relief_glb (grupo img-to-3d)

Completa la promoción del flujo imagen->3D al registry (grupo de capacidad
img-to-3d), extraído de la app img_to_3d_webapp.

- remove_background_py_datascience (nueva): elimina el fondo con cascada
  rembg/U2Net -> OpenCV GrabCut -> umbral NumPy, compone el objeto sobre gris
  neutro y devuelve image + mask + engine. Impura, nunca lanza. Adaptada de
  backend/bg_removal.py con firma de ruta (image_path) y salida dict, demo CLI
  JSON-serializable.
- depth_to_relief_glb_py_datascience (v1.1.0): añade el parámetro opcional mask
  para recortar la malla de relieve al objeto (descarta las caras del fondo),
  cerrando la cadena con remove_background. Aditivo (mask=None = comportamiento
  previo), fiel al original de backend/depth.py.
- docs/capabilities/img-to-3d.md: incorpora remove_background como paso 0
  (pre-proceso), actualiza el flujo a 3 pasos encadenados, la tabla de funciones
  (4), el ejemplo end-to-end con mask y las deps (rembg/opencv).
- docs/capabilities/INDEX.md: conteo del grupo 3 -> 4.

Las dos funciones ya presentes (estimate_image_depth, depth_to_relief_glb) y el
pipeline build_relief_glb_from_image fueron promovidas en una ronda previa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:43:08 +02:00
parent cbefc82c02
commit 3cf8b21fea
6 changed files with 368 additions and 21 deletions
@@ -22,6 +22,7 @@ def depth_to_relief_glb(
out_glb_path: str,
z_scale: float = 0.35,
max_dim: int = 220,
mask: "np.ndarray | None" = None,
) -> dict:
"""
Construye una malla de relieve texturizada y la exporta como .glb.
@@ -33,6 +34,9 @@ def depth_to_relief_glb(
z_scale: amplitud del relieve (fracción del lado de la malla). Default 0.35.
max_dim: lado máximo del grid tras downsample (controla nº de vértices/caras).
Default 220 (~48k vértices, ~96k caras).
mask: máscara opcional HxW (0..255, 255 = objeto), típicamente la "mask" devuelta por
remove_background. Si se pasa, el fondo se aplana y las caras cuyos vértices caigan
en el fondo se descartan: la malla contiene solo el objeto, sin el plano de fondo.
Devuelve (dict, nunca lanza):
Éxito: {"status": "ok", "glb_path": out_glb_path, "vertices": int, "faces": int,
@@ -58,6 +62,14 @@ def depth_to_relief_glb(
depth = np.asarray(depth_img, dtype=np.float32) / 255.0
H, W = depth.shape
# Si se pasó máscara (objeto vs fondo), reescalarla al grid ya downsampleado: el fondo
# no aporta relieve (se aplana a 0) y luego sus caras se descartan, dejando solo el objeto.
fg = None
if mask is not None:
mask_img = Image.fromarray(np.asarray(mask).astype(np.uint8)).resize((W, H), Image.NEAREST)
fg = np.asarray(mask_img) >= 128
depth = np.where(fg, depth, 0.0).astype(np.float32)
# Coordenadas del grid: X corrige aspect ratio, Y hacia abajo, Z = profundidad.
aspect = W / float(H)
xs = np.linspace(-aspect / 2.0, aspect / 2.0, W, dtype=np.float32)
@@ -79,6 +91,12 @@ def depth_to_relief_glb(
]
)
# Con máscara: conservar solo las caras cuyos tres vértices son objeto. La malla queda
# recortada al objeto, sin el plano de fondo que deformaría el relieve.
if fg is not None:
keep = fg.ravel()[faces].all(axis=1)
faces = faces[keep]
# UV mapeando cada vértice al pixel de la imagen (V invertido para convención glTF).
u = np.linspace(0.0, 1.0, W, dtype=np.float32)
v = np.linspace(0.0, 1.0, H, dtype=np.float32)