wlatt's picture
Update app.py
f1cb7bb verified
Raw
History Blame Contribute Delete
30.9 kB
import os
import re
import tempfile
from functools import lru_cache
import cv2
import numpy as np
import gradio as gr
# ==========================================
# 0. CONFIG DATASET
# ==========================================
DATASET_DIR = "dataset"
EXPECTED_DATS = ["06", "09", "11", "14", "16", "19", "23", "26"]
SAMPLES = ["A", "B", "C"]
UI_CONDITIONS = ["Stress", "No Stress"]
COND_CODE = {"Stress": "S", "No Stress": "NS"}
FILENAME_RE = re.compile(r"^(S|NS)_([A-Za-z0-9]+)_(\d{2})\.(jpg|jpeg|png)$", re.IGNORECASE)
def _list_dataset_files(dataset_dir: str) -> list[str]:
if not os.path.isdir(dataset_dir): return []
return [f for f in os.listdir(dataset_dir) if os.path.isfile(os.path.join(dataset_dir, f))]
def calcola_dats_disponibili(dataset_dir=DATASET_DIR, samples=SAMPLES, expected_dats=EXPECTED_DATS):
files = _list_dataset_files(dataset_dir)
if not files: return expected_dats.copy()
per_combo = { (code, s): set() for code in ("S", "NS") for s in samples }
for fname in files:
m = FILENAME_RE.match(fname)
if m: per_combo[(m.group(1).upper(), m.group(2).upper())].add(m.group(3))
sets = list(per_combo.values())
common = set.intersection(*sets) if sets else set()
out = [d for d in expected_dats if d in common]
if not out:
present = set().union(*sets) if sets else set()
out = [d for d in expected_dats if d in present]
return out if out else expected_dats.copy()
AVAILABLE_DATS = calcola_dats_disponibili()
# ==========================================
# 1. FUNZIONI DI UTILITÀ
# ==========================================
@lru_cache(maxsize=16)
def _load_rgb(filepath: str) -> np.ndarray | None:
img_bgr = cv2.imread(filepath)
if img_bgr is None: return None
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
max_dim = 1500
h, w = img_rgb.shape[:2]
if max(h, w) > max_dim:
scale = max_dim / max(h, w)
img_rgb = cv2.resize(img_rgb, (int(w * scale), int(h * scale)))
return img_rgb
def carica_da_dataset(condizione: str, campione: str, dat: str):
codice_cond = COND_CODE.get(condizione, "NS")
campione = str(campione).upper().strip()
dat = str(dat).zfill(2)
candidates = [f"{codice_cond}_{campione}_{dat}.{ext}" for ext in ["jpg", "jpeg", "png"]]
for filename in candidates:
filepath = os.path.join(DATASET_DIR, filename)
if os.path.exists(filepath):
img = _load_rgb(filepath)
return img, f"✅ Caricato: {filename}"
return None, f"❌ Errore: File mancante {candidates[0]}."
def get_spazio_attivo(main_space, alt_space):
return alt_space if alt_space != "Nessuno" else main_space
def aggiorna_sliders(spazio_principale, spazio_altri):
spazio = get_spazio_attivo(spazio_principale, spazio_altri)
if spazio == "HSV":
return (gr.update(label="Tinta (H) Min", value=30, maximum=179, visible=True), gr.update(label="Tinta (H) Max", value=80, maximum=179, visible=True),
gr.update(label="Saturazione (S) Min", value=40, visible=True), gr.update(label="Saturazione (S) Max", value=255, visible=True),
gr.update(label="Valore (V) Min", value=40, visible=True), gr.update(label="Valore (V) Max", value=255, visible=True))
elif spazio == "ExG":
return (gr.update(label="Soglia Minima ExG", value=40, maximum=255, visible=True), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False))
elif spazio == "LAB":
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),
gr.update(label="Asse A Min", value=0, visible=True), gr.update(label="Asse A Max", value=110, visible=True),
gr.update(label="Asse B Min", value=130, visible=True), gr.update(label="Asse B Max", value=255, visible=True))
else: # RGB
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),
gr.update(label="Verde (G) Min", value=100, visible=True), gr.update(label="Verde (G) Max", value=255, visible=True),
gr.update(label="Blu (B) Min", value=0, visible=True), gr.update(label="Blu (B) Max", value=100, visible=True))
def converti_spazio_colore(image, color_space):
if color_space == "HSV": return cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
elif color_space == "LAB": return cv2.cvtColor(image, cv2.COLOR_RGB2LAB)
return image.copy()
def cattura_colore(image, evt: gr.SelectData, s_main, s_alt):
if image is None: return (0, 255, 0, 255, 0, 255)
x, y = evt.index
h, w = image.shape[:2]
if not (0 <= x < w and 0 <= y < h): return (0, 255, 0, 255, 0, 255)
spazio = get_spazio_attivo(s_main, s_alt)
pixel_rgb = image[y, x]
if spazio == "ExG":
r, g, b = float(pixel_rgb[0]), float(pixel_rgb[1]), float(pixel_rgb[2])
exg = int(np.clip(2 * g - r - b, 0, 255))
return (max(0, exg-20), 255, 0, 255, 0, 255)
pixel_img = np.uint8([[pixel_rgb]])
pixel_conv = converti_spazio_colore(pixel_img, spazio)[0][0]
v1, v2, v3 = [int(v) for v in pixel_conv]
max_v1 = 179 if spazio == "HSV" else 255
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))
def disegna_etichetta_pianta(img, mask, quad_vertices):
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours: return img
cv2.drawContours(img, contours, -1, (255, 0, 255), 2)
c = max(contours, key=cv2.contourArea)
topmost = tuple(c[c[:, :, 1].argmin()][0])
tail = (max(20, topmost[0] - 120), max(40, topmost[1] - 80))
cv2.arrowedLine(img, tail, topmost, (0, 255, 255), 3, tipLength=0.2)
px_area = int(np.sum(mask == 255))
h, w = img.shape[:2]
# QoL 4 & 5: Calcolo percentuale dinamico
if quad_vertices is not None and len(quad_vertices) > 0:
valid_area = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(valid_area, [quad_vertices], 255)
ref_area = np.sum(valid_area == 255)
ref_type = "della Cornice"
else:
ref_area = h * w
ref_type = "della Foto"
perc = (px_area / ref_area) * 100 if ref_area > 0 else 0
text = f"Pianta: {px_area} px ({perc:.1f}% {ref_type})"
text_pos = (max(10, tail[0] - 50), max(20, tail[1] - 15))
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 0, 0), 6)
cv2.putText(img, text, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 255), 3)
return img
def salva_temp_pulita(img_rgb):
"""QoL 1: Salva l'immagine senza scritte per il download"""
if img_rgb is None: return None
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
fd, path = tempfile.mkstemp(suffix=".png", prefix="pianta_pulita_")
os.close(fd)
cv2.imwrite(path, img_bgr)
return path
# ==========================================
# 2. MOTORE GEOMETRICO (FASE 1)
# ==========================================
def _ordina_vertici(pts):
pts = pts.reshape((4, 2))
rect = np.zeros((4, 2), dtype=np.float32)
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # TL
rect[2] = pts[np.argmax(s)] # BR
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # TR
rect[3] = pts[np.argmax(diff)] # BL
return rect
def _genera_maschera_auto(image, bg_color, frame_color, step):
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
mask = np.zeros(gray.shape, dtype=np.uint8)
s_min = max(50, 150 - step * 25)
v_min = max(50, 150 - step * 25)
v_max_dark = min(200, 50 + step * 25)
v_min_light = max(100, 200 - step * 20)
if frame_color == "Rossa":
lower1, upper1 = np.array([0, s_min, v_min]), np.array([10, 255, 255])
lower2, upper2 = np.array([170, s_min, v_min]), np.array([179, 255, 255])
mask = cv2.bitwise_or(cv2.inRange(hsv, lower1, upper1), cv2.inRange(hsv, lower2, upper2))
elif frame_color == "Blu":
mask = cv2.inRange(hsv, np.array([100, s_min, v_min]), np.array([140, 255, 255]))
elif frame_color == "Viola":
mask = cv2.inRange(hsv, np.array([125, s_min, v_min]), np.array([165, 255, 255]))
elif frame_color == "Bianca":
mask = cv2.inRange(gray, v_min_light, 255)
elif frame_color == "Nera":
mask = cv2.inRange(gray, 0, v_max_dark)
return mask
def _trova_quad_in_maschera(mask):
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
edges = cv2.Canny(mask_closed, 50, 150)
lines = cv2.HoughLines(edges, rho=1, theta=np.pi / 180, threshold=100)
if lines is None: return None
orizzontali, verticali = [], []
for line in lines:
rho, theta = line[0]
angolo = theta * 180 / np.pi
if 45 < angolo < 135: orizzontali.append((rho, theta))
else: verticali.append((rho, theta, rho / np.cos(theta) if np.cos(theta) != 0 else rho))
if len(orizzontali) >= 2 and len(verticali) >= 2:
orizzontali.sort(key=lambda x: x[0])
verticali.sort(key=lambda x: x[2])
top, bottom = orizzontali[0], orizzontali[-1]
left, right = verticali[0][:2], verticali[-1][:2]
def intersezione(l1, l2):
A = np.array([[np.cos(l1[1]), np.sin(l1[1])], [np.cos(l2[1]), np.sin(l2[1])]])
b = np.array([l1[0], l2[0]])
try: return [int(round(np.linalg.solve(A, b)[0])), int(round(np.linalg.solve(A, b)[1]))]
except: return None
vertici = [intersezione(top, left), intersezione(top, right), intersezione(bottom, right), intersezione(bottom, left)]
if None not in vertici:
quad = np.array(vertici, dtype=np.float32)
h, w = mask.shape
if (w * h * 0.05) < cv2.contourArea(quad) < (w * h * 0.95):
return _ordina_vertici(quad)
return None
def elabora_riferimento_automatico(image, bg_color, frame_color):
if image is None: return None, None, "0", "In attesa...", None
if not bg_color or not frame_color:
gr.Warning("⚠️ Seleziona sia il Colore Sfondo che il Colore Cornice!")
return None, None, "0", "Errore: Colori non selezionati.", None
valid_quads, masks_debug = [], []
for step in range(5):
mask = _genera_maschera_auto(image, bg_color, frame_color, step)
masks_debug.append(mask)
quad = _trova_quad_in_maschera(mask)
if quad is not None: valid_quads.append(quad)
if not valid_quads:
return cv2.cvtColor(masks_debug[2], cv2.COLOR_GRAY2RGB), image.copy(), "0", "❌ Fallito: Nessun quadrilatero.", None
median_quad = np.median(np.array(valid_quads), axis=0).astype(np.int32)
quad_state_val = median_quad.reshape((-1, 1, 2))
debug_img = image.copy()
cv2.polylines(debug_img, [quad_state_val], isClosed=True, color=(0, 255, 0), thickness=4)
for v in median_quad: cv2.circle(debug_img, tuple(v), 15, (255, 0, 0), -1)
area_pixel = int(abs(cv2.contourArea(median_quad)))
return cv2.cvtColor(masks_debug[2], cv2.COLOR_GRAY2RGB), debug_img, str(area_pixel), f"✅ Consenso: {len(valid_quads)}/5 step.", quad_state_val
def ordina_min_max(v1, v2): return int(min(v1, v2)), int(max(v1, v2))
def elabora_riferimento_manuale(image, s_main, s_alt, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max):
if image is None: return None, None, "0", "In attesa...", None
spazio = get_spazio_attivo(s_main, s_alt)
min1, max1 = ordina_min_max(c1_min, c1_max)
if spazio == "ExG":
img_float = image.astype(np.float32)
exg = np.clip(2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2], 0, 255).astype(np.uint8)
mask = cv2.inRange(exg, min1, 255)
else:
min2, max2 = ordina_min_max(c2_min, c2_max)
min3, max3 = ordina_min_max(c3_min, c3_max)
img_conv = converti_spazio_colore(image, spazio)
mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
mask_closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask_visiva = cv2.cvtColor(mask_closed, cv2.COLOR_GRAY2RGB)
debug_img, area_pixel, quad_state_val = image.copy(), 0, None
lines = cv2.HoughLines(cv2.Canny(mask_closed, 50, 150), 1, np.pi / 180, 100)
if lines is not None:
orizzontali, verticali = [], []
for line in lines:
rho, theta = line[0]
a, b = np.cos(theta), np.sin(theta)
pt1 = (int(a * rho + 10000 * (-b)), int(b * rho + 10000 * (a)))
pt2 = (int(a * rho - 10000 * (-b)), int(b * rho - 10000 * (a)))
cv2.line(debug_img, pt1, pt2, (0, 50, 255), 1)
if 45 < theta * 180 / np.pi < 135: orizzontali.append((rho, theta))
else: verticali.append((rho, theta, rho / np.cos(theta) if np.cos(theta) != 0 else rho))
if len(orizzontali) >= 2 and len(verticali) >= 2:
orizzontali.sort(key=lambda x: x[0])
verticali.sort(key=lambda x: x[2])
top, bottom = orizzontali[0], orizzontali[-1]
left, right = verticali[0][:2], verticali[-1][:2]
def intersezione(l1, l2):
A, b = np.array([[np.cos(l1[1]), np.sin(l1[1])], [np.cos(l2[1]), np.sin(l2[1])]]), np.array([l1[0], l2[0]])
try: return (int(round(np.linalg.solve(A, b)[0])), int(round(np.linalg.solve(A, b)[1])))
except: return None
vertici = [intersezione(top, left), intersezione(top, right), intersezione(bottom, right), intersezione(bottom, left)]
if None not in vertici:
quad_state_val = np.array(vertici, dtype=np.int32).reshape((-1, 1, 2))
for v in vertici: cv2.circle(debug_img, v, 15, (255, 0, 0), -1)
cv2.polylines(debug_img, [quad_state_val], isClosed=True, color=(0, 255, 0), thickness=4)
area_pixel = int(abs(cv2.contourArea(quad_state_val)))
return mask_visiva, debug_img, str(area_pixel), "Elaborazione manuale completata.", quad_state_val
# ==========================================
# 3. MOTORE SEGMENTAZIONE PIANTA (FASE 2)
# ==========================================
def applica_morfologia(mask, tipo, intensita):
if tipo == "Nessuna" or intensita == 0: return mask
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, ((intensita * 2) + 1, (intensita * 2) + 1))
if tipo == "Opening (Rimuove Rumore)": return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
elif tipo == "Closing (Chiude Buchi)": return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
elif tipo == "Open + Close": return cv2.morphologyEx(cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel), cv2.MORPH_CLOSE, kernel)
return mask
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):
if image is None: return None, "0", gr.update(visible=False)
spazio = get_spazio_attivo(s_main, s_alt)
min1, max1 = ordina_min_max(c1_min, c1_max)
if spazio == "ExG":
img_float = image.astype(np.float32)
exg = np.clip(2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2], 0, 255).astype(np.uint8)
mask = cv2.inRange(exg, min1, 255)
else:
min2, max2 = ordina_min_max(c2_min, c2_max)
min3, max3 = ordina_min_max(c3_min, c3_max)
img_conv = converti_spazio_colore(image, spazio)
mask = cv2.inRange(img_conv, np.array([min1, min2, min3]), np.array([max1, max2, max3]))
mask_pulita = applica_morfologia(mask, morfo_tipo, morfo_int)
# Crea l'immagine pulita (solo pixel) per il download
segmented_clean = cv2.bitwise_and(image, image, mask=mask_pulita)
clean_path = salva_temp_pulita(segmented_clean)
# Crea l'immagine UI (con etichetta e percentuale)
segmented_ui = segmented_clean.copy()
if np.sum(mask_pulita == 255) > 500:
segmented_ui = disegna_etichetta_pianta(segmented_ui, mask_pulita, quad_state)
pixel_count = int(np.sum(mask_pulita == 255))
return segmented_ui, f"{pixel_count} px isolati.", gr.update(value=clean_path, visible=True)
# ==========================================
# 4. MOTORE AI FASTSAM (FASE 3)
# ==========================================
@lru_cache(maxsize=1)
def carica_fastsam():
from ultralytics import FastSAM
return FastSAM("FastSAM-s.pt")
def segmenta_ai_manuale(image, evt: gr.SelectData, quad_vertices):
if image is None: return None, "Nessuna immagine", gr.update(visible=False)
x, y = evt.index
model = carica_fastsam()
results = model.predict(image, points=[[x, y]], labels=[1], device="cpu", verbose=False)
if len(results) > 0 and results[0].masks is not None:
mask = results[0].masks.data[0].cpu().numpy()
mask = (cv2.resize(mask, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST) * 255).astype(np.uint8)
segmented_clean = cv2.bitwise_and(image, image, mask=mask)
clean_path = salva_temp_pulita(segmented_clean)
segmented_ui = disegna_etichetta_pianta(segmented_clean.copy(), mask, quad_vertices)
return segmented_ui, f"{int(np.sum(mask == 255))} px estratti.", gr.update(value=clean_path, visible=True)
return image, "Nessun oggetto trovato.", gr.update(visible=False)
def segmenta_ai_automatico(image, quad_vertices):
if image is None: return image, "Errore: Nessuna immagine.", gr.update(visible=False)
h, w = image.shape[:2]
valid_area = np.zeros((h, w), dtype=np.uint8)
if quad_vertices is not None:
cv2.fillPoly(valid_area, [quad_vertices], 255)
else:
valid_area.fill(255) # Fallback: usa tutta l'immagine se manca la calibrazione
quad_area_px = np.sum(valid_area == 255)
exclude_mask = cv2.bitwise_not(valid_area)
instances = []
model = carica_fastsam()
for step in range(5):
allowed_area = cv2.bitwise_not(exclude_mask)
if (np.sum(allowed_area == 255) / quad_area_px) < 0.10: break
if step == 0:
M = cv2.moments(valid_area)
cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
else:
dist = cv2.distanceTransform(allowed_area, cv2.DIST_L2, 5)
_, max_val, _, max_loc = cv2.minMaxLoc(dist)
if max_val < 5: break
cx, cy = max_loc
results = model.predict(image, points=[[cx, cy]], labels=[1], device="cpu", verbose=False)
if len(results) > 0 and results[0].masks is not None:
mask = results[0].masks.data[0].cpu().numpy()
mask = (cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST) * 255).astype(np.uint8)
mask = cv2.bitwise_and(mask, valid_area)
new_pixels = cv2.bitwise_and(mask, cv2.bitwise_not(exclude_mask))
if np.sum(new_pixels == 255) < 100:
cv2.circle(exclude_mask, (cx, cy), max(15, int(max_val if step > 0 else 20)), 255, -1)
continue
instances.append(mask)
exclude_mask = cv2.bitwise_or(exclude_mask, mask)
else:
cv2.circle(exclude_mask, (cx, cy), 20, 255, -1)
if not instances: return cv2.bitwise_and(image, image, mask=valid_area), "Fallimento AI.", gr.update(visible=False)
img_float = image.astype(np.float32)
exg_img = np.clip(2 * img_float[:,:,1] - img_float[:,:,0] - img_float[:,:,2], 0, 255).astype(np.uint8)
max_exg, plant_idx = -1, -1
for i, mask in enumerate(instances):
mean_exg = cv2.mean(exg_img, mask=mask)[0]
if mean_exg > max_exg: max_exg, plant_idx = mean_exg, i
best_mask = instances[plant_idx]
segmented_clean = cv2.bitwise_and(image, image, mask=best_mask)
clean_path = salva_temp_pulita(segmented_clean)
segmented_ui = disegna_etichetta_pianta(segmented_clean.copy(), best_mask, quad_vertices)
return segmented_ui, f"Esplorazione completata. Pianta isolata ({int(np.sum(best_mask == 255))} px).", gr.update(value=clean_path, visible=True)
# ==========================================
# 5. INTERFACCIA UTENTE (UI) & SINCRONIZZAZIONE
# ==========================================
# QoL 7: Tema Soft e moderno
with gr.Blocks(theme=gr.themes.Glass(primary_hue="emerald", neutral_hue="slate")) as app:
gr.Markdown("# 🥬 Computer Vision in Agricoltura: Segmentazione")
quad_state = gr.State(None)
# Header Unificato (QoL 2 & 3)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 📂 1. Carica dal Dataset (default fornito - consigliato per esplorazione funzionalità)")
ui_cond = gr.Dropdown(UI_CONDITIONS, label="Condizione", value="No Stress")
ui_camp = gr.Dropdown(SAMPLES, label="Campione", value="A")
ui_dat = gr.Dropdown(AVAILABLE_DATS, label="Giorni dal trapianto", value=AVAILABLE_DATS[0])
btn_carica = gr.Button("⬇️ Carica Dataset", variant="primary")
stato_db = gr.Textbox(label="Log di Caricamento", interactive=False)
with gr.Column(scale=1):
gr.Markdown("### 🗂️ 2. Oppure Carica File Manualmente")
global_in_img = gr.Image(label="Immagine Sorgente Globale", type="numpy", height=320)
# Warning dinamici se manca la calibrazione (QoL 5)
warning_html = "<div style='background-color: #332200; color: #ffcc00; padding: 10px; border-radius: 5px; text-align: center; font-weight: bold;'>⚠️ Riferimento non calibrato. Consigliato eseguire la Fase 1.</div>"
with gr.Tabs():
# --- TAB 1: GEOMETRIA ---
with gr.Tab("📐 Fase 1: Calibrazione Riferimento"):
gr.Markdown("**Isola l'area di riferimento**. L'algoritmo calcolerà il quadrilatero interno per contenere la pianta.")
modalita_rif = gr.Radio(["Manuale", "Automatica"], value="Manuale", label="Modalità di Calibrazione")
with gr.Group(visible=False) as box_automatico:
gr.Markdown("Seleziona i colori attesi per trovare automaticamente il quadrilatero tramite un algoritmo deterministico (no AI).")
with gr.Row():
auto_bg = gr.Dropdown(["Nero", "Bianco"], label="Colore Sfondo")
auto_frame = gr.Dropdown(["Nera", "Bianca", "Viola", "Rossa", "Blu"], label="Colore Cornice")
btn_calcola_auto = gr.Button("🪄 Esegui Calibrazione Automatica", variant="primary")
with gr.Group(visible=True) as box_manuale:
with gr.Row():
sp_main_r = gr.Radio(["HSV", "ExG"], value="HSV", label="Spazio Colore Consigliato")
with gr.Accordion("Altri Spazi (Avanzato)", open=False):
sp_altri_r = gr.Radio(["Nessuno", "RGB", "LAB"], value="Nessuno", label="Override Spazio")
with gr.Row():
with gr.Column():
r1_min = gr.Slider(0, 179, 20, label="Tinta (H) Min")
r1_max = gr.Slider(0, 179, 80, label="Tinta (H) Max")
with gr.Column():
r2_min = gr.Slider(0, 255, 40, label="Saturazione (S) Min")
r2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max")
with gr.Column():
r3_min = gr.Slider(0, 255, 40, label="Valore (V) Min")
r3_max = gr.Slider(0, 255, 255, label="Valore (V) Max")
# QoL 6: Bottone Calcola Manuale
btn_calc_f1_man = gr.Button("▶️ Esegui Calcolo Manuale", variant="secondary")
with gr.Row():
img_rif_in = gr.Image(label="Immagine in analisi", interactive=False, height=350)
img_mask_out = gr.Image(label="Maschera Corrente", height=350)
img_geom_out = gr.Image(label="Geometria Trovata", height=350)
with gr.Row():
pixel_rif_out = gr.Textbox(label="🔥 AREA CORNICE (Pixel²)")
log_geom = gr.Textbox(label="Log di Sistema")
# Switch view Tab 1
modalita_rif.change(fn=lambda c: (gr.update(visible=(c == "Manuale")), gr.update(visible=(c == "Automatica"))), inputs=modalita_rif, outputs=[box_manuale, box_automatico])
# Logic Tab 1
sliders_r = [r1_min, r1_max, r2_min, r2_max, r3_min, r3_max]
for sp in [sp_main_r, sp_altri_r]: sp.change(fn=aggiorna_sliders, inputs=[sp_main_r, sp_altri_r], outputs=sliders_r)
img_rif_in.select(fn=cattura_colore, inputs=[img_rif_in, sp_main_r, sp_altri_r], outputs=sliders_r)
fn_manuale_f1 = lambda *args: elabora_riferimento_manuale(*args)
inputs_man_f1 = [img_rif_in, sp_main_r, sp_altri_r] + sliders_r
outputs_f1 = [img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state]
for s in sliders_r + [sp_main_r, sp_altri_r]:
s.change(fn=fn_manuale_f1, inputs=inputs_man_f1, outputs=outputs_f1)
btn_calc_f1_man.click(fn=fn_manuale_f1, inputs=inputs_man_f1, outputs=outputs_f1)
btn_calcola_auto.click(fn=elabora_riferimento_automatico, inputs=[img_rif_in, auto_bg, auto_frame], outputs=outputs_f1)
# --- TAB 2: COLORE PIANTA ---
with gr.Tab("🌱 Fase 2: Segmentazione Colore"):
warn_f2 = gr.HTML(warning_html, visible=True)
with gr.Group():
with gr.Row():
sp_main_l = gr.Radio(["HSV", "ExG"], value="ExG", label="consigliato default in RGB, con indice di eccesso di verde ExG = 2G - R - B")
with gr.Accordion("Altri Spazi (Avanzato)", open=False):
sp_altri_l = gr.Radio(["Nessuno", "RGB", "LAB"], value="Nessuno", label="Override Spazio")
with gr.Row():
with gr.Column():
l1_min = gr.Slider(0, 255, 40, label="Soglia Minima ExG")
l1_max = gr.Slider(0, 255, 255, label="Tinta (H) Max", visible=False)
with gr.Column():
l2_min = gr.Slider(0, 255, 40, label="Saturazione (S) Min", visible=False)
l2_max = gr.Slider(0, 255, 255, label="Saturazione (S) Max", visible=False)
with gr.Column():
l3_min = gr.Slider(0, 255, 40, label="Valore (V) Min", visible=False)
l3_max = gr.Slider(0, 255, 255, label="Valore (V) Max", visible=False)
with gr.Row():
morfo_tipo = gr.Radio(["Nessuna", "Opening (Rimuove Rumore)", "Closing (Chiude Buchi)", "Open + Close"], value="Nessuna", label="Pulizia Morfologica")
morfo_int = gr.Slider(1, 10, value=3, step=1, label="Intensità Pulizia (Kernel)")
# QoL 6: Bottone Calcola Segmentazione
btn_calc_f2 = gr.Button("▶️ Esegui Segmentazione", variant="secondary")
with gr.Row():
img_lat_in = gr.Image(label="Immagine in analisi", interactive=False, height=350)
with gr.Column():
img_lat_out = gr.Image(label="Pianta Segmentata", height=350)
btn_down_f2 = gr.DownloadButton("💾 Scarica Immagine Pulita", visible=False) # QoL 1
pixel_lat_out = gr.Textbox(label="Dati Estratti")
# Logic Tab 2
sliders_l = [l1_min, l1_max, l2_min, l2_max, l3_min, l3_max]
for sp in [sp_main_l, sp_altri_l]: sp.change(fn=aggiorna_sliders, inputs=[sp_main_l, sp_altri_l], outputs=sliders_l)
img_lat_in.select(fn=cattura_colore, inputs=[img_lat_in, sp_main_l, sp_altri_l], outputs=sliders_l)
inputs_f2 = [img_lat_in, sp_main_l, sp_altri_l] + sliders_l + [morfo_tipo, morfo_int, quad_state]
outputs_f2 = [img_lat_out, pixel_lat_out, btn_down_f2]
for s in sliders_l + [sp_main_l, sp_altri_l, morfo_tipo, morfo_int]:
s.change(fn=elabora_pianta, inputs=inputs_f2, outputs=outputs_f2)
btn_calc_f2.click(fn=elabora_pianta, inputs=inputs_f2, outputs=outputs_f2)
# --- TAB 3: AI FASTSAM ---
with gr.Tab("🧠 Fase 3: AI (FastSAM)"):
warn_f3 = gr.HTML(warning_html, visible=True)
gr.Markdown("**Clicca sull'immagine** L'auto-segmentataore IA (FastSAM) isolerà l'oggetto cliccato.")
btn_auto_sam = gr.Button("🤖 Segmenta Automaticamente la pianta- Algoritmo ibrido (Loop deterministico con chiamate IA)", variant="primary")
with gr.Row():
img_ai_in = gr.Image(label="Clicca sull'oggetto", interactive=False, height=350)
with gr.Column():
img_ai_out = gr.Image(label="Risultato AI", height=350)
btn_down_f3 = gr.DownloadButton("💾 Scarica Immagine Pulita", visible=False) # QoL 1
pixel_ai_out = gr.Textbox(label="Dati AI")
img_ai_in.select(fn=segmenta_ai_manuale, inputs=[img_ai_in, quad_state], outputs=[img_ai_out, pixel_ai_out, btn_down_f3])
btn_auto_sam.click(fn=segmenta_ai_automatico, inputs=[img_ai_in, quad_state], outputs=[img_ai_out, pixel_ai_out, btn_down_f3])
# Sincronizzazione Globale QoL 2, 3 e 5
def imposta_immagine_globale(img):
return (
img, img, img, # Le 3 immagini di input nelle tab
None, None, "0", "", None, # Output Fase 1
None, "", gr.update(visible=False), # Output Fase 2
None, "", gr.update(visible=False) # Output Fase 3
)
# Aggiorna banner di avviso quando si ricalcola o resetta la cornice
quad_state.change(
fn=lambda q: (gr.update(visible=(q is None)), gr.update(visible=(q is None))),
inputs=quad_state,
outputs=[warn_f2, warn_f3]
)
global_in_img.change(
fn=imposta_immagine_globale,
inputs=global_in_img,
outputs=[
img_rif_in, img_lat_in, img_ai_in,
img_mask_out, img_geom_out, pixel_rif_out, log_geom, quad_state,
img_lat_out, pixel_lat_out, btn_down_f2,
img_ai_out, pixel_ai_out, btn_down_f3
]
)
btn_carica.click(fn=carica_da_dataset, inputs=[ui_cond, ui_camp, ui_dat], outputs=[global_in_img, stato_db])
app.launch()