Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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À
|
| 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((
|
| 70 |
-
cv2.putText(blank, f"Non trovato: {candidates[0]}", (30,
|
| 71 |
return blank, blank, blank, f"❌ Errore: File mancante."
|
| 72 |
|
| 73 |
-
def
|
| 74 |
-
|
|
|
|
| 75 |
|
| 76 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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,
|
| 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
|
| 95 |
# ==========================================
|
| 96 |
-
def
|
| 97 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 149 |
for v in vertici: cv2.circle(debug_img, v, 15, (255, 0, 0), -1)
|
| 150 |
-
cv2.polylines(debug_img, [
|
| 151 |
-
area_pixel = int(abs(cv2.contourArea(
|
| 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
|
| 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
|
| 191 |
if image is None: return None, "0"
|
| 192 |
|
|
|
|
| 193 |
min1, max1 = ordina_min_max(c1_min, c1_max)
|
| 194 |
|
| 195 |
-
if
|
| 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 |
-
|
| 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,
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
with gr.Row():
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
| 282 |
s.change(
|
| 283 |
fn=elabora_nastro,
|
| 284 |
-
inputs=[img_rif_in,
|
| 285 |
-
outputs=[img_mask_out, img_geom_out, pixel_rif_out, log_geom
|
| 286 |
)
|
| 287 |
|
| 288 |
-
# TAB 2:
|
| 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 |
-
|
|
|
|
|
|
|
| 295 |
|
| 296 |
with gr.Row():
|
| 297 |
with gr.Column():
|
| 298 |
-
l1_min = gr.Slider(0, 255,
|
| 299 |
-
l1_max = gr.Slider(0, 255, 255, label="Tinta (H) Max")
|
| 300 |
with gr.Column():
|
| 301 |
-
l2_min = gr.Slider(0, 255,
|
| 302 |
-
l2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max")
|
| 303 |
with gr.Column():
|
| 304 |
-
l3_min = gr.Slider(0, 255,
|
| 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 |
-
|
| 314 |
-
|
| 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 |
-
|
| 320 |
-
|
|
|
|
|
|
|
| 321 |
|
| 322 |
-
for s in sliders_l + [
|
| 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
|
| 330 |
with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
|
| 331 |
-
gr.Markdown("**
|
|
|
|
|
|
|
| 332 |
|
| 333 |
with gr.Row():
|
| 334 |
-
img_ai_in = gr.Image(label="Clicca
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
img_ai_in.select(
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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],
|