From f00cf9b6217a67a9e58aaee6427b19feca81e51e Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 4 May 2026 15:31:37 +0200 Subject: [PATCH] feat: cache features template per _refine_angle Cache LRU (chiave: angolo arrotondato a 0.05deg, scale) di (fx, fy, fb) per evitare warpAffine + gradient + extract ripetuti durante golden-search refine. Bucket condiviso tra match della stessa find() e tra find() consecutive sulla stessa ricetta. Cache invalidata in train(): il template puo essere cambiato. Limite 256 entry (sufficiente per 32 candidati x 8 valutazioni). Co-Authored-By: Claude Opus 4.7 (1M context) --- pm2d/line_matcher.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index e5f212a..72cf49b 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -239,6 +239,8 @@ class LineShapeMatcher: self._train_mask = mask_full.copy() self.variants.clear() + # Invalida cache feature di refine: il template e cambiato. + self._refine_feat_cache = {} for s in self._scale_list(): sw = max(16, int(round(w * s))) sh = max(16, int(round(h * s))) @@ -433,17 +435,36 @@ class LineShapeMatcher: H, W = spread0.shape 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]: """Ritorna (score, best_cx, best_cy) per angolo = angle_deg + off.""" ang = angle_deg + off - M = cv2.getRotationMatrix2D(center, ang, 1.0) - gray_r = cv2.warpAffine(gray_p, M, (diag, diag), - flags=cv2.INTER_LINEAR, - 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) + ck = (round(ang * 20), cache_scale_key) + cached = feat_cache.get(ck) + if cached is not None: + fx, fy, fb = cached + else: + M = cv2.getRotationMatrix2D(center, ang, 1.0) + gray_r = cv2.warpAffine(gray_p, M, (diag, diag), + flags=cv2.INTER_LINEAR, + 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: return (0.0, cx, cy) dx = (fx - center[0]).astype(np.int32)