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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -70
app.py CHANGED
@@ -75,7 +75,6 @@ def get_spazio_attivo(main_space, alt_space):
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),
@@ -116,17 +115,50 @@ def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
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)
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
- # 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)
@@ -211,7 +243,7 @@ def applica_morfologia(mask, tipo, intensita):
211
  return cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel)
212
  return mask
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)
@@ -228,7 +260,12 @@ def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min,
228
  mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
229
 
230
  mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
 
 
231
  segmented = cv2.bitwise_and(image, image, mask=mask_pulita)
 
 
 
232
  pixel_count = int(np.sum(mask_pulita == 255))
233
  return segmented, f"{pixel_count} px"
234
 
@@ -240,7 +277,7 @@ def carica_fastsam():
240
  from ultralytics import FastSAM
241
  return FastSAM("FastSAM-s.pt")
242
 
243
- def segmenta_ai_manuale(image, evt: gr.SelectData):
244
  if image is None: return None, "Nessuna immagine"
245
  x, y = evt.index
246
  model = carica_fastsam()
@@ -252,64 +289,65 @@ def segmenta_ai_manuale(image, evt: gr.SelectData):
252
  mask = (mask * 255).astype(np.uint8)
253
 
254
  segmented = cv2.bitwise_and(image, image, mask=mask)
255
- cv2.drawMarker(segmented, (x, y), (255, 0, 0), cv2.MARKER_CROSS, 20, 3)
256
  pixel_count = int(np.sum(mask == 255))
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)
@@ -320,35 +358,13 @@ def segmenta_ai_automatico(image, quad_vertices, input_area_rif):
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)
@@ -407,7 +423,6 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
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,
@@ -440,7 +455,7 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
440
  with gr.Row():
441
  img_lat_in = gr.Image(label="Immagine Originale", interactive=True, height=350)
442
  img_lat_out = gr.Image(label="Pianta Segmentata", height=350)
443
- pixel_lat_out = gr.Textbox(label="Pixel Pianta Rilevati (Area)")
444
 
445
  sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
446
 
@@ -448,8 +463,11 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
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)
450
 
 
451
  for s in sliders_l + [sp_main_l, sp_altri_l, morfo_tipo, morfo_int]:
452
- 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])
 
 
453
 
454
  # --- TAB 3: AI FASTSAM ---
455
  with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
@@ -460,11 +478,10 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue="green")) as app:
460
  with gr.Row():
461
  img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=True, height=350)
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)
 
75
  def aggiorna_sliders(spazio_principale, spazio_altri):
76
  spazio = get_spazio_attivo(spazio_principale, spazio_altri)
77
 
 
78
  if spazio == "HSV":
79
  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),
80
  gr.update(label="Saturazione (S) Min", value=40, visible=True), gr.update(label="Saturazione (S) Max", value=255, visible=True),
 
115
  pixel_conv = converti_spazio_colore(pixel_img, spazio)[0][0]
116
  v1, v2, v3 = [int(v) for v in pixel_conv]
117
 
 
118
  max_v1 = 179 if spazio == "HSV" else 255
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)
143
+ quad_px_area = np.sum(valid_area == 255)
144
+ if quad_px_area > 0:
145
+ cm2 = (input_area_rif / quad_px_area) * px_area
146
+ text += f" | {cm2:.2f} cm2"
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
+
154
+ return img
155
+
156
  # ==========================================
157
  # 2. MOTORE GEOMETRICO (FASE 1)
158
  # ==========================================
159
  def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
160
 
161
  def elabora_nastro(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
 
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)
 
243
  return cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel)
244
  return mask
245
 
246
+ def elabora_pianta(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max, morfo_tipo, morfo_int, quad_state, input_area_rif):
247
  if image is None: return None, "0"
248
  spazio = get_spazio_attivo(s_main, s_alt)
249
  min1, max1 = ordina_min_max(c1_min, c1_max)
 
260
  mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
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)
268
+
269
  pixel_count = int(np.sum(mask_pulita == 255))
270
  return segmented, f"{pixel_count} px"
271
 
 
277
  from ultralytics import FastSAM
278
  return FastSAM("FastSAM-s.pt")
279
 
280
+ def segmenta_ai_manuale(image, evt: gr.SelectData, quad_vertices, input_area_rif):
281
  if image is None: return None, "Nessuna immagine"
282
  x, y = evt.index
283
  model = carica_fastsam()
 
289
  mask = (mask * 255).astype(np.uint8)
290
 
291
  segmented = cv2.bitwise_and(image, image, mask=mask)
292
+ segmented = disegna_etichetta_pianta(segmented, mask, quad_vertices, input_area_rif)
293
  pixel_count = int(np.sum(mask == 255))
294
  return segmented, f"{pixel_count} px"
295
  return image, "Nessun oggetto trovato."
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 il quadrilatero in Fase 1."
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
+ # exclude_mask tiene traccia di tutto ciò che NON deve essere cliccato
306
+ exclude_mask = cv2.bitwise_not(valid_area)
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
+ if max_val < 5: break # Se il punto più lontano è a meno di 5 pixel dal bordo, abbiamo esplorato tutto
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:
329
  mask = results[0].masks.data[0].cpu().numpy()
330
  mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
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
+ # Se la maschera non porta nulla di nuovo, marca il punto esplorato e riprova
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) # Aggiungi la foglia alle zone da non cliccare
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
  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)
 
423
  img_rif_in.select(fn=cattura_colore, inputs=[img_rif_in, sp_main_r, sp_altri_r], outputs=sliders_r)
424
 
425
  for s in sliders_r + [sp_main_r, sp_altri_r]:
 
426
  s.change(
427
  fn=elabora_nastro,
428
  inputs=[img_rif_in, sp_main_r, sp_altri_r] + sliders_r,
 
455
  with gr.Row():
456
  img_lat_in = gr.Image(label="Immagine Originale", interactive=True, height=350)
457
  img_lat_out = gr.Image(label="Pianta Segmentata", height=350)
458
+ pixel_lat_out = gr.Textbox(label="Dati Segmentazione")
459
 
460
  sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
461
 
 
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],
470
+ outputs=[img_lat_out, pixel_lat_out])
471
 
472
  # --- TAB 3: AI FASTSAM ---
473
  with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
 
478
  with gr.Row():
479
  img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=True, height=350)
480
  img_ai_out = gr.Image(label="Risultato Rete Neurale", height=350)
481
+ pixel_ai_out = gr.Textbox(label="Dati Segmentazione AI")
482
 
483
+ img_ai_in.select(fn=segmenta_ai_manuale, inputs=[img_ai_in, quad_state, input_area_rif], outputs=[img_ai_out, pixel_ai_out])
484
 
 
485
  btn_auto_sam.click(fn=segmenta_ai_automatico, inputs=[img_ai_in, quad_state, input_area_rif], outputs=[img_ai_out, pixel_ai_out])
486
 
487
  img_rif_in.change(lambda: gr.update(visible=False), None, btn_auto_sam)