Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9451a418a6 | |||
| 2c9160e4be | |||
| 6d6dcc3b7a | |||
| ee1c4a8f92 | |||
| 5002515b41 | |||
| 8029a1e12b | |||
| d37833076e | |||
| e1ed9206a3 | |||
| e84ae199ac | |||
| 5f0c4542d3 | |||
| 29c034fb05 | |||
| 6fb1efcab8 | |||
| 35df4c473c | |||
| 64f2c8b5dc | |||
| 7e076deb80 | |||
| 852597ed51 | |||
| a78884f950 |
+179
@@ -0,0 +1,179 @@
|
|||||||
|
"""Benchmark suite per LineShapeMatcher.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m pm2d.bench [--quick]
|
||||||
|
|
||||||
|
Misura tempi find() su 3 template-tipo × 3 scene-tipo × N config:
|
||||||
|
- Template: rettangolo 80×80, L-shape 120×120, cerchio 150×150
|
||||||
|
- Scene: pulita 800×600, cluttered 1080×1920, multi-pezzo 1080×1920
|
||||||
|
- Config: baseline, polarity, gpu, pyramid_propagate, greediness=0.7
|
||||||
|
|
||||||
|
Per ogni config stampa: ms/find, ms per fase (profile), n. match.
|
||||||
|
Output tabellare per detectare regressioni in CI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from pm2d.line_matcher import LineShapeMatcher, opencl_available
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Sintetizzatori template/scena ----------
|
||||||
|
|
||||||
|
def _tpl_rect() -> np.ndarray:
|
||||||
|
t = np.zeros((80, 80, 3), np.uint8)
|
||||||
|
cv2.rectangle(t, (15, 15), (65, 65), (255, 255, 255), 3)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _tpl_lshape() -> np.ndarray:
|
||||||
|
t = np.zeros((120, 120, 3), np.uint8)
|
||||||
|
cv2.rectangle(t, (20, 20), (50, 100), (255, 255, 255), -1)
|
||||||
|
cv2.rectangle(t, (20, 70), (100, 100), (255, 255, 255), -1)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _tpl_circle() -> np.ndarray:
|
||||||
|
t = np.zeros((150, 150, 3), np.uint8)
|
||||||
|
cv2.circle(t, (75, 75), 60, (255, 255, 255), 4)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_clean(W: int, H: int, n_pieces: int = 1) -> np.ndarray:
|
||||||
|
np.random.seed(0)
|
||||||
|
s = np.zeros((H, W, 3), np.uint8)
|
||||||
|
for _ in range(n_pieces):
|
||||||
|
cx = np.random.randint(80, W - 80)
|
||||||
|
cy = np.random.randint(80, H - 80)
|
||||||
|
cv2.rectangle(s, (cx - 25, cy - 25), (cx + 25, cy + 25), (255, 255, 255), 3)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_cluttered(W: int, H: int) -> np.ndarray:
|
||||||
|
np.random.seed(0)
|
||||||
|
s = np.random.randint(50, 200, (H, W, 3), np.uint8)
|
||||||
|
cv2.rectangle(s, (300, 200), (350, 250), (255, 255, 255), 3)
|
||||||
|
cv2.rectangle(s, (1500, 800), (1550, 850), (255, 255, 255), 3)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Single benchmark ----------
|
||||||
|
|
||||||
|
def _bench_config(template, scene, config_name: str,
|
||||||
|
init_kw: dict, find_kw: dict,
|
||||||
|
n_iter: int = 5) -> dict:
|
||||||
|
m = LineShapeMatcher(**init_kw)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
n_var = m.train(template)
|
||||||
|
t_train = time.perf_counter() - t0
|
||||||
|
|
||||||
|
# Warmup (Numba JIT)
|
||||||
|
m.find(scene, **find_kw)
|
||||||
|
m.find(scene, **find_kw)
|
||||||
|
|
||||||
|
# Run
|
||||||
|
times_ms = []
|
||||||
|
for _ in range(n_iter):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
matches = m.find(scene, **find_kw)
|
||||||
|
times_ms.append((time.perf_counter() - t0) * 1000.0)
|
||||||
|
|
||||||
|
# Profile (1 iter)
|
||||||
|
m.find(scene, profile=True, **find_kw)
|
||||||
|
prof = m.get_last_profile() or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"config": config_name,
|
||||||
|
"n_variants": n_var,
|
||||||
|
"t_train_s": round(t_train, 3),
|
||||||
|
"ms_avg": round(float(np.mean(times_ms)), 1),
|
||||||
|
"ms_min": round(float(np.min(times_ms)), 1),
|
||||||
|
"ms_max": round(float(np.max(times_ms)), 1),
|
||||||
|
"n_matches": len(matches),
|
||||||
|
"profile_ms": {k: round(v, 1) for k, v in prof.items()},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Suite ----------
|
||||||
|
|
||||||
|
CONFIGS = [
|
||||||
|
("baseline",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||||
|
("polarity",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2, "use_polarity": True},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2}),
|
||||||
|
("propagate",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 3},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2,
|
||||||
|
"pyramid_propagate": True, "propagate_topk": 4}),
|
||||||
|
("greedy_07",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2, "greediness": 0.7}),
|
||||||
|
("stride2",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2, "coarse_stride": 2}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if opencl_available():
|
||||||
|
CONFIGS.append(
|
||||||
|
("gpu_umat",
|
||||||
|
{"angle_step_deg": 10, "pyramid_levels": 2, "use_gpu": True},
|
||||||
|
{"min_score": 0.4, "verify_threshold": 0.2})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SCENARIOS = [
|
||||||
|
("rect_80 vs scene_800x600", _tpl_rect, lambda: _scene_clean(800, 600, 1)),
|
||||||
|
("lshape_120 vs scene_1080x1920_clutter",
|
||||||
|
_tpl_lshape, lambda: _scene_cluttered(1920, 1080)),
|
||||||
|
("circle_150 vs scene_clean_3pieces",
|
||||||
|
_tpl_circle, lambda: _scene_clean(1920, 1080, 3)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run(quick: bool = False) -> int:
|
||||||
|
n_iter = 2 if quick else 5
|
||||||
|
print(f"=== PM2D Benchmark Suite ({len(SCENARIOS)} scenarios x "
|
||||||
|
f"{len(CONFIGS)} configs, n_iter={n_iter}) ===\n")
|
||||||
|
rows = []
|
||||||
|
for sc_name, tpl_fn, scn_fn in SCENARIOS:
|
||||||
|
template = tpl_fn()
|
||||||
|
scene = scn_fn()
|
||||||
|
print(f"--- Scenario: {sc_name} (tpl={template.shape}, "
|
||||||
|
f"scn={scene.shape}) ---")
|
||||||
|
for cfg_name, init_kw, find_kw in CONFIGS:
|
||||||
|
r = _bench_config(template, scene, cfg_name, init_kw, find_kw,
|
||||||
|
n_iter=n_iter)
|
||||||
|
r["scenario"] = sc_name
|
||||||
|
rows.append(r)
|
||||||
|
prof_str = " ".join(
|
||||||
|
f"{k}={v:.1f}" for k, v in r["profile_ms"].items()
|
||||||
|
)
|
||||||
|
print(f" {cfg_name:14s} {r['ms_avg']:6.1f}ms "
|
||||||
|
f"(min {r['ms_min']:.1f} max {r['ms_max']:.1f}) "
|
||||||
|
f"vars={r['n_variants']:3d} "
|
||||||
|
f"matches={r['n_matches']:2d}")
|
||||||
|
if prof_str:
|
||||||
|
print(f" profile: {prof_str}")
|
||||||
|
print()
|
||||||
|
print("=== Done ===")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser(description="PM2D benchmark suite")
|
||||||
|
p.add_argument("--quick", action="store_true",
|
||||||
|
help="2 iterazioni per config invece di 5 (smoke test)")
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
return run(quick=args.quick)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
sys.exit(main())
|
||||||
+108
-4
@@ -127,6 +127,7 @@ class Match:
|
|||||||
scale: float
|
scale: float
|
||||||
score: float
|
score: float
|
||||||
bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato)
|
bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato)
|
||||||
|
variant_idx: int = -1 # indice variante usata (per overlay coerente)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -735,7 +736,24 @@ class LineShapeMatcher:
|
|||||||
nb = self._n_bins
|
nb = self._n_bins
|
||||||
dtype = np.uint16 if nb > 8 else np.uint8
|
dtype = np.uint16 if nb > 8 else np.uint8
|
||||||
spread = np.zeros((H, W), dtype=dtype)
|
spread = np.zeros((H, W), dtype=dtype)
|
||||||
|
# XX optimization: skip dilate per bin senza pixel attivi.
|
||||||
|
# Su scene a bassa varianza orientation (es. pezzi industriali con
|
||||||
|
# poche direzioni dominanti) tipicamente 50-70% dei bin sono vuoti.
|
||||||
|
# Pre-calcolo bin presenti via mask globale; per bin assenti niente
|
||||||
|
# dilate (resta zero nel bitmap).
|
||||||
|
if isinstance(bins, np.ndarray):
|
||||||
|
valid_bins = bins[valid] if isinstance(valid, np.ndarray) else None
|
||||||
|
if valid_bins is not None and valid_bins.size > 0:
|
||||||
|
bin_present = np.zeros(nb, dtype=bool)
|
||||||
|
unique_bins = np.unique(valid_bins)
|
||||||
|
bin_present[unique_bins[unique_bins < nb]] = True
|
||||||
|
else:
|
||||||
|
bin_present = np.zeros(nb, dtype=bool)
|
||||||
|
else:
|
||||||
|
bin_present = np.ones(nb, dtype=bool)
|
||||||
for b in range(nb):
|
for b in range(nb):
|
||||||
|
if not bin_present[b]:
|
||||||
|
continue # XX: nessun pixel di questo bin sopra weak_grad
|
||||||
mask_b = ((bins == b) & valid).astype(np.uint8)
|
mask_b = ((bins == b) & valid).astype(np.uint8)
|
||||||
if self.use_gpu:
|
if self.use_gpu:
|
||||||
d = cv2.dilate(cv2.UMat(mask_b), kernel)
|
d = cv2.dilate(cv2.UMat(mask_b), kernel)
|
||||||
@@ -936,7 +954,7 @@ class LineShapeMatcher:
|
|||||||
# variante e' quantizzato a multipli di angle_step (5 deg default).
|
# variante e' quantizzato a multipli di angle_step (5 deg default).
|
||||||
# Refine angolare e' essenziale per orientamento sub-step.
|
# Refine angolare e' essenziale per orientamento sub-step.
|
||||||
if search_radius is None:
|
if search_radius is None:
|
||||||
search_radius = self._effective_angle_step() / 2.0
|
search_radius = self._effective_angle_step()
|
||||||
|
|
||||||
h, w = template_gray.shape
|
h, w = template_gray.shape
|
||||||
sw = max(16, int(round(w * scale)))
|
sw = max(16, int(round(w * scale)))
|
||||||
@@ -1024,8 +1042,11 @@ class LineShapeMatcher:
|
|||||||
# Score all'origine come riferimento (ang offset 0)
|
# Score all'origine come riferimento (ang offset 0)
|
||||||
s0, cx0_s, cy0_s = _score_at_angle(0.0)
|
s0, cx0_s, cy0_s = _score_at_angle(0.0)
|
||||||
best = (angle_deg, s0, cx0_s, cy0_s)
|
best = (angle_deg, s0, cx0_s, cy0_s)
|
||||||
tol = 0.1 # gradi
|
# Precisione angolare: tol piu' stretto + piu' iterazioni golden.
|
||||||
for _ in range(8):
|
# 16 iter golden coprono ~1e-3 della search_radius -> precisione
|
||||||
|
# ~0.005 deg per step=10 deg.
|
||||||
|
tol = 0.02
|
||||||
|
for _ in range(16):
|
||||||
if s1 > best[1]:
|
if s1 > best[1]:
|
||||||
best = (angle_deg + x1, s1, cx1, cy1)
|
best = (angle_deg + x1, s1, cx1, cy1)
|
||||||
if s2 > best[1]:
|
if s2 > best[1]:
|
||||||
@@ -1042,6 +1063,22 @@ class LineShapeMatcher:
|
|||||||
x1 = x2; s1 = s2; cx1 = cx2; cy1 = cy2
|
x1 = x2; s1 = s2; cx1 = cx2; cy1 = cy2
|
||||||
x2 = a_lo + _GOLDEN * (a_hi - a_lo)
|
x2 = a_lo + _GOLDEN * (a_hi - a_lo)
|
||||||
s2, cx2, cy2 = _score_at_angle(x2)
|
s2, cx2, cy2 = _score_at_angle(x2)
|
||||||
|
# Parabolic fit finale sui 3 punti vicini al best per refinement
|
||||||
|
# sub-step ulteriore (precisione <0.01 deg quando lo score map e'
|
||||||
|
# smooth attorno al picco).
|
||||||
|
best_off = best[0] - angle_deg
|
||||||
|
delta = max(0.05, abs(a_hi - a_lo) * 0.5)
|
||||||
|
sm, _, _ = _score_at_angle(best_off - delta)
|
||||||
|
sp, _, _ = _score_at_angle(best_off + delta)
|
||||||
|
# Vertice parabola su 3 punti (sm, sb, sp) a (-delta, 0, +delta)
|
||||||
|
denom = sm - 2 * best[1] + sp
|
||||||
|
if abs(denom) > 1e-9:
|
||||||
|
shift = 0.5 * (sm - sp) / denom * delta
|
||||||
|
shift = max(-delta, min(delta, shift))
|
||||||
|
new_off = best_off + shift
|
||||||
|
sn, cxn, cyn = _score_at_angle(new_off)
|
||||||
|
if sn >= best[1]:
|
||||||
|
return (angle_deg + new_off, sn, cxn, cyn)
|
||||||
return best
|
return best
|
||||||
|
|
||||||
def _get_view_template(
|
def _get_view_template(
|
||||||
@@ -1357,6 +1394,7 @@ class LineShapeMatcher:
|
|||||||
use_soft_score: bool = False,
|
use_soft_score: bool = False,
|
||||||
subpixel_lm: bool = False,
|
subpixel_lm: bool = False,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
|
profile: bool = False,
|
||||||
) -> list[Match]:
|
) -> list[Match]:
|
||||||
"""
|
"""
|
||||||
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
||||||
@@ -1389,6 +1427,7 @@ class LineShapeMatcher:
|
|||||||
"drop_recall_low": 0,
|
"drop_recall_low": 0,
|
||||||
"drop_bbox_out_of_scene": 0,
|
"drop_bbox_out_of_scene": 0,
|
||||||
"drop_nms_iou": 0,
|
"drop_nms_iou": 0,
|
||||||
|
"n_variants_pruned_histogram": 0,
|
||||||
"n_final": 0,
|
"n_final": 0,
|
||||||
"top_thresh_used": 0.0,
|
"top_thresh_used": 0.0,
|
||||||
"verify_threshold_used": float(verify_threshold),
|
"verify_threshold_used": float(verify_threshold),
|
||||||
@@ -1400,7 +1439,21 @@ class LineShapeMatcher:
|
|||||||
}
|
}
|
||||||
self._last_diag = diag
|
self._last_diag = diag
|
||||||
|
|
||||||
|
# GGG: profile mode → timing per fase, esposto via get_last_profile()
|
||||||
|
import time as _time
|
||||||
|
prof = {} if profile else None
|
||||||
|
_t_prev = _time.perf_counter() if profile else 0.0
|
||||||
|
def _checkpoint(name: str):
|
||||||
|
nonlocal _t_prev
|
||||||
|
if prof is None:
|
||||||
|
return
|
||||||
|
now = _time.perf_counter()
|
||||||
|
prof[name] = (now - _t_prev) * 1000.0 # ms
|
||||||
|
_t_prev = now
|
||||||
|
self._last_profile = prof
|
||||||
|
|
||||||
gray_full = self._to_gray(scene_bgr)
|
gray_full = self._to_gray(scene_bgr)
|
||||||
|
_checkpoint("to_gray")
|
||||||
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
||||||
# ri-traslare le coordinate dei match a fine pipeline.
|
# ri-traslare le coordinate dei match a fine pipeline.
|
||||||
if search_roi is not None:
|
if search_roi is not None:
|
||||||
@@ -1439,6 +1492,7 @@ class LineShapeMatcher:
|
|||||||
spread0 = None
|
spread0 = None
|
||||||
bit_active_full = None
|
bit_active_full = None
|
||||||
density_full = None
|
density_full = None
|
||||||
|
_checkpoint("spread_top")
|
||||||
if nms_radius is None:
|
if nms_radius is None:
|
||||||
nms_radius = max(8, min(self.template_size) // 2)
|
nms_radius = max(8, min(self.template_size) // 2)
|
||||||
# Pruning adattivo allo step angolare: con step piccolo (<= 3 deg)
|
# Pruning adattivo allo step angolare: con step piccolo (<= 3 deg)
|
||||||
@@ -1500,6 +1554,38 @@ class LineShapeMatcher:
|
|||||||
end = min(n, i + half + 1)
|
end = min(n, i + half + 1)
|
||||||
neighbor_map[vi_c] = vi_sorted[start:end]
|
neighbor_map[vi_c] = vi_sorted[start:end]
|
||||||
|
|
||||||
|
# VV: pruning preliminare via overlap istogramma orientation.
|
||||||
|
# Scene-bins-attivi vs variant-feature-bins. Se la variante ha bin
|
||||||
|
# dominanti che la scena non possiede → score impossibile, skip
|
||||||
|
# senza chiamare il kernel. Costo: O(n_variants * 8 ops).
|
||||||
|
scene_bins = np.array(
|
||||||
|
[bool((bit_active_top >> b) & 1) for b in range(self._n_bins)],
|
||||||
|
dtype=bool,
|
||||||
|
)
|
||||||
|
if scene_bins.any():
|
||||||
|
n_scene_active = int(scene_bins.sum())
|
||||||
|
# Soglia: variante deve avere >= 50% delle sue feature in bin
|
||||||
|
# presenti nella scena. Sotto = score certamente < 0.5.
|
||||||
|
pruned_idx_list = []
|
||||||
|
n_pruned = 0
|
||||||
|
for vi in coarse_idx_list:
|
||||||
|
lvl = self.variants[vi].levels[
|
||||||
|
min(top, len(self.variants[vi].levels) - 1)
|
||||||
|
]
|
||||||
|
if len(lvl.bin) == 0:
|
||||||
|
continue
|
||||||
|
feat_in_scene = int(np.isin(lvl.bin, np.where(scene_bins)[0]).sum())
|
||||||
|
ratio = feat_in_scene / len(lvl.bin)
|
||||||
|
if ratio < 0.5 * min_score:
|
||||||
|
n_pruned += 1
|
||||||
|
continue
|
||||||
|
pruned_idx_list.append(vi)
|
||||||
|
if n_pruned > 0 and pruned_idx_list:
|
||||||
|
coarse_idx_list = pruned_idx_list
|
||||||
|
diag["n_variants_pruned_histogram"] = n_pruned
|
||||||
|
else:
|
||||||
|
diag["n_variants_pruned_histogram"] = 0
|
||||||
|
|
||||||
# Pruning varianti via top-level (parallelizzato).
|
# Pruning varianti via top-level (parallelizzato).
|
||||||
# coarse_stride > 1: 1 pixel ogni stride (~stride^2 speed-up).
|
# coarse_stride > 1: 1 pixel ogni stride (~stride^2 speed-up).
|
||||||
# pyramid_propagate=True: top-K picchi per restringere full-res.
|
# pyramid_propagate=True: top-K picchi per restringere full-res.
|
||||||
@@ -1595,6 +1681,7 @@ class LineShapeMatcher:
|
|||||||
kept_variants: list[tuple[int, float]] = [
|
kept_variants: list[tuple[int, float]] = [
|
||||||
(vi, score_by_vi[vi]) for vi in expanded
|
(vi, score_by_vi[vi]) for vi in expanded
|
||||||
]
|
]
|
||||||
|
_checkpoint("top_pruning")
|
||||||
|
|
||||||
if not kept_variants:
|
if not kept_variants:
|
||||||
return []
|
return []
|
||||||
@@ -1701,6 +1788,7 @@ class LineShapeMatcher:
|
|||||||
|
|
||||||
raw.sort(key=lambda c: -c[0])
|
raw.sort(key=lambda c: -c[0])
|
||||||
diag["n_raw_candidates"] = len(raw)
|
diag["n_raw_candidates"] = len(raw)
|
||||||
|
_checkpoint("full_kernel")
|
||||||
|
|
||||||
# Mappa vi → score_map per subpixel/refinement
|
# Mappa vi → score_map per subpixel/refinement
|
||||||
score_maps = dict(candidates_per_var)
|
score_maps = dict(candidates_per_var)
|
||||||
@@ -1756,7 +1844,10 @@ class LineShapeMatcher:
|
|||||||
ang_f, score_f, cx_f, cy_f = self._refine_angle(
|
ang_f, score_f, cx_f, cy_f = self._refine_angle(
|
||||||
spread0, bit_active_full, self.template_gray, cx_f, cy_f,
|
spread0, bit_active_full, self.template_gray, cx_f, cy_f,
|
||||||
var.angle_deg, var.scale, mask_full,
|
var.angle_deg, var.scale, mask_full,
|
||||||
search_radius=self._effective_angle_step() / 2.0,
|
# Search radius esteso allo step pieno (era step/2):
|
||||||
|
# copre il caso peggiore in cui il picco vero e' all'estremo
|
||||||
|
# del bin angolare della variante grezza.
|
||||||
|
search_radius=self._effective_angle_step(),
|
||||||
original_score=score,
|
original_score=score,
|
||||||
)
|
)
|
||||||
# Halcon SubPixel='least_squares_high': refinement iterativo
|
# Halcon SubPixel='least_squares_high': refinement iterativo
|
||||||
@@ -1863,10 +1954,14 @@ class LineShapeMatcher:
|
|||||||
scale=var.scale,
|
scale=var.scale,
|
||||||
score=score_f,
|
score=score_f,
|
||||||
bbox_poly=poly,
|
bbox_poly=poly,
|
||||||
|
variant_idx=int(vi),
|
||||||
))
|
))
|
||||||
if len(kept) >= max_matches:
|
if len(kept) >= max_matches:
|
||||||
break
|
break
|
||||||
diag["n_final"] = len(kept)
|
diag["n_final"] = len(kept)
|
||||||
|
_checkpoint("refine_verify_nms")
|
||||||
|
if profile:
|
||||||
|
self._last_profile = prof
|
||||||
if debug:
|
if debug:
|
||||||
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
@@ -1890,6 +1985,15 @@ class LineShapeMatcher:
|
|||||||
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_last_profile(self) -> dict | None:
|
||||||
|
"""Ritorna timing per fase dell'ultimo find(profile=True).
|
||||||
|
|
||||||
|
Chiavi: to_gray, spread_top, top_pruning, full_kernel,
|
||||||
|
refine_verify_nms (millisecondi). Util per identificare bottleneck
|
||||||
|
dove ottimizzare.
|
||||||
|
"""
|
||||||
|
return getattr(self, "_last_profile", None)
|
||||||
|
|
||||||
def get_last_diag(self) -> dict | None:
|
def get_last_diag(self) -> dict | None:
|
||||||
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
||||||
|
|
||||||
|
|||||||
+210
-36
@@ -78,6 +78,7 @@ def _matcher_cache_key(roi: np.ndarray, tech: dict) -> str:
|
|||||||
h.update(roi.tobytes())
|
h.update(roi.tobytes())
|
||||||
# Solo parametri che influenzano il training
|
# Solo parametri che influenzano il training
|
||||||
relevant = ("num_features", "weak_grad", "strong_grad",
|
relevant = ("num_features", "weak_grad", "strong_grad",
|
||||||
|
"min_feature_spacing",
|
||||||
"angle_min", "angle_max", "angle_step",
|
"angle_min", "angle_max", "angle_step",
|
||||||
"scale_min", "scale_max", "scale_step",
|
"scale_min", "scale_max", "scale_step",
|
||||||
"spread_radius", "pyramid_levels")
|
"spread_radius", "pyramid_levels")
|
||||||
@@ -131,45 +132,87 @@ def _encode_png(img: np.ndarray) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def _draw_matches(scene: np.ndarray, matches: list[Match],
|
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()
|
out = scene.copy()
|
||||||
H, W = scene.shape[:2]
|
# Lunghezza assi UCS: stessa formula dell'anteprima modello
|
||||||
palette = [
|
# (0.15 * max lato template) scalata per m.scale → coerenza dimensionale.
|
||||||
(0, 255, 0), (0, 200, 255), (255, 100, 100), (255, 200, 0),
|
if matcher is not None and matcher.template_size != (0, 0):
|
||||||
(200, 0, 255), (100, 255, 200), (255, 0, 0), (0, 255, 255),
|
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):
|
for i, m in enumerate(matches):
|
||||||
color = palette[i % len(palette)]
|
# UCS posizionato esattamente sul CENTRO POSE del match (m.cx, m.cy):
|
||||||
if template_gray is not None:
|
# 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
|
t = template_gray
|
||||||
th, tw = t.shape
|
th, tw = t.shape
|
||||||
edge = cv2.Canny(t, 50, 150)
|
|
||||||
cx_t = (tw - 1) / 2.0; cy_t = (th - 1) / 2.0
|
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 = cv2.getRotationMatrix2D((cx_t, cy_t), m.angle_deg, m.scale)
|
||||||
M[0, 2] += m.cx - cx_t
|
M[0, 2] += m.cx - cx_t
|
||||||
M[1, 2] += m.cy - cy_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)
|
flags=cv2.INTER_NEAREST, borderValue=0)
|
||||||
mask = warped > 0
|
# Erode di spread_radius per scartare la fascia di transizione
|
||||||
if mask.any():
|
# bordo che produce gradient spurio
|
||||||
overlay = np.zeros_like(out)
|
er_k = max(3, 2 * matcher.spread_radius + 1)
|
||||||
overlay[mask] = color
|
kernel_er = np.ones((er_k, er_k), np.uint8)
|
||||||
out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8)
|
warped_mask = cv2.erode(warped_mask, kernel_er)
|
||||||
poly = m.bbox_poly.astype(np.int32).reshape(-1, 1, 2)
|
mag, _ = matcher._gradient(warped_gray)
|
||||||
cv2.polylines(out, [poly], True, color, 2, cv2.LINE_AA)
|
if matcher.weak_grad < matcher.strong_grad:
|
||||||
p0 = tuple(m.bbox_poly[0].astype(int))
|
edge_mask = matcher._hysteresis_mask(mag)
|
||||||
p1 = tuple(m.bbox_poly[1].astype(int))
|
else:
|
||||||
cv2.line(out, p0, p1, color, 4, cv2.LINE_AA)
|
edge_mask = mag >= matcher.strong_grad
|
||||||
cx, cy = int(round(m.cx)), int(round(m.cy))
|
edge_mask = edge_mask & (warped_mask > 0)
|
||||||
cv2.drawMarker(out, (cx, cy), color, cv2.MARKER_CROSS, 22, 2, cv2.LINE_AA)
|
if edge_mask.any():
|
||||||
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
edge_overlay = np.zeros_like(out)
|
||||||
a = np.deg2rad(m.angle_deg)
|
edge_overlay[edge_mask] = (0, 220, 0) # verde brillante
|
||||||
cv2.arrowedLine(out, (cx, cy),
|
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
|
||||||
(int(cx + L * np.cos(a)), int(cy - L * np.sin(a))),
|
L = max(20, int(L_base * m.scale))
|
||||||
color, 2, cv2.LINE_AA, tipLength=0.2)
|
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
|
||||||
label = f"#{i+1} {m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.2f}"
|
x_end = (int(cx + L * ca), int(cy - L * sa))
|
||||||
cv2.putText(out, label, (cx + 8, cy - 8),
|
# Y axis = rotazione di (0, 1) con cv2 matrix → (sin, cos)
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
# 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
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -272,6 +315,15 @@ class SimpleMatchParams(BaseModel):
|
|||||||
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
||||||
min_score: float = 0.65
|
min_score: float = 0.65
|
||||||
max_matches: int = 25
|
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) ---
|
# --- Halcon-mode flags (default off = backward compat) ---
|
||||||
# Init-time (richiede ri-train se cambiato)
|
# Init-time (richiede ri-train se cambiato)
|
||||||
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
||||||
@@ -320,10 +372,24 @@ def _simple_to_technical(
|
|||||||
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
||||||
ang_step = PRECISION_ANGLE_STEP.get(p.precisione, 5.0)
|
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 {
|
return {
|
||||||
"num_features": nf,
|
"num_features": n_feat,
|
||||||
"weak_grad": tune["weak_grad"],
|
"weak_grad": weak_g,
|
||||||
"strong_grad": tune["strong_grad"],
|
"strong_grad": strong_g,
|
||||||
|
"min_feature_spacing": min_sp,
|
||||||
"spread_radius": spread,
|
"spread_radius": spread,
|
||||||
"pyramid_levels": pyr,
|
"pyramid_levels": pyr,
|
||||||
"angle_min": 0.0,
|
"angle_min": 0.0,
|
||||||
@@ -511,7 +577,7 @@ def match(p: MatchParams):
|
|||||||
|
|
||||||
# Render annotated image
|
# Render annotated image
|
||||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
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)
|
ann_id = _store_image(annotated)
|
||||||
|
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
@@ -559,6 +625,7 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
scale_range=(tech["scale_min"], tech["scale_max"]),
|
scale_range=(tech["scale_min"], tech["scale_max"]),
|
||||||
scale_step=tech["scale_step"],
|
scale_step=tech["scale_step"],
|
||||||
spread_radius=tech["spread_radius"],
|
spread_radius=tech["spread_radius"],
|
||||||
|
min_feature_spacing=tech.get("min_feature_spacing", 3),
|
||||||
pyramid_levels=tech["pyramid_levels"],
|
pyramid_levels=tech["pyramid_levels"],
|
||||||
use_polarity=p.use_polarity,
|
use_polarity=p.use_polarity,
|
||||||
use_gpu=p.use_gpu,
|
use_gpu=p.use_gpu,
|
||||||
@@ -588,7 +655,7 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
t_find = time.time() - t0
|
t_find = time.time() - t0
|
||||||
|
|
||||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
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)
|
ann_id = _store_image(annotated)
|
||||||
|
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
@@ -628,9 +695,112 @@ class SaveRecipeParams(BaseModel):
|
|||||||
precisione: str = "normale"
|
precisione: str = "normale"
|
||||||
use_polarity: bool = False
|
use_polarity: bool = False
|
||||||
use_gpu: 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)
|
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")
|
@app.post("/recipes")
|
||||||
def save_recipe(p: SaveRecipeParams):
|
def save_recipe(p: SaveRecipeParams):
|
||||||
"""Allena matcher e salva su disco come ricetta riutilizzabile."""
|
"""Allena matcher e salva su disco come ricetta riutilizzabile."""
|
||||||
@@ -644,6 +814,10 @@ def save_recipe(p: SaveRecipeParams):
|
|||||||
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
||||||
precisione=p.precisione,
|
precisione=p.precisione,
|
||||||
use_polarity=p.use_polarity, use_gpu=p.use_gpu,
|
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)
|
tech = _simple_to_technical(sp, roi_img)
|
||||||
m = LineShapeMatcher(
|
m = LineShapeMatcher(
|
||||||
@@ -763,7 +937,7 @@ def match_recipe(p: RecipeMatchParams):
|
|||||||
)
|
)
|
||||||
t_find = time.time() - t0
|
t_find = time.time() - t0
|
||||||
tg = m.template_gray if m.template_gray is not None else np.zeros((1, 1), np.uint8)
|
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)
|
ann_id = _store_image(annotated)
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
matches=[MatchResult(
|
matches=[MatchResult(
|
||||||
|
|||||||
@@ -53,10 +53,34 @@ function readUserParams() {
|
|||||||
document.getElementById("p-penalita-scala").value),
|
document.getElementById("p-penalita-scala").value),
|
||||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||||
|
...readEdgeOverrides(),
|
||||||
...readHalconFlags(),
|
...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() {
|
function readHalconFlags() {
|
||||||
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
||||||
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
||||||
@@ -438,6 +462,109 @@ function setStatus(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Init ----------
|
// ---------- 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 ----------
|
// ---------- CC: Diagnostica match ----------
|
||||||
function renderDiag(diag, n_matches) {
|
function renderDiag(diag, n_matches) {
|
||||||
const el = document.getElementById("diag-content");
|
const el = document.getElementById("diag-content");
|
||||||
@@ -613,6 +740,10 @@ async function saveRecipe() {
|
|||||||
precisione: user.precisione,
|
precisione: user.precisione,
|
||||||
use_polarity: user.use_polarity,
|
use_polarity: user.use_polarity,
|
||||||
use_gpu: user.use_gpu,
|
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,
|
name: name,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -665,6 +796,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
document.getElementById("btn-unload-recipe").addEventListener("click",
|
document.getElementById("btn-unload-recipe").addEventListener("click",
|
||||||
unloadRecipe);
|
unloadRecipe);
|
||||||
refreshRecipeList();
|
refreshRecipeList();
|
||||||
|
bindEdgePreviewControls();
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
slider.addEventListener("input", (e) => {
|
slider.addEventListener("input", (e) => {
|
||||||
document.getElementById("v-score").textContent =
|
document.getElementById("v-score").textContent =
|
||||||
|
|||||||
@@ -45,6 +45,40 @@
|
|||||||
<canvas id="c-model" width="380" height="420"></canvas>
|
<canvas id="c-model" width="380" height="420"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div id="roi-info">ROI: (nessuna)</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>
|
||||||
|
|
||||||
<section class="col" id="col-scene">
|
<section class="col" id="col-scene">
|
||||||
@@ -68,8 +102,8 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Simmetria</label>
|
<label>Simmetria</label>
|
||||||
<select id="p-simmetria">
|
<select id="p-simmetria">
|
||||||
|
<option value="nessuna" selected>Nessuna (0..360°)</option>
|
||||||
<option value="invariante">Invariante (cerchi — no rotazione)</option>
|
<option value="invariante">Invariante (cerchi — no rotazione)</option>
|
||||||
<option value="nessuna">Nessuna (0..360°)</option>
|
|
||||||
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
||||||
<option value="rot_3">Rotazionale 3× (120°)</option>
|
<option value="rot_3">Rotazionale 3× (120°)</option>
|
||||||
<option value="rot_4">Rotazionale 4× (90°)</option>
|
<option value="rot_4">Rotazionale 4× (90°)</option>
|
||||||
|
|||||||
@@ -173,3 +173,18 @@ footer h2 {
|
|||||||
}
|
}
|
||||||
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||||
.hc-row.hc-num input { width: 100%; }
|
.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; }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies = [
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pm2d-eval = "pm2d.eval:main"
|
pm2d-eval = "pm2d.eval:main"
|
||||||
|
pm2d-bench = "pm2d.bench:main"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in New Issue
Block a user