fix: bg_map per-scala + rimosso cap varianti full-res
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) <noreply@anthropic.com>
This commit is contained in:
+23
-15
@@ -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]] = []
|
||||
|
||||
Reference in New Issue
Block a user