From d37833076e47b3055b7ae90692aaaa1524845bd2 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Tue, 5 May 2026 12:04:24 +0200 Subject: [PATCH] fix: UCS coerente sul centro pose, no traslazione fissata sbagliata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pm2d/web/server.py | 61 +++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/pm2d/web/server.py b/pm2d/web/server.py index 03ee3cb..0a1e0de 100644 --- a/pm2d/web/server.py +++ b/pm2d/web/server.py @@ -143,17 +143,6 @@ def _draw_matches(scene: np.ndarray, matches: list[Match], nell'anteprima modello. """ 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 # (0.15 * max lato template) scalata per m.scale → coerenza dimensionale. 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: L_base = 30 H_scene, W_scene = scene.shape[:2] + for i, m in enumerate(matches): - # Proietta baricentro template alla pose del match. - # cv2.getRotationMatrix2D con angle positivo applica: - # new_x = cos*x + sin*y new_y = -sin*x + cos*y - # Visivamente in image y-down e' rotazione anti-clockwise. + # UCS posizionato esattamente sul CENTRO POSE del match (m.cx, m.cy): + # 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) ca, sa = np.cos(ax), np.sin(ax) - bx_scene = m.cx + (bary_dx * ca + bary_dy * sa) * m.scale - by_scene = m.cy + (-bary_dx * sa + bary_dy * ca) * m.scale - cx, cy = int(round(bx_scene)), int(round(by_scene)) + cx, cy = int(round(m.cx)), int(round(m.cy)) # Overlay edge del modello orientato (richiesta utente): # warpa template alla pose, applica hysteresis identica al matcher, # disegna pixel edge come overlay verde tenue. @@ -763,26 +751,23 @@ def preview_edges(p: EdgePreviewParams): b = int(fb[i]) col = bin_colors[b % len(bin_colors)] 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 - bary_cx = bary_cy = None - if len(fx) > 0: - bary_cx = float(np.mean(fx)) - bary_cy = float(np.mean(fy)) - bx, by = int(round(bary_cx)), int(round(bary_cy)) - axis_len = max(20, int(0.15 * max(out.shape[:2]))) - # X axis (rosso, verso destra) - cv2.arrowedLine(out, (bx, by), (bx + axis_len, by), - (0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2) - cv2.putText(out, "X", (bx + axis_len + 4, by + 5), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA) - # Y axis (verde, verso il basso = convenzione image y-down) - cv2.arrowedLine(out, (bx, by), (bx, by + axis_len), - (0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2) - cv2.putText(out, "Y", (bx + 4, by + axis_len + 12), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA) - # 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) + # UCS sul CENTRO ROI (coerente con _draw_matches che usa centro pose). + # In questo modo l'UCS visualizzato nel modello = UCS del match (modulo + # rotazione/traslazione data dalla pose del pezzo trovato). + rh, rw = roi_img.shape[:2] + bx, by = (rw - 1) // 2, (rh - 1) // 2 + axis_len = max(20, int(0.15 * max(rw, rh))) + cv2.arrowedLine(out, (bx, by), (bx + axis_len, by), + (0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2) + cv2.putText(out, "X", (bx + axis_len + 4, by + 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA) + cv2.arrowedLine(out, (bx, by), (bx, by + axis_len), + (0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2) + cv2.putText(out, "Y", (bx + 4, by + axis_len + 12), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA) + 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) + bary_cx, bary_cy = float(bx), float(by) img_id = _store_image(out) n_edge_strong = int((mag >= m.strong_grad).sum()) n_edge_total = int(edge_mask.sum() / 255)