Compare commits

..

1 Commits

Author SHA1 Message Date
Adriano ba4024d252 feat: search_roi parametro find() per limitare area di ricerca
Equivalente a Halcon set_aoi: matching opera su crop locale, coord
output ri-traslate al sistema scena. Costo proporzionale a w*h del
ROI invece di W*H scena intera.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:22:43 +02:00
+34 -32
View File
@@ -239,8 +239,6 @@ class LineShapeMatcher:
self._train_mask = mask_full.copy() self._train_mask = mask_full.copy()
self.variants.clear() self.variants.clear()
# Invalida cache feature di refine: il template e cambiato.
self._refine_feat_cache = {}
for s in self._scale_list(): for s in self._scale_list():
sw = max(16, int(round(w * s))) sw = max(16, int(round(w * s)))
sh = max(16, int(round(h * s))) sh = max(16, int(round(h * s)))
@@ -435,36 +433,17 @@ class LineShapeMatcher:
H, W = spread0.shape H, W = spread0.shape
margin = 3 margin = 3
# Cache template features per angolo (chiave: int(round(ang*20)) =
# bucket di 0.05°). Golden-search ricontratto puo richiedere lo
# stesso bucket piu volte; evita re-warp+gradient+extract (costoso).
# Cache a livello matcher per riusare tra chiamate find() su scene
# diverse: la rotazione del template non dipende dalla scena.
if not hasattr(self, '_refine_feat_cache'):
self._refine_feat_cache = {}
feat_cache = self._refine_feat_cache
cache_scale_key = round(scale * 1000)
def _score_at_angle(off: float) -> tuple[float, float, float]: def _score_at_angle(off: float) -> tuple[float, float, float]:
"""Ritorna (score, best_cx, best_cy) per angolo = angle_deg + off.""" """Ritorna (score, best_cx, best_cy) per angolo = angle_deg + off."""
ang = angle_deg + off ang = angle_deg + off
ck = (round(ang * 20), cache_scale_key) M = cv2.getRotationMatrix2D(center, ang, 1.0)
cached = feat_cache.get(ck) gray_r = cv2.warpAffine(gray_p, M, (diag, diag),
if cached is not None: flags=cv2.INTER_LINEAR,
fx, fy, fb = cached borderMode=cv2.BORDER_REPLICATE)
else: mask_r = cv2.warpAffine(mask_p, M, (diag, diag),
M = cv2.getRotationMatrix2D(center, ang, 1.0) flags=cv2.INTER_NEAREST, borderValue=0)
gray_r = cv2.warpAffine(gray_p, M, (diag, diag), mag, bins = self._gradient(gray_r)
flags=cv2.INTER_LINEAR, fx, fy, fb = self._extract_features(mag, bins, mask_r)
borderMode=cv2.BORDER_REPLICATE)
mask_r = cv2.warpAffine(mask_p, M, (diag, diag),
flags=cv2.INTER_NEAREST, borderValue=0)
mag, bins = self._gradient(gray_r)
fx, fy, fb = self._extract_features(mag, bins, mask_r)
# LRU semplice: limita cache a ~256 angoli (8 angoli * 32 candidati)
if len(feat_cache) > 256:
feat_cache.pop(next(iter(feat_cache)))
feat_cache[ck] = (fx, fy, fb)
if len(fx) < 8: if len(fx) < 8:
return (0.0, cx, cy) return (0.0, cx, cy)
dx = (fx - center[0]).astype(np.int32) dx = (fx - center[0]).astype(np.int32)
@@ -595,6 +574,7 @@ 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:
@@ -602,11 +582,30 @@ 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.")
gray0 = self._to_gray(scene_bgr) 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.
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]))
@@ -831,8 +830,11 @@ 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_f, cy_f, tw * var.scale, th * var.scale, ang_f, cx_out, cy_out, 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:
@@ -840,7 +842,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_f, cy=cy_f, cx=cx_out, cy=cy_out,
angle_deg=ang_f, angle_deg=ang_f,
scale=var.scale, scale=var.scale,
score=score_f, score=score_f,