imghost11 commited on
Commit
06f4503
Β·
verified Β·
1 Parent(s): 33ae16c

Upload 4 files

Browse files
Files changed (4) hide show
  1. convert_onnx.py +191 -0
  2. imgnet_conv10_epoch39.onnx +3 -0
  3. verif_onnx.py +90 -0
  4. visualize_onnx.py +620 -0
convert_onnx.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # export_imgnet_onnx.py
2
+ # Export IMGNet Conv10 ke ONNX
3
+ # SW Block di-rewrite agar ONNX-compatible (slice instead of unfold+mask)
4
+ # Bobot tetap dari checkpoint asli β€” hasil embedding identik
5
+
6
+ import os
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.nn.functional as F
11
+
12
+ # ── CONFIG ─────────────────────────────────────────────────
13
+ CKPT_PATH = r"C:\PythonProj\img_bnn\checkpoints_sw357_conv10_imgsign\SW357_conv10_imgsign\best_model_epoch39_plateau.pth"
14
+ ONNX_PATH = r"C:\PythonProj\img_bnn\imgnet_conv10_epoch39.onnx"
15
+ OPSET = 18
16
+
17
+
18
+ # ── SW BLOCK (ONNX-compatible version) ─────────────────────
19
+ class SWBlock(nn.Module):
20
+ """
21
+ SW Block β€” ONNX-compatible
22
+ Reflect padding di-emulate dengan flip + concat
23
+ Mathematically identik dengan training, 100% ONNX-safe
24
+ """
25
+ def __init__(self):
26
+ super().__init__()
27
+ self._off = {}
28
+ for ws in [3, 5, 7]:
29
+ mid = ws // 2
30
+ self._off[ws] = [(dr, dc) for dr in range(ws) for dc in range(ws)
31
+ if not (dr == mid and dc == mid)]
32
+ self.fc = nn.Sequential(
33
+ nn.Linear(240, 64),
34
+ nn.ReLU(inplace=True),
35
+ nn.Linear(64, 32),
36
+ )
37
+
38
+ def _reflect_pad(self, x, p):
39
+ left = x[:, :, :, 1:p+1].flip(dims=[3])
40
+ right = x[:, :, :, -(p+1):-1].flip(dims=[3])
41
+ x = torch.cat([left, x, right], dim=3)
42
+ top = x[:, :, 1:p+1, :].flip(dims=[2])
43
+ bottom = x[:, :, -(p+1):-1, :].flip(dims=[2])
44
+ return torch.cat([top, x, bottom], dim=2)
45
+
46
+ def forward(self, x):
47
+ B, C, H, W = x.shape
48
+ # Kumpulkan diff per window size, lalu per offset β€” sama persis dengan unfold+mask
49
+ # unfold menghasilkan: (B,C,H,W,ws,ws) β†’ flatten ws*ws β†’ remove center
50
+ # Ekuivalen: untuk tiap (dr,dc) dalam row-major order (skip center),
51
+ # diff[:,:,:,:,idx] = center - neighbor[dr,dc]
52
+ all_diffs = []
53
+ for ws in [3, 5, 7]:
54
+ p = ws // 2
55
+ xp = self._reflect_pad(x, p)
56
+ mid = ws // 2
57
+ # Kumpulkan per channel dulu, baru per offset β€” matching unfold layout
58
+ # unfold layout: diff shape = (B, C, H, W, ws*ws-1)
59
+ # FC input = (B*H*W, C*(ws*ws-1)*3windows) = (B*H*W, 240)
60
+ ws_diffs = []
61
+ for dr in range(ws):
62
+ for dc in range(ws):
63
+ if dr == mid and dc == mid: continue
64
+ neighbor = xp[:, :, dr:dr+H, dc:dc+W]
65
+ ws_diffs.append(x - neighbor) # (B, C, H, W)
66
+ # Stack: (B, C*(wsΒ²-1), H, W)
67
+ all_diffs.extend(ws_diffs)
68
+
69
+ # Susun sama dengan unfold: per window size, semua channel dulu baru offset
70
+ # unfold: (B, C, H, W, N) β†’ permute β†’ (B*H*W, C*N)
71
+ # kita: list of (B,C,H,W) dengan panjang 80 β†’ cat dim=1 β†’ (B, C*80, H, W)
72
+ # Tapi FC expect (B*H*W, C*80) dengan susunan C bersebelahan untuk tiap offset
73
+ # Perlu: untuk tiap posisi spatial, urutan input ke FC = [all_diffs_dim0, all_diffs_dim1, ...]
74
+ # Ini sama dengan cat lalu permute
75
+
76
+ # Reorder: unfold hasilkan (B, C, H, W, N_diff) β†’ permute(0,2,3,1,4) β†’ (B,H,W,C,N)
77
+ # β†’ reshape (B*H*W, C*N)
78
+ # Kita punya list N_diff tensor (B,C,H,W) β†’ stack dim=4 β†’ (B,C,H,W,N)
79
+ d = torch.stack(all_diffs, dim=4) # (B, C, H, W, 80)
80
+ B2, C2, H2, W2, N = d.shape
81
+ d = d.permute(0, 2, 3, 1, 4).reshape(B2 * H2 * W2, C2 * N) # (B*H*W, C*80=240)
82
+ o = self.fc(d)
83
+ return o.reshape(B2, H2, W2, -1).permute(0, 3, 1, 2)
84
+
85
+
86
+ # ── IMGNET MODEL ───────────────────────────────────────────
87
+ class IMGNet(nn.Module):
88
+ def __init__(self):
89
+ super().__init__()
90
+ self.sw1 = SWBlock(); self.bn1 = nn.BatchNorm2d(32)
91
+ self.conv2 = nn.Conv2d(32, 64, 3, stride=1, padding=1, bias=False); self.bn2 = nn.BatchNorm2d(64)
92
+ self.conv3 = nn.Conv2d(64, 64, 3, stride=2, padding=1, bias=False); self.bn3 = nn.BatchNorm2d(64)
93
+ self.conv4 = nn.Conv2d(64, 128, 3, stride=1, padding=1, bias=False); self.bn4 = nn.BatchNorm2d(128)
94
+ self.conv5 = nn.Conv2d(128, 128, 3, stride=1, padding=1, bias=False); self.bn5 = nn.BatchNorm2d(128)
95
+ self.conv6 = nn.Conv2d(128, 128, 3, stride=2, padding=1, bias=False); self.bn6 = nn.BatchNorm2d(128)
96
+ self.conv7 = nn.Conv2d(128, 256, 3, stride=1, padding=1, bias=False); self.bn7 = nn.BatchNorm2d(256)
97
+ self.conv8 = nn.Conv2d(256, 256, 3, stride=1, padding=1, bias=False); self.bn8 = nn.BatchNorm2d(256)
98
+ self.conv9 = nn.Conv2d(256, 256, 3, stride=2, padding=1, bias=False); self.bn9 = nn.BatchNorm2d(256)
99
+ self.conv10 = nn.Conv2d(256, 256, 3, stride=1, padding=1, bias=False); self.bn10 = nn.BatchNorm2d(256)
100
+ self.gap = nn.AdaptiveAvgPool2d(1)
101
+ self.fc = nn.Linear(256, 1024)
102
+ self.bn = nn.BatchNorm1d(1024)
103
+
104
+ def forward(self, x):
105
+ x = F.relu(self.bn1(self.sw1(x)))
106
+ for i in range(2, 11):
107
+ x = F.relu(getattr(self, f'bn{i}')(getattr(self, f'conv{i}')(x)))
108
+ x = self.gap(x).view(x.size(0), -1)
109
+ return self.bn(self.fc(x))
110
+
111
+
112
+ # ── EXPORT ─────────────────────────────────────────────────
113
+ def export():
114
+ device = torch.device("cpu") # export dari CPU untuk portabilitas
115
+ print(f"Device : {device}")
116
+
117
+ # Load model
118
+ model = IMGNet().to(device)
119
+ st = torch.load(CKPT_PATH, map_location="cpu", weights_only=False)
120
+ if isinstance(st, dict) and "model" in st: st = st["model"]
121
+ model.load_state_dict(st, strict=True)
122
+ model.eval()
123
+ n_params = sum(p.numel() for p in model.parameters())
124
+ print(f"βœ“ Model loaded β€” {n_params:,} params (~{n_params*4/1024/1024:.2f} MB FP32)")
125
+
126
+ # Test forward
127
+ dummy = torch.randn(1, 3, 112, 112)
128
+ with torch.no_grad():
129
+ out = model(dummy)
130
+ print(f"βœ“ Forward pass OK β€” output: {out.shape}")
131
+
132
+ # Export ONNX
133
+ print(f"\nExporting ONNX (opset {OPSET}) ...")
134
+ torch.onnx.export(
135
+ model,
136
+ dummy,
137
+ ONNX_PATH,
138
+ opset_version = OPSET,
139
+ input_names = ["input"],
140
+ output_names = ["embedding"],
141
+ dynamic_axes = {
142
+ "input" : {0: "batch_size"},
143
+ "embedding": {0: "batch_size"},
144
+ },
145
+ do_constant_folding = True,
146
+ verbose = False,
147
+ export_params = True,
148
+ )
149
+ print(f"βœ“ ONNX saved: {ONNX_PATH}")
150
+
151
+ # Verify
152
+ try:
153
+ import onnx, onnxruntime as rt
154
+
155
+ onnx.checker.check_model(onnx.load(ONNX_PATH))
156
+ print(f"βœ“ ONNX model valid (checker passed)")
157
+
158
+ sess = rt.InferenceSession(ONNX_PATH, providers=["CPUExecutionProvider"])
159
+ out_onnx = sess.run(["embedding"], {"input": dummy.numpy()})[0]
160
+ max_diff = float(np.abs(out_onnx - out.numpy()).max())
161
+ cos_sim = float(np.dot(out_onnx[0], out.numpy()[0]) /
162
+ (np.linalg.norm(out_onnx[0]) * np.linalg.norm(out.numpy()[0]) + 1e-8))
163
+ print(f"βœ“ PyTorch vs ONNX max diff : {max_diff:.6f} {'βœ“ OK' if max_diff < 1e-3 else '⚠ WARNING'}")
164
+ print(f"βœ“ PyTorch vs ONNX cosine sim: {cos_sim:.6f} {'βœ“ OK' if cos_sim > 0.999 else '⚠ EMBEDDING BERBEDA!'}")
165
+
166
+ size_mb = os.path.getsize(ONNX_PATH) / 1024 / 1024
167
+ print(f"βœ“ File size: {size_mb:.2f} MB")
168
+
169
+ # Test batch size > 1
170
+ dummy2 = torch.randn(4, 3, 112, 112)
171
+ out_b4 = sess.run(["embedding"], {"input": dummy2.numpy()})[0]
172
+ print(f"βœ“ Batch test (4Γ—): output shape {out_b4.shape}")
173
+
174
+ print(f"\n{'='*55}")
175
+ print(f"EXPORT SUKSES")
176
+ print(f" Input : (batch, 3, 112, 112) FLOAT32")
177
+ print(f" Output : (batch, 1024) FLOAT32 β€” embedding")
178
+ print(f" Opset : {OPSET}")
179
+ print(f" Size : {size_mb:.2f} MB")
180
+ print(f" Dynamic : batch size (bisa 1, 4, 8, ...)")
181
+ print(f" Path : {ONNX_PATH}")
182
+ print(f"{'='*55}")
183
+
184
+ except ImportError as e:
185
+ print(f"Verification skip β€” install: pip install onnx onnxruntime ({e})")
186
+ except Exception as e:
187
+ print(f"Verification error: {e}")
188
+
189
+
190
+ if __name__ == "__main__":
191
+ export()
imgnet_conv10_epoch39.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ecda2370a2dc0b19500afb7930b5d1c969db3804cc5a77ce66adf881416ca611
3
+ size 11208187
verif_onnx.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # verify_onnx.py
2
+ # Bandingkan output PyTorch vs ONNX dengan foto wajah sungguhan
3
+
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from PIL import Image
9
+ import sys
10
+
11
+ CKPT_PATH = r"C:\PythonProj\img_bnn\checkpoints_sw357_conv10_imgsign\SW357_conv10_imgsign\best_model_epoch39_plateau.pth"
12
+ ONNX_PATH = r"C:\PythonProj\img_bnn\imgnet_conv10_epoch39.onnx"
13
+ IMG_PATH = sys.argv[1] if len(sys.argv) > 1 else None
14
+
15
+ # ── PyTorch model ──────────────────────────────────────────
16
+ class SWBlock(nn.Module):
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.fc = nn.Sequential(nn.Linear(240,64),nn.ReLU(True),nn.Linear(64,32))
20
+ def forward(self, x):
21
+ B,C,H,W=x.shape; diffs=[]
22
+ for ws in [3,5,7]:
23
+ p=ws//2; xp=F.pad(x,[p,p,p,p],mode='reflect')
24
+ patches=xp.unfold(2,ws,1).unfold(3,ws,1)
25
+ diff=x.unsqueeze(-1).unsqueeze(-1)-patches
26
+ mid=ws//2
27
+ mask=torch.ones(ws,ws,dtype=torch.bool); mask[mid,mid]=False
28
+ diffs.append(diff[:,:,:,:,mask])
29
+ d=torch.cat(diffs,-1); B,C,H,W,N=d.shape
30
+ o=self.fc(d.permute(0,2,3,1,4).reshape(B*H*W,C*N))
31
+ return o.reshape(B,H,W,-1).permute(0,3,1,2)
32
+
33
+ class IMGNet(nn.Module):
34
+ def __init__(self):
35
+ super().__init__()
36
+ self.sw1=SWBlock(); self.bn1=nn.BatchNorm2d(32)
37
+ self.conv2 =nn.Conv2d(32, 64,3,stride=1,padding=1,bias=False); self.bn2 =nn.BatchNorm2d(64)
38
+ self.conv3 =nn.Conv2d(64, 64,3,stride=2,padding=1,bias=False); self.bn3 =nn.BatchNorm2d(64)
39
+ self.conv4 =nn.Conv2d(64,128,3,stride=1,padding=1,bias=False); self.bn4 =nn.BatchNorm2d(128)
40
+ self.conv5 =nn.Conv2d(128,128,3,stride=1,padding=1,bias=False); self.bn5 =nn.BatchNorm2d(128)
41
+ self.conv6 =nn.Conv2d(128,128,3,stride=2,padding=1,bias=False); self.bn6 =nn.BatchNorm2d(128)
42
+ self.conv7 =nn.Conv2d(128,256,3,stride=1,padding=1,bias=False); self.bn7 =nn.BatchNorm2d(256)
43
+ self.conv8 =nn.Conv2d(256,256,3,stride=1,padding=1,bias=False); self.bn8 =nn.BatchNorm2d(256)
44
+ self.conv9 =nn.Conv2d(256,256,3,stride=2,padding=1,bias=False); self.bn9 =nn.BatchNorm2d(256)
45
+ self.conv10=nn.Conv2d(256,256,3,stride=1,padding=1,bias=False); self.bn10=nn.BatchNorm2d(256)
46
+ self.gap=nn.AdaptiveAvgPool2d(1); self.fc=nn.Linear(256,1024); self.bn=nn.BatchNorm1d(1024)
47
+ def forward(self,x):
48
+ x=F.relu(self.bn1(self.sw1(x)))
49
+ for i in range(2,11):
50
+ x=F.relu(getattr(self,f'bn{i}')(getattr(self,f'conv{i}')(x)))
51
+ return self.bn(self.fc(self.gap(x).view(x.size(0),-1)))
52
+
53
+ # Load
54
+ model = IMGNet()
55
+ st = torch.load(CKPT_PATH, map_location="cpu", weights_only=False)
56
+ if isinstance(st, dict) and "model" in st: st = st["model"]
57
+ model.load_state_dict(st); model.eval()
58
+
59
+ # Load ONNX
60
+ import onnxruntime as rt
61
+ sess = rt.InferenceSession(ONNX_PATH, providers=["CPUExecutionProvider"])
62
+ inp_name = sess.get_inputs()[0].name
63
+
64
+ # Test dengan foto wajah atau random
65
+ if IMG_PATH:
66
+ arr = np.array(Image.open(IMG_PATH).convert("RGB").resize((112,112), Image.BILINEAR))
67
+ print(f"Testing dengan foto: {IMG_PATH}")
68
+ else:
69
+ # Pakai foto sintetis yang mirip wajah (bukan pure random)
70
+ arr = np.random.randint(50, 200, (112,112,3), dtype=np.uint8)
71
+ print("Testing dengan gambar random...")
72
+
73
+ # Preprocess
74
+ t = torch.from_numpy(arr.astype(np.float32)/255.0).permute(2,0,1).unsqueeze(0)
75
+ t_np = t.numpy()
76
+
77
+ # Run keduanya
78
+ with torch.no_grad():
79
+ e_torch = model(t).squeeze(0).numpy()
80
+ e_onnx = sess.run(None, {inp_name: t_np})[0][0]
81
+
82
+ # Compare
83
+ max_diff = np.abs(e_torch - e_onnx).max()
84
+ cos_sim = np.dot(e_torch, e_onnx) / (np.linalg.norm(e_torch) * np.linalg.norm(e_onnx) + 1e-8)
85
+
86
+ print(f"\nPyTorch output[:5] : {e_torch[:5]}")
87
+ print(f"ONNX output[:5] : {e_onnx[:5]}")
88
+ print(f"\nMax diff : {max_diff:.8f}")
89
+ print(f"Cosine sim: {cos_sim:.8f}")
90
+ print(f"\nStatus: {'βœ“ IDENTIK' if cos_sim > 0.9999 else 'βœ— BERBEDA β€” masalah di ONNX!'}")
visualize_onnx.py ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # imgnet_visualizer_onnx.py
2
+ # IMGNet Interactive Visualizer β€” pakai ONNX Runtime (tidak butuh PyTorch)
3
+ # Panel Kiri : Upload 2 foto + MTCNN crop
4
+ # Panel Tengah: Sliding window embedding analysis (Training vs Metric mode)
5
+ # Panel Kanan : Embedding bars + Sign + Chain pattern
6
+
7
+ import tkinter as tk
8
+ from tkinter import filedialog
9
+ from PIL import Image, ImageTk
10
+ import numpy as np
11
+ import math
12
+ from collections import Counter
13
+
14
+ # ── CONFIG ─────────────────────────────────────────────────
15
+ ONNX_PATH = r"C:\PythonProj\img_bnn\imgnet_conv10_epoch39.onnx"
16
+ WINDOW_SIZE = 11
17
+ THRESHOLD = 8
18
+ EMB_DIM = 1024
19
+ IMG_SIZE = 112
20
+ BETA = 10.0
21
+ NEUTRAL_LEN = 29
22
+ REWARD_RATE = 0.3
23
+ PUNISH_RATE = 1.0
24
+
25
+ # ── COLORS ─────────────────────────────────────────────────
26
+ BG= "#0a0e1a"; CARD= "#111827"; BORDER="#1e293b"
27
+ BLUE= "#6366f1"; GREEN= "#10b981"; ORANGE="#f59e0b"
28
+ PURPLE="#a855f7"; TEAL= "#14b8a6"; RED= "#ef4444"
29
+ YELLOW="#fbbf24"; WHITE= "#ffffff"; SUB= "#64748b"; TEXT="#e2e8f0"
30
+
31
+ # ── ONNX RUNTIME LOAD ──────────────────────────────────────
32
+ try:
33
+ import onnxruntime as rt
34
+ import os
35
+ if os.path.exists(ONNX_PATH):
36
+ providers = ["CUDAExecutionProvider","CPUExecutionProvider"]
37
+ _sess = rt.InferenceSession(ONNX_PATH, providers=providers)
38
+ _inp_name = _sess.get_inputs()[0].name
39
+ ONNX_OK = True
40
+ # Detect actual device
41
+ used = _sess.get_providers()[0]
42
+ DEVICE_STR = "CUDA" if "CUDA" in used else "CPU"
43
+ print(f"βœ“ ONNX loaded β€” provider: {used}")
44
+ else:
45
+ _sess = None; ONNX_OK = False; DEVICE_STR = "β€”"
46
+ print(f"βœ— ONNX file tidak ditemukan: {ONNX_PATH}")
47
+ except ImportError:
48
+ _sess = None; ONNX_OK = False; DEVICE_STR = "β€”"
49
+ print("βœ— onnxruntime tidak terinstall β€” pip install onnxruntime")
50
+
51
+ # ── MTCNN ──────────────────────────────────────────────────
52
+ try:
53
+ from facenet_pytorch import MTCNN
54
+ _mtcnn = MTCNN(image_size=112, keep_all=False, post_process=False, device="cpu")
55
+ MTCNN_OK = True
56
+ except: _mtcnn = None; MTCNN_OK = False
57
+
58
+
59
+ # ── INFERENCE ──────────────────────────────────────────────
60
+ def get_emb(arr):
61
+ """arr: np.uint8 (112,112,3) β†’ embedding (1024,)"""
62
+ if _sess is not None:
63
+ t = arr.astype(np.float32) / 255.0
64
+ t = t.transpose(2, 0, 1)[np.newaxis] # (1,3,112,112)
65
+ return _sess.run(None, {_inp_name: t})[0][0]
66
+ # Fallback dummy
67
+ np.random.seed(int(arr.sum()) % 2**31)
68
+ e = np.random.randn(EMB_DIM).astype(np.float32)
69
+ return e / (np.linalg.norm(e) + 1e-8)
70
+
71
+ def load_face(path):
72
+ img = Image.open(path).convert("RGB")
73
+ if MTCNN_OK and _mtcnn:
74
+ try:
75
+ face = _mtcnn(img)
76
+ if face is not None:
77
+ return np.clip(face.permute(1,2,0).numpy(), 0, 255).astype(np.uint8)
78
+ except: pass
79
+ return np.array(img.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR))
80
+
81
+
82
+ # ── METRICS ────────────────────────────────────────────────
83
+ def img_sign_score(e1, e2):
84
+ n = len(e1) - WINDOW_SIZE + 1
85
+ return sum(1 for i in range(n)
86
+ if sum(1 for j in range(WINDOW_SIZE)
87
+ if (e1[i+j]>=0)==(e2[i+j]>=0)) >= THRESHOLD) / max(n,1)
88
+
89
+ def amp_score(e1, e2):
90
+ n = len(e1) - WINDOW_SIZE + 1; tot = 0.0
91
+ for i in range(n):
92
+ w1, w2 = e1[i:i+WINDOW_SIZE], e2[i:i+WINDOW_SIZE]
93
+ s1 = np.where(w1>=0,1,-1).astype(np.int8)
94
+ s2 = np.where(w2>=0,1,-1).astype(np.int8)
95
+ if int(np.sum(s1==s2)) >= THRESHOLD:
96
+ a1, a2 = np.mean(np.abs(w1)), np.mean(np.abs(w2))
97
+ tot += max(0.0, 1 - abs(a1-a2)/max(a1,a2,1e-6))
98
+ return tot / max(n,1)
99
+
100
+ def chain_score(e1, e2):
101
+ n = len(e1) - WINDOW_SIZE + 1
102
+ flags = [int(np.sum(
103
+ np.where(e1[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8) ==
104
+ np.where(e2[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8)
105
+ )) >= THRESHOLD for i in range(n)]
106
+ total = sum(flags); sg = total / max(n,1)
107
+ nc = 0; ic = False
108
+ for f in flags:
109
+ if f and not ic: nc+=1; ic=True
110
+ elif not f: ic=False
111
+ if nc==0 or total==0: return 0.0, 0, 0.0
112
+ ac = total/nc; diff = ac - NEUTRAL_LEN
113
+ s = sg + (REWARD_RATE*diff if diff>=0 else PUNISH_RATE*diff)/100
114
+ return float(np.clip(s,0,1)), nc, ac
115
+
116
+ def cosine(e1, e2):
117
+ return float(np.dot(e1,e2)/(np.linalg.norm(e1)*np.linalg.norm(e2)+1e-8))
118
+
119
+ def tanh_agreement(e1, e2):
120
+ return (np.tanh(BETA * e1 * e2) + 1) / 2
121
+
122
+
123
+ # ============================================================
124
+ # APP
125
+ # ============================================================
126
+ class App(tk.Tk):
127
+ def __init__(self):
128
+ super().__init__()
129
+ self.title("IMGNet Visualizer β€” ONNX Runtime")
130
+ self.geometry("1500x880")
131
+ self.configure(bg=BG)
132
+ self.resizable(True, True)
133
+
134
+ self.arr1 = None; self.arr2 = None
135
+ self.e1 = None; self.e2 = None
136
+ self.win_pos = 0
137
+ self.animating = False
138
+ self.mode = tk.StringVar(value="metric")
139
+
140
+ self._build()
141
+
142
+ def _build(self):
143
+ # Top bar
144
+ top = tk.Frame(self, bg=BG); top.pack(fill="x", padx=12, pady=(8,4))
145
+ tk.Label(top, text="IMGNet Β· Interactive Visualizer Β· ONNX Runtime",
146
+ font=("Courier",13,"bold"), bg=BG, fg=TEXT).pack(side="left")
147
+ st = f"ONNX={'βœ“' if ONNX_OK else 'βœ—'} Device={DEVICE_STR} MTCNN={'βœ“' if MTCNN_OK else 'βœ—'} w={WINDOW_SIZE} t={THRESHOLD}/11"
148
+ tk.Label(top, text=st, font=("Courier",9), bg=BG, fg=SUB).pack(side="right")
149
+
150
+ main = tk.Frame(self, bg=BG); main.pack(fill="both", expand=True, padx=8, pady=4)
151
+ main.grid_columnconfigure(0, weight=0, minsize=240)
152
+ main.grid_columnconfigure(1, weight=1)
153
+ main.grid_columnconfigure(2, weight=0, minsize=260)
154
+ main.grid_rowconfigure(0, weight=1)
155
+
156
+ self._build_left(main)
157
+ self._build_center(main)
158
+ self._build_right(main)
159
+
160
+ # ── LEFT ─────────────────────────────────────────────
161
+ def _build_left(self, parent):
162
+ lf = tk.Frame(parent, bg=CARD, highlightthickness=1,
163
+ highlightbackground=BORDER, width=240)
164
+ lf.grid(row=0, column=0, sticky="nsew", padx=(0,6))
165
+ lf.grid_propagate(False)
166
+
167
+ tk.Label(lf, text="INPUT IMAGES", font=("Courier",10,"bold"),
168
+ bg=CARD, fg=BLUE).pack(pady=(10,4))
169
+
170
+ # Upload buttons
171
+ bf = tk.Frame(lf, bg=CARD); bf.pack(fill="x", padx=8)
172
+ tk.Button(bf, text="Upload Foto 1", command=self.upload1,
173
+ bg=BLUE, fg=WHITE, font=("Courier",9,"bold"),
174
+ relief="flat", pady=5, cursor="hand2").pack(side="left", expand=True, fill="x", padx=(0,2))
175
+ tk.Button(bf, text="Upload Foto 2", command=self.upload2,
176
+ bg=GREEN, fg=WHITE, font=("Courier",9,"bold"),
177
+ relief="flat", pady=5, cursor="hand2").pack(side="left", expand=True, fill="x", padx=(2,0))
178
+
179
+ # Preview
180
+ pf = tk.Frame(lf, bg=CARD); pf.pack(fill="x", padx=8, pady=6)
181
+ pf.grid_columnconfigure(0, weight=1); pf.grid_columnconfigure(1, weight=1)
182
+ for col, label, color, attr in [(0,"Foto 1",BLUE,"c1"),(1,"Foto 2",GREEN,"c2")]:
183
+ f = tk.Frame(pf, bg=CARD); f.grid(row=0, column=col, padx=2)
184
+ tk.Label(f, text=label, font=("Courier",8,"bold"), bg=CARD, fg=color).pack()
185
+ c = tk.Canvas(f, width=100, height=100, bg="#050810",
186
+ highlightthickness=1, highlightbackground=BORDER)
187
+ c.pack(); setattr(self, attr, c)
188
+
189
+ # Score boxes
190
+ tk.Label(lf, text="METRICS", font=("Courier",9,"bold"),
191
+ bg=CARD, fg=SUB).pack(pady=(8,2))
192
+ sf = tk.Frame(lf, bg=CARD); sf.pack(fill="x", padx=6)
193
+ self.m_sign = self._mbox(sf, "IMG SIGN", GREEN)
194
+ self.m_amp = self._mbox(sf, "AMP IMG", ORANGE)
195
+ self.m_chain = self._mbox(sf, "CHAIN", TEAL)
196
+ self.m_cos = self._mbox(sf, "COSINE", PURPLE)
197
+
198
+ # Verdict
199
+ self.verdict = tk.Label(lf, text="β€”",
200
+ font=("Courier",20,"bold"), bg=CARD, fg=SUB,
201
+ pady=8, highlightthickness=2, highlightbackground=BORDER)
202
+ self.verdict.pack(fill="x", padx=8, pady=6)
203
+
204
+ # Chain detail
205
+ self.chain_lbl = tk.Label(lf, text="", font=("Courier",8),
206
+ bg=CARD, fg=SUB, justify="left", wraplength=200)
207
+ self.chain_lbl.pack(padx=10)
208
+
209
+ # Mode
210
+ tk.Label(lf, text="MODE", font=("Courier",8,"bold"),
211
+ bg=CARD, fg=SUB).pack(pady=(10,2))
212
+ mf = tk.Frame(lf, bg=CARD); mf.pack()
213
+ for val, label, col in [("metric","METRIC",GREEN),("training","TRAINING",ORANGE)]:
214
+ tk.Radiobutton(mf, text=label, variable=self.mode, value=val,
215
+ bg=CARD, fg=col, selectcolor=CARD,
216
+ font=("Courier",8,"bold"),
217
+ command=self._update_center).pack(side="left", padx=6)
218
+
219
+ # Nav buttons
220
+ tk.Label(lf, text="WINDOW NAV", font=("Courier",8,"bold"),
221
+ bg=CARD, fg=SUB).pack(pady=(10,2))
222
+ nf = tk.Frame(lf, bg=CARD); nf.pack(fill="x", padx=6)
223
+ for text, cmd, col in [
224
+ ("β—€β—€",self._win_first,SUB), ("β—€",self._win_prev,BLUE),
225
+ ("β–Ά",self._win_next,BLUE), ("AUTO",self._win_auto,PURPLE),
226
+ ("β– ",self._win_stop,RED)
227
+ ]:
228
+ tk.Button(nf, text=text, command=cmd, bg=CARD, fg=col,
229
+ font=("Courier",9,"bold"), relief="flat",
230
+ padx=4, pady=4, cursor="hand2").pack(side="left", expand=True)
231
+
232
+ # Win info
233
+ self.win_info = tk.Label(lf, text="Window: β€”", font=("Courier",8),
234
+ bg=CARD, fg=SUB, wraplength=200)
235
+ self.win_info.pack(padx=8, pady=4)
236
+
237
+ def _mbox(self, parent, label, color):
238
+ f = tk.Frame(parent, bg="#0a0e1a", highlightthickness=1, highlightbackground=BORDER)
239
+ f.pack(side="left", expand=True, fill="both", padx=2, pady=2)
240
+ tk.Label(f, text=label, font=("Courier",6,"bold"), bg="#0a0e1a", fg=color).pack(pady=(3,0))
241
+ lbl = tk.Label(f, text="β€”", font=("Courier",12,"bold"), bg="#0a0e1a", fg=color)
242
+ lbl.pack(pady=(0,3)); return lbl
243
+
244
+ # ── CENTER ───────────────────────────────────────────
245
+ def _build_center(self, parent):
246
+ cf = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=BORDER)
247
+ cf.grid(row=0, column=1, sticky="nsew", padx=6)
248
+
249
+ tk.Label(cf, text="SLIDING WINDOW EMBEDDING ANALYSIS",
250
+ font=("Courier",10,"bold"), bg=CARD, fg=PURPLE).pack(pady=(8,2))
251
+
252
+ # Embedding overlay bar
253
+ tk.Label(cf, text="Embedding: Biru=E1 Hijau=E2 β€” orange box = window aktif",
254
+ font=("Courier",8), bg=CARD, fg=SUB).pack()
255
+ self.c_emb = tk.Canvas(cf, bg="#050810", height=180,
256
+ highlightthickness=1, highlightbackground=BORDER)
257
+ self.c_emb.pack(fill="x", padx=8, pady=2)
258
+
259
+ # Window detail
260
+ self.win_title = tk.Label(cf, text="Window detail",
261
+ font=("Courier",8), bg=CARD, fg=SUB)
262
+ self.win_title.pack()
263
+ self.c_win = tk.Canvas(cf, bg="#050810", height=150,
264
+ highlightthickness=1, highlightbackground=BORDER)
265
+ self.c_win.pack(fill="x", padx=8, pady=2)
266
+
267
+ # tanh curve
268
+ tk.Label(cf, text="tanh(Ξ²Β·E1Β·E2) Agreement Curve (Ξ²=10) β€” titik = dimensi di window aktif",
269
+ font=("Courier",8), bg=CARD, fg=ORANGE).pack()
270
+ self.c_tanh = tk.Canvas(cf, bg="#050810", height=130,
271
+ highlightthickness=1, highlightbackground=BORDER)
272
+ self.c_tanh.pack(fill="x", padx=8, pady=2)
273
+
274
+ # Sign pattern all windows
275
+ tk.Label(cf, text="Sign match ratio per window (hijau β‰₯ threshold, merah = tidak)",
276
+ font=("Courier",8), bg=CARD, fg=GREEN).pack(pady=(4,0))
277
+ self.c_sign = tk.Canvas(cf, bg="#050810", height=70,
278
+ highlightthickness=1, highlightbackground=BORDER)
279
+ self.c_sign.pack(fill="x", padx=8, pady=2)
280
+
281
+ # Chain pattern
282
+ tk.Label(cf, text="Chain pattern (rantai match kontinu)",
283
+ font=("Courier",8), bg=CARD, fg=TEAL).pack()
284
+ self.c_chain = tk.Canvas(cf, bg="#050810", height=55,
285
+ highlightthickness=1, highlightbackground=BORDER)
286
+ self.c_chain.pack(fill="x", padx=8, pady=(2,6))
287
+
288
+ # ── RIGHT ────────────────────────────────────────────
289
+ def _build_right(self, parent):
290
+ rf = tk.Frame(parent, bg=CARD, highlightthickness=1,
291
+ highlightbackground=BORDER, width=260)
292
+ rf.grid(row=0, column=2, sticky="nsew", padx=(6,0))
293
+ rf.grid_propagate(False)
294
+
295
+ tk.Label(rf, text="EMBEDDING BARS",
296
+ font=("Courier",10,"bold"), bg=CARD, fg=TEAL).pack(pady=(8,2))
297
+
298
+ tk.Label(rf, text="E1 β€” Foto 1 (1024D)", font=("Courier",8,"bold"),
299
+ bg=CARD, fg=BLUE).pack()
300
+ self.c_e1 = tk.Canvas(rf, bg="#050810", height=55,
301
+ highlightthickness=1, highlightbackground=BORDER)
302
+ self.c_e1.pack(fill="x", padx=8, pady=2)
303
+
304
+ tk.Label(rf, text="E2 β€” Foto 2 (1024D)", font=("Courier",8,"bold"),
305
+ bg=CARD, fg=GREEN).pack()
306
+ self.c_e2 = tk.Canvas(rf, bg="#050810", height=55,
307
+ highlightthickness=1, highlightbackground=BORDER)
308
+ self.c_e2.pack(fill="x", padx=8, pady=2)
309
+
310
+ tk.Label(rf, text="ARCHITECTURE", font=("Courier",9,"bold"),
311
+ bg=CARD, fg=SUB).pack(pady=(12,4))
312
+ steps = [
313
+ ("SW1", "112β†’56", BLUE),
314
+ ("Conv2", "56β†’56", GREEN), ("Conv3", "56οΏ½οΏ½οΏ½28", GREEN),
315
+ ("Conv4", "28β†’28", GREEN), ("Conv5", "28β†’28", GREEN),
316
+ ("Conv6", "28β†’14", GREEN), ("Conv7", "14β†’14", GREEN),
317
+ ("Conv8", "14β†’14", GREEN), ("Conv9", "14β†’7", GREEN),
318
+ ("Conv10","7β†’7", GREEN), ("GAP", "β†’256", TEAL),
319
+ ("FC", "β†’1024", PURPLE),
320
+ ]
321
+ gf = tk.Frame(rf, bg=CARD); gf.pack(padx=8, fill="x")
322
+ for i, (name, res, col) in enumerate(steps):
323
+ f = tk.Frame(gf, bg=CARD); f.grid(row=i//3, column=i%3, padx=2, pady=1, sticky="w")
324
+ tk.Label(f, text=name, font=("Courier",7,"bold"), bg=CARD, fg=col).pack()
325
+ tk.Label(f, text=res, font=("Courier",6), bg=CARD, fg=SUB).pack()
326
+
327
+ tk.Label(rf, text="STATS", font=("Courier",9,"bold"),
328
+ bg=CARD, fg=SUB).pack(pady=(12,4))
329
+ self.stats_lbl = tk.Label(rf, text="β€”", font=("Courier",8),
330
+ bg=CARD, fg=TEXT, justify="left", wraplength=240)
331
+ self.stats_lbl.pack(padx=10)
332
+
333
+ # ── UPLOAD ───────────────────────────────────────────
334
+ def upload1(self):
335
+ path = filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")])
336
+ if not path: return
337
+ self.arr1 = load_face(path)
338
+ self.e1 = get_emb(self.arr1)
339
+ self._show(self.arr1, self.c1, 100)
340
+ self._refresh()
341
+
342
+ def upload2(self):
343
+ path = filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")])
344
+ if not path: return
345
+ self.arr2 = load_face(path)
346
+ self.e2 = get_emb(self.arr2)
347
+ self._show(self.arr2, self.c2, 100)
348
+ self._refresh()
349
+
350
+ def _show(self, arr, canvas, size):
351
+ img = Image.fromarray(arr.astype(np.uint8)).resize((size,size), Image.NEAREST)
352
+ tk_img = ImageTk.PhotoImage(img)
353
+ canvas.delete("all")
354
+ canvas.create_image(0, 0, anchor="nw", image=tk_img)
355
+ canvas.image = tk_img
356
+
357
+ def _refresh(self):
358
+ if self.e1 is None or self.e2 is None: return
359
+ self._update_scores()
360
+ self._draw_emb_bars()
361
+ self._update_center()
362
+ self._draw_sign_pattern()
363
+ self._draw_chain_pattern()
364
+ self._update_stats()
365
+
366
+ # ── SCORES ───────────────────────────────────────────
367
+ def _update_scores(self):
368
+ e1, e2 = self.e1, self.e2
369
+ sg = img_sign_score(e1, e2)
370
+ ap = amp_score(e1, e2)
371
+ cs, nc, ac = chain_score(e1, e2)
372
+ co = cosine(e1, e2)
373
+ self.m_sign.config(text=f"{sg:.3f}")
374
+ self.m_amp.config(text=f"{ap:.3f}")
375
+ self.m_chain.config(text=f"{cs:.3f}")
376
+ self.m_cos.config(text=f"{co:.3f}")
377
+ self.chain_lbl.config(text=f"Chains: {nc} AvgLen: {ac:.1f}\n(neutral={NEUTRAL_LEN})")
378
+
379
+ thr = 0.79; npass = sum([sg>=thr, ap>=thr, cs>=thr])
380
+ if npass >= 2:
381
+ self.verdict.config(text="βœ… MATCH", fg=WHITE, bg="#064e3b", highlightbackground=GREEN)
382
+ elif npass == 1:
383
+ self.verdict.config(text="⚠️ UNCERTAIN", fg=WHITE, bg="#78350f", highlightbackground=ORANGE)
384
+ else:
385
+ self.verdict.config(text="❌ DIFFERENT", fg=WHITE, bg="#450a0a", highlightbackground=RED)
386
+
387
+ def _update_stats(self):
388
+ e1, e2 = self.e1, self.e2
389
+ n = len(e1) - WINDOW_SIZE + 1
390
+ n_match = sum(1 for i in range(n)
391
+ if sum(1 for j in range(WINDOW_SIZE)
392
+ if (e1[i+j]>=0)==(e2[i+j]>=0)) >= THRESHOLD)
393
+ self.stats_lbl.config(
394
+ text=f"EMB dim : {len(e1)}\n"
395
+ f"N windows : {n}\n"
396
+ f"Pass thr : {n_match}/{n}\n"
397
+ f"E1 norm : {np.linalg.norm(e1):.3f}\n"
398
+ f"E2 norm : {np.linalg.norm(e2):.3f}\n"
399
+ f"E1 pos dim : {(e1>=0).sum()}/{len(e1)}\n"
400
+ f"E2 pos dim : {(e2>=0).sum()}/{len(e2)}\n"
401
+ f"Sign agree : {((e1>=0)==(e2>=0)).sum()}/{len(e1)}")
402
+
403
+ # ── EMBEDDING BARS ────────────────────────────────────
404
+ def _draw_emb_bars(self):
405
+ c = self.c_emb; c.delete("all")
406
+ W = c.winfo_width() or 900; H = 180
407
+ n = len(self.e1); bw = W/n; mid = H//2
408
+ c.create_line(0, mid, W, mid, fill=BORDER, width=1, dash=(3,2))
409
+ for i in range(n):
410
+ x0 = i*bw; x1 = x0+bw-0.3
411
+ v1 = float(self.e1[i]); h1 = abs(v1)*(mid-4)
412
+ in_win = self.win_pos <= i < self.win_pos + WINDOW_SIZE
413
+ col1 = "#a5b4fc" if in_win else BLUE
414
+ if v1>=0: c.create_rectangle(x0, mid-h1, x1, mid, fill=col1, outline="")
415
+ else: c.create_rectangle(x0, mid, x1, mid+h1, fill=col1, outline="")
416
+ v2 = float(self.e2[i]); h2 = abs(v2)*(mid-4)*0.7
417
+ col2 = "#6ee7b7" if in_win else GREEN
418
+ if v2>=0: c.create_rectangle(x0, mid-h2, x1, mid, fill=col2, outline="", stipple="gray25")
419
+ else: c.create_rectangle(x0, mid, x1, mid+h2, fill=col2, outline="", stipple="gray25")
420
+
421
+ # Window highlight box
422
+ wx0 = self.win_pos * bw; wx1 = (self.win_pos + WINDOW_SIZE) * bw
423
+ c.create_rectangle(wx0, 2, wx1, H-2, outline=ORANGE, width=2)
424
+
425
+ # Emb bars kanan
426
+ for cv, emb, col in [(self.c_e1, self.e1, BLUE), (self.c_e2, self.e2, GREEN)]:
427
+ cv.delete("all")
428
+ W2 = cv.winfo_width() or 240; H2 = 55
429
+ n2 = len(emb); bw2 = W2/n2; mid2 = H2//2
430
+ for i, v in enumerate(emb):
431
+ x0 = i*bw2; h = abs(float(v))*(mid2-2)
432
+ fc = col if float(v)>=0 else RED
433
+ if float(v)>=0: cv.create_rectangle(x0, mid2-h, x0+bw2-0.3, mid2, fill=fc, outline="")
434
+ else: cv.create_rectangle(x0, mid2, x0+bw2-0.3, mid2+h, fill=fc, outline="")
435
+
436
+ # ── CENTER DRAWS ──────────────────────────────────────
437
+ def _update_center(self):
438
+ if self.e1 is None: return
439
+ self._draw_emb_bars()
440
+ self._draw_window_detail()
441
+ self._draw_tanh_curve()
442
+
443
+ # Update win info
444
+ e1, e2 = self.e1, self.e2
445
+ n = len(e1) - WINDOW_SIZE + 1
446
+ n_match = sum(1 for j in range(WINDOW_SIZE)
447
+ if (e1[self.win_pos+j]>=0)==(e2[self.win_pos+j]>=0))
448
+ self.win_info.config(
449
+ text=f"Win {self.win_pos}/{n-1} match {n_match}/{WINDOW_SIZE} "
450
+ f"{'βœ“ PASS' if n_match>=THRESHOLD else 'βœ— FAIL'}",
451
+ fg=GREEN if n_match>=THRESHOLD else RED)
452
+ self.win_title.config(
453
+ text=f"Window [{self.win_pos}:{self.win_pos+WINDOW_SIZE}] β€” "
454
+ f"{'tanh agreement (training)' if self.mode.get()=='training' else 'sign matching (metric)'}")
455
+
456
+ def _draw_window_detail(self):
457
+ c = self.c_win; c.delete("all")
458
+ W = c.winfo_width() or 900; H = 150
459
+ pos = self.win_pos
460
+ w1 = self.e1[pos:pos+WINDOW_SIZE]
461
+ w2 = self.e2[pos:pos+WINDOW_SIZE]
462
+ bw = W / WINDOW_SIZE; mid = H//2 - 10
463
+ mode = self.mode.get()
464
+
465
+ for i in range(WINDOW_SIZE):
466
+ x0 = i*bw+2; x1 = x0+bw-4; xc = (x0+x1)/2
467
+ v1, v2 = float(w1[i]), float(w2[i])
468
+ same = (v1>=0) == (v2>=0)
469
+
470
+ if mode == "training":
471
+ agree = float(tanh_agreement(v1, v2))
472
+ col = self._lerp(RED, GREEN, agree)
473
+ h = agree*(mid-5)
474
+ c.create_rectangle(x0, mid-h, x1, mid, fill=col, outline="")
475
+ c.create_text(xc, H-22, anchor="center",
476
+ text=f"{agree:.2f}", font=("Courier",6), fill=col)
477
+ arrow = "β–²" if agree>0.5 else "β–Ό"
478
+ c.create_text(xc, mid-h-10, anchor="center",
479
+ text=arrow, font=("Courier",9),
480
+ fill=GREEN if agree>0.5 else RED)
481
+ # Gradient label
482
+ if agree > 0.5:
483
+ c.create_text(xc, H-8, anchor="center",
484
+ text="β†’1", font=("Courier",5), fill=GREEN)
485
+ else:
486
+ c.create_text(xc, H-8, anchor="center",
487
+ text="β†’0", font=("Courier",5), fill=RED)
488
+ else:
489
+ s1 = "+" if v1>=0 else "βˆ’"
490
+ s2 = "+" if v2>=0 else "βˆ’"
491
+ col = GREEN if same else RED
492
+ c.create_rectangle(x0, 20, x1, mid, fill=col, outline="")
493
+ c.create_text(xc, 32, anchor="center",
494
+ text=s1, font=("Courier",12,"bold"), fill=WHITE)
495
+ c.create_text(xc, 54, anchor="center",
496
+ text=s2, font=("Courier",12,"bold"), fill=WHITE)
497
+ c.create_text(xc, mid+8, anchor="center",
498
+ text="βœ“" if same else "βœ—", font=("Courier",9), fill=col)
499
+
500
+ # Match bar
501
+ n_match = sum(1 for j in range(WINDOW_SIZE)
502
+ if (w1[j]>=0)==(w2[j]>=0))
503
+ mw = (n_match/WINDOW_SIZE)*(W-16)
504
+ c.create_rectangle(8, H-6, 8+mw, H-1,
505
+ fill=GREEN if n_match>=THRESHOLD else RED, outline="")
506
+ c.create_text(W//2, H-4, anchor="center",
507
+ text=f"Match {n_match}/{WINDOW_SIZE} thr={THRESHOLD} {'PASS βœ“' if n_match>=THRESHOLD else 'FAIL βœ—'}",
508
+ font=("Courier",7), fill=GREEN if n_match>=THRESHOLD else RED)
509
+
510
+ def _draw_tanh_curve(self):
511
+ c = self.c_tanh; c.delete("all")
512
+ W = c.winfo_width() or 900; H = 130
513
+ mid_y = H//2
514
+ c.create_line(0, mid_y, W, mid_y, fill=BORDER, width=1, dash=(3,2))
515
+ c.create_line(W//2, 0, W//2, H, fill=BORDER, width=1, dash=(3,2))
516
+
517
+ # Curve
518
+ xs = np.linspace(-3, 3, W)
519
+ ys = (np.tanh(xs) + 1) / 2
520
+ pts = [(i, int(mid_y - ys[i]*(mid_y-8))) for i in range(W)]
521
+ for i in range(len(pts)-1):
522
+ c.create_line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1],
523
+ fill=ORANGE, width=2)
524
+
525
+ # Dots per dimensi di window
526
+ pos = self.win_pos
527
+ w1 = self.e1[pos:pos+WINDOW_SIZE]
528
+ w2 = self.e2[pos:pos+WINDOW_SIZE]
529
+ for j in range(WINDOW_SIZE):
530
+ prod = float(w1[j]) * float(w2[j]) * BETA
531
+ agree = (math.tanh(prod) + 1) / 2
532
+ px = int((prod + 3) / 6 * W); px = max(0, min(W-1, px))
533
+ py = int(mid_y - agree*(mid_y-8))
534
+ same = (w1[j]>=0) == (w2[j]>=0)
535
+ c.create_oval(px-4, py-4, px+4, py+4,
536
+ fill=GREEN if same else RED, outline=WHITE)
537
+
538
+ mode = self.mode.get()
539
+ c.create_text(4, 4, anchor="nw",
540
+ text=f"{'Training: gradient dorong ke 1.0 (same) atau 0.0 (diff)' if mode=='training' else 'Metric: posisi dot = agreement di window ini'}",
541
+ font=("Courier",7), fill=YELLOW)
542
+ c.create_text(4, H-4, anchor="sw",
543
+ text="prod<0 (beda tanda)", font=("Courier",6), fill=RED)
544
+ c.create_text(W-4, H-4, anchor="se",
545
+ text="prod>0 (sama tanda)", font=("Courier",6), fill=GREEN)
546
+
547
+ def _draw_sign_pattern(self):
548
+ c = self.c_sign; c.delete("all")
549
+ W = c.winfo_width() or 900; H = 70
550
+ e1, e2 = self.e1, self.e2
551
+ n = len(e1) - WINDOW_SIZE + 1; bw = W/n
552
+ for i in range(n):
553
+ mc = sum(1 for j in range(WINDOW_SIZE)
554
+ if (e1[i+j]>=0)==(e2[i+j]>=0))
555
+ h = (mc/WINDOW_SIZE)*(H-4)
556
+ col = GREEN if mc>=THRESHOLD else RED
557
+ c.create_rectangle(i*bw, H-h, i*bw+bw-0.3, H, fill=col, outline="")
558
+ # Threshold line
559
+ ty = H - (THRESHOLD/WINDOW_SIZE)*(H-4)
560
+ c.create_line(0, ty, W, ty, fill=YELLOW, width=1, dash=(4,2))
561
+ c.create_text(4, 4, anchor="nw",
562
+ text=f"Sign match ratio per window (garis kuning = thr {THRESHOLD}/{WINDOW_SIZE})",
563
+ font=("Courier",7), fill=SUB)
564
+
565
+ def _draw_chain_pattern(self):
566
+ c = self.c_chain; c.delete("all")
567
+ W = c.winfo_width() or 900; H = 55
568
+ e1, e2 = self.e1, self.e2
569
+ n = len(e1) - WINDOW_SIZE + 1; bw = W/n
570
+ in_chain = False; chain_num = 0
571
+ for i in range(n):
572
+ mc = sum(1 for j in range(WINDOW_SIZE)
573
+ if (e1[i+j]>=0)==(e2[i+j]>=0))
574
+ match = mc >= THRESHOLD; x0 = i*bw
575
+ if match:
576
+ c.create_rectangle(x0, 8, x0+bw-0.3, H-8, fill=TEAL, outline="")
577
+ if not in_chain:
578
+ in_chain = True; chain_num += 1
579
+ c.create_line(x0, 4, x0, H-4, fill=WHITE, width=1)
580
+ c.create_text(x0+1, 5, anchor="nw",
581
+ text=str(chain_num), font=("Courier",5), fill=WHITE)
582
+ else:
583
+ in_chain = False
584
+ c.create_text(4, 4, anchor="nw",
585
+ text=f"Chain pattern β€” teal=match, putih=awal chain baru",
586
+ font=("Courier",7), fill=SUB)
587
+
588
+ # ── WINDOW NAV ────────────────────────────────────────
589
+ def _win_first(self):
590
+ self.win_pos=0; self._update_center()
591
+ def _win_prev(self):
592
+ self.win_pos=max(0,self.win_pos-1); self._update_center()
593
+ def _win_next(self):
594
+ if self.e1 is None: return
595
+ n=len(self.e1)-WINDOW_SIZE+1
596
+ self.win_pos=min(self.win_pos+1,n-1); self._update_center()
597
+ def _win_stop(self):
598
+ self.animating=False
599
+ def _win_auto(self):
600
+ self.animating=True; self._auto_loop()
601
+ def _auto_loop(self):
602
+ if not self.animating or self.e1 is None: return
603
+ n=len(self.e1)-WINDOW_SIZE+1
604
+ self.win_pos=(self.win_pos+1)%n
605
+ self._update_center()
606
+ self.after(100, self._auto_loop)
607
+
608
+ # ── HELPERS ──────────────────────────────────────────
609
+ def _lerp(self, c1, c2, t):
610
+ r1,g1,b1=int(c1[1:3],16),int(c1[3:5],16),int(c1[5:7],16)
611
+ r2,g2,b2=int(c2[1:3],16),int(c2[3:5],16),int(c2[5:7],16)
612
+ r=int(r1+(r2-r1)*t); g=int(g1+(g2-g1)*t); b=int(b1+(b2-b1)*t)
613
+ return f"#{r:02x}{g:02x}{b:02x}"
614
+
615
+
616
+ # ── MAIN ───────────────────────────────────────────────────
617
+ if __name__ == "__main__":
618
+ app = App()
619
+ app.after(200, app._refresh)
620
+ app.mainloop()