075b014bd7
LineShapeMatcher: - Feature piramidate precomputate al training (_LevelFeatures per livello piramide, dedup risolto una volta) - Refinement angolare: 5 offset ±step/2 + parabolic fit → precisione ~0.5° con angle_step=5° (10x fine rispetto a step training) - Subpixel posizione: parabolic fit 2D sul picco → frazione pixel - Multithreading: n_threads auto=CPU-1, parallelizza top-level pruning e full-res matching tramite ThreadPoolExecutor (numpy/cv2 rilasciano GIL) GUI: - Dialog edit_params con bottone Auto-tune - Legenda numerata match con pallino colore (#i, coords, angle, scala, score) - Hotkey finestra: r=params, o=nuovo ROI, m=nuovo modello, s=nuova scena - Pannello con train/find time + HOTKEY in basso auto_tune.py: - Analisi template: soglie grad da percentili, num_features da densità edge, pyramid_levels da min_side, min_score da entropia orientation, rilevazione simmetria rotazionale (soglia 0.75 NCC su magnitude) Benchmark clip.png (13 istanze, 72 varianti angolari): prima: 5.84s, precisione 5° (step training) ora: 1.67s, precisione ~0.5°, subpixel posizione speed-up: 3.5x, precisione angolare 10x Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
571 lines
20 KiB
Python
571 lines
20 KiB
Python
"""GUI standalone OpenCV per Pattern Matching 2D.
|
|
|
|
Flusso:
|
|
1. Apri immagine modello (file dialog tk)
|
|
2. Selezione ROI con cv2.selectROI
|
|
3. Apri immagine scena
|
|
4. Esegui matching
|
|
5. Visualizza risultati (baricentro, angolo, score, bbox)
|
|
|
|
Tutta la logica algoritmica vive in pm2d.matcher.EdgeShapeMatcher.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from tkinter import Tk, filedialog
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from pm2d.matcher import EdgeShapeMatcher
|
|
from pm2d.line_matcher import LineShapeMatcher, Match
|
|
from pm2d.auto_tune import auto_tune, summarize as tune_summary
|
|
|
|
|
|
# Schema campi form parametri: (key, label, type, initial)
|
|
PARAM_SCHEMA: list[tuple[str, str, type]] = [
|
|
("backend", "Backend (line | edge)", str),
|
|
("angle_min", "Angolo min [deg]", float),
|
|
("angle_max", "Angolo max [deg]", float),
|
|
("angle_step", "Angolo step [deg]", float),
|
|
("scale_min", "Scala min", float),
|
|
("scale_max", "Scala max", float),
|
|
("scale_step", "Scala step", float),
|
|
("min_score", "Score minimo [0..1]", float),
|
|
("max_matches", "Max match", int),
|
|
("nms_radius", "NMS radius [px] (0=auto)", int),
|
|
("num_features", "Num feature (line)", int),
|
|
("weak_grad", "Weak grad (line)", float),
|
|
("strong_grad", "Strong grad (line)", float),
|
|
("spread_radius", "Spread radius (line)", int),
|
|
("pyramid_levels", "Pyramid levels", int),
|
|
]
|
|
|
|
|
|
def edit_params(defaults: dict, template_bgr: np.ndarray | None = None) -> dict | None:
|
|
"""Dialog tkinter per modificare i parametri.
|
|
|
|
Se `template_bgr` fornito, mostra bottone "Auto-tune" che analizza il template
|
|
e pre-popola i campi con valori suggeriti.
|
|
"""
|
|
root = tk.Tk()
|
|
root.title("Parametri Pattern Matching 2D")
|
|
try:
|
|
root.attributes("-topmost", True)
|
|
except Exception:
|
|
pass
|
|
|
|
result: dict = {}
|
|
entries: dict[str, tk.Entry] = {}
|
|
|
|
frame = ttk.Frame(root, padding=12)
|
|
frame.grid(row=0, column=0, sticky="nsew")
|
|
for i, (key, label, _typ) in enumerate(PARAM_SCHEMA):
|
|
ttk.Label(frame, text=label).grid(row=i, column=0, sticky="w", padx=4, pady=3)
|
|
e = ttk.Entry(frame, width=14)
|
|
e.insert(0, str(defaults.get(key, "")))
|
|
e.grid(row=i, column=1, padx=4, pady=3)
|
|
entries[key] = e
|
|
|
|
hint_var = tk.StringVar(value="")
|
|
hint_lbl = ttk.Label(frame, textvariable=hint_var, foreground="#0088bb",
|
|
wraplength=280)
|
|
hint_lbl.grid(row=len(PARAM_SCHEMA), column=0, columnspan=2,
|
|
sticky="w", pady=(6, 0))
|
|
|
|
def apply_tune():
|
|
if template_bgr is None:
|
|
hint_var.set("Auto-tune non disponibile (template mancante)")
|
|
return
|
|
tune = auto_tune(template_bgr)
|
|
for key, _label, _typ in PARAM_SCHEMA:
|
|
if key in tune:
|
|
entries[key].delete(0, tk.END)
|
|
entries[key].insert(0, str(tune[key]))
|
|
hint_var.set("Auto-tune: " + tune_summary(tune))
|
|
|
|
state = {"ok": False}
|
|
def on_ok():
|
|
try:
|
|
for key, _label, typ in PARAM_SCHEMA:
|
|
val = entries[key].get().strip()
|
|
if typ is int:
|
|
result[key] = int(float(val))
|
|
elif typ is float:
|
|
result[key] = float(val)
|
|
else:
|
|
result[key] = val
|
|
state["ok"] = True
|
|
root.destroy()
|
|
except ValueError as ex:
|
|
hint_var.set(f"Errore parametri: {ex}")
|
|
|
|
def on_cancel():
|
|
root.destroy()
|
|
|
|
btns = ttk.Frame(frame)
|
|
btns.grid(row=len(PARAM_SCHEMA) + 1, column=0, columnspan=2, pady=(10, 0))
|
|
if template_bgr is not None:
|
|
ttk.Button(btns, text="Auto-tune", command=apply_tune).pack(side="left", padx=6)
|
|
ttk.Button(btns, text="Annulla", command=on_cancel).pack(side="left", padx=6)
|
|
ttk.Button(btns, text="OK", command=on_ok).pack(side="left", padx=6)
|
|
|
|
root.bind("<Return>", lambda _e: on_ok())
|
|
root.bind("<Escape>", lambda _e: on_cancel())
|
|
root.mainloop()
|
|
return result if state["ok"] else None
|
|
|
|
|
|
WINDOW_MODEL = "Modello (selezionare ROI - INVIO conferma, c annulla)"
|
|
WINDOW_RESULT = "Risultato matching"
|
|
|
|
|
|
def pick_file(title: str, initialdir: str | None = None) -> str | None:
|
|
"""Tk file picker (root nascosto)."""
|
|
root = Tk()
|
|
root.withdraw()
|
|
path = filedialog.askopenfilename(
|
|
title=title,
|
|
initialdir=initialdir,
|
|
filetypes=[
|
|
("Immagini", "*.png *.jpg *.jpeg *.bmp *.tif *.tiff"),
|
|
("Tutti i file", "*.*"),
|
|
],
|
|
)
|
|
root.destroy()
|
|
return path or None
|
|
|
|
|
|
def load_image(path: str) -> np.ndarray:
|
|
img = cv2.imread(path, cv2.IMREAD_COLOR)
|
|
if img is None:
|
|
raise FileNotFoundError(f"Impossibile leggere immagine: {path}")
|
|
return img
|
|
|
|
|
|
def select_roi(image: np.ndarray) -> np.ndarray | None:
|
|
"""Apre finestra di selezione ROI. Ritorna ROI BGR o None se annullato."""
|
|
disp = _fit_for_display(image, max_side=1200)
|
|
scale = disp.shape[1] / image.shape[1]
|
|
r = cv2.selectROI(WINDOW_MODEL, disp, showCrosshair=True, fromCenter=False)
|
|
cv2.destroyWindow(WINDOW_MODEL)
|
|
x, y, w, h = r
|
|
if w == 0 or h == 0:
|
|
return None
|
|
# Riporta a coordinate immagine originale
|
|
x0 = int(round(x / scale))
|
|
y0 = int(round(y / scale))
|
|
w0 = int(round(w / scale))
|
|
h0 = int(round(h / scale))
|
|
x0 = max(0, x0); y0 = max(0, y0)
|
|
w0 = max(1, min(w0, image.shape[1] - x0))
|
|
h0 = max(1, min(h0, image.shape[0] - y0))
|
|
return image[y0:y0 + h0, x0:x0 + w0].copy()
|
|
|
|
|
|
def _fit_for_display(image: np.ndarray, max_side: int = 1200) -> np.ndarray:
|
|
h, w = image.shape[:2]
|
|
m = max(h, w)
|
|
if m <= max_side:
|
|
return image
|
|
s = max_side / m
|
|
return cv2.resize(image, (int(w * s), int(h * s)), interpolation=cv2.INTER_AREA)
|
|
|
|
|
|
def _warp_template_edges_to_scene(
|
|
template_gray: np.ndarray,
|
|
cx: float, cy: float, angle_deg: float, scale: float,
|
|
scene_shape: tuple[int, int],
|
|
canny_low: int = 50, canny_high: int = 150,
|
|
) -> np.ndarray:
|
|
"""Ritorna mask edge del template ruotato+scalato posizionato in scena."""
|
|
h, w = template_gray.shape
|
|
edge = cv2.Canny(template_gray, canny_low, canny_high)
|
|
# Matrice affine: scala + rotazione attorno al centro template, poi traslazione
|
|
Ht, Wt = h, w
|
|
cx_t = (Wt - 1) / 2.0
|
|
cy_t = (Ht - 1) / 2.0
|
|
M = cv2.getRotationMatrix2D((cx_t, cy_t), angle_deg, scale)
|
|
# Traslazione per portare centro template a (cx, cy) della scena
|
|
M[0, 2] += cx - cx_t
|
|
M[1, 2] += cy - cy_t
|
|
warped = cv2.warpAffine(
|
|
edge, M, (scene_shape[1], scene_shape[0]),
|
|
flags=cv2.INTER_NEAREST, borderValue=0,
|
|
)
|
|
return warped
|
|
|
|
|
|
def draw_matches(
|
|
scene: np.ndarray,
|
|
matches: list[Match],
|
|
template_gray: np.ndarray | None = None,
|
|
overlay_alpha: float = 0.7,
|
|
canny_for_overlay: tuple[int, int] = (50, 150),
|
|
) -> np.ndarray:
|
|
"""Disegna bbox orientato + overlay edge template + baricentro + etichetta."""
|
|
out = scene.copy()
|
|
H, W = scene.shape[:2]
|
|
for i, m in enumerate(matches):
|
|
color = _color_for(i)
|
|
# Overlay edge template nella pose del match (se template disponibile)
|
|
if template_gray is not None:
|
|
emap = _warp_template_edges_to_scene(
|
|
template_gray, m.cx, m.cy, m.angle_deg, m.scale, (H, W),
|
|
canny_low=canny_for_overlay[0], canny_high=canny_for_overlay[1],
|
|
)
|
|
mask = emap > 0
|
|
if mask.any():
|
|
overlay_color = np.zeros_like(out)
|
|
overlay_color[mask] = color
|
|
out[mask] = (
|
|
(1 - overlay_alpha) * out[mask]
|
|
+ overlay_alpha * overlay_color[mask]
|
|
).astype(np.uint8)
|
|
# Bbox orientato (poligono)
|
|
poly = m.bbox_poly.astype(np.int32).reshape(-1, 1, 2)
|
|
cv2.polylines(out, [poly], isClosed=True,
|
|
color=color, thickness=2, lineType=cv2.LINE_AA)
|
|
# Lato top evidenziato per leggere orientamento
|
|
p0 = tuple(m.bbox_poly[0].astype(int))
|
|
p1 = tuple(m.bbox_poly[1].astype(int))
|
|
cv2.line(out, p0, p1, color, 4, cv2.LINE_AA)
|
|
# Baricentro
|
|
cx, cy = int(round(m.cx)), int(round(m.cy))
|
|
cv2.drawMarker(out, (cx, cy), color, cv2.MARKER_CROSS, 22, 2, cv2.LINE_AA)
|
|
cv2.circle(out, (cx, cy), 4, color, -1, cv2.LINE_AA)
|
|
# Asse orientamento
|
|
L = int(np.linalg.norm(m.bbox_poly[1] - m.bbox_poly[0])) // 2
|
|
a = np.deg2rad(m.angle_deg)
|
|
ex = int(round(cx + L * np.cos(a)))
|
|
ey = int(round(cy - L * np.sin(a)))
|
|
cv2.arrowedLine(out, (cx, cy), (ex, ey), color, 2,
|
|
cv2.LINE_AA, tipLength=0.2)
|
|
# Etichetta
|
|
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.FONT_HERSHEY_SIMPLEX, 0.5, color, 2, cv2.LINE_AA)
|
|
return out
|
|
|
|
|
|
def build_info_panel(
|
|
template_bgr: np.ndarray,
|
|
params: dict,
|
|
matches: list[Match],
|
|
panel_width: int = 380,
|
|
panel_height: int | None = None,
|
|
) -> np.ndarray:
|
|
"""Costruisce pannello laterale: thumbnail modello + parametri + legenda
|
|
numerata dei match + hotkey."""
|
|
if panel_height is None:
|
|
panel_height = panel_width * 2
|
|
panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8)
|
|
pad = 12
|
|
y = pad
|
|
|
|
def _text(img, s, y, size=0.5, color=(220, 220, 220), thick=1, x=None):
|
|
cv2.putText(img, s, (x if x is not None else pad, y),
|
|
cv2.FONT_HERSHEY_SIMPLEX, size, color, thick, cv2.LINE_AA)
|
|
|
|
# Titolo
|
|
_text(panel, "MODELLO", y + 18, size=0.7, color=(0, 200, 255), thick=2)
|
|
y += 34
|
|
|
|
# Thumbnail modello
|
|
th_h, th_w = template_bgr.shape[:2]
|
|
max_tw = panel_width - 2 * pad
|
|
max_th = 150
|
|
sc = min(max_tw / th_w, max_th / th_h)
|
|
tw = max(1, int(th_w * sc)); th = max(1, int(th_h * sc))
|
|
thumb = cv2.resize(template_bgr, (tw, th), interpolation=cv2.INTER_AREA)
|
|
if thumb.ndim == 2:
|
|
thumb = cv2.cvtColor(thumb, cv2.COLOR_GRAY2BGR)
|
|
tx = (panel_width - tw) // 2
|
|
panel[y:y + th, tx:tx + tw] = thumb
|
|
cv2.rectangle(panel, (tx - 1, y - 1), (tx + tw, y + th),
|
|
(90, 90, 90), 1, cv2.LINE_AA)
|
|
y += th + 12
|
|
|
|
# Parametri
|
|
_text(panel, "PARAMETRI", y, size=0.55, color=(0, 200, 255), thick=2)
|
|
y += 20
|
|
for k, v in params.items():
|
|
_text(panel, f"{k}: {v}", y, size=0.42)
|
|
y += 16
|
|
|
|
y += 6
|
|
_text(panel, f"RISULTATI ({len(matches)})", y,
|
|
size=0.55, color=(0, 200, 255), thick=2)
|
|
y += 20
|
|
if matches:
|
|
scores = [m.score for m in matches]
|
|
scales = [m.scale for m in matches]
|
|
_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}", y,
|
|
size=0.42); y += 16
|
|
if max(scales) != min(scales):
|
|
_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}", y,
|
|
size=0.42); y += 16
|
|
|
|
# Legenda numerata con colore per ogni match
|
|
max_rows = max(1, (panel_height - y - 120) // 16)
|
|
shown = matches[:max_rows]
|
|
for i, m in enumerate(shown):
|
|
color = _color_for(i)
|
|
# Pallino di colore
|
|
cv2.circle(panel, (pad + 6, y - 4), 5, color, -1, cv2.LINE_AA)
|
|
txt = (f"#{i+1} ({int(m.cx)},{int(m.cy)}) "
|
|
f"{m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.3f}")
|
|
_text(panel, txt, y, size=0.40, x=pad + 18)
|
|
y += 16
|
|
if len(matches) > len(shown):
|
|
_text(panel, f"... +{len(matches) - len(shown)} altri",
|
|
y, size=0.40, color=(150, 150, 150)); y += 16
|
|
|
|
# Hotkey in fondo
|
|
footer_y = panel_height - 92
|
|
_text(panel, "HOTKEY", footer_y, size=0.55, color=(0, 200, 255), thick=2)
|
|
fy = footer_y + 18
|
|
for line in [
|
|
"r modifica parametri",
|
|
"o nuovo ROI (stesso modello)",
|
|
"m nuovo file modello",
|
|
"s nuova scena",
|
|
"q / Esc esci",
|
|
]:
|
|
_text(panel, line, fy, size=0.40, color=(180, 180, 180))
|
|
fy += 14
|
|
return panel
|
|
|
|
|
|
def compose_result(
|
|
scene_annotated: np.ndarray,
|
|
panel: np.ndarray,
|
|
) -> np.ndarray:
|
|
"""Affianca pannello a sinistra + scena a destra, altezza uniforme."""
|
|
sH, sW = scene_annotated.shape[:2]
|
|
pH, pW = panel.shape[:2]
|
|
if pH != sH:
|
|
sc = sH / pH
|
|
new_pW = max(1, int(pW * sc))
|
|
panel = cv2.resize(panel, (new_pW, sH), interpolation=cv2.INTER_AREA)
|
|
pW = new_pW
|
|
out = np.zeros((sH, pW + sW, 3), dtype=np.uint8)
|
|
out[:, :pW] = panel
|
|
out[:, pW:] = scene_annotated
|
|
return out
|
|
|
|
|
|
def _color_for(i: int) -> tuple[int, int, int]:
|
|
palette = [
|
|
(0, 255, 0), (0, 200, 255), (255, 100, 100),
|
|
(255, 200, 0), (200, 0, 255), (100, 255, 200),
|
|
(255, 0, 0), (0, 255, 255),
|
|
]
|
|
return palette[i % len(palette)]
|
|
|
|
|
|
def show_results(
|
|
scene: np.ndarray,
|
|
matches: list[Match],
|
|
template_bgr: np.ndarray | None = None,
|
|
params: dict | None = None,
|
|
) -> str:
|
|
"""Visualizza risultati. Ritorna 'rematch' se l'utente preme 'r', altrimenti 'quit'."""
|
|
print(f"\n=== {len(matches)} match trovati ===")
|
|
for i, m in enumerate(matches):
|
|
print(f" #{i+1}: cx={m.cx:.1f} cy={m.cy:.1f} "
|
|
f"angle={m.angle_deg:.1f}d scale={m.scale:.2f} score={m.score:.3f}")
|
|
template_gray = None
|
|
if template_bgr is not None:
|
|
template_gray = (template_bgr if template_bgr.ndim == 2
|
|
else cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY))
|
|
annotated = draw_matches(scene, matches, template_gray=template_gray)
|
|
if template_bgr is not None and params is not None:
|
|
panel = build_info_panel(template_bgr, params, matches,
|
|
panel_height=annotated.shape[0])
|
|
composed = compose_result(annotated, panel)
|
|
else:
|
|
composed = annotated
|
|
disp = _fit_for_display(composed, max_side=1600)
|
|
cv2.imshow(WINDOW_RESULT, disp)
|
|
print("\n[r] parametri [o] nuovo ROI [m] nuovo modello [s] nuova scena [q/Esc] chiudi")
|
|
action = "quit"
|
|
while True:
|
|
k = cv2.waitKey(0) & 0xFF
|
|
if k in (ord("r"), ord("R")):
|
|
action = "rematch"; break
|
|
if k in (ord("o"), ord("O")):
|
|
action = "new_roi"; break
|
|
if k in (ord("m"), ord("M")):
|
|
action = "new_model"; break
|
|
if k in (ord("s"), ord("S")):
|
|
action = "new_scene"; break
|
|
if k in (27, ord("q"), ord("Q")):
|
|
action = "quit"; break
|
|
if k != 255:
|
|
action = "quit"; break
|
|
cv2.destroyAllWindows()
|
|
return action
|
|
|
|
|
|
def run(
|
|
initial_dir: str | None = None,
|
|
angle_step_deg: float = 5.0,
|
|
angle_range_deg: tuple[float, float] = (0.0, 360.0),
|
|
scale_range: tuple[float, float] = (1.0, 1.0),
|
|
scale_step: float = 0.1,
|
|
num_features: int = 96,
|
|
weak_grad: float = 30.0,
|
|
strong_grad: float = 60.0,
|
|
spread_radius: int = 5,
|
|
pyramid_levels: int = 3,
|
|
min_score: float = 0.55,
|
|
max_matches: int = 25,
|
|
nms_radius: int = 0,
|
|
backend: str = "line",
|
|
) -> None:
|
|
"""Entry-point GUI completo."""
|
|
print("[1] Selezionare immagine MODELLO...")
|
|
model_path = pick_file("Immagine MODELLO", initialdir=initial_dir)
|
|
if not model_path:
|
|
print("Annullato."); return
|
|
model_img = load_image(model_path)
|
|
print(f" caricato: {model_path} shape={model_img.shape}")
|
|
|
|
print("[2] Selezionare ROI sul modello (trascinare, INVIO conferma).")
|
|
roi = select_roi(model_img)
|
|
if roi is None:
|
|
print("ROI vuota, annullato."); return
|
|
print(f" ROI: {roi.shape[1]}x{roi.shape[0]} px")
|
|
|
|
print("[3] Selezionare immagine SCENA...")
|
|
scene_path = pick_file("Immagine SCENA",
|
|
initialdir=str(Path(model_path).parent))
|
|
if not scene_path:
|
|
print("Annullato."); return
|
|
scene = load_image(scene_path)
|
|
print(f" caricato: {scene_path} shape={scene.shape}")
|
|
|
|
# Valori iniziali del form parametri
|
|
cur = {
|
|
"backend": backend,
|
|
"angle_min": angle_range_deg[0],
|
|
"angle_max": angle_range_deg[1],
|
|
"angle_step": angle_step_deg,
|
|
"scale_min": scale_range[0],
|
|
"scale_max": scale_range[1],
|
|
"scale_step": scale_step,
|
|
"min_score": min_score,
|
|
"max_matches": max_matches,
|
|
"nms_radius": nms_radius,
|
|
"num_features": num_features,
|
|
"weak_grad": weak_grad,
|
|
"strong_grad": strong_grad,
|
|
"spread_radius": spread_radius,
|
|
"pyramid_levels": pyramid_levels,
|
|
}
|
|
|
|
while True:
|
|
print("[4/?] Dialog parametri (OK=conferma, Annulla=esci)...")
|
|
new = edit_params(cur, template_bgr=roi)
|
|
if new is None:
|
|
print("Annullato."); return
|
|
cur = new
|
|
|
|
print(f" Train + match (backend={cur['backend']})...")
|
|
if cur["backend"] == "edge":
|
|
matcher: EdgeShapeMatcher | LineShapeMatcher = EdgeShapeMatcher(
|
|
angle_step_deg=cur["angle_step"],
|
|
angle_range_deg=(cur["angle_min"], cur["angle_max"]),
|
|
scale_range=(cur["scale_min"], cur["scale_max"]),
|
|
scale_step=cur["scale_step"],
|
|
)
|
|
else:
|
|
matcher = LineShapeMatcher(
|
|
num_features=cur["num_features"],
|
|
weak_grad=cur["weak_grad"], strong_grad=cur["strong_grad"],
|
|
angle_step_deg=cur["angle_step"],
|
|
angle_range_deg=(cur["angle_min"], cur["angle_max"]),
|
|
scale_range=(cur["scale_min"], cur["scale_max"]),
|
|
scale_step=cur["scale_step"],
|
|
spread_radius=cur["spread_radius"],
|
|
pyramid_levels=cur["pyramid_levels"],
|
|
)
|
|
import time
|
|
t0 = time.time()
|
|
n = matcher.train(roi)
|
|
t_train = time.time() - t0
|
|
print(f" train: {n} varianti in {t_train:.2f}s")
|
|
t0 = time.time()
|
|
nms = cur["nms_radius"] if cur["nms_radius"] > 0 else None
|
|
matches = matcher.find(
|
|
scene, min_score=cur["min_score"],
|
|
max_matches=cur["max_matches"], nms_radius=nms,
|
|
)
|
|
t_find = time.time() - t0
|
|
print(f" find: {len(matches)} match in {t_find:.2f}s")
|
|
|
|
params = {
|
|
"backend": cur["backend"],
|
|
"angle_range": f"{cur['angle_min']:.0f}..{cur['angle_max']:.0f}d",
|
|
"angle_step": f"{cur['angle_step']:.1f}d",
|
|
"scale_range": f"{cur['scale_min']:.2f}..{cur['scale_max']:.2f}",
|
|
"scale_step": f"{cur['scale_step']:.2f}",
|
|
"min_score": f"{cur['min_score']:.2f}",
|
|
"max_matches": str(cur["max_matches"]),
|
|
"nms_radius": str(nms if nms else "auto"),
|
|
"num_variants": str(n),
|
|
"train_time": f"{t_train:.2f}s",
|
|
"find_time": f"{t_find:.2f}s",
|
|
}
|
|
if cur["backend"] == "line":
|
|
params["num_features"] = str(cur["num_features"])
|
|
params["weak/strong"] = f"{cur['weak_grad']:.0f}/{cur['strong_grad']:.0f}"
|
|
params["spread_radius"] = str(cur["spread_radius"])
|
|
params["pyramid_levels"] = str(cur["pyramid_levels"])
|
|
action = show_results(scene, matches, template_bgr=roi, params=params)
|
|
if action == "rematch":
|
|
continue
|
|
if action == "new_roi":
|
|
new_roi = select_roi(model_img)
|
|
if new_roi is None:
|
|
print("ROI annullata, esco.")
|
|
break
|
|
roi = new_roi
|
|
print(f" nuovo ROI: {roi.shape[1]}x{roi.shape[0]} px")
|
|
continue
|
|
if action == "new_model":
|
|
p = pick_file("Nuovo MODELLO",
|
|
initialdir=str(Path(model_path).parent))
|
|
if not p:
|
|
print("Annullato."); break
|
|
model_path = p
|
|
model_img = load_image(model_path)
|
|
print(f" modello: {model_path} shape={model_img.shape}")
|
|
new_roi = select_roi(model_img)
|
|
if new_roi is None:
|
|
print("ROI annullata, esco."); break
|
|
roi = new_roi
|
|
print(f" nuovo ROI: {roi.shape[1]}x{roi.shape[0]} px")
|
|
continue
|
|
if action == "new_scene":
|
|
p = pick_file("Nuova SCENA",
|
|
initialdir=str(Path(scene_path).parent))
|
|
if not p:
|
|
print("Annullato."); break
|
|
scene_path = p
|
|
scene = load_image(scene_path)
|
|
print(f" scena: {scene_path} shape={scene.shape}")
|
|
continue
|
|
# quit
|
|
break
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_dir = "/home/adriano/Documenti/Git_XYZ/VisionSuite/Shape_model_2d/Test"
|
|
run(initial_dir=test_dir if Path(test_dir).is_dir() else None)
|