diff --git a/pm2d/gui.py b/pm2d/gui.py index 418b53d..316f787 100644 --- a/pm2d/gui.py +++ b/pm2d/gui.py @@ -261,33 +261,33 @@ def draw_matches( 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, - params: dict, matches: list[Match], - panel_width: int = 380, - panel_height: int | None = None, + panel_width: int = 300, + panel_height: int = 900, ) -> np.ndarray: - """Costruisce pannello laterale: thumbnail modello + parametri + legenda - numerata dei match + hotkey.""" - if panel_height is None: - panel_height = panel_width * 2 + """Pannello sinistro: thumbnail modello + legenda risultati (senza parametri).""" 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) + # Titolo MODELLO + _put_text(panel, "MODELLO", pad, y + 18, size=0.7, + color=(0, 200, 255), thick=2) y += 34 - # Thumbnail modello + # Thumbnail th_h, th_w = template_bgr.shape[:2] max_tw = panel_width - 2 * pad - max_th = 150 + max_th = 160 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) @@ -297,74 +297,123 @@ def build_info_panel( 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 + y += th + 16 - # 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 + # Risultati + _put_text(panel, f"RISULTATI ({len(matches)})", pad, y, + size=0.6, color=(0, 200, 255), thick=2) + y += 22 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 + _put_text(panel, f"score: {min(scores):.2f}..{max(scores):.2f}", + pad, 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) + _put_text(panel, f"scale: {min(scales):.2f}..{max(scales):.2f}", + pad, y, size=0.42); y += 16 + y += 4 + max_rows = max(1, (panel_height - y - 12) // 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) + _put_text(panel, txt, pad + 18, y, size=0.40) 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 + _put_text(panel, f"... +{len(matches) - len(shown)} altri", + pad, y, size=0.40, color=(150, 150, 150)) return panel -def compose_result( - scene_annotated: np.ndarray, - panel: np.ndarray, +def build_right_panel( + params: dict, + panel_width: int = 320, + panel_height: int = 900, ) -> 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 + """Pannello destro: parametri correnti + hotkey.""" + panel = np.full((panel_height, panel_width, 3), 28, dtype=np.uint8) + pad = 12 + y = pad + + _put_text(panel, "PARAMETRI", pad, y + 18, size=0.7, + color=(0, 200, 255), thick=2) + y += 36 + + # Tempi in alto, evidenziati + 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 @@ -382,28 +431,38 @@ def show_results( matches: list[Match], template_bgr: np.ndarray | None = None, params: dict | None = None, + window_w: int = 1600, + window_h: int = 900, ) -> 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 ===") 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) + + if template_bgr is not None: + left = build_left_panel(template_bgr, matches, panel_height=window_h) else: - composed = annotated - disp = _fit_for_display(composed, max_side=1600) - cv2.namedWindow(WINDOW_RESULT, cv2.WINDOW_NORMAL) - cv2.resizeWindow(WINDOW_RESULT, min(disp.shape[1], 1600), - min(disp.shape[0], 900)) - cv2.imshow(WINDOW_RESULT, disp) + left = np.full((window_h, 300, 3), 28, dtype=np.uint8) + if params is not None: + right = build_right_panel(params, panel_height=window_h) + else: + right = np.full((window_h, 320, 3), 28, dtype=np.uint8) + + 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") action = "quit" while True: