Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| import tempfile | |
| from pathlib import Path | |
| from typing import Any | |
| from functools import lru_cache | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.utils import ImageReader | |
| from reportlab.pdfgen import canvas | |
| DATASET_DIR = Path("dataset") | |
| DAYS = ["06", "09", "11", "14", "16", "19", "23", "26"] | |
| CONDITIONS = ["NS", "S"] | |
| REPLICATES = ["A", "B", "C"] | |
| DEFAULT_HSV = { | |
| "h_min": 50.0, | |
| "h_max": 200.0, | |
| "s_min": 14.0, | |
| "s_max": 100.0, | |
| "v_min": 10.0, | |
| "v_max": 100.0, | |
| } | |
| CSS = """ | |
| .note {background:#eef6f0;border-left:5px solid #2e7d32;padding:10px;margin:8px 0} | |
| .info {background:#edf4fb;border-left:5px solid #326a9a;padding:10px;margin:8px 0} | |
| .warning {background:#fff4d6;border-left:5px solid #b67800;padding:10px;margin:8px 0} | |
| .compact p {margin:0.35rem 0} | |
| .matrix-help {font-size:0.95rem} | |
| """ | |
| def ensure_rgb(image: Any) -> np.ndarray | None: | |
| if image is None: | |
| return None | |
| if isinstance(image, Image.Image): | |
| image = np.asarray(image) | |
| 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) | |
| if arr.shape[-1] != 3: | |
| raise ValueError("L'immagine deve avere tre canali RGB.") | |
| return arr.astype(np.uint8) | |
| def read_image(path: Path) -> np.ndarray: | |
| bgr = cv2.imread(str(path)) | |
| if bgr is None: | |
| raise FileNotFoundError(path) | |
| image = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) | |
| max_dim = 1800 | |
| h, w = image.shape[:2] | |
| if max(h, w) > max_dim: | |
| scale = max_dim / max(h, w) | |
| image = cv2.resize( | |
| image, | |
| (round(w * scale), round(h * scale)), | |
| interpolation=cv2.INTER_AREA, | |
| ) | |
| return image | |
| def dataset_name(condition: str, replicate: str, day: str) -> str: | |
| return f"{condition}_{replicate}_{day}.jpg" | |
| def load_dataset_image(condition: str, replicate: str, day: str): | |
| name = dataset_name(condition, replicate, day) | |
| path = DATASET_DIR / name | |
| try: | |
| return read_image(path), ( | |
| f"Caricata {name}. NS = non stress; S = stress idrico; " | |
| "A/B/C = replica; il numero indica i giorni dal trapianto." | |
| ) | |
| except Exception as exc: | |
| return None, f"Immagine non disponibile: {name}. Dettaglio: {exc}" | |
| # ----------------------------------------------------------------------------- | |
| # 6-9: scheda PDF a quadretti | |
| # ----------------------------------------------------------------------------- | |
| def prepare_grid_image( | |
| image: Any, | |
| rows: int, | |
| cols: int, | |
| lighten: float, | |
| use_color: bool, | |
| ) -> np.ndarray: | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| raise ValueError("Caricare una fotografia.") | |
| pil = Image.fromarray(rgb) | |
| if not use_color: | |
| pil = pil.convert("L").convert("RGB") | |
| pil = Image.blend(pil, Image.new("RGB", pil.size, "white"), float(lighten)) | |
| # La griglia viene disegnata sull'immagine finale, con spessore proporzionato. | |
| draw = ImageDraw.Draw(pil) | |
| width, height = pil.size | |
| line_width = max(1, round(min(width, height) / 500)) | |
| for col in range(1, int(cols)): | |
| x = round(col * width / int(cols)) | |
| draw.line((x, 0, x, height), fill=(15, 15, 15), width=line_width) | |
| for row in range(1, int(rows)): | |
| y = round(row * height / int(rows)) | |
| draw.line((0, y, width, y), fill=(15, 15, 15), width=line_width) | |
| return np.asarray(pil) | |
| def create_grid_pdf( | |
| image: Any, | |
| rows: int, | |
| cols: int, | |
| lighten: float, | |
| use_color: bool, | |
| activity_title: str, | |
| ): | |
| try: | |
| grid = prepare_grid_image(image, rows, cols, lighten, use_color) | |
| except Exception as exc: | |
| return None, None, f"Impossibile generare la scheda: {exc}" | |
| title = (activity_title or "Piante a quadretti").strip() | |
| fd_pdf, pdf_path = tempfile.mkstemp(prefix="piante_a_quadretti_", suffix=".pdf") | |
| os.close(fd_pdf) | |
| page_w, page_h = A4 | |
| margin_x = 42 | |
| top_y = page_h - 42 | |
| title_h = 34 | |
| note_h = 42 | |
| available_w = page_w - 2 * margin_x | |
| available_h = page_h - 2 * 42 - title_h - note_h - 16 | |
| img_h, img_w = grid.shape[:2] | |
| scale = min(available_w / img_w, available_h / img_h) | |
| draw_w = img_w * scale | |
| draw_h = img_h * scale | |
| draw_x = (page_w - draw_w) / 2 | |
| draw_y = top_y - title_h - draw_h | |
| pdf = canvas.Canvas(pdf_path, pagesize=A4) | |
| pdf.setTitle(title) | |
| pdf.setFont("Helvetica-Bold", 16) | |
| pdf.drawCentredString(page_w / 2, top_y, title) | |
| pdf.setFont("Helvetica", 9) | |
| pdf.drawCentredString( | |
| page_w / 2, | |
| top_y - 17, | |
| f"Griglia: {int(rows)} righe × {int(cols)} colonne", | |
| ) | |
| # ReportLab incorpora il JPEG già compresso, riducendo sensibilmente il peso del PDF. | |
| fd_jpg, jpg_path = tempfile.mkstemp(prefix="griglia_pdf_", suffix=".jpg") | |
| os.close(fd_jpg) | |
| Image.fromarray(grid).save(jpg_path, "JPEG", quality=92, optimize=True) | |
| pdf.drawImage( | |
| jpg_path, | |
| draw_x, | |
| draw_y, | |
| width=draw_w, | |
| height=draw_h, | |
| preserveAspectRatio=True, | |
| mask="auto", | |
| ) | |
| line_y = 52 | |
| pdf.setLineWidth(0.8) | |
| pdf.line(margin_x, line_y + 18, page_w - margin_x, line_y + 18) | |
| pdf.setFont("Helvetica", 11) | |
| pdf.drawString( | |
| margin_x, | |
| line_y, | |
| "Numero di quadratini verdi identificati/colorati: ____________________", | |
| ) | |
| pdf.showPage() | |
| pdf.save() | |
| try: | |
| os.remove(jpg_path) | |
| except OSError: | |
| pass | |
| # L'anteprima riproduce la pagina del PDF in modo leggibile dentro l'interfaccia. | |
| preview_w = 1240 | |
| preview_h = round(preview_w * page_h / page_w) | |
| preview = Image.new("RGB", (preview_w, preview_h), "white") | |
| d = ImageDraw.Draw(preview) | |
| try: | |
| font_title = ImageFont.truetype("DejaVuSans-Bold.ttf", 34) | |
| font_small = ImageFont.truetype("DejaVuSans.ttf", 20) | |
| font_note = ImageFont.truetype("DejaVuSans.ttf", 23) | |
| except OSError: | |
| font_title = font_small = font_note = None | |
| d.text((preview_w / 2, 48), title, fill="black", anchor="ma", font=font_title) | |
| d.text( | |
| (preview_w / 2, 98), | |
| f"Griglia: {int(rows)} righe × {int(cols)} colonne", | |
| fill="black", | |
| anchor="ma", | |
| font=font_small, | |
| ) | |
| grid_pil = Image.fromarray(grid) | |
| max_w = preview_w - 150 | |
| max_h = preview_h - 300 | |
| pscale = min(max_w / grid_pil.width, max_h / grid_pil.height) | |
| resized = grid_pil.resize( | |
| (round(grid_pil.width * pscale), round(grid_pil.height * pscale)), | |
| Image.Resampling.LANCZOS, | |
| ) | |
| px = (preview_w - resized.width) // 2 | |
| py = 135 | |
| preview.paste(resized, (px, py)) | |
| note_y = py + resized.height + 48 | |
| d.line((75, note_y - 24, preview_w - 75, note_y - 24), fill="black", width=2) | |
| d.text( | |
| (75, note_y), | |
| "Numero di quadratini verdi identificati/colorati: ____________________", | |
| fill="black", | |
| font=font_note, | |
| ) | |
| return np.asarray(preview), pdf_path, "Scheda PDF generata." | |
| # ----------------------------------------------------------------------------- | |
| # 9-11: esplorazione di pixel e trasformazioni additive del colore | |
| # ----------------------------------------------------------------------------- | |
| def didactic_hsv(image: np.ndarray) -> np.ndarray: | |
| hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV).astype(np.float32) | |
| hsv[:, :, 0] = hsv[:, :, 0] * (360.0 / 180.0) | |
| hsv[:, :, 1] = hsv[:, :, 1] * (100.0 / 255.0) | |
| hsv[:, :, 2] = hsv[:, :, 2] * (100.0 / 255.0) | |
| return hsv | |
| def channel_choices(space: str): | |
| choices = ["R", "G", "B"] if space == "RGB" else ["H", "S", "V"] | |
| return gr.update(choices=choices, value=choices[0]) | |
| def extract_channel(image: np.ndarray, space: str, channel: str) -> np.ndarray: | |
| if space == "RGB": | |
| idx = {"R": 0, "G": 1, "B": 2}[channel] | |
| return image[:, :, idx].astype(np.float32) | |
| hsv = didactic_hsv(image) | |
| idx = {"H": 0, "S": 1, "V": 2}[channel] | |
| return hsv[:, :, idx] | |
| def selection_bounds(x: int, y: int, size: int, width: int, height: int): | |
| size = int(size) | |
| half = size // 2 | |
| x0 = min(max(x - half, 0), max(width - size, 0)) | |
| y0 = min(max(y - half, 0), max(height - size, 0)) | |
| return x0, y0, min(x0 + size, width), min(y0 + size, height) | |
| def render_pixel_selection( | |
| image: Any, | |
| size: int, | |
| space: str, | |
| channel: str, | |
| coords: list[int] | tuple[int, int] | None, | |
| ): | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, [], "Caricare un'immagine.", coords | |
| if not coords or coords[0] is None: | |
| return rgb, None, [], "Fare click sull'immagine per posizionare il selettore.", coords | |
| x, y = map(int, coords) | |
| h, w = rgb.shape[:2] | |
| x = min(max(x, 0), w - 1) | |
| y = min(max(y, 0), h - 1) | |
| x0, y0, x1, y1 = selection_bounds(x, y, int(size), w, h) | |
| crop = rgb[y0:y1, x0:x1] | |
| if crop.shape[0] != int(size) or crop.shape[1] != int(size): | |
| return rgb, None, [], "Selezione troppo vicina al bordo.", [x, y] | |
| annotated = rgb.copy() | |
| cv2.rectangle(annotated, (x0, y0), (x1 - 1, y1 - 1), (255, 30, 30), max(2, round(min(h, w) / 350))) | |
| zoom_size = 360 | |
| zoom = cv2.resize(crop, (zoom_size, zoom_size), interpolation=cv2.INTER_NEAREST) | |
| matrix = extract_channel(crop, space, channel) | |
| if space == "HSV": | |
| matrix = np.rint(matrix).astype(int) | |
| unit = "gradi" if channel == "H" else "%" | |
| else: | |
| matrix = np.rint(matrix).astype(int) | |
| unit = "0-255" | |
| status = ( | |
| f"Selettore {int(size)}×{int(size)} centrato vicino a x={x}, y={y}. " | |
| f"Matrice del canale {channel} in {space} ({unit})." | |
| ) | |
| return annotated, zoom, matrix.tolist(), status, [x, y] | |
| def select_pixel_region(image, size, space, channel, evt: gr.SelectData): | |
| try: | |
| x, y = map(int, evt.index) | |
| except Exception: | |
| return None, None, [], "Impossibile leggere il punto selezionato.", [None, None] | |
| return render_pixel_selection(image, size, space, channel, [x, y]) | |
| def refresh_pixel_region(image, size, space, channel, coords): | |
| return render_pixel_selection(image, size, space, channel, coords) | |
| def editor_value_from_image(image: Any): | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, "Caricare un'immagine nel campo sorgente." | |
| value = {"background": rgb, "layers": [], "composite": rgb} | |
| return value, rgb, "Immagine caricata nel laboratorio del colore. Dipingere la zona da modificare." | |
| def editor_background_and_mask(editor_value: dict[str, Any] | None): | |
| if not editor_value: | |
| raise ValueError("Caricare un'immagine nel laboratorio del colore.") | |
| background = ensure_rgb(editor_value.get("background")) | |
| if background is None: | |
| raise ValueError("Sfondo dell'editor non disponibile.") | |
| mask = np.zeros(background.shape[:2], dtype=bool) | |
| for layer in editor_value.get("layers") or []: | |
| arr = np.asarray(layer) | |
| if arr.ndim == 3 and arr.shape[2] == 4: | |
| alpha = arr[:, :, 3] | |
| if alpha.shape != mask.shape: | |
| alpha = cv2.resize(alpha, (mask.shape[1], mask.shape[0]), interpolation=cv2.INTER_NEAREST) | |
| mask |= alpha > 0 | |
| elif arr.ndim == 3: | |
| layer_rgb = ensure_rgb(arr) | |
| if layer_rgb.shape[:2] != mask.shape: | |
| layer_rgb = cv2.resize(layer_rgb, (mask.shape[1], mask.shape[0]), interpolation=cv2.INTER_NEAREST) | |
| mask |= np.any(layer_rgb != 0, axis=2) | |
| # Fallback: usa la differenza fra composito e sfondo se il livello non espone alpha. | |
| if not mask.any() and editor_value.get("composite") is not None: | |
| composite = ensure_rgb(editor_value.get("composite")) | |
| if composite.shape[:2] != background.shape[:2]: | |
| composite = cv2.resize(composite, (background.shape[1], background.shape[0]), interpolation=cv2.INTER_LINEAR) | |
| mask = np.any(np.abs(composite.astype(np.int16) - background.astype(np.int16)) > 2, axis=2) | |
| return background, mask | |
| def apply_additive_brush(editor_value, space: str, channel: str, delta: float): | |
| try: | |
| background, mask = editor_background_and_mask(editor_value) | |
| except Exception as exc: | |
| return editor_value, None, f"Modifica non applicata: {exc}" | |
| if not mask.any(): | |
| return editor_value, background, "Dipingere almeno una zona prima di applicare la modifica." | |
| delta = float(delta) | |
| if space == "RGB": | |
| idx = {"R": 0, "G": 1, "B": 2}[channel] | |
| work = background.astype(np.int16) | |
| work[:, :, idx][mask] = np.clip(work[:, :, idx][mask] + round(delta), 0, 255) | |
| result = work.astype(np.uint8) | |
| explanation = f"Canale {channel}: variazione additiva {delta:+.0f}, con limite 0-255." | |
| else: | |
| hsv = cv2.cvtColor(background, cv2.COLOR_RGB2HSV).astype(np.float32) | |
| idx = {"H": 0, "S": 1, "V": 2}[channel] | |
| if channel == "H": | |
| # OpenCV usa 0-179; l'interfaccia usa gradi 0-359. | |
| hsv[:, :, idx][mask] = (hsv[:, :, idx][mask] + delta / 2.0) % 180.0 | |
| explanation = ( | |
| f"Canale H: variazione {delta:+.0f}°. H è circolare: superati 360° si riparte da 0°." | |
| ) | |
| else: | |
| step = delta * 255.0 / 100.0 | |
| hsv[:, :, idx][mask] = np.clip(hsv[:, :, idx][mask] + step, 0, 255) | |
| explanation = f"Canale {channel}: variazione {delta:+.0f} punti percentuali, con limite 0-100%." | |
| result = cv2.cvtColor(np.clip(hsv, 0, 255).astype(np.uint8), cv2.COLOR_HSV2RGB) | |
| new_value = {"background": result, "layers": [], "composite": result} | |
| selected = int(np.count_nonzero(mask)) | |
| return new_value, result, f"Modificati {selected} pixel. {explanation} Per sommare un altro passaggio, dipingere di nuovo e premere ancora." | |
| # ----------------------------------------------------------------------------- | |
| # 9-11: quanto verde? | |
| # ----------------------------------------------------------------------------- | |
| def automatic_kernel(image: np.ndarray) -> int: | |
| return 3 if min(image.shape[:2]) < 1200 else 5 | |
| def clean_mask_auto(mask: np.ndarray, image: np.ndarray) -> np.ndarray: | |
| kernel_size = automatic_kernel(image) | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) | |
| opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) | |
| return cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel) | |
| def hsv_bounds_to_cv(h_min, h_max, s_min, s_max, v_min, v_max): | |
| low = np.array([ | |
| np.clip(float(h_min) / 2.0, 0, 179), | |
| np.clip(float(s_min) * 255.0 / 100.0, 0, 255), | |
| np.clip(float(v_min) * 255.0 / 100.0, 0, 255), | |
| ], dtype=np.uint8) | |
| high = np.array([ | |
| np.clip(float(h_max) / 2.0, 0, 179), | |
| np.clip(float(s_max) * 255.0 / 100.0, 0, 255), | |
| np.clip(float(v_max) * 255.0 / 100.0, 0, 255), | |
| ], dtype=np.uint8) | |
| return low, high | |
| def hsv_mask(image: np.ndarray, h_min, h_max, s_min, s_max, v_min, v_max): | |
| hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV) | |
| low, high = hsv_bounds_to_cv(h_min, h_max, s_min, s_max, v_min, v_max) | |
| if low[0] <= high[0]: | |
| mask = cv2.inRange(hsv, low, high) | |
| else: | |
| # Intervallo H che attraversa 360°/0°. | |
| low_a = low.copy(); high_a = high.copy() | |
| high_a[0] = 179 | |
| low_b = low.copy(); high_b = high.copy() | |
| low_b[0] = 0 | |
| mask = cv2.bitwise_or(cv2.inRange(hsv, low_a, high_a), cv2.inRange(hsv, low_b, high_b)) | |
| return clean_mask_auto(mask, image) | |
| def exg_mask(image: np.ndarray, threshold: float): | |
| values = image.astype(np.float32) | |
| exg = 2.0 * values[:, :, 1] - values[:, :, 0] - values[:, :, 2] | |
| mask = np.where(exg > float(threshold), 255, 0).astype(np.uint8) | |
| return clean_mask_auto(mask, image) | |
| def build_overlay(image: np.ndarray, mask: np.ndarray, tint=(0, 235, 70)) -> np.ndarray: | |
| overlay = image.copy() | |
| color = np.zeros_like(image) | |
| color[:, :] = np.array(tint, dtype=np.uint8) | |
| overlay[mask > 0] = cv2.addWeighted(image[mask > 0], 0.55, color[mask > 0], 0.45, 0) | |
| return overlay | |
| def mask_result(image: Any, mask: np.ndarray, label: str): | |
| rgb = ensure_rgb(image) | |
| overlay = build_overlay(rgb, mask) | |
| pixels = int(np.count_nonzero(mask)) | |
| pct = 100.0 * pixels / mask.size | |
| message = ( | |
| f"{label}: {pixels:,} pixel selezionati ({pct:.1f}% dell'immagine). " | |
| f"Pulizia automatica con kernel {automatic_kernel(rgb)}×{automatic_kernel(rgb)}." | |
| ).replace(",", ".") | |
| return cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB), overlay, message, mask | |
| def default_hsv_outputs(image: Any): | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, "Caricare un'immagine.", None | |
| mask = hsv_mask(rgb, **DEFAULT_HSV) | |
| return mask_result(rgb, mask, "Metodo cromatico HSV") | |
| def custom_hsv_outputs(image, h_min, h_max, s_min, s_max, v_min, v_max): | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, "Caricare un'immagine.", None | |
| mask = hsv_mask(rgb, h_min, h_max, s_min, s_max, v_min, v_max) | |
| return mask_result(rgb, mask, "HSV con soglie modificate") | |
| def exg_outputs(image, threshold): | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, "Caricare un'immagine.", None | |
| mask = exg_mask(rgb, threshold) | |
| return mask_result(rgb, mask, f"ExG > {float(threshold):.0f}") | |
| def parse_estimates(text: str): | |
| values = [] | |
| for token in str(text or "").replace(";", ",").split(","): | |
| try: | |
| values.append(float(token.strip().replace("%", ""))) | |
| except ValueError: | |
| continue | |
| return values | |
| def estimate_green(image, estimates): | |
| mask_img, overlay, message, raw_mask = default_hsv_outputs(image) | |
| if raw_mask is None: | |
| return mask_img, overlay, message | |
| pct = 100.0 * float(np.count_nonzero(raw_mask)) / raw_mask.size | |
| values = parse_estimates(estimates) | |
| if values: | |
| differences = [abs(value - pct) for value in values] | |
| best = min(range(len(values)), key=differences.__getitem__) | |
| message += f" Migliore stima: {values[best]:.1f}% (scarto {differences[best]:.1f} punti)." | |
| return mask_img, overlay, message | |
| # ----------------------------------------------------------------------------- | |
| # 11-14: confronto progressivo fra HSV, ExG e FastSAM | |
| # ----------------------------------------------------------------------------- | |
| def load_fastsam_cpu(): | |
| """Carica una sola volta il modello, come nello Space consolidato.""" | |
| from ultralytics import FastSAM | |
| return FastSAM("FastSAM-s.pt") | |
| def run_fastsam_on_click(image: Any, evt: gr.SelectData): | |
| """Esegue FastSAM direttamente sul punto cliccato, su CPU Basic.""" | |
| rgb = ensure_rgb(image) | |
| if rgb is None: | |
| return None, None, "Caricare un'immagine.", None | |
| try: | |
| x, y = map(int, evt.index) | |
| except Exception: | |
| return None, None, "Impossibile leggere il punto selezionato.", None | |
| h, w = rgb.shape[:2] | |
| x = min(max(x, 0), w - 1) | |
| y = min(max(y, 0), h - 1) | |
| try: | |
| model = load_fastsam_cpu() | |
| # Stesso schema dello Space consolidato: prompt puntuale e inferenza CPU. | |
| results = model.predict( | |
| rgb, | |
| points=[[x, y]], | |
| labels=[1], | |
| device="cpu", | |
| verbose=False, | |
| ) | |
| if not results or results[0].masks is None or len(results[0].masks.data) == 0: | |
| return None, None, "FastSAM non ha trovato un oggetto associato al punto.", None | |
| data = results[0].masks.data.detach().float().cpu().numpy() | |
| # Di norma il prompt restituisce una sola maschera. Se ne arrivano più di una, | |
| # si sceglie la più piccola fra quelle che contengono il punto cliccato. | |
| candidates = [] | |
| for raw in data: | |
| candidate = cv2.resize( | |
| raw, (w, h), interpolation=cv2.INTER_NEAREST | |
| ) > 0.5 | |
| if candidate[y, x]: | |
| 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 | |
| except Exception as exc: | |
| return None, None, f"FastSAM non disponibile: {exc}", None | |
| overlay = build_overlay(rgb, mask, tint=(205, 35, 205)) | |
| cv2.circle(overlay, (x, y), max(6, round(min(h, w) / 100)), (255, 30, 30), -1) | |
| pixels = int(np.count_nonzero(mask)) | |
| pct = 100.0 * pixels / mask.size | |
| pixel_text = f"{pixels:,}".replace(",", ".") | |
| message = ( | |
| f"FastSAM: {pixel_text} pixel ({pct:.1f}%) nella regione associata " | |
| f"al punto x={x}, y={y}." | |
| ) | |
| overlay_state = overlay.copy() | |
| return cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB), overlay, message, overlay_state | |
| def compare_overlays(color_overlay, ai_overlay): | |
| if color_overlay is None or ai_overlay is None: | |
| return None | |
| first = ensure_rgb(color_overlay) | |
| second = ensure_rgb(ai_overlay) | |
| h = min(first.shape[0], second.shape[0]) | |
| a = cv2.resize(first, (round(first.shape[1] * h / first.shape[0]), h)) | |
| b = cv2.resize(second, (round(second.shape[1] * h / second.shape[0]), h)) | |
| return np.concatenate([a, b], axis=1) | |
| # ----------------------------------------------------------------------------- | |
| # Interfaccia | |
| # ----------------------------------------------------------------------------- | |
| with gr.Blocks(title="Computer vision e piante", theme=gr.themes.Soft(primary_hue="green"), css=CSS) as demo: | |
| gr.Markdown("# Computer vision e piante - percorsi 6-14 anni") | |
| gr.Markdown( | |
| "Lo Space raccoglie tre moduli distinti. Il dataset sperimentale completo rimane disponibile " | |
| "per le attività che richiedono fotografie già acquisite." | |
| ) | |
| with gr.Tab("6-9"): | |
| gr.Markdown("## Piante a quadretti") | |
| gr.HTML( | |
| '<div class="note">Il programma prepara una scheda PDF da stampare. ' | |
| "Non evidenzia automaticamente la pianta e non assegna una risposta corretta.</div>" | |
| ) | |
| with gr.Row(): | |
| q_image = gr.Image(type="numpy", label="Fotografia", interactive=True) | |
| with gr.Column(): | |
| q_condition = gr.Radio(CONDITIONS, value="NS", label="Condizione dataset") | |
| q_replicate = gr.Radio(REPLICATES, value="A", label="Replica") | |
| q_day = gr.Dropdown(DAYS, value="06", label="Giorno") | |
| q_load = gr.Button("Carica dal dataset") | |
| q_status = gr.Textbox(label="Stato", interactive=False) | |
| q_title = gr.Textbox(value="Piante a quadretti", label="Titolo dell'attività") | |
| with gr.Row(): | |
| q_rows = gr.Slider(4, 36, value=12, step=1, label="Righe") | |
| q_cols = gr.Slider(4, 36, value=12, step=1, label="Colonne") | |
| q_light = gr.Slider(0, 1.0, value=0.50, step=0.05, label="Schiarimento") | |
| q_color = gr.Checkbox(value=False, label="Mantieni la fotografia a colori") | |
| q_button = gr.Button("Genera la scheda PDF", variant="primary") | |
| q_message = gr.Textbox(label="Stato generazione", interactive=False) | |
| with gr.Row(): | |
| q_preview = gr.Image(label="Anteprima del PDF", interactive=False) | |
| q_pdf = gr.File(label="Scarica il PDF", interactive=False) | |
| q_load.click(load_dataset_image, [q_condition, q_replicate, q_day], [q_image, q_status]) | |
| q_button.click( | |
| create_grid_pdf, | |
| [q_image, q_rows, q_cols, q_light, q_color, q_title], | |
| [q_preview, q_pdf, q_message], | |
| ) | |
| gr.Markdown("### Attività con la famiglia\nDa definire con il personale di progetto.") | |
| with gr.Tab("9-11"): | |
| with gr.Tab("A scuola - Pixel"): | |
| gr.Markdown("## Esplora una piccola matrice di pixel") | |
| gr.HTML( | |
| '<div class="info">Fare click sull’immagine per collocare un selettore quadrato. ' | |
| "La zona scelta resta visibile sull’originale, viene ingrandita e viene tradotta in una matrice numerica.</div>" | |
| ) | |
| p_coords = gr.State([None, None]) | |
| with gr.Row(): | |
| p_source = gr.Image(type="numpy", label="Fotografia - fare click per selezionare l'area", interactive=True) | |
| p_marked = gr.Image(label="Fotografia con selettore", interactive=False) | |
| with gr.Row(): | |
| p_size = gr.Radio([3, 5], value=5, label="Dimensione del selettore") | |
| p_space = gr.Radio(["RGB", "HSV"], value="RGB", label="Spazio di colore") | |
| p_channel = gr.Dropdown(["R", "G", "B"], value="R", label="Canale") | |
| p_status = gr.Textbox(label="Lettura", interactive=False) | |
| with gr.Row(): | |
| p_zoom = gr.Image(label="Area ingrandita", interactive=False) | |
| p_matrix = gr.Dataframe( | |
| value=[], | |
| datatype="number", | |
| type="array", | |
| label="Matrice dei valori", | |
| interactive=False, | |
| wrap=True, | |
| max_height=420, | |
| ) | |
| p_source.select( | |
| select_pixel_region, | |
| [p_source, p_size, p_space, p_channel], | |
| [p_marked, p_zoom, p_matrix, p_status, p_coords], | |
| show_progress="hidden", | |
| ) | |
| p_space.change(channel_choices, p_space, p_channel, show_progress="hidden").then( | |
| refresh_pixel_region, | |
| [p_source, p_size, p_space, p_channel, p_coords], | |
| [p_marked, p_zoom, p_matrix, p_status, p_coords], | |
| show_progress="hidden", | |
| ) | |
| for control in [p_size, p_channel]: | |
| control.change( | |
| refresh_pixel_region, | |
| [p_source, p_size, p_space, p_channel, p_coords], | |
| [p_marked, p_zoom, p_matrix, p_status, p_coords], | |
| show_progress="hidden", | |
| ) | |
| gr.Markdown("## Laboratorio del colore") | |
| gr.HTML( | |
| '<div class="info">Dipingere una zona, scegliere un canale e applicare una variazione additiva. ' | |
| "Ogni pressione del pulsante aggiunge o sottrae il valore scelto. Nel canale H il percorso è circolare.</div>" | |
| ) | |
| with gr.Row(): | |
| b_source = gr.Image(type="numpy", label="Immagine sorgente", interactive=True) | |
| with gr.Column(): | |
| b_load = gr.Button("Carica nel pennello") | |
| b_space = gr.Radio(["RGB", "HSV"], value="RGB", label="Spazio di colore") | |
| b_channel = gr.Dropdown(["R", "G", "B"], value="G", label="Canale da modificare") | |
| b_delta = gr.Slider(-100, 100, value=20, step=5, label="Variazione additiva") | |
| b_apply = gr.Button("Applica la modifica alla zona dipinta", variant="primary") | |
| with gr.Row(): | |
| b_editor = gr.ImageEditor( | |
| type="numpy", | |
| label="Pennello", | |
| brush=gr.Brush(default_size=45, colors=[("#ff0000", 0.65)], default_color=("#ff0000", 0.65), color_mode="fixed"), | |
| eraser=gr.Eraser(default_size=45), | |
| layers=gr.LayerOptions(allow_additional_layers=False, layers=["Zona da modificare"]), | |
| transforms=(), | |
| interactive=True, | |
| ) | |
| b_result = gr.Image(label="Risultato", interactive=False) | |
| b_message = gr.Textbox(label="Risultato della trasformazione", interactive=False) | |
| b_space.change(channel_choices, b_space, b_channel, show_progress="hidden") | |
| b_load.click(editor_value_from_image, b_source, [b_editor, b_result, b_message]) | |
| b_apply.click( | |
| apply_additive_brush, | |
| [b_editor, b_space, b_channel, b_delta], | |
| [b_editor, b_result, b_message], | |
| ) | |
| with gr.Tab("Con la famiglia - Quanto verde?"): | |
| gr.Markdown("## Quanto verde vedi?") | |
| gr.HTML( | |
| '<div class="note">Il valore prodotto dal programma è una stima automatica ottenuta con soglie HSV. ' | |
| "La maschera mostra quali pixel sono stati inclusi e permette di discutere gli errori.</div>" | |
| ) | |
| with gr.Row(): | |
| g_image = gr.Image(type="numpy", label="Fotografia orizzontale d'insieme", interactive=True) | |
| with gr.Column(): | |
| g_estimates = gr.Textbox(label="Stime separate da virgole (%)", placeholder="30, 45, 50") | |
| g_button = gr.Button("Calcola la stima automatica", variant="primary") | |
| g_message = gr.Textbox(label="Risultato", interactive=False) | |
| with gr.Row(): | |
| g_mask = gr.Image(label="Maschera HSV", interactive=False) | |
| g_overlay = gr.Image(label="Sovrapposizione", interactive=False) | |
| g_button.click(estimate_green, [g_image, g_estimates], [g_mask, g_overlay, g_message]) | |
| with gr.Tab("11-14"): | |
| gr.Markdown("## Confronta modi diversi di segmentare una pianta") | |
| gr.Markdown( | |
| "Caricare la fotografia della caccia alla pianta segmentabile oppure scegliere un'immagine del dataset. " | |
| "La stessa immagine viene utilizzata in tutti i passaggi." | |
| ) | |
| c_color_overlay_state = gr.State(None) | |
| c_ai_overlay_state = gr.State(None) | |
| with gr.Row(): | |
| c_image = gr.Image(type="numpy", label="Fotografia sorgente - per FastSAM fare click sulla pianta", interactive=True) | |
| with gr.Column(): | |
| c_condition = gr.Radio(CONDITIONS, value="NS", label="Condizione dataset") | |
| c_replicate = gr.Radio(REPLICATES, value="A", label="Replica") | |
| c_day = gr.Dropdown(DAYS, value="14", label="Giorno") | |
| c_load = gr.Button("Carica dal dataset") | |
| c_load_status = gr.Textbox(label="Stato", interactive=False) | |
| c_load.click(load_dataset_image, [c_condition, c_replicate, c_day], [c_image, c_load_status]) | |
| gr.Markdown("### 1. Applica il metodo cromatico") | |
| gr.HTML( | |
| '<div class="note">Il pulsante usa impostazioni HSV predefinite e pulisce automaticamente la maschera. ' | |
| "Non è necessario regolare parametri per il primo tentativo.</div>" | |
| ) | |
| c_default_button = gr.Button("Applica il metodo cromatico", variant="primary") | |
| with gr.Row(): | |
| c_default_mask = gr.Image(label="Maschera HSV", interactive=False) | |
| c_default_overlay = gr.Image(label="Sovrapposizione HSV", interactive=False) | |
| c_default_message = gr.Textbox(label="Risultato HSV", interactive=False) | |
| c_default_raw = gr.State(None) | |
| c_default_button.click( | |
| default_hsv_outputs, | |
| c_image, | |
| [c_default_mask, c_default_overlay, c_default_message, c_default_raw], | |
| ).then(lambda x: x, c_default_overlay, c_color_overlay_state) | |
| with gr.Accordion("Prova a modificare le soglie nello spazio HSV", open=False): | |
| gr.Markdown( | |
| "H descrive la tonalità lungo un cerchio da 0° a 360°. S descrive la saturazione e V la luminosità. " | |
| "Modificando i limiti cambia l'insieme dei pixel accettati." | |
| ) | |
| with gr.Row(): | |
| c_h_min = gr.Slider(0, 360, value=DEFAULT_HSV["h_min"], step=2, label="H minimo (°)") | |
| c_h_max = gr.Slider(0, 360, value=DEFAULT_HSV["h_max"], step=2, label="H massimo (°)") | |
| with gr.Row(): | |
| c_s_min = gr.Slider(0, 100, value=DEFAULT_HSV["s_min"], step=1, label="S minimo (%)") | |
| c_s_max = gr.Slider(0, 100, value=DEFAULT_HSV["s_max"], step=1, label="S massimo (%)") | |
| c_v_min = gr.Slider(0, 100, value=DEFAULT_HSV["v_min"], step=1, label="V minimo (%)") | |
| c_v_max = gr.Slider(0, 100, value=DEFAULT_HSV["v_max"], step=1, label="V massimo (%)") | |
| c_custom_button = gr.Button("Applica le soglie HSV modificate") | |
| with gr.Row(): | |
| c_custom_mask = gr.Image(label="Maschera HSV modificata", interactive=False) | |
| c_custom_overlay = gr.Image(label="Sovrapposizione HSV modificata", interactive=False) | |
| c_custom_message = gr.Textbox(label="Risultato soglie", interactive=False) | |
| c_custom_raw = gr.State(None) | |
| c_custom_button.click( | |
| custom_hsv_outputs, | |
| [c_image, c_h_min, c_h_max, c_s_min, c_s_max, c_v_min, c_v_max], | |
| [c_custom_mask, c_custom_overlay, c_custom_message, c_custom_raw], | |
| ).then(lambda x: x, c_custom_overlay, c_color_overlay_state) | |
| with gr.Accordion("Prova l'indice ExG", open=False): | |
| gr.HTML( | |
| '<div class="info"><b>ExG = 2G - R - B.</b> Il programma calcola questo valore per ogni pixel ' | |
| "e conserva i pixel per i quali ExG supera la soglia scelta.</div>" | |
| ) | |
| c_exg_threshold = gr.Slider(-100, 200, value=35, step=1, label="Soglia ExG") | |
| c_exg_button = gr.Button("Applica ExG") | |
| with gr.Row(): | |
| c_exg_mask = gr.Image(label="Maschera ExG", interactive=False) | |
| c_exg_overlay = gr.Image(label="Sovrapposizione ExG", interactive=False) | |
| c_exg_message = gr.Textbox(label="Risultato ExG", interactive=False) | |
| c_exg_raw = gr.State(None) | |
| c_exg_button.click( | |
| exg_outputs, | |
| [c_image, c_exg_threshold], | |
| [c_exg_mask, c_exg_overlay, c_exg_message, c_exg_raw], | |
| ).then(lambda x: x, c_exg_overlay, c_color_overlay_state) | |
| with gr.Accordion("Adesso prova FastSAM", open=False): | |
| gr.HTML( | |
| '<div class="info">FastSAM cerca i pixel che formano una figura coerente con il punto indicato ' | |
| "e distinta dal resto dell'immagine. Aprire questo pannello, poi fare click direttamente sulla " | |
| "<b>fotografia sorgente</b> mostrata in alto. Non è necessario caricare una seconda immagine. " | |
| "L'elaborazione parte dal click e può richiedere alcuni secondi su CPU.</div>" | |
| ) | |
| with gr.Row(): | |
| c_ai_mask = gr.Image(label="Maschera FastSAM", interactive=False) | |
| c_ai_overlay = gr.Image(label="Sovrapposizione FastSAM", interactive=False) | |
| c_ai_message = gr.Textbox( | |
| value="Aprire il pannello e fare click sulla pianta nella fotografia sorgente in alto.", | |
| label="Risultato FastSAM", | |
| interactive=False, | |
| ) | |
| # Come nello Space consolidato, FastSAM parte dal click su un componente | |
| # che contiene già l'immagine. Qui si usa direttamente la fotografia sorgente: | |
| # si elimina così la copia intermedia che in alcune versioni di Gradio rimaneva vuota. | |
| c_image.select( | |
| run_fastsam_on_click, | |
| inputs=[c_image], | |
| outputs=[c_ai_mask, c_ai_overlay, c_ai_message, c_ai_overlay_state], | |
| concurrency_limit=1, | |
| ) | |
| c_image.change( | |
| lambda: (None, None, "Aprire il pannello e fare click sulla pianta nella fotografia sorgente in alto.", None), | |
| inputs=None, | |
| outputs=[c_ai_mask, c_ai_overlay, c_ai_message, c_ai_overlay_state], | |
| show_progress="hidden", | |
| ) | |
| gr.Markdown("### Confronto finale") | |
| c_compare_button = gr.Button("Confronta l'ultimo metodo cromatico con FastSAM") | |
| c_compare = gr.Image(label="Metodo cromatico | FastSAM", interactive=False) | |
| c_compare_button.click(compare_overlays, [c_color_overlay_state, c_ai_overlay_state], c_compare) | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=4).launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| ) | |