fix: duplicati, score saturato e angolo impreciso

3 problemi visibili da test interattivo:
1. Match duplicati: stesso oggetto trovato da varianti angolari
   diverse, NMS pre-refine non basta perche refine sposta i match.
   Aggiunto NMS post-refine cross-variant.

2. Score sempre alto/saturato a 1.0: NCC era opzionale (skip>=0.85)
   e non veniva mescolato nello score. Ora ncc_skip_above=1.01
   (NCC sempre) e score finale = (shape + NCC) / 2: piu discriminante.

3. Angolo impreciso: _refine_angle aveva early-exit per shape-score
   >= 0.99, ma quel valore satura facile (con pyramid_propagate o
   spread ampio) senza garantire angolo preciso. Rimosso early-exit:
   refine angolare e' sempre essenziale per orientamento sub-step.

Inoltre: pyramid_propagate default False (era True), riduce duplicati
da picchi propagati su angle-vicini. propagate_topk default 4 (era 8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 16:33:58 +02:00
parent 4ef7a4a85f
commit 41976f574d
+26 -10
View File
@@ -570,9 +570,11 @@ class LineShapeMatcher:
l'angolo con score massimo (parabolic fit sulle 3 score centrali). l'angolo con score massimo (parabolic fit sulle 3 score centrali).
Ritorna (angle_refined, score, cx_refined, cy_refined). Ritorna (angle_refined, score, cx_refined, cy_refined).
""" """
# Se il match grezzo è già quasi perfetto, NON refinare # NB: rimosso early-skip su score >= 0.99. Lo score linemod/shape
if original_score is not None and original_score >= 0.99: # satura facilmente a 1.0 (specie con pyramid_propagate o spread
return (angle_deg, original_score, cx, cy) # ampio) ma NON garantisce angolo preciso: l'angolo grezzo della
# variante e' quantizzato a multipli di angle_step (5 deg default).
# Refine angolare e' essenziale per orientamento sub-step.
if search_radius is None: if search_radius is None:
search_radius = self._effective_angle_step() / 2.0 search_radius = self._effective_angle_step() / 2.0
@@ -750,13 +752,13 @@ class LineShapeMatcher:
subpixel: bool = True, subpixel: bool = True,
verify_ncc: bool = True, verify_ncc: bool = True,
verify_threshold: float = 0.4, verify_threshold: float = 0.4,
ncc_skip_above: float = 0.85, ncc_skip_above: float = 1.01, # disabilitato di default: NCC sempre
coarse_angle_factor: int = 2, coarse_angle_factor: int = 2,
coarse_stride: int = 1, coarse_stride: int = 1,
scale_penalty: float = 0.0, scale_penalty: float = 0.0,
search_roi: tuple[int, int, int, int] | None = None, search_roi: tuple[int, int, int, int] | None = None,
pyramid_propagate: bool = True, pyramid_propagate: bool = False, # off di default: meno duplicati
propagate_topk: int = 8, propagate_topk: int = 4,
refine_pose_joint: bool = False, refine_pose_joint: bool = False,
greediness: float = 0.0, greediness: float = 0.0,
batch_top: bool = False, batch_top: bool = False,
@@ -1096,14 +1098,18 @@ class LineShapeMatcher:
search_radius=self._effective_angle_step() / 2.0, search_radius=self._effective_angle_step() / 2.0,
original_score=score, original_score=score,
) )
# NCC verify lazy (Halcon-style): skip se shape-score gia molto # NCC verify (Halcon-style): se ncc_skip_above < 1.0 salta
# alto (probabilita falso positivo trascurabile). NCC e l'op # il calcolo per shape-score gia alti. Default 1.01 = NCC sempre,
# piu costosa per match (warp + corr), quindi vale la pena # piu sicuro contro falsi positivi (lo shape-score satura facile).
# saltarlo quando il gradiente shape e gia conclusivo. # Quando NCC viene calcolato, lo score finale e' la MEDIA tra
# shape-score e NCC: rende lo score piu discriminante per
# ranking/visualizzazione (uno score 1.0 vero richiede sia
# match shape sia template gray identici).
if verify_ncc and float(score_f) < ncc_skip_above: if verify_ncc and float(score_f) < ncc_skip_above:
ncc = self._verify_ncc(gray0, cx_f, cy_f, ang_f, var.scale) ncc = self._verify_ncc(gray0, cx_f, cy_f, ang_f, var.scale)
if ncc < verify_threshold: if ncc < verify_threshold:
continue continue
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
# Ri-traslo coord da spazio crop ROI a spazio scena originale. # Ri-traslo coord da spazio crop ROI a spazio scena originale.
cx_out = cx_f + roi_offset[0] cx_out = cx_f + roi_offset[0]
@@ -1116,6 +1122,16 @@ class LineShapeMatcher:
score_f = float(score_f) * max( score_f = float(score_f) * max(
0.0, 1.0 - scale_penalty * abs(var.scale - 1.0) 0.0, 1.0 - scale_penalty * abs(var.scale - 1.0)
) )
# NMS post-refine: refine puo spostare il match di nms_radius;
# ricontrollo overlap su match gia accettati per evitare
# duplicati (stesso oggetto trovato da varianti angolari diverse).
dup = False
for k in kept:
if (k.cx - cx_out) ** 2 + (k.cy - cy_out) ** 2 < r2:
dup = True
break
if dup:
continue
kept.append(Match( kept.append(Match(
cx=cx_out, cy=cy_out, cx=cx_out, cy=cy_out,
angle_deg=ang_f, angle_deg=ang_f,