Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -70,27 +70,26 @@ def carica_da_dataset(condizione: str, campione: str, dat: str):
|
|
| 70 |
return blank, blank, blank, f"❌ Errore: File mancante."
|
| 71 |
|
| 72 |
def get_spazio_attivo(main_space, alt_space):
|
| 73 |
-
"""Sceglie lo spazio colore attivo gestendo la priorità della tendina avanzata."""
|
| 74 |
return alt_space if alt_space != "Nessuno" else main_space
|
| 75 |
|
| 76 |
def aggiorna_sliders(spazio_principale, spazio_altri):
|
| 77 |
-
"""Resetta etichette, valori ottimali e visibilità in base allo spazio colore selezionato."""
|
| 78 |
spazio = get_spazio_attivo(spazio_principale, spazio_altri)
|
| 79 |
|
|
|
|
| 80 |
if spazio == "HSV":
|
| 81 |
-
return (gr.update(label="Tinta (H) Min", value=20, visible=True), gr.update(label="Tinta (H) Max", value=80, visible=True),
|
| 82 |
gr.update(label="Saturazione (S) Min", value=40, visible=True), gr.update(label="Saturazione (S) Max", value=255, visible=True),
|
| 83 |
gr.update(label="Valore (V) Min", value=40, visible=True), gr.update(label="Valore (V) Max", value=255, visible=True))
|
| 84 |
elif spazio == "ExG":
|
| 85 |
-
return (gr.update(label="Soglia Minima ExG", value=40, visible=True), gr.update(visible=False),
|
| 86 |
gr.update(visible=False), gr.update(visible=False),
|
| 87 |
gr.update(visible=False), gr.update(visible=False))
|
| 88 |
elif spazio == "LAB":
|
| 89 |
-
return (gr.update(label="Luminanza (L) Min", value=0, visible=True), gr.update(label="Luminanza (L) Max", value=255, visible=True),
|
| 90 |
-
gr.update(label="Asse A
|
| 91 |
-
gr.update(label="Asse B
|
| 92 |
else: # RGB
|
| 93 |
-
return (gr.update(label="Rosso (R) Min", value=0, visible=True), gr.update(label="Rosso (R) Max", value=100, visible=True),
|
| 94 |
gr.update(label="Verde (G) Min", value=100, visible=True), gr.update(label="Verde (G) Max", value=255, visible=True),
|
| 95 |
gr.update(label="Blu (B) Min", value=0, visible=True), gr.update(label="Blu (B) Max", value=100, visible=True))
|
| 96 |
|
|
@@ -111,14 +110,15 @@ def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
|
|
| 111 |
if spazio == "ExG":
|
| 112 |
r, g, b = float(pixel_rgb[0]), float(pixel_rgb[1]), float(pixel_rgb[2])
|
| 113 |
exg = int(np.clip(2 * g - r - b, 0, 255))
|
| 114 |
-
|
| 115 |
-
return (max(0, exg-delta), 255, 0, 255, 0, 255) # Modifica solo la soglia min
|
| 116 |
|
| 117 |
pixel_img = np.uint8([[pixel_rgb]])
|
| 118 |
pixel_conv = converti_spazio_colore(pixel_img, spazio)[0][0]
|
| 119 |
v1, v2, v3 = [int(v) for v in pixel_conv]
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
|
| 123 |
# ==========================================
|
| 124 |
# 2. MOTORE GEOMETRICO (FASE 1)
|
|
@@ -126,7 +126,8 @@ def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
|
|
| 126 |
def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
|
| 127 |
|
| 128 |
def elabora_nastro(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
|
| 129 |
-
|
|
|
|
| 130 |
|
| 131 |
spazio = get_spazio_attivo(s_main, s_alt)
|
| 132 |
min1, max1 = ordina_min_max(c1_min, c1_max)
|
|
@@ -154,7 +155,6 @@ def elabora_nastro(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
|
|
| 154 |
log_msg = "Linee trovate. "
|
| 155 |
quad_state_val = None
|
| 156 |
mostra_area = gr.update(visible=False)
|
| 157 |
-
mostra_btn_auto = gr.update(visible=False)
|
| 158 |
|
| 159 |
if lines is not None:
|
| 160 |
orizzontali, verticali = [], []
|
|
@@ -191,12 +191,11 @@ def elabora_nastro(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
|
|
| 191 |
area_pixel = int(abs(cv2.contourArea(quad_state_val)))
|
| 192 |
log_msg += "Poligono chiuso."
|
| 193 |
mostra_area = gr.update(visible=True)
|
| 194 |
-
mostra_btn_auto = gr.update(visible=True)
|
| 195 |
else: log_msg += "Poligono aperto."
|
| 196 |
else: log_msg += "Lati mancanti."
|
| 197 |
else: log_msg += "Nessuna linea rilevata."
|
| 198 |
|
| 199 |
-
return mask_visiva, debug_img, str(area_pixel), log_msg, quad_state_val, mostra_area
|
| 200 |
|
| 201 |
# ==========================================
|
| 202 |
# 3. MOTORE SEGMENTAZIONE PIANTA (FASE 2)
|
|
@@ -214,7 +213,6 @@ def applica_morfologia(mask, tipo, intensita):
|
|
| 214 |
|
| 215 |
def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max, morfo_tipo, morfo_int):
|
| 216 |
if image is None: return None, "0"
|
| 217 |
-
|
| 218 |
spazio = get_spazio_attivo(s_main, s_alt)
|
| 219 |
min1, max1 = ordina_min_max(c1_min, c1_max)
|
| 220 |
|
|
@@ -222,7 +220,7 @@ def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
|
|
| 222 |
img_float = image.astype(np.float32)
|
| 223 |
r, g, b = img_float[:,:,0], img_float[:,:,1], img_float[:,:,2]
|
| 224 |
exg = np.clip(2 * g - r - b, 0, 255).astype(np.uint8)
|
| 225 |
-
mask = cv2.inRange(exg, min1, 255)
|
| 226 |
else:
|
| 227 |
min2, max2 = ordina_min_max(c2_min, c2_max)
|
| 228 |
min3, max3 = ordina_min_max(c3_min, c3_max)
|
|
@@ -235,7 +233,7 @@ def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
|
|
| 235 |
return segmented, f"{pixel_count} px"
|
| 236 |
|
| 237 |
# ==========================================
|
| 238 |
-
# 4. MOTORE AI FASTSAM (FASE 3
|
| 239 |
# ==========================================
|
| 240 |
@lru_cache(maxsize=1)
|
| 241 |
def carica_fastsam():
|
|
@@ -259,59 +257,98 @@ def segmenta_ai_manuale(image, evt: gr.SelectData):
|
|
| 259 |
return segmented, f"{pixel_count} px"
|
| 260 |
return image, "Nessun oggetto trovato."
|
| 261 |
|
| 262 |
-
def segmenta_ai_automatico(image, quad_vertices):
|
| 263 |
-
|
| 264 |
-
if
|
|
|
|
| 265 |
|
| 266 |
h, w = image.shape[:2]
|
| 267 |
-
|
|
|
|
| 268 |
valid_area = np.zeros((h, w), dtype=np.uint8)
|
| 269 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
segmented_mask_total = np.zeros((h, w), dtype=np.uint8)
|
|
|
|
| 272 |
model = carica_fastsam()
|
| 273 |
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
for _ in range(max_iter):
|
| 278 |
-
# 2. Trova l'area valida ANCORA NON segmentata
|
| 279 |
remaining = cv2.bitwise_and(valid_area, cv2.bitwise_not(segmented_mask_total))
|
| 280 |
-
|
| 281 |
-
# 3. Trova il "pezzo" più grande rimasto
|
| 282 |
contours, _ = cv2.findContours(remaining, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 283 |
if not contours: break
|
| 284 |
|
| 285 |
largest_contour = max(contours, key=cv2.contourArea)
|
| 286 |
-
if cv2.contourArea(largest_contour) <
|
| 287 |
-
|
| 288 |
-
#
|
| 289 |
M = cv2.moments(largest_contour)
|
| 290 |
-
if M["m00"] != 0
|
| 291 |
-
|
| 292 |
-
cy = int(M["m01"] / M["m00"])
|
| 293 |
-
else:
|
| 294 |
-
cx, cy = largest_contour[0][0][0], largest_contour[0][0][1]
|
| 295 |
|
| 296 |
-
# 5. Spara il punto a FastSAM
|
| 297 |
results = model.predict(image, points=[[cx, cy]], labels=[1], device="cpu", verbose=False)
|
| 298 |
|
| 299 |
if len(results) > 0 and results[0].masks is not None:
|
| 300 |
mask = results[0].masks.data[0].cpu().numpy()
|
| 301 |
mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 302 |
mask = (mask * 255).astype(np.uint8)
|
|
|
|
| 303 |
|
| 304 |
-
#
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
| 306 |
segmented_mask_total = cv2.bitwise_or(segmented_mask_total, mask)
|
| 307 |
-
iterazioni += 1
|
| 308 |
else: break
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# ==========================================
|
| 317 |
# 5. INTERFACCIA UTENTE (UI)
|
|
@@ -319,7 +356,6 @@ def segmenta_ai_automatico(image, quad_vertices):
|
|
| 319 |
with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
| 320 |
gr.Markdown("# 🥬 Computer Vision in Agricoltura: segmentazione")
|
| 321 |
|
| 322 |
-
# Variabile invisibile per passare i dati del nastro dalla Fase 1 alla Fase 3
|
| 323 |
quad_state = gr.State(None)
|
| 324 |
|
| 325 |
with gr.Group():
|
|
@@ -343,8 +379,8 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 343 |
|
| 344 |
with gr.Row():
|
| 345 |
with gr.Column():
|
| 346 |
-
r1_min = gr.Slider(0,
|
| 347 |
-
r1_max = gr.Slider(0,
|
| 348 |
with gr.Column():
|
| 349 |
r2_min = gr.Slider(0, 255, 40, label="Saturazione (S) Min")
|
| 350 |
r2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max")
|
|
@@ -361,22 +397,21 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 361 |
pixel_rif_out = gr.Textbox(label="🔥 AREA CORNICE (Pixel²)")
|
| 362 |
log_geom = gr.Textbox(label="Log di Sistema")
|
| 363 |
|
| 364 |
-
# Area opzionale (visibile solo a quadrilatero chiuso)
|
| 365 |
with gr.Group(visible=False) as box_area_rif:
|
| 366 |
input_area_rif = gr.Number(label="Inserisci Area Reale Riferimento (opzionale)")
|
| 367 |
|
| 368 |
sliders_r = [r1_min, r1_max, r2_min, r2_max, r3_min, r3_max]
|
| 369 |
|
| 370 |
-
# Gestione Eventi Tab 1
|
| 371 |
for sp in [sp_main_r, sp_altri_r]:
|
| 372 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_r, sp_altri_r], outputs=sliders_r)
|
| 373 |
img_rif_in.select(fn=cattura_colore, inputs=[img_rif_in, sp_main_r, sp_altri_r], outputs=sliders_r)
|
| 374 |
|
| 375 |
for s in sliders_r + [sp_main_r, sp_altri_r]:
|
|
|
|
| 376 |
s.change(
|
| 377 |
fn=elabora_nastro,
|
| 378 |
inputs=[img_rif_in, sp_main_r, sp_altri_r] + sliders_r,
|
| 379 |
-
outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state, box_area_rif
|
| 380 |
)
|
| 381 |
|
| 382 |
# --- TAB 2: COLORE PIANTA ---
|
|
@@ -409,7 +444,6 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 409 |
|
| 410 |
sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
|
| 411 |
|
| 412 |
-
# Gestione Eventi Tab 2
|
| 413 |
for sp in [sp_main_l, sp_altri_l]:
|
| 414 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 415 |
img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, sp_main_l, sp_altri_l], outputs=sliders_l)
|
|
@@ -428,21 +462,16 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
|
| 428 |
img_ai_out = gr.Image(label="Risultato Rete Neurale", height=350)
|
| 429 |
pixel_ai_out = gr.Textbox(label="Pixel Pianta Rilevati (Area)")
|
| 430 |
|
| 431 |
-
# Evento Manuale
|
| 432 |
img_ai_in.select(fn=segmenta_ai_manuale, inputs=[img_ai_in], outputs=[img_ai_out, pixel_ai_out])
|
| 433 |
|
| 434 |
-
#
|
| 435 |
-
btn_auto_sam.click(fn=segmenta_ai_automatico, inputs=[img_ai_in, quad_state], outputs=[img_ai_out, pixel_ai_out])
|
| 436 |
|
| 437 |
-
|
| 438 |
-
img_rif_in.change(lambda: gr.update(visible=False), None, btn_auto_sam) # Nasconde se cambi foto
|
| 439 |
|
| 440 |
-
|
| 441 |
-
def aggiorna_btn_auto(quad):
|
| 442 |
-
return gr.update(visible=(quad is not None))
|
| 443 |
quad_state.change(fn=aggiorna_btn_auto, inputs=quad_state, outputs=btn_auto_sam)
|
| 444 |
|
| 445 |
-
# Caricamento Globale (Spara la foto a tutte le tab)
|
| 446 |
btn_carica.click(
|
| 447 |
fn=carica_da_dataset,
|
| 448 |
inputs=[ui_cond, ui_camp, ui_dat],
|
|
|
|
| 70 |
return blank, blank, blank, f"❌ Errore: File mancante."
|
| 71 |
|
| 72 |
def get_spazio_attivo(main_space, alt_space):
|
|
|
|
| 73 |
return alt_space if alt_space != "Nessuno" else main_space
|
| 74 |
|
| 75 |
def aggiorna_sliders(spazio_principale, spazio_altri):
|
|
|
|
| 76 |
spazio = get_spazio_attivo(spazio_principale, spazio_altri)
|
| 77 |
|
| 78 |
+
# FIX: Gestione corretta HUE (0-179) e visibilità
|
| 79 |
if spazio == "HSV":
|
| 80 |
+
return (gr.update(label="Tinta (H) Min", value=20, maximum=179, visible=True), gr.update(label="Tinta (H) Max", value=80, maximum=179, visible=True),
|
| 81 |
gr.update(label="Saturazione (S) Min", value=40, visible=True), gr.update(label="Saturazione (S) Max", value=255, visible=True),
|
| 82 |
gr.update(label="Valore (V) Min", value=40, visible=True), gr.update(label="Valore (V) Max", value=255, visible=True))
|
| 83 |
elif spazio == "ExG":
|
| 84 |
+
return (gr.update(label="Soglia Minima ExG", value=40, maximum=255, visible=True), gr.update(visible=False),
|
| 85 |
gr.update(visible=False), gr.update(visible=False),
|
| 86 |
gr.update(visible=False), gr.update(visible=False))
|
| 87 |
elif spazio == "LAB":
|
| 88 |
+
return (gr.update(label="Luminanza (L) Min", value=0, maximum=255, visible=True), gr.update(label="Luminanza (L) Max", value=255, maximum=255, visible=True),
|
| 89 |
+
gr.update(label="Asse A Min", value=0, visible=True), gr.update(label="Asse A Max", value=110, visible=True),
|
| 90 |
+
gr.update(label="Asse B Min", value=130, visible=True), gr.update(label="Asse B Max", value=255, visible=True))
|
| 91 |
else: # RGB
|
| 92 |
+
return (gr.update(label="Rosso (R) Min", value=0, maximum=255, visible=True), gr.update(label="Rosso (R) Max", value=100, maximum=255, visible=True),
|
| 93 |
gr.update(label="Verde (G) Min", value=100, visible=True), gr.update(label="Verde (G) Max", value=255, visible=True),
|
| 94 |
gr.update(label="Blu (B) Min", value=0, visible=True), gr.update(label="Blu (B) Max", value=100, visible=True))
|
| 95 |
|
|
|
|
| 110 |
if spazio == "ExG":
|
| 111 |
r, g, b = float(pixel_rgb[0]), float(pixel_rgb[1]), float(pixel_rgb[2])
|
| 112 |
exg = int(np.clip(2 * g - r - b, 0, 255))
|
| 113 |
+
return (max(0, exg-20), 255, 0, 255, 0, 255)
|
|
|
|
| 114 |
|
| 115 |
pixel_img = np.uint8([[pixel_rgb]])
|
| 116 |
pixel_conv = converti_spazio_colore(pixel_img, spazio)[0][0]
|
| 117 |
v1, v2, v3 = [int(v) for v in pixel_conv]
|
| 118 |
+
|
| 119 |
+
# FIX: Limite HUE nel contagocce
|
| 120 |
+
max_v1 = 179 if spazio == "HSV" else 255
|
| 121 |
+
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))
|
| 122 |
|
| 123 |
# ==========================================
|
| 124 |
# 2. MOTORE GEOMETRICO (FASE 1)
|
|
|
|
| 126 |
def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
|
| 127 |
|
| 128 |
def elabora_nastro(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
|
| 129 |
+
# FIX: Tolto l'output fantasma che causava il Ghost Button
|
| 130 |
+
if image is None: return None, None, "0", "In attesa...", None, gr.update(visible=False)
|
| 131 |
|
| 132 |
spazio = get_spazio_attivo(s_main, s_alt)
|
| 133 |
min1, max1 = ordina_min_max(c1_min, c1_max)
|
|
|
|
| 155 |
log_msg = "Linee trovate. "
|
| 156 |
quad_state_val = None
|
| 157 |
mostra_area = gr.update(visible=False)
|
|
|
|
| 158 |
|
| 159 |
if lines is not None:
|
| 160 |
orizzontali, verticali = [], []
|
|
|
|
| 191 |
area_pixel = int(abs(cv2.contourArea(quad_state_val)))
|
| 192 |
log_msg += "Poligono chiuso."
|
| 193 |
mostra_area = gr.update(visible=True)
|
|
|
|
| 194 |
else: log_msg += "Poligono aperto."
|
| 195 |
else: log_msg += "Lati mancanti."
|
| 196 |
else: log_msg += "Nessuna linea rilevata."
|
| 197 |
|
| 198 |
+
return mask_visiva, debug_img, str(area_pixel), log_msg, quad_state_val, mostra_area
|
| 199 |
|
| 200 |
# ==========================================
|
| 201 |
# 3. MOTORE SEGMENTAZIONE PIANTA (FASE 2)
|
|
|
|
| 213 |
|
| 214 |
def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max, morfo_tipo, morfo_int):
|
| 215 |
if image is None: return None, "0"
|
|
|
|
| 216 |
spazio = get_spazio_attivo(s_main, s_alt)
|
| 217 |
min1, max1 = ordina_min_max(c1_min, c1_max)
|
| 218 |
|
|
|
|
| 220 |
img_float = image.astype(np.float32)
|
| 221 |
r, g, b = img_float[:,:,0], img_float[:,:,1], img_float[:,:,2]
|
| 222 |
exg = np.clip(2 * g - r - b, 0, 255).astype(np.uint8)
|
| 223 |
+
mask = cv2.inRange(exg, min1, 255)
|
| 224 |
else:
|
| 225 |
min2, max2 = ordina_min_max(c2_min, c2_max)
|
| 226 |
min3, max3 = ordina_min_max(c3_min, c3_max)
|
|
|
|
| 233 |
return segmented, f"{pixel_count} px"
|
| 234 |
|
| 235 |
# ==========================================
|
| 236 |
+
# 4. MOTORE AI FASTSAM (FASE 3)
|
| 237 |
# ==========================================
|
| 238 |
@lru_cache(maxsize=1)
|
| 239 |
def carica_fastsam():
|
|
|
|
| 257 |
return segmented, f"{pixel_count} px"
|
| 258 |
return image, "Nessun oggetto trovato."
|
| 259 |
|
| 260 |
+
def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
|
| 261 |
+
# FIX: Motore iterativo riprogettato con calcolo ExG e Grafica Avanzata
|
| 262 |
+
if image is None or quad_vertices is None:
|
| 263 |
+
return image, "Errore: Calibra il quadrilatero in Fase 1."
|
| 264 |
|
| 265 |
h, w = image.shape[:2]
|
| 266 |
+
|
| 267 |
+
# 1. Maschera del quadrilatero per annerire l'esterno
|
| 268 |
valid_area = np.zeros((h, w), dtype=np.uint8)
|
| 269 |
cv2.fillPoly(valid_area, [quad_vertices], 255)
|
| 270 |
+
|
| 271 |
+
# Immagine di base (Nera fuori dal nastro)
|
| 272 |
+
base_img = cv2.bitwise_and(image, image, mask=valid_area)
|
| 273 |
+
final_output = base_img.copy()
|
| 274 |
|
| 275 |
segmented_mask_total = np.zeros((h, w), dtype=np.uint8)
|
| 276 |
+
instances = []
|
| 277 |
model = carica_fastsam()
|
| 278 |
|
| 279 |
+
# 2. Loop Iterativo SAM
|
| 280 |
+
for _ in range(5):
|
|
|
|
|
|
|
|
|
|
| 281 |
remaining = cv2.bitwise_and(valid_area, cv2.bitwise_not(segmented_mask_total))
|
|
|
|
|
|
|
| 282 |
contours, _ = cv2.findContours(remaining, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 283 |
if not contours: break
|
| 284 |
|
| 285 |
largest_contour = max(contours, key=cv2.contourArea)
|
| 286 |
+
if cv2.contourArea(largest_contour) < 500: break # Ignora rimasugli troppo piccoli
|
| 287 |
+
|
| 288 |
+
# Baricentro per il prompt
|
| 289 |
M = cv2.moments(largest_contour)
|
| 290 |
+
cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else largest_contour[0][0][0]
|
| 291 |
+
cy = int(M["m01"] / M["m00"]) if M["m00"] != 0 else largest_contour[0][0][1]
|
|
|
|
|
|
|
|
|
|
| 292 |
|
|
|
|
| 293 |
results = model.predict(image, points=[[cx, cy]], labels=[1], device="cpu", verbose=False)
|
| 294 |
|
| 295 |
if len(results) > 0 and results[0].masks is not None:
|
| 296 |
mask = results[0].masks.data[0].cpu().numpy()
|
| 297 |
mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 298 |
mask = (mask * 255).astype(np.uint8)
|
| 299 |
+
mask = cv2.bitwise_and(mask, valid_area) # Non sbordare mai dal nastro
|
| 300 |
|
| 301 |
+
# Sicurezza: se SAM trova un pezzo già esplorato per errore, interrompi per evitare loop infiniti
|
| 302 |
+
new_pixels = cv2.bitwise_and(mask, cv2.bitwise_not(segmented_mask_total))
|
| 303 |
+
if np.sum(new_pixels == 255) < 100: break
|
| 304 |
+
|
| 305 |
+
instances.append(mask)
|
| 306 |
segmented_mask_total = cv2.bitwise_or(segmented_mask_total, mask)
|
|
|
|
| 307 |
else: break
|
| 308 |
|
| 309 |
+
if not instances:
|
| 310 |
+
return final_output, "Nessuna segmentazione AI riuscita."
|
| 311 |
+
|
| 312 |
+
# 3. Calcolo Excess Green per trovare la PIANTA VERA tra i segmenti trovati
|
| 313 |
+
img_float = image.astype(np.float32)
|
| 314 |
+
exg_img = 2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2]
|
| 315 |
+
exg_img = np.clip(exg_img, 0, 255).astype(np.uint8)
|
| 316 |
+
|
| 317 |
+
max_exg, plant_idx = -1, -1
|
| 318 |
+
for i, mask in enumerate(instances):
|
| 319 |
+
mean_exg = cv2.mean(exg_img, mask=mask)[0]
|
| 320 |
+
if mean_exg > max_exg:
|
| 321 |
+
max_exg, plant_idx = mean_exg, i
|
| 322 |
+
|
| 323 |
+
# 4. Disegno bordi e grafiche
|
| 324 |
+
plant_px_area = 0
|
| 325 |
+
for i, mask in enumerate(instances):
|
| 326 |
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 327 |
+
# Bordo netto per TUTTE le istanze (Magenta)
|
| 328 |
+
cv2.drawContours(final_output, contours, -1, (255, 0, 255), 2)
|
| 329 |
+
|
| 330 |
+
if i == plant_idx:
|
| 331 |
+
plant_px_area = int(np.sum(mask == 255))
|
| 332 |
+
if contours:
|
| 333 |
+
c = max(contours, key=cv2.contourArea)
|
| 334 |
+
# Trova il punto più alto del contorno per agganciare la freccia senza coprire la foglia
|
| 335 |
+
topmost = tuple(c[c[:, :, 1].argmin()][0])
|
| 336 |
+
tail = (max(0, topmost[0] - 60), max(0, topmost[1] - 80)) # Coda esterna in alto a sx
|
| 337 |
+
|
| 338 |
+
# Freccia Gialla verso la pianta
|
| 339 |
+
cv2.arrowedLine(final_output, tail, topmost, (0, 255, 255), 3, tipLength=0.2)
|
| 340 |
+
|
| 341 |
+
# Testo Dati
|
| 342 |
+
text = f"Pianta, Area: {plant_px_area} px"
|
| 343 |
+
if input_area_rif and input_area_rif > 0:
|
| 344 |
+
quad_px_area = np.sum(valid_area == 255)
|
| 345 |
+
cm2 = (input_area_rif / quad_px_area) * plant_px_area
|
| 346 |
+
text += f" | {cm2:.2f} cm2"
|
| 347 |
+
|
| 348 |
+
cv2.putText(final_output, text, (max(0, tail[0]-40), max(0, tail[1]-10)),
|
| 349 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
|
| 350 |
+
|
| 351 |
+
return final_output, f"Elaborazione completata. {len(instances)} aree trovate."
|
| 352 |
|
| 353 |
# ==========================================
|
| 354 |
# 5. INTERFACCIA UTENTE (UI)
|
|
|
|
| 356 |
with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
|
| 357 |
gr.Markdown("# 🥬 Computer Vision in Agricoltura: segmentazione")
|
| 358 |
|
|
|
|
| 359 |
quad_state = gr.State(None)
|
| 360 |
|
| 361 |
with gr.Group():
|
|
|
|
| 379 |
|
| 380 |
with gr.Row():
|
| 381 |
with gr.Column():
|
| 382 |
+
r1_min = gr.Slider(0, 179, 20, label="Tinta (H) Min")
|
| 383 |
+
r1_max = gr.Slider(0, 179, 80, label="Tinta (H) Max")
|
| 384 |
with gr.Column():
|
| 385 |
r2_min = gr.Slider(0, 255, 40, label="Saturazione (S) Min")
|
| 386 |
r2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max")
|
|
|
|
| 397 |
pixel_rif_out = gr.Textbox(label="🔥 AREA CORNICE (Pixel²)")
|
| 398 |
log_geom = gr.Textbox(label="Log di Sistema")
|
| 399 |
|
|
|
|
| 400 |
with gr.Group(visible=False) as box_area_rif:
|
| 401 |
input_area_rif = gr.Number(label="Inserisci Area Reale Riferimento (opzionale)")
|
| 402 |
|
| 403 |
sliders_r = [r1_min, r1_max, r2_min, r2_max, r3_min, r3_max]
|
| 404 |
|
|
|
|
| 405 |
for sp in [sp_main_r, sp_altri_r]:
|
| 406 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_r, sp_altri_r], outputs=sliders_r)
|
| 407 |
img_rif_in.select(fn=cattura_colore, inputs=[img_rif_in, sp_main_r, sp_altri_r], outputs=sliders_r)
|
| 408 |
|
| 409 |
for s in sliders_r + [sp_main_r, sp_altri_r]:
|
| 410 |
+
# FIX: Rimossa l'ultima variabile (bottone fantasma)
|
| 411 |
s.change(
|
| 412 |
fn=elabora_nastro,
|
| 413 |
inputs=[img_rif_in, sp_main_r, sp_altri_r] + sliders_r,
|
| 414 |
+
outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state, box_area_rif]
|
| 415 |
)
|
| 416 |
|
| 417 |
# --- TAB 2: COLORE PIANTA ---
|
|
|
|
| 444 |
|
| 445 |
sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
|
| 446 |
|
|
|
|
| 447 |
for sp in [sp_main_l, sp_altri_l]:
|
| 448 |
sp.change(fn=aggiorna_sliders, inputs=[sp_main_l, sp_altri_l], outputs=sliders_l)
|
| 449 |
img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, sp_main_l, sp_altri_l], outputs=sliders_l)
|
|
|
|
| 462 |
img_ai_out = gr.Image(label="Risultato Rete Neurale", height=350)
|
| 463 |
pixel_ai_out = gr.Textbox(label="Pixel Pianta Rilevati (Area)")
|
| 464 |
|
|
|
|
| 465 |
img_ai_in.select(fn=segmenta_ai_manuale, inputs=[img_ai_in], outputs=[img_ai_out, pixel_ai_out])
|
| 466 |
|
| 467 |
+
# L'evento Auto-SAM ora riceve anche input_area_rif per il calcolo in cm2
|
| 468 |
+
btn_auto_sam.click(fn=segmenta_ai_automatico, inputs=[img_ai_in, quad_state, input_area_rif], outputs=[img_ai_out, pixel_ai_out])
|
| 469 |
|
| 470 |
+
img_rif_in.change(lambda: gr.update(visible=False), None, btn_auto_sam)
|
|
|
|
| 471 |
|
| 472 |
+
def aggiorna_btn_auto(quad): return gr.update(visible=(quad is not None))
|
|
|
|
|
|
|
| 473 |
quad_state.change(fn=aggiorna_btn_auto, inputs=quad_state, outputs=btn_auto_sam)
|
| 474 |
|
|
|
|
| 475 |
btn_carica.click(
|
| 476 |
fn=carica_da_dataset,
|
| 477 |
inputs=[ui_cond, ui_camp, ui_dat],
|