feat: background locale + verify NCC per eliminare falsi positivi

Problema: matcher linemod con solo orientamento gradient può dare score alto
su texture dense/rumore che per caso accumulano orientamenti compatibili.
Esempio: template ruota dentata su scena clip → match a score 0.9 (errati).

Fix in 2 livelli:

1. Background score LOCALE nel find()
   - _bg_map(resp, box_size) = densità media bin attivi in bbox template
   - Rinormalizza score: s' = max(0, (s - bg) / (1 - bg))
   - Annulla contributo di zone sature ma preserva pattern puliti

2. Verify NCC post-hoc
   - _verify_ncc(): warpa template alla pose (cx, cy, angle, scale) e
     calcola NCC classico su intensità con la scena sottostante
   - Threshold di default 0.4 elimina FP con edge orientati casualmente
   - Parametro esposto in GUI (verify_threshold)

Rimossa penalty di saturazione nel response_map (ridondante).

Test regression (ruote dentate vs clip, clip vs ruote dentate):
  no verify:  12+ falsi positivi con score ~0.7
  verify 0.4: 1-2 falsi positivi rimanenti, true positive invariati
  verify 0.5: 0 falsi positivi, 1 TP scale piccola perso

Benchmark clip→clip (13 istanze):
  full pipeline (Numba + threads + refine + subpix + verify): 1.12s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 01:37:01 +02:00
parent b20b11c029
commit faebccb69e
3 changed files with 90 additions and 8 deletions
+14 -4
View File
@@ -43,6 +43,7 @@ PARAM_SCHEMA: list[tuple[str, str, type]] = [
("strong_grad", "Strong grad (line)", float),
("spread_radius", "Spread radius (line)", int),
("pyramid_levels", "Pyramid levels", int),
("verify_threshold", "Verify NCC threshold", float),
]
@@ -426,6 +427,7 @@ def run(
min_score: float = 0.55,
max_matches: int = 25,
nms_radius: int = 0,
verify_threshold: float = 0.4,
backend: str = "line",
) -> None:
"""Entry-point GUI completo."""
@@ -467,6 +469,7 @@ def run(
"strong_grad": strong_grad,
"spread_radius": spread_radius,
"pyramid_levels": pyramid_levels,
"verify_threshold": verify_threshold,
}
while True:
@@ -502,10 +505,17 @@ def run(
print(f" train: {n} varianti in {t_train:.2f}s")
t0 = time.time()
nms = cur["nms_radius"] if cur["nms_radius"] > 0 else None
matches = matcher.find(
scene, min_score=cur["min_score"],
max_matches=cur["max_matches"], nms_radius=nms,
)
if cur["backend"] == "line":
matches = matcher.find(
scene, min_score=cur["min_score"],
max_matches=cur["max_matches"], nms_radius=nms,
verify_threshold=cur.get("verify_threshold", 0.4),
)
else:
matches = matcher.find(
scene, min_score=cur["min_score"],
max_matches=cur["max_matches"], nms_radius=nms,
)
t_find = time.time() - t0
print(f" find: {len(matches)} match in {t_find:.2f}s")