Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0296083e3c |
+83
-102
@@ -125,6 +125,11 @@ class _Variant:
|
||||
kw: int
|
||||
cx_local: float # centro-modello dentro al bbox kernel
|
||||
cy_local: float
|
||||
# Indice template view (X feature - multi-template ensemble).
|
||||
# 0 = template principale del train(); 1+ = view aggiunte via
|
||||
# add_template_view(). Usato in _verify_ncc/_compute_recall per
|
||||
# scegliere il template gray corretto per match.
|
||||
view_idx: int = 0
|
||||
|
||||
|
||||
class LineShapeMatcher:
|
||||
@@ -170,6 +175,11 @@ class LineShapeMatcher:
|
||||
self.template_gray: np.ndarray | None = None
|
||||
# Maschera usata in training (propagata al refine per coerenza).
|
||||
self._train_mask: np.ndarray | None = None
|
||||
# Multi-template ensemble (X feature): N view dello stesso pezzo
|
||||
# (chiari/scuri, condizioni diverse). Template principale e' [0],
|
||||
# view aggiunte via add_template_view() sono [1+]. Match restituisce
|
||||
# la view che ha matchato meglio.
|
||||
self._view_templates: list[tuple[np.ndarray, np.ndarray | None]] = []
|
||||
|
||||
# --- Helpers -------------------------------------------------------
|
||||
|
||||
@@ -314,8 +324,60 @@ class LineShapeMatcher:
|
||||
self._train_mask = mask_full.copy()
|
||||
|
||||
self.variants.clear()
|
||||
# Reset view list: template principale = view 0
|
||||
self._view_templates = [(gray.copy(), mask_full.copy())]
|
||||
# Invalida cache feature di refine: il template e cambiato.
|
||||
self._refine_feat_cache = {}
|
||||
self._build_variants_for_view(gray, mask_full, view_idx=0)
|
||||
self._dedup_variants()
|
||||
return len(self.variants)
|
||||
|
||||
def add_template_view(
|
||||
self, template_bgr: np.ndarray, mask: np.ndarray | None = None,
|
||||
) -> int:
|
||||
"""Aggiunge una view template extra all'ensemble (Halcon-style
|
||||
create_aniso_shape_model con fusione N viste).
|
||||
|
||||
Genera varianti del nuovo template con stessi parametri (range
|
||||
angle/scale) e le APPENDE a self.variants. NCC/recall usano
|
||||
automaticamente il template della view che ha matchato.
|
||||
|
||||
Use case: pezzo che cambia aspetto (chiaro/scuro, prima/dopo
|
||||
trattamento, illuminazioni diverse) → un solo matcher resistente.
|
||||
|
||||
Ritorna numero TOTALE varianti dopo l'aggiunta. Le view sono
|
||||
indicizzate da 1 in poi (0 e' il template del train).
|
||||
"""
|
||||
if not self.variants:
|
||||
raise RuntimeError(
|
||||
"Chiamare train(template_principale) prima di add_template_view")
|
||||
gray = self._to_gray(template_bgr)
|
||||
h, w = gray.shape
|
||||
if (w, h) != self.template_size:
|
||||
# Resize per coerenza con bbox/poly
|
||||
gray = cv2.resize(gray, self.template_size, interpolation=cv2.INTER_LINEAR)
|
||||
if mask is not None:
|
||||
mask = cv2.resize(mask, self.template_size, interpolation=cv2.INTER_NEAREST)
|
||||
if mask is None:
|
||||
mask_full = np.full(gray.shape, 255, dtype=np.uint8)
|
||||
else:
|
||||
mask_full = (mask > 0).astype(np.uint8) * 255
|
||||
view_idx = len(self._view_templates)
|
||||
self._view_templates.append((gray.copy(), mask_full.copy()))
|
||||
n_before = len(self.variants)
|
||||
self._build_variants_for_view(gray, mask_full, view_idx=view_idx)
|
||||
self._dedup_variants()
|
||||
return len(self.variants) - n_before
|
||||
|
||||
def _build_variants_for_view(
|
||||
self, gray: np.ndarray, mask_full: np.ndarray, view_idx: int,
|
||||
) -> None:
|
||||
"""Estrae varianti rotate+scalate per UNA view template.
|
||||
|
||||
Estrazione algorithm identica al train() originale, separato per
|
||||
riuso da add_template_view (multi-template ensemble).
|
||||
"""
|
||||
h, w = gray.shape
|
||||
for s in self._scale_list():
|
||||
sw = max(16, int(round(w * s)))
|
||||
sh = max(16, int(round(h * s)))
|
||||
@@ -369,9 +431,8 @@ class LineShapeMatcher:
|
||||
levels=levels,
|
||||
kh=kh, kw=kw,
|
||||
cx_local=float(cx_local), cy_local=float(cy_local),
|
||||
view_idx=view_idx,
|
||||
))
|
||||
self._dedup_variants()
|
||||
return len(self.variants)
|
||||
|
||||
def _dedup_variants(self) -> int:
|
||||
"""Rimuove varianti con feature-set identico (post-quantizzazione).
|
||||
@@ -740,97 +801,23 @@ class LineShapeMatcher:
|
||||
s2, cx2, cy2 = _score_at_angle(x2)
|
||||
return best
|
||||
|
||||
def _compute_soft_score(
|
||||
self, scene_gray: np.ndarray, variant: _Variant,
|
||||
cx: float, cy: float, angle_deg: float,
|
||||
) -> float:
|
||||
"""Soft-margin gradient similarity (Halcon Metric='use_polarity').
|
||||
def _get_view_template(
|
||||
self, view_idx: int,
|
||||
) -> tuple[np.ndarray | None, np.ndarray | None]:
|
||||
"""Ritorna (template_gray, mask) per la view specificata.
|
||||
|
||||
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)
|
||||
view_idx 0 = template principale (train), 1+ = view extra
|
||||
aggiunte via add_template_view. Usato per scegliere il template
|
||||
corretto in NCC/recall verification quando il matcher e'
|
||||
ensemble multi-template.
|
||||
"""
|
||||
if self.template_gray is None:
|
||||
return 0.0
|
||||
h, w = self.template_gray.shape
|
||||
scale = variant.scale
|
||||
sw = max(16, int(round(w * scale)))
|
||||
sh = max(16, int(round(h * scale)))
|
||||
gray_s = cv2.resize(self.template_gray, (sw, sh), interpolation=cv2.INTER_LINEAR)
|
||||
mask_src = (
|
||||
self._train_mask if self._train_mask is not None
|
||||
else np.full_like(self.template_gray, 255)
|
||||
)
|
||||
mask_s = cv2.resize(mask_src, (sw, sh), interpolation=cv2.INTER_NEAREST)
|
||||
diag = int(np.ceil(np.hypot(sh, sw))) + 6
|
||||
py = (diag - sh) // 2; px = (diag - sw) // 2
|
||||
gray_p = cv2.copyMakeBorder(
|
||||
gray_s, py, diag - sh - py, px, diag - sw - px, cv2.BORDER_REPLICATE,
|
||||
)
|
||||
mask_p = cv2.copyMakeBorder(
|
||||
mask_s, py, diag - sh - py, px, diag - sw - px,
|
||||
cv2.BORDER_CONSTANT, value=0,
|
||||
)
|
||||
center = (diag / 2.0, diag / 2.0)
|
||||
M = cv2.getRotationMatrix2D(center, angle_deg, 1.0)
|
||||
gray_r = cv2.warpAffine(gray_p, M, (diag, diag),
|
||||
flags=cv2.INTER_LINEAR,
|
||||
borderMode=cv2.BORDER_REPLICATE)
|
||||
mask_r = cv2.warpAffine(mask_p, M, (diag, diag),
|
||||
flags=cv2.INTER_NEAREST, borderValue=0)
|
||||
# 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
|
||||
# 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))
|
||||
sims = []
|
||||
weights = []
|
||||
for i in range(len(fx)):
|
||||
xs = ix + int(fx[i] - center[0])
|
||||
ys = iy + int(fy[i] - center[1])
|
||||
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))
|
||||
if 0 <= view_idx < len(self._view_templates):
|
||||
return self._view_templates[view_idx]
|
||||
return self.template_gray, self._train_mask
|
||||
|
||||
def _verify_ncc(
|
||||
self, scene_gray: np.ndarray, cx: float, cy: float,
|
||||
angle_deg: float, scale: float,
|
||||
angle_deg: float, scale: float, view_idx: int = 0,
|
||||
) -> float:
|
||||
"""NCC tra template warpato alla pose e scena sottostante.
|
||||
|
||||
@@ -842,9 +829,9 @@ class LineShapeMatcher:
|
||||
il matcher linemod può dare score alto su texture generiche ma
|
||||
sovrapponendo il template gray i pixel non corrispondono.
|
||||
"""
|
||||
if self.template_gray is None:
|
||||
t, train_mask = self._get_view_template(view_idx)
|
||||
if t is None:
|
||||
return 1.0
|
||||
t = self.template_gray
|
||||
h, w = t.shape
|
||||
cx_t = (w - 1) / 2.0
|
||||
cy_t = (h - 1) / 2.0
|
||||
@@ -869,8 +856,8 @@ class LineShapeMatcher:
|
||||
t, M, (cw, ch),
|
||||
flags=cv2.INTER_LINEAR, borderValue=0,
|
||||
)
|
||||
if self._train_mask is not None:
|
||||
mask_src = self._train_mask
|
||||
if train_mask is not None:
|
||||
mask_src = train_mask
|
||||
else:
|
||||
mask_src = np.full_like(t, 255)
|
||||
mask_w = cv2.warpAffine(
|
||||
@@ -916,7 +903,6 @@ class LineShapeMatcher:
|
||||
greediness: float = 0.0,
|
||||
batch_top: bool = False,
|
||||
nms_iou_threshold: float = 0.3,
|
||||
use_soft_score: bool = False,
|
||||
) -> list[Match]:
|
||||
"""
|
||||
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
||||
@@ -1274,18 +1260,13 @@ class LineShapeMatcher:
|
||||
# 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:
|
||||
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,
|
||||
view_idx=getattr(var, "view_idx", 0),
|
||||
)
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user