wlatt's picture
Upload 81 files
d5e46bc verified
Raw
History Blame Contribute Delete
49.9 kB
from __future__ import annotations
import os
import tempfile
from pathlib import Path
from typing import Any
os.environ.setdefault("YOLO_CONFIG_DIR", "/tmp/Ultralytics")
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
from fastsam_shared import load_fastsam_cpu
from advanced_14_16 import build_14_16_tab
try:
import spaces
except ImportError: # Consente test locali fuori da Hugging Face.
class _SpacesFallback:
@staticmethod
def GPU(*args, **kwargs):
def decorator(func):
return func
return decorator
spaces = _SpacesFallback()
DATASET_DIR = Path("dataset")
DAYS = ["06", "09", "11", "14", "16", "19", "23", "26"]
CONDITIONS = ["NS", "S"]
REPLICATES = ["A", "B", "C"]
@spaces.GPU(duration=1)
def _zerogpu_runtime_anchor():
"""Ancora richiesta dal runtime ZeroGPU; non è collegata all’interfaccia."""
return "ready"
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}
.fastsam-instruction {background:#e8f3ff;border:2px solid #245f91;border-radius:8px;padding:12px 14px;margin:10px 0;font-size:1.02rem}
.compact p {margin:0.35rem 0}
.matrix-help {font-size:0.95rem}
.action-button button, button.action-button {
background:#1f6f43 !important; color:#ffffff !important; border:1px solid #155333 !important;
font-weight:700 !important; border-radius:7px !important; min-height:42px !important;
}
.action-button button:hover, button.action-button:hover {background:#155333 !important}
.action-button button:disabled, button.action-button:disabled {background:#8ca79a !important;color:#f6f6f6 !important}
.result-card img {border-radius:12px !important}
.workflow-panel {border:1px solid #cbd8cf !important;border-radius:10px !important;padding:14px !important;margin:10px 0 18px 0 !important;background:#ffffff !important}
.manual-panel {border:1px solid #9fb3a7 !important;border-left:5px solid #6f8f7a !important;border-radius:8px !important;padding:14px !important;margin:14px 0 !important;background:#f8faf8 !important}
.panel-kicker {font-weight:800;color:#17653a;letter-spacing:.04em;font-size:1.02rem;margin-bottom:8px}
.step-strip {display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 18px 0}
.step-box {display:flex;align-items:center;gap:8px;min-width:145px;flex:1;background:#edf6ef;border:1px solid #b9d2c0;border-radius:9px;padding:9px 12px;color:#194e31}
.step-box span {display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#1f6f43;color:white;font-weight:800}
.guide-panel,.docs-panel {background:#f7faf8;border:1px solid #d2ddd5;border-radius:10px;padding:14px;margin:10px 0;line-height:1.42}
.guide-panel h3,.docs-panel h3 {color:#17653a;margin-top:0}
.metrics-wrap {margin:8px 0 12px}.metrics-title{font-weight:800;color:#194e31;margin-bottom:7px}.metric-grid{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px}.metric-card{border:1px solid #d4ded7;border-radius:8px;padding:10px;background:#f8fbf9}.metric-card span{display:block;font-size:.82rem;color:#596660}.metric-card strong{display:block;font-size:1.35rem;color:#153c28;margin-top:3px}
@media(max-width:900px){.metric-grid{grid-template-columns:repeat(2,minmax(120px,1fr))}.step-box{min-width:45%}}
"""
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 _load_ui_font(size: int, bold: bool = False):
candidates = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
]
for candidate in candidates:
if Path(candidate).exists():
return ImageFont.truetype(candidate, size=size)
return ImageFont.load_default()
def _pixel_triplet(crop: np.ndarray, space: str) -> np.ndarray:
if space == "RGB":
return crop.astype(np.float32)
return didactic_hsv(crop)
def _pixel_label(values: np.ndarray, space: str) -> str:
if space == "RGB":
r, g, b = [int(round(v)) for v in values]
return f"R {r:3d}\nG {g:3d}\nB {b:3d}"
h, sat, val = values
return f"H {int(round(h)):3d}°\nS {int(round(sat)):3d}%\nV {int(round(val)):3d}%"
def build_annotated_pixel_patch(crop: np.ndarray, space: str, canvas_size: int = 800) -> np.ndarray:
"""Ingrandisce il patch 5x5 e scrive i tre valori del colore su ogni pixel."""
size = crop.shape[0]
cell = canvas_size // size
width = cell * size
height = cell * size
board = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(board)
values = _pixel_triplet(crop, space)
font = _load_ui_font(max(17, cell // 8), bold=True)
for row in range(size):
for col in range(size):
x0, y0 = col * cell, row * cell
x1, y1 = x0 + cell, y0 + cell
color = tuple(int(v) for v in crop[row, col])
draw.rectangle((x0, y0, x1, y1), fill=color, outline=(255, 255, 255), width=2)
label = _pixel_label(values[row, col], space)
bbox = draw.multiline_textbbox((0, 0), label, font=font, spacing=2, align="center")
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
pad_x, pad_y = 8, 5
bx0 = x0 + (cell - tw) // 2 - pad_x
by0 = y0 + (cell - th) // 2 - pad_y
bx1 = bx0 + tw + 2 * pad_x
by1 = by0 + th + 2 * pad_y
overlay = Image.new("RGBA", board.size, (0, 0, 0, 0))
od = ImageDraw.Draw(overlay)
od.rounded_rectangle((bx0, by0, bx1, by1), radius=8, fill=(0, 0, 0, 155))
board = Image.alpha_composite(board.convert("RGBA"), overlay).convert("RGB")
draw = ImageDraw.Draw(board)
draw.multiline_text(
(x0 + cell / 2, y0 + cell / 2),
label,
font=font,
fill=(255, 255, 255),
anchor="mm",
align="center",
spacing=2,
)
return np.asarray(board)
def render_pixel_selection(
image: Any,
space: str,
coords: list[int] | tuple[int, int] | None,
):
rgb = ensure_rgb(image)
size = 5
if rgb is None:
return None, "Caricare una fotografia una sola volta nel pannello superiore.", coords
if not coords or coords[0] is None:
return None, "Fare click sulla fotografia per scegliere il centro del patch 5×5.", 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, size, w, h)
crop = rgb[y0:y1, x0:x1]
if crop.shape[:2] != (size, size):
return None, "Selezione troppo vicina al bordo: scegliere un punto leggermente più interno.", [x, y]
patch = build_annotated_pixel_patch(crop, space, canvas_size=800)
status = (
f"Patch 5×5 centrato vicino a x={x}, y={y}. "
+ ("Ogni cella mostra R, G e B (0-255)." if space == "RGB" else "Ogni cella mostra H in gradi, S e V in percentuale.")
)
return patch, status, [x, y]
def select_pixel_region(image, space, evt: gr.SelectData):
try:
x, y = map(int, evt.index)
except Exception:
return None, "Impossibile leggere il punto selezionato.", [None, None]
return render_pixel_selection(image, space, [x, y])
def refresh_pixel_region(image, space, coords):
return render_pixel_selection(image, space, 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 green_focus_overlay(image: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Mantiene invariata la zona verde e rende il resto scuro e quasi monocromatico."""
rgb = ensure_rgb(image)
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
dark = np.clip(gray.astype(np.float32) * 0.23, 0, 255).astype(np.uint8)
dark_rgb = cv2.cvtColor(dark, cv2.COLOR_GRAY2RGB)
# Una lieve tinta fredda rende la zona esclusa più leggibile senza competere con il verde.
dark_rgb = np.clip(dark_rgb.astype(np.int16) + np.array([0, 2, 5], dtype=np.int16), 0, 255).astype(np.uint8)
out = dark_rgb
out[mask > 0] = rgb[mask > 0]
return out
def _draw_crown(draw: ImageDraw.ImageDraw, center_x: int, top_y: int, scale: int = 1):
w, h = 62 * scale, 38 * scale
x0 = center_x - w // 2
points = [
(x0, top_y + h),
(x0 + 7 * scale, top_y + 11 * scale),
(x0 + 20 * scale, top_y + 23 * scale),
(x0 + 31 * scale, top_y),
(x0 + 43 * scale, top_y + 23 * scale),
(x0 + 56 * scale, top_y + 11 * scale),
(x0 + w, top_y + h),
]
draw.polygon(points, fill=(246, 190, 35), outline=(151, 103, 0))
draw.rectangle((x0 + 4 * scale, top_y + h - 7 * scale, x0 + w - 4 * scale, top_y + h + 2 * scale), fill=(246, 190, 35), outline=(151, 103, 0))
def comparison_card(image: np.ndarray, mask: np.ndarray, pct: float, label: str, winner: bool, tie: bool = False) -> np.ndarray:
overlay = green_focus_overlay(image, mask)
card_w = 760
photo_h = 500
header_h = 155
card = Image.new("RGB", (card_w, header_h + photo_h), (248, 250, 249))
draw = ImageDraw.Draw(card)
title_font = _load_ui_font(25, bold=True)
pct_font = _load_ui_font(64, bold=(winner or tie))
badge_font = _load_ui_font(19, bold=True)
draw.text((28, 18), label, font=title_font, fill=(42, 55, 48))
pct_text = f"{pct:.1f}%"
pct_bbox = draw.textbbox((0, 0), pct_text, font=pct_font)
pct_w = pct_bbox[2] - pct_bbox[0]
px = (card_w - pct_w) // 2
if winner:
badge = (px - 28, 48, px + pct_w + 28, 132)
draw.rounded_rectangle(badge, radius=22, fill=(255, 235, 128), outline=(222, 171, 20), width=4)
_draw_crown(draw, min(card_w - 48, px + pct_w + 62), 54, scale=1)
elif tie:
badge = (px - 28, 48, px + pct_w + 28, 132)
draw.rounded_rectangle(badge, radius=22, fill=(229, 238, 232), outline=(112, 138, 120), width=3)
draw.text((card_w // 2, 88), pct_text, font=pct_font, fill=(24, 57, 35), anchor="mm")
if winner:
draw.text((card_w // 2, 137), "PIÙ VERDE", font=badge_font, fill=(105, 75, 0), anchor="mm")
elif tie:
draw.text((card_w // 2, 137), "PARITÀ", font=badge_font, fill=(65, 83, 70), anchor="mm")
pil = Image.fromarray(overlay)
scale = min(card_w / pil.width, photo_h / pil.height)
nw, nh = max(1, round(pil.width * scale)), max(1, round(pil.height * scale))
pil = pil.resize((nw, nh), Image.Resampling.LANCZOS)
bg = Image.new("RGB", (card_w, photo_h), (14, 18, 16))
bg.paste(pil, ((card_w - nw) // 2, (photo_h - nh) // 2))
card.paste(bg, (0, header_h))
return np.asarray(card)
def compare_green_pair(image_a: Any, image_b: Any):
a = ensure_rgb(image_a)
b = ensure_rgb(image_b)
if a is None or b is None:
msg = "Caricare entrambe le fotografie prima di avviare il confronto."
return (
gr.update(value=None, visible=False),
gr.update(value=None, visible=False),
gr.update(value=None, visible=False),
gr.update(value=None, visible=False),
msg,
)
mask_a = hsv_mask(a, **DEFAULT_HSV)
mask_b = hsv_mask(b, **DEFAULT_HSV)
pct_a = 100.0 * float(np.count_nonzero(mask_a)) / mask_a.size
pct_b = 100.0 * float(np.count_nonzero(mask_b)) / mask_b.size
tie = abs(pct_a - pct_b) < 0.05
winner_a = pct_a > pct_b and not tie
winner_b = pct_b > pct_a and not tie
card_a = comparison_card(a, mask_a, pct_a, "Fotografia A", winner_a, tie)
card_b = comparison_card(b, mask_b, pct_b, "Fotografia B", winner_b, tie)
mask_a_rgb = cv2.cvtColor(mask_a, cv2.COLOR_GRAY2RGB)
mask_b_rgb = cv2.cvtColor(mask_b, cv2.COLOR_GRAY2RGB)
if tie:
msg = f"Parità: entrambe le fotografie contengono circa {pct_a:.1f}% di verde secondo le soglie HSV."
elif winner_a:
msg = f"Fotografia A contiene più verde: {pct_a:.1f}% contro {pct_b:.1f}% nella fotografia B."
else:
msg = f"Fotografia B contiene più verde: {pct_b:.1f}% contro {pct_a:.1f}% nella fotografia A."
return (
gr.update(value=card_a, visible=True),
gr.update(value=card_b, visible=True),
gr.update(value=mask_a_rgb, visible=True),
gr.update(value=mask_b_rgb, visible=True),
msg,
)
# -----------------------------------------------------------------------------
# 11-14: confronto progressivo fra HSV, ExG e FastSAM
# -----------------------------------------------------------------------------
def sync_fastsam_source(image: Any):
"""Sincronizza la copia cliccabile e azzera i risultati precedenti."""
rgb = ensure_rgb(image)
instruction = (
"1. Cliccare sulla pianta nell’immagine qui sotto. "
"2. Controllare il punto rosso. 3. Premere il pulsante di segmentazione."
)
return (
rgb,
None,
gr.update(value=None, visible=False),
gr.update(value=None, visible=False),
instruction,
None,
)
def select_fastsam_point(source_image: Any, evt: gr.SelectData):
"""Registra il punto senza avviare l’inferenza e lo mostra sulla copia locale."""
rgb = ensure_rgb(source_image)
if rgb is None:
return None, None, "Caricare prima una fotografia."
try:
x, y = map(int, evt.index)
except Exception:
return rgb, None, "Impossibile leggere il punto selezionato."
h, w = rgb.shape[:2]
x = min(max(x, 0), w - 1)
y = min(max(y, 0), h - 1)
marked = rgb.copy()
radius = max(7, round(min(h, w) / 90))
cv2.circle(marked, (x, y), radius, (255, 35, 35), -1)
cv2.circle(marked, (x, y), radius + 3, (255, 255, 255), 2)
return (
marked,
[x, y],
f"Punto registrato: x={x}, y={y}. Ora premere ‘Segmenta l’oggetto indicato’.",
)
def run_fastsam_cpu(image: Any, point: Any, progress=gr.Progress()):
"""Esegue FastSAM su CPU; nessuna quota ZeroGPU viene richiesta."""
hidden = gr.update(value=None, visible=False)
rgb = ensure_rgb(image)
if rgb is None:
return hidden, hidden, "Caricare un’immagine.", None
if not point or len(point) != 2 or point[0] is None or point[1] is None:
return hidden, hidden, "Cliccare prima sulla pianta nell’immagine FastSAM.", None
x, y = map(int, point)
h, w = rgb.shape[:2]
x = min(max(x, 0), w - 1)
y = min(max(y, 0), h - 1)
# Riduzione prudenziale: accelera la CPU e limita l’uso di memoria.
max_side = 1024
scale = min(1.0, max_side / max(h, w))
if scale < 1.0:
infer_rgb = cv2.resize(
rgb, (round(w * scale), round(h * scale)), interpolation=cv2.INTER_AREA
)
infer_x, infer_y = round(x * scale), round(y * scale)
else:
infer_rgb = rgb
infer_x, infer_y = x, y
try:
progress(0.05, desc="Caricamento di FastSAM su CPU")
model = load_fastsam_cpu()
progress(0.25, desc="Segmentazione dell’oggetto indicato")
results = model.predict(
infer_rgb,
points=[[infer_x, infer_y]],
labels=[1],
device="cpu",
imgsz=1024,
retina_masks=True,
verbose=False,
)
if not results or results[0].masks is None or len(results[0].masks.data) == 0:
return hidden, hidden, "FastSAM non ha trovato un oggetto associato al punto.", None
data = results[0].masks.data.detach().float().cpu().numpy()
candidates = []
ih, iw = infer_rgb.shape[:2]
for raw in data:
candidate_small = cv2.resize(raw, (iw, ih), interpolation=cv2.INTER_NEAREST) > 0.5
if candidate_small[infer_y, infer_x]:
candidates.append(candidate_small)
selected_small = (
min(candidates, key=lambda m: int(m.sum()))
if candidates
else cv2.resize(data[0], (iw, ih), interpolation=cv2.INTER_NEAREST) > 0.5
)
selected = cv2.resize(
selected_small.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST
) > 0
mask = selected.astype(np.uint8) * 255
except Exception as exc:
return hidden, hidden, f"FastSAM non disponibile: {exc}", None
progress(0.90, desc="Preparazione dei risultati")
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 su CPU: {pixel_text} pixel ({pct:.1f}%) nella regione associata "
f"al punto x={x}, y={y}."
)
mask_rgb = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB)
return (
gr.update(value=mask_rgb, visible=True),
gr.update(value=overlay, visible=True),
message,
overlay.copy(),
)
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-16 anni")
gr.Markdown(
"Lo Space raccoglie quattro percorsi progressivi. Il dataset sperimentale completo rimane condiviso "
"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", variant="primary", elem_classes=["action-button"])
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", elem_classes=["action-button"])
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 e colore"):
gr.Markdown("## Una fotografia, due attività")
gr.HTML(
'<div class="info"><b>Caricare la fotografia una sola volta.</b> '
"Lo stesso file viene usato sia per esplorare il patch 5×5 sia per il laboratorio del colore.</div>"
)
p_coords = gr.State([None, None])
p_source = gr.Image(
type="numpy",
label="Fotografia condivisa - fare click per scegliere il patch 5×5",
interactive=True,
height=440,
)
with gr.Group(elem_classes=["workflow-panel"]):
gr.Markdown("### 1. Esplora un patch 5×5")
gr.Markdown(
"Fare click sulla fotografia. Il programma ingrandisce esattamente 25 pixel e scrive "
"direttamente su ciascun pixel i valori del colore."
)
p_space = gr.Radio(["RGB", "HSV"], value="RGB", label="Numeri da mostrare")
p_status = gr.Textbox(label="Lettura", interactive=False)
p_patch = gr.Image(
label="Patch 5×5 ingrandito con valori sovrapposti",
interactive=False,
height=760,
)
p_source.select(
select_pixel_region,
[p_source, p_space],
[p_patch, p_status, p_coords],
show_progress="hidden",
)
p_space.change(
refresh_pixel_region,
[p_source, p_space, p_coords],
[p_patch, p_status, p_coords],
show_progress="hidden",
)
with gr.Group(elem_classes=["workflow-panel"]):
gr.Markdown("### 2. Laboratorio del colore")
gr.HTML(
'<div class="info">La fotografia caricata sopra viene trasferita automaticamente nel pennello. '
"Dipingere una zona, scegliere un canale e applicare una variazione additiva. "
"Nel canale H il percorso è circolare.</div>"
)
with gr.Row():
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")
with gr.Row():
b_reset = gr.Button("Ripristina la fotografia originale", variant="primary", elem_classes=["action-button"])
b_apply = gr.Button("Applica la modifica alla zona dipinta", variant="primary", elem_classes=["action-button"])
with gr.Row():
b_editor = gr.ImageEditor(
type="numpy",
label="Pennello - dipingere la zona da modificare",
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_reset.click(editor_value_from_image, p_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],
)
# Un solo caricamento iniziale alimenta automaticamente anche il laboratorio del colore.
p_source.change(
editor_value_from_image,
p_source,
[b_editor, b_result, b_message],
show_progress="hidden",
)
with gr.Tab("Con la famiglia - Quale foto contiene più verde?"):
gr.Markdown("## Quale fotografia contiene più verde?")
gr.HTML(
'<div class="note">Caricare due fotografie e formulare prima una previsione. '
"Il confronto usa le stesse soglie HSV per entrambe. Nelle immagini risultato la parte riconosciuta come verde "
"rimane invariata; il resto viene fortemente desaturato e scurito.</div>"
)
with gr.Row():
g_image_a = gr.Image(type="numpy", label="Fotografia A", interactive=True, height=380)
g_image_b = gr.Image(type="numpy", label="Fotografia B", interactive=True, height=380)
g_button = gr.Button(
"Confronta: quale fotografia contiene più verde?",
variant="primary",
elem_classes=["action-button"],
)
g_message = gr.Textbox(label="Risultato del confronto", interactive=False)
with gr.Row():
g_card_a = gr.Image(label="Fotografia A - risultato", interactive=False, visible=False)
g_card_b = gr.Image(label="Fotografia B - risultato", interactive=False, visible=False)
gr.Markdown("### Maschere utilizzate dal programma")
with gr.Row():
g_mask_a = gr.Image(label="Maschera A", interactive=False, visible=False)
g_mask_b = gr.Image(label="Maschera B", interactive=False, visible=False)
g_button.click(
compare_green_pair,
[g_image_a, g_image_b],
[g_card_a, g_card_b, g_mask_a, g_mask_b, 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."
)
gr.HTML('<div class="step-strip"><div class="step-box"><span>1</span><b>Caricamento</b></div><div class="step-box"><span>2</span><b>HSV</b></div><div class="step-box"><span>3</span><b>ExG</b></div><div class="step-box"><span>4</span><b>FastSAM</b></div><div class="step-box"><span>5</span><b>Confronto</b></div></div>')
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", 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", variant="primary", elem_classes=["action-button"])
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", elem_classes=["action-button"])
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.Group(elem_classes=["manual-panel"]):
gr.Markdown("### 2. Modifica le soglie nello spazio HSV")
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 · gradi")
c_h_max = gr.Slider(0, 360, value=DEFAULT_HSV["h_max"], step=2, label="H massimo · gradi")
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", variant="primary", elem_classes=["action-button"])
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.Group(elem_classes=["workflow-panel"]):
gr.Markdown("### 3. Prova l’indice ExG")
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", variant="primary", elem_classes=["action-button"])
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)
c_fastsam_point = gr.State(None)
with gr.Group(elem_classes=["workflow-panel"]):
gr.Markdown("### 4. Prova FastSAM")
gr.HTML(
'<div class="fastsam-instruction"><b>Procedura FastSAM</b><br>'
'1. Cliccare sulla pianta nell’immagine immediatamente sotto.<br>'
'2. Controllare che il punto rosso sia nella posizione desiderata.<br>'
'3. Premere <b>Segmenta l’oggetto indicato</b>.<br>'
'FastSAM viene eseguito su CPU e non utilizza la quota ZeroGPU.</div>'
)
c_fastsam_image = gr.Image(
type="numpy",
label="Immagine FastSAM — cliccare sulla pianta",
interactive=True,
height=430,
)
c_ai_message = gr.Textbox(
value=(
"1. Cliccare sulla pianta nell’immagine qui sopra. "
"2. Controllare il punto rosso. 3. Premere il pulsante."
),
label="Istruzioni e risultato FastSAM",
interactive=False,
)
c_ai_button = gr.Button(
"Segmenta l’oggetto indicato",
variant="primary",
elem_classes=["action-button"],
)
with gr.Row():
c_ai_mask = gr.Image(label="Maschera FastSAM", interactive=False, visible=False)
c_ai_overlay = gr.Image(label="Sovrapposizione FastSAM", interactive=False, visible=False)
# La copia locale elimina la necessità di tornare all’immagine in alto.
c_image.change(
sync_fastsam_source,
inputs=[c_image],
outputs=[
c_fastsam_image,
c_fastsam_point,
c_ai_mask,
c_ai_overlay,
c_ai_message,
c_ai_overlay_state,
],
show_progress="hidden",
)
c_fastsam_image.select(
select_fastsam_point,
inputs=[c_image],
outputs=[c_fastsam_image, c_fastsam_point, c_ai_message],
show_progress="hidden",
)
c_ai_button.click(
run_fastsam_cpu,
inputs=[c_image, c_fastsam_point],
outputs=[c_ai_mask, c_ai_overlay, c_ai_message, c_ai_overlay_state],
concurrency_limit=1,
)
gr.Markdown("### Confronto finale")
c_compare_button = gr.Button("Confronta l'ultimo metodo cromatico con FastSAM", variant="primary", elem_classes=["action-button"])
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)
build_14_16_tab(
load_dataset_image=load_dataset_image,
sync_fastsam_source=sync_fastsam_source,
select_fastsam_point=select_fastsam_point,
run_fastsam_cpu=run_fastsam_cpu,
load_fastsam_cpu=load_fastsam_cpu,
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=4).launch(
server_name="0.0.0.0",
server_port=7860,
ssr_mode=False,
)