Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fb1efcab8 | |||
| 35df4c473c | |||
| 64f2c8b5dc | |||
| 7e076deb80 | |||
| 852597ed51 |
@@ -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
|
||||||
|
|||||||
+111
-18
@@ -131,23 +131,102 @@ def _encode_png(img: np.ndarray) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def _draw_matches(scene: np.ndarray, matches: list[Match],
|
def _draw_matches(scene: np.ndarray, matches: list[Match],
|
||||||
template_gray: np.ndarray | None) -> np.ndarray:
|
template_gray: np.ndarray | None,
|
||||||
|
matcher: "LineShapeMatcher | None" = None) -> np.ndarray:
|
||||||
|
"""Disegna match annotati sulla scena.
|
||||||
|
|
||||||
|
Se matcher e' passato, usa la stessa pipeline di edge filtering
|
||||||
|
(hysteresis weak/strong_grad) e selezione feature usata in training,
|
||||||
|
cosi' l'overlay nel match riflette ESATTAMENTE quello che l'utente
|
||||||
|
ha visto nel preview "Anteprima edge". Inoltre disegna UCS
|
||||||
|
(asse X rosso, Y verde) sul centro pose del match.
|
||||||
|
|
||||||
|
Senza matcher: fallback Canny (legacy).
|
||||||
|
"""
|
||||||
out = scene.copy()
|
out = scene.copy()
|
||||||
H, W = scene.shape[:2]
|
H, W = scene.shape[:2]
|
||||||
palette = [
|
palette = [
|
||||||
(0, 255, 0), (0, 200, 255), (255, 100, 100), (255, 200, 0),
|
(0, 255, 0), (0, 200, 255), (255, 100, 100), (255, 200, 0),
|
||||||
(200, 0, 255), (100, 255, 200), (255, 0, 0), (0, 255, 255),
|
(200, 0, 255), (100, 255, 200), (255, 0, 0), (0, 255, 255),
|
||||||
]
|
]
|
||||||
|
bin_colors = [
|
||||||
|
(255, 0, 0), (255, 128, 0), (255, 255, 0), (0, 255, 0),
|
||||||
|
(0, 255, 255), (0, 128, 255), (0, 0, 255), (255, 0, 255),
|
||||||
|
(255, 100, 100), (255, 180, 100), (255, 230, 100), (180, 255, 100),
|
||||||
|
(100, 255, 200), (100, 180, 255), (180, 100, 255), (255, 100, 200),
|
||||||
|
]
|
||||||
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
|
||||||
edge = cv2.Canny(t, 50, 150)
|
|
||||||
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
|
||||||
|
# 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:
|
||||||
|
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),
|
warped = cv2.warpAffine(edge, M, (W, H),
|
||||||
flags=cv2.INTER_NEAREST, borderValue=0)
|
flags=cv2.INTER_NEAREST, borderValue=0)
|
||||||
mask = warped > 0
|
mask = warped > 0
|
||||||
@@ -155,20 +234,34 @@ def _draw_matches(scene: np.ndarray, matches: list[Match],
|
|||||||
overlay = np.zeros_like(out)
|
overlay = np.zeros_like(out)
|
||||||
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)
|
||||||
poly = m.bbox_poly.astype(np.int32).reshape(-1, 1, 2)
|
# bbox poly e linea-marker rimossi (richiesta utente "togli la ROI"):
|
||||||
cv2.polylines(out, [poly], True, color, 2, cv2.LINE_AA)
|
# UCS + edge filtrati gia' identificano pose e orientamento.
|
||||||
p0 = tuple(m.bbox_poly[0].astype(int))
|
cx, cy = int(round(ucs_x)), int(round(ucs_y))
|
||||||
p1 = tuple(m.bbox_poly[1].astype(int))
|
# UCS sul centro pose match (richiesta utente: come nell'anteprima
|
||||||
cv2.line(out, p0, p1, color, 4, cv2.LINE_AA)
|
# modello). Asse X rosso destra, Y verde basso (image y-down).
|
||||||
cx, cy = int(round(m.cx)), int(round(m.cy))
|
# Lunghezza derivata dalla diagonale bbox per scala-invariante.
|
||||||
cv2.drawMarker(out, (cx, cy), color, cv2.MARKER_CROSS, 22, 2, cv2.LINE_AA)
|
|
||||||
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
||||||
a = np.deg2rad(m.angle_deg)
|
if L < 10:
|
||||||
cv2.arrowedLine(out, (cx, cy),
|
L = 30 # fallback se bbox degenere
|
||||||
(int(cx + L * np.cos(a)), int(cy - L * np.sin(a))),
|
ax = np.deg2rad(m.angle_deg)
|
||||||
color, 2, cv2.LINE_AA, tipLength=0.2)
|
# X axis ruotato (rosso)
|
||||||
|
x_end = (int(cx + L * np.cos(ax)), int(cy - L * np.sin(ax)))
|
||||||
|
cv2.arrowedLine(out, (cx, cy), x_end,
|
||||||
|
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
|
||||||
|
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)
|
||||||
|
# Y axis perpendicolare (verde, +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,
|
||||||
|
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
|
||||||
|
cv2.putText(out, "Y", (y_end[0] + 4, y_end[1] + 12),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
|
||||||
|
# Origine UCS: cerchio bianco con bordo nero
|
||||||
|
cv2.circle(out, (cx, cy), 4, (0, 0, 0), -1, cv2.LINE_AA)
|
||||||
|
cv2.circle(out, (cx, cy), 3, (255, 255, 255), -1, cv2.LINE_AA)
|
||||||
label = f"#{i+1} {m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.2f}"
|
label = f"#{i+1} {m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.2f}"
|
||||||
cv2.putText(out, label, (cx + 8, cy - 8),
|
cv2.putText(out, label, (cx + 12, cy - 12),
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -511,7 +604,7 @@ def match(p: MatchParams):
|
|||||||
|
|
||||||
# Render annotated image
|
# Render annotated image
|
||||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
||||||
annotated = _draw_matches(scene, matches, tg)
|
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||||
ann_id = _store_image(annotated)
|
ann_id = _store_image(annotated)
|
||||||
|
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
@@ -588,7 +681,7 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
t_find = time.time() - t0
|
t_find = time.time() - t0
|
||||||
|
|
||||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
||||||
annotated = _draw_matches(scene, matches, tg)
|
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||||
ann_id = _store_image(annotated)
|
ann_id = _store_image(annotated)
|
||||||
|
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
@@ -864,7 +957,7 @@ def match_recipe(p: RecipeMatchParams):
|
|||||||
)
|
)
|
||||||
t_find = time.time() - t0
|
t_find = time.time() - t0
|
||||||
tg = m.template_gray if m.template_gray is not None else np.zeros((1, 1), np.uint8)
|
tg = m.template_gray if m.template_gray is not None else np.zeros((1, 1), np.uint8)
|
||||||
annotated = _draw_matches(scene, matches, tg)
|
annotated = _draw_matches(scene, matches, tg, matcher=m)
|
||||||
ann_id = _store_image(annotated)
|
ann_id = _store_image(annotated)
|
||||||
return MatchResp(
|
return MatchResp(
|
||||||
matches=[MatchResult(
|
matches=[MatchResult(
|
||||||
|
|||||||
Reference in New Issue
Block a user