Compare commits

..

1 Commits

Author SHA1 Message Date
Adriano 5059ce1d89 feat: use_soft_score - Halcon Metric soft-margin gradient similarity
_compute_soft_score: cos(theta_template - theta_scena) continuo
(non quantizzato a bin) pesato per magnitude. Polarity-aware se
use_polarity=True (mod 2pi) else |cos| (mod pi).

Quando use_soft_score=True (default off, backward compat), lo score
finale e' fuso con quello shape: piu discriminante per match a
piccola rotazione (penalita' graduale invece di binaria on/off).

Equivalente a Halcon Metric='use_polarity' / 'ignore_global_polarity'
in find_shape_model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:32:17 +02:00
+61 -33
View File
@@ -740,19 +740,26 @@ class LineShapeMatcher:
s2, cx2, cy2 = _score_at_angle(x2)
return best
def _compute_recall(
self, spread0: np.ndarray, variant: _Variant,
def _compute_soft_score(
self, scene_gray: np.ndarray, variant: _Variant,
cx: float, cy: float, angle_deg: float,
) -> float:
"""Frazione di feature template che combaciano nello spread scena
alla pose (cx, cy, angle, variant.scale).
"""Soft-margin gradient similarity (Halcon Metric='use_polarity').
Riusa template_gray + warp per estrarre features alla pose esatta
(vs feature pre-computate alla pose della variante grezza). Ritorna
hits/N in [0, 1]. Halcon-equivalent: questo e' il "MinScore" originale.
Score = mean(max(0, cos(theta_template - theta_scene))) sulle
feature template alla pose, pesato per magnitude scena. Continuo
in [0, 1], piu discriminante della metric a bin (Y di "Halcon
improvements"): match a leggera rotazione = penalita' graduale
invece di on/off bin.
Polarity:
- use_polarity=True: cos(theta_t - theta_s) considera direzione
completa (mod 2pi)
- use_polarity=False: |cos(theta_t - theta_s)| considera solo
orientazione (mod pi)
"""
if self.template_gray is None:
return 1.0
return 0.0
h, w = self.template_gray.shape
scale = variant.scale
sw = max(16, int(round(w * scale)))
@@ -779,23 +786,47 @@ class LineShapeMatcher:
borderMode=cv2.BORDER_REPLICATE)
mask_r = cv2.warpAffine(mask_p, M, (diag, diag),
flags=cv2.INTER_NEAREST, borderValue=0)
mag, bins = self._gradient(gray_r)
fx, fy, fb = self._extract_features(mag, bins, mask_r)
n_feat = len(fx)
if n_feat < 4:
# Gradient template (continuo, non quantizzato)
gx_t = cv2.Sobel(gray_r, cv2.CV_32F, 1, 0, ksize=3)
gy_t = cv2.Sobel(gray_r, cv2.CV_32F, 0, 1, ksize=3)
mag_t = cv2.magnitude(gx_t, gy_t)
# Estrai posizioni feature alla pose
_, bins_t = self._gradient(gray_r)
fx, fy, _ = self._extract_features(mag_t, bins_t, mask_r)
if len(fx) < 4:
return 0.0
H, W = spread0.shape
spread_dtype = spread0.dtype.type
# Gradient scena (continuo)
gx_s = cv2.Sobel(scene_gray, cv2.CV_32F, 1, 0, ksize=3)
gy_s = cv2.Sobel(scene_gray, cv2.CV_32F, 0, 1, ksize=3)
H, W = scene_gray.shape
ix = int(round(cx)); iy = int(round(cy))
hits = 0
for i in range(n_feat):
sims = []
weights = []
for i in range(len(fx)):
xs = ix + int(fx[i] - center[0])
ys = iy + int(fy[i] - center[1])
if 0 <= xs < W and 0 <= ys < H:
bit = spread_dtype(1 << int(fb[i]))
if spread0[ys, xs] & bit:
hits += 1
return hits / n_feat
if not (0 <= xs < W and 0 <= ys < H):
continue
tx = float(gx_t[int(fy[i]), int(fx[i])])
ty = float(gy_t[int(fy[i]), int(fx[i])])
sx = float(gx_s[ys, xs]); sy = float(gy_s[ys, xs])
tm = math.hypot(tx, ty); sm = math.hypot(sx, sy)
if tm < 1e-3 or sm < 1e-3:
continue
# cos(theta_t - theta_s) = (tx*sx + ty*sy) / (tm*sm)
cos_sim = (tx * sx + ty * sy) / (tm * sm)
if not self.use_polarity:
# Mod pi: |cos| considera solo orientazione (no polarity)
cos_sim = abs(cos_sim)
else:
cos_sim = max(0.0, cos_sim)
sims.append(cos_sim)
weights.append(min(sm, 255.0))
if not sims:
return 0.0
sims_arr = np.asarray(sims, dtype=np.float32)
w_arr = np.asarray(weights, dtype=np.float32)
return float((sims_arr * w_arr).sum() / (w_arr.sum() + 1e-9))
def _verify_ncc(
self, scene_gray: np.ndarray, cx: float, cy: float,
@@ -885,7 +916,7 @@ class LineShapeMatcher:
greediness: float = 0.0,
batch_top: bool = False,
nms_iou_threshold: float = 0.3,
min_recall: float = 0.0,
use_soft_score: bool = False,
) -> list[Match]:
"""
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
@@ -1247,23 +1278,20 @@ class LineShapeMatcher:
if ncc < verify_threshold:
continue
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
# Soft-margin gradient similarity: sostituisce o integra lo
# score con metric continua (cos sim gradients) invece di
# bin discreto. Halcon-style: piu robusto a piccole rotazioni.
if use_soft_score:
soft = self._compute_soft_score(
gray0, var, cx_f, cy_f, ang_f,
)
score_f = (float(score_f) + soft) * 0.5
# Re-check min_score sullo score finale: NCC averaging puo
# abbattere lo shape-score sotto la soglia user. Senza questo
# check apparivano match con score < min_score (UI confusing).
if float(score_f) < min_score:
continue
# Feature recall (Halcon MinScore-style): conta quante feature
# template effettivamente combaciano nello spread scena alla
# pose finale. Scarta se sotto min_recall (default 0 = off).
# Util contro match parziali ad alto NCC ma poche feature reali.
if min_recall > 0.0:
recall = self._compute_recall(
spread0, var, cx_f, cy_f, ang_f,
)
if recall < min_recall:
continue
# Ri-traslo coord da spazio crop ROI a spazio scena originale.
cx_out = cx_f + roi_offset[0]
cy_out = cy_f + roi_offset[1]