Compare commits

..

1 Commits

Author SHA1 Message Date
Adriano d9a40952c4 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) <noreply@anthropic.com>
2026-05-04 15:27:35 +02:00
2 changed files with 32 additions and 33 deletions
+5 -2
View File
@@ -220,8 +220,11 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
else: else:
min_score = 0.45 min_score = 0.45
# angle step: 5° default; se simmetria, mantengo step ma range ridotto # angle step adattivo (Halcon-style): atan(2/max_side) deg, clampato.
angle_step = 5.0 # 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 = { result = {
"backend": "line", "backend": "line",
+27 -31
View File
@@ -197,12 +197,31 @@ class LineShapeMatcher:
n = int(np.floor((s1 - s0) / self.scale_step)) + 1 n = int(np.floor((s1 - s0) / self.scale_step)) + 1
return [float(s0 + i * self.scale_step) for i in range(n)] 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]: def _angle_list(self) -> list[float]:
a0, a1 = self.angle_range_deg 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)] return [float(a0)]
n = int(np.floor((a1 - a0) / self.angle_step_deg)) n = int(np.floor((a1 - a0) / step))
return [float(a0 + i * self.angle_step_deg) for i in range(n)] return [float(a0 + i * step) for i in range(n)]
# --- Training ------------------------------------------------------ # --- Training ------------------------------------------------------
@@ -415,7 +434,7 @@ class LineShapeMatcher:
if original_score is not None and original_score >= 0.99: if original_score is not None and original_score >= 0.99:
return (angle_deg, original_score, cx, cy) return (angle_deg, original_score, cx, cy)
if search_radius is None: 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 h, w = template_gray.shape
sw = max(16, int(round(w * scale))) sw = max(16, int(round(w * scale)))
@@ -574,7 +593,6 @@ class LineShapeMatcher:
verify_threshold: float = 0.4, verify_threshold: float = 0.4,
coarse_angle_factor: int = 2, coarse_angle_factor: int = 2,
scale_penalty: float = 0.0, scale_penalty: float = 0.0,
search_roi: tuple[int, int, int, int] | None = None,
) -> 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:
@@ -582,30 +600,11 @@ class LineShapeMatcher:
Utile se l'operatore vuole che match "identico al template anche per Utile se l'operatore vuole che match "identico al template anche per
dimensione" abbia score più alto di match "stessa forma, dimensione dimensione" abbia score più alto di match "stessa forma, dimensione
diversa". scale_penalty=0 (default) = comportamento shape puro. diversa". scale_penalty=0 (default) = comportamento shape puro.
search_roi: (x, y, w, h) limita la ricerca a una regione della scena.
Equivalente a Halcon set_aoi: il matching opera su crop locale e le
coordinate output sono ri-traslate al sistema scena originale. Usare
quando si conosce a priori l'area in cui il pezzo può apparire (es.
feeder a posizione fissa) → costo proporzionale a w·h invece di W·H.
""" """
if not self.variants: if not self.variants:
raise RuntimeError("Matcher non addestrato: chiamare train() prima.") raise RuntimeError("Matcher non addestrato: chiamare train() prima.")
gray_full = self._to_gray(scene_bgr) gray0 = 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.
if search_roi is not None:
rx, ry, rw, rh = search_roi
H_s, W_s = gray_full.shape
rx = max(0, int(rx)); ry = max(0, int(ry))
rw = max(1, min(int(rw), W_s - rx))
rh = max(1, min(int(rh), H_s - ry))
gray0 = gray_full[ry:ry + rh, rx:rx + rw]
roi_offset = (rx, ry)
else:
gray0 = gray_full
roi_offset = (0, 0)
grays = [gray0] grays = [gray0]
for _ in range(self.pyramid_levels - 1): for _ in range(self.pyramid_levels - 1):
grays.append(cv2.pyrDown(grays[-1])) grays.append(cv2.pyrDown(grays[-1]))
@@ -822,7 +821,7 @@ 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.angle_step_deg / 2.0, search_radius=self._effective_angle_step() / 2.0,
original_score=score, original_score=score,
) )
if verify_ncc: if verify_ncc:
@@ -830,11 +829,8 @@ class LineShapeMatcher:
if ncc < verify_threshold: if ncc < verify_threshold:
continue continue
# Ri-traslo coord da spazio crop ROI a spazio scena originale.
cx_out = cx_f + roi_offset[0]
cy_out = cy_f + roi_offset[1]
poly = _oriented_bbox_polygon( poly = _oriented_bbox_polygon(
cx_out, cy_out, tw * var.scale, th * var.scale, ang_f, cx_f, cy_f, tw * var.scale, th * var.scale, ang_f,
) )
# Penalità scala opzionale: score degrada con distanza da 1.0 # Penalità scala opzionale: score degrada con distanza da 1.0
if scale_penalty > 0.0 and var.scale != 1.0: if scale_penalty > 0.0 and var.scale != 1.0:
@@ -842,7 +838,7 @@ class LineShapeMatcher:
0.0, 1.0 - scale_penalty * abs(var.scale - 1.0) 0.0, 1.0 - scale_penalty * abs(var.scale - 1.0)
) )
kept.append(Match( kept.append(Match(
cx=cx_out, cy=cy_out, cx=cx_f, cy=cy_f,
angle_deg=ang_f, angle_deg=ang_f,
scale=var.scale, scale=var.scale,
score=score_f, score=score_f,