wlatt commited on
Commit
e0f01de
·
verified ·
1 Parent(s): ea2cdbf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -63
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 (Verde-Rosso) Min", value=0, visible=True), gr.update(label="Asse A (Verde-Rosso) Max", value=110, visible=True),
91
- gr.update(label="Asse B (Blu-Giallo) Min", value=130, visible=True), gr.update(label="Asse B (Blu-Giallo) Max", value=255, visible=True))
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
- delta = 20
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
- delta = 25
121
- return (max(0, v1-delta), min(255, v1+delta), max(0, v2-delta), min(255, v2+delta), max(0, v3-delta), min(255, v3+delta))
 
 
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
- if image is None: return None, None, "0", "In attesa...", None, gr.update(visible=False), gr.update(visible=False)
 
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, mostra_btn_auto
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) # Usa solo il Minimo
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 - MANUALE E AUTO)
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
- if image is None: return None, "Immagine mancante."
264
- if quad_vertices is None: return image, "Calibra il quadrilatero in Fase 1 prima di usare questa funzione."
 
265
 
266
  h, w = image.shape[:2]
267
- # 1. Crea la maschera del quadrilatero per isolare l'interno
 
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
- iterazioni = 0
275
- max_iter = 5
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) < 300: break # Se è rimasto solo rumore, fermati
287
-
288
- # 4. Calcola il centro di massa del pezzo per usarlo come prompt SAM
289
  M = cv2.moments(largest_contour)
290
- if M["m00"] != 0:
291
- cx = int(M["m10"] / M["m00"])
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
- # Limita la maschera AI per non sbordare mai fuori dal quadrilatero
305
- mask = cv2.bitwise_and(mask, valid_area)
 
 
 
306
  segmented_mask_total = cv2.bitwise_or(segmented_mask_total, mask)
307
- iterazioni += 1
308
  else: break
309
 
310
- # 6. Genera l'immagine finale (Nera fuori, RGB segmentato dentro)
311
- final_output = cv2.bitwise_and(image, image, mask=segmented_mask_total)
312
- pixel_count = int(np.sum(segmented_mask_total == 255))
313
-
314
- return final_output, f"Auto-Completato in {iterazioni} step AI. Totale: {pixel_count} px"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, 255, 20, label="Tinta (H) Min")
347
- r1_max = gr.Slider(0, 255, 80, label="Tinta (H) Max")
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, gr.Button(visible=False)] # Il button triggera quello in tab 3
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
- # Evento Automatico (Iterativo)
435
- btn_auto_sam.click(fn=segmenta_ai_automatico, inputs=[img_ai_in, quad_state], outputs=[img_ai_out, pixel_ai_out])
436
 
437
- # Il pulsante Auto-SAM si attiva solo se la Fase 1 calcola i vertici
438
- img_rif_in.change(lambda: gr.update(visible=False), None, btn_auto_sam) # Nasconde se cambi foto
439
 
440
- # Collegamento tra Tab 1 e bottone Tab 3
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],