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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -118
app.py CHANGED
@@ -38,7 +38,7 @@ def calcola_dats_disponibili(dataset_dir=DATASET_DIR, samples=SAMPLES, expected_
38
  AVAILABLE_DATS = calcola_dats_disponibili()
39
 
40
  # ==========================================
41
- # 1. FUNZIONI DI UTILITÀ (CON PROTEZIONE RAM)
42
  # ==========================================
43
  @lru_cache(maxsize=16)
44
  def _load_rgb(filepath: str) -> np.ndarray | None:
@@ -63,45 +63,84 @@ def carica_da_dataset(condizione: str, campione: str, dat: str):
63
  filepath = os.path.join(DATASET_DIR, filename)
64
  if os.path.exists(filepath):
65
  img = _load_rgb(filepath)
66
- # Restituisce l'immagine alle 3 tab
67
  return img, img, img, f"✅ Caricato: {filename}"
68
 
69
- blank = np.zeros((600, 900, 3), dtype=np.uint8)
70
- cv2.putText(blank, f"Non trovato: {candidates[0]}", (30, 300), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)
71
  return blank, blank, blank, f"❌ Errore: File mancante."
72
 
73
- def ordina_min_max(v1, v2):
74
- return int(min(v1, v2)), int(max(v1, v2))
 
75
 
76
- def converti_spazio_colore(image: np.ndarray, color_space: str) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  if color_space == "HSV": return cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
78
  elif color_space == "LAB": return cv2.cvtColor(image, cv2.COLOR_RGB2LAB)
79
  return image.copy()
80
 
81
- def cattura_colore(image: np.ndarray, evt: gr.SelectData, color_space: str):
82
  if image is None: return (0, 255, 0, 255, 0, 255)
83
  x, y = evt.index
84
  h, w = image.shape[:2]
85
  if not (0 <= x < w and 0 <= y < h): return (0, 255, 0, 255, 0, 255)
 
 
86
  pixel_rgb = image[y, x]
 
 
 
 
 
 
 
87
  pixel_img = np.uint8([[pixel_rgb]])
88
- pixel_conv = converti_spazio_colore(pixel_img, color_space)[0][0]
89
  v1, v2, v3 = [int(v) for v in pixel_conv]
90
  delta = 25
91
  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))
92
 
93
  # ==========================================
94
- # 2. MOTORE GEOMETRICO (FASE 1 - NASTRO)
95
  # ==========================================
96
- def elabora_nastro(image, color_space, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
97
- if image is None: return None, None, "0", "In attesa..."
 
 
98
 
 
99
  min1, max1 = ordina_min_max(c1_min, c1_max)
100
- min2, max2 = ordina_min_max(c2_min, c2_max)
101
- min3, max3 = ordina_min_max(c3_min, c3_max)
102
 
103
- img_conv = converti_spazio_colore(image, color_space)
104
- mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
 
 
 
 
 
 
 
 
105
 
106
  kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
107
  mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
@@ -113,6 +152,9 @@ def elabora_nastro(image, color_space, c1_min, c1_max, c2_min, c2_max, c3_min, c
113
 
114
  area_pixel = 0
115
  log_msg = "Linee trovate. "
 
 
 
116
 
117
  if lines is not None:
118
  orizzontali, verticali = [], []
@@ -137,45 +179,28 @@ def elabora_nastro(image, color_space, c1_min, c1_max, c2_min, c2_max, c3_min, c
137
  def intersezione(l1, l2):
138
  A = np.array([[np.cos(l1[1]), np.sin(l1[1])], [np.cos(l2[1]), np.sin(l2[1])]])
139
  b = np.array([l1[0], l2[0]])
140
- try:
141
- inter = np.linalg.solve(A, b)
142
- return (int(round(inter[0])), int(round(inter[1])))
143
- except Exception: return None
144
 
145
  vertici = [intersezione(top, left), intersezione(top, right), intersezione(bottom, right), intersezione(bottom, left)]
146
 
147
  if None not in vertici:
148
- vertici_np = np.array(vertici, dtype=np.int32).reshape((-1, 1, 2))
149
  for v in vertici: cv2.circle(debug_img, v, 15, (255, 0, 0), -1)
150
- cv2.polylines(debug_img, [vertici_np], isClosed=True, color=(0, 255, 0), thickness=4)
151
- area_pixel = int(abs(cv2.contourArea(vertici_np)))
152
  log_msg += "Poligono chiuso."
 
 
153
  else: log_msg += "Poligono aperto."
154
  else: log_msg += "Lati mancanti."
155
  else: log_msg += "Nessuna linea rilevata."
156
 
157
- return mask_visiva, debug_img, str(area_pixel), log_msg
158
 
159
  # ==========================================
160
- # 3. MOTORE LATTUGA (TRADIZIONALE & ExG)
161
  # ==========================================
162
- def aggiorna_ui_spazio_colore(spazio):
163
- if spazio == "HSV":
164
- return (gr.update(label="Tinta (H) Min", visible=True), gr.update(label="Tinta (H) Max", visible=True),
165
- gr.update(label="Saturazione (S) Min", visible=True), gr.update(label="Saturazione (S) Max", visible=True),
166
- gr.update(label="Valore (V) Min", visible=True), gr.update(label="Valore (V) Max", visible=True))
167
- elif spazio == "LAB":
168
- return (gr.update(label="Luminanza (L) Min", visible=True), gr.update(label="Luminanza (L) Max", visible=True),
169
- gr.update(label="Asse a (Verde-Rosso) Min", visible=True), gr.update(label="Asse a (Verde-Rosso) Max", visible=True),
170
- gr.update(label="Asse b (Blu-Giallo) Min", visible=True), gr.update(label="Asse b (Blu-Giallo) Max", visible=True))
171
- elif spazio == "ExG":
172
- return (gr.update(label="Soglia ExG Min", visible=True), gr.update(visible=False),
173
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False))
174
- else:
175
- return (gr.update(label="Rosso (R) Min", visible=True), gr.update(label="Rosso (R) Max", visible=True),
176
- gr.update(label="Verde (G) Min", visible=True), gr.update(label="Verde (G) Max", visible=True),
177
- gr.update(label="Blu (B) Min", visible=True), gr.update(label="Blu (B) Max", visible=True))
178
-
179
  def applica_morfologia(mask, tipo, intensita):
180
  if tipo == "Nessuna" or intensita == 0: return mask
181
  kernel_size = (intensita * 2) + 1
@@ -187,21 +212,21 @@ def applica_morfologia(mask, tipo, intensita):
187
  return cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel)
188
  return mask
189
 
190
- def elabora_lattuga_classica(image, color_space, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max, morfo_tipo, morfo_int):
191
  if image is None: return None, "0"
192
 
 
193
  min1, max1 = ordina_min_max(c1_min, c1_max)
194
 
195
- if color_space == "ExG":
196
  img_float = image.astype(np.float32)
197
  r, g, b = img_float[:,:,0], img_float[:,:,1], img_float[:,:,2]
198
- exg = 2 * g - r - b
199
- exg = np.clip(exg, 0, 255).astype(np.uint8)
200
- mask = cv2.inRange(exg, min1, max1)
201
  else:
202
  min2, max2 = ordina_min_max(c2_min, c2_max)
203
  min3, max3 = ordina_min_max(c3_min, c3_max)
204
- img_conv = converti_spazio_colore(image, color_space)
205
  mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
206
 
207
  mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
@@ -210,42 +235,92 @@ def elabora_lattuga_classica(image, color_space, c1_min, c1_max, c2_min, c2_max,
210
  return segmented, f"{pixel_count} px"
211
 
212
  # ==========================================
213
- # 4. MOTORE AI (FASTSAM)
214
  # ==========================================
215
  @lru_cache(maxsize=1)
216
  def carica_fastsam():
217
- """Scarica e carica FastSAM in RAM una sola volta."""
218
  from ultralytics import FastSAM
219
  return FastSAM("FastSAM-s.pt")
220
 
221
- def segmenta_ai(image, evt: gr.SelectData):
222
  if image is None: return None, "Nessuna immagine"
223
-
224
  x, y = evt.index
225
  model = carica_fastsam()
226
 
227
- # Inferenza AI
228
  results = model.predict(image, points=[[x, y]], labels=[1], device="cpu", verbose=False)
229
-
230
  if len(results) > 0 and results[0].masks is not None:
231
  mask = results[0].masks.data[0].cpu().numpy()
232
  mask = cv2.resize(mask, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST)
233
  mask = (mask * 255).astype(np.uint8)
234
 
235
  segmented = cv2.bitwise_and(image, image, mask=mask)
236
- pixel_count = int(np.sum(mask == 255))
237
-
238
- # Disegna un mirino (crosshair) per mostrare dove ha cliccato lo studente
239
  cv2.drawMarker(segmented, (x, y), (255, 0, 0), cv2.MARKER_CROSS, 20, 3)
 
240
  return segmented, f"{pixel_count} px"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- return image, "Nessun oggetto trovato in quel punto."
243
 
244
  # ==========================================
245
  # 5. INTERFACCIA UTENTE (UI)
246
  # ==========================================
247
  with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
248
  gr.Markdown("# 🥬 Computer Vision in Agricoltura: segmentazione")
 
 
 
249
 
250
  with gr.Group():
251
  gr.Markdown("### 📂 1. Usa il Dataset fornito o carica la tua immagine")
@@ -253,96 +328,121 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
253
  ui_cond = gr.Dropdown(UI_CONDITIONS, label="Condizione", value="No Stress")
254
  ui_camp = gr.Dropdown(SAMPLES, label="Campione", value="A")
255
  ui_dat = gr.Dropdown(AVAILABLE_DATS, label="Giorni (DAT)", value=AVAILABLE_DATS[0])
256
- btn_carica = gr.Button("⬇️ Carica Foto dal dataset", variant="primary")
257
- stato_db = gr.Textbox(label="Stato", interactive=False)
258
 
259
  with gr.Tabs():
260
-
261
- # TAB 1: CALIBRAZIONE NASTRO
262
  with gr.Tab("📐 Fase 1: Calibrazione Riferimento"):
263
- gr.Markdown("**Isola il riferimento**. L'algoritmo chiuderà i buchi, cercherà le rette e calcolerà le intersezioni.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  with gr.Row():
265
- with gr.Column(scale=1):
266
- img_rif_in = gr.Image(label="Foto Operativa", interactive=True)
267
- spazio_colore_rif = gr.Radio(["RGB", "HSV", "LAB"], value="HSV", label="Analisi del riferimento")
268
-
269
- r1_min, r1_max = gr.Slider(0, 255, 0, label="C1 Min"), gr.Slider(0, 255, 255, label="C1 Max")
270
- r2_min, r2_max = gr.Slider(0, 255, 0, label="C2 Min"), gr.Slider(0, 255, 255, label="C2 Max")
271
- r3_min, r3_max = gr.Slider(0, 255, 0, label="C3 Min"), gr.Slider(0, 255, 255, label="C3 Max")
272
- sliders_r = [r1_min, r1_max, r2_min, r2_max, r3_min, r3_max]
273
-
274
- with gr.Column(scale=1):
275
- img_mask_out = gr.Image(label="Step 1: Maschera Morfologica")
276
- img_geom_out = gr.Image(label="Step 2: Geometria (Rette e Angoli)")
277
- pixel_rif_out = gr.Textbox(label="🔥 AREA CORNICE (Pixel²)")
278
- log_geom = gr.Textbox(label="Log di Sistema")
279
-
280
- img_rif_in.select(fn=cattura_colore, inputs=[img_rif_in, spazio_colore_rif], outputs=sliders_r)
281
- for s in sliders_r + [spazio_colore_rif]:
 
 
 
282
  s.change(
283
  fn=elabora_nastro,
284
- inputs=[img_rif_in, spazio_colore_rif] + sliders_r,
285
- outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom],
286
  )
287
 
288
- # TAB 2: SEGMENTAZIONE PIANTA
289
  with gr.Tab("🌱 Fase 2: Segmentazione Colore e ExG"):
290
- gr.Markdown("**Regola gli slider per isolare la pianta.** Usa l'indice ExG per un taglio rapido o HSV per un controllo preciso.")
291
-
292
  with gr.Group():
293
  with gr.Row():
294
- spazio_colore_lat = gr.Radio(["RGB", "HSV", "LAB", "ExG"], value="HSV", label="Spazio Colore / Indice")
 
 
295
 
296
  with gr.Row():
297
  with gr.Column():
298
- l1_min = gr.Slider(0, 255, 0, label="Tinta (H) Min")
299
- l1_max = gr.Slider(0, 255, 255, label="Tinta (H) Max")
300
  with gr.Column():
301
- l2_min = gr.Slider(0, 255, 0, label="Saturazione (S) Min")
302
- l2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max")
303
  with gr.Column():
304
- l3_min = gr.Slider(0, 255, 0, label="Valore (V) Min")
305
- l3_max = gr.Slider(0, 255, 255, label="Valore (V) Max")
306
 
307
  with gr.Row():
308
  morfo_tipo = gr.Radio(["Nessuna", "Opening (Rimuove Rumore)", "Closing (Chiude Buchi)", "Open + Close"], value="Nessuna", label="Pulizia Morfologica")
309
  morfo_int = gr.Slider(1, 10, value=3, step=1, label="Intensità Pulizia (Kernel Size)")
310
 
311
  with gr.Row():
312
- img_lat_in = gr.Image(label="Immagine Originale", interactive=True)
313
- with gr.Column():
314
- img_lat_out = gr.Image(label="Pianta Segmentata")
315
- pixel_lat_out = gr.Textbox(label="Pixel Lattuga Rilevati (Area)")
316
 
317
  sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
318
 
319
- img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, spazio_colore_lat], outputs=sliders_l)
320
- spazio_colore_lat.change(fn=aggiorna_ui_spazio_colore, inputs=spazio_colore_lat, outputs=sliders_l)
 
 
321
 
322
- for s in sliders_l + [spazio_colore_lat, morfo_tipo, morfo_int]:
323
- s.change(
324
- fn=elabora_lattuga_classica,
325
- inputs=[img_lat_in, spazio_colore_lat] + sliders_l + [morfo_tipo, morfo_int],
326
- outputs=[img_lat_out, pixel_lat_out],
327
- )
328
 
329
- # TAB 3: AI (FASTSAM)
330
  with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
331
- gr.Markdown("**Segmentazione Automatica Zero-Shot.** Clicca direttamente su una foglia per isolare la pianta.")
 
 
332
 
333
  with gr.Row():
334
- img_ai_in = gr.Image(label="Clicca sulla pianta in questa finestra", interactive=True)
335
- with gr.Column():
336
- img_ai_out = gr.Image(label="Risultato Rete Neurale")
337
- pixel_ai_out = gr.Textbox(label="Pixel Lattuga Rilevati (Area)")
338
-
339
- img_ai_in.select(
340
- fn=segmenta_ai,
341
- inputs=[img_ai_in],
342
- outputs=[img_ai_out, pixel_ai_out]
343
- )
344
-
345
- # Il pulsante di caricamento spara l'immagine in tutte e tre le tab!
 
 
 
 
 
 
 
346
  btn_carica.click(
347
  fn=carica_da_dataset,
348
  inputs=[ui_cond, ui_camp, ui_dat],
 
38
  AVAILABLE_DATS = calcola_dats_disponibili()
39
 
40
  # ==========================================
41
+ # 1. FUNZIONI DI UTILITÀ
42
  # ==========================================
43
  @lru_cache(maxsize=16)
44
  def _load_rgb(filepath: str) -> np.ndarray | None:
 
63
  filepath = os.path.join(DATASET_DIR, filename)
64
  if os.path.exists(filepath):
65
  img = _load_rgb(filepath)
 
66
  return img, img, img, f"✅ Caricato: {filename}"
67
 
68
+ blank = np.zeros((350, 700, 3), dtype=np.uint8)
69
+ cv2.putText(blank, f"Non trovato: {candidates[0]}", (30, 175), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)
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
+
97
+ def converti_spazio_colore(image, color_space):
98
  if color_space == "HSV": return cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
99
  elif color_space == "LAB": return cv2.cvtColor(image, cv2.COLOR_RGB2LAB)
100
  return image.copy()
101
 
102
+ def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
103
  if image is None: return (0, 255, 0, 255, 0, 255)
104
  x, y = evt.index
105
  h, w = image.shape[:2]
106
  if not (0 <= x < w and 0 <= y < h): return (0, 255, 0, 255, 0, 255)
107
+
108
+ spazio = get_spazio_attivo(s_main, s_alt)
109
  pixel_rgb = image[y, x]
110
+
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)
125
  # ==========================================
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)
 
 
133
 
134
+ if spazio == "ExG":
135
+ img_float = image.astype(np.float32)
136
+ r, g, b = img_float[:,:,0], img_float[:,:,1], img_float[:,:,2]
137
+ exg = np.clip(2 * g - r - b, 0, 255).astype(np.uint8)
138
+ mask = cv2.inRange(exg, min1, 255)
139
+ else:
140
+ min2, max2 = ordina_min_max(c2_min, c2_max)
141
+ min3, max3 = ordina_min_max(c3_min, c3_max)
142
+ img_conv = converti_spazio_colore(image, spazio)
143
+ mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
144
 
145
  kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
146
  mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
 
152
 
153
  area_pixel = 0
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 = [], []
 
179
  def intersezione(l1, l2):
180
  A = np.array([[np.cos(l1[1]), np.sin(l1[1])], [np.cos(l2[1]), np.sin(l2[1])]])
181
  b = np.array([l1[0], l2[0]])
182
+ try: return (int(round(np.linalg.solve(A, b)[0])), int(round(np.linalg.solve(A, b)[1])))
183
+ except: return None
 
 
184
 
185
  vertici = [intersezione(top, left), intersezione(top, right), intersezione(bottom, right), intersezione(bottom, left)]
186
 
187
  if None not in vertici:
188
+ quad_state_val = np.array(vertici, dtype=np.int32).reshape((-1, 1, 2))
189
  for v in vertici: cv2.circle(debug_img, v, 15, (255, 0, 0), -1)
190
+ cv2.polylines(debug_img, [quad_state_val], isClosed=True, color=(0, 255, 0), thickness=4)
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)
203
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  def applica_morfologia(mask, tipo, intensita):
205
  if tipo == "Nessuna" or intensita == 0: return mask
206
  kernel_size = (intensita * 2) + 1
 
212
  return cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel)
213
  return mask
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
 
221
+ if spazio == "ExG":
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)
229
+ img_conv = converti_spazio_colore(image, spazio)
230
  mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
231
 
232
  mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
 
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():
 
242
  from ultralytics import FastSAM
243
  return FastSAM("FastSAM-s.pt")
244
 
245
+ def segmenta_ai_manuale(image, evt: gr.SelectData):
246
  if image is None: return None, "Nessuna immagine"
 
247
  x, y = evt.index
248
  model = carica_fastsam()
249
 
 
250
  results = model.predict(image, points=[[x, y]], labels=[1], device="cpu", verbose=False)
 
251
  if len(results) > 0 and results[0].masks is not None:
252
  mask = results[0].masks.data[0].cpu().numpy()
253
  mask = cv2.resize(mask, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST)
254
  mask = (mask * 255).astype(np.uint8)
255
 
256
  segmented = cv2.bitwise_and(image, image, mask=mask)
 
 
 
257
  cv2.drawMarker(segmented, (x, y), (255, 0, 0), cv2.MARKER_CROSS, 20, 3)
258
+ pixel_count = int(np.sum(mask == 255))
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)
318
  # ==========================================
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():
326
  gr.Markdown("### 📂 1. Usa il Dataset fornito o carica la tua immagine")
 
328
  ui_cond = gr.Dropdown(UI_CONDITIONS, label="Condizione", value="No Stress")
329
  ui_camp = gr.Dropdown(SAMPLES, label="Campione", value="A")
330
  ui_dat = gr.Dropdown(AVAILABLE_DATS, label="Giorni (DAT)", value=AVAILABLE_DATS[0])
331
+ btn_carica = gr.Button("⬇️ Carica Foto", variant="primary")
332
+ stato_db = gr.Textbox(label="Stato Sistema", interactive=False)
333
 
334
  with gr.Tabs():
335
+ # --- TAB 1: GEOMETRIA ---
 
336
  with gr.Tab("📐 Fase 1: Calibrazione Riferimento"):
337
+ gr.Markdown("**Isola il nastro**. L'algoritmo calcolerà il quadrilatero interno per contenere la pianta.")
338
+ with gr.Group():
339
+ with gr.Row():
340
+ sp_main_r = gr.Radio(["HSV", "ExG"], value="HSV", label="Spazio Colore Consigliato")
341
+ with gr.Accordion("Altri Spazi di Colore (Avanzato)", open=False):
342
+ sp_altri_r = gr.Radio(["Nessuno", "RGB", "LAB"], value="Nessuno", label="Override Spazio Colore")
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")
351
+ with gr.Column():
352
+ r3_min = gr.Slider(0, 255, 40, label="Valore (V) Min")
353
+ r3_max = gr.Slider(0, 255, 255, label="Valore (V) Max")
354
+
355
  with gr.Row():
356
+ img_rif_in = gr.Image(label="Foto Operativa", interactive=True, height=350)
357
+ img_mask_out = gr.Image(label="Step 1: Maschera Morfologica", height=350)
358
+ img_geom_out = gr.Image(label="Step 2: Geometria (Rette e Angoli)", height=350)
359
+
360
+ with gr.Row():
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 ---
383
  with gr.Tab("🌱 Fase 2: Segmentazione Colore e ExG"):
 
 
384
  with gr.Group():
385
  with gr.Row():
386
+ sp_main_l = gr.Radio(["HSV", "ExG"], value="ExG", label="Spazio Colore Consigliato")
387
+ with gr.Accordion("Altri Spazi di Colore (Avanzato)", open=False):
388
+ sp_altri_l = gr.Radio(["Nessuno", "RGB", "LAB"], value="Nessuno", label="Override Spazio Colore")
389
 
390
  with gr.Row():
391
  with gr.Column():
392
+ l1_min = gr.Slider(0, 255, 40, label="Soglia Minima ExG")
393
+ l1_max = gr.Slider(0, 255, 255, label="Tinta (H) Max", visible=False)
394
  with gr.Column():
395
+ l2_min = gr.Slider(0, 255, 40, label="Saturazione (S) Min", visible=False)
396
+ l2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max", visible=False)
397
  with gr.Column():
398
+ l3_min = gr.Slider(0, 255, 40, label="Valore (V) Min", visible=False)
399
+ l3_max = gr.Slider(0, 255, 255, label="Valore (V) Max", visible=False)
400
 
401
  with gr.Row():
402
  morfo_tipo = gr.Radio(["Nessuna", "Opening (Rimuove Rumore)", "Closing (Chiude Buchi)", "Open + Close"], value="Nessuna", label="Pulizia Morfologica")
403
  morfo_int = gr.Slider(1, 10, value=3, step=1, label="Intensità Pulizia (Kernel Size)")
404
 
405
  with gr.Row():
406
+ img_lat_in = gr.Image(label="Immagine Originale", interactive=True, height=350)
407
+ img_lat_out = gr.Image(label="Pianta Segmentata", height=350)
408
+ pixel_lat_out = gr.Textbox(label="Pixel Pianta Rilevati (Area)")
 
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)
416
 
417
+ for s in sliders_l + [sp_main_l, sp_altri_l, morfo_tipo, morfo_int]:
418
+ s.change(fn=elabora_pianta, inputs=[img_lat_in, sp_main_l, sp_altri_l] + sliders_l + [morfo_tipo, morfo_int], outputs=[img_lat_out, pixel_lat_out])
 
 
 
 
419
 
420
+ # --- TAB 3: AI FASTSAM ---
421
  with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
422
+ gr.Markdown("**Clicca sulla pianta** per estrarla, oppure usa l'Auto-Segmentatore basato sulla cornice elaborata in Fase 1.")
423
+
424
+ btn_auto_sam = gr.Button("🤖 Segmenta Automaticamente l'interno del Nastro", variant="primary", visible=False)
425
 
426
  with gr.Row():
427
+ img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=True, height=350)
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],