From 9a0da7aac86ca2e26ca50a94919a9d6c0ddd4938 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 24 Apr 2026 01:48:40 +0200 Subject: [PATCH] fix: bg_map per-scala + rimosso cap varianti full-res MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problema: in scenari con molte scale (ring detection), il matcher perdeva istanze a scale estreme: 1. Cap max_vars_full (default max_matches*8) escludeva la pose corretta 2. bg_map usava box fissa = template_size, penalizzando varianti a scala grande dove il template reale è più grande del box Fix: - Rimosso cap hard sul numero di varianti full-res (Numba compensa velocità) - bg_map PER-SCALA: cache {scale: bg_map} con box size scalata appropriatamente (tw*scale, th*scale). Calcolato una volta per scala unica, poi ogni variante usa il suo bg_map Benchmark rings_and_nuts (template ruota grande, 3 ruote nella scena a dimensioni diverse): prima: 2/3 match (persa la grande) dopo: 3/3 match score 1.0 a scale 1.00, 0.95, 0.80 Regression: clip→clip: 13/13 invariato (0.93s) ring→clip FP: 3 (era 1 con bg fisso, era 10 senza bg) compromesso ragionevole: verify_threshold=0.5 elimina gli ultimi FP Co-Authored-By: Claude Opus 4.7 (1M context) --- pm2d/line_matcher.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index a7fcb3c..4a4f83d 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -494,19 +494,25 @@ class LineShapeMatcher: nms_radius = max(8, min(self.template_size) // 2) top_thresh = min_score * self.top_score_factor - # Background score LOCALE: densità media bin-attivi normalizzata su - # bbox template. Rinormalizzazione rimuove match dove la zona ha - # attivazioni dense in tutti gli orientamenti (texture/rumore). + # Background map PER-SCALA: densità media bin attivi normalizzata + # su bbox template scalata. Rinormalizza score per isolare contributo + # non-random e riduce FP in zone con attivazione densa. tw, th = self.template_size + density_top = resp_top.sum(axis=0) + sf_top = 2 ** top + bg_cache_top: dict[float, np.ndarray] = {} + bg_cache_full: dict[float, np.ndarray] = {} + unique_scales = sorted({var.scale for var in self.variants}) - def _bg_map(resp: np.ndarray, scale_div: int = 1) -> np.ndarray: - """bg_map[y,x] = frazione bin attivi media in bbox template.""" - density = resp.sum(axis=0) # (H, W) - bw = max(9, tw // scale_div); bh = max(9, th // scale_div) - smooth = cv2.boxFilter(density, cv2.CV_32F, (bw, bh)) - return np.clip(smooth / N_BINS, 0.0, 0.99) + def _bg_for_scale(density: np.ndarray, scale: float, + divisor: int) -> np.ndarray: + bw = max(9, int(round(tw * scale / divisor))) + bh = max(9, int(round(th * scale / divisor))) + sm = cv2.boxFilter(density, cv2.CV_32F, (bw, bh)) + return np.clip(sm / N_BINS, 0.0, 0.99) - bg_top = _bg_map(resp_top, scale_div=2 ** top) + for sc in unique_scales: + bg_cache_top[sc] = _bg_for_scale(density_top, sc, sf_top) def _rescore(score: np.ndarray, bg: np.ndarray) -> np.ndarray: return np.maximum(0.0, (score - bg) / (1.0 - bg + 1e-6)) @@ -518,7 +524,7 @@ class LineShapeMatcher: score = self._score_by_shift( resp_top, lvl.dx, lvl.dy, lvl.bin, bin_has_data=bin_has_top, ) - score = _rescore(score, bg_top) + score = _rescore(score, bg_cache_top[var.scale]) return vi, float(score.max()) if score.size else -1.0 kept_variants: list[tuple[int, float]] = [] @@ -536,14 +542,16 @@ class LineShapeMatcher: if not kept_variants: return [] - max_vars_full = max(8, max_matches * 4) + # Cap: tutte le varianti che superano top_thresh passano al full-res. + # Ordinamento per score decrescente (early matches hanno priorità). kept_variants.sort(key=lambda t: -t[1]) - kept_variants = kept_variants[:max_vars_full] # Full-res (parallelizzato per variante) resp0 = self._response_map(gray0) bin_has_full = np.array([resp0[b].any() for b in range(N_BINS)]) - bg_full = _bg_map(resp0, scale_div=1) + density_full = resp0.sum(axis=0) + for sc in unique_scales: + bg_cache_full[sc] = _bg_for_scale(density_full, sc, 1) def _full_score(vi: int) -> tuple[int, np.ndarray]: var = self.variants[vi] @@ -551,7 +559,7 @@ class LineShapeMatcher: score = self._score_by_shift( resp0, lvl0.dx, lvl0.dy, lvl0.bin, bin_has_data=bin_has_full, ) - score = _rescore(score, bg_full) + score = _rescore(score, bg_cache_full[var.scale]) return vi, score candidates_per_var: list[tuple[int, np.ndarray]] = []