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 a671f3f..b8a41b8 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))) @@ -880,7 +899,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: