Compare commits

..

2 Commits

Author SHA1 Message Date
Adriano 35df4c473c fix: UCS match e numero feature ora coerenti con anteprima modello
Bug visibili da screenshot:
1. UCS match diverso da UCS anteprima modello (centro pose vs baricentro)
2. Numero feature disegnate < di quelle anteprima modello

Cause:
1. Match UCS era posto su (cx, cy) = centro template, mentre l'anteprima
   modello mostra UCS sul baricentro feature (mean fx, fy).
2. _draw_matches estraeva feature dal template warpato → re-quantizza
   gradient su immagine warp+interp, perdendo precisione vs feature
   pre-computate del matcher.

Fix:
- Match.variant_idx: nuovo field con indice variante usata dal find()
- _draw_matches usa lvl0.dx/dy/bin pre-computati invece di re-estrarre:
  * applica delta-rotation (m.angle_deg - var.angle_deg) per refine
    sub-step
  * proietta in scene coords intorno a (m.cx, m.cy)
  * stesso identico set di feature dell'anteprima modello (modulo
    rotazione+traslazione)
- UCS match calcolato sul baricentro delle feature warpate, non su
  (cx, cy) → coerente con UCS anteprima

Fallback (variant_idx == -1, es. ricetta caricata da save_model
prima di questo commit): usa estrazione warpata legacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:02:04 +02:00
Adriano 64f2c8b5dc merge: match overlay edges+UCS, no ROI 2026-05-05 10:55:54 +02:00
2 changed files with 74 additions and 35 deletions
+2
View File
@@ -127,6 +127,7 @@ class Match:
scale: float scale: float
score: float score: float
bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato) bbox_poly: np.ndarray # (4, 2) float32 - 4 vertici ordinati (ruotato)
variant_idx: int = -1 # indice variante usata (per overlay coerente)
@dataclass @dataclass
@@ -1863,6 +1864,7 @@ class LineShapeMatcher:
scale=var.scale, scale=var.scale,
score=score_f, score=score_f,
bbox_poly=poly, bbox_poly=poly,
variant_idx=int(vi),
)) ))
if len(kept) >= max_matches: if len(kept) >= max_matches:
break break
+53 -16
View File
@@ -157,37 +157,75 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
] ]
for i, m in enumerate(matches): for i, m in enumerate(matches):
color = palette[i % len(palette)] 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 t = template_gray
th, tw = t.shape th, tw = t.shape
cx_t = (tw - 1) / 2.0; cy_t = (th - 1) / 2.0 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 = cv2.getRotationMatrix2D((cx_t, cy_t), m.angle_deg, m.scale)
M[0, 2] += m.cx - cx_t M[0, 2] += m.cx - cx_t
M[1, 2] += m.cy - cy_t M[1, 2] += m.cy - cy_t
if matcher is not None: # Background edge filtrati: warpa template + hysteresis
# Edge filtrati con stessi param matcher (hysteresis)
warped_gray = cv2.warpAffine( warped_gray = cv2.warpAffine(
t, M, (W, H), flags=cv2.INTER_LINEAR, borderValue=0) t, M, (W, H), flags=cv2.INTER_LINEAR, borderValue=0)
mag, bins = matcher._gradient(warped_gray) mag, _ = matcher._gradient(warped_gray)
if matcher.weak_grad < matcher.strong_grad: if matcher.weak_grad < matcher.strong_grad:
edge_mask = matcher._hysteresis_mask(mag) edge_mask = matcher._hysteresis_mask(mag)
else: else:
edge_mask = mag >= matcher.strong_grad edge_mask = mag >= matcher.strong_grad
# Background edge filtrati: tinta scura colore match
if edge_mask.any(): if edge_mask.any():
bg_overlay = np.zeros_like(out) bg_overlay = np.zeros_like(out)
dark = tuple(int(c * 0.35) for c in color) dark = tuple(int(c * 0.35) for c in color)
bg_overlay[edge_mask] = dark bg_overlay[edge_mask] = dark
out = cv2.addWeighted(out, 1.0, bg_overlay, 0.7, 0) out = cv2.addWeighted(out, 1.0, bg_overlay, 0.7, 0)
# Feature scelte: estrazione alla pose, dot colorati per bin # Feature reali del matcher: usa quelle pre-computate della
fx, fy, fb = matcher._extract_features(mag, bins, None) # variante che ha generato il match. Stesse identiche feature
for k in range(len(fx)): # mostrate nell'anteprima modello (ruotate/scalate alla pose).
px, py = int(fx[k]), int(fy[k]) vi = getattr(m, "variant_idx", -1)
if 0 <= px < W and 0 <= py < H: fx_scene = fy_scene = fb_arr = None
bcol = bin_colors[int(fb[k]) % len(bin_colors)] if 0 <= vi < len(matcher.variants):
cv2.circle(out, (px, py), 2, bcol, -1, cv2.LINE_AA) 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: else:
# Legacy Canny # 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) edge = cv2.Canny(t, 50, 150)
warped = cv2.warpAffine(edge, M, (W, H), warped = cv2.warpAffine(edge, M, (W, H),
flags=cv2.INTER_NEAREST, borderValue=0) flags=cv2.INTER_NEAREST, borderValue=0)
@@ -197,9 +235,8 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
overlay[mask] = color overlay[mask] = color
out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8) out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8)
# bbox poly e linea-marker rimossi (richiesta utente "togli la ROI"): # bbox poly e linea-marker rimossi (richiesta utente "togli la ROI"):
# UCS + edge filtrati gia' identificano pose e orientamento, # UCS + edge filtrati gia' identificano pose e orientamento.
# il rettangolo aggiunto era ridondante e copriva il pezzo. cx, cy = int(round(ucs_x)), int(round(ucs_y))
cx, cy = int(round(m.cx)), int(round(m.cy))
# UCS sul centro pose match (richiesta utente: come nell'anteprima # UCS sul centro pose match (richiesta utente: come nell'anteprima
# modello). Asse X rosso destra, Y verde basso (image y-down). # modello). Asse X rosso destra, Y verde basso (image y-down).
# Lunghezza derivata dalla diagonale bbox per scala-invariante. # Lunghezza derivata dalla diagonale bbox per scala-invariante.