Spaces:
Running on Zero
Running on Zero
Upload 4 files
Browse files- README.md +17 -71
- app.py +161 -59
- packages.txt +2 -0
README.md
CHANGED
|
@@ -1,71 +1,17 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Hyper Reality SAM2 GPU
|
| 3 |
-
emoji: 🏠
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
license: mit
|
| 11 |
-
---
|
| 12 |
-
|
| 13 |
-
# Hyper Reality — SAM2 Segmentation GPU
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
Este proyecto es una app de Gradio que usa SAM para segmentar automáticamente una imagen subida.
|
| 19 |
-
|
| 20 |
-
## Qué hace
|
| 21 |
-
|
| 22 |
-
- Permite subir una imagen
|
| 23 |
-
- Ejecuta la segmentación automática con SAM
|
| 24 |
-
- Permite buscar uno o varios objetos por palabra clave (separados por comas) y solo segmentar las máscaras encontradas
|
| 25 |
-
- Muestra la imagen con las máscaras superpuestas
|
| 26 |
-
|
| 27 |
-
## Ejecutar localmente
|
| 28 |
-
|
| 29 |
-
1. Crear un entorno virtual:
|
| 30 |
-
|
| 31 |
-
```powershell
|
| 32 |
-
python -m venv .venv
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
2. Activar el entorno:
|
| 36 |
-
|
| 37 |
-
```powershell
|
| 38 |
-
.venv\Scripts\activate
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
3. Instalar dependencias:
|
| 42 |
-
|
| 43 |
-
```powershell
|
| 44 |
-
pip install -r requirements.txt
|
| 45 |
-
```
|
| 46 |
-
|
| 47 |
-
Si ya habías instalado antes y recibiste el error de `torchvision`, ejecuta:
|
| 48 |
-
|
| 49 |
-
```powershell
|
| 50 |
-
pip install torchvision
|
| 51 |
-
```
|
| 52 |
-
|
| 53 |
-
4. Ejecutar la app:
|
| 54 |
-
|
| 55 |
-
```powershell
|
| 56 |
-
python app.py
|
| 57 |
-
```
|
| 58 |
-
|
| 59 |
-
5. Abrir el enlace local que muestra Gradio, por ejemplo `http://127.0.0.1:7860`.
|
| 60 |
-
|
| 61 |
-
## Notas
|
| 62 |
-
|
| 63 |
-
- La primera vez que corras la app, descargará el checkpoint del modelo SAM desde Hugging Face.
|
| 64 |
-
- Si quieres usar otro modelo de SAM, cambia `MODEL_REPO` y `CHECKPOINT_FILENAME` en `app.py`.
|
| 65 |
-
|
| 66 |
-
## Subir a Hugging Face Spaces
|
| 67 |
-
|
| 68 |
-
1. Crea una nueva Space en Hugging Face.
|
| 69 |
-
2. Selecciona el tipo `Gradio`.
|
| 70 |
-
3. Sube este repositorio completo o copia `app.py` y `requirements.txt`.
|
| 71 |
-
4. La Space descargará el checkpoint y ejecutará la app.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Hyper Reality SAM2 GPU
|
| 3 |
+
emoji: 🏠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.29.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Hyper Reality — SAM2 Segmentation GPU
|
| 14 |
+
|
| 15 |
+
Segmentación automática de habitaciones con SAM 2.1 usando ZeroGPU.
|
| 16 |
+
|
| 17 |
+
Este Space actúa como motor de IA para el [visualizador principal](https://huggingface.co/spaces/eduardo4547/hyper-reality-visualizer).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
import numpy as np
|
| 4 |
import torch
|
|
@@ -204,60 +208,17 @@ def limpiar_mascara(mask: np.ndarray, area_minima: int = 2000) -> np.ndarray:
|
|
| 204 |
|
| 205 |
return mask_final.astype(bool)
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
if imagen is None:
|
| 215 |
-
entorno_predicho = claves_entorno[0]
|
| 216 |
-
nuevas_opciones = list(CATALOGO_POR_ENTORNO[entorno_predicho].keys())
|
| 217 |
-
motor_seleccionado = "Híbrido Arquitectura (Cityscapes Grande + DINO Pequeño)" if entorno_predicho in exteriores else "SegFormer (SegFormer ADE20K+ DINO) + SAM 2.1"
|
| 218 |
-
return (
|
| 219 |
-
gr.update(value=entorno_predicho),
|
| 220 |
-
gr.update(choices=nuevas_opciones, value=nuevas_opciones),
|
| 221 |
-
gr.update(value=motor_seleccionado)
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
if clip_model is None:
|
| 225 |
-
clip_processor = CLIPProcessor.from_pretrained(CLIP_ID)
|
| 226 |
-
clip_model = CLIPModel.from_pretrained(CLIP_ID).to(DEVICE)
|
| 227 |
-
|
| 228 |
-
imagen = imagen.convert("RGB")
|
| 229 |
-
inputs = clip_processor(text=DESCRIPCIONES_CLIP, images=imagen, return_tensors="pt", padding=True).to(DEVICE)
|
| 230 |
-
outputs = clip_model(**inputs)
|
| 231 |
-
probabilidades = outputs.logits_per_image.softmax(dim=1).cpu().numpy()[0]
|
| 232 |
-
indice_ganador = probabilidades.argmax()
|
| 233 |
-
|
| 234 |
-
entorno_detectado = claves_entorno[indice_ganador]
|
| 235 |
-
nuevas_opciones = list(CATALOGO_POR_ENTORNO[entorno_detectado].keys())
|
| 236 |
-
motor_seleccionado = "Híbrido Arquitectura (Cityscapes Grande + DINO Pequeño)" if entorno_detectado in exteriores else "SegFormer (SegFormer ADE20K+ DINO) + SAM 2.1"
|
| 237 |
-
|
| 238 |
-
return (
|
| 239 |
-
gr.update(value=entorno_detectado),
|
| 240 |
-
gr.update(choices=nuevas_opciones, value=nuevas_opciones),
|
| 241 |
-
gr.update(value=motor_seleccionado)
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
@spaces.GPU
|
| 245 |
-
@torch.no_grad()
|
| 246 |
-
def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umbral_sensibilidad: float, motor: str, usar_limpieza: bool):
|
| 247 |
-
print(f"\n--- Iniciando análisis con motor: {motor} ---")
|
| 248 |
global sam2_predictor, gdino_model, gdino_processor, segformer_city_model, segformer_city_processor, segformer_ade_model, segformer_ade_processor
|
| 249 |
|
| 250 |
-
if imagen is None or len(seleccion) == 0:
|
| 251 |
-
return None, "Sube una imagen y selecciona al menos un elemento.", None
|
| 252 |
-
|
| 253 |
terminos_crudos = [CATALOGO_POR_ENTORNO[entorno][item] for item in seleccion]
|
| 254 |
texto_para_ia = " ".join(terminos_crudos)
|
| 255 |
-
|
| 256 |
-
print(f"Palabras clave/términos crudos para DINO: {terminos_crudos}")
|
| 257 |
-
|
| 258 |
-
imagen_rgb = imagen.convert("RGB")
|
| 259 |
-
imagen_np = np.array(imagen_rgb)
|
| 260 |
-
total_pixels = imagen.width * imagen.height
|
| 261 |
masks_finales = []
|
| 262 |
etiquetas_finales = []
|
| 263 |
debug_image = None
|
|
@@ -285,7 +246,7 @@ def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umb
|
|
| 285 |
if score > umbral_sensibilidad:
|
| 286 |
boxes_filt.append(box)
|
| 287 |
labels_filt.append(label)
|
| 288 |
-
|
| 289 |
if boxes_filt:
|
| 290 |
sam2_predictor.set_image(imagen_np)
|
| 291 |
masks, _, _ = sam2_predictor.predict(box=torch.stack(boxes_filt).cpu().numpy(), multimask_output=False)
|
|
@@ -338,8 +299,7 @@ def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umb
|
|
| 338 |
results = gdino_processor.post_process_grounded_object_detection(outputs_dino, inputs_dino.input_ids, target_sizes=[imagen_rgb.size[::-1]])[0]
|
| 339 |
|
| 340 |
for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
|
| 341 |
-
|
| 342 |
-
if score > min_score:
|
| 343 |
etiquetas_todos.append(f"{label} (Detalle DINO)")
|
| 344 |
cajas_todos.append(box.cpu().numpy())
|
| 345 |
|
|
@@ -395,8 +355,7 @@ def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umb
|
|
| 395 |
results = gdino_processor.post_process_grounded_object_detection(outputs_dino, inputs_dino.input_ids, target_sizes=[imagen_rgb.size[::-1]])[0]
|
| 396 |
|
| 397 |
for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
|
| 398 |
-
|
| 399 |
-
if score > min_score:
|
| 400 |
etiquetas_todos.append(f"{label} (Detalle DINO)")
|
| 401 |
cajas_todos.append(box.cpu().numpy())
|
| 402 |
|
|
@@ -413,17 +372,73 @@ def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umb
|
|
| 413 |
if usar_limpieza:
|
| 414 |
masks_limpias = []
|
| 415 |
etiquetas_limpias = []
|
| 416 |
-
UMBRAL_AREA_MINIMA = 1500
|
| 417 |
-
|
| 418 |
for mask, etiqueta in zip(masks_finales, etiquetas_finales):
|
| 419 |
mask_sin_ruido = limpiar_mascara(mask, area_minima=UMBRAL_AREA_MINIMA)
|
| 420 |
-
if np.sum(mask_sin_ruido) > 2000:
|
| 421 |
masks_limpias.append(mask_sin_ruido)
|
| 422 |
etiquetas_limpias.append(etiqueta)
|
| 423 |
-
|
| 424 |
masks_finales = masks_limpias
|
| 425 |
etiquetas_finales = etiquetas_limpias
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
# --- RESULTADOS Y REPORTE ---
|
| 428 |
if not masks_finales:
|
| 429 |
return imagen_rgb, f"No se encontró nada válido o las detecciones tenían demasiado ruido con {motor}.", debug_image
|
|
@@ -449,6 +464,80 @@ def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umb
|
|
| 449 |
print("--- Análisis completado ---")
|
| 450 |
return resultado_img, f"📊 REPORTE ({motor}):<br>" + "<br>".join(reporte_lineas), debug_image
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
def seleccionar_motor_por_entorno(entorno):
|
| 453 |
exteriores = ["🏙️ Fachada / Exterior", "🌳 Terraza / Patio / Jardín"]
|
| 454 |
interiores = [
|
|
@@ -509,6 +598,19 @@ def crear_app():
|
|
| 509 |
motor.change(fn=actualizar_opciones, inputs=[tipo_entorno, motor], outputs=elementos)
|
| 510 |
boton.click(fn=segmentar_y_analizar, inputs=[imagen_entrada, tipo_entorno, elementos, umbral, motor, usar_limpieza], outputs=[imagen_salida, estado, debug_dino_image])
|
| 511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
return demo
|
| 513 |
|
| 514 |
download_sam_checkpoint()
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import io
|
| 3 |
+
import json
|
| 4 |
import os
|
| 5 |
+
import traceback
|
| 6 |
import gradio as gr
|
| 7 |
import numpy as np
|
| 8 |
import torch
|
|
|
|
| 208 |
|
| 209 |
return mask_final.astype(bool)
|
| 210 |
|
| 211 |
+
def _run_engines_raw(imagen_rgb, imagen_np, entorno, seleccion, umbral_sensibilidad, motor, usar_limpieza):
|
| 212 |
+
"""
|
| 213 |
+
Núcleo de los 3 motores, sin decoradores GPU.
|
| 214 |
+
Debe llamarse siempre desde una función con @spaces.GPU.
|
| 215 |
+
Retorna (masks_finales, etiquetas_finales, debug_image).
|
| 216 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
global sam2_predictor, gdino_model, gdino_processor, segformer_city_model, segformer_city_processor, segformer_ade_model, segformer_ade_processor
|
| 218 |
|
|
|
|
|
|
|
|
|
|
| 219 |
terminos_crudos = [CATALOGO_POR_ENTORNO[entorno][item] for item in seleccion]
|
| 220 |
texto_para_ia = " ".join(terminos_crudos)
|
| 221 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
masks_finales = []
|
| 223 |
etiquetas_finales = []
|
| 224 |
debug_image = None
|
|
|
|
| 246 |
if score > umbral_sensibilidad:
|
| 247 |
boxes_filt.append(box)
|
| 248 |
labels_filt.append(label)
|
| 249 |
+
|
| 250 |
if boxes_filt:
|
| 251 |
sam2_predictor.set_image(imagen_np)
|
| 252 |
masks, _, _ = sam2_predictor.predict(box=torch.stack(boxes_filt).cpu().numpy(), multimask_output=False)
|
|
|
|
| 299 |
results = gdino_processor.post_process_grounded_object_detection(outputs_dino, inputs_dino.input_ids, target_sizes=[imagen_rgb.size[::-1]])[0]
|
| 300 |
|
| 301 |
for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
|
| 302 |
+
if score > umbral_sensibilidad:
|
|
|
|
| 303 |
etiquetas_todos.append(f"{label} (Detalle DINO)")
|
| 304 |
cajas_todos.append(box.cpu().numpy())
|
| 305 |
|
|
|
|
| 355 |
results = gdino_processor.post_process_grounded_object_detection(outputs_dino, inputs_dino.input_ids, target_sizes=[imagen_rgb.size[::-1]])[0]
|
| 356 |
|
| 357 |
for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
|
| 358 |
+
if score > umbral_sensibilidad:
|
|
|
|
| 359 |
etiquetas_todos.append(f"{label} (Detalle DINO)")
|
| 360 |
cajas_todos.append(box.cpu().numpy())
|
| 361 |
|
|
|
|
| 372 |
if usar_limpieza:
|
| 373 |
masks_limpias = []
|
| 374 |
etiquetas_limpias = []
|
| 375 |
+
UMBRAL_AREA_MINIMA = 1500
|
| 376 |
+
|
| 377 |
for mask, etiqueta in zip(masks_finales, etiquetas_finales):
|
| 378 |
mask_sin_ruido = limpiar_mascara(mask, area_minima=UMBRAL_AREA_MINIMA)
|
| 379 |
+
if np.sum(mask_sin_ruido) > 2000:
|
| 380 |
masks_limpias.append(mask_sin_ruido)
|
| 381 |
etiquetas_limpias.append(etiqueta)
|
| 382 |
+
|
| 383 |
masks_finales = masks_limpias
|
| 384 |
etiquetas_finales = etiquetas_limpias
|
| 385 |
|
| 386 |
+
return masks_finales, etiquetas_finales, debug_image
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
@spaces.GPU
|
| 390 |
+
@torch.no_grad()
|
| 391 |
+
def autodetectar_entorno(imagen: Image.Image):
|
| 392 |
+
global clip_model, clip_processor
|
| 393 |
+
claves_entorno = list(CATALOGO_POR_ENTORNO.keys())
|
| 394 |
+
exteriores = ["🏙️ Fachada / Exterior", "🌳 Terraza / Patio / Jardín"]
|
| 395 |
+
|
| 396 |
+
if imagen is None:
|
| 397 |
+
entorno_predicho = claves_entorno[0]
|
| 398 |
+
nuevas_opciones = list(CATALOGO_POR_ENTORNO[entorno_predicho].keys())
|
| 399 |
+
motor_seleccionado = "Híbrido Arquitectura (Cityscapes Grande + DINO Pequeño)" if entorno_predicho in exteriores else "SegFormer (SegFormer ADE20K+ DINO) + SAM 2.1"
|
| 400 |
+
return (
|
| 401 |
+
gr.update(value=entorno_predicho),
|
| 402 |
+
gr.update(choices=nuevas_opciones, value=nuevas_opciones),
|
| 403 |
+
gr.update(value=motor_seleccionado)
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
if clip_model is None:
|
| 407 |
+
clip_processor = CLIPProcessor.from_pretrained(CLIP_ID)
|
| 408 |
+
clip_model = CLIPModel.from_pretrained(CLIP_ID).to(DEVICE)
|
| 409 |
+
|
| 410 |
+
imagen = imagen.convert("RGB")
|
| 411 |
+
inputs = clip_processor(text=DESCRIPCIONES_CLIP, images=imagen, return_tensors="pt", padding=True).to(DEVICE)
|
| 412 |
+
outputs = clip_model(**inputs)
|
| 413 |
+
probabilidades = outputs.logits_per_image.softmax(dim=1).cpu().numpy()[0]
|
| 414 |
+
indice_ganador = probabilidades.argmax()
|
| 415 |
+
|
| 416 |
+
entorno_detectado = claves_entorno[indice_ganador]
|
| 417 |
+
nuevas_opciones = list(CATALOGO_POR_ENTORNO[entorno_detectado].keys())
|
| 418 |
+
motor_seleccionado = "Híbrido Arquitectura (Cityscapes Grande + DINO Pequeño)" if entorno_detectado in exteriores else "SegFormer (SegFormer ADE20K+ DINO) + SAM 2.1"
|
| 419 |
+
|
| 420 |
+
return (
|
| 421 |
+
gr.update(value=entorno_detectado),
|
| 422 |
+
gr.update(choices=nuevas_opciones, value=nuevas_opciones),
|
| 423 |
+
gr.update(value=motor_seleccionado)
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
@spaces.GPU
|
| 427 |
+
@torch.no_grad()
|
| 428 |
+
def segmentar_y_analizar(imagen: Image.Image, entorno: str, seleccion: list, umbral_sensibilidad: float, motor: str, usar_limpieza: bool):
|
| 429 |
+
print(f"\n--- Iniciando análisis con motor: {motor} ---")
|
| 430 |
+
|
| 431 |
+
if imagen is None or len(seleccion) == 0:
|
| 432 |
+
return None, "Sube una imagen y selecciona al menos un elemento.", None
|
| 433 |
+
|
| 434 |
+
imagen_rgb = imagen.convert("RGB")
|
| 435 |
+
imagen_np = np.array(imagen_rgb)
|
| 436 |
+
total_pixels = imagen.width * imagen.height
|
| 437 |
+
|
| 438 |
+
masks_finales, etiquetas_finales, debug_image = _run_engines_raw(
|
| 439 |
+
imagen_rgb, imagen_np, entorno, seleccion, umbral_sensibilidad, motor, usar_limpieza
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
# --- RESULTADOS Y REPORTE ---
|
| 443 |
if not masks_finales:
|
| 444 |
return imagen_rgb, f"No se encontró nada válido o las detecciones tenían demasiado ruido con {motor}.", debug_image
|
|
|
|
| 464 |
print("--- Análisis completado ---")
|
| 465 |
return resultado_img, f"📊 REPORTE ({motor}):<br>" + "<br>".join(reporte_lineas), debug_image
|
| 466 |
|
| 467 |
+
@spaces.GPU
|
| 468 |
+
@torch.no_grad()
|
| 469 |
+
def segment_for_backend(image_np: np.ndarray):
|
| 470 |
+
"""
|
| 471 |
+
Endpoint para el backend Docker (llamado via gradio_client).
|
| 472 |
+
Entrada : imagen numpy uint8 H×W×3.
|
| 473 |
+
Salida : (overlay_np, combined_json_str)
|
| 474 |
+
combined_json tiene "masks" (lista de dicts) y "label_map_b64" (PNG base64).
|
| 475 |
+
"""
|
| 476 |
+
try:
|
| 477 |
+
if image_np is None:
|
| 478 |
+
empty = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 479 |
+
return empty, json.dumps({"masks": [], "label_map_b64": ""})
|
| 480 |
+
|
| 481 |
+
pil_image = Image.fromarray(image_np.astype(np.uint8)).convert("RGB")
|
| 482 |
+
img_np = np.array(pil_image)
|
| 483 |
+
h, w = img_np.shape[:2]
|
| 484 |
+
|
| 485 |
+
# Auto-detectar entorno con CLIP
|
| 486 |
+
global clip_model, clip_processor
|
| 487 |
+
claves_entorno = list(CATALOGO_POR_ENTORNO.keys())
|
| 488 |
+
exteriores = ["🏙️ Fachada / Exterior", "🌳 Terraza / Patio / Jardín"]
|
| 489 |
+
|
| 490 |
+
if clip_model is None:
|
| 491 |
+
clip_processor = CLIPProcessor.from_pretrained(CLIP_ID)
|
| 492 |
+
clip_model = CLIPModel.from_pretrained(CLIP_ID).to(DEVICE)
|
| 493 |
+
|
| 494 |
+
inputs = clip_processor(text=DESCRIPCIONES_CLIP, images=pil_image, return_tensors="pt", padding=True).to(DEVICE)
|
| 495 |
+
outputs = clip_model(**inputs)
|
| 496 |
+
probabilidades = outputs.logits_per_image.softmax(dim=1).cpu().numpy()[0]
|
| 497 |
+
entorno = claves_entorno[int(probabilidades.argmax())]
|
| 498 |
+
motor = "Híbrido Arquitectura (Cityscapes Grande + DINO Pequeño)" if entorno in exteriores else "SegFormer (SegFormer ADE20K+ DINO) + SAM 2.1"
|
| 499 |
+
seleccion = list(CATALOGO_POR_ENTORNO[entorno].keys())
|
| 500 |
+
|
| 501 |
+
# Ejecutar motores
|
| 502 |
+
masks_finales, etiquetas_finales, _ = _run_engines_raw(
|
| 503 |
+
pil_image, img_np, entorno, seleccion, 0.25, motor, True
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
if not masks_finales:
|
| 507 |
+
return np.array(pil_image), json.dumps({"masks": [], "label_map_b64": "", "entorno": entorno, "motor": motor})
|
| 508 |
+
|
| 509 |
+
# Construir label_map (uint8, valores 1..N por segmento)
|
| 510 |
+
label_map = np.zeros((h, w), dtype=np.uint8)
|
| 511 |
+
masks_out = []
|
| 512 |
+
for i, (mask, etiqueta) in enumerate(zip(masks_finales[:254], etiquetas_finales[:254]), start=1):
|
| 513 |
+
m = mask.astype(bool)
|
| 514 |
+
label_map[m] = i
|
| 515 |
+
area_ratio = float(np.sum(m)) / max(1, h * w)
|
| 516 |
+
ys, xs = np.where(m)
|
| 517 |
+
bbox = [int(xs.min()), int(ys.min()), int(xs.max() - xs.min()), int(ys.max() - ys.min())] if len(ys) else [0, 0, 0, 0]
|
| 518 |
+
masks_out.append({"index": i, "surface": etiqueta, "area_ratio": round(area_ratio, 4), "bbox_xywh": bbox})
|
| 519 |
+
|
| 520 |
+
# Codificar label_map como PNG base64 (sin pérdida, preserva valores uint8)
|
| 521 |
+
pil_label = Image.fromarray(label_map, mode="L")
|
| 522 |
+
buf = io.BytesIO()
|
| 523 |
+
pil_label.save(buf, format="PNG")
|
| 524 |
+
label_map_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 525 |
+
|
| 526 |
+
# Overlay coloreado
|
| 527 |
+
categorias_unicas = sorted(set(etiquetas_finales))
|
| 528 |
+
mapa_colores = {cat: EXTENDED_PALETTE[i % len(EXTENDED_PALETTE)] for i, cat in enumerate(categorias_unicas)}
|
| 529 |
+
overlay_pil = create_instance_overlay(pil_image, masks_finales, etiquetas_finales, mapa_colores)
|
| 530 |
+
overlay_np = np.array(overlay_pil.convert("RGB"))
|
| 531 |
+
|
| 532 |
+
combined = {"masks": masks_out, "label_map_b64": label_map_b64, "entorno": entorno, "motor": motor}
|
| 533 |
+
return overlay_np, json.dumps(combined, ensure_ascii=False)
|
| 534 |
+
|
| 535 |
+
except Exception:
|
| 536 |
+
err = traceback.format_exc()
|
| 537 |
+
empty = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 538 |
+
return empty, json.dumps({"error": err, "masks": [], "label_map_b64": ""})
|
| 539 |
+
|
| 540 |
+
|
| 541 |
def seleccionar_motor_por_entorno(entorno):
|
| 542 |
exteriores = ["🏙️ Fachada / Exterior", "🌳 Terraza / Patio / Jardín"]
|
| 543 |
interiores = [
|
|
|
|
| 598 |
motor.change(fn=actualizar_opciones, inputs=[tipo_entorno, motor], outputs=elementos)
|
| 599 |
boton.click(fn=segmentar_y_analizar, inputs=[imagen_entrada, tipo_entorno, elementos, umbral, motor, usar_limpieza], outputs=[imagen_salida, estado, debug_dino_image])
|
| 600 |
|
| 601 |
+
# ── Endpoint oculto para el backend Docker (gradio_client lo llama) ──────
|
| 602 |
+
with gr.Row(visible=False):
|
| 603 |
+
_api_in = gr.Image(type="numpy", label="backend_input")
|
| 604 |
+
_api_over = gr.Image(type="numpy", label="backend_overlay")
|
| 605 |
+
_api_json = gr.Textbox(label="backend_json")
|
| 606 |
+
|
| 607 |
+
gr.Button(visible=False).click(
|
| 608 |
+
fn=segment_for_backend,
|
| 609 |
+
inputs=[_api_in],
|
| 610 |
+
outputs=[_api_over, _api_json],
|
| 611 |
+
api_name="segment",
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
return demo
|
| 615 |
|
| 616 |
download_sam_checkpoint()
|
packages.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
libgl1-mesa-glx
|
| 2 |
+
libglib2.0-0
|