From 7f6571bdd177f14fc262eacd673da55ef7a047c6 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 4 May 2026 23:01:54 +0200 Subject: [PATCH] feat: hysteresis edge linking (Halcon Contrast='auto' two-threshold) _hysteresis_mask: edge linking via componenti connesse. - seed = mag >= strong_grad - weak = mag >= weak_grad - Promuove a feature ogni componente weak che contiene almeno un pixel strong (connettivita' 8-vicini) Riduce simultaneamente: - Falsi positivi: edge debole isolato (rumore puro) escluso - Falsi negativi: edge debole connesso a edge forte incluso (continuita' bordi sottili a basso contrasto) Attivo automaticamente quando weak_grad < strong_grad. Se uguali, fallback a sogliatura singola standard. Backward compat completo dato che default weak=30, strong=60. Co-Authored-By: Claude Opus 4.7 (1M context) --- pm2d/line_matcher.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index c37b7f8..899a746 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -241,13 +241,49 @@ class LineShapeMatcher: bins = np.clip(bins, 0, N_BINS - 1) return mag, bins + def _hysteresis_mask(self, mag: np.ndarray) -> np.ndarray: + """Edge mask con hysteresis (Halcon Contrast='auto' two-threshold). + + Procedura: + 1. seed = pixel con mag >= strong_grad (edge nitidi) + 2. weak = pixel con mag >= weak_grad (edge candidati) + 3. Espande seed dentro weak via componenti connesse 8-vicini + + Risultato: edge debole connesso a edge forte viene PROMOSSO a + feature valida; edge debole isolato (rumore) viene SCARTATO. + + Riduce sia falsi-positivi (rumore puro) sia falsi-negativi + (continuita' interrotta su edge sottili a basso contrasto). + """ + weak = (mag >= self.weak_grad).astype(np.uint8) + strong = (mag >= self.strong_grad).astype(np.uint8) + # connectedComponentsWithStats su weak: per ogni componente, + # se contiene almeno un pixel strong → tutto componente accettato + n_lab, labels = cv2.connectedComponents(weak, connectivity=8) + if n_lab <= 1: + return strong.astype(bool) + # Label dei pixel strong: marker per componenti da accettare + strong_labels = np.unique(labels[strong > 0]) + strong_labels = strong_labels[strong_labels > 0] # 0 = bg + if len(strong_labels) == 0: + return strong.astype(bool) + # Mask = appartiene a label di componente "promosso" + keep = np.isin(labels, strong_labels) + return keep + def _extract_features( self, mag: np.ndarray, bins: np.ndarray, mask: np.ndarray | None, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: if mask is not None: mag = np.where(mask > 0, mag, 0) - strong = mag >= self.strong_grad - ys, xs = np.where(strong) + # Halcon-style edge selection: hysteresis tra weak_grad e strong_grad. + # Edge weak connessi a edge strong sono inclusi (continuita' bordi). + # Se weak_grad >= strong_grad → fallback a soglia singola strong. + if self.weak_grad < self.strong_grad: + edge = self._hysteresis_mask(mag) + else: + edge = mag >= self.strong_grad + ys, xs = np.where(edge) if len(xs) == 0: return (np.zeros(0, np.int32),) * 3 vals = mag[ys, xs]