Compare commits

...

2 Commits

Author SHA1 Message Date
Adriano 7f6571bdd1 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) <noreply@anthropic.com>
2026-05-04 23:01:54 +02:00
Adriano 7cb1ae2df7 merge: UI wiring modalita Halcon 2026-05-04 22:49:17 +02:00
+38 -2
View File
@@ -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]