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