diff --git a/pm2d/line_matcher.py b/pm2d/line_matcher.py index daebe1e..94c653b 100644 --- a/pm2d/line_matcher.py +++ b/pm2d/line_matcher.py @@ -127,6 +127,7 @@ class Match: scale: float score: float bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato) + variant_idx: int = -1 # indice variante usata (per overlay coerente) @dataclass @@ -1863,6 +1864,7 @@ class LineShapeMatcher: scale=var.scale, score=score_f, bbox_poly=poly, + variant_idx=int(vi), )) if len(kept) >= max_matches: break diff --git a/pm2d/web/server.py b/pm2d/web/server.py index fb70714..d82666d 100644 --- a/pm2d/web/server.py +++ b/pm2d/web/server.py @@ -157,49 +157,86 @@ def _draw_matches(scene: np.ndarray, matches: list[Match], ] for i, m in enumerate(matches): color = palette[i % len(palette)] - if template_gray is not None: + # Posizione UCS: baricentro feature warpate (default = cx, cy se non disponibile). + # Mantiene coerenza con anteprima modello che mostra UCS sul baricentro. + ucs_x, ucs_y = float(m.cx), float(m.cy) + if template_gray is not None and matcher is not None: t = template_gray th, tw = t.shape 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 - if matcher is not None: - # Edge filtrati con stessi param matcher (hysteresis) - warped_gray = cv2.warpAffine( - t, M, (W, H), flags=cv2.INTER_LINEAR, borderValue=0) - mag, bins = matcher._gradient(warped_gray) - if matcher.weak_grad < matcher.strong_grad: - edge_mask = matcher._hysteresis_mask(mag) - else: - edge_mask = mag >= matcher.strong_grad - # Background edge filtrati: tinta scura colore match - if edge_mask.any(): - bg_overlay = np.zeros_like(out) - dark = tuple(int(c * 0.35) for c in color) - bg_overlay[edge_mask] = dark - out = cv2.addWeighted(out, 1.0, bg_overlay, 0.7, 0) - # Feature scelte: estrazione alla pose, dot colorati per bin - fx, fy, fb = matcher._extract_features(mag, bins, None) - for k in range(len(fx)): - px, py = int(fx[k]), int(fy[k]) - if 0 <= px < W and 0 <= py < H: - bcol = bin_colors[int(fb[k]) % len(bin_colors)] - cv2.circle(out, (px, py), 2, bcol, -1, cv2.LINE_AA) + # Background edge filtrati: warpa template + hysteresis + warped_gray = cv2.warpAffine( + t, M, (W, H), flags=cv2.INTER_LINEAR, borderValue=0) + mag, _ = matcher._gradient(warped_gray) + if matcher.weak_grad < matcher.strong_grad: + edge_mask = matcher._hysteresis_mask(mag) else: - # Legacy Canny - edge = cv2.Canny(t, 50, 150) - warped = cv2.warpAffine(edge, M, (W, H), - flags=cv2.INTER_NEAREST, borderValue=0) - mask = warped > 0 - if mask.any(): - overlay = np.zeros_like(out) - overlay[mask] = color - out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8) + edge_mask = mag >= matcher.strong_grad + if edge_mask.any(): + bg_overlay = np.zeros_like(out) + dark = tuple(int(c * 0.35) for c in color) + bg_overlay[edge_mask] = dark + out = cv2.addWeighted(out, 1.0, bg_overlay, 0.7, 0) + # Feature reali del matcher: usa quelle pre-computate della + # variante che ha generato il match. Stesse identiche feature + # mostrate nell'anteprima modello (ruotate/scalate alla pose). + vi = getattr(m, "variant_idx", -1) + fx_scene = fy_scene = fb_arr = None + if 0 <= vi < len(matcher.variants): + lvl0 = matcher.variants[vi].levels[0] + # dx/dy sono offsets relativi al CENTRO del template warpato + # nelle coordinate del kernel template (gia' pre-ruotate + # all'angolo della variante grezza). Per coerenza con la + # pose finale m.angle_deg (post-refine), ri-rotazione del + # delta angolare (m.angle_deg - var.angle_deg). + var = matcher.variants[vi] + dang = np.deg2rad(m.angle_deg - var.angle_deg) + ca, sa = np.cos(dang), np.sin(dang) + dxr = lvl0.dx * ca + lvl0.dy * sa + dyr = -lvl0.dx * sa + lvl0.dy * ca + fx_scene = m.cx + dxr + fy_scene = m.cy + dyr + fb_arr = lvl0.bin + else: + # Fallback: estrai feature dal warpato (perde precisione) + _, bins_w = matcher._gradient(warped_gray) + fx, fy, fb = matcher._extract_features(mag, bins_w, None) + fx_scene = fx.astype(np.float32) + fy_scene = fy.astype(np.float32) + fb_arr = fb + # Disegna feature + for k in range(len(fx_scene)): + px = int(round(float(fx_scene[k]))) + py = int(round(float(fy_scene[k]))) + if 0 <= px < W and 0 <= py < H: + bcol = bin_colors[int(fb_arr[k]) % len(bin_colors)] + cv2.circle(out, (px, py), 2, bcol, -1, cv2.LINE_AA) + # UCS sul baricentro feature (in scene coords) + if len(fx_scene) > 0: + ucs_x = float(np.mean(fx_scene)) + ucs_y = float(np.mean(fy_scene)) + elif template_gray is not None: + # Senza matcher: legacy Canny + t = template_gray + th, tw = t.shape + 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 + edge = cv2.Canny(t, 50, 150) + warped = cv2.warpAffine(edge, M, (W, H), + flags=cv2.INTER_NEAREST, borderValue=0) + mask = warped > 0 + if mask.any(): + overlay = np.zeros_like(out) + overlay[mask] = color + out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8) # bbox poly e linea-marker rimossi (richiesta utente "togli la ROI"): - # UCS + edge filtrati gia' identificano pose e orientamento, - # il rettangolo aggiunto era ridondante e copriva il pezzo. - cx, cy = int(round(m.cx)), int(round(m.cy)) + # UCS + edge filtrati gia' identificano pose e orientamento. + cx, cy = int(round(ucs_x)), int(round(ucs_y)) # UCS sul centro pose match (richiesta utente: come nell'anteprima # modello). Asse X rosso destra, Y verde basso (image y-down). # Lunghezza derivata dalla diagonale bbox per scala-invariante.