eduardo4547 commited on
Commit
df2d84d
·
verified ·
1 Parent(s): 65879fd

Upload 4 files

Browse files
Files changed (3) hide show
  1. README.md +17 -71
  2. app.py +161 -59
  3. 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: 6.13.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- # Hyper Reality — SAM2 Segmentation GPU
14
-
15
-
16
- # Demo de Gradio con SAM
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
- @spaces.GPU
208
- @torch.no_grad()
209
- def autodetectar_entorno(imagen: Image.Image):
210
- global clip_model, clip_processor
211
- claves_entorno = list(CATALOGO_POR_ENTORNO.keys())
212
- exteriores = ["🏙️ Fachada / Exterior", "🌳 Terraza / Patio / Jardín"]
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
- min_score = umbral_sensibilidad
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
- min_score = umbral_sensibilidad
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