Compare commits

..

5 Commits

Author SHA1 Message Date
Adriano 5002515b41 fix: rimuove edge spuri sui bordi template warpato (apparivano come ROI)
Bug: per ogni match l'overlay edge del modello includeva anche il
PERIMETRO del template warpato (transizione bordo nero borderValue=0
→ scena = forte gradient artefatto). Con N match si vedevano N
rettangoli verdi attorno ai pezzi, simili a "ROI ripetute".

Fix:
- Warpa anche _train_mask alla pose
- Erode di (2*spread_radius+1) per scartare la fascia di transizione
  bordo che produce gradient spurio
- Maschera edge_mask con warped_mask: solo edge interni al pezzo
  vengono visualizzati

Risultato: overlay edge pulito che mostra solo i veri edge del
modello allineati al pezzo trovato, niente cornici fasulle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:13:07 +02:00
Adriano 8029a1e12b merge: UCS coerente centro pose 2026-05-05 12:04:24 +02:00
Adriano d37833076e fix: UCS coerente sul centro pose, no traslazione fissata sbagliata
L'UCS del match precedentemente proiettava il baricentro feature
template alla pose, ma:
- Il baricentro veniva calcolato da una variante a 0° (v0) i cui dx/dy
  sono offsets relativi al centro PADDED (non al centro template puro)
- _extract_features dipende dai parametri matcher che possono differire
  da quelli del preview se la ricetta e' caricata
- Risultato: UCS appariva con offset costante errato rispetto al centro
  visibile del pezzo

Fix: UCS sul centro POSE del match (m.cx, m.cy) = posizione del centro
template originale nella scena (questo e' esattamente cio' che
_subpixel_peak ritorna). Coerente, prevedibile, "fissato" sul centro
del pezzo.

Per coerenza visiva, anche preview_edges sposta UCS dal baricentro al
CENTRO ROI (rh/2, rw/2). Cosi' il modello mostra UCS nello stesso
identico punto relativo dove apparira' nel match dopo
traslazione+rotazione della pose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:04:24 +02:00
Adriano e1ed9206a3 merge: fix UCS match + edge modello overlay 2026-05-05 11:58:21 +02:00
Adriano e84ae199ac fix: UCS match dimensione + orientamento Y + overlay edge modello
3 problemi visibili da screenshot:

1. UCS match troppo grande: usava 0.4 * lato bbox (~114 px su template
   286). Anteprima modello usa 0.15 * max(lato_template) (~42 px).
   Fix: stessa formula scalata per m.scale → coerenza dimensionale.

2. Asse Y match orientamento sbagliato: a m.angle_deg=0 puntava
   in alto invece che in basso (errore segno trigonometrico:
   sin(ax + pi/2) ≠ cos(ax) per il segno y-down).
   Fix corretto:
   - X axis = (cos(ax), -sin(ax))   # rotazione cv2 di (1, 0)
   - Y axis = (sin(ax), cos(ax))    # rotazione cv2 di (0, 1)
   Verificato: a ax=0 → X destra, Y giu' (matches modello).

3. Overlay edge modello orientato (richiesta utente): warpa template
   alla pose (cx, cy, angle, scale), applica hysteresis identica al
   matcher, disegna pixel edge come overlay verde brillante (60% alpha).
   Permette di vedere visivamente l'allineamento del modello sul pezzo
   rilevato.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:58:21 +02:00
+72 -46
View File
@@ -143,40 +143,69 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
nell'anteprima modello. nell'anteprima modello.
""" """
out = scene.copy() out = scene.copy()
# Baricentro UCS in coord template (calcolato una volta dal matcher # Lunghezza assi UCS: stessa formula dell'anteprima modello
# se disponibile): mean delle feature di una variante a 0°. Questo e' # (0.15 * max lato template) scalata per m.scale → coerenza dimensionale.
# lo stesso baricentro mostrato nell'anteprima modello. if matcher is not None and matcher.template_size != (0, 0):
bary_dx = bary_dy = 0.0 L_base = int(0.15 * max(matcher.template_size))
if matcher is not None and matcher.variants: else:
# Trova variante con angle_deg piu vicino a 0 L_base = 30
v0 = min(matcher.variants, key=lambda v: abs(v.angle_deg)) H_scene, W_scene = scene.shape[:2]
if len(v0.levels[0].dx) > 0:
bary_dx = float(np.mean(v0.levels[0].dx))
bary_dy = float(np.mean(v0.levels[0].dy))
for i, m in enumerate(matches): for i, m in enumerate(matches):
# Proietta baricentro template alla pose del match # UCS posizionato esattamente sul CENTRO POSE del match (m.cx, m.cy):
# (delta-rotation rispetto alla variante a 0) # equivale al centro template traslato alla scena, ruotato con
# m.angle_deg. Coerente con UCS dell'anteprima modello che ora
# e' anche sul centro ROI (vedi preview_edges).
ax = np.deg2rad(m.angle_deg) ax = np.deg2rad(m.angle_deg)
ca, sa = np.cos(ax), np.sin(ax) ca, sa = np.cos(ax), np.sin(ax)
bx_scene = m.cx + (bary_dx * ca + bary_dy * sa) * m.scale cx, cy = int(round(m.cx)), int(round(m.cy))
by_scene = m.cy + (-bary_dx * sa + bary_dy * ca) * m.scale # Overlay edge del modello orientato (richiesta utente):
cx, cy = int(round(bx_scene)), int(round(by_scene)) # warpa template alla pose, applica hysteresis identica al matcher,
# Lunghezza assi: 30% del lato bbox per essere visibile e scalato # disegna pixel edge come overlay verde tenue. Maschera col
if m.bbox_poly is not None and len(m.bbox_poly) >= 2: # _train_mask warpato + erode per rimuovere edge sui BORDI del
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0]) * 0.4) # rettangolo template (transizione bordo nero → scena = falso edge
else: # che appariva come "ROI" attorno a ogni match).
L = 40 if template_gray is not None and matcher is not None:
L = max(20, L) t = template_gray
# X axis (rosso) ruotato secondo angle del match th, tw = t.shape
x_end = (int(cx + L * np.cos(ax)), int(cy - L * np.sin(ax))) cx_t = (tw - 1) / 2.0; cy_t = (th - 1) / 2.0
M = cv2.getRotationMatrix2D((cx_t, cy_t), m.angle_deg, m.scale)
M[0, 2] += m.cx - cx_t
M[1, 2] += m.cy - cy_t
warped_gray = cv2.warpAffine(
t, M, (W_scene, H_scene),
flags=cv2.INTER_LINEAR, borderValue=0)
# Maschera: train_mask se disponibile, altrimenti rettangolo pieno
mask_src = (matcher._train_mask if matcher._train_mask is not None
else np.full((th, tw), 255, dtype=np.uint8))
warped_mask = cv2.warpAffine(
mask_src, M, (W_scene, H_scene),
flags=cv2.INTER_NEAREST, borderValue=0)
# Erode di spread_radius per scartare la fascia di transizione
# bordo che produce gradient spurio
er_k = max(3, 2 * matcher.spread_radius + 1)
kernel_er = np.ones((er_k, er_k), np.uint8)
warped_mask = cv2.erode(warped_mask, kernel_er)
mag, _ = matcher._gradient(warped_gray)
if matcher.weak_grad < matcher.strong_grad:
edge_mask = matcher._hysteresis_mask(mag)
else:
edge_mask = mag >= matcher.strong_grad
edge_mask = edge_mask & (warped_mask > 0)
if edge_mask.any():
edge_overlay = np.zeros_like(out)
edge_overlay[edge_mask] = (0, 220, 0) # verde brillante
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
L = max(20, int(L_base * m.scale))
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
x_end = (int(cx + L * ca), int(cy - L * sa))
# Y axis = rotazione di (0, 1) con cv2 matrix → (sin, cos)
# A m.angle_deg=0 deve puntare GIU' (image y-down convenzione modello)
y_end = (int(cx + L * sa), int(cy + L * ca))
cv2.arrowedLine(out, (cx, cy), x_end, cv2.arrowedLine(out, (cx, cy), x_end,
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2) (0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
cv2.putText(out, "X", (x_end[0] + 4, x_end[1] + 5), cv2.putText(out, "X", (x_end[0] + 4, x_end[1] + 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA) cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
# Y axis (verde) perpendicolare; +90° in image coords = giu' visivo
y_end = (int(cx + L * np.cos(ax + np.pi / 2)),
int(cy - L * np.sin(ax + np.pi / 2)))
cv2.arrowedLine(out, (cx, cy), y_end, cv2.arrowedLine(out, (cx, cy), y_end,
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2) (0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
cv2.putText(out, "Y", (y_end[0] + 4, y_end[1] + 12), cv2.putText(out, "Y", (y_end[0] + 4, y_end[1] + 12),
@@ -737,26 +766,23 @@ def preview_edges(p: EdgePreviewParams):
b = int(fb[i]) b = int(fb[i])
col = bin_colors[b % len(bin_colors)] col = bin_colors[b % len(bin_colors)]
cv2.circle(out, (int(fx[i]), int(fy[i])), 2, col, -1, cv2.LINE_AA) cv2.circle(out, (int(fx[i]), int(fy[i])), 2, col, -1, cv2.LINE_AA)
# UCS sul baricentro feature (richiesta utente): assi X rosso, Y verde # UCS sul CENTRO ROI (coerente con _draw_matches che usa centro pose).
bary_cx = bary_cy = None # In questo modo l'UCS visualizzato nel modello = UCS del match (modulo
if len(fx) > 0: # rotazione/traslazione data dalla pose del pezzo trovato).
bary_cx = float(np.mean(fx)) rh, rw = roi_img.shape[:2]
bary_cy = float(np.mean(fy)) bx, by = (rw - 1) // 2, (rh - 1) // 2
bx, by = int(round(bary_cx)), int(round(bary_cy)) axis_len = max(20, int(0.15 * max(rw, rh)))
axis_len = max(20, int(0.15 * max(out.shape[:2]))) cv2.arrowedLine(out, (bx, by), (bx + axis_len, by),
# X axis (rosso, verso destra) (0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
cv2.arrowedLine(out, (bx, by), (bx + axis_len, by), cv2.putText(out, "X", (bx + axis_len + 4, by + 5),
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2) cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
cv2.putText(out, "X", (bx + axis_len + 4, by + 5), cv2.arrowedLine(out, (bx, by), (bx, by + axis_len),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA) (0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
# Y axis (verde, verso il basso = convenzione image y-down) cv2.putText(out, "Y", (bx + 4, by + axis_len + 12),
cv2.arrowedLine(out, (bx, by), (bx, by + axis_len), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2) cv2.circle(out, (bx, by), 4, (0, 0, 0), -1, cv2.LINE_AA)
cv2.putText(out, "Y", (bx + 4, by + axis_len + 12), cv2.circle(out, (bx, by), 3, (255, 255, 255), -1, cv2.LINE_AA)
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA) bary_cx, bary_cy = float(bx), float(by)
# Origine: cerchio bianco con bordo nero
cv2.circle(out, (bx, by), 4, (0, 0, 0), -1, cv2.LINE_AA)
cv2.circle(out, (bx, by), 3, (255, 255, 255), -1, cv2.LINE_AA)
img_id = _store_image(out) img_id = _store_image(out)
n_edge_strong = int((mag >= m.strong_grad).sum()) n_edge_strong = int((mag >= m.strong_grad).sum())
n_edge_total = int(edge_mask.sum() / 255) n_edge_total = int(edge_mask.sum() / 255)