Spaces:
Sleeping
Sleeping
| 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À | |
| # ========================================== | |
| 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) | |
| # ========================================== | |
| 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() |