Spaces:
Running on Zero
Running on Zero
| 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'<div class="metrics-wrap"><div class="metrics-title">{title}</div><div class="metric-grid">' | |
| f'<div class="metric-card"><span>Pixel pianta</span><strong>{plant_text}</strong></div>' | |
| f'<div class="metric-card"><span>Pixel riferimento</span><strong>{ref_text}</strong></div>' | |
| f'<div class="metric-card"><span>Area sul riferimento</span><strong>{pct:.2f}%</strong></div>' | |
| f'<div class="metric-card"><span>Area reale stimata</span><strong>{real}</strong></div>' | |
| '</div></div>' | |
| ) | |
| 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, '<div class="warning">Caricare una fotografia.</div>', "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, '<div class="warning">Caricare una fotografia.</div>', "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 '<div class="warning">Nessuna maschera disponibile.</div>', 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, '<div class="warning">Caricare una fotografia.</div>', "Caricare una fotografia.", None | |
| if _FASTSAM_LOADER is None: | |
| return None, None, '<div class="warning">FastSAM non configurato.</div>', "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, '<div class="warning">Errore FastSAM.</div>', 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, '<div class="warning">Il metodo ibrido non ha isolato una regione valida.</div>', "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 '''<div class="guide-panel"><h3>Guida rapida</h3> | |
| <p><b>1 · Caricamento</b><br>Usa il dataset oppure carica una fotografia.</p> | |
| <p><b>2 · Calibrazione</b><br>Prova prima il rilevamento automatico della cornice. Se serve, usa i controlli manuali già visibili.</p> | |
| <p><b>3 · Segmentazione</b><br>Esegui il metodo automatico e controlla maschera, overlay e misure. Poi sperimenta con gli altri spazi di colore.</p> | |
| <p><b>4 · FastSAM</b><br>Clicca la pianta, controlla il punto e avvia la segmentazione.</p> | |
| <p><b>5 · Metodo ibrido</b><br>Il programma esplora più regioni con FastSAM e seleziona quella con risposta ExG maggiore.</p> | |
| <p><b>6 · Esportazione</b><br>Scarica le immagini pulite prodotte nei diversi passaggi.</p></div>''' | |
| def _steps_html(): | |
| labels = ["Caricamento", "Calibrazione", "Segmentazione e misure", "FastSAM", "Metodo ibrido", "Esportazione"] | |
| boxes = "".join(f'<div class="step-box"><span>{i}</span><b>{label}</b></div>' for i, label in enumerate(labels, 1)) | |
| return f'<div class="step-strip">{boxes}</div>' | |
| 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('<div class="panel-kicker">1 · CARICAMENTO</div>') | |
| 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('<div class="note">La fotografia viene caricata una sola volta e alimenta tutti i passaggi successivi.</div>') | |
| with gr.Group(elem_classes=["workflow-panel"]): | |
| gr.HTML('<div class="panel-kicker">2 · CALIBRAZIONE DEL RIFERIMENTO</div>') | |
| 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('<div class="panel-kicker">3 · SEGMENTAZIONE E MISURE</div>') | |
| 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('<div class="panel-kicker">4 · FASTSAM</div>') | |
| gr.HTML('<div class="fastsam-instruction"><b>Procedura</b><br>1. Clicca sulla pianta nell’immagine qui sotto.<br>2. Controlla il punto rosso.<br>3. Premi <b>Segmenta l’oggetto indicato</b>.</div>') | |
| 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('<div class="panel-kicker">5 · METODO IBRIDO</div>') | |
| 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('<div class="panel-kicker">6 · ESPORTAZIONE E DOCUMENTAZIONE</div>') | |
| 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'<div class="docs-panel">' | |
| f'<h3>Documentazione</h3>' | |
| f'<p><a href="{SPACE_REPO}/blob/main/README_14-16.md" target="_blank">Guida tecnica 14-16</a></p>' | |
| f'<p><a href="{SPACE_REPO}/blob/main/assets/14-16/Tutorial_setup_sperimentale.pdf" target="_blank">Tutorial setup</a></p>' | |
| f'<p><a href="{SPACE_REPO}/blob/main/assets/14-16/Scheda_esperimento.pdf" target="_blank">Scheda esperimento</a></p>' | |
| f'</div>' | |
| ) | |
| 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) | |