Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee1c4a8f92 | |||
| 5002515b41 | |||
| 8029a1e12b | |||
| d37833076e | |||
| e1ed9206a3 | |||
| e84ae199ac | |||
| 5f0c4542d3 | |||
| 29c034fb05 | |||
| 6fb1efcab8 | |||
| 35df4c473c | |||
| 64f2c8b5dc | |||
| 7e076deb80 | |||
| 852597ed51 | |||
| a78884f950 | |||
| 543ae0f643 | |||
| a12574f3c5 | |||
| 110dc87b08 | |||
| 2bb2cf63cc | |||
| ea6a9163ad | |||
| 74a332a2dd | |||
| dae49eb4a3 |
+145
-6
@@ -127,6 +127,7 @@ class Match:
|
||||
scale: float
|
||||
score: float
|
||||
bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato)
|
||||
variant_idx: int = -1 # indice variante usata (per overlay coerente)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -512,8 +513,10 @@ class LineShapeMatcher:
|
||||
self.variants.clear()
|
||||
# Reset view list: template principale = view 0
|
||||
self._view_templates = [(gray.copy(), mask_full.copy())]
|
||||
# Invalida cache feature di refine: il template e cambiato.
|
||||
# Invalida cache: template/param cambiati → spread/feature obsoleti.
|
||||
self._refine_feat_cache = {}
|
||||
if hasattr(self, "_scene_cache"):
|
||||
self._scene_cache.clear()
|
||||
self._build_variants_for_view(gray, mask_full, view_idx=0)
|
||||
self._dedup_variants()
|
||||
return len(self.variants)
|
||||
@@ -669,6 +672,51 @@ class LineShapeMatcher:
|
||||
raw[b] = d.astype(np.float32)
|
||||
return raw
|
||||
|
||||
# --- Scene precompute cache (II Halcon-style) -----------------------
|
||||
_SCENE_CACHE_SIZE = 4
|
||||
|
||||
def _scene_cache_key(self, gray: np.ndarray) -> str | None:
|
||||
"""Hash compatto della scena + param che influenzano spread/density.
|
||||
|
||||
Hash su prime 64KB della scena (sufficiente discriminante per
|
||||
scene fotografiche) + parametri matcher rilevanti. None se cache
|
||||
disabilitata (es. scene troppo piccole).
|
||||
"""
|
||||
if gray.size < 100:
|
||||
return None
|
||||
try:
|
||||
import hashlib
|
||||
h = hashlib.md5()
|
||||
sample = gray.tobytes()[:65536]
|
||||
h.update(sample)
|
||||
h.update(f"|{gray.shape}|{gray.dtype}".encode())
|
||||
h.update(
|
||||
f"|{self.weak_grad}|{self.strong_grad}"
|
||||
f"|{self.spread_radius}|{self._n_bins}"
|
||||
f"|{self.pyramid_levels}".encode()
|
||||
)
|
||||
return h.hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _scene_cache_get(self, key: str) -> tuple | None:
|
||||
cache = getattr(self, "_scene_cache", None)
|
||||
if cache is None:
|
||||
return None
|
||||
v = cache.get(key)
|
||||
if v is not None:
|
||||
cache.move_to_end(key)
|
||||
return v
|
||||
|
||||
def _scene_cache_put(self, key: str, value: tuple) -> None:
|
||||
from collections import OrderedDict
|
||||
if not hasattr(self, "_scene_cache"):
|
||||
self._scene_cache = OrderedDict()
|
||||
self._scene_cache[key] = value
|
||||
self._scene_cache.move_to_end(key)
|
||||
while len(self._scene_cache) > self._SCENE_CACHE_SIZE:
|
||||
self._scene_cache.popitem(last=False)
|
||||
|
||||
def _spread_bitmap(self, gray: np.ndarray) -> np.ndarray:
|
||||
"""Spread bitmap: bit b acceso dove bin b è presente nel raggio.
|
||||
|
||||
@@ -1309,6 +1357,7 @@ class LineShapeMatcher:
|
||||
min_recall: float = 0.0,
|
||||
use_soft_score: bool = False,
|
||||
subpixel_lm: bool = False,
|
||||
debug: bool = False,
|
||||
) -> list[Match]:
|
||||
"""
|
||||
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
||||
@@ -1326,6 +1375,32 @@ class LineShapeMatcher:
|
||||
if not self.variants:
|
||||
raise RuntimeError("Matcher non addestrato: chiamare train() prima.")
|
||||
|
||||
# Diagnostic counter: traccia perche' candidati sono droppati lungo
|
||||
# la pipeline. Esposto via get_last_diag() o ritornato implicitamente
|
||||
# se debug=True (vedi sotto).
|
||||
diag = {
|
||||
"n_variants_total": len(self.variants),
|
||||
"n_variants_top_evaluated": 0,
|
||||
"n_variants_top_passed": 0,
|
||||
"n_variants_full_evaluated": 0,
|
||||
"n_raw_candidates": 0,
|
||||
"n_after_pre_nms": 0,
|
||||
"drop_ncc_low": 0,
|
||||
"drop_min_score_post_avg": 0,
|
||||
"drop_recall_low": 0,
|
||||
"drop_bbox_out_of_scene": 0,
|
||||
"drop_nms_iou": 0,
|
||||
"n_final": 0,
|
||||
"top_thresh_used": 0.0,
|
||||
"verify_threshold_used": float(verify_threshold),
|
||||
"min_score_used": float(min_score),
|
||||
"min_recall_used": float(min_recall),
|
||||
"use_polarity": bool(self.use_polarity),
|
||||
"use_soft_score": bool(use_soft_score),
|
||||
"subpixel_lm": bool(subpixel_lm),
|
||||
}
|
||||
self._last_diag = diag
|
||||
|
||||
gray_full = self._to_gray(scene_bgr)
|
||||
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
||||
# ri-traslare le coordinate dei match a fine pipeline.
|
||||
@@ -1340,18 +1415,31 @@ class LineShapeMatcher:
|
||||
else:
|
||||
gray0 = gray_full
|
||||
roi_offset = (0, 0)
|
||||
|
||||
# Cache pre-compute scena (II Halcon-style): hash bytes scene + param
|
||||
# gradient/spread → riusa spread piramide + density tra find()
|
||||
# consecutive con stessa scena (typical UI tuning: slider produce
|
||||
# 10+ find() su scena identica). Risparmia ~80% del costo non-kernel.
|
||||
cache_key = self._scene_cache_key(gray0)
|
||||
cached = self._scene_cache_get(cache_key) if cache_key else None
|
||||
if cached is not None:
|
||||
grays, spread_top, bit_active_top, density_top, spread0, \
|
||||
bit_active_full, density_full, top = cached
|
||||
else:
|
||||
grays = [gray0]
|
||||
for _ in range(self.pyramid_levels - 1):
|
||||
grays.append(cv2.pyrDown(grays[-1]))
|
||||
top = len(grays) - 1
|
||||
|
||||
# Spread bitmap (uint8) al top level: 32× meno memoria della response
|
||||
# map float32 → MOLTO più cache-friendly per _score_by_shift.
|
||||
spread_top = self._spread_bitmap(grays[top])
|
||||
bit_active_top = int(
|
||||
sum(1 << b for b in range(self._n_bins)
|
||||
if (spread_top & (spread_top.dtype.type(1) << b)).any())
|
||||
)
|
||||
density_top = _jit_popcount(spread_top)
|
||||
# spread0 + density_full computati piu sotto, quindi salvo dopo.
|
||||
spread0 = None
|
||||
bit_active_full = None
|
||||
density_full = None
|
||||
if nms_radius is None:
|
||||
nms_radius = max(8, min(self.template_size) // 2)
|
||||
# Pruning adattivo allo step angolare: con step piccolo (<= 3 deg)
|
||||
@@ -1368,9 +1456,10 @@ class LineShapeMatcher:
|
||||
top_factor = max(top_factor, 0.7)
|
||||
cf_eff = 1
|
||||
top_thresh = min_score * top_factor
|
||||
diag["top_thresh_used"] = float(top_thresh)
|
||||
|
||||
tw, th = self.template_size
|
||||
density_top = _jit_popcount(spread_top)
|
||||
# density_top gia' computato sopra (cache o miss)
|
||||
sf_top = 2 ** top
|
||||
bg_cache_top: dict[float, np.ndarray] = {}
|
||||
bg_cache_full: dict[float, np.ndarray] = {}
|
||||
@@ -1453,6 +1542,7 @@ class LineShapeMatcher:
|
||||
|
||||
kept_coarse: list[tuple[int, float]] = []
|
||||
all_top_scores: list[tuple[int, float]] = []
|
||||
diag["n_variants_top_evaluated"] = len(coarse_idx_list)
|
||||
# batch_top: usa kernel batch single-call con prange-esterno su
|
||||
# varianti. Vince su threadpool quando n_vars >> n_threads e quando
|
||||
# H*W top e' piccolo (overhead chiamate JIT > costo kernel).
|
||||
@@ -1516,14 +1606,24 @@ class LineShapeMatcher:
|
||||
kept_variants.sort(key=lambda t: -t[1])
|
||||
max_vars_full = max(max_matches * 8, len(self.variants) // 2)
|
||||
kept_variants = kept_variants[:max_vars_full]
|
||||
diag["n_variants_top_passed"] = len(kept_coarse)
|
||||
diag["n_variants_full_evaluated"] = len(kept_variants)
|
||||
|
||||
# Full-res (parallelizzato) con bitmap
|
||||
# Full-res (parallelizzato) con bitmap.
|
||||
# Riusa cache se disponibile, altrimenti computa e salva.
|
||||
if spread0 is None:
|
||||
spread0 = self._spread_bitmap(gray0)
|
||||
bit_active_full = int(
|
||||
sum(1 << b for b in range(self._n_bins)
|
||||
if (spread0 & (spread0.dtype.type(1) << b)).any())
|
||||
)
|
||||
density_full = _jit_popcount(spread0)
|
||||
# Salva cache scena complete
|
||||
if cache_key is not None:
|
||||
self._scene_cache_put(cache_key, (
|
||||
grays, spread_top, bit_active_top, density_top,
|
||||
spread0, bit_active_full, density_full, top,
|
||||
))
|
||||
for sc in unique_scales:
|
||||
bg_cache_full[sc] = _bg_for_scale(density_full, sc, 1)
|
||||
|
||||
@@ -1601,6 +1701,7 @@ class LineShapeMatcher:
|
||||
raw.append((float(vals[i]), int(xs[i]), int(ys[i]), vi))
|
||||
|
||||
raw.sort(key=lambda c: -c[0])
|
||||
diag["n_raw_candidates"] = len(raw)
|
||||
|
||||
# Mappa vi → score_map per subpixel/refinement
|
||||
score_maps = dict(candidates_per_var)
|
||||
@@ -1632,6 +1733,7 @@ class LineShapeMatcher:
|
||||
preliminary_int.append((score, xi, yi, vi))
|
||||
if len(preliminary_int) >= pre_cap:
|
||||
break
|
||||
diag["n_after_pre_nms"] = len(preliminary_int)
|
||||
|
||||
# Subpixel + refine + verify solo sui candidati pre-NMS (max pre_cap)
|
||||
kept: list[Match] = []
|
||||
@@ -1678,6 +1780,7 @@ class LineShapeMatcher:
|
||||
view_idx=getattr(var, "view_idx", 0),
|
||||
)
|
||||
if ncc < verify_threshold:
|
||||
diag["drop_ncc_low"] += 1
|
||||
continue
|
||||
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
|
||||
# Soft-margin gradient similarity: sostituisce o integra lo
|
||||
@@ -1692,6 +1795,7 @@ class LineShapeMatcher:
|
||||
# abbattere lo shape-score sotto la soglia user. Senza questo
|
||||
# check apparivano match con score < min_score (UI confusing).
|
||||
if float(score_f) < min_score:
|
||||
diag["drop_min_score_post_avg"] += 1
|
||||
continue
|
||||
|
||||
# Feature recall (Halcon MinScore-style): conta quante feature
|
||||
@@ -1703,6 +1807,7 @@ class LineShapeMatcher:
|
||||
spread0, var, cx_f, cy_f, ang_f,
|
||||
)
|
||||
if recall < min_recall:
|
||||
diag["drop_recall_low"] += 1
|
||||
continue
|
||||
|
||||
# Ri-traslo coord da spazio crop ROI a spazio scena originale.
|
||||
@@ -1726,6 +1831,7 @@ class LineShapeMatcher:
|
||||
)
|
||||
inside_ratio = float(inter) / poly_area
|
||||
if inside_ratio < 0.75:
|
||||
diag["drop_bbox_out_of_scene"] += 1
|
||||
continue
|
||||
# Penalità scala opzionale: score degrada con distanza da 1.0
|
||||
if scale_penalty > 0.0 and var.scale != 1.0:
|
||||
@@ -1750,6 +1856,7 @@ class LineShapeMatcher:
|
||||
dup = True
|
||||
break
|
||||
if dup:
|
||||
diag["drop_nms_iou"] += 1
|
||||
continue
|
||||
kept.append(Match(
|
||||
cx=cx_out, cy=cy_out,
|
||||
@@ -1757,7 +1864,39 @@ class LineShapeMatcher:
|
||||
scale=var.scale,
|
||||
score=score_f,
|
||||
bbox_poly=poly,
|
||||
variant_idx=int(vi),
|
||||
))
|
||||
if len(kept) >= max_matches:
|
||||
break
|
||||
diag["n_final"] = len(kept)
|
||||
if debug:
|
||||
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
||||
import sys as _sys
|
||||
_sys.stderr.write(f"[pm2d.find debug] {self._format_diag(diag)}\n")
|
||||
return kept
|
||||
|
||||
def _format_diag(self, diag: dict) -> str:
|
||||
"""Formatta dict diagnostica in una linea leggibile."""
|
||||
return (
|
||||
f"vars: {diag['n_variants_total']} -> "
|
||||
f"top_eval={diag['n_variants_top_evaluated']} "
|
||||
f"top_pass={diag['n_variants_top_passed']} "
|
||||
f"full_eval={diag['n_variants_full_evaluated']} | "
|
||||
f"raw={diag['n_raw_candidates']} "
|
||||
f"pre_nms={diag['n_after_pre_nms']} -> "
|
||||
f"drop[ncc={diag['drop_ncc_low']}, "
|
||||
f"score={diag['drop_min_score_post_avg']}, "
|
||||
f"recall={diag['drop_recall_low']}, "
|
||||
f"bbox={diag['drop_bbox_out_of_scene']}, "
|
||||
f"nms={diag['drop_nms_iou']}] = "
|
||||
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
||||
)
|
||||
|
||||
def get_last_diag(self) -> dict | None:
|
||||
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
||||
|
||||
Halcon-equivalent: oggi inspect_shape_model espone parziali contatori.
|
||||
Util per debug 'perche' 0 match', tuning interattivo, validation.
|
||||
Vedi diag keys per significato (n_variants_top_evaluated, drop_*, ...).
|
||||
"""
|
||||
return getattr(self, "_last_diag", None)
|
||||
|
||||
+214
-36
@@ -78,6 +78,7 @@ def _matcher_cache_key(roi: np.ndarray, tech: dict) -> str:
|
||||
h.update(roi.tobytes())
|
||||
# Solo parametri che influenzano il training
|
||||
relevant = ("num_features", "weak_grad", "strong_grad",
|
||||
"min_feature_spacing",
|
||||
"angle_min", "angle_max", "angle_step",
|
||||
"scale_min", "scale_max", "scale_step",
|
||||
"spread_radius", "pyramid_levels")
|
||||
@@ -131,45 +132,87 @@ def _encode_png(img: np.ndarray) -> bytes:
|
||||
|
||||
|
||||
def _draw_matches(scene: np.ndarray, matches: list[Match],
|
||||
template_gray: np.ndarray | None) -> np.ndarray:
|
||||
template_gray: np.ndarray | None,
|
||||
matcher: "LineShapeMatcher | None" = None) -> np.ndarray:
|
||||
"""Disegna SOLO UCS (richiesta utente) per ogni match trovato.
|
||||
|
||||
UCS = sistema di coordinate (X rosso, Y verde) posizionato sul
|
||||
baricentro feature del modello, ruotato secondo l'angolo del match.
|
||||
Niente edge, niente cerchietti feature, niente bbox: i match sulla
|
||||
scena reale devono essere puliti, gli edge filtrati si vedono solo
|
||||
nell'anteprima modello.
|
||||
"""
|
||||
out = scene.copy()
|
||||
H, W = scene.shape[:2]
|
||||
palette = [
|
||||
(0, 255, 0), (0, 200, 255), (255, 100, 100), (255, 200, 0),
|
||||
(200, 0, 255), (100, 255, 200), (255, 0, 0), (0, 255, 255),
|
||||
]
|
||||
# Lunghezza assi UCS: stessa formula dell'anteprima modello
|
||||
# (0.15 * max lato template) scalata per m.scale → coerenza dimensionale.
|
||||
if matcher is not None and matcher.template_size != (0, 0):
|
||||
L_base = int(0.15 * max(matcher.template_size))
|
||||
else:
|
||||
L_base = 30
|
||||
H_scene, W_scene = scene.shape[:2]
|
||||
|
||||
for i, m in enumerate(matches):
|
||||
color = palette[i % len(palette)]
|
||||
if template_gray is not None:
|
||||
# UCS posizionato esattamente sul CENTRO POSE del match (m.cx, m.cy):
|
||||
# equivale al centro template traslato alla scena, ruotato con
|
||||
# m.angle_deg. Coerente con UCS dell'anteprima modello che ora
|
||||
# e' anche sul centro ROI (vedi preview_edges).
|
||||
ax = np.deg2rad(m.angle_deg)
|
||||
ca, sa = np.cos(ax), np.sin(ax)
|
||||
cx, cy = int(round(m.cx)), int(round(m.cy))
|
||||
# Overlay edge del modello orientato (richiesta utente):
|
||||
# warpa template alla pose, applica hysteresis identica al matcher,
|
||||
# disegna pixel edge come overlay verde tenue. Maschera col
|
||||
# _train_mask warpato + erode per rimuovere edge sui BORDI del
|
||||
# rettangolo template (transizione bordo nero → scena = falso edge
|
||||
# che appariva come "ROI" attorno a ogni match).
|
||||
if template_gray is not None and matcher is not None:
|
||||
t = template_gray
|
||||
th, tw = t.shape
|
||||
edge = cv2.Canny(t, 50, 150)
|
||||
cx_t = (tw - 1) / 2.0; cy_t = (th - 1) / 2.0
|
||||
M = cv2.getRotationMatrix2D((cx_t, cy_t), m.angle_deg, m.scale)
|
||||
M[0, 2] += m.cx - cx_t
|
||||
M[1, 2] += m.cy - cy_t
|
||||
warped = cv2.warpAffine(edge, M, (W, H),
|
||||
warped_gray = cv2.warpAffine(
|
||||
t, M, (W_scene, H_scene),
|
||||
flags=cv2.INTER_LINEAR, borderValue=0)
|
||||
# Maschera: train_mask se disponibile, altrimenti rettangolo pieno
|
||||
mask_src = (matcher._train_mask if matcher._train_mask is not None
|
||||
else np.full((th, tw), 255, dtype=np.uint8))
|
||||
warped_mask = cv2.warpAffine(
|
||||
mask_src, M, (W_scene, H_scene),
|
||||
flags=cv2.INTER_NEAREST, borderValue=0)
|
||||
mask = warped > 0
|
||||
if mask.any():
|
||||
overlay = np.zeros_like(out)
|
||||
overlay[mask] = color
|
||||
out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8)
|
||||
poly = m.bbox_poly.astype(np.int32).reshape(-1, 1, 2)
|
||||
cv2.polylines(out, [poly], True, color, 2, cv2.LINE_AA)
|
||||
p0 = tuple(m.bbox_poly[0].astype(int))
|
||||
p1 = tuple(m.bbox_poly[1].astype(int))
|
||||
cv2.line(out, p0, p1, color, 4, cv2.LINE_AA)
|
||||
cx, cy = int(round(m.cx)), int(round(m.cy))
|
||||
cv2.drawMarker(out, (cx, cy), color, cv2.MARKER_CROSS, 22, 2, cv2.LINE_AA)
|
||||
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
||||
a = np.deg2rad(m.angle_deg)
|
||||
cv2.arrowedLine(out, (cx, cy),
|
||||
(int(cx + L * np.cos(a)), int(cy - L * np.sin(a))),
|
||||
color, 2, cv2.LINE_AA, tipLength=0.2)
|
||||
label = f"#{i+1} {m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.2f}"
|
||||
cv2.putText(out, label, (cx + 8, cy - 8),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
||||
# Erode di spread_radius per scartare la fascia di transizione
|
||||
# bordo che produce gradient spurio
|
||||
er_k = max(3, 2 * matcher.spread_radius + 1)
|
||||
kernel_er = np.ones((er_k, er_k), np.uint8)
|
||||
warped_mask = cv2.erode(warped_mask, kernel_er)
|
||||
mag, _ = matcher._gradient(warped_gray)
|
||||
if matcher.weak_grad < matcher.strong_grad:
|
||||
edge_mask = matcher._hysteresis_mask(mag)
|
||||
else:
|
||||
edge_mask = mag >= matcher.strong_grad
|
||||
edge_mask = edge_mask & (warped_mask > 0)
|
||||
if edge_mask.any():
|
||||
edge_overlay = np.zeros_like(out)
|
||||
edge_overlay[edge_mask] = (0, 220, 0) # verde brillante
|
||||
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
|
||||
L = max(20, int(L_base * m.scale))
|
||||
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
|
||||
x_end = (int(cx + L * ca), int(cy - L * sa))
|
||||
# Y axis = rotazione di (0, 1) con cv2 matrix → (sin, cos)
|
||||
# A m.angle_deg=0 deve puntare GIU' (image y-down convenzione modello)
|
||||
y_end = (int(cx + L * sa), int(cy + L * ca))
|
||||
cv2.arrowedLine(out, (cx, cy), x_end,
|
||||
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "X", (x_end[0] + 4, x_end[1] + 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
||||
cv2.arrowedLine(out, (cx, cy), y_end,
|
||||
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "Y", (y_end[0] + 4, y_end[1] + 12),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
|
||||
# Origine UCS: cerchio bianco con bordo nero
|
||||
cv2.circle(out, (cx, cy), 4, (0, 0, 0), -1, cv2.LINE_AA)
|
||||
cv2.circle(out, (cx, cy), 3, (255, 255, 255), -1, cv2.LINE_AA)
|
||||
return out
|
||||
|
||||
|
||||
@@ -217,6 +260,7 @@ class MatchResp(BaseModel):
|
||||
find_time: float
|
||||
num_variants: int
|
||||
annotated_id: str
|
||||
diag: dict | None = None # CC: diagnostica pipeline (drop reasons)
|
||||
|
||||
|
||||
class TuneParams(BaseModel):
|
||||
@@ -271,6 +315,15 @@ class SimpleMatchParams(BaseModel):
|
||||
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
||||
min_score: float = 0.65
|
||||
max_matches: int = 25
|
||||
# --- Override edge da pannello "Anteprima edge" (None = auto_tune) ---
|
||||
# Quando settati, sovrascrivono i valori derivati da auto_tune e
|
||||
# vengono usati identici sia nel training del matcher sia nel find.
|
||||
# Salvati nella ricetta cosi' la stessa pulizia rumore e' replicata
|
||||
# quando la ricetta viene caricata.
|
||||
edge_weak_grad: float | None = None
|
||||
edge_strong_grad: float | None = None
|
||||
edge_num_features: int | None = None
|
||||
edge_min_feature_spacing: int | None = None
|
||||
# --- Halcon-mode flags (default off = backward compat) ---
|
||||
# Init-time (richiede ri-train se cambiato)
|
||||
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
||||
@@ -319,10 +372,24 @@ def _simple_to_technical(
|
||||
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
||||
ang_step = PRECISION_ANGLE_STEP.get(p.precisione, 5.0)
|
||||
|
||||
# Override edge dal pannello "Anteprima edge" se utente li ha settati.
|
||||
# Questi sostituiscono i valori auto_tune nel training del matcher,
|
||||
# garantendo che la selezione edge identica a quella del preview
|
||||
# venga usata sia in training sia in find.
|
||||
weak_g = (p.edge_weak_grad if p.edge_weak_grad is not None
|
||||
else tune["weak_grad"])
|
||||
strong_g = (p.edge_strong_grad if p.edge_strong_grad is not None
|
||||
else tune["strong_grad"])
|
||||
n_feat = (p.edge_num_features if p.edge_num_features is not None
|
||||
else nf)
|
||||
min_sp = (p.edge_min_feature_spacing if p.edge_min_feature_spacing is not None
|
||||
else 3)
|
||||
|
||||
return {
|
||||
"num_features": nf,
|
||||
"weak_grad": tune["weak_grad"],
|
||||
"strong_grad": tune["strong_grad"],
|
||||
"num_features": n_feat,
|
||||
"weak_grad": weak_g,
|
||||
"strong_grad": strong_g,
|
||||
"min_feature_spacing": min_sp,
|
||||
"spread_radius": spread,
|
||||
"pyramid_levels": pyr,
|
||||
"angle_min": 0.0,
|
||||
@@ -510,7 +577,7 @@ def match(p: MatchParams):
|
||||
|
||||
# Render annotated image
|
||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
||||
annotated = _draw_matches(scene, matches, tg)
|
||||
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||
ann_id = _store_image(annotated)
|
||||
|
||||
return MatchResp(
|
||||
@@ -521,6 +588,7 @@ def match(p: MatchParams):
|
||||
) for m_ in matches],
|
||||
train_time=t_train, find_time=t_find,
|
||||
num_variants=n, annotated_id=ann_id,
|
||||
diag=m.get_last_diag() if hasattr(m, "get_last_diag") else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -557,6 +625,7 @@ def match_simple(p: SimpleMatchParams):
|
||||
scale_range=(tech["scale_min"], tech["scale_max"]),
|
||||
scale_step=tech["scale_step"],
|
||||
spread_radius=tech["spread_radius"],
|
||||
min_feature_spacing=tech.get("min_feature_spacing", 3),
|
||||
pyramid_levels=tech["pyramid_levels"],
|
||||
use_polarity=p.use_polarity,
|
||||
use_gpu=p.use_gpu,
|
||||
@@ -586,7 +655,7 @@ def match_simple(p: SimpleMatchParams):
|
||||
t_find = time.time() - t0
|
||||
|
||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
||||
annotated = _draw_matches(scene, matches, tg)
|
||||
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||
ann_id = _store_image(annotated)
|
||||
|
||||
return MatchResp(
|
||||
@@ -596,6 +665,7 @@ def match_simple(p: SimpleMatchParams):
|
||||
) for mt in matches],
|
||||
train_time=t_train, find_time=t_find,
|
||||
num_variants=n, annotated_id=ann_id,
|
||||
diag=m.get_last_diag() if hasattr(m, "get_last_diag") else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -625,9 +695,112 @@ class SaveRecipeParams(BaseModel):
|
||||
precisione: str = "normale"
|
||||
use_polarity: bool = False
|
||||
use_gpu: bool = False
|
||||
# Override edge dal pannello "Anteprima edge" (None = auto_tune)
|
||||
edge_weak_grad: float | None = None
|
||||
edge_strong_grad: float | None = None
|
||||
edge_num_features: int | None = None
|
||||
edge_min_feature_spacing: int | None = None
|
||||
name: str # nome file ricetta (no path)
|
||||
|
||||
|
||||
class EdgePreviewParams(BaseModel):
|
||||
model_id: str
|
||||
roi: list[int]
|
||||
weak_grad: float = 30.0
|
||||
strong_grad: float = 60.0
|
||||
num_features: int = 96
|
||||
min_feature_spacing: int = 3
|
||||
use_polarity: bool = False
|
||||
|
||||
|
||||
@app.post("/preview_edges")
|
||||
def preview_edges(p: EdgePreviewParams):
|
||||
"""Estrae edge feature dalla ROI con i parametri dati e ritorna
|
||||
immagine annotata con i pixel selezionati come overlay.
|
||||
|
||||
Permette tuning interattivo delle soglie weak/strong_grad e
|
||||
num_features per "togliere le sporcizie" (rumore di sfondo,
|
||||
edge spuri) prima di trainare il matcher vero.
|
||||
"""
|
||||
model = _load_image(p.model_id)
|
||||
if model is None:
|
||||
raise HTTPException(404, "Modello non trovato")
|
||||
x, y, w, h = p.roi
|
||||
H_m, W_m = model.shape[:2]
|
||||
x = max(0, min(int(x), W_m - 1)); y = max(0, min(int(y), H_m - 1))
|
||||
w = max(1, min(int(w), W_m - x)); h = max(1, min(int(h), H_m - y))
|
||||
roi_img = model[y:y + h, x:x + w]
|
||||
# Matcher temporaneo solo per estrazione feature (no train completo)
|
||||
m = LineShapeMatcher(
|
||||
weak_grad=p.weak_grad,
|
||||
strong_grad=p.strong_grad,
|
||||
num_features=p.num_features,
|
||||
min_feature_spacing=p.min_feature_spacing,
|
||||
use_polarity=p.use_polarity,
|
||||
)
|
||||
gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY) if roi_img.ndim == 3 else roi_img
|
||||
mag, bins = m._gradient(gray)
|
||||
fx, fy, fb = m._extract_features(mag, bins, None)
|
||||
# Mostra anche i pixel "weak/strong" come heatmap di sfondo
|
||||
out = roi_img.copy() if roi_img.ndim == 3 else cv2.cvtColor(roi_img, cv2.COLOR_GRAY2BGR)
|
||||
# Overlay magnitude leggera
|
||||
mag_norm = np.clip(mag / max(1.0, mag.max()) * 255, 0, 255).astype(np.uint8)
|
||||
mag_color = cv2.applyColorMap(mag_norm, cv2.COLORMAP_BONE)
|
||||
out = cv2.addWeighted(out, 0.6, mag_color, 0.4, 0)
|
||||
# Pixel "strong" con hysteresis: contorno verde scuro tenue
|
||||
if m.weak_grad < m.strong_grad:
|
||||
edge_mask = m._hysteresis_mask(mag).astype(np.uint8) * 255
|
||||
else:
|
||||
edge_mask = (mag >= m.strong_grad).astype(np.uint8) * 255
|
||||
edge_overlay = np.zeros_like(out)
|
||||
edge_overlay[edge_mask > 0] = (0, 80, 0) # verde scuro
|
||||
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.5, 0)
|
||||
# Feature scelte: cerchietti colorati per bin
|
||||
bin_colors = [
|
||||
(255, 0, 0), (255, 128, 0), (255, 255, 0), (0, 255, 0),
|
||||
(0, 255, 255), (0, 128, 255), (0, 0, 255), (255, 0, 255),
|
||||
(255, 100, 100), (255, 180, 100), (255, 230, 100), (180, 255, 100),
|
||||
(100, 255, 200), (100, 180, 255), (180, 100, 255), (255, 100, 200),
|
||||
]
|
||||
for i in range(len(fx)):
|
||||
b = int(fb[i])
|
||||
col = bin_colors[b % len(bin_colors)]
|
||||
cv2.circle(out, (int(fx[i]), int(fy[i])), 2, col, -1, cv2.LINE_AA)
|
||||
# UCS sul CENTRO ROI (coerente con _draw_matches che usa centro pose).
|
||||
# In questo modo l'UCS visualizzato nel modello = UCS del match (modulo
|
||||
# rotazione/traslazione data dalla pose del pezzo trovato).
|
||||
rh, rw = roi_img.shape[:2]
|
||||
bx, by = (rw - 1) // 2, (rh - 1) // 2
|
||||
axis_len = max(20, int(0.15 * max(rw, rh)))
|
||||
cv2.arrowedLine(out, (bx, by), (bx + axis_len, by),
|
||||
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "X", (bx + axis_len + 4, by + 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
||||
cv2.arrowedLine(out, (bx, by), (bx, by + axis_len),
|
||||
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "Y", (bx + 4, by + axis_len + 12),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
|
||||
cv2.circle(out, (bx, by), 4, (0, 0, 0), -1, cv2.LINE_AA)
|
||||
cv2.circle(out, (bx, by), 3, (255, 255, 255), -1, cv2.LINE_AA)
|
||||
bary_cx, bary_cy = float(bx), float(by)
|
||||
img_id = _store_image(out)
|
||||
n_edge_strong = int((mag >= m.strong_grad).sum())
|
||||
n_edge_total = int(edge_mask.sum() / 255)
|
||||
return {
|
||||
"preview_id": img_id,
|
||||
"n_features": len(fx),
|
||||
"n_edge_strong": n_edge_strong,
|
||||
"n_edge_after_hysteresis": n_edge_total,
|
||||
"mag_max": float(mag.max()),
|
||||
"mag_p50": float(np.percentile(mag, 50)),
|
||||
"mag_p85": float(np.percentile(mag, 85)),
|
||||
"ucs_baricentro": (
|
||||
{"cx": round(bary_cx, 2), "cy": round(bary_cy, 2)}
|
||||
if bary_cx is not None else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/recipes")
|
||||
def save_recipe(p: SaveRecipeParams):
|
||||
"""Allena matcher e salva su disco come ricetta riutilizzabile."""
|
||||
@@ -641,6 +814,10 @@ def save_recipe(p: SaveRecipeParams):
|
||||
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
||||
precisione=p.precisione,
|
||||
use_polarity=p.use_polarity, use_gpu=p.use_gpu,
|
||||
edge_weak_grad=p.edge_weak_grad,
|
||||
edge_strong_grad=p.edge_strong_grad,
|
||||
edge_num_features=p.edge_num_features,
|
||||
edge_min_feature_spacing=p.edge_min_feature_spacing,
|
||||
)
|
||||
tech = _simple_to_technical(sp, roi_img)
|
||||
m = LineShapeMatcher(
|
||||
@@ -760,7 +937,7 @@ def match_recipe(p: RecipeMatchParams):
|
||||
)
|
||||
t_find = time.time() - t0
|
||||
tg = m.template_gray if m.template_gray is not None else np.zeros((1, 1), np.uint8)
|
||||
annotated = _draw_matches(scene, matches, tg)
|
||||
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||
ann_id = _store_image(annotated)
|
||||
return MatchResp(
|
||||
matches=[MatchResult(
|
||||
@@ -769,6 +946,7 @@ def match_recipe(p: RecipeMatchParams):
|
||||
) for mt in matches],
|
||||
train_time=0.0, find_time=t_find,
|
||||
num_variants=len(m.variants), annotated_id=ann_id,
|
||||
diag=m.get_last_diag() if hasattr(m, "get_last_diag") else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -53,10 +53,34 @@ function readUserParams() {
|
||||
document.getElementById("p-penalita-scala").value),
|
||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||
...readEdgeOverrides(),
|
||||
...readHalconFlags(),
|
||||
};
|
||||
}
|
||||
|
||||
function readEdgeOverrides() {
|
||||
// Override edge dal pannello "Anteprima edge". Settati = utente li ha
|
||||
// toccati (anche se uguali al default attuale). Vengono propagati a
|
||||
// _simple_to_technical e usati identici sia in training sia in find.
|
||||
// Inoltre salvati nella ricetta cosi' si replicano al load.
|
||||
const _v = (id, parser) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return null;
|
||||
const v = parser(el.value);
|
||||
return Number.isFinite(v) ? v : null;
|
||||
};
|
||||
// Sempre passa i valori correnti degli slider: e' la richiesta utente
|
||||
// che i param di pulizia rumore vengano usati anche nel find/ricetta.
|
||||
const polCb = document.getElementById("hc-use-polarity");
|
||||
return {
|
||||
edge_weak_grad: _v("ep-weak", parseFloat),
|
||||
edge_strong_grad: _v("ep-strong", parseFloat),
|
||||
edge_num_features: _v("ep-nf", parseInt),
|
||||
edge_min_feature_spacing: _v("ep-sp", parseInt),
|
||||
use_polarity: polCb?.checked || document.getElementById("ep-pol")?.checked,
|
||||
};
|
||||
}
|
||||
|
||||
function readHalconFlags() {
|
||||
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
||||
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
||||
@@ -336,6 +360,7 @@ async function doMatchRecipe() {
|
||||
document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
|
||||
document.getElementById("t-var").textContent = data.num_variants;
|
||||
document.getElementById("t-match").textContent = data.matches.length;
|
||||
renderDiag(data.diag, data.matches.length);
|
||||
setStatus(`${data.matches.length} match trovati (ricetta ${state.active_recipe})`);
|
||||
}
|
||||
|
||||
@@ -409,6 +434,7 @@ async function doMatch() {
|
||||
document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
|
||||
document.getElementById("t-var").textContent = data.num_variants;
|
||||
document.getElementById("t-match").textContent = data.matches.length;
|
||||
renderDiag(data.diag, data.matches.length);
|
||||
setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`);
|
||||
}
|
||||
|
||||
@@ -436,6 +462,164 @@ function setStatus(s) {
|
||||
}
|
||||
|
||||
// ---------- Init ----------
|
||||
// ---------- Edge preview (clean rumore) ----------
|
||||
let _epDebounce = null;
|
||||
let _epLastImg = null;
|
||||
|
||||
async function fetchEdgePreview() {
|
||||
if (!state.model || !state.roi) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
"Disegna prima la ROI sul modello";
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
model_id: state.model.id,
|
||||
roi: state.roi,
|
||||
weak_grad: parseFloat(document.getElementById("ep-weak").value),
|
||||
strong_grad: parseFloat(document.getElementById("ep-strong").value),
|
||||
num_features: parseInt(document.getElementById("ep-nf").value, 10),
|
||||
min_feature_spacing: parseInt(document.getElementById("ep-sp").value, 10),
|
||||
use_polarity: document.getElementById("ep-pol").checked,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/preview_edges", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
_epLastImg = await loadImage(`/image/${j.preview_id}/raw?t=${Date.now()}`);
|
||||
drawEdgePreview();
|
||||
const ucs = j.ucs_baricentro
|
||||
? ` | UCS=(${j.ucs_baricentro.cx},${j.ucs_baricentro.cy})`
|
||||
: "";
|
||||
document.getElementById("edge-preview-info").innerHTML =
|
||||
`<b>${j.n_features}</b> feature scelte (di ${j.n_edge_after_hysteresis} edge totali)<br>` +
|
||||
`mag: max=${j.mag_max.toFixed(0)} p50=${j.mag_p50.toFixed(0)} ` +
|
||||
`p85=${j.mag_p85.toFixed(0)}${ucs}`;
|
||||
} catch (e) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
`Errore preview: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdgePreview() {
|
||||
const cnv = document.getElementById("c-edge-preview");
|
||||
if (!_epLastImg) return;
|
||||
const ctx = cnv.getContext("2d");
|
||||
// Fit-contain
|
||||
const r = Math.min(cnv.width / _epLastImg.width,
|
||||
cnv.height / _epLastImg.height);
|
||||
const w = _epLastImg.width * r;
|
||||
const h = _epLastImg.height * r;
|
||||
const ox = (cnv.width - w) / 2;
|
||||
const oy = (cnv.height - h) / 2;
|
||||
ctx.fillStyle = "#000"; ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(_epLastImg, ox, oy, w, h);
|
||||
}
|
||||
|
||||
function scheduleEdgePreview() {
|
||||
if (_epDebounce) clearTimeout(_epDebounce);
|
||||
_epDebounce = setTimeout(fetchEdgePreview, 200);
|
||||
}
|
||||
|
||||
function bindEdgePreviewControls() {
|
||||
const slid = (id, valEl) => {
|
||||
const el = document.getElementById(id);
|
||||
const v = document.getElementById(valEl);
|
||||
el.addEventListener("input", () => {
|
||||
v.textContent = el.value;
|
||||
scheduleEdgePreview();
|
||||
});
|
||||
};
|
||||
slid("ep-weak", "ep-weak-v");
|
||||
slid("ep-strong", "ep-strong-v");
|
||||
slid("ep-nf", "ep-nf-v");
|
||||
slid("ep-sp", "ep-sp-v");
|
||||
document.getElementById("ep-pol").addEventListener("change",
|
||||
scheduleEdgePreview);
|
||||
// Auto-refresh quando il pannello viene aperto
|
||||
document.getElementById("edge-preview-panel").addEventListener("toggle",
|
||||
(e) => { if (e.target.open) fetchEdgePreview(); });
|
||||
document.getElementById("btn-edge-apply").addEventListener("click", () => {
|
||||
// Copia i valori correnti nei campi avanzati
|
||||
const map = {
|
||||
"ep-weak": "adv-weak_grad",
|
||||
"ep-strong": "adv-strong_grad",
|
||||
"ep-nf": "adv-num_features",
|
||||
"ep-sp": "adv-min_feature_spacing",
|
||||
};
|
||||
for (const [src, dst] of Object.entries(map)) {
|
||||
const dstEl = document.getElementById(dst);
|
||||
if (dstEl) dstEl.value = document.getElementById(src).value;
|
||||
}
|
||||
// use_polarity: alla checkbox della modalita Halcon
|
||||
const polCb = document.getElementById("hc-use-polarity");
|
||||
if (polCb) polCb.checked = document.getElementById("ep-pol").checked;
|
||||
// Apri pannello Avanzate per feedback
|
||||
const advDetails = document.querySelectorAll("#col-params details");
|
||||
advDetails.forEach((d) => { d.open = true; });
|
||||
alert("Parametri edge applicati. Esegui MATCH per usare i valori scelti.");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- CC: Diagnostica match ----------
|
||||
function renderDiag(diag, n_matches) {
|
||||
const el = document.getElementById("diag-content");
|
||||
if (!diag) {
|
||||
el.innerHTML = '<em style="color:#888">Diagnostica non disponibile</em>';
|
||||
return;
|
||||
}
|
||||
const dropTotal = (diag.drop_ncc_low || 0) + (diag.drop_min_score_post_avg || 0)
|
||||
+ (diag.drop_recall_low || 0) + (diag.drop_bbox_out_of_scene || 0)
|
||||
+ (diag.drop_nms_iou || 0);
|
||||
// Hint contestuali se 0 match
|
||||
let hint = "";
|
||||
if (n_matches === 0) {
|
||||
if (diag.n_after_pre_nms === 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ Nessun candidato sopra soglia.
|
||||
Prova: ↓ <b>min_score</b> o ↓ <b>top_thresh</b> (currently ${diag.top_thresh_used.toFixed(2)})</div>`;
|
||||
} else if (diag.drop_ncc_low > 0 && dropTotal === diag.drop_ncc_low) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_ncc_low} candidati droppati da NCC.
|
||||
Prova: ↓ <b>verify_threshold</b> (filtro_fp più leggero)</div>`;
|
||||
} else if (diag.drop_min_score_post_avg > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_min_score_post_avg} match sotto min_score post-NCC.
|
||||
Prova: ↓ <b>min_score</b></div>`;
|
||||
} else if (diag.drop_recall_low > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_recall_low} match con recall < ${diag.min_recall_used}.
|
||||
Prova: ↓ <b>min_recall</b></div>`;
|
||||
} else if (diag.drop_bbox_out_of_scene > 0) {
|
||||
hint = `<div style="color:#f88; margin-top:6px">⚠ ${diag.drop_bbox_out_of_scene} match con bbox fuori scena.
|
||||
Centro derivato male: aumenta <b>min_score</b> o restringi <b>search_roi</b></div>`;
|
||||
}
|
||||
}
|
||||
const flags = [];
|
||||
if (diag.use_polarity) flags.push("polarity");
|
||||
if (diag.use_soft_score) flags.push("soft");
|
||||
if (diag.subpixel_lm) flags.push("subpix-LM");
|
||||
el.innerHTML = `
|
||||
<div><b>Pipeline pruning:</b></div>
|
||||
<div>varianti: ${diag.n_variants_total} → top_eval=${diag.n_variants_top_evaluated}
|
||||
→ top_pass=${diag.n_variants_top_passed} → full_eval=${diag.n_variants_full_evaluated}</div>
|
||||
<div><b>Candidati:</b> raw=${diag.n_raw_candidates}
|
||||
→ pre_nms=${diag.n_after_pre_nms} → final=${diag.n_final}</div>
|
||||
<div><b>Drop reasons:</b> NCC=${diag.drop_ncc_low}, score=${diag.drop_min_score_post_avg},
|
||||
recall=${diag.drop_recall_low}, bbox=${diag.drop_bbox_out_of_scene}, NMS=${diag.drop_nms_iou}</div>
|
||||
<div><b>Soglie:</b> top=${diag.top_thresh_used.toFixed(2)},
|
||||
min_score=${diag.min_score_used.toFixed(2)},
|
||||
NCC=${diag.verify_threshold_used.toFixed(2)},
|
||||
recall=${diag.min_recall_used.toFixed(2)}</div>
|
||||
${flags.length ? `<div><b>Flag attivi:</b> ${flags.join(", ")}</div>` : ""}
|
||||
${hint}
|
||||
`;
|
||||
// Auto-apri pannello se 0 match (segnala problema)
|
||||
if (n_matches === 0) {
|
||||
document.getElementById("diag-panel").open = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Auto-tune (Halcon-style) ----------
|
||||
async function doAutoTune() {
|
||||
if (!state.model || !state.roi) {
|
||||
@@ -556,6 +740,10 @@ async function saveRecipe() {
|
||||
precisione: user.precisione,
|
||||
use_polarity: user.use_polarity,
|
||||
use_gpu: user.use_gpu,
|
||||
edge_weak_grad: user.edge_weak_grad,
|
||||
edge_strong_grad: user.edge_strong_grad,
|
||||
edge_num_features: user.edge_num_features,
|
||||
edge_min_feature_spacing: user.edge_min_feature_spacing,
|
||||
name: name,
|
||||
};
|
||||
try {
|
||||
@@ -608,6 +796,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
document.getElementById("btn-unload-recipe").addEventListener("click",
|
||||
unloadRecipe);
|
||||
refreshRecipeList();
|
||||
bindEdgePreviewControls();
|
||||
const slider = document.getElementById("p-min-score");
|
||||
slider.addEventListener("input", (e) => {
|
||||
document.getElementById("v-score").textContent =
|
||||
|
||||
@@ -45,6 +45,40 @@
|
||||
<canvas id="c-model" width="380" height="420"></canvas>
|
||||
</div>
|
||||
<div id="roi-info">ROI: (nessuna)</div>
|
||||
<details id="edge-preview-panel" style="margin-top:10px">
|
||||
<summary>🔬 Anteprima edge / pulizia rumore</summary>
|
||||
<div style="font-size:11px; color:#aaa; margin:4px 0">
|
||||
Regola le soglie per togliere edge spuri (sporcizie). UCS rosso/verde
|
||||
sul baricentro feature.
|
||||
</div>
|
||||
<div class="ep-grid">
|
||||
<label class="ep-row">weak_grad <span id="ep-weak-v">30</span>
|
||||
<input type="range" id="ep-weak" min="5" max="200" value="30" step="1">
|
||||
</label>
|
||||
<label class="ep-row">strong_grad <span id="ep-strong-v">60</span>
|
||||
<input type="range" id="ep-strong" min="10" max="400" value="60" step="1">
|
||||
</label>
|
||||
<label class="ep-row">num_features <span id="ep-nf-v">96</span>
|
||||
<input type="range" id="ep-nf" min="16" max="300" value="96" step="1">
|
||||
</label>
|
||||
<label class="ep-row">spacing <span id="ep-sp-v">3</span>
|
||||
<input type="range" id="ep-sp" min="1" max="15" value="3" step="1">
|
||||
</label>
|
||||
<label class="ep-row" style="flex-direction:row; gap:6px">
|
||||
<input type="checkbox" id="ep-pol"> polarity
|
||||
</label>
|
||||
<button class="btn" id="btn-edge-apply" type="button"
|
||||
style="grid-column:1/-1">
|
||||
✓ Applica ai parametri Avanzate
|
||||
</button>
|
||||
</div>
|
||||
<div class="canvas-wrap" style="margin-top:6px">
|
||||
<canvas id="c-edge-preview" width="380" height="380"></canvas>
|
||||
</div>
|
||||
<div id="edge-preview-info" style="font-size:11px; color:#888; margin-top:4px">
|
||||
Disegna ROI e apri questo pannello per generare anteprima
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="col" id="col-scene">
|
||||
@@ -214,6 +248,16 @@
|
||||
<div class="kv"><span>find:</span><span id="t-find">-</span></div>
|
||||
<div class="kv"><span>varianti:</span><span id="t-var">-</span></div>
|
||||
<div class="kv"><span>match:</span><span id="t-match">-</span></div>
|
||||
|
||||
<details id="diag-panel" style="margin-top:10px">
|
||||
<summary>🔍 Diagnostica (CC)</summary>
|
||||
<div id="diag-content" style="font-family:monospace; font-size:11px;
|
||||
background:#1a1a1a; padding:8px;
|
||||
border-radius:3px; margin-top:6px;
|
||||
line-height:1.5">
|
||||
<em style="color:#888">Esegui un MATCH per vedere la diagnostica</em>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -173,3 +173,18 @@ footer h2 {
|
||||
}
|
||||
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||
.hc-row.hc-num input { width: 100%; }
|
||||
|
||||
/* Edge preview panel */
|
||||
.ep-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ep-row {
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
font-size: 11px; color: #aaa;
|
||||
}
|
||||
.ep-row input[type="range"] { width: 100%; }
|
||||
.ep-row span { color: #fff; font-weight: bold; font-family: monospace; }
|
||||
|
||||
Reference in New Issue
Block a user