From 6da4dd532981522abdd38a54d4dfc9a9333da1f3 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 4 May 2026 15:37:42 +0200 Subject: [PATCH] feat: dedup varianti con feature-set identico post-quantizzazione Hash byte-exact su (dx, dy, bin) ordinati + scale. Se due varianti post-rasterizzazione hanno lo stesso feature-set, ne tiene solo una. Tipico caso d'uso: template con simmetrie discrete (quadrati, croci, forme regolari) generano duplicati esatti per rotazioni multiple del periodo. Su quadrato 80x80 con angle_step=10 deg: 36 -> 27 varianti (~25% in meno di lavoro top-pruning). Approccio conservativo (byte-exact): zero rischio di rimuovere varianti distinte. Forme arrotondate (cerchi) o template asimmetrici non beneficiano ma non vengono compromessi. Co-Authored-By: Claude Opus 4.7 (1M context) --- pm2d/line_matcher.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index e5f212a..3133243 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -293,8 +293,42 @@ class LineShapeMatcher: kh=kh, kw=kw, cx_local=float(cx_local), cy_local=float(cy_local), )) + self._dedup_variants() return len(self.variants) + def _dedup_variants(self) -> int: + """Rimuove varianti con feature-set identico (post-quantizzazione). + + Halcon-style: con angle range = (0, 360) e simmetrie del template, + molte rotazioni producono lo stesso set quantizzato di feature. + Es: quadrato a 0/90/180/270 deg → stesse features (modulo permutazione). + Hash su feature ordinate (livello 0, full-res) elimina i duplicati. + + Vantaggio: meno varianti = meno chiamate kernel JIT al top-level + senza perdere copertura angolare effettiva. Per template asimmetrici + non rimuove nulla. + """ + seen: dict[bytes, int] = {} + kept: list[_Variant] = [] + removed = 0 + for var in self.variants: + lvl0 = var.levels[0] + order = np.lexsort((lvl0.bin, lvl0.dy, lvl0.dx)) + key = ( + lvl0.dx[order].tobytes() + + b"|" + lvl0.dy[order].tobytes() + + b"|" + lvl0.bin[order].tobytes() + + b"|" + str(round(var.scale, 4)).encode() + ) + h = key # diretto, senza hash crypto (collision ok solo se identici) + if h in seen: + removed += 1 + continue + seen[h] = len(kept) + kept.append(var) + self.variants = kept + return removed + # --- Matching ------------------------------------------------------ def _response_map(self, gray: np.ndarray) -> np.ndarray: