imgnetV1 / visualize_onnx.py
imghost11's picture
Upload 4 files
06f4503 verified
Raw
History Blame Contribute Delete
29.9 kB
# imgnet_visualizer_onnx.py
# IMGNet Interactive Visualizer β€” pakai ONNX Runtime (tidak butuh PyTorch)
# Panel Kiri : Upload 2 foto + MTCNN crop
# Panel Tengah: Sliding window embedding analysis (Training vs Metric mode)
# Panel Kanan : Embedding bars + Sign + Chain pattern
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
import numpy as np
import math
from collections import Counter
# ── CONFIG ─────────────────────────────────────────────────
ONNX_PATH = r"C:\PythonProj\img_bnn\imgnet_conv10_epoch39.onnx"
WINDOW_SIZE = 11
THRESHOLD = 8
EMB_DIM = 1024
IMG_SIZE = 112
BETA = 10.0
NEUTRAL_LEN = 29
REWARD_RATE = 0.3
PUNISH_RATE = 1.0
# ── COLORS ─────────────────────────────────────────────────
BG= "#0a0e1a"; CARD= "#111827"; BORDER="#1e293b"
BLUE= "#6366f1"; GREEN= "#10b981"; ORANGE="#f59e0b"
PURPLE="#a855f7"; TEAL= "#14b8a6"; RED= "#ef4444"
YELLOW="#fbbf24"; WHITE= "#ffffff"; SUB= "#64748b"; TEXT="#e2e8f0"
# ── ONNX RUNTIME LOAD ──────────────────────────────────────
try:
import onnxruntime as rt
import os
if os.path.exists(ONNX_PATH):
providers = ["CUDAExecutionProvider","CPUExecutionProvider"]
_sess = rt.InferenceSession(ONNX_PATH, providers=providers)
_inp_name = _sess.get_inputs()[0].name
ONNX_OK = True
# Detect actual device
used = _sess.get_providers()[0]
DEVICE_STR = "CUDA" if "CUDA" in used else "CPU"
print(f"βœ“ ONNX loaded β€” provider: {used}")
else:
_sess = None; ONNX_OK = False; DEVICE_STR = "β€”"
print(f"βœ— ONNX file tidak ditemukan: {ONNX_PATH}")
except ImportError:
_sess = None; ONNX_OK = False; DEVICE_STR = "β€”"
print("βœ— onnxruntime tidak terinstall β€” pip install onnxruntime")
# ── MTCNN ──────────────────────────────────────────────────
try:
from facenet_pytorch import MTCNN
_mtcnn = MTCNN(image_size=112, keep_all=False, post_process=False, device="cpu")
MTCNN_OK = True
except: _mtcnn = None; MTCNN_OK = False
# ── INFERENCE ──────────────────────────────────────────────
def get_emb(arr):
"""arr: np.uint8 (112,112,3) β†’ embedding (1024,)"""
if _sess is not None:
t = arr.astype(np.float32) / 255.0
t = t.transpose(2, 0, 1)[np.newaxis] # (1,3,112,112)
return _sess.run(None, {_inp_name: t})[0][0]
# Fallback dummy
np.random.seed(int(arr.sum()) % 2**31)
e = np.random.randn(EMB_DIM).astype(np.float32)
return e / (np.linalg.norm(e) + 1e-8)
def load_face(path):
img = Image.open(path).convert("RGB")
if MTCNN_OK and _mtcnn:
try:
face = _mtcnn(img)
if face is not None:
return np.clip(face.permute(1,2,0).numpy(), 0, 255).astype(np.uint8)
except: pass
return np.array(img.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR))
# ── METRICS ────────────────────────────────────────────────
def img_sign_score(e1, e2):
n = len(e1) - WINDOW_SIZE + 1
return sum(1 for i in range(n)
if sum(1 for j in range(WINDOW_SIZE)
if (e1[i+j]>=0)==(e2[i+j]>=0)) >= THRESHOLD) / max(n,1)
def amp_score(e1, e2):
n = len(e1) - WINDOW_SIZE + 1; tot = 0.0
for i in range(n):
w1, w2 = e1[i:i+WINDOW_SIZE], e2[i:i+WINDOW_SIZE]
s1 = np.where(w1>=0,1,-1).astype(np.int8)
s2 = np.where(w2>=0,1,-1).astype(np.int8)
if int(np.sum(s1==s2)) >= THRESHOLD:
a1, a2 = np.mean(np.abs(w1)), np.mean(np.abs(w2))
tot += max(0.0, 1 - abs(a1-a2)/max(a1,a2,1e-6))
return tot / max(n,1)
def chain_score(e1, e2):
n = len(e1) - WINDOW_SIZE + 1
flags = [int(np.sum(
np.where(e1[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8) ==
np.where(e2[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8)
)) >= THRESHOLD for i in range(n)]
total = sum(flags); sg = total / max(n,1)
nc = 0; ic = False
for f in flags:
if f and not ic: nc+=1; ic=True
elif not f: ic=False
if nc==0 or total==0: return 0.0, 0, 0.0
ac = total/nc; diff = ac - NEUTRAL_LEN
s = sg + (REWARD_RATE*diff if diff>=0 else PUNISH_RATE*diff)/100
return float(np.clip(s,0,1)), nc, ac
def cosine(e1, e2):
return float(np.dot(e1,e2)/(np.linalg.norm(e1)*np.linalg.norm(e2)+1e-8))
def tanh_agreement(e1, e2):
return (np.tanh(BETA * e1 * e2) + 1) / 2
# ============================================================
# APP
# ============================================================
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("IMGNet Visualizer β€” ONNX Runtime")
self.geometry("1500x880")
self.configure(bg=BG)
self.resizable(True, True)
self.arr1 = None; self.arr2 = None
self.e1 = None; self.e2 = None
self.win_pos = 0
self.animating = False
self.mode = tk.StringVar(value="metric")
self._build()
def _build(self):
# Top bar
top = tk.Frame(self, bg=BG); top.pack(fill="x", padx=12, pady=(8,4))
tk.Label(top, text="IMGNet Β· Interactive Visualizer Β· ONNX Runtime",
font=("Courier",13,"bold"), bg=BG, fg=TEXT).pack(side="left")
st = f"ONNX={'βœ“' if ONNX_OK else 'βœ—'} Device={DEVICE_STR} MTCNN={'βœ“' if MTCNN_OK else 'βœ—'} w={WINDOW_SIZE} t={THRESHOLD}/11"
tk.Label(top, text=st, font=("Courier",9), bg=BG, fg=SUB).pack(side="right")
main = tk.Frame(self, bg=BG); main.pack(fill="both", expand=True, padx=8, pady=4)
main.grid_columnconfigure(0, weight=0, minsize=240)
main.grid_columnconfigure(1, weight=1)
main.grid_columnconfigure(2, weight=0, minsize=260)
main.grid_rowconfigure(0, weight=1)
self._build_left(main)
self._build_center(main)
self._build_right(main)
# ── LEFT ─────────────────────────────────────────────
def _build_left(self, parent):
lf = tk.Frame(parent, bg=CARD, highlightthickness=1,
highlightbackground=BORDER, width=240)
lf.grid(row=0, column=0, sticky="nsew", padx=(0,6))
lf.grid_propagate(False)
tk.Label(lf, text="INPUT IMAGES", font=("Courier",10,"bold"),
bg=CARD, fg=BLUE).pack(pady=(10,4))
# Upload buttons
bf = tk.Frame(lf, bg=CARD); bf.pack(fill="x", padx=8)
tk.Button(bf, text="Upload Foto 1", command=self.upload1,
bg=BLUE, fg=WHITE, font=("Courier",9,"bold"),
relief="flat", pady=5, cursor="hand2").pack(side="left", expand=True, fill="x", padx=(0,2))
tk.Button(bf, text="Upload Foto 2", command=self.upload2,
bg=GREEN, fg=WHITE, font=("Courier",9,"bold"),
relief="flat", pady=5, cursor="hand2").pack(side="left", expand=True, fill="x", padx=(2,0))
# Preview
pf = tk.Frame(lf, bg=CARD); pf.pack(fill="x", padx=8, pady=6)
pf.grid_columnconfigure(0, weight=1); pf.grid_columnconfigure(1, weight=1)
for col, label, color, attr in [(0,"Foto 1",BLUE,"c1"),(1,"Foto 2",GREEN,"c2")]:
f = tk.Frame(pf, bg=CARD); f.grid(row=0, column=col, padx=2)
tk.Label(f, text=label, font=("Courier",8,"bold"), bg=CARD, fg=color).pack()
c = tk.Canvas(f, width=100, height=100, bg="#050810",
highlightthickness=1, highlightbackground=BORDER)
c.pack(); setattr(self, attr, c)
# Score boxes
tk.Label(lf, text="METRICS", font=("Courier",9,"bold"),
bg=CARD, fg=SUB).pack(pady=(8,2))
sf = tk.Frame(lf, bg=CARD); sf.pack(fill="x", padx=6)
self.m_sign = self._mbox(sf, "IMG SIGN", GREEN)
self.m_amp = self._mbox(sf, "AMP IMG", ORANGE)
self.m_chain = self._mbox(sf, "CHAIN", TEAL)
self.m_cos = self._mbox(sf, "COSINE", PURPLE)
# Verdict
self.verdict = tk.Label(lf, text="β€”",
font=("Courier",20,"bold"), bg=CARD, fg=SUB,
pady=8, highlightthickness=2, highlightbackground=BORDER)
self.verdict.pack(fill="x", padx=8, pady=6)
# Chain detail
self.chain_lbl = tk.Label(lf, text="", font=("Courier",8),
bg=CARD, fg=SUB, justify="left", wraplength=200)
self.chain_lbl.pack(padx=10)
# Mode
tk.Label(lf, text="MODE", font=("Courier",8,"bold"),
bg=CARD, fg=SUB).pack(pady=(10,2))
mf = tk.Frame(lf, bg=CARD); mf.pack()
for val, label, col in [("metric","METRIC",GREEN),("training","TRAINING",ORANGE)]:
tk.Radiobutton(mf, text=label, variable=self.mode, value=val,
bg=CARD, fg=col, selectcolor=CARD,
font=("Courier",8,"bold"),
command=self._update_center).pack(side="left", padx=6)
# Nav buttons
tk.Label(lf, text="WINDOW NAV", font=("Courier",8,"bold"),
bg=CARD, fg=SUB).pack(pady=(10,2))
nf = tk.Frame(lf, bg=CARD); nf.pack(fill="x", padx=6)
for text, cmd, col in [
("β—€β—€",self._win_first,SUB), ("β—€",self._win_prev,BLUE),
("β–Ά",self._win_next,BLUE), ("AUTO",self._win_auto,PURPLE),
("β– ",self._win_stop,RED)
]:
tk.Button(nf, text=text, command=cmd, bg=CARD, fg=col,
font=("Courier",9,"bold"), relief="flat",
padx=4, pady=4, cursor="hand2").pack(side="left", expand=True)
# Win info
self.win_info = tk.Label(lf, text="Window: β€”", font=("Courier",8),
bg=CARD, fg=SUB, wraplength=200)
self.win_info.pack(padx=8, pady=4)
def _mbox(self, parent, label, color):
f = tk.Frame(parent, bg="#0a0e1a", highlightthickness=1, highlightbackground=BORDER)
f.pack(side="left", expand=True, fill="both", padx=2, pady=2)
tk.Label(f, text=label, font=("Courier",6,"bold"), bg="#0a0e1a", fg=color).pack(pady=(3,0))
lbl = tk.Label(f, text="β€”", font=("Courier",12,"bold"), bg="#0a0e1a", fg=color)
lbl.pack(pady=(0,3)); return lbl
# ── CENTER ───────────────────────────────────────────
def _build_center(self, parent):
cf = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=BORDER)
cf.grid(row=0, column=1, sticky="nsew", padx=6)
tk.Label(cf, text="SLIDING WINDOW EMBEDDING ANALYSIS",
font=("Courier",10,"bold"), bg=CARD, fg=PURPLE).pack(pady=(8,2))
# Embedding overlay bar
tk.Label(cf, text="Embedding: Biru=E1 Hijau=E2 β€” orange box = window aktif",
font=("Courier",8), bg=CARD, fg=SUB).pack()
self.c_emb = tk.Canvas(cf, bg="#050810", height=180,
highlightthickness=1, highlightbackground=BORDER)
self.c_emb.pack(fill="x", padx=8, pady=2)
# Window detail
self.win_title = tk.Label(cf, text="Window detail",
font=("Courier",8), bg=CARD, fg=SUB)
self.win_title.pack()
self.c_win = tk.Canvas(cf, bg="#050810", height=150,
highlightthickness=1, highlightbackground=BORDER)
self.c_win.pack(fill="x", padx=8, pady=2)
# tanh curve
tk.Label(cf, text="tanh(Ξ²Β·E1Β·E2) Agreement Curve (Ξ²=10) β€” titik = dimensi di window aktif",
font=("Courier",8), bg=CARD, fg=ORANGE).pack()
self.c_tanh = tk.Canvas(cf, bg="#050810", height=130,
highlightthickness=1, highlightbackground=BORDER)
self.c_tanh.pack(fill="x", padx=8, pady=2)
# Sign pattern all windows
tk.Label(cf, text="Sign match ratio per window (hijau β‰₯ threshold, merah = tidak)",
font=("Courier",8), bg=CARD, fg=GREEN).pack(pady=(4,0))
self.c_sign = tk.Canvas(cf, bg="#050810", height=70,
highlightthickness=1, highlightbackground=BORDER)
self.c_sign.pack(fill="x", padx=8, pady=2)
# Chain pattern
tk.Label(cf, text="Chain pattern (rantai match kontinu)",
font=("Courier",8), bg=CARD, fg=TEAL).pack()
self.c_chain = tk.Canvas(cf, bg="#050810", height=55,
highlightthickness=1, highlightbackground=BORDER)
self.c_chain.pack(fill="x", padx=8, pady=(2,6))
# ── RIGHT ────────────────────────────────────────────
def _build_right(self, parent):
rf = tk.Frame(parent, bg=CARD, highlightthickness=1,
highlightbackground=BORDER, width=260)
rf.grid(row=0, column=2, sticky="nsew", padx=(6,0))
rf.grid_propagate(False)
tk.Label(rf, text="EMBEDDING BARS",
font=("Courier",10,"bold"), bg=CARD, fg=TEAL).pack(pady=(8,2))
tk.Label(rf, text="E1 β€” Foto 1 (1024D)", font=("Courier",8,"bold"),
bg=CARD, fg=BLUE).pack()
self.c_e1 = tk.Canvas(rf, bg="#050810", height=55,
highlightthickness=1, highlightbackground=BORDER)
self.c_e1.pack(fill="x", padx=8, pady=2)
tk.Label(rf, text="E2 β€” Foto 2 (1024D)", font=("Courier",8,"bold"),
bg=CARD, fg=GREEN).pack()
self.c_e2 = tk.Canvas(rf, bg="#050810", height=55,
highlightthickness=1, highlightbackground=BORDER)
self.c_e2.pack(fill="x", padx=8, pady=2)
tk.Label(rf, text="ARCHITECTURE", font=("Courier",9,"bold"),
bg=CARD, fg=SUB).pack(pady=(12,4))
steps = [
("SW1", "112β†’56", BLUE),
("Conv2", "56β†’56", GREEN), ("Conv3", "56β†’28", GREEN),
("Conv4", "28β†’28", GREEN), ("Conv5", "28β†’28", GREEN),
("Conv6", "28β†’14", GREEN), ("Conv7", "14β†’14", GREEN),
("Conv8", "14β†’14", GREEN), ("Conv9", "14β†’7", GREEN),
("Conv10","7β†’7", GREEN), ("GAP", "β†’256", TEAL),
("FC", "β†’1024", PURPLE),
]
gf = tk.Frame(rf, bg=CARD); gf.pack(padx=8, fill="x")
for i, (name, res, col) in enumerate(steps):
f = tk.Frame(gf, bg=CARD); f.grid(row=i//3, column=i%3, padx=2, pady=1, sticky="w")
tk.Label(f, text=name, font=("Courier",7,"bold"), bg=CARD, fg=col).pack()
tk.Label(f, text=res, font=("Courier",6), bg=CARD, fg=SUB).pack()
tk.Label(rf, text="STATS", font=("Courier",9,"bold"),
bg=CARD, fg=SUB).pack(pady=(12,4))
self.stats_lbl = tk.Label(rf, text="β€”", font=("Courier",8),
bg=CARD, fg=TEXT, justify="left", wraplength=240)
self.stats_lbl.pack(padx=10)
# ── UPLOAD ───────────────────────────────────────────
def upload1(self):
path = filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")])
if not path: return
self.arr1 = load_face(path)
self.e1 = get_emb(self.arr1)
self._show(self.arr1, self.c1, 100)
self._refresh()
def upload2(self):
path = filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")])
if not path: return
self.arr2 = load_face(path)
self.e2 = get_emb(self.arr2)
self._show(self.arr2, self.c2, 100)
self._refresh()
def _show(self, arr, canvas, size):
img = Image.fromarray(arr.astype(np.uint8)).resize((size,size), Image.NEAREST)
tk_img = ImageTk.PhotoImage(img)
canvas.delete("all")
canvas.create_image(0, 0, anchor="nw", image=tk_img)
canvas.image = tk_img
def _refresh(self):
if self.e1 is None or self.e2 is None: return
self._update_scores()
self._draw_emb_bars()
self._update_center()
self._draw_sign_pattern()
self._draw_chain_pattern()
self._update_stats()
# ── SCORES ───────────────────────────────────────────
def _update_scores(self):
e1, e2 = self.e1, self.e2
sg = img_sign_score(e1, e2)
ap = amp_score(e1, e2)
cs, nc, ac = chain_score(e1, e2)
co = cosine(e1, e2)
self.m_sign.config(text=f"{sg:.3f}")
self.m_amp.config(text=f"{ap:.3f}")
self.m_chain.config(text=f"{cs:.3f}")
self.m_cos.config(text=f"{co:.3f}")
self.chain_lbl.config(text=f"Chains: {nc} AvgLen: {ac:.1f}\n(neutral={NEUTRAL_LEN})")
thr = 0.79; npass = sum([sg>=thr, ap>=thr, cs>=thr])
if npass >= 2:
self.verdict.config(text="βœ… MATCH", fg=WHITE, bg="#064e3b", highlightbackground=GREEN)
elif npass == 1:
self.verdict.config(text="⚠️ UNCERTAIN", fg=WHITE, bg="#78350f", highlightbackground=ORANGE)
else:
self.verdict.config(text="❌ DIFFERENT", fg=WHITE, bg="#450a0a", highlightbackground=RED)
def _update_stats(self):
e1, e2 = self.e1, self.e2
n = len(e1) - WINDOW_SIZE + 1
n_match = sum(1 for i in range(n)
if sum(1 for j in range(WINDOW_SIZE)
if (e1[i+j]>=0)==(e2[i+j]>=0)) >= THRESHOLD)
self.stats_lbl.config(
text=f"EMB dim : {len(e1)}\n"
f"N windows : {n}\n"
f"Pass thr : {n_match}/{n}\n"
f"E1 norm : {np.linalg.norm(e1):.3f}\n"
f"E2 norm : {np.linalg.norm(e2):.3f}\n"
f"E1 pos dim : {(e1>=0).sum()}/{len(e1)}\n"
f"E2 pos dim : {(e2>=0).sum()}/{len(e2)}\n"
f"Sign agree : {((e1>=0)==(e2>=0)).sum()}/{len(e1)}")
# ── EMBEDDING BARS ────────────────────────────────────
def _draw_emb_bars(self):
c = self.c_emb; c.delete("all")
W = c.winfo_width() or 900; H = 180
n = len(self.e1); bw = W/n; mid = H//2
c.create_line(0, mid, W, mid, fill=BORDER, width=1, dash=(3,2))
for i in range(n):
x0 = i*bw; x1 = x0+bw-0.3
v1 = float(self.e1[i]); h1 = abs(v1)*(mid-4)
in_win = self.win_pos <= i < self.win_pos + WINDOW_SIZE
col1 = "#a5b4fc" if in_win else BLUE
if v1>=0: c.create_rectangle(x0, mid-h1, x1, mid, fill=col1, outline="")
else: c.create_rectangle(x0, mid, x1, mid+h1, fill=col1, outline="")
v2 = float(self.e2[i]); h2 = abs(v2)*(mid-4)*0.7
col2 = "#6ee7b7" if in_win else GREEN
if v2>=0: c.create_rectangle(x0, mid-h2, x1, mid, fill=col2, outline="", stipple="gray25")
else: c.create_rectangle(x0, mid, x1, mid+h2, fill=col2, outline="", stipple="gray25")
# Window highlight box
wx0 = self.win_pos * bw; wx1 = (self.win_pos + WINDOW_SIZE) * bw
c.create_rectangle(wx0, 2, wx1, H-2, outline=ORANGE, width=2)
# Emb bars kanan
for cv, emb, col in [(self.c_e1, self.e1, BLUE), (self.c_e2, self.e2, GREEN)]:
cv.delete("all")
W2 = cv.winfo_width() or 240; H2 = 55
n2 = len(emb); bw2 = W2/n2; mid2 = H2//2
for i, v in enumerate(emb):
x0 = i*bw2; h = abs(float(v))*(mid2-2)
fc = col if float(v)>=0 else RED
if float(v)>=0: cv.create_rectangle(x0, mid2-h, x0+bw2-0.3, mid2, fill=fc, outline="")
else: cv.create_rectangle(x0, mid2, x0+bw2-0.3, mid2+h, fill=fc, outline="")
# ── CENTER DRAWS ──────────────────────────────────────
def _update_center(self):
if self.e1 is None: return
self._draw_emb_bars()
self._draw_window_detail()
self._draw_tanh_curve()
# Update win info
e1, e2 = self.e1, self.e2
n = len(e1) - WINDOW_SIZE + 1
n_match = sum(1 for j in range(WINDOW_SIZE)
if (e1[self.win_pos+j]>=0)==(e2[self.win_pos+j]>=0))
self.win_info.config(
text=f"Win {self.win_pos}/{n-1} match {n_match}/{WINDOW_SIZE} "
f"{'βœ“ PASS' if n_match>=THRESHOLD else 'βœ— FAIL'}",
fg=GREEN if n_match>=THRESHOLD else RED)
self.win_title.config(
text=f"Window [{self.win_pos}:{self.win_pos+WINDOW_SIZE}] β€” "
f"{'tanh agreement (training)' if self.mode.get()=='training' else 'sign matching (metric)'}")
def _draw_window_detail(self):
c = self.c_win; c.delete("all")
W = c.winfo_width() or 900; H = 150
pos = self.win_pos
w1 = self.e1[pos:pos+WINDOW_SIZE]
w2 = self.e2[pos:pos+WINDOW_SIZE]
bw = W / WINDOW_SIZE; mid = H//2 - 10
mode = self.mode.get()
for i in range(WINDOW_SIZE):
x0 = i*bw+2; x1 = x0+bw-4; xc = (x0+x1)/2
v1, v2 = float(w1[i]), float(w2[i])
same = (v1>=0) == (v2>=0)
if mode == "training":
agree = float(tanh_agreement(v1, v2))
col = self._lerp(RED, GREEN, agree)
h = agree*(mid-5)
c.create_rectangle(x0, mid-h, x1, mid, fill=col, outline="")
c.create_text(xc, H-22, anchor="center",
text=f"{agree:.2f}", font=("Courier",6), fill=col)
arrow = "β–²" if agree>0.5 else "β–Ό"
c.create_text(xc, mid-h-10, anchor="center",
text=arrow, font=("Courier",9),
fill=GREEN if agree>0.5 else RED)
# Gradient label
if agree > 0.5:
c.create_text(xc, H-8, anchor="center",
text="β†’1", font=("Courier",5), fill=GREEN)
else:
c.create_text(xc, H-8, anchor="center",
text="β†’0", font=("Courier",5), fill=RED)
else:
s1 = "+" if v1>=0 else "βˆ’"
s2 = "+" if v2>=0 else "βˆ’"
col = GREEN if same else RED
c.create_rectangle(x0, 20, x1, mid, fill=col, outline="")
c.create_text(xc, 32, anchor="center",
text=s1, font=("Courier",12,"bold"), fill=WHITE)
c.create_text(xc, 54, anchor="center",
text=s2, font=("Courier",12,"bold"), fill=WHITE)
c.create_text(xc, mid+8, anchor="center",
text="βœ“" if same else "βœ—", font=("Courier",9), fill=col)
# Match bar
n_match = sum(1 for j in range(WINDOW_SIZE)
if (w1[j]>=0)==(w2[j]>=0))
mw = (n_match/WINDOW_SIZE)*(W-16)
c.create_rectangle(8, H-6, 8+mw, H-1,
fill=GREEN if n_match>=THRESHOLD else RED, outline="")
c.create_text(W//2, H-4, anchor="center",
text=f"Match {n_match}/{WINDOW_SIZE} thr={THRESHOLD} {'PASS βœ“' if n_match>=THRESHOLD else 'FAIL βœ—'}",
font=("Courier",7), fill=GREEN if n_match>=THRESHOLD else RED)
def _draw_tanh_curve(self):
c = self.c_tanh; c.delete("all")
W = c.winfo_width() or 900; H = 130
mid_y = H//2
c.create_line(0, mid_y, W, mid_y, fill=BORDER, width=1, dash=(3,2))
c.create_line(W//2, 0, W//2, H, fill=BORDER, width=1, dash=(3,2))
# Curve
xs = np.linspace(-3, 3, W)
ys = (np.tanh(xs) + 1) / 2
pts = [(i, int(mid_y - ys[i]*(mid_y-8))) for i in range(W)]
for i in range(len(pts)-1):
c.create_line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1],
fill=ORANGE, width=2)
# Dots per dimensi di window
pos = self.win_pos
w1 = self.e1[pos:pos+WINDOW_SIZE]
w2 = self.e2[pos:pos+WINDOW_SIZE]
for j in range(WINDOW_SIZE):
prod = float(w1[j]) * float(w2[j]) * BETA
agree = (math.tanh(prod) + 1) / 2
px = int((prod + 3) / 6 * W); px = max(0, min(W-1, px))
py = int(mid_y - agree*(mid_y-8))
same = (w1[j]>=0) == (w2[j]>=0)
c.create_oval(px-4, py-4, px+4, py+4,
fill=GREEN if same else RED, outline=WHITE)
mode = self.mode.get()
c.create_text(4, 4, anchor="nw",
text=f"{'Training: gradient dorong ke 1.0 (same) atau 0.0 (diff)' if mode=='training' else 'Metric: posisi dot = agreement di window ini'}",
font=("Courier",7), fill=YELLOW)
c.create_text(4, H-4, anchor="sw",
text="prod<0 (beda tanda)", font=("Courier",6), fill=RED)
c.create_text(W-4, H-4, anchor="se",
text="prod>0 (sama tanda)", font=("Courier",6), fill=GREEN)
def _draw_sign_pattern(self):
c = self.c_sign; c.delete("all")
W = c.winfo_width() or 900; H = 70
e1, e2 = self.e1, self.e2
n = len(e1) - WINDOW_SIZE + 1; bw = W/n
for i in range(n):
mc = sum(1 for j in range(WINDOW_SIZE)
if (e1[i+j]>=0)==(e2[i+j]>=0))
h = (mc/WINDOW_SIZE)*(H-4)
col = GREEN if mc>=THRESHOLD else RED
c.create_rectangle(i*bw, H-h, i*bw+bw-0.3, H, fill=col, outline="")
# Threshold line
ty = H - (THRESHOLD/WINDOW_SIZE)*(H-4)
c.create_line(0, ty, W, ty, fill=YELLOW, width=1, dash=(4,2))
c.create_text(4, 4, anchor="nw",
text=f"Sign match ratio per window (garis kuning = thr {THRESHOLD}/{WINDOW_SIZE})",
font=("Courier",7), fill=SUB)
def _draw_chain_pattern(self):
c = self.c_chain; c.delete("all")
W = c.winfo_width() or 900; H = 55
e1, e2 = self.e1, self.e2
n = len(e1) - WINDOW_SIZE + 1; bw = W/n
in_chain = False; chain_num = 0
for i in range(n):
mc = sum(1 for j in range(WINDOW_SIZE)
if (e1[i+j]>=0)==(e2[i+j]>=0))
match = mc >= THRESHOLD; x0 = i*bw
if match:
c.create_rectangle(x0, 8, x0+bw-0.3, H-8, fill=TEAL, outline="")
if not in_chain:
in_chain = True; chain_num += 1
c.create_line(x0, 4, x0, H-4, fill=WHITE, width=1)
c.create_text(x0+1, 5, anchor="nw",
text=str(chain_num), font=("Courier",5), fill=WHITE)
else:
in_chain = False
c.create_text(4, 4, anchor="nw",
text=f"Chain pattern β€” teal=match, putih=awal chain baru",
font=("Courier",7), fill=SUB)
# ── WINDOW NAV ────────────────────────────────────────
def _win_first(self):
self.win_pos=0; self._update_center()
def _win_prev(self):
self.win_pos=max(0,self.win_pos-1); self._update_center()
def _win_next(self):
if self.e1 is None: return
n=len(self.e1)-WINDOW_SIZE+1
self.win_pos=min(self.win_pos+1,n-1); self._update_center()
def _win_stop(self):
self.animating=False
def _win_auto(self):
self.animating=True; self._auto_loop()
def _auto_loop(self):
if not self.animating or self.e1 is None: return
n=len(self.e1)-WINDOW_SIZE+1
self.win_pos=(self.win_pos+1)%n
self._update_center()
self.after(100, self._auto_loop)
# ── HELPERS ──────────────────────────────────────────
def _lerp(self, c1, c2, t):
r1,g1,b1=int(c1[1:3],16),int(c1[3:5],16),int(c1[5:7],16)
r2,g2,b2=int(c2[1:3],16),int(c2[3:5],16),int(c2[5:7],16)
r=int(r1+(r2-r1)*t); g=int(g1+(g2-g1)*t); b=int(b1+(b2-b1)*t)
return f"#{r:02x}{g:02x}{b:02x}"
# ── MAIN ───────────────────────────────────────────────────
if __name__ == "__main__":
app = App()
app.after(200, app._refresh)
app.mainloop()