Computer_vision_piante_scuole / advanced_14_16.py
wlatt's picture
Update advanced_14_16.py
deb52a7 verified
Raw
History Blame Contribute Delete
32.3 kB
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)