Compare commits

..

2 Commits

Author SHA1 Message Date
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
+23 -38
View File
@@ -143,17 +143,6 @@ 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
# se disponibile): mean delle feature di una variante a 0°. Questo e'
# lo stesso baricentro mostrato nell'anteprima modello.
bary_dx = bary_dy = 0.0
if matcher is not None and matcher.variants:
# Trova variante con angle_deg piu vicino a 0
v0 = min(matcher.variants, key=lambda v: abs(v.angle_deg))
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))
# Lunghezza assi UCS: stessa formula dell'anteprima modello # Lunghezza assi UCS: stessa formula dell'anteprima modello
# (0.15 * max lato template) scalata per m.scale → coerenza dimensionale. # (0.15 * max lato template) scalata per m.scale → coerenza dimensionale.
if matcher is not None and matcher.template_size != (0, 0): if matcher is not None and matcher.template_size != (0, 0):
@@ -161,16 +150,15 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
else: else:
L_base = 30 L_base = 30
H_scene, W_scene = scene.shape[:2] H_scene, W_scene = scene.shape[:2]
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):
# cv2.getRotationMatrix2D con angle positivo applica: # equivale al centro template traslato alla scena, ruotato con
# new_x = cos*x + sin*y new_y = -sin*x + cos*y # m.angle_deg. Coerente con UCS dell'anteprima modello che ora
# Visivamente in image y-down e' rotazione anti-clockwise. # 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
cx, cy = int(round(bx_scene)), int(round(by_scene))
# Overlay edge del modello orientato (richiesta utente): # Overlay edge del modello orientato (richiesta utente):
# warpa template alla pose, applica hysteresis identica al matcher, # warpa template alla pose, applica hysteresis identica al matcher,
# disegna pixel edge come overlay verde tenue. # disegna pixel edge come overlay verde tenue.
@@ -763,26 +751,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)