ui: layout fisso 1600x900 con pannelli SX/DX, scena scalata

- Finestra dimensione fissa: scena scalata fit-to-box mantenendo aspect
  ratio (anche immagini piccole riempiono il layout)
- Pannello sinistro: MODELLO thumbnail + RISULTATI legenda numerata
- Pannello destro: PARAMETRI sempre visibili (train/find time evidenziati)
  + HOTKEY
- Rimossi parametri duplicati da pannello sinistro

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 08:59:06 +02:00
parent ba54b42fdc
commit 4ddda1ec62
+137 -78
View File
@@ -261,33 +261,33 @@ def draw_matches(
return out return out
def build_info_panel( def _put_text(img: np.ndarray, s: str, x: int, y: int,
size: float = 0.5, color: tuple = (220, 220, 220),
thick: int = 1) -> None:
cv2.putText(img, s, (x, y), cv2.FONT_HERSHEY_SIMPLEX,
size, color, thick, cv2.LINE_AA)
def build_left_panel(
template_bgr: np.ndarray, template_bgr: np.ndarray,
params: dict,
matches: list[Match], matches: list[Match],
panel_width: int = 380, panel_width: int = 300,
panel_height: int | None = None, panel_height: int = 900,
) -> np.ndarray: ) -> np.ndarray:
"""Costruisce pannello laterale: thumbnail modello + parametri + legenda """Pannello sinistro: thumbnail modello + legenda risultati (senza parametri)."""
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) panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8)
pad = 12 pad = 12
y = pad y = pad
def _text(img, s, y, size=0.5, color=(220, 220, 220), thick=1, x=None): # Titolo MODELLO
cv2.putText(img, s, (x if x is not None else pad, y), _put_text(panel, "MODELLO", pad, y + 18, size=0.7,
cv2.FONT_HERSHEY_SIMPLEX, size, color, thick, cv2.LINE_AA) color=(0, 200, 255), thick=2)
# Titolo
_text(panel, "MODELLO", y + 18, size=0.7, color=(0, 200, 255), thick=2)
y += 34 y += 34
# Thumbnail modello # Thumbnail
th_h, th_w = template_bgr.shape[:2] th_h, th_w = template_bgr.shape[:2]
max_tw = panel_width - 2 * pad max_tw = panel_width - 2 * pad
max_th = 150 max_th = 160
sc = min(max_tw / th_w, max_th / th_h) sc = min(max_tw / th_w, max_th / th_h)
tw = max(1, int(th_w * sc)); th = max(1, int(th_h * sc)) 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) thumb = cv2.resize(template_bgr, (tw, th), interpolation=cv2.INTER_AREA)
@@ -297,74 +297,123 @@ def build_info_panel(
panel[y:y + th, tx:tx + tw] = thumb panel[y:y + th, tx:tx + tw] = thumb
cv2.rectangle(panel, (tx - 1, y - 1), (tx + tw, y + th), cv2.rectangle(panel, (tx - 1, y - 1), (tx + tw, y + th),
(90, 90, 90), 1, cv2.LINE_AA) (90, 90, 90), 1, cv2.LINE_AA)
y += th + 12 y += th + 16
# Parametri # Risultati
_text(panel, "PARAMETRI", y, size=0.55, color=(0, 200, 255), thick=2) _put_text(panel, f"RISULTATI ({len(matches)})", pad, y,
y += 20 size=0.6, color=(0, 200, 255), thick=2)
for k, v in params.items(): y += 22
_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: if matches:
scores = [m.score for m in matches] scores = [m.score for m in matches]
scales = [m.scale for m in matches] scales = [m.scale for m in matches]
_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}", y, _put_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}",
size=0.42); y += 16 pad, y, size=0.42); y += 16
if max(scales) != min(scales): if max(scales) != min(scales):
_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}", y, _put_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}",
size=0.42); y += 16 pad, y, size=0.42); y += 16
y += 4
# Legenda numerata con colore per ogni match max_rows = max(1, (panel_height - y - 12) // 16)
max_rows = max(1, (panel_height - y - 120) // 16)
shown = matches[:max_rows] shown = matches[:max_rows]
for i, m in enumerate(shown): for i, m in enumerate(shown):
color = _color_for(i) color = _color_for(i)
# Pallino di colore
cv2.circle(panel, (pad + 6, y - 4), 5, color, -1, cv2.LINE_AA) cv2.circle(panel, (pad + 6, y - 4), 5, color, -1, cv2.LINE_AA)
txt = (f"#{i+1} ({int(m.cx)},{int(m.cy)}) " txt = (f"#{i+1} ({int(m.cx)},{int(m.cy)}) "
f"{m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.3f}") f"{m.angle_deg:.0f}d s={m.scale:.2f} {m.score:.3f}")
_text(panel, txt, y, size=0.40, x=pad + 18) _put_text(panel, txt, pad + 18, y, size=0.40)
y += 16 y += 16
if len(matches) > len(shown): if len(matches) > len(shown):
_text(panel, f"... +{len(matches) - len(shown)} altri", _put_text(panel, f"... +{len(matches) - len(shown)} altri",
y, size=0.40, color=(150, 150, 150)); y += 16 pad, y, size=0.40, color=(150, 150, 150))
# 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 return panel
def compose_result( def build_right_panel(
scene_annotated: np.ndarray, params: dict,
panel: np.ndarray, panel_width: int = 320,
panel_height: int = 900,
) -> np.ndarray: ) -> np.ndarray:
"""Affianca pannello a sinistra + scena a destra, altezza uniforme.""" """Pannello destro: parametri correnti + hotkey."""
sH, sW = scene_annotated.shape[:2] panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8)
pH, pW = panel.shape[:2] pad = 12
if pH != sH: y = pad
sc = sH / pH
new_pW = max(1, int(pW * sc)) _put_text(panel, "PARAMETRI", pad, y + 18, size=0.7,
panel = cv2.resize(panel, (new_pW, sH), interpolation=cv2.INTER_AREA) color=(0, 200, 255), thick=2)
pW = new_pW y += 36
out = np.zeros((sH, pW + sW, 3), dtype=np.uint8)
out[:, :pW] = panel # Tempi in alto, evidenziati
out[:, pW:] = scene_annotated for k in ("train_time", "find_time", "num_variants"):
if k in params:
v = params[k]
_put_text(panel, f"{k}:", pad, y,
size=0.45, color=(160, 220, 160))
_put_text(panel, str(v), pad + 140, y,
size=0.45, color=(200, 255, 200), thick=2)
y += 18
y += 6
# Altri parametri
skip = {"train_time", "find_time", "num_variants"}
for k, v in params.items():
if k in skip:
continue
_put_text(panel, f"{k}:", pad, y, size=0.42, color=(180, 180, 180))
_put_text(panel, str(v), pad + 140, y, size=0.42,
color=(220, 220, 220))
y += 16
# Hotkey in fondo
footer_y = panel_height - 110
_put_text(panel, "HOTKEY", pad, footer_y, size=0.6,
color=(0, 200, 255), thick=2)
fy = footer_y + 22
for line in [
"r modifica parametri",
"o nuovo ROI",
"m nuovo modello",
"s nuova scena",
"q / Esc esci",
]:
_put_text(panel, line, pad, fy, size=0.42, color=(200, 200, 200))
fy += 16
return panel
def _fit_scene_center(
scene: np.ndarray, target_w: int, target_h: int,
) -> np.ndarray:
"""Scala scena a fit (target_w, target_h) mantenendo aspect; padding bg."""
h, w = scene.shape[:2]
sc = min(target_w / w, target_h / h)
new_w = max(1, int(w * sc)); new_h = max(1, int(h * sc))
resized = cv2.resize(scene, (new_w, new_h), interpolation=cv2.INTER_AREA)
out = np.full((target_h, target_w, 3), 20, dtype=np.uint8)
y0 = (target_h - new_h) // 2
x0 = (target_w - new_w) // 2
out[y0:y0 + new_h, x0:x0 + new_w] = resized
return out
def compose_fixed_layout(
scene_annotated: np.ndarray,
left_panel: np.ndarray,
right_panel: np.ndarray,
window_w: int = 1600,
window_h: int = 900,
) -> np.ndarray:
"""Layout fisso: [left | scena fit-scaled | right]."""
lH, lW = left_panel.shape[:2]
rH, rW = right_panel.shape[:2]
# Altezza uniforme (pannelli dovrebbero essere già window_h)
if lH != window_h:
left_panel = cv2.resize(left_panel, (lW, window_h),
interpolation=cv2.INTER_AREA)
if rH != window_h:
right_panel = cv2.resize(right_panel, (rW, window_h),
interpolation=cv2.INTER_AREA)
center_w = window_w - lW - rW
center = _fit_scene_center(scene_annotated, center_w, window_h)
out = np.concatenate([left_panel, center, right_panel], axis=1)
return out return out
@@ -382,28 +431,38 @@ def show_results(
matches: list[Match], matches: list[Match],
template_bgr: np.ndarray | None = None, template_bgr: np.ndarray | None = None,
params: dict | None = None, params: dict | None = None,
window_w: int = 1600,
window_h: int = 900,
) -> str: ) -> str:
"""Visualizza risultati. Ritorna 'rematch' se l'utente preme 'r', altrimenti 'quit'.""" """Visualizza risultati in layout fisso [SX panel | scena scalata | DX panel].
Ritorna 'rematch'/'new_roi'/'new_model'/'new_scene'/'quit'.
"""
print(f"\n=== {len(matches)} match trovati ===") print(f"\n=== {len(matches)} match trovati ===")
for i, m in enumerate(matches): for i, m in enumerate(matches):
print(f" #{i+1}: cx={m.cx:.1f} cy={m.cy:.1f} " 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}") f"angle={m.angle_deg:.1f}d scale={m.scale:.2f} score={m.score:.3f}")
template_gray = None template_gray = None
if template_bgr is not None: if template_bgr is not None:
template_gray = (template_bgr if template_bgr.ndim == 2 template_gray = (template_bgr if template_bgr.ndim == 2
else cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY)) else cv2.cvtColor(template_bgr, cv2.COLOR_BGR2GRAY))
annotated = draw_matches(scene, matches, template_gray=template_gray) 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, if template_bgr is not None:
panel_height=annotated.shape[0]) left = build_left_panel(template_bgr, matches, panel_height=window_h)
composed = compose_result(annotated, panel)
else: else:
composed = annotated left = np.full((window_h, 300, 3), 28, dtype=np.uint8)
disp = _fit_for_display(composed, max_side=1600) if params is not None:
cv2.namedWindow(WINDOW_RESULT, cv2.WINDOW_NORMAL) right = build_right_panel(params, panel_height=window_h)
cv2.resizeWindow(WINDOW_RESULT, min(disp.shape[1], 1600), else:
min(disp.shape[0], 900)) right = np.full((window_h, 320, 3), 28, dtype=np.uint8)
cv2.imshow(WINDOW_RESULT, disp)
composed = compose_fixed_layout(
annotated, left, right, window_w=window_w, window_h=window_h,
)
cv2.namedWindow(WINDOW_RESULT, cv2.WINDOW_AUTOSIZE)
cv2.imshow(WINDOW_RESULT, composed)
print("\n[r] parametri [o] nuovo ROI [m] nuovo modello [s] nuova scena [q/Esc] chiudi") print("\n[r] parametri [o] nuovo ROI [m] nuovo modello [s] nuova scena [q/Esc] chiudi")
action = "quit" action = "quit"
while True: while True: