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:
+137
-78
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user