diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index 65d0361..daebe1e 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -512,8 +512,10 @@ class LineShapeMatcher: self.variants.clear() # Reset view list: template principale = view 0 self._view_templates = [(gray.copy(), mask_full.copy())] - # Invalida cache feature di refine: il template e cambiato. + # Invalida cache: template/param cambiati → spread/feature obsoleti. self._refine_feat_cache = {} + if hasattr(self, "_scene_cache"): + self._scene_cache.clear() self._build_variants_for_view(gray, mask_full, view_idx=0) self._dedup_variants() return len(self.variants) @@ -669,6 +671,51 @@ class LineShapeMatcher: raw[b] = d.astype(np.float32) return raw + # --- Scene precompute cache (II Halcon-style) ----------------------- + _SCENE_CACHE_SIZE = 4 + + def _scene_cache_key(self, gray: np.ndarray) -> str | None: + """Hash compatto della scena + param che influenzano spread/density. + + Hash su prime 64KB della scena (sufficiente discriminante per + scene fotografiche) + parametri matcher rilevanti. None se cache + disabilitata (es. scene troppo piccole). + """ + if gray.size < 100: + return None + try: + import hashlib + h = hashlib.md5() + sample = gray.tobytes()[:65536] + h.update(sample) + h.update(f"|{gray.shape}|{gray.dtype}".encode()) + h.update( + f"|{self.weak_grad}|{self.strong_grad}" + f"|{self.spread_radius}|{self._n_bins}" + f"|{self.pyramid_levels}".encode() + ) + return h.hexdigest() + except Exception: + return None + + def _scene_cache_get(self, key: str) -> tuple | None: + cache = getattr(self, "_scene_cache", None) + if cache is None: + return None + v = cache.get(key) + if v is not None: + cache.move_to_end(key) + return v + + def _scene_cache_put(self, key: str, value: tuple) -> None: + from collections import OrderedDict + if not hasattr(self, "_scene_cache"): + self._scene_cache = OrderedDict() + self._scene_cache[key] = value + self._scene_cache.move_to_end(key) + while len(self._scene_cache) > self._SCENE_CACHE_SIZE: + self._scene_cache.popitem(last=False) + def _spread_bitmap(self, gray: np.ndarray) -> np.ndarray: """Spread bitmap: bit b acceso dove bin b è presente nel raggio. @@ -1367,18 +1414,31 @@ class LineShapeMatcher: else: gray0 = gray_full roi_offset = (0, 0) - grays = [gray0] - for _ in range(self.pyramid_levels - 1): - grays.append(cv2.pyrDown(grays[-1])) - top = len(grays) - 1 - # Spread bitmap (uint8) al top level: 32× meno memoria della response - # map float32 → MOLTO più cache-friendly per _score_by_shift. - spread_top = self._spread_bitmap(grays[top]) - bit_active_top = int( - sum(1 << b for b in range(self._n_bins) - if (spread_top & (spread_top.dtype.type(1) << b)).any()) - ) + # Cache pre-compute scena (II Halcon-style): hash bytes scene + param + # gradient/spread → riusa spread piramide + density tra find() + # consecutive con stessa scena (typical UI tuning: slider produce + # 10+ find() su scena identica). Risparmia ~80% del costo non-kernel. + cache_key = self._scene_cache_key(gray0) + cached = self._scene_cache_get(cache_key) if cache_key else None + if cached is not None: + grays, spread_top, bit_active_top, density_top, spread0, \ + bit_active_full, density_full, top = cached + else: + grays = [gray0] + for _ in range(self.pyramid_levels - 1): + grays.append(cv2.pyrDown(grays[-1])) + top = len(grays) - 1 + spread_top = self._spread_bitmap(grays[top]) + bit_active_top = int( + sum(1 << b for b in range(self._n_bins) + if (spread_top & (spread_top.dtype.type(1) << b)).any()) + ) + density_top = _jit_popcount(spread_top) + # spread0 + density_full computati piu sotto, quindi salvo dopo. + spread0 = None + bit_active_full = None + density_full = None if nms_radius is None: nms_radius = max(8, min(self.template_size) // 2) # Pruning adattivo allo step angolare: con step piccolo (<= 3 deg) @@ -1398,7 +1458,7 @@ class LineShapeMatcher: diag["top_thresh_used"] = float(top_thresh) tw, th = self.template_size - density_top = _jit_popcount(spread_top) + # density_top gia' computato sopra (cache o miss) sf_top = 2 ** top bg_cache_top: dict[float, np.ndarray] = {} bg_cache_full: dict[float, np.ndarray] = {} @@ -1548,13 +1608,21 @@ class LineShapeMatcher: diag["n_variants_top_passed"] = len(kept_coarse) diag["n_variants_full_evaluated"] = len(kept_variants) - # Full-res (parallelizzato) con bitmap - spread0 = self._spread_bitmap(gray0) - bit_active_full = int( - sum(1 << b for b in range(self._n_bins) - if (spread0 & (spread0.dtype.type(1) << b)).any()) - ) - density_full = _jit_popcount(spread0) + # Full-res (parallelizzato) con bitmap. + # Riusa cache se disponibile, altrimenti computa e salva. + if spread0 is None: + spread0 = self._spread_bitmap(gray0) + bit_active_full = int( + sum(1 << b for b in range(self._n_bins) + if (spread0 & (spread0.dtype.type(1) << b)).any()) + ) + density_full = _jit_popcount(spread0) + # Salva cache scena complete + if cache_key is not None: + self._scene_cache_put(cache_key, ( + grays, spread_top, bit_active_top, density_top, + spread0, bit_active_full, density_full, top, + )) for sc in unique_scales: bg_cache_full[sc] = _bg_for_scale(density_full, sc, 1)