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 = "
⚠️ Riferimento non calibrato. Consigliato eseguire la Fase 1.
" 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()