Upload 4 files
Browse files- convert_onnx.py +191 -0
- imgnet_conv10_epoch39.onnx +3 -0
- verif_onnx.py +90 -0
- 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()
|