| |
| |
| |
| |
| |
|
|
| import tkinter as tk |
| from tkinter import filedialog, ttk |
| from PIL import Image, ImageTk, ImageDraw |
| import numpy as np |
| import math |
| import threading |
| import time |
|
|
| |
| try: |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| TORCH_OK = True |
| except ImportError: |
| TORCH_OK = False |
|
|
| |
| try: |
| from facenet_pytorch import MTCNN |
| _mtcnn = MTCNN(image_size=112, keep_all=False, post_process=False, |
| device="cuda" if (TORCH_OK and torch.cuda.is_available()) else "cpu") |
| MTCNN_OK = True |
| except Exception: |
| _mtcnn = None |
| MTCNN_OK = False |
|
|
| |
| CKPT_PATH = r"C:\PythonProj\img_bnn\checkpoints_sw357_conv10_imgsign\SW357_conv10_imgsign\best_model_epoch39_plateau.pth" |
| EMB_DIM = 1024 |
|
|
| _imgnet_model = None |
| _imgnet_device = "cpu" |
|
|
| if TORCH_OK: |
| class _SWBlock(nn.Module): |
| def __init__(self): |
| super().__init__() |
| n_diff = (8 + 24 + 48) * 3 |
| self.fc = nn.Sequential(nn.Linear(240, 64), nn.ReLU(inplace=True), nn.Linear(64, 32)) |
| def forward(self, x): |
| B, C, H, W = x.shape |
| diffs = [] |
| for ws in [3, 5, 7]: |
| pad = ws // 2 |
| x_pad = F.pad(x, [pad,pad,pad,pad], mode='reflect') |
| patches = x_pad.unfold(2,ws,1).unfold(3,ws,1) |
| diff = x.unsqueeze(-1).unsqueeze(-1) - patches |
| mid = ws // 2 |
| mask = torch.ones(ws, ws, dtype=torch.bool, device=x.device) |
| mask[mid,mid] = False |
| diffs.append(diff[:,:,:,:,mask]) |
| diffs = torch.cat(diffs, -1) |
| B,C,H,W,N = diffs.shape |
| out = self.fc(diffs.permute(0,2,3,1,4).reshape(B*H*W, C*N)) |
| return out.reshape(B,H,W,-1).permute(0,3,1,2) |
|
|
| class _IMGNet(nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.sw1 = _SWBlock(); self.bn1 = nn.BatchNorm2d(32) |
| self.conv2 = nn.Conv2d(32, 64, 3,stride=1,padding=1,bias=False); self.bn2 = nn.BatchNorm2d(64) |
| self.conv3 = nn.Conv2d(64, 64, 3,stride=2,padding=1,bias=False); self.bn3 = nn.BatchNorm2d(64) |
| self.conv4 = nn.Conv2d(64, 128, 3,stride=1,padding=1,bias=False); self.bn4 = nn.BatchNorm2d(128) |
| self.conv5 = nn.Conv2d(128, 128, 3,stride=1,padding=1,bias=False); self.bn5 = nn.BatchNorm2d(128) |
| self.conv6 = nn.Conv2d(128, 128, 3,stride=2,padding=1,bias=False); self.bn6 = nn.BatchNorm2d(128) |
| self.conv7 = nn.Conv2d(128, 256, 3,stride=1,padding=1,bias=False); self.bn7 = nn.BatchNorm2d(256) |
| self.conv8 = nn.Conv2d(256, 256, 3,stride=1,padding=1,bias=False); self.bn8 = nn.BatchNorm2d(256) |
| self.conv9 = nn.Conv2d(256, 256, 3,stride=2,padding=1,bias=False); self.bn9 = nn.BatchNorm2d(256) |
| self.conv10 = nn.Conv2d(256, 256, 3,stride=1,padding=1,bias=False); self.bn10 = nn.BatchNorm2d(256) |
| self.gap = nn.AdaptiveAvgPool2d(1) |
| self.fc = nn.Linear(256, 1024) |
| self.bn = nn.BatchNorm1d(1024) |
| def forward(self, x): |
| x = F.relu(self.bn1(self.sw1(x))) |
| x = F.relu(self.bn2(self.conv2(x))); x = F.relu(self.bn3(self.conv3(x))) |
| x = F.relu(self.bn4(self.conv4(x))); x = F.relu(self.bn5(self.conv5(x))) |
| x = F.relu(self.bn6(self.conv6(x))); x = F.relu(self.bn7(self.conv7(x))) |
| x = F.relu(self.bn8(self.conv8(x))); x = F.relu(self.bn9(self.conv9(x))) |
| x = F.relu(self.bn10(self.conv10(x))) |
| x = self.gap(x).view(x.size(0), -1) |
| return self.bn(self.fc(x)) |
|
|
| import os |
| if os.path.exists(CKPT_PATH): |
| try: |
| _imgnet_device = "cuda" if torch.cuda.is_available() else "cpu" |
| _imgnet_model = _IMGNet().to(_imgnet_device) |
| state = torch.load(CKPT_PATH, map_location="cpu", weights_only=False) |
| if isinstance(state, dict) and "model" in state: |
| state = state["model"] |
| _imgnet_model.load_state_dict(state) |
| _imgnet_model.eval() |
|
|
| |
| _feature_maps = {} |
| def _make_hook(name): |
| def hook(module, inp, out): |
| _feature_maps[name] = out.detach().cpu() |
| return hook |
|
|
| _imgnet_model.sw1.register_forward_hook(_make_hook("sw1")) |
| for i in range(2, 11): |
| getattr(_imgnet_model, f"conv{i}").register_forward_hook(_make_hook(f"conv{i}")) |
| _imgnet_model.gap.register_forward_hook(_make_hook("gap")) |
|
|
| print(f"✓ IMGNet loaded from {CKPT_PATH}") |
| except Exception as e: |
| print(f"✗ IMGNet load failed: {e}") |
| _imgnet_model = None |
| else: |
| print(f"✗ Checkpoint tidak ditemukan: {CKPT_PATH}") |
| else: |
| _feature_maps = {} |
|
|
| |
| BG = "#0a0e1a" |
| CARD = "#111827" |
| BORDER = "#1e293b" |
| BLUE = "#6366f1" |
| GREEN = "#10b981" |
| ORANGE = "#f59e0b" |
| PURPLE = "#a855f7" |
| TEAL = "#14b8a6" |
| RED = "#ef4444" |
| SUB = "#64748b" |
| TEXT = "#e2e8f0" |
| WHITE = "#ffffff" |
| YELLOW = "#fbbf24" |
|
|
| |
| WINDOW_SIZE = 11 |
| THRESHOLD = 8 |
| EMB_DIM = 64 |
| IMG_SIZE = 112 |
| BETA = 10.0 |
|
|
|
|
| |
| |
| |
| def dummy_embed(img_array): |
| """Generate pseudo-embedding (fallback kalau model tidak ada)""" |
| flat = img_array.flatten().astype(np.float32) / 255.0 |
| np.random.seed(int(flat.sum() * 1000) % 2**31) |
| emb = np.random.randn(EMB_DIM).astype(np.float32) |
| return emb / (np.linalg.norm(emb) + 1e-8) |
|
|
| def get_embedding(img_array): |
| """Get real IMGNet embedding, fallback ke dummy""" |
| if _imgnet_model is not None and TORCH_OK: |
| try: |
| arr = img_array.astype(np.float32) / 255.0 |
| t = torch.from_numpy(arr).permute(2,0,1).unsqueeze(0).to(_imgnet_device) |
| with torch.no_grad(): |
| emb = _imgnet_model(t).squeeze(0).cpu().numpy() |
| return emb |
| except Exception as e: |
| print(f"Embed error: {e}") |
| return dummy_embed(img_array) |
|
|
|
|
| |
| |
| |
| def tanh_agreement(e1, e2, beta=BETA): |
| return (np.tanh(beta * e1 * e2) + 1) / 2 |
|
|
| def img_sign_score(e1, e2): |
| n = len(e1) - WINDOW_SIZE + 1 |
| scores = [] |
| for i in range(n): |
| w1, w2 = e1[i:i+WINDOW_SIZE], e2[i:i+WINDOW_SIZE] |
| s1 = np.where(w1 >= 0, 1, -1) |
| s2 = np.where(w2 >= 0, 1, -1) |
| mc = int(np.sum(s1 == s2)) |
| scores.append(mc / WINDOW_SIZE) |
| return np.array(scores) |
|
|
| def chain_score(e1, e2): |
| n = len(e1) - WINDOW_SIZE + 1 |
| flags = [] |
| for i in range(n): |
| s1 = np.where(e1[i:i+WINDOW_SIZE] >= 0, 1, -1) |
| s2 = np.where(e2[i:i+WINDOW_SIZE] >= 0, 1, -1) |
| flags.append(int(np.sum(s1 == s2)) >= THRESHOLD) |
| total = sum(flags); img_s = total / max(n, 1) |
| chains = 0; in_c = False |
| for f in flags: |
| if f and not in_c: chains += 1; in_c = True |
| elif not f: in_c = False |
| avg_c = total / max(chains, 1) |
| diff = avg_c - 29 |
| score = img_s + (0.3 * diff if diff >= 0 else 1.0 * diff) / 100 |
| return float(np.clip(score, 0, 1)), chains, avg_c |
|
|
|
|
| |
| |
| |
| def sw_scan_result(img_array, window_size=3): |
| """ |
| Scan 112×112 image with SW Block window |
| Returns: heat map (H×W) of relational activity |
| """ |
| img = img_array.astype(np.float32) / 255.0 |
| if len(img.shape) == 3: |
| gray = 0.299*img[:,:,0] + 0.587*img[:,:,1] + 0.114*img[:,:,2] |
| else: |
| gray = img |
| h, w = gray.shape |
| pad = window_size // 2 |
| padded = np.pad(gray, pad, mode='reflect') |
| result = np.zeros((h, w)) |
| for r in range(h): |
| for c in range(w): |
| patch = padded[r:r+window_size, c:c+window_size] |
| center = gray[r, c] |
| diffs = patch.flatten() |
| mid = len(diffs) // 2 |
| diffs = np.delete(diffs, mid) |
| result[r, c] = np.mean(np.abs(diffs - center)) |
| return result |
|
|
|
|
| |
| |
| |
| class IMGNetVisualizer: |
| def __init__(self, root): |
| self.root = root |
| root.title("IMGNet Interactive Visualizer") |
| root.geometry("1400x900") |
| root.configure(bg=BG) |
| root.resizable(True, True) |
|
|
| |
| self.img1_array = None |
| self.img2_array = None |
| self.emb1 = None |
| self.emb2 = None |
| self.sw_window = 3 |
| self.conv_layer = 2 |
| self.win_pos = 0 |
| self.mode = tk.StringVar(value="metric") |
| self.animating = False |
| self.sw_animating = False |
|
|
| self._build_ui() |
|
|
| |
| |
| |
| def _build_ui(self): |
| |
| top = tk.Frame(self.root, bg=BG, height=50) |
| top.pack(fill="x", padx=16, pady=(12,0)) |
|
|
| tk.Label(top, |
| text="IMGNet · Multi-Scale Sliding Window Face Verification · Interactive Visualizer", |
| font=("Courier", 13, "bold"), bg=BG, fg=TEXT).pack(side="left") |
|
|
| model_status = "✓ epoch39" if _imgnet_model is not None else "✗ dummy" |
| tk.Label(top, |
| text=f"EMB {EMB_DIM}D · SW {{3,5,7}} · w={WINDOW_SIZE} t={THRESHOLD}/11 · MTCNN={'✓' if MTCNN_OK else '✗'} · IMGNet={model_status}", |
| font=("Courier", 9), bg=BG, fg=SUB).pack(side="right") |
|
|
| |
| main = tk.Frame(self.root, bg=BG) |
| main.pack(fill="both", expand=True, padx=12, pady=8) |
| main.grid_columnconfigure(0, weight=2) |
| main.grid_columnconfigure(1, weight=3) |
| main.grid_columnconfigure(2, weight=2) |
| main.grid_rowconfigure(0, weight=1) |
|
|
| self._build_left(main) |
| self._build_center(main) |
| self._build_right(main) |
|
|
| |
| def _build_left(self, parent): |
| left = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=BORDER) |
| left.grid(row=0, column=0, sticky="nsew", padx=(0,6)) |
|
|
| tk.Label(left, text="INPUT IMAGES · SW BLOCK SCAN", |
| font=("Courier", 10, "bold"), bg=CARD, fg=BLUE).pack(pady=(10,4)) |
|
|
| |
| imgs = tk.Frame(left, bg=CARD) |
| imgs.pack(fill="x", padx=8) |
| imgs.grid_columnconfigure(0, weight=1) |
| imgs.grid_columnconfigure(1, weight=1) |
|
|
| self.img1_canvas = self._image_panel(imgs, "IMAGE 1", BLUE, self.load_img1, 0) |
| self.img2_canvas = self._image_panel(imgs, "IMAGE 2", GREEN, self.load_img2, 1) |
|
|
| |
| sw_ctrl = tk.Frame(left, bg=CARD) |
| sw_ctrl.pack(fill="x", padx=8, pady=4) |
|
|
| tk.Label(sw_ctrl, text="SW Window:", font=("Courier", 9), bg=CARD, fg=SUB).pack(side="left") |
| for ws in [3, 5, 7]: |
| tk.Button(sw_ctrl, text=f"{ws}×{ws}", |
| command=lambda w=ws: self._set_sw_window(w), |
| bg=CARD, fg=ORANGE, font=("Courier", 9, "bold"), |
| relief="flat", padx=6, pady=2, |
| cursor="hand2").pack(side="left", padx=2) |
|
|
| tk.Button(sw_ctrl, text="▶ ANIMATE SW", |
| command=self.animate_sw, |
| bg=PURPLE, fg=WHITE, font=("Courier", 9, "bold"), |
| relief="flat", padx=10, pady=3, |
| cursor="hand2").pack(side="right", padx=4) |
|
|
| |
| tk.Label(left, text="SW Block Scan — Image 1", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack() |
| self.sw_canvas = tk.Canvas(left, width=224, height=224, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.sw_canvas.pack(pady=4) |
|
|
| |
| tk.Label(left, text="Relational Activity Heatmap", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack() |
| self.heat_canvas = tk.Canvas(left, width=224, height=112, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.heat_canvas.pack(pady=4) |
|
|
| |
| conv_ctrl = tk.Frame(left, bg=CARD) |
| conv_ctrl.pack(fill="x", padx=8, pady=4) |
| tk.Label(conv_ctrl, text="Conv Layer:", font=("Courier", 9), bg=CARD, fg=SUB).pack(side="left") |
| self.conv_var = tk.IntVar(value=2) |
| for i in range(2, 11): |
| tk.Radiobutton(conv_ctrl, text=str(i), variable=self.conv_var, value=i, |
| bg=CARD, fg=TEAL, selectcolor=CARD, |
| font=("Courier", 8), command=self._update_conv).pack(side="left") |
|
|
| |
| tk.Label(left, text="Conv Feature Map (simulated)", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack() |
| self.conv_canvas = tk.Canvas(left, width=224, height=56, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.conv_canvas.pack(pady=(4, 4)) |
|
|
| |
| tk.Button(left, text="🔬 ABLATION STUDY", |
| command=self.open_ablation_window, |
| bg="#7c3aed", fg=WHITE, font=("Courier", 10, "bold"), |
| relief="flat", padx=16, pady=6, cursor="hand2").pack(pady=(4,8)) |
|
|
| def _image_panel(self, parent, title, color, cmd, col): |
| f = tk.Frame(parent, bg=CARD) |
| f.grid(row=0, column=col, padx=4, pady=4) |
| tk.Label(f, text=title, font=("Courier", 9, "bold"), bg=CARD, fg=color).pack() |
| canvas = tk.Canvas(f, width=104, height=104, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| canvas.pack() |
| tk.Button(f, text="Upload", command=cmd, |
| bg=color, fg=BG, font=("Courier", 8, "bold"), |
| relief="flat", padx=6, pady=2, cursor="hand2").pack(pady=3) |
| return canvas |
|
|
| |
| def _build_center(self, parent): |
| center = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=BORDER) |
| center.grid(row=0, column=1, sticky="nsew", padx=6) |
|
|
| tk.Label(center, text="SLIDING WINDOW EMBEDDING ANALYSIS", |
| font=("Courier", 10, "bold"), bg=CARD, fg=PURPLE).pack(pady=(10,4)) |
|
|
| |
| mode_f = tk.Frame(center, bg=CARD) |
| mode_f.pack() |
| for val, label, col in [("metric","METRIC MODE",GREEN),("training","TRAINING MODE",ORANGE)]: |
| tk.Radiobutton(mode_f, text=label, variable=self.mode, value=val, |
| bg=CARD, fg=col, selectcolor=CARD, |
| font=("Courier", 9, "bold"), |
| command=self._update_center).pack(side="left", padx=12) |
|
|
| |
| self.win_info = tk.Label(center, |
| text="Window: — | Position: —/—", |
| font=("Courier", 9), bg=CARD, fg=SUB) |
| self.win_info.pack() |
|
|
| |
| self.emb_canvas = tk.Canvas(center, width=560, height=180, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.emb_canvas.pack(padx=8, pady=4) |
|
|
| |
| self.win_canvas = tk.Canvas(center, width=560, height=140, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.win_canvas.pack(padx=8, pady=4) |
|
|
| |
| self.tanh_frame = tk.Frame(center, bg=CARD) |
| self.tanh_frame.pack(fill="x", padx=8) |
| tk.Label(self.tanh_frame, text="tanh(β·E1·E2) Agreement Curve (β=10)", |
| font=("Courier", 8), bg=CARD, fg=ORANGE).pack() |
| self.tanh_canvas = tk.Canvas(self.tanh_frame, width=560, height=120, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.tanh_canvas.pack() |
|
|
| |
| nav = tk.Frame(center, bg=CARD) |
| nav.pack(pady=6) |
|
|
| tk.Button(nav, text="◀◀ FIRST", command=self._win_first, |
| bg=CARD, fg=SUB, font=("Courier", 9), relief="flat", |
| padx=8, pady=4, cursor="hand2").pack(side="left", padx=3) |
| tk.Button(nav, text="◀ PREV", command=self._win_prev, |
| bg=CARD, fg=BLUE, font=("Courier", 9, "bold"), relief="flat", |
| padx=10, pady=4, cursor="hand2").pack(side="left", padx=3) |
| tk.Button(nav, text="▶ NEXT", command=self._win_next, |
| bg=BLUE, fg=WHITE, font=("Courier", 9, "bold"), relief="flat", |
| padx=10, pady=4, cursor="hand2").pack(side="left", padx=3) |
| tk.Button(nav, text="▶▶ AUTO", command=self._win_auto, |
| bg=PURPLE, fg=WHITE, font=("Courier", 9, "bold"), relief="flat", |
| padx=10, pady=4, cursor="hand2").pack(side="left", padx=3) |
| tk.Button(nav, text="■ STOP", command=self._win_stop, |
| bg=RED, fg=WHITE, font=("Courier", 9, "bold"), relief="flat", |
| padx=10, pady=4, cursor="hand2").pack(side="left", padx=3) |
|
|
| |
| score_f = tk.Frame(center, bg=CARD) |
| score_f.pack(fill="x", padx=8, pady=4) |
| self.lbl_sign = self._score_box(score_f, "IMG SIGN", GREEN) |
| self.lbl_amp = self._score_box(score_f, "AMP IMG", ORANGE) |
| self.lbl_chain = self._score_box(score_f, "CHAIN", TEAL) |
| self.lbl_cos = self._score_box(score_f, "COSINE", PURPLE) |
|
|
| |
| self.verdict_lbl = tk.Label(center, |
| text="Upload dua gambar untuk memulai analisis", |
| font=("Courier", 22, "bold"), bg=CARD, fg=SUB, |
| pady=12, padx=20, |
| highlightthickness=2, highlightbackground=BORDER) |
| self.verdict_lbl.pack(pady=8, fill="x", padx=16) |
|
|
| def _score_box(self, parent, label, color): |
| f = tk.Frame(parent, bg="#0a0e1a", highlightthickness=1, highlightbackground=BORDER) |
| f.pack(side="left", expand=True, fill="both", padx=4, pady=2) |
| tk.Label(f, text=label, font=("Courier", 7, "bold"), bg="#0a0e1a", fg=color).pack(pady=(6,1)) |
| lbl = tk.Label(f, text="—", font=("Courier", 16, "bold"), bg="#0a0e1a", fg=color) |
| lbl.pack(pady=(0,6)) |
| return lbl |
|
|
| |
| def _build_right(self, parent): |
| right = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=BORDER) |
| right.grid(row=0, column=2, sticky="nsew", padx=(6,0)) |
|
|
| tk.Label(right, text="CONV PROCESSING · FEATURE MAPS", |
| font=("Courier", 10, "bold"), bg=CARD, fg=TEAL).pack(pady=(10,4)) |
|
|
| |
| res_path = tk.Frame(right, bg=CARD) |
| res_path.pack(fill="x", padx=8, pady=2) |
| steps = [ |
| ("SW1", "112→56"), ("Conv2", "56→56"), ("Conv3", "56→28"), |
| ("Conv4", "28→28"), ("Conv5", "28→28"), ("Conv6", "28→14"), |
| ("Conv7", "14→14"), ("Conv8", "14→14"), ("Conv9", "14→7"), |
| ("Conv10","7→7"), ("GAP","→EMB"), |
| ] |
| for i, (name, res) in enumerate(steps): |
| col = BLUE if name.startswith("SW") else (TEAL if "GAP" in name else GREEN) |
| f = tk.Frame(res_path, bg=CARD) |
| f.grid(row=i//4, column=i%4, padx=2, pady=1) |
| 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(right, text="Simulated Feature Maps per Layer", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack(pady=(6,2)) |
|
|
| |
| self.feat_canvases = [] |
| feat_f = tk.Frame(right, bg=CARD) |
| feat_f.pack(padx=8) |
| for i in range(3): |
| c = tk.Canvas(feat_f, width=90, height=90, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| c.grid(row=0, column=i, padx=2) |
| self.feat_canvases.append(c) |
|
|
| |
| tk.Label(right, text="Final Embedding (1024D → visualized)", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack(pady=(8,2)) |
|
|
| self.emb1_bar = tk.Canvas(right, width=280, height=40, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.emb1_bar.pack(padx=8) |
| tk.Label(right, text="Embedding 1", font=("Courier", 7), bg=CARD, fg=BLUE).pack() |
|
|
| self.emb2_bar = tk.Canvas(right, width=280, height=40, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.emb2_bar.pack(padx=8, pady=2) |
| tk.Label(right, text="Embedding 2", font=("Courier", 7), bg=CARD, fg=GREEN).pack() |
|
|
| |
| tk.Label(right, text="Sign Pattern Match (per window)", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack(pady=(6,2)) |
| self.sign_canvas = tk.Canvas(right, width=280, height=60, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.sign_canvas.pack(padx=8) |
|
|
| |
| tk.Label(right, text="Chain Pattern (continuous matches)", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack(pady=(6,2)) |
| self.chain_canvas = tk.Canvas(right, width=280, height=40, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.chain_canvas.pack(padx=8, pady=(0,8)) |
|
|
|
|
| |
| |
| |
| def load_img1(self): |
| path = filedialog.askopenfilename( |
| filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp")]) |
| if path: |
| self.img1_array = self._load_img(path) |
| self._display_img(self.img1_array, self.img1_canvas, 104) |
| self._compute_and_update() |
|
|
| def load_img2(self): |
| path = filedialog.askopenfilename( |
| filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp")]) |
| if path: |
| self.img2_array = self._load_img(path) |
| self._display_img(self.img2_array, self.img2_canvas, 104) |
| self._compute_and_update() |
|
|
| def _load_img(self, path): |
| img = Image.open(path).convert("RGB") |
| |
| if MTCNN_OK and _mtcnn is not None: |
| try: |
| face = _mtcnn(img) |
| if face is not None: |
| |
| arr = face.permute(1, 2, 0).numpy() |
| arr = np.clip(arr, 0, 255).astype(np.uint8) |
| return arr |
| except Exception: |
| pass |
| |
| img = img.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR) |
| return np.array(img) |
|
|
| def _display_img(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 _compute_and_update(self): |
| if self.img1_array is None or self.img2_array is None: |
| return |
| |
| self.emb1 = get_embedding(self.img1_array) |
| self.emb2 = get_embedding(self.img2_array) |
| self.win_pos = 0 |
| self._update_sw_scan() |
| self._update_center() |
| self._update_right() |
| self._update_scores() |
|
|
| def _update_scores(self): |
| if self.emb1 is None or self.emb2 is None: return |
| e1, e2 = self.emb1, self.emb2 |
| n = len(e1) - WINDOW_SIZE + 1 |
|
|
| |
| total_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 |
| ) |
| sign = total_match / max(n, 1) |
|
|
| |
| amp_total = 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) |
| s2 = np.where(w2 >= 0, 1, -1) |
| if int(np.sum(s1 == s2)) >= THRESHOLD: |
| a1, a2 = np.mean(np.abs(w1)), np.mean(np.abs(w2)) |
| amp_total += max(0.0, 1 - abs(a1-a2) / max(a1,a2,1e-6)) |
| amp = amp_total / max(n, 1) |
|
|
| |
| cs, n_chains, avg_chain = chain_score(e1, e2) |
|
|
| |
| cos = float(np.dot(e1, e2) / (np.linalg.norm(e1) * np.linalg.norm(e2) + 1e-8)) |
|
|
| self.lbl_sign.config(text=f"{sign:.3f}") |
| self.lbl_amp.config(text=f"{amp:.3f}") |
| self.lbl_chain.config(text=f"{cs:.3f}") |
| self.lbl_cos.config(text=f"{cos:.3f}") |
|
|
| |
| thr = 0.79 |
| n_pass = sum([sign >= thr, amp >= thr, cs >= thr]) |
| if n_pass >= 2: |
| self.verdict_lbl.config( |
| text="✅ MATCH", |
| fg=WHITE, bg="#064e3b", |
| highlightbackground=GREEN, |
| font=("Courier", 26, "bold")) |
| elif n_pass == 1: |
| self.verdict_lbl.config( |
| text="⚠️ UNCERTAIN", |
| fg=WHITE, bg="#78350f", |
| highlightbackground=ORANGE, |
| font=("Courier", 26, "bold")) |
| else: |
| self.verdict_lbl.config( |
| text="❌ DIFFERENT", |
| fg=WHITE, bg="#450a0a", |
| highlightbackground=RED, |
| font=("Courier", 26, "bold")) |
|
|
|
|
| |
| |
| |
| def _set_sw_window(self, ws): |
| self.sw_window = ws |
| self._update_sw_scan() |
|
|
| def _update_sw_scan(self): |
| if self.img1_array is None: return |
| |
| self._draw_sw_overlay(0, 0) |
| |
| self._draw_heatmap() |
| |
| self._update_conv() |
|
|
| def _draw_sw_overlay(self, scan_r, scan_c): |
| if self.img1_array is None: return |
| canvas_size = 224 |
| scale = canvas_size / IMG_SIZE |
|
|
| img = Image.fromarray(self.img1_array.astype(np.uint8)) |
| img = img.resize((canvas_size, canvas_size), Image.NEAREST) |
|
|
| |
| draw = ImageDraw.Draw(img, "RGBA") |
| ws = self.sw_window |
| x0 = int(scan_c * scale) |
| y0 = int(scan_r * scale) |
| x1 = int((scan_c + ws) * scale) |
| y1 = int((scan_r + ws) * scale) |
| |
| draw.rectangle([x0, y0, x1, y1], fill=(255, 165, 0, 60), outline=(255, 165, 0, 200), width=2) |
| |
| cx = int((scan_c + ws//2) * scale) |
| cy = int((scan_r + ws//2) * scale) |
| draw.ellipse([cx-3, cy-3, cx+3, cy+3], fill=(255, 100, 100, 200)) |
|
|
| |
| for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]: |
| nr, nc = scan_r + ws//2 + dr, scan_c + ws//2 + dc |
| if 0 <= nr < IMG_SIZE and 0 <= nc < IMG_SIZE: |
| nx = int(nc * scale) |
| ny = int(nr * scale) |
| draw.line([cx, cy, nx, ny], fill=(100, 255, 200, 150), width=1) |
|
|
| tk_img = ImageTk.PhotoImage(img) |
| self.sw_canvas.delete("all") |
| self.sw_canvas.create_image(0, 0, anchor="nw", image=tk_img) |
| self.sw_canvas.image = tk_img |
|
|
| |
| self.sw_canvas.create_text(4, 4, anchor="nw", |
| text=f"SW {ws}×{ws} pos=({scan_r},{scan_c})", |
| font=("Courier", 8), fill=ORANGE) |
|
|
| def _draw_heatmap(self): |
| if self.img1_array is None: return |
| hmap = sw_scan_result(self.img1_array, self.sw_window) |
| hmap_norm = (hmap - hmap.min()) / (hmap.max() - hmap.min() + 1e-8) |
|
|
| |
| h_img = np.zeros((IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8) |
| h_img[:,:,0] = (hmap_norm * 255).astype(np.uint8) |
| h_img[:,:,1] = ((1 - hmap_norm) * 200).astype(np.uint8) |
| h_img[:,:,2] = ((1 - hmap_norm) * 255).astype(np.uint8) |
|
|
| pil_img = Image.fromarray(h_img).resize((224, 112), Image.NEAREST) |
| tk_img = ImageTk.PhotoImage(pil_img) |
| self.heat_canvas.delete("all") |
| self.heat_canvas.create_image(0, 0, anchor="nw", image=tk_img) |
| self.heat_canvas.image = tk_img |
| self.heat_canvas.create_text(4, 4, anchor="nw", |
| text=f"Relational diff (SW {self.sw_window}×{self.sw_window})", |
| font=("Courier", 7), fill=ORANGE) |
|
|
| def animate_sw(self): |
| if self.img1_array is None: return |
| self.sw_animating = not self.sw_animating |
| if self.sw_animating: |
| self._sw_animate_loop(0, 0) |
|
|
| def _sw_animate_loop(self, r, c): |
| if not self.sw_animating: return |
| ws = self.sw_window |
| stride = max(1, ws // 2) |
| self._draw_sw_overlay(r, c) |
| |
| nc = c + stride |
| nr = r |
| if nc + ws > IMG_SIZE: |
| nc = 0 |
| nr = r + stride |
| if nr + ws > IMG_SIZE: |
| nr = 0 |
| nc = 0 |
| self.root.after(80, self._sw_animate_loop, nr, nc) |
|
|
| def _update_conv(self): |
| if self.img1_array is None: return |
| layer = self.conv_var.get() |
| layer_name = f"conv{layer}" |
|
|
| |
| sizes = {2:56, 3:28, 4:28, 5:28, 6:14, 7:14, 8:14, 9:7, 10:7} |
| sz = sizes.get(layer, 14) |
|
|
| |
| fmap = _feature_maps.get(layer_name, None) |
|
|
| if fmap is not None: |
| |
| fmap_np = fmap[0].numpy() |
| n_ch = fmap_np.shape[0] |
| for i, canvas in enumerate(self.feat_canvases): |
| ch_idx = int(i * n_ch / 3) |
| ch = fmap_np[ch_idx] |
| |
| vmin, vmax = ch.min(), ch.max() |
| ch_norm = (ch - vmin) / (vmax - vmin + 1e-8) |
| |
| rgb = np.zeros((ch.shape[0], ch.shape[1], 3), dtype=np.uint8) |
| if i == 0: |
| rgb[:,:,0] = (ch_norm * 255).astype(np.uint8) |
| rgb[:,:,2] = ((1-ch_norm) * 150).astype(np.uint8) |
| elif i == 1: |
| rgb[:,:,1] = (ch_norm * 255).astype(np.uint8) |
| rgb[:,:,2] = ((1-ch_norm) * 100).astype(np.uint8) |
| else: |
| rgb[:,:,0] = (ch_norm * 150).astype(np.uint8) |
| rgb[:,:,1] = (ch_norm * 200).astype(np.uint8) |
| pil = Image.fromarray(rgb).resize((90, 90), Image.NEAREST) |
| tk_img = ImageTk.PhotoImage(pil) |
| canvas.delete("all") |
| canvas.create_image(0, 0, anchor="nw", image=tk_img) |
| canvas.image = tk_img |
| canvas.create_text(4, 4, anchor="nw", |
| text=f"Ch{ch_idx+1}/{n_ch} {layer_name} {ch.shape[0]}²", |
| font=("Courier", 6), fill=WHITE) |
| else: |
| |
| from PIL import ImageFilter |
| img = Image.fromarray(self.img1_array.astype(np.uint8)).convert("L") |
| img_small = img.resize((sz, sz), Image.BILINEAR) |
| for i, canvas in enumerate(self.feat_canvases): |
| filtered = img_small.filter(ImageFilter.GaussianBlur(radius=i+1)) |
| arr = np.array(filtered, dtype=np.float32) |
| arr = (arr - arr.min()) / (arr.max() - arr.min() + 1e-8) |
| rgb = np.zeros((sz, sz, 3), dtype=np.uint8) |
| rgb[:,:,i % 3] = (arr * 200).astype(np.uint8) |
| pil = Image.fromarray(rgb).resize((90, 90), Image.NEAREST) |
| tk_img = ImageTk.PhotoImage(pil) |
| canvas.delete("all") |
| canvas.create_image(0, 0, anchor="nw", image=tk_img) |
| canvas.image = tk_img |
| canvas.create_text(4, 4, anchor="nw", |
| text=f"simulated {layer_name} {sz}²", |
| font=("Courier", 6), fill=SUB) |
|
|
| |
| self.conv_canvas.delete("all") |
| if fmap is not None: |
| |
| avg = fmap[0].mean(dim=0).numpy() |
| avg_flat = avg.flatten() |
| avg_norm = (avg_flat - avg_flat.min()) / (avg_flat.max() - avg_flat.min() + 1e-8) |
| cw = 224 |
| for x in range(cw): |
| idx = int(x / cw * len(avg_norm)) |
| v = int(avg_norm[idx] * 255) |
| col = f"#{v:02x}{min(255,v+60):02x}{max(0,255-v):02x}" |
| self.conv_canvas.create_line(x, 0, x, 56, fill=col) |
| self.conv_canvas.create_text(4, 4, anchor="nw", |
| text=f"Conv{layer} mean activation ({sz}×{sz}, {fmap.shape[1]}ch) — REAL", |
| font=("Courier", 7), fill=TEAL) |
| else: |
| self.conv_canvas.create_text(4, 28, anchor="w", |
| text=f"Upload gambar untuk lihat feature map Conv{layer}", |
| font=("Courier", 7), fill=SUB) |
|
|
|
|
| |
| |
| |
| def _update_center(self): |
| if self.emb1 is None or self.emb2 is None: return |
| self._draw_embedding_bars() |
| self._draw_window_detail() |
| if self.mode.get() == "training": |
| self._draw_tanh_curve() |
| else: |
| self._draw_tanh_curve() |
|
|
| def _draw_embedding_bars(self): |
| """Draw full embedding as bar chart with current window highlighted""" |
| if self.emb1 is None: return |
| canvas = self.emb_canvas |
| canvas.delete("all") |
| W, H = 560, 180 |
| n = len(self.emb1) |
| bar_w = W / n |
| mid = H // 2 |
|
|
| |
| canvas.create_line(0, mid, W, mid, fill=BORDER, width=1) |
| canvas.create_text(4, 4, anchor="nw", |
| text=f"Embedding vectors ({n}D) — Biru=E1 Hijau=E2", |
| font=("Courier", 8), fill=SUB) |
|
|
| n_win = n - WINDOW_SIZE + 1 |
|
|
| for i in range(n): |
| |
| in_window = self.win_pos <= i < self.win_pos + WINDOW_SIZE |
| x0 = i * bar_w |
| x1 = x0 + bar_w - 0.5 |
|
|
| |
| v1 = float(self.emb1[i]) |
| h1 = abs(v1) * (mid - 10) |
| col1 = BLUE if not in_window else "#a5b4fc" |
| if v1 >= 0: |
| canvas.create_rectangle(x0, mid-h1, x1, mid, fill=col1, outline="") |
| else: |
| canvas.create_rectangle(x0, mid, x1, mid+h1, fill=col1, outline="") |
|
|
| |
| v2 = float(self.emb2[i]) |
| h2 = abs(v2) * (mid - 10) * 0.6 |
| col2 = GREEN if not in_window else "#6ee7b7" |
| if v2 >= 0: |
| canvas.create_rectangle(x0, mid-h2, x1, mid, fill=col2, outline="", stipple="gray25") |
| else: |
| canvas.create_rectangle(x0, mid, x1, mid+h2, fill=col2, outline="", stipple="gray25") |
|
|
| |
| wx0 = self.win_pos * bar_w |
| wx1 = (self.win_pos + WINDOW_SIZE) * bar_w |
| canvas.create_rectangle(wx0, 2, wx1, H-2, outline=ORANGE, width=2) |
| canvas.create_text(wx0+2, H-14, anchor="sw", |
| text=f"w={self.win_pos}", font=("Courier", 7), fill=ORANGE) |
|
|
| |
| n_match = sum(1 for j in range(WINDOW_SIZE) |
| if (self.emb1[self.win_pos+j] >= 0) == (self.emb2[self.win_pos+j] >= 0)) |
| self.win_info.config( |
| text=f"Window: {self.win_pos} | Position: {self.win_pos}/{n_win-1} | Match: {n_match}/{WINDOW_SIZE} ({'✓ PASS' if n_match>=THRESHOLD else '✗ FAIL'})", |
| fg=GREEN if n_match >= THRESHOLD else RED |
| ) |
|
|
| def _draw_window_detail(self): |
| """Draw detailed view of current window""" |
| canvas = self.win_canvas |
| canvas.delete("all") |
| W, H = 560, 140 |
|
|
| if self.emb1 is None: return |
| mode = self.mode.get() |
|
|
| pos = self.win_pos |
| w1 = self.emb1[pos:pos+WINDOW_SIZE] |
| w2 = self.emb2[pos:pos+WINDOW_SIZE] |
|
|
| bar_w = W / WINDOW_SIZE |
| mid = H // 2 - 10 |
|
|
| canvas.create_text(4, 4, anchor="nw", |
| text=f"Window [{pos}:{pos+WINDOW_SIZE}] — {'Training: tanh agreement' if mode=='training' else 'Metric: sign matching'}", |
| font=("Courier", 8), fill=ORANGE if mode == "training" else PURPLE) |
|
|
| for i in range(WINDOW_SIZE): |
| x0 = i * bar_w + 2 |
| x1 = x0 + bar_w - 4 |
| xc = (x0 + x1) / 2 |
|
|
| v1 = float(w1[i]) |
| v2 = float(w2[i]) |
| same_sign = (v1 >= 0) == (v2 >= 0) |
|
|
| if mode == "training": |
| |
| agree = float(tanh_agreement(v1, v2)) |
| col = self._lerp_color(RED, GREEN, agree) |
| h = agree * (mid - 5) |
| canvas.create_rectangle(x0, mid-h, x1, mid, fill=col, outline="") |
| canvas.create_text(xc, H-20, anchor="center", |
| text=f"{agree:.2f}", font=("Courier", 6), fill=col) |
| |
| if agree > 0.5: |
| canvas.create_text(xc, mid-h-10, anchor="center", |
| text="▲", font=("Courier", 8), fill=GREEN) |
| else: |
| canvas.create_text(xc, mid+8, anchor="center", |
| text="▼", font=("Courier", 8), fill=RED) |
| else: |
| |
| s1 = "+" if v1 >= 0 else "−" |
| s2 = "+" if v2 >= 0 else "−" |
| col = GREEN if same_sign else RED |
| canvas.create_rectangle(x0, 20, x1, mid, fill=col, outline="") |
| canvas.create_text(xc, 30, anchor="center", |
| text=s1, font=("Courier", 12, "bold"), fill=WHITE) |
| canvas.create_text(xc, 50, anchor="center", |
| text=s2, font=("Courier", 12, "bold"), fill=WHITE) |
| canvas.create_text(xc, mid+8, anchor="center", |
| text="✓" if same_sign else "✗", |
| font=("Courier", 10), fill=col) |
|
|
| |
| canvas.create_text(xc, H-8, anchor="center", |
| text=f"{v1:.1f}", font=("Courier", 5), fill=BLUE) |
|
|
| |
| n_match = sum(1 for j in range(WINDOW_SIZE) |
| if (w1[j] >= 0) == (w2[j] >= 0)) |
| match_w = (n_match / WINDOW_SIZE) * (W - 20) |
| canvas.create_rectangle(10, H-4, 10+match_w, H-1, |
| fill=GREEN if n_match >= THRESHOLD else RED, outline="") |
| canvas.create_text(W//2, H-3, 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): |
| """Draw tanh curve for current window""" |
| canvas = self.tanh_canvas |
| canvas.delete("all") |
| W, H = 560, 120 |
|
|
| if self.emb1 is None: return |
|
|
| pos = self.win_pos |
| w1 = self.emb1[pos:pos+WINDOW_SIZE] |
| w2 = self.emb2[pos:pos+WINDOW_SIZE] |
|
|
| |
| mid_y = H // 2 |
| canvas.create_line(0, mid_y, W, mid_y, fill=BORDER, width=1, dash=(4,2)) |
| canvas.create_line(W//2, 0, W//2, H, fill=BORDER, width=1, dash=(4,2)) |
|
|
| |
| xs = np.linspace(-3, 3, W) |
| ys_tanh = (np.tanh(xs) + 1) / 2 |
|
|
| pts_curve = [] |
| for px in range(W): |
| x_val = xs[px] |
| y_val = ys_tanh[px] |
| py = int(mid_y - y_val * (mid_y - 10)) |
| pts_curve.append((px, py)) |
|
|
| for i in range(len(pts_curve)-1): |
| canvas.create_line(pts_curve[i][0], pts_curve[i][1], |
| pts_curve[i+1][0], pts_curve[i+1][1], |
| fill=ORANGE, width=2) |
|
|
| |
| for j in range(WINDOW_SIZE): |
| v1, v2 = float(w1[j]), float(w2[j]) |
| prod = v1 * v2 * 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 - 10)) |
| same = (v1 >= 0) == (v2 >= 0) |
| col = GREEN if same else RED |
| canvas.create_oval(px-4, py-4, px+4, py+4, fill=col, outline=WHITE) |
|
|
| |
| canvas.create_text(4, 4, anchor="nw", |
| text=f"tanh(β·E1·E2) — β={BETA} | Hijau=sign cocok Merah=berbeda | {'Training: gradient dorong ke 1.0' if self.mode.get()=='training' else 'Metric: ambang batas sign'}", |
| font=("Courier", 7), fill=SUB) |
| canvas.create_text(4, H-4, anchor="sw", |
| text="prod<0 (berbeda tanda)", font=("Courier", 7), fill=RED) |
| canvas.create_text(W-4, H-4, anchor="se", |
| text="prod>0 (sama tanda)", font=("Courier", 7), fill=GREEN) |
|
|
| |
| if self.mode.get() == "training": |
| canvas.create_text(W//2, 10, anchor="center", |
| text="▲ Loss = (1-score)² → dorong agreement ke 1.0 untuk same-pair", |
| font=("Courier", 7), fill=YELLOW) |
|
|
|
|
| |
| |
| |
| def _update_right(self): |
| if self.emb1 is None: return |
| self._draw_emb_bar(self.emb1_bar, self.emb1, BLUE) |
| self._draw_emb_bar(self.emb2_bar, self.emb2, GREEN) |
| self._draw_sign_pattern() |
| self._draw_chain_pattern() |
|
|
| def _draw_emb_bar(self, canvas, emb, color): |
| canvas.delete("all") |
| W, H = 280, 40 |
| n = len(emb) |
| bw = W / n |
| mid = H // 2 |
| for i, v in enumerate(emb): |
| x0 = i * bw |
| h = abs(float(v)) * (mid - 2) |
| col = color if float(v) >= 0 else RED |
| if float(v) >= 0: |
| canvas.create_rectangle(x0, mid-h, x0+bw-0.5, mid, fill=col, outline="") |
| else: |
| canvas.create_rectangle(x0, mid, x0+bw-0.5, mid+h, fill=col, outline="") |
|
|
| def _draw_sign_pattern(self): |
| canvas = self.sign_canvas |
| canvas.delete("all") |
| if self.emb1 is None: return |
| W, H = 280, 60 |
| n = len(self.emb1) - WINDOW_SIZE + 1 |
| bw = W / n |
| scores = img_sign_score(self.emb1, self.emb2) |
| for i, s in enumerate(scores): |
| x0 = i * bw |
| col = GREEN if s >= THRESHOLD/WINDOW_SIZE else RED |
| h = s * (H - 4) |
| canvas.create_rectangle(x0, H-h, x0+bw-0.3, H, fill=col, outline="") |
| canvas.create_text(4, 4, anchor="nw", |
| text=f"Sign match score per window (thr={THRESHOLD}/{WINDOW_SIZE})", |
| font=("Courier", 6), fill=SUB) |
|
|
| def _draw_chain_pattern(self): |
| canvas = self.chain_canvas |
| canvas.delete("all") |
| if self.emb1 is None: return |
| W, H = 280, 40 |
| e1, e2 = self.emb1, self.emb2 |
| n = len(e1) - WINDOW_SIZE + 1 |
| bw = W / n |
| in_chain = False |
| for i in range(n): |
| s1 = np.where(e1[i:i+WINDOW_SIZE]>=0, 1, -1) |
| s2 = np.where(e2[i:i+WINDOW_SIZE]>=0, 1, -1) |
| match = int(np.sum(s1==s2)) >= THRESHOLD |
| x0 = i * bw |
| if match: |
| canvas.create_rectangle(x0, 8, x0+bw-0.3, H-8, fill=TEAL, outline="") |
| if not in_chain: |
| canvas.create_line(x0, 4, x0, H-4, fill=WHITE, width=1) |
| in_chain = True |
| else: |
| in_chain = False |
| canvas.create_text(4, 4, anchor="nw", |
| text="Chain pattern (hijau=match run, garis=chain start)", |
| font=("Courier", 6), fill=SUB) |
|
|
|
|
| |
| |
| |
| def _win_first(self): |
| self.win_pos = 0 |
| self._update_center() |
|
|
| def _win_next(self): |
| if self.emb1 is None: return |
| n = len(self.emb1) - WINDOW_SIZE + 1 |
| self.win_pos = min(self.win_pos + 1, n - 1) |
| self._update_center() |
|
|
| def _win_prev(self): |
| self.win_pos = max(self.win_pos - 1, 0) |
| 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: return |
| if self.emb1 is None: return |
| n = len(self.emb1) - WINDOW_SIZE + 1 |
| self.win_pos = (self.win_pos + 1) % n |
| self._update_center() |
| self.root.after(120, self._auto_loop) |
|
|
|
|
| |
| |
| |
| def _lerp_color(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}" |
|
|
| def open_ablation_window(self): |
| """Buka window ablation study terpisah""" |
| if self.img1_array is None: |
| tk.messagebox.showwarning("Warning", "Upload dulu Image 1!") |
| return |
| AblationWindow(self.root, self.img1_array, self.emb1) |
|
|
|
|
| |
| |
| |
| |
| class AblationWindow(tk.Toplevel): |
| REGIONS = { |
| "Mata Kiri" : (25, 20, 50, 55), |
| "Mata Kanan" : (25, 57, 50, 90), |
| "Hidung" : (50, 35, 75, 77), |
| "Mulut" : (75, 28, 95, 84), |
| "Dahi" : (5, 20, 28, 92), |
| "Rahang Kiri" : (75, 5, 112, 42), |
| "Rahang Kanan": (75, 70, 112, 107), |
| "Semua Mata" : (20, 15, 55, 97), |
| "Bagian Atas" : (0, 0, 56, 112), |
| "Bagian Bawah": (56, 0, 112, 112), |
| } |
| MASK_COLOR = 128 |
|
|
| def __init__(self, parent, img_array, emb_original): |
| super().__init__(parent) |
| self.title("IMGNet — Ablation Study: Occlusion Sensitivity") |
| self.geometry("1200x780") |
| self.configure(bg=BG) |
|
|
| self.img_original = img_array.copy() |
| self.emb_original = emb_original.copy() if emb_original is not None else None |
| self.selected_regs = {} |
| self.delta_cache = {} |
|
|
| self._build_ui() |
| self._precompute_all() |
|
|
| def _build_ui(self): |
| |
| tk.Label(self, text="Ablation Study · Occlusion Sensitivity Analysis", |
| font=("Courier", 13, "bold"), bg=BG, fg=PURPLE).pack(pady=(10,2)) |
| tk.Label(self, |
| text="Hapus region wajah → bandingkan embedding → lihat dimensi mana yang paling sensitif", |
| font=("Courier", 9), bg=BG, fg=SUB).pack(pady=(0,8)) |
|
|
| main = tk.Frame(self, bg=BG) |
| main.pack(fill="both", expand=True, padx=12, pady=4) |
| main.grid_columnconfigure(0, weight=1) |
| main.grid_columnconfigure(1, weight=3) |
| main.grid_rowconfigure(0, weight=1) |
|
|
| |
| left = tk.Frame(main, bg=CARD, highlightthickness=1, highlightbackground=BORDER) |
| left.grid(row=0, column=0, sticky="nsew", padx=(0,6)) |
|
|
| tk.Label(left, text="PILIH REGION OKLUASI", |
| font=("Courier", 10, "bold"), bg=CARD, fg=ORANGE).pack(pady=(10,4)) |
|
|
| |
| for name in self.REGIONS: |
| var = tk.BooleanVar(value=False) |
| self.selected_regs[name] = var |
| cb = tk.Checkbutton(left, text=name, variable=var, |
| bg=CARD, fg=TEXT, selectcolor=CARD, |
| font=("Courier", 9), |
| command=self._update_preview) |
| cb.pack(anchor="w", padx=16) |
|
|
| tk.Button(left, text="□ Clear All", |
| command=self._clear_all, |
| bg=CARD, fg=RED, font=("Courier", 8), |
| relief="flat", pady=2, cursor="hand2").pack(pady=4) |
|
|
| tk.Button(left, text="■ Select All", |
| command=self._select_all, |
| bg=CARD, fg=GREEN, font=("Courier", 8), |
| relief="flat", pady=2, cursor="hand2").pack() |
|
|
| |
| tk.Label(left, text="Original", font=("Courier", 8), bg=CARD, fg=SUB).pack(pady=(12,0)) |
| self.orig_canvas = tk.Canvas(left, width=140, height=140, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.orig_canvas.pack(padx=8) |
| self._show_img(self.img_original, self.orig_canvas, 140) |
|
|
| tk.Label(left, text="With Occlusion", font=("Courier", 8), bg=CARD, fg=ORANGE).pack(pady=(6,0)) |
| self.mask_canvas = tk.Canvas(left, width=140, height=140, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.mask_canvas.pack(padx=8, pady=(0,8)) |
|
|
| |
| self.delta_score_lbl = tk.Label(left, text="Δ score: —", |
| font=("Courier", 11, "bold"), bg=CARD, fg=YELLOW) |
| self.delta_score_lbl.pack(pady=4) |
|
|
| |
| right = tk.Frame(main, bg=CARD, highlightthickness=1, highlightbackground=BORDER) |
| right.grid(row=0, column=1, sticky="nsew") |
|
|
| tk.Label(right, text="DELTA EMBEDDING — |E_original - E_occluded| per dimensi", |
| font=("Courier", 10, "bold"), bg=CARD, fg=TEAL).pack(pady=(10,2)) |
| tk.Label(right, |
| text="Dimensi dengan delta TINGGI = sensitif terhadap region yang dihapus", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack() |
|
|
| |
| self.delta_canvas = tk.Canvas(right, width=820, height=200, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.delta_canvas.pack(padx=8, pady=4, fill="x") |
|
|
| |
| tk.Label(right, text="Smoothed Delta (window=20) — identifikasi cluster region", |
| font=("Courier", 8), bg=CARD, fg=SUB).pack() |
| self.smooth_canvas = tk.Canvas(right, width=820, height=120, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.smooth_canvas.pack(padx=8, pady=2, fill="x") |
|
|
| |
| tk.Label(right, text="Perbandingan Semua Region (overlay)", |
| font=("Courier", 9, "bold"), bg=CARD, fg=PURPLE).pack(pady=(8,2)) |
| self.overlay_canvas = tk.Canvas(right, width=820, height=160, bg="#050810", |
| highlightthickness=1, highlightbackground=BORDER) |
| self.overlay_canvas.pack(padx=8, pady=2, fill="x") |
|
|
| |
| tk.Label(right, text="Top 10 Dimensi Paling Sensitif", |
| font=("Courier", 9, "bold"), bg=CARD, fg=YELLOW).pack(pady=(8,2)) |
| self.top_dims_lbl = tk.Label(right, text="—", |
| font=("Courier", 9), bg=CARD, fg=TEXT, |
| justify="left", wraplength=800) |
| self.top_dims_lbl.pack(padx=16, pady=(0,8)) |
|
|
| |
| tk.Button(right, text="🔄 ANALYZE", |
| command=self._update_all, |
| bg=PURPLE, fg=WHITE, font=("Courier", 11, "bold"), |
| relief="flat", padx=20, pady=6, cursor="hand2").pack(pady=4) |
|
|
| def _show_img(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 _apply_mask(self, regions): |
| """Terapkan okluasi abu-abu ke region yang dipilih""" |
| masked = self.img_original.copy() |
| for name in regions: |
| r1, c1, r2, c2 = self.REGIONS[name] |
| masked[r1:r2, c1:c2] = self.MASK_COLOR |
| return masked |
|
|
| def _precompute_all(self): |
| """Precompute delta untuk semua region""" |
| if _imgnet_model is None or self.emb_original is None: return |
|
|
| def worker(): |
| for name, (r1,c1,r2,c2) in self.REGIONS.items(): |
| masked = self.img_original.copy() |
| masked[r1:r2, c1:c2] = self.MASK_COLOR |
| emb_masked = get_embedding(masked) |
| self.delta_cache[name] = np.abs(self.emb_original - emb_masked) |
| self.root.after(0, lambda: self._draw_overlay()) |
|
|
| threading.Thread(target=worker, daemon=True).start() |
|
|
| def _clear_all(self): |
| for v in self.selected_regs.values(): v.set(False) |
| self._update_preview() |
|
|
| def _select_all(self): |
| for v in self.selected_regs.values(): v.set(True) |
| self._update_preview() |
|
|
| def _update_preview(self): |
| selected = [n for n, v in self.selected_regs.items() if v.get()] |
| masked = self._apply_mask(selected) |
| |
| img = Image.fromarray(masked.astype(np.uint8)).resize((140, 140), Image.NEAREST) |
| draw = ImageDraw.Draw(img) |
| scale = 140 / 112 |
| for name in selected: |
| r1,c1,r2,c2 = self.REGIONS[name] |
| draw.rectangle([c1*scale, r1*scale, c2*scale, r2*scale], |
| outline="#f59e0b", width=2) |
| draw.text((c1*scale+2, r1*scale+2), name[:4], fill="#f59e0b") |
| tk_img = ImageTk.PhotoImage(img) |
| self.mask_canvas.delete("all") |
| self.mask_canvas.create_image(0, 0, anchor="nw", image=tk_img) |
| self.mask_canvas.image = tk_img |
|
|
| def _update_all(self): |
| selected = [n for n, v in self.selected_regs.items() if v.get()] |
| if not selected: |
| return |
| self._update_preview() |
|
|
| |
| if _imgnet_model is not None: |
| masked = self._apply_mask(selected) |
| emb_masked = get_embedding(masked) |
| delta = np.abs(self.emb_original - emb_masked) |
| self._draw_delta(delta, f"Delta: {', '.join(selected)}") |
| self._draw_smoothed(delta) |
|
|
| |
| n = len(self.emb_original) - WINDOW_SIZE + 1 |
| orig_sign = sum( |
| 1 for i in range(n) |
| if sum(1 for j in range(WINDOW_SIZE) |
| if (self.emb_original[i+j]>=0)==(emb_masked[i+j]>=0)) >= THRESHOLD |
| ) / max(n, 1) |
| self.delta_score_lbl.config( |
| text=f"IMG Sign drop: {1-orig_sign:.3f}", |
| fg=RED if (1-orig_sign) > 0.1 else YELLOW) |
|
|
| |
| top10 = np.argsort(delta)[-10:][::-1] |
| self.top_dims_lbl.config( |
| text=f"Dimensi: {list(top10)} | Delta: {[f'{delta[i]:.3f}' for i in top10]}") |
|
|
| def _draw_delta(self, delta, title="Delta"): |
| canvas = self.delta_canvas |
| canvas.delete("all") |
| W = canvas.winfo_width() or 820 |
| H = 200 |
| n = len(delta) |
| bw = W / n |
| d_max = delta.max() + 1e-8 |
|
|
| for i, d in enumerate(delta): |
| x0 = i * bw |
| h = (d / d_max) * (H - 20) |
| |
| t = d / d_max |
| r = int(255 * t) |
| b = int(255 * (1-t)) |
| col = f"#{r:02x}00{b:02x}" |
| canvas.create_rectangle(x0, H-h, x0+bw-0.3, H, fill=col, outline="") |
|
|
| |
| top5 = np.argsort(delta)[-5:] |
| for idx in top5: |
| x = idx * bw + bw/2 |
| h = (delta[idx] / d_max) * (H - 20) |
| canvas.create_oval(x-3, H-h-3, x+3, H-h+3, fill=YELLOW, outline="") |
| canvas.create_text(x, H-h-10, text=str(idx), |
| font=("Courier", 6), fill=YELLOW) |
|
|
| canvas.create_text(4, 4, anchor="nw", text=title, |
| font=("Courier", 8), fill=TEAL) |
| canvas.create_text(W-4, 4, anchor="ne", |
| text=f"max_delta={delta.max():.4f} mean={delta.mean():.4f}", |
| font=("Courier", 7), fill=SUB) |
|
|
| def _draw_smoothed(self, delta, window=20): |
| canvas = self.smooth_canvas |
| canvas.delete("all") |
| W = canvas.winfo_width() or 820 |
| H = 120 |
|
|
| |
| smoothed = np.convolve(delta, np.ones(window)/window, mode='same') |
| s_max = smoothed.max() + 1e-8 |
| n = len(smoothed) |
| bw = W / n |
|
|
| |
| pts = [(0, H)] |
| for i, s in enumerate(smoothed): |
| x = i * bw |
| y = H - (s / s_max) * (H - 10) |
| pts.append((x, y)) |
| pts.append((W, H)) |
|
|
| if len(pts) > 2: |
| canvas.create_polygon(pts, fill="#1e3a5f", outline="") |
| |
| for i in range(len(pts)-2): |
| canvas.create_line(pts[i+1][0], pts[i+1][1], |
| pts[i+2][0], pts[i+2][1], |
| fill=BLUE, width=1) |
|
|
| |
| peaks = [] |
| for i in range(1, n-1): |
| if smoothed[i] > smoothed[i-1] and smoothed[i] > smoothed[i+1]: |
| if smoothed[i] > s_max * 0.5: |
| peaks.append(i) |
|
|
| for pk in peaks[:5]: |
| x = pk * bw |
| y = H - (smoothed[pk] / s_max) * (H - 10) |
| canvas.create_oval(x-4, y-4, x+4, y+4, fill=ORANGE, outline="") |
| canvas.create_text(x, y-12, text=f"dim{pk}", |
| font=("Courier", 6), fill=ORANGE) |
|
|
| canvas.create_text(4, 4, anchor="nw", |
| text=f"Smoothed delta (window={window}) — cluster = kemungkinan region spasial di embedding", |
| font=("Courier", 7), fill=SUB) |
|
|
| def _draw_overlay(self): |
| """Overlay semua region yang sudah diprecompute""" |
| canvas = self.overlay_canvas |
| canvas.delete("all") |
| if not self.delta_cache: return |
|
|
| W = canvas.winfo_width() or 820 |
| H = 160 |
|
|
| REGION_COLORS = [ |
| BLUE, GREEN, ORANGE, RED, PURPLE, |
| TEAL, YELLOW, "#f472b6", "#34d399", "#60a5fa" |
| ] |
|
|
| names = list(self.delta_cache.keys()) |
| for idx, name in enumerate(names): |
| delta = self.delta_cache[name] |
| d_max = max(d.max() for d in self.delta_cache.values()) + 1e-8 |
| n = len(delta) |
| bw = W / n |
| col = REGION_COLORS[idx % len(REGION_COLORS)] |
|
|
| smoothed = np.convolve(delta, np.ones(15)/15, mode='same') |
| pts = [] |
| for i, s in enumerate(smoothed): |
| x = i * bw |
| y = H - 10 - (s / d_max) * (H - 20) |
| pts.append((x, y)) |
|
|
| for i in range(len(pts)-1): |
| canvas.create_line(pts[i][0], pts[i][1], |
| pts[i+1][0], pts[i+1][1], |
| fill=col, width=1) |
|
|
| |
| for idx, name in enumerate(names): |
| col = REGION_COLORS[idx % len(REGION_COLORS)] |
| x = 8 + (idx % 5) * 155 |
| y = 8 + (idx // 5) * 14 |
| canvas.create_rectangle(x, y, x+8, y+8, fill=col, outline="") |
| canvas.create_text(x+10, y, anchor="nw", |
| text=name, font=("Courier", 6), fill=col) |
|
|
| canvas.create_text(W//2, H-4, anchor="s", |
| text="Tiap warna = region berbeda · Puncak = cluster dimensi sensitif", |
| font=("Courier", 7), fill=SUB) |
|
|
|
|
| |
| |
| |
| if __name__ == "__main__": |
| root = tk.Tk() |
| app = IMGNetVisualizer(root) |
| root.mainloop() |