File size: 9,627 Bytes
a948cb1
444d63f
a948cb1
 
 
 
 
 
 
03fa9a6
 
a948cb1
ab7d075
 
 
444d63f
a948cb1
39ce9f1
 
 
 
ab7d075
39ce9f1
 
 
 
03fa9a6
 
 
 
 
 
175b670
 
 
03fa9a6
 
 
 
39ce9f1
03fa9a6
 
ab7d075
03fa9a6
 
 
ab7d075
a948cb1
 
39ce9f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a948cb1
03fa9a6
 
39ce9f1
 
 
 
 
a948cb1
03fa9a6
 
a948cb1
03fa9a6
 
 
a948cb1
39ce9f1
 
 
ab7d075
39ce9f1
 
 
a948cb1
 
03fa9a6
a948cb1
 
39ce9f1
 
 
a948cb1
 
 
 
 
 
 
39ce9f1
a948cb1
03fa9a6
 
 
 
39ce9f1
 
 
 
 
a948cb1
03fa9a6
 
a948cb1
03fa9a6
175b670
a948cb1
ab40b10
 
 
 
a948cb1
 
03fa9a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a948cb1
 
 
 
 
03fa9a6
a948cb1
 
03fa9a6
 
 
ab7d075
 
39ce9f1
03fa9a6
a948cb1
 
 
 
 
 
 
 
03fa9a6
ab7d075
 
 
03fa9a6
ab7d075
 
03fa9a6
ab7d075
03fa9a6
ab7d075
 
 
 
 
 
 
 
 
 
39ce9f1
ab7d075
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a948cb1
 
ab7d075
a948cb1
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import base64
import hashlib
import io
import json
import traceback
import gradio as gr
import numpy as np
import torch
import cv2
from PIL import Image
from transformers import pipeline as hf_pipeline

# CPU-only: use more threads for better throughput
torch.set_num_threads(4)

sam_vit_pipeline = None

# ── Parametros sincronizados entre UI y backend ───────────────────────────────
PARAMS = {
    "pred_iou_thresh":        0.95,
    "stability_score_thresh": 0.5,
    "points_per_batch":       16,
    "min_mask_region_area":   4500,
    "box_nms_thresh":         0.8,
}


# ── Renderizado ───────────────────────────────────────────────────────────────
def _render_masks(imagen_rgb: Image.Image, masks: list) -> Image.Image:
    img_arr = np.array(imagen_rgb).copy()
    overlay = img_arr.copy()
    for i, mask in enumerate(masks):
        h = hashlib.md5(str(i).encode()).hexdigest()[:6]
        color = (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
        overlay[np.array(mask) > 0] = color
    blended = cv2.addWeighted(img_arr, 0.5, overlay, 0.5, 0)
    return Image.fromarray(blended)


def _load_pipeline():
    global sam_vit_pipeline
    if sam_vit_pipeline is None:
        print("Cargando SAM ViT-Huge (CPU)...")
        sam_vit_pipeline = hf_pipeline(
            "mask-generation",
            model="facebook/sam-vit-huge",
            device=-1,
        )


# ── Segmentacion UI ───────────────────────────────────────────────────────────
def segmentar(
    imagen: Image.Image,
    pred_iou_thresh: float,
    stability_score_thresh: float,
    points_per_batch: int,
    min_mask_region_area: int,
    box_nms_thresh: float,
):
    global PARAMS
    if imagen is None:
        return None, "Sube una imagen para comenzar."

    PARAMS.update({
        "pred_iou_thresh":        float(pred_iou_thresh),
        "stability_score_thresh": float(stability_score_thresh),
        "points_per_batch":       int(points_per_batch),
        "min_mask_region_area":   int(min_mask_region_area),
        "box_nms_thresh":         float(box_nms_thresh),
    })

    _load_pipeline()
    imagen_rgb = imagen.convert("RGB")
    resultado = sam_vit_pipeline(
        imagen_rgb,
        points_per_batch=PARAMS["points_per_batch"],
        pred_iou_thresh=PARAMS["pred_iou_thresh"],
        stability_score_thresh=PARAMS["stability_score_thresh"],
        min_mask_region_area=PARAMS["min_mask_region_area"],
        box_nms_thresh=PARAMS["box_nms_thresh"],
    )
    if isinstance(resultado, list):
        resultado = resultado[0]

    masks = resultado.get("masks", [])
    if not masks:
        return imagen_rgb, "No se detectaron zonas."

    info = (
        f"UI: {len(masks)} zonas  |  "
        f"iou={PARAMS['pred_iou_thresh']}  stab={PARAMS['stability_score_thresh']}  "
        f"min_area={PARAMS['min_mask_region_area']}  "
        f"nms={PARAMS['box_nms_thresh']}  batch={PARAMS['points_per_batch']}"
    )
    return _render_masks(imagen_rgb, masks), info


# ── Endpoint para el backend Docker ──────────────────────────────────────────
def segment_for_backend(image_np: np.ndarray):
    """
    Llamado por el backend via gradio_client (api_name='/segment').
    Usa los mismos PARAMS que la UI β€” sincronizados al ultimo "Segmentar".
    Entrada : numpy uint8 H x W x 3.
    Salida  : (overlay_np, combined_json_str)
    """
    try:
        if image_np is None:
            empty = np.zeros((100, 100, 3), dtype=np.uint8)
            return empty, json.dumps({"masks": [], "label_map_b64": ""})

        _load_pipeline()
        pil_image = Image.fromarray(image_np.astype(np.uint8)).convert("RGB")
        h, w = image_np.shape[:2]

        resultado = sam_vit_pipeline(
            pil_image,
            points_per_batch=PARAMS["points_per_batch"],
            pred_iou_thresh=PARAMS["pred_iou_thresh"],
            stability_score_thresh=PARAMS["stability_score_thresh"],
            min_mask_region_area=PARAMS["min_mask_region_area"],
            box_nms_thresh=PARAMS["box_nms_thresh"],
        )
        if isinstance(resultado, list):
            resultado = resultado[0]

        all_masks_raw = resultado.get("masks", [])
        masks_bool = [np.array(m).astype(bool) for m in all_masks_raw]

        # Ordenar de mayor a menor area: grandes primero, pequenas al final para
        # que ventanas y detalles sobreescriban al muro en el label_map.
        masks_bool = sorted(masks_bool, key=lambda m: m.sum(), reverse=True)

        label_map = np.zeros((h, w), dtype=np.uint8)
        masks_out = []
        for i, mask in enumerate(masks_bool[:254], start=1):
            label_map[mask] = i
            area_ratio = float(mask.sum()) / max(1, h * w)
            ys, xs = np.where(mask)
            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]
            )
            masks_out.append({
                "index": i,
                "surface": f"Zona {i}",
                "area_ratio": round(area_ratio, 4),
                "bbox_xywh": bbox,
            })

        pil_label = Image.fromarray(label_map, mode="L")
        buf = io.BytesIO()
        pil_label.save(buf, format="PNG")
        label_map_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")

        overlay_pil = _render_masks(pil_image, masks_bool)
        overlay_np = np.array(overlay_pil.convert("RGB"))

        combined = {
            "masks": masks_out,
            "label_map_b64": label_map_b64,
            "entorno": "cpu",
            "motor": "SAM Auto (CPU)",
            "params_used": dict(PARAMS),
        }
        return overlay_np, json.dumps(combined, ensure_ascii=False)

    except Exception:
        err = traceback.format_exc()
        empty = np.zeros((100, 100, 3), dtype=np.uint8)
        return empty, json.dumps({"error": err, "masks": [], "label_map_b64": ""})


# ── UI ────────────────────────────────────────────────────────────────────────
def crear_app():
    with gr.Blocks(title="SAM Auto - CPU") as demo:
        gr.Markdown("# Segmentacion Automatica - SAM ViT-Huge (CPU)")
        gr.Markdown(
            "SAM detecta todos los elementos de la imagen de forma automatica, "
            "sin necesidad de seleccionar zonas ni escribir prompts."
        )

        with gr.Row():
            imagen_entrada = gr.Image(type="pil", label="Foto del Espacio")
            imagen_salida  = gr.Image(label="Resultado")

        estado = gr.Markdown()
        boton  = gr.Button("Segmentar", variant="primary")

        with gr.Accordion("Parametros de segmentacion (sincronizados con el backend)", open=True):
            gr.Markdown(
                "> Los parametros que configures aqui se aplican tanto a la UI como al backend Docker. "
                "Haz clic en **Segmentar** para que el backend adopte los nuevos valores."
            )
            with gr.Row():
                sl_pred_iou = gr.Slider(
                    minimum=0.0, maximum=1.0, step=0.01, value=PARAMS["pred_iou_thresh"],
                    label="pred_iou_thresh  (↑ menos mascaras, mas limpias | HF default: 0.88)"
                )
                sl_stability = gr.Slider(
                    minimum=0.0, maximum=1.0, step=0.01, value=PARAMS["stability_score_thresh"],
                    label="stability_score_thresh  (↑ descarta zonas inestables | HF default: 0.95)"
                )
            with gr.Row():
                sl_batch = gr.Slider(
                    minimum=8, maximum=64, step=8, value=PARAMS["points_per_batch"],
                    label="points_per_batch  (en CPU mantener bajo, max recomendado: 16)"
                )
                sl_min_area = gr.Slider(
                    minimum=0, maximum=5000, step=100, value=PARAMS["min_mask_region_area"],
                    label="min_mask_region_area px  (↑ filtra zonas pequenas)"
                )
            with gr.Row():
                sl_nms = gr.Slider(
                    minimum=0.0, maximum=1.0, step=0.05, value=PARAMS["box_nms_thresh"],
                    label="box_nms_thresh  (↑ permite mas solapamiento entre mascaras)"
                )

        all_inputs = [imagen_entrada, sl_pred_iou, sl_stability, sl_batch, sl_min_area, sl_nms]
        boton.click(fn=segmentar, inputs=all_inputs, outputs=[imagen_salida, estado])
        imagen_entrada.upload(fn=segmentar, inputs=all_inputs, outputs=[imagen_salida, estado])

        # Endpoint oculto para el backend Docker
        _api_in   = gr.Image(type="numpy", label="backend_input",  visible=False)
        _api_over = gr.Image(type="numpy", label="backend_overlay", visible=False)
        _api_json = gr.Textbox(label="backend_json",                visible=False)
        _api_btn  = gr.Button(visible=False)
        _api_btn.click(
            fn=segment_for_backend,
            inputs=[_api_in],
            outputs=[_api_over, _api_json],
            api_name="segment",
        )

    return demo


demo = crear_app()
if __name__ == "__main__":
    demo.launch()