Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -119,24 +119,20 @@ def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
|
|
| 119 |
return (max(0, v1-25), min(max_v1, v1+25), max(0, v2-25), min(255, v2+25), max(0, v3-25), min(255, v3+25))
|
| 120 |
|
| 121 |
def disegna_etichetta_pianta(img, mask, quad_vertices, input_area_rif):
|
| 122 |
-
"""Funzione universale per disegnare contorni, freccia ed etichetta testuale."""
|
| 123 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 124 |
if not contours: return img
|
| 125 |
|
| 126 |
-
# Bordo Magenta
|
| 127 |
cv2.drawContours(img, contours, -1, (255, 0, 255), 2)
|
| 128 |
|
| 129 |
c = max(contours, key=cv2.contourArea)
|
| 130 |
topmost = tuple(c[c[:, :, 1].argmin()][0])
|
| 131 |
|
| 132 |
-
# Calcola coordinate coda della freccia (in alto a sinistra rispetto al top)
|
| 133 |
tail = (max(20, topmost[0] - 120), max(40, topmost[1] - 80))
|
| 134 |
cv2.arrowedLine(img, tail, topmost, (0, 255, 255), 3, tipLength=0.2)
|
| 135 |
|
| 136 |
px_area = int(np.sum(mask == 255))
|
| 137 |
text = f"Pianta: {px_area} px"
|
| 138 |
|
| 139 |
-
# Se abbiamo il quadrilatero e l'area di riferimento, calcola in cm2
|
| 140 |
if quad_vertices is not None and input_area_rif and input_area_rif > 0:
|
| 141 |
valid_area = np.zeros(img.shape[:2], dtype=np.uint8)
|
| 142 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
|
@@ -147,7 +143,6 @@ def disegna_etichetta_pianta(img, mask, quad_vertices, input_area_rif):
|
|
| 147 |
|
| 148 |
text_pos = (max(10, tail[0] - 50), max(20, tail[1] - 15))
|
| 149 |
|
| 150 |
-
# Testo con Outline nero per massima leggibilità
|
| 151 |
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 0, 0), 6)
|
| 152 |
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 255), 3)
|
| 153 |
|
|
@@ -158,7 +153,7 @@ def disegna_etichetta_pianta(img, mask, quad_vertices, input_area_rif):
|
|
| 158 |
# ==========================================
|
| 159 |
def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
|
| 160 |
|
| 161 |
-
def
|
| 162 |
if image is None: return None, None, "0", "In attesa...", None, gr.update(visible=False)
|
| 163 |
|
| 164 |
spazio = get_spazio_attivo(s_main, s_alt)
|
|
@@ -261,7 +256,6 @@ def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
|
|
| 261 |
|
| 262 |
mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
|
| 263 |
|
| 264 |
-
# Se abbiamo isolato correttamente un oggetto e abbiamo dati, mettiamo l'etichetta grossa
|
| 265 |
segmented = cv2.bitwise_and(image, image, mask=mask_pulita)
|
| 266 |
if np.sum(mask_pulita == 255) > 500:
|
| 267 |
segmented = disegna_etichetta_pianta(segmented, mask_pulita, quad_state, input_area_rif)
|
|
@@ -296,33 +290,40 @@ def segmenta_ai_manuale(image, evt: gr.SelectData, quad_vertices, input_area_rif
|
|
| 296 |
|
| 297 |
def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
|
| 298 |
if image is None or quad_vertices is None:
|
| 299 |
-
return image, "Errore: Calibra
|
| 300 |
|
| 301 |
h, w = image.shape[:2]
|
| 302 |
valid_area = np.zeros((h, w), dtype=np.uint8)
|
| 303 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
|
|
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
|
|
|
|
| 308 |
instances = []
|
| 309 |
model = carica_fastsam()
|
| 310 |
|
| 311 |
for step in range(5):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
if step == 0:
|
| 313 |
-
# Step 1: Click esatto nel baricentro del nastro
|
| 314 |
M = cv2.moments(valid_area)
|
| 315 |
cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
|
| 316 |
else:
|
| 317 |
-
# Step 2-5: Distance Transform per trovare il "Polo dell'inaccessibilità"
|
| 318 |
-
allowed_area = cv2.bitwise_not(exclude_mask)
|
| 319 |
dist = cv2.distanceTransform(allowed_area, cv2.DIST_L2, 5)
|
| 320 |
_, max_val, _, max_loc = cv2.minMaxLoc(dist)
|
| 321 |
|
| 322 |
-
|
|
|
|
|
|
|
| 323 |
cx, cy = max_loc
|
| 324 |
|
| 325 |
-
# Inferenza
|
| 326 |
results = model.predict(image, points=[[cx, cy]], labels=[1], device="cpu", verbose=False)
|
| 327 |
|
| 328 |
if len(results) > 0 and results[0].masks is not None:
|
|
@@ -331,23 +332,21 @@ def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
|
|
| 331 |
mask = (mask * 255).astype(np.uint8)
|
| 332 |
mask = cv2.bitwise_and(mask, valid_area)
|
| 333 |
|
| 334 |
-
# Controllo anti-loop infinito
|
| 335 |
new_pixels = cv2.bitwise_and(mask, cv2.bitwise_not(exclude_mask))
|
|
|
|
|
|
|
| 336 |
if np.sum(new_pixels == 255) < 100:
|
| 337 |
-
|
| 338 |
-
cv2.circle(exclude_mask, (cx, cy), 20, 255, -1)
|
| 339 |
continue
|
| 340 |
|
| 341 |
instances.append(mask)
|
| 342 |
-
exclude_mask = cv2.bitwise_or(exclude_mask, mask)
|
| 343 |
else:
|
| 344 |
-
# Se SAM fallisce su quel punto, non cliccarlo più
|
| 345 |
cv2.circle(exclude_mask, (cx, cy), 20, 255, -1)
|
| 346 |
|
| 347 |
if not instances:
|
| 348 |
return cv2.bitwise_and(image, image, mask=valid_area), "Nessuna segmentazione AI riuscita."
|
| 349 |
|
| 350 |
-
# Seleziona la vera pianta usando l'ExG medio
|
| 351 |
img_float = image.astype(np.float32)
|
| 352 |
exg_img = 2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2]
|
| 353 |
exg_img = np.clip(exg_img, 0, 255).astype(np.uint8)
|
|
@@ -358,13 +357,12 @@ def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
|
|
| 358 |
if mean_exg > max_exg:
|
| 359 |
max_exg, plant_idx = mean_exg, i
|
| 360 |
|
| 361 |
-
# Produci l'output ESCLUSIVO per la pianta vincitrice
|
| 362 |
best_mask = instances[plant_idx]
|
| 363 |
final_output = cv2.bitwise_and(image, image, mask=best_mask)
|
| 364 |
final_output = disegna_etichetta_pianta(final_output, best_mask, quad_vertices, input_area_rif)
|
| 365 |
|
| 366 |
final_px = int(np.sum(best_mask == 255))
|
| 367 |
-
return final_output, f"Esplorazione AI completata. Pianta isolata ({final_px} px)."
|
| 368 |
|
| 369 |
# ==========================================
|
| 370 |
# 5. INTERFACCIA UTENTE (UI)
|
|
@@ -386,7 +384,7 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 386 |
with gr.Tabs():
|
| 387 |
# --- TAB 1: GEOMETRIA ---
|
| 388 |
with gr.Tab("📐 Fase 1: Calibrazione Riferimento"):
|
| 389 |
-
gr.Markdown("**Isola
|
| 390 |
with gr.Group():
|
| 391 |
with gr.Row():
|
| 392 |
sp_main_r = gr.Radio(["HSV", "ExG"], value="HSV", label="Spazio Colore Consigliato")
|
|
@@ -424,7 +422,7 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 424 |
|
| 425 |
for s in sliders_r + [sp_main_r, sp_altri_r]:
|
| 426 |
s.change(
|
| 427 |
-
fn=
|
| 428 |
inputs=[img_rif_in, sp_main_r, sp_altri_r] + sliders_r,
|
| 429 |
outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state, box_area_rif]
|
| 430 |
)
|
|
@@ -463,7 +461,6 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 463 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 464 |
img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 465 |
|
| 466 |
-
# Aggiunti quad_state e input_area_rif per generare l'etichetta in cm2
|
| 467 |
for s in sliders_l + [sp_main_l, sp_altri_l, morfo_tipo, morfo_int]:
|
| 468 |
s.change(fn=elabora_pianta,
|
| 469 |
inputs=[img_lat_in, sp_main_l, sp_altri_l] + sliders_l + [morfo_tipo, morfo_int, quad_state, input_area_rif],
|
|
@@ -473,7 +470,7 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 473 |
with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
|
| 474 |
gr.Markdown("**Clicca sulla pianta** per estrarla, oppure usa l'Auto-Segmentatore basato sulla cornice elaborata in Fase 1.")
|
| 475 |
|
| 476 |
-
btn_auto_sam = gr.Button("🤖 Segmenta Automaticamente l'interno
|
| 477 |
|
| 478 |
with gr.Row():
|
| 479 |
img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=True, height=350)
|
|
|
|
| 119 |
return (max(0, v1-25), min(max_v1, v1+25), max(0, v2-25), min(255, v2+25), max(0, v3-25), min(255, v3+25))
|
| 120 |
|
| 121 |
def disegna_etichetta_pianta(img, mask, quad_vertices, input_area_rif):
|
|
|
|
| 122 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 123 |
if not contours: return img
|
| 124 |
|
|
|
|
| 125 |
cv2.drawContours(img, contours, -1, (255, 0, 255), 2)
|
| 126 |
|
| 127 |
c = max(contours, key=cv2.contourArea)
|
| 128 |
topmost = tuple(c[c[:, :, 1].argmin()][0])
|
| 129 |
|
|
|
|
| 130 |
tail = (max(20, topmost[0] - 120), max(40, topmost[1] - 80))
|
| 131 |
cv2.arrowedLine(img, tail, topmost, (0, 255, 255), 3, tipLength=0.2)
|
| 132 |
|
| 133 |
px_area = int(np.sum(mask == 255))
|
| 134 |
text = f"Pianta: {px_area} px"
|
| 135 |
|
|
|
|
| 136 |
if quad_vertices is not None and input_area_rif and input_area_rif > 0:
|
| 137 |
valid_area = np.zeros(img.shape[:2], dtype=np.uint8)
|
| 138 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
|
|
|
| 143 |
|
| 144 |
text_pos = (max(10, tail[0] - 50), max(20, tail[1] - 15))
|
| 145 |
|
|
|
|
| 146 |
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 0, 0), 6)
|
| 147 |
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 255), 3)
|
| 148 |
|
|
|
|
| 153 |
# ==========================================
|
| 154 |
def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
|
| 155 |
|
| 156 |
+
def elabora_riferimento(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
|
| 157 |
if image is None: return None, None, "0", "In attesa...", None, gr.update(visible=False)
|
| 158 |
|
| 159 |
spazio = get_spazio_attivo(s_main, s_alt)
|
|
|
|
| 256 |
|
| 257 |
mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
|
| 258 |
|
|
|
|
| 259 |
segmented = cv2.bitwise_and(image, image, mask=mask_pulita)
|
| 260 |
if np.sum(mask_pulita == 255) > 500:
|
| 261 |
segmented = disegna_etichetta_pianta(segmented, mask_pulita, quad_state, input_area_rif)
|
|
|
|
| 290 |
|
| 291 |
def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
|
| 292 |
if image is None or quad_vertices is None:
|
| 293 |
+
return image, "Errore: Calibra l'area di riferimento in Fase 1."
|
| 294 |
|
| 295 |
h, w = image.shape[:2]
|
| 296 |
valid_area = np.zeros((h, w), dtype=np.uint8)
|
| 297 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
| 298 |
+
quad_area_px = np.sum(valid_area == 255)
|
| 299 |
|
| 300 |
+
if quad_area_px == 0:
|
| 301 |
+
return image, "Errore: Area di riferimento nulla."
|
| 302 |
|
| 303 |
+
exclude_mask = cv2.bitwise_not(valid_area)
|
| 304 |
instances = []
|
| 305 |
model = carica_fastsam()
|
| 306 |
|
| 307 |
for step in range(5):
|
| 308 |
+
allowed_area = cv2.bitwise_not(exclude_mask)
|
| 309 |
+
remaining_px = np.sum(allowed_area == 255)
|
| 310 |
+
|
| 311 |
+
# SCUDO 1: Fallback se l'area rimanente è < 10% del totale (Richiesta Utente)
|
| 312 |
+
if (remaining_px / quad_area_px) < 0.10:
|
| 313 |
+
break
|
| 314 |
+
|
| 315 |
if step == 0:
|
|
|
|
| 316 |
M = cv2.moments(valid_area)
|
| 317 |
cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
|
| 318 |
else:
|
|
|
|
|
|
|
| 319 |
dist = cv2.distanceTransform(allowed_area, cv2.DIST_L2, 5)
|
| 320 |
_, max_val, _, max_loc = cv2.minMaxLoc(dist)
|
| 321 |
|
| 322 |
+
# SCUDO 2: Fallback Topografico (spazi troppo stretti)
|
| 323 |
+
if max_val < 5:
|
| 324 |
+
break
|
| 325 |
cx, cy = max_loc
|
| 326 |
|
|
|
|
| 327 |
results = model.predict(image, points=[[cx, cy]], labels=[1], device="cpu", verbose=False)
|
| 328 |
|
| 329 |
if len(results) > 0 and results[0].masks is not None:
|
|
|
|
| 332 |
mask = (mask * 255).astype(np.uint8)
|
| 333 |
mask = cv2.bitwise_and(mask, valid_area)
|
| 334 |
|
|
|
|
| 335 |
new_pixels = cv2.bitwise_and(mask, cv2.bitwise_not(exclude_mask))
|
| 336 |
+
|
| 337 |
+
# SCUDO 3: Anti-Loop Infinito. Disegna zona morta se SAM ripete l'output.
|
| 338 |
if np.sum(new_pixels == 255) < 100:
|
| 339 |
+
cv2.circle(exclude_mask, (cx, cy), max(15, int(max_val if step > 0 else 20)), 255, -1)
|
|
|
|
| 340 |
continue
|
| 341 |
|
| 342 |
instances.append(mask)
|
| 343 |
+
exclude_mask = cv2.bitwise_or(exclude_mask, mask)
|
| 344 |
else:
|
|
|
|
| 345 |
cv2.circle(exclude_mask, (cx, cy), 20, 255, -1)
|
| 346 |
|
| 347 |
if not instances:
|
| 348 |
return cv2.bitwise_and(image, image, mask=valid_area), "Nessuna segmentazione AI riuscita."
|
| 349 |
|
|
|
|
| 350 |
img_float = image.astype(np.float32)
|
| 351 |
exg_img = 2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2]
|
| 352 |
exg_img = np.clip(exg_img, 0, 255).astype(np.uint8)
|
|
|
|
| 357 |
if mean_exg > max_exg:
|
| 358 |
max_exg, plant_idx = mean_exg, i
|
| 359 |
|
|
|
|
| 360 |
best_mask = instances[plant_idx]
|
| 361 |
final_output = cv2.bitwise_and(image, image, mask=best_mask)
|
| 362 |
final_output = disegna_etichetta_pianta(final_output, best_mask, quad_vertices, input_area_rif)
|
| 363 |
|
| 364 |
final_px = int(np.sum(best_mask == 255))
|
| 365 |
+
return final_output, f"Esplorazione AI completata in {step+1} steps. Pianta isolata ({final_px} px)."
|
| 366 |
|
| 367 |
# ==========================================
|
| 368 |
# 5. INTERFACCIA UTENTE (UI)
|
|
|
|
| 384 |
with gr.Tabs():
|
| 385 |
# --- TAB 1: GEOMETRIA ---
|
| 386 |
with gr.Tab("📐 Fase 1: Calibrazione Riferimento"):
|
| 387 |
+
gr.Markdown("**Isola l'area di riferimento**. L'algoritmo calcolerà il quadrilatero interno per contenere la pianta.")
|
| 388 |
with gr.Group():
|
| 389 |
with gr.Row():
|
| 390 |
sp_main_r = gr.Radio(["HSV", "ExG"], value="HSV", label="Spazio Colore Consigliato")
|
|
|
|
| 422 |
|
| 423 |
for s in sliders_r + [sp_main_r, sp_altri_r]:
|
| 424 |
s.change(
|
| 425 |
+
fn=elabora_riferimento,
|
| 426 |
inputs=[img_rif_in, sp_main_r, sp_altri_r] + sliders_r,
|
| 427 |
outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state, box_area_rif]
|
| 428 |
)
|
|
|
|
| 461 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 462 |
img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 463 |
|
|
|
|
| 464 |
for s in sliders_l + [sp_main_l, sp_altri_l, morfo_tipo, morfo_int]:
|
| 465 |
s.change(fn=elabora_pianta,
|
| 466 |
inputs=[img_lat_in, sp_main_l, sp_altri_l] + sliders_l + [morfo_tipo, morfo_int, quad_state, input_area_rif],
|
|
|
|
| 470 |
with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
|
| 471 |
gr.Markdown("**Clicca sulla pianta** per estrarla, oppure usa l'Auto-Segmentatore basato sulla cornice elaborata in Fase 1.")
|
| 472 |
|
| 473 |
+
btn_auto_sam = gr.Button("🤖 Segmenta Automaticamente l'interno dell'Area", variant="primary", visible=False)
|
| 474 |
|
| 475 |
with gr.Row():
|
| 476 |
img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=True, height=350)
|