from __future__ import annotations import os import tempfile from typing import Any, Callable import cv2 import gradio as gr import numpy as np _FASTSAM_LOADER: Callable | None = None SPACE_REPO = "https://huggingface.co/spaces/wlatt/Computer_vision_piante_scuole" def _rgb(image: Any) -> np.ndarray | None: if image is None: return None arr = np.asarray(image) if arr.ndim == 2: arr = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_GRAY2RGB) if arr.ndim != 3: raise ValueError("Formato immagine non supportato.") if arr.shape[-1] == 4: arr = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_RGBA2RGB) return arr.astype(np.uint8) def _save_png(image: np.ndarray, prefix: str) -> str: fd, path = tempfile.mkstemp(prefix=prefix, suffix=".png") os.close(fd) cv2.imwrite(path, cv2.cvtColor(image, cv2.COLOR_RGB2BGR)) return path def _mask_rgb(mask: np.ndarray) -> np.ndarray: return cv2.cvtColor(mask.astype(np.uint8), cv2.COLOR_GRAY2RGB) def _overlay(image: np.ndarray, mask: np.ndarray, tint=(30, 220, 80)) -> np.ndarray: out = image.copy() color = np.zeros_like(image) color[:] = np.array(tint, dtype=np.uint8) idx = mask > 0 if np.any(idx): out[idx] = cv2.addWeighted(image[idx], 0.56, color[idx], 0.44, 0) return out def _order_vertices(pts: np.ndarray) -> np.ndarray: pts = np.asarray(pts, dtype=np.float32).reshape((4, 2)) rect = np.zeros((4, 2), dtype=np.float32) sums = pts.sum(axis=1) rect[0] = pts[np.argmin(sums)] rect[2] = pts[np.argmax(sums)] diff = np.diff(pts, axis=1).ravel() rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] return rect def _automatic_frame_mask(image: np.ndarray, frame: str, step: int) -> np.ndarray: hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) 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 == "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]) return cv2.bitwise_or(cv2.inRange(hsv, lower1, upper1), cv2.inRange(hsv, lower2, upper2)) if frame == "Blu": return cv2.inRange(hsv, np.array([100, s_min, v_min]), np.array([140, 255, 255])) if frame == "Viola": return cv2.inRange(hsv, np.array([125, s_min, v_min]), np.array([165, 255, 255])) if frame == "Bianca": return cv2.inRange(gray, v_min_light, 255) return cv2.inRange(gray, 0, v_max_dark) def _quad_from_mask(mask: np.ndarray) -> np.ndarray | None: kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) edges = cv2.Canny(closed, 50, 150) lines = cv2.HoughLines(edges, 1, np.pi / 180, 100) if lines is None: return None horizontal, vertical = [], [] for line in lines: rho, theta = line[0] angle = theta * 180 / np.pi if 45 < angle < 135: horizontal.append((rho, theta)) else: cos = np.cos(theta) vertical.append((rho, theta, rho / cos if abs(cos) > 1e-8 else rho)) if len(horizontal) < 2 or len(vertical) < 2: return None horizontal.sort(key=lambda x: x[0]) vertical.sort(key=lambda x: x[2]) top, bottom = horizontal[0], horizontal[-1] left, right = vertical[0][:2], vertical[-1][:2] def intersection(l1, l2): matrix = np.array([[np.cos(l1[1]), np.sin(l1[1])], [np.cos(l2[1]), np.sin(l2[1])]]) vector = np.array([l1[0], l2[0]]) try: point = np.linalg.solve(matrix, vector) return [int(round(point[0])), int(round(point[1]))] except Exception: return None vertices = [intersection(top, left), intersection(top, right), intersection(bottom, right), intersection(bottom, left)] if any(v is None for v in vertices): return None quad = np.asarray(vertices, dtype=np.float32) h, w = mask.shape area = cv2.contourArea(quad) if not (w * h * 0.05 < area < w * h * 0.95): return None return _order_vertices(quad) def calibrate_reference_auto(image: Any, background: str, frame: str): rgb = _rgb(image) if rgb is None: return None, None, "0", "Caricare una fotografia.", None masks, quads = [], [] for step in range(5): mask = _automatic_frame_mask(rgb, frame, step) masks.append(mask) quad = _quad_from_mask(mask) if quad is not None: quads.append(quad) if not quads: return _mask_rgb(masks[2]), rgb.copy(), "0", "Riferimento non individuato. Usare la regolazione manuale visibile sotto.", None median = np.median(np.asarray(quads), axis=0).astype(np.int32) contour = median.reshape((-1, 1, 2)) geometry = rgb.copy() cv2.polylines(geometry, [contour], True, (25, 220, 70), 5) for vertex in median: cv2.circle(geometry, tuple(vertex), 13, (255, 45, 45), -1) area = int(abs(cv2.contourArea(median))) return _mask_rgb(masks[2]), geometry, f"{area}", f"Riferimento rilevato: consenso in {len(quads)} tentativi su 5.", contour def _convert_space(image: np.ndarray, space: str) -> np.ndarray: if space == "HSV": return cv2.cvtColor(image, cv2.COLOR_RGB2HSV) if space == "LAB": return cv2.cvtColor(image, cv2.COLOR_RGB2LAB) return image.copy() def _space_updates(space: str): if space == "HSV": values = [("H minimo", 30, 179, True), ("H massimo", 80, 179, True), ("S minimo", 40, 255, True), ("S massimo", 255, 255, True), ("V minimo", 40, 255, True), ("V massimo", 255, 255, True)] elif space == "ExG": values = [("Soglia minima ExG", 40, 255, True), ("", 255, 255, False), ("", 0, 255, False), ("", 255, 255, False), ("", 0, 255, False), ("", 255, 255, False)] elif space == "LAB": values = [("L minimo", 0, 255, True), ("L massimo", 255, 255, True), ("a minimo", 0, 255, True), ("a massimo", 110, 255, True), ("b minimo", 130, 255, True), ("b massimo", 255, 255, True)] else: values = [("R minimo", 0, 255, True), ("R massimo", 100, 255, True), ("G minimo", 100, 255, True), ("G massimo", 255, 255, True), ("B minimo", 0, 255, True), ("B massimo", 100, 255, True)] return tuple(gr.update(label=label, value=value, maximum=maximum, visible=visible) for label, value, maximum, visible in values) def _threshold_mask(image: np.ndarray, space: str, values: list[float]) -> np.ndarray: c1_min, c1_max, c2_min, c2_max, c3_min, c3_max = values if space == "ExG": f = image.astype(np.float32) exg = 2 * f[:, :, 1] - f[:, :, 0] - f[:, :, 2] return np.where(exg >= float(c1_min), 255, 0).astype(np.uint8) conv = _convert_space(image, space) lo = np.array([min(c1_min, c1_max), min(c2_min, c2_max), min(c3_min, c3_max)], dtype=np.uint8) hi = np.array([max(c1_min, c1_max), max(c2_min, c2_max), max(c3_min, c3_max)], dtype=np.uint8) return cv2.inRange(conv, lo, hi) def _morph(mask: np.ndarray, mode: str, intensity: int) -> np.ndarray: if mode == "Nessuna" or int(intensity) <= 0: return mask size = int(intensity) * 2 + 1 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (size, size)) if mode == "Apertura": return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) if mode == "Chiusura": return cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return cv2.morphologyEx(cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel), cv2.MORPH_CLOSE, kernel) def _auto_morph(mask: np.ndarray, image: np.ndarray) -> np.ndarray: size = 3 if max(image.shape[:2]) < 1400 else 5 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (size, size)) return cv2.morphologyEx(cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel), cv2.MORPH_CLOSE, kernel) def calibrate_reference_manual(image: Any, space: str, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max): rgb = _rgb(image) if rgb is None: return None, None, "0", "Caricare una fotografia.", None mask = _threshold_mask(rgb, space, [c1_min, c1_max, c2_min, c2_max, c3_min, c3_max]) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) quad = _quad_from_mask(mask) geometry = rgb.copy() area = 0 contour = None if quad is not None: contour = quad.astype(np.int32).reshape((-1, 1, 2)) area = int(abs(cv2.contourArea(quad))) cv2.polylines(geometry, [contour], True, (25, 220, 70), 5) for v in quad.astype(np.int32): cv2.circle(geometry, tuple(v), 13, (255, 45, 45), -1) message = "Riferimento individuato con le soglie manuali." else: message = "Riferimento non individuato con queste soglie." return _mask_rgb(mask), geometry, str(area), message, contour def sample_manual_thresholds(image: Any, space: str, evt: gr.SelectData): rgb = _rgb(image) if rgb is None: return _space_updates(space) try: x, y = map(int, evt.index) except Exception: return _space_updates(space) h, w = rgb.shape[:2] if not (0 <= x < w and 0 <= y < h): return _space_updates(space) pixel = rgb[y, x] if space == "ExG": r, g, b = [float(v) for v in pixel] exg = int(np.clip(2 * g - r - b, -255, 510)) return (gr.update(value=max(-255, exg - 20)), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) converted = _convert_space(np.uint8([[pixel]]), space)[0, 0] max1 = 179 if space == "HSV" else 255 vals = [int(v) for v in converted] return ( gr.update(value=max(0, vals[0] - 25)), gr.update(value=min(max1, vals[0] + 25)), gr.update(value=max(0, vals[1] - 25)), gr.update(value=min(255, vals[1] + 25)), gr.update(value=max(0, vals[2] - 25)), gr.update(value=min(255, vals[2] + 25)), ) def _valid_area_mask(image: np.ndarray, quad: Any) -> np.ndarray: valid = np.zeros(image.shape[:2], dtype=np.uint8) if quad is None: valid.fill(255) else: contour = np.asarray(quad, dtype=np.int32).reshape((-1, 1, 2)) cv2.fillPoly(valid, [contour], 255) return valid def _measurement_values(mask: np.ndarray, image: np.ndarray, quad: Any, width_cm: float, height_cm: float, l1_cm: float, l2_cm: float): plant_px = int(np.count_nonzero(mask)) valid = _valid_area_mask(image, quad) ref_px = int(np.count_nonzero(valid)) pct = 100.0 * plant_px / ref_px if ref_px else 0.0 area_cm2 = None try: width_cm, height_cm, l1_cm, l2_cm = map(float, [width_cm, height_cm, l1_cm, l2_cm]) if width_cm > 0 and height_cm > 0 and l1_cm > 0 and l2_cm > 0 and ref_px > 0: area_cm2 = (plant_px / ref_px) * (width_cm * height_cm) * ((l2_cm / l1_cm) ** 2) except Exception: area_cm2 = None return plant_px, ref_px, pct, area_cm2 def _metrics_html(mask: np.ndarray, image: np.ndarray, quad: Any, width_cm: float, height_cm: float, l1_cm: float, l2_cm: float, title="Risultati") -> str: plant_px, ref_px, pct, area_cm2 = _measurement_values(mask, image, quad, width_cm, height_cm, l1_cm, l2_cm) real = f"{area_cm2:.2f} cm²" if area_cm2 is not None else "—" plant_text = f"{plant_px:,}".replace(",", ".") ref_text = f"{ref_px:,}".replace(",", ".") return ( f'
{title}
' f'
Pixel pianta{plant_text}
' f'
Pixel riferimento{ref_text}
' f'
Area sul riferimento{pct:.2f}%
' f'
Area reale stimata{real}
' '
' ) def automatic_plant_segmentation(image: Any, quad: Any, width_cm, height_cm, l1_cm, l2_cm): rgb = _rgb(image) if rgb is None: return None, None, '
Caricare una fotografia.
', "Caricare una fotografia.", None, None f = rgb.astype(np.float32) exg = 2 * f[:, :, 1] - f[:, :, 0] - f[:, :, 2] mask = np.where(exg >= 40, 255, 0).astype(np.uint8) mask = _auto_morph(mask, rgb) mask = cv2.bitwise_and(mask, _valid_area_mask(rgb, quad)) over = _overlay(rgb, mask) clean = cv2.bitwise_and(rgb, rgb, mask=mask) return _mask_rgb(mask), over, _metrics_html(mask, rgb, quad, width_cm, height_cm, l1_cm, l2_cm, "Segmentazione automatica"), "Eseguita segmentazione ExG con pulizia automatica.", _save_png(clean, "pianta_auto_"), mask def manual_plant_segmentation(image: Any, space: str, c1_min, c1_max, c2_min, c2_max, c3_min, c3_max, morph_mode: str, morph_int: int, quad: Any, width_cm, height_cm, l1_cm, l2_cm): rgb = _rgb(image) if rgb is None: return None, None, '
Caricare una fotografia.
', "Caricare una fotografia.", None, None mask = _threshold_mask(rgb, space, [c1_min, c1_max, c2_min, c2_max, c3_min, c3_max]) mask = _morph(mask, morph_mode, morph_int) mask = cv2.bitwise_and(mask, _valid_area_mask(rgb, quad)) over = _overlay(rgb, mask) clean = cv2.bitwise_and(rgb, rgb, mask=mask) return _mask_rgb(mask), over, _metrics_html(mask, rgb, quad, width_cm, height_cm, l1_cm, l2_cm, "Segmentazione manuale"), f"Eseguita segmentazione in {space}.", _save_png(clean, "pianta_manuale_"), mask def metrics_from_mask_component(mask_image: Any, source_image: Any, quad: Any, width_cm, height_cm, l1_cm, l2_cm): rgb = _rgb(source_image) arr = _rgb(mask_image) if rgb is None or arr is None: return '
Nessuna maschera disponibile.
', None gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY) mask = np.where(gray > 127, 255, 0).astype(np.uint8) clean = cv2.bitwise_and(rgb, rgb, mask=mask) return _metrics_html(mask, rgb, quad, width_cm, height_cm, l1_cm, l2_cm, "FastSAM"), _save_png(clean, "pianta_fastsam_") def hybrid_automatic(image: Any, quad: Any, width_cm, height_cm, l1_cm, l2_cm, progress=gr.Progress()): rgb = _rgb(image) if rgb is None: return None, None, '
Caricare una fotografia.
', "Caricare una fotografia.", None if _FASTSAM_LOADER is None: return None, None, '
FastSAM non configurato.
', "FastSAM non configurato.", None h, w = rgb.shape[:2] valid = _valid_area_mask(rgb, quad) ref_px = max(1, int(np.count_nonzero(valid))) exclude = cv2.bitwise_not(valid) instances = [] model = _FASTSAM_LOADER() max_side = 1024 scale = min(1.0, max_side / max(h, w)) infer = cv2.resize(rgb, (round(w * scale), round(h * scale)), interpolation=cv2.INTER_AREA) if scale < 1 else rgb for step in range(5): progress((step + 0.2) / 5.5, desc=f"FastSAM: tentativo {step + 1} di 5") allowed = cv2.bitwise_not(exclude) if np.count_nonzero(allowed) / ref_px < 0.10: break if step == 0: m = cv2.moments(valid) if m["m00"] == 0: break cx, cy = int(m["m10"] / m["m00"]), int(m["m01"] / m["m00"]) else: dist = cv2.distanceTransform(allowed, cv2.DIST_L2, 5) _, max_val, _, max_loc = cv2.minMaxLoc(dist) if max_val < 5: break cx, cy = max_loc ix, iy = round(cx * scale), round(cy * scale) try: results = model.predict(infer, points=[[ix, iy]], labels=[1], device="cpu", imgsz=1024, retina_masks=True, verbose=False) except Exception as exc: return None, None, '
Errore FastSAM.
', f"FastSAM non disponibile: {exc}", None if not results or results[0].masks is None or len(results[0].masks.data) == 0: cv2.circle(exclude, (cx, cy), 20, 255, -1) continue data = results[0].masks.data.detach().float().cpu().numpy() candidates = [] for raw in data: candidate = cv2.resize(raw, (w, h), interpolation=cv2.INTER_NEAREST) > 0.5 if candidate[cy, cx]: candidates.append(candidate) selected = min(candidates, key=lambda m: int(m.sum())) if candidates else (cv2.resize(data[0], (w, h), interpolation=cv2.INTER_NEAREST) > 0.5) mask = selected.astype(np.uint8) * 255 mask = cv2.bitwise_and(mask, valid) new_pixels = cv2.bitwise_and(mask, cv2.bitwise_not(exclude)) if np.count_nonzero(new_pixels) < 100: cv2.circle(exclude, (cx, cy), 20, 255, -1) continue instances.append(mask) exclude = cv2.bitwise_or(exclude, mask) if not instances: return None, None, '
Il metodo ibrido non ha isolato una regione valida.
', "Il metodo ibrido non ha isolato una regione valida.", None f = rgb.astype(np.float32) exg = np.clip(2 * f[:, :, 1] - f[:, :, 0] - f[:, :, 2], -255, 510) scores = [cv2.mean(exg.astype(np.float32), mask=m)[0] for m in instances] best = instances[int(np.argmax(scores))] over = _overlay(rgb, best, (165, 55, 220)) clean = cv2.bitwise_and(rgb, rgb, mask=best) return _mask_rgb(best), over, _metrics_html(best, rgb, quad, width_cm, height_cm, l1_cm, l2_cm, "Metodo ibrido"), f"Esplorazione completata: {len(instances)} regioni candidate, selezione finale tramite ExG.", _save_png(clean, "pianta_ibrida_") def _guide_html(): return '''

Guida rapida

1 · Caricamento
Usa il dataset oppure carica una fotografia.

2 · Calibrazione
Prova prima il rilevamento automatico della cornice. Se serve, usa i controlli manuali già visibili.

3 · Segmentazione
Esegui il metodo automatico e controlla maschera, overlay e misure. Poi sperimenta con gli altri spazi di colore.

4 · FastSAM
Clicca la pianta, controlla il punto e avvia la segmentazione.

5 · Metodo ibrido
Il programma esplora più regioni con FastSAM e seleziona quella con risposta ExG maggiore.

6 · Esportazione
Scarica le immagini pulite prodotte nei diversi passaggi.

''' def _steps_html(): labels = ["Caricamento", "Calibrazione", "Segmentazione e misure", "FastSAM", "Metodo ibrido", "Esportazione"] boxes = "".join(f'
{i}{label}
' for i, label in enumerate(labels, 1)) return f'
{boxes}
' def build_14_16_tab(load_dataset_image, sync_fastsam_source, select_fastsam_point, run_fastsam_cpu, load_fastsam_cpu): global _FASTSAM_LOADER _FASTSAM_LOADER = load_fastsam_cpu with gr.Tab("14-16"): gr.Markdown("## Laboratorio di analisi: calibrazione, segmentazione e misura") gr.Markdown("Il percorso riunisce le funzioni del precedente Space sperimentale in una sequenza continua. Le azioni automatiche sono evidenziate; i controlli manuali rimangono sempre visibili sotto ogni passaggio.") gr.HTML(_steps_html()) quad_state = gr.State(None) fastsam_point = gr.State(None) automatic_mask_state = gr.State(None) with gr.Row(): with gr.Column(scale=4): with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
1 · CARICAMENTO
') with gr.Row(): source = gr.Image(type="numpy", label="Fotografia del campione", interactive=True, height=430) with gr.Column(): condition = gr.Radio(["NS", "S"], value="NS", label="Condizione dataset") replicate = gr.Radio(["A", "B", "C"], value="A", label="Replica") day = gr.Dropdown(["06", "09", "11", "14", "16", "19", "23", "26"], value="14", label="Giorno dal trapianto") load_btn = gr.Button("Carica dal dataset", variant="primary", elem_classes=["action-button"]) load_status = gr.Textbox(label="Stato", interactive=False) gr.HTML('
La fotografia viene caricata una sola volta e alimenta tutti i passaggi successivi.
') with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
2 · CALIBRAZIONE DEL RIFERIMENTO
') gr.Markdown("### Rilevamento automatico") gr.Markdown("Usa per primo questo comando. I valori iniziali corrispondono al setup del dataset: sfondo nero e cornice rossa.") with gr.Row(): bg = gr.Dropdown(["Nero", "Bianco"], value="Nero", label="Colore dello sfondo") frame = gr.Dropdown(["Rossa", "Nera", "Bianca", "Viola", "Blu"], value="Rossa", label="Colore della cornice") auto_cal_btn = gr.Button("Rileva automaticamente il riferimento", variant="primary", elem_classes=["action-button"]) with gr.Row(): cal_mask = gr.Image(label="Maschera del riferimento", interactive=False, height=300) cal_geom = gr.Image(label="Geometria rilevata", interactive=False, height=300) with gr.Row(): ref_px = gr.Textbox(label="Area del riferimento in pixel", interactive=False) cal_status = gr.Textbox(label="Esito della calibrazione", interactive=False) with gr.Group(elem_classes=["manual-panel"]): gr.Markdown("### Regolazione manuale") gr.Markdown("Questi controlli restano disponibili se il rilevamento automatico non è soddisfacente. Facendo click sulla fotografia sorgente si inizializzano le soglie intorno al colore selezionato.") cal_space = gr.Radio(["HSV", "ExG", "RGB", "LAB"], value="HSV", label="Rappresentazione del colore") with gr.Row(): r1min = gr.Slider(0, 179, 30, label="H minimo") r1max = gr.Slider(0, 179, 80, label="H massimo") r2min = gr.Slider(0, 255, 40, label="S minimo") r2max = gr.Slider(0, 255, 255, label="S massimo") r3min = gr.Slider(0, 255, 40, label="V minimo") r3max = gr.Slider(0, 255, 255, label="V massimo") manual_cal_btn = gr.Button("Calcola il riferimento con le soglie manuali", variant="primary", elem_classes=["action-button"]) gr.Markdown("### Misure geometriche per l’area reale") gr.Markdown("La percentuale rispetto alla cornice è disponibile senza misure reali. Per stimare i cm² inserire base, altezza e le distanze L1 e L2 descritte nel tutorial del setup.") with gr.Row(): width_cm = gr.Number(value=0, label="Base del riferimento · cm") height_cm = gr.Number(value=0, label="Altezza del riferimento · cm") l1_cm = gr.Number(value=0, label="Distanza L1 · cm") l2_cm = gr.Number(value=0, label="Distanza L2 · cm") with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
3 · SEGMENTAZIONE E MISURE
') gr.Markdown("### Segmentazione automatica") gr.Markdown("Il primo tentativo usa ExG con soglia iniziale e pulizia morfologica automatica. Non richiede regolazioni.") auto_seg_btn = gr.Button("Esegui la segmentazione automatica", variant="primary", elem_classes=["action-button"]) with gr.Row(): auto_mask = gr.Image(label="Maschera automatica", interactive=False, height=330) auto_overlay = gr.Image(label="Overlay automatico", interactive=False, height=330) auto_metrics = gr.HTML() auto_status = gr.Textbox(label="Esito", interactive=False) auto_file = gr.File(label="Scarica la pianta segmentata", interactive=False) with gr.Group(elem_classes=["manual-panel"]): gr.Markdown("### Regolazione manuale della segmentazione") gr.Markdown("Usa questi parametri per confrontare ExG, HSV, RGB e Lab e per osservare l’effetto della pulizia morfologica.") seg_space = gr.Radio(["ExG", "HSV", "RGB", "LAB"], value="ExG", label="Metodo di segmentazione") with gr.Row(): s1min = gr.Slider(0, 255, 40, label="Soglia minima ExG") s1max = gr.Slider(0, 255, 255, label="", visible=False) s2min = gr.Slider(0, 255, 0, label="", visible=False) s2max = gr.Slider(0, 255, 255, label="", visible=False) s3min = gr.Slider(0, 255, 0, label="", visible=False) s3max = gr.Slider(0, 255, 255, label="", visible=False) with gr.Row(): morph_mode = gr.Radio(["Nessuna", "Apertura", "Chiusura", "Apertura + chiusura"], value="Apertura + chiusura", label="Pulizia morfologica") morph_int = gr.Slider(1, 10, value=2, step=1, label="Intensità della pulizia") manual_seg_btn = gr.Button("Esegui la segmentazione con i parametri manuali", variant="primary", elem_classes=["action-button"]) with gr.Row(): manual_mask = gr.Image(label="Maschera manuale", interactive=False, height=300) manual_overlay = gr.Image(label="Overlay manuale", interactive=False, height=300) manual_metrics = gr.HTML() manual_status = gr.Textbox(label="Esito", interactive=False) manual_file = gr.File(label="Scarica la pianta segmentata", interactive=False) with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
4 · FASTSAM
') gr.HTML('
Procedura
1. Clicca sulla pianta nell’immagine qui sotto.
2. Controlla il punto rosso.
3. Premi Segmenta l’oggetto indicato.
') fastsam_image = gr.Image(type="numpy", label="Clicca sulla pianta", interactive=True, height=430) fastsam_message = gr.Textbox(label="Istruzioni e risultato", value="Caricare una fotografia e scegliere un punto.", interactive=False) fastsam_btn = gr.Button("Segmenta l’oggetto indicato", variant="primary", elem_classes=["action-button"]) with gr.Row(): fastsam_mask = gr.Image(label="Maschera FastSAM", interactive=False, visible=False, height=320) fastsam_overlay = gr.Image(label="Overlay FastSAM", interactive=False, visible=False, height=320) fastsam_metrics = gr.HTML() fastsam_file = gr.File(label="Scarica la pianta segmentata", interactive=False) fastsam_overlay_state = gr.State(None) with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
5 · METODO IBRIDO
') gr.Markdown("Il metodo esplora automaticamente più regioni con FastSAM all’interno del riferimento e seleziona la regione finale confrontando l’indice ExG. È il passaggio più impegnativo sulla CPU.") hybrid_btn = gr.Button("Esegui il metodo ibrido automatico", variant="primary", elem_classes=["action-button"]) with gr.Row(): hybrid_mask = gr.Image(label="Maschera ibrida", interactive=False, height=320) hybrid_overlay = gr.Image(label="Overlay ibrido", interactive=False, height=320) hybrid_metrics = gr.HTML() hybrid_status = gr.Textbox(label="Esito", interactive=False) hybrid_file = gr.File(label="Scarica la pianta segmentata", interactive=False) with gr.Group(elem_classes=["workflow-panel"]): gr.HTML('
6 · ESPORTAZIONE E DOCUMENTAZIONE
') gr.Markdown("I file prodotti nei passaggi precedenti possono essere scaricati direttamente dai rispettivi pannelli.") gr.Markdown( f"- [Guida tecnica dettagliata 14-16]({SPACE_REPO}/blob/main/README_14-16.md)\n" f"- [Tutorial setup sperimentale]({SPACE_REPO}/blob/main/assets/14-16/Tutorial_setup_sperimentale.pdf)\n" f"- [Scheda esperimento]({SPACE_REPO}/blob/main/assets/14-16/Scheda_esperimento.pdf)" ) with gr.Column(scale=1, min_width=270): gr.HTML(_guide_html()) gr.HTML( f'
' f'

Documentazione

' f'

Guida tecnica 14-16

' f'

Tutorial setup

' f'

Scheda esperimento

' f'
' ) load_btn.click(load_dataset_image, [condition, replicate, day], [source, load_status]) auto_cal_btn.click(calibrate_reference_auto, [source, bg, frame], [cal_mask, cal_geom, ref_px, cal_status, quad_state]) manual_cal_btn.click(calibrate_reference_manual, [source, cal_space, r1min, r1max, r2min, r2max, r3min, r3max], [cal_mask, cal_geom, ref_px, cal_status, quad_state]) cal_space.change(_space_updates, cal_space, [r1min, r1max, r2min, r2max, r3min, r3max], show_progress="hidden") source.select(sample_manual_thresholds, [source, cal_space], [r1min, r1max, r2min, r2max, r3min, r3max], show_progress="hidden") auto_seg_btn.click(automatic_plant_segmentation, [source, quad_state, width_cm, height_cm, l1_cm, l2_cm], [auto_mask, auto_overlay, auto_metrics, auto_status, auto_file, automatic_mask_state]) seg_space.change(_space_updates, seg_space, [s1min, s1max, s2min, s2max, s3min, s3max], show_progress="hidden") manual_seg_btn.click(manual_plant_segmentation, [source, seg_space, s1min, s1max, s2min, s2max, s3min, s3max, morph_mode, morph_int, quad_state, width_cm, height_cm, l1_cm, l2_cm], [manual_mask, manual_overlay, manual_metrics, manual_status, manual_file, automatic_mask_state]) source.change(sync_fastsam_source, source, [fastsam_image, fastsam_point, fastsam_mask, fastsam_overlay, fastsam_message, fastsam_overlay_state], show_progress="hidden") fastsam_image.select(select_fastsam_point, [source], [fastsam_image, fastsam_point, fastsam_message], show_progress="hidden") fastsam_btn.click(run_fastsam_cpu, [source, fastsam_point], [fastsam_mask, fastsam_overlay, fastsam_message, fastsam_overlay_state], concurrency_limit=1).then( metrics_from_mask_component, [fastsam_mask, source, quad_state, width_cm, height_cm, l1_cm, l2_cm], [fastsam_metrics, fastsam_file] ) hybrid_btn.click(hybrid_automatic, [source, quad_state, width_cm, height_cm, l1_cm, l2_cm], [hybrid_mask, hybrid_overlay, hybrid_metrics, hybrid_status, hybrid_file], concurrency_limit=1)