eduardo4547 commited on
Commit
26f3a79
·
verified ·
1 Parent(s): 1013236

Upload 4 files

Browse files
Files changed (4) hide show
  1. .gitignore +4 -0
  2. README.md +71 -0
  3. app.py +191 -0
  4. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ .venv/
3
+ *.pyc
4
+ .DS_Store
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import gradio as gr
6
+ import numpy as np
7
+ import torch
8
+ from huggingface_hub import hf_hub_download
9
+ from PIL import Image
10
+ from segment_anything import SamAutomaticMaskGenerator, sam_model_registry
11
+ from transformers import CLIPModel, CLIPProcessor
12
+
13
+ MODEL_REPO = "segments-arnaud/sam_vit_h"
14
+ CHECKPOINT_FILENAME = "sam_vit_h_4b8939.pth"
15
+ CLIP_MODEL_NAME = "openai/clip-vit-base-patch32"
16
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
+
18
+
19
+ def download_checkpoint() -> str:
20
+ cache_dir = Path("./models")
21
+ cache_dir.mkdir(parents=True, exist_ok=True)
22
+ local_path = cache_dir / CHECKPOINT_FILENAME
23
+ if not local_path.exists():
24
+ local_path = Path(
25
+ hf_hub_download(
26
+ repo_id=MODEL_REPO,
27
+ filename=CHECKPOINT_FILENAME,
28
+ cache_dir=str(cache_dir),
29
+ )
30
+ )
31
+ return str(local_path)
32
+
33
+
34
+ def create_mask_overlay(image: Image.Image, masks: list[dict]) -> Image.Image:
35
+ image = image.convert("RGBA")
36
+ all_mask = np.zeros((image.height, image.width), dtype=np.uint8)
37
+
38
+ for mask in masks:
39
+ all_mask |= mask["segmentation"].astype(np.uint8)
40
+
41
+ mask_image = Image.fromarray(all_mask * 255, mode="L")
42
+ color_overlay = Image.new("RGBA", image.size, (255, 0, 0, 120))
43
+ overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
44
+ overlay.paste(color_overlay, mask=mask_image)
45
+ return Image.alpha_composite(image, overlay)
46
+
47
+
48
+ def mask_to_bbox(mask: np.ndarray):
49
+ ys, xs = np.where(mask.astype(np.uint8))
50
+ if ys.size == 0 or xs.size == 0:
51
+ return None
52
+ return int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1
53
+
54
+
55
+ def crop_masked_region(image: Image.Image, mask: np.ndarray) -> Image.Image | None:
56
+ bbox = mask_to_bbox(mask)
57
+ if bbox is None:
58
+ return None
59
+
60
+ mask_img = Image.fromarray((mask.astype(np.uint8) * 255).astype(np.uint8), mode="L")
61
+ background = Image.new("RGB", image.size, (127, 127, 127))
62
+ masked = Image.composite(image, background, mask_img)
63
+ return masked.crop(bbox)
64
+
65
+
66
+ def normalize_features(features: torch.Tensor | object) -> torch.Tensor:
67
+ if hasattr(features, "pooler_output"):
68
+ features = features.pooler_output
69
+ elif hasattr(features, "last_hidden_state"):
70
+ features = features.last_hidden_state[:, 0, :]
71
+
72
+ if not isinstance(features, torch.Tensor):
73
+ raise RuntimeError("No se pudieron obtener características de CLIP.")
74
+
75
+ return features / features.norm(dim=-1, keepdim=True)
76
+
77
+
78
+ def compute_clip_features(images: list[Image.Image]):
79
+ inputs = clip_processor(images=images, return_tensors="pt", padding=True).to(DEVICE)
80
+ with torch.no_grad():
81
+ features = clip_model.get_image_features(**inputs)
82
+ return normalize_features(features)
83
+
84
+
85
+ def select_masks_by_text(image: Image.Image, masks: list[dict], prompt: str) -> tuple[list[dict], list[tuple[str, float | None]]]:
86
+ terms = [t.strip() for t in re.split(r"[,\n]+", prompt) if t.strip()]
87
+ if len(terms) == 0:
88
+ return [], []
89
+
90
+ crops = []
91
+ valid_masks = []
92
+ for mask in masks:
93
+ crop = crop_masked_region(image, mask["segmentation"])
94
+ if crop is not None:
95
+ valid_masks.append(mask)
96
+ crops.append(crop)
97
+
98
+ if len(crops) == 0:
99
+ return [], [(term, None) for term in terms]
100
+
101
+ image_features = compute_clip_features(crops)
102
+ text_inputs = clip_processor(text=terms, return_tensors="pt", padding=True).to(DEVICE)
103
+ with torch.no_grad():
104
+ text_features = clip_model.get_text_features(**text_inputs)
105
+ text_features = normalize_features(text_features)
106
+
107
+ similarities = (image_features @ text_features.T).cpu()
108
+ selected = []
109
+ hits = []
110
+ threshold = 0.15
111
+
112
+ for term_idx, term in enumerate(terms):
113
+ scores = similarities[:, term_idx]
114
+ best_idx = int(torch.argmax(scores).item())
115
+ best_score = float(scores[best_idx].item())
116
+ if best_score >= threshold:
117
+ mask = valid_masks[best_idx]
118
+ if mask not in selected:
119
+ selected.append(mask)
120
+ hits.append((term, best_score))
121
+ else:
122
+ hits.append((term, None))
123
+
124
+ return selected, hits
125
+
126
+
127
+ @torch.no_grad()
128
+ def segmentar_imagen(imagen: Image.Image, texto: str):
129
+ if imagen is None:
130
+ return None, "Subí una imagen para segmentar."
131
+
132
+ imagen = imagen.convert("RGB")
133
+ imagen_np = np.array(imagen)
134
+ masks = mask_generator.generate(imagen_np)
135
+
136
+ if len(masks) == 0:
137
+ return None, "No se generaron máscaras para esta imagen."
138
+
139
+ texto = texto.strip()
140
+ if texto == "":
141
+ overlay = create_mask_overlay(imagen, masks)
142
+ return overlay, f"Generadas {len(masks)} máscaras con SAM."
143
+
144
+ selected_masks, hits = select_masks_by_text(imagen, masks, texto)
145
+ if len(selected_masks) == 0:
146
+ terms = [t.strip() for t in re.split(r"[,\n]+", texto) if t.strip()]
147
+ return None, f"No se encontró un objeto que coincida con: {', '.join(terms)}."
148
+
149
+ found_terms = [term for term, score in hits if score is not None]
150
+ missing_terms = [term for term, score in hits if score is None]
151
+ overlay = create_mask_overlay(imagen, selected_masks)
152
+
153
+ message = f"Encontradas {len(selected_masks)} máscara(s) para: {', '.join(found_terms)}."
154
+ if missing_terms:
155
+ message += f" No se encontró: {', '.join(missing_terms)}."
156
+ return overlay, message
157
+
158
+
159
+ def crear_app():
160
+ with gr.Blocks(title="Gradio + SAM 2.1 Demo") as demo:
161
+ gr.Markdown("# 🎯 Segmentación automática con SAM")
162
+ gr.Markdown(
163
+ "Subí una imagen y escribe una palabra para encontrar y segmentar el objeto deseado. Si dejas el texto vacío, se mostrarán todas las máscaras generadas."
164
+ )
165
+
166
+ with gr.Row():
167
+ with gr.Column(scale=1):
168
+ imagen_entrada = gr.Image(type="pil", label="Subí tu imagen")
169
+ texto_objeto = gr.Textbox(label="Buscar objeto", placeholder="Ej. perro, coche, persona")
170
+ boton = gr.Button("Segmentar")
171
+ with gr.Column(scale=1):
172
+ imagen_salida = gr.Image(label="Resultado segmentado")
173
+ estado = gr.Textbox(label="Estado", interactive=False)
174
+
175
+ boton.click(
176
+ fn=segmentar_imagen,
177
+ inputs=[imagen_entrada, texto_objeto],
178
+ outputs=[imagen_salida, estado],
179
+ )
180
+
181
+ return demo
182
+
183
+
184
+ if __name__ == "__main__":
185
+ checkpoint_path = download_checkpoint()
186
+ sam = sam_model_registry["vit_h"](checkpoint=checkpoint_path)
187
+ mask_generator = SamAutomaticMaskGenerator(sam)
188
+ clip_model = CLIPModel.from_pretrained(CLIP_MODEL_NAME).to(DEVICE)
189
+ clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_NAME)
190
+ demo = crear_app()
191
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False, ssr=False)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio==6.13.0
2
+ segment-anything
3
+ torch>=2.0.0
4
+ torchvision
5
+ transformers
6
+ huggingface-hub
7
+ numpy
8
+ pillow