|
|
@@ -78,6 +78,7 @@ def _matcher_cache_key(roi: np.ndarray, tech: dict) -> str:
|
|
|
|
h.update(roi.tobytes())
|
|
|
|
h.update(roi.tobytes())
|
|
|
|
# Solo parametri che influenzano il training
|
|
|
|
# Solo parametri che influenzano il training
|
|
|
|
relevant = ("num_features", "weak_grad", "strong_grad",
|
|
|
|
relevant = ("num_features", "weak_grad", "strong_grad",
|
|
|
|
|
|
|
|
"min_feature_spacing",
|
|
|
|
"angle_min", "angle_max", "angle_step",
|
|
|
|
"angle_min", "angle_max", "angle_step",
|
|
|
|
"scale_min", "scale_max", "scale_step",
|
|
|
|
"scale_min", "scale_max", "scale_step",
|
|
|
|
"spread_radius", "pyramid_levels")
|
|
|
|
"spread_radius", "pyramid_levels")
|
|
|
@@ -131,45 +132,84 @@ 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 SOLO UCS (richiesta utente) per ogni match trovato.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
UCS = sistema di coordinate (X rosso, Y verde) posizionato sul
|
|
|
|
|
|
|
|
baricentro feature del modello, ruotato secondo l'angolo del match.
|
|
|
|
|
|
|
|
Niente edge, niente cerchietti feature, niente bbox: i match sulla
|
|
|
|
|
|
|
|
scena reale devono essere puliti, gli edge filtrati si vedono solo
|
|
|
|
|
|
|
|
nell'anteprima modello.
|
|
|
|
|
|
|
|
"""
|
|
|
|
out = scene.copy()
|
|
|
|
out = scene.copy()
|
|
|
|
H, W = scene.shape[:2]
|
|
|
|
# Baricentro UCS in coord template (calcolato una volta dal matcher
|
|
|
|
palette = [
|
|
|
|
# se disponibile): mean delle feature di una variante a 0°. Questo e'
|
|
|
|
(0, 255, 0), (0, 200, 255), (255, 100, 100), (255, 200, 0),
|
|
|
|
# lo stesso baricentro mostrato nell'anteprima modello.
|
|
|
|
(200, 0, 255), (100, 255, 200), (255, 0, 0), (0, 255, 255),
|
|
|
|
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):
|
|
|
|
|
|
|
|
L_base = int(0.15 * max(matcher.template_size))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
L_base = 30
|
|
|
|
|
|
|
|
H_scene, W_scene = scene.shape[:2]
|
|
|
|
for i, m in enumerate(matches):
|
|
|
|
for i, m in enumerate(matches):
|
|
|
|
color = palette[i % len(palette)]
|
|
|
|
# Proietta baricentro template alla pose del match.
|
|
|
|
if template_gray is not None:
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
# Overlay edge del modello orientato (richiesta utente):
|
|
|
|
|
|
|
|
# warpa template alla pose, applica hysteresis identica al matcher,
|
|
|
|
|
|
|
|
# disegna pixel edge come overlay verde tenue.
|
|
|
|
|
|
|
|
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
|
|
|
|
warped = cv2.warpAffine(edge, M, (W, H),
|
|
|
|
warped_gray = cv2.warpAffine(
|
|
|
|
flags=cv2.INTER_NEAREST, borderValue=0)
|
|
|
|
t, M, (W_scene, H_scene),
|
|
|
|
mask = warped > 0
|
|
|
|
flags=cv2.INTER_LINEAR, borderValue=0)
|
|
|
|
if mask.any():
|
|
|
|
mag, _ = matcher._gradient(warped_gray)
|
|
|
|
overlay = np.zeros_like(out)
|
|
|
|
if matcher.weak_grad < matcher.strong_grad:
|
|
|
|
overlay[mask] = color
|
|
|
|
edge_mask = matcher._hysteresis_mask(mag)
|
|
|
|
out[mask] = (0.3 * out[mask] + 0.7 * overlay[mask]).astype(np.uint8)
|
|
|
|
else:
|
|
|
|
poly = m.bbox_poly.astype(np.int32).reshape(-1, 1, 2)
|
|
|
|
edge_mask = mag >= matcher.strong_grad
|
|
|
|
cv2.polylines(out, [poly], True, color, 2, cv2.LINE_AA)
|
|
|
|
if edge_mask.any():
|
|
|
|
p0 = tuple(m.bbox_poly[0].astype(int))
|
|
|
|
edge_overlay = np.zeros_like(out)
|
|
|
|
p1 = tuple(m.bbox_poly[1].astype(int))
|
|
|
|
edge_overlay[edge_mask] = (0, 220, 0) # verde brillante
|
|
|
|
cv2.line(out, p0, p1, color, 4, cv2.LINE_AA)
|
|
|
|
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.6, 0)
|
|
|
|
cx, cy = int(round(m.cx)), int(round(m.cy))
|
|
|
|
L = max(20, int(L_base * m.scale))
|
|
|
|
cv2.drawMarker(out, (cx, cy), color, cv2.MARKER_CROSS, 22, 2, cv2.LINE_AA)
|
|
|
|
# X axis = rotazione di (1, 0) con cv2 matrix → (cos, -sin)
|
|
|
|
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
|
|
|
x_end = (int(cx + L * ca), int(cy - L * sa))
|
|
|
|
a = np.deg2rad(m.angle_deg)
|
|
|
|
# Y axis = rotazione di (0, 1) con cv2 matrix → (sin, cos)
|
|
|
|
cv2.arrowedLine(out, (cx, cy),
|
|
|
|
# A m.angle_deg=0 deve puntare GIU' (image y-down convenzione modello)
|
|
|
|
(int(cx + L * np.cos(a)), int(cy - L * np.sin(a))),
|
|
|
|
y_end = (int(cx + L * sa), int(cy + L * ca))
|
|
|
|
color, 2, cv2.LINE_AA, tipLength=0.2)
|
|
|
|
cv2.arrowedLine(out, (cx, cy), x_end,
|
|
|
|
label = f"#{i+1} {m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.2f}"
|
|
|
|
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
|
|
|
|
cv2.putText(out, label, (cx + 8, cy - 8),
|
|
|
|
cv2.putText(out, "X", (x_end[0] + 4, x_end[1] + 5),
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
|
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
|
|
|
|
|
|
|
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)
|
|
|
|
return out
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -272,6 +312,15 @@ class SimpleMatchParams(BaseModel):
|
|
|
|
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
|
|
|
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
|
|
|
min_score: float = 0.65
|
|
|
|
min_score: float = 0.65
|
|
|
|
max_matches: int = 25
|
|
|
|
max_matches: int = 25
|
|
|
|
|
|
|
|
# --- Override edge da pannello "Anteprima edge" (None = auto_tune) ---
|
|
|
|
|
|
|
|
# Quando settati, sovrascrivono i valori derivati da auto_tune e
|
|
|
|
|
|
|
|
# vengono usati identici sia nel training del matcher sia nel find.
|
|
|
|
|
|
|
|
# Salvati nella ricetta cosi' la stessa pulizia rumore e' replicata
|
|
|
|
|
|
|
|
# quando la ricetta viene caricata.
|
|
|
|
|
|
|
|
edge_weak_grad: float | None = None
|
|
|
|
|
|
|
|
edge_strong_grad: float | None = None
|
|
|
|
|
|
|
|
edge_num_features: int | None = None
|
|
|
|
|
|
|
|
edge_min_feature_spacing: int | None = None
|
|
|
|
# --- Halcon-mode flags (default off = backward compat) ---
|
|
|
|
# --- Halcon-mode flags (default off = backward compat) ---
|
|
|
|
# Init-time (richiede ri-train se cambiato)
|
|
|
|
# Init-time (richiede ri-train se cambiato)
|
|
|
|
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
|
|
|
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
|
|
@@ -320,10 +369,24 @@ def _simple_to_technical(
|
|
|
|
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
|
|
|
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
|
|
|
ang_step = PRECISION_ANGLE_STEP.get(p.precisione, 5.0)
|
|
|
|
ang_step = PRECISION_ANGLE_STEP.get(p.precisione, 5.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Override edge dal pannello "Anteprima edge" se utente li ha settati.
|
|
|
|
|
|
|
|
# Questi sostituiscono i valori auto_tune nel training del matcher,
|
|
|
|
|
|
|
|
# garantendo che la selezione edge identica a quella del preview
|
|
|
|
|
|
|
|
# venga usata sia in training sia in find.
|
|
|
|
|
|
|
|
weak_g = (p.edge_weak_grad if p.edge_weak_grad is not None
|
|
|
|
|
|
|
|
else tune["weak_grad"])
|
|
|
|
|
|
|
|
strong_g = (p.edge_strong_grad if p.edge_strong_grad is not None
|
|
|
|
|
|
|
|
else tune["strong_grad"])
|
|
|
|
|
|
|
|
n_feat = (p.edge_num_features if p.edge_num_features is not None
|
|
|
|
|
|
|
|
else nf)
|
|
|
|
|
|
|
|
min_sp = (p.edge_min_feature_spacing if p.edge_min_feature_spacing is not None
|
|
|
|
|
|
|
|
else 3)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"num_features": nf,
|
|
|
|
"num_features": n_feat,
|
|
|
|
"weak_grad": tune["weak_grad"],
|
|
|
|
"weak_grad": weak_g,
|
|
|
|
"strong_grad": tune["strong_grad"],
|
|
|
|
"strong_grad": strong_g,
|
|
|
|
|
|
|
|
"min_feature_spacing": min_sp,
|
|
|
|
"spread_radius": spread,
|
|
|
|
"spread_radius": spread,
|
|
|
|
"pyramid_levels": pyr,
|
|
|
|
"pyramid_levels": pyr,
|
|
|
|
"angle_min": 0.0,
|
|
|
|
"angle_min": 0.0,
|
|
|
@@ -511,7 +574,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(
|
|
|
@@ -559,6 +622,7 @@ def match_simple(p: SimpleMatchParams):
|
|
|
|
scale_range=(tech["scale_min"], tech["scale_max"]),
|
|
|
|
scale_range=(tech["scale_min"], tech["scale_max"]),
|
|
|
|
scale_step=tech["scale_step"],
|
|
|
|
scale_step=tech["scale_step"],
|
|
|
|
spread_radius=tech["spread_radius"],
|
|
|
|
spread_radius=tech["spread_radius"],
|
|
|
|
|
|
|
|
min_feature_spacing=tech.get("min_feature_spacing", 3),
|
|
|
|
pyramid_levels=tech["pyramid_levels"],
|
|
|
|
pyramid_levels=tech["pyramid_levels"],
|
|
|
|
use_polarity=p.use_polarity,
|
|
|
|
use_polarity=p.use_polarity,
|
|
|
|
use_gpu=p.use_gpu,
|
|
|
|
use_gpu=p.use_gpu,
|
|
|
@@ -588,7 +652,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(
|
|
|
@@ -628,6 +692,11 @@ class SaveRecipeParams(BaseModel):
|
|
|
|
precisione: str = "normale"
|
|
|
|
precisione: str = "normale"
|
|
|
|
use_polarity: bool = False
|
|
|
|
use_polarity: bool = False
|
|
|
|
use_gpu: bool = False
|
|
|
|
use_gpu: bool = False
|
|
|
|
|
|
|
|
# Override edge dal pannello "Anteprima edge" (None = auto_tune)
|
|
|
|
|
|
|
|
edge_weak_grad: float | None = None
|
|
|
|
|
|
|
|
edge_strong_grad: float | None = None
|
|
|
|
|
|
|
|
edge_num_features: int | None = None
|
|
|
|
|
|
|
|
edge_min_feature_spacing: int | None = None
|
|
|
|
name: str # nome file ricetta (no path)
|
|
|
|
name: str # nome file ricetta (no path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -745,6 +814,10 @@ def save_recipe(p: SaveRecipeParams):
|
|
|
|
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
|
|
|
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
|
|
|
precisione=p.precisione,
|
|
|
|
precisione=p.precisione,
|
|
|
|
use_polarity=p.use_polarity, use_gpu=p.use_gpu,
|
|
|
|
use_polarity=p.use_polarity, use_gpu=p.use_gpu,
|
|
|
|
|
|
|
|
edge_weak_grad=p.edge_weak_grad,
|
|
|
|
|
|
|
|
edge_strong_grad=p.edge_strong_grad,
|
|
|
|
|
|
|
|
edge_num_features=p.edge_num_features,
|
|
|
|
|
|
|
|
edge_min_feature_spacing=p.edge_min_feature_spacing,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
tech = _simple_to_technical(sp, roi_img)
|
|
|
|
tech = _simple_to_technical(sp, roi_img)
|
|
|
|
m = LineShapeMatcher(
|
|
|
|
m = LineShapeMatcher(
|
|
|
@@ -864,7 +937,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(
|
|
|
|