From d9a40952c44cc0679ea1d05078edddc987aadd33 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 4 May 2026 15:27:35 +0200 Subject: [PATCH] feat: angle_step auto adattivo a dimensione template Halcon-style: angle_step_deg=0 attiva derivazione automatica step = atan(2/max_side) deg, clampato [0.5, 10]. Template grande ottiene step fine, piccolo step grosso. auto_tune emette il valore calcolato direttamente. _refine_angle ora usa _effective_angle_step() per coerenza con training quando la modalita auto e attiva. Co-Authored-By: Claude Opus 4.7 (1M context) --- pm2d/auto_tune.py | 7 +++++-- pm2d/line_matcher.py | 29 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/pm2d/auto_tune.py b/pm2d/auto_tune.py index 4ca7bc2..3c83b7b 100644 --- a/pm2d/auto_tune.py +++ b/pm2d/auto_tune.py @@ -220,8 +220,11 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict: else: min_score = 0.45 - # angle step: 5° default; se simmetria, mantengo step ma range ridotto - angle_step = 5.0 + # angle step adattivo (Halcon-style): atan(2/max_side) deg, clampato. + # Template grande → step fine (rotazione minima visibile su perimetro). + # Template piccolo → step grosso (over-sampling = sprecato). + max_side = max(h, w) + angle_step = float(np.clip(np.degrees(np.arctan2(2.0, max_side)), 1.0, 8.0)) result = { "backend": "line", diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index e5f212a..172c8dc 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -197,12 +197,31 @@ class LineShapeMatcher: n = int(np.floor((s1 - s0) / self.scale_step)) + 1 return [float(s0 + i * self.scale_step) for i in range(n)] + def _auto_angle_step(self) -> float: + """Step angolare derivato da dimensione template (Halcon-style). + + Formula: step ≈ atan(2 / max_side) gradi. Garantisce che la + rotazione minima produca uno spostamento di ≥2 px sul perimetro + del template (sotto sample il matching coarse perde candidati). + Clampato in [0.5°, 10°]. + """ + max_side = max(self.template_size) if self.template_size != (0, 0) else 64 + step = math.degrees(math.atan2(2.0, float(max_side))) + return float(np.clip(step, 0.5, 10.0)) + + def _effective_angle_step(self) -> float: + """Risolve angle_step_deg gestendo modalità auto (<=0).""" + if self.angle_step_deg <= 0: + return self._auto_angle_step() + return self.angle_step_deg + def _angle_list(self) -> list[float]: a0, a1 = self.angle_range_deg - if self.angle_step_deg <= 0 or a0 >= a1: + step = self._effective_angle_step() + if step <= 0 or a0 >= a1: return [float(a0)] - n = int(np.floor((a1 - a0) / self.angle_step_deg)) - return [float(a0 + i * self.angle_step_deg) for i in range(n)] + n = int(np.floor((a1 - a0) / step)) + return [float(a0 + i * step) for i in range(n)] # --- Training ------------------------------------------------------ @@ -415,7 +434,7 @@ class LineShapeMatcher: if original_score is not None and original_score >= 0.99: return (angle_deg, original_score, cx, cy) if search_radius is None: - search_radius = self.angle_step_deg / 2.0 + search_radius = self._effective_angle_step() / 2.0 h, w = template_gray.shape sw = max(16, int(round(w * scale))) @@ -802,7 +821,7 @@ class LineShapeMatcher: ang_f, score_f, cx_f, cy_f = self._refine_angle( spread0, bit_active_full, self.template_gray, cx_f, cy_f, var.angle_deg, var.scale, mask_full, - search_radius=self.angle_step_deg / 2.0, + search_radius=self._effective_angle_step() / 2.0, original_score=score, ) if verify_ncc: