# imgnet_ablation_viewer.py # IMGNet Ablation + Comparison Viewer # Kiri : Upload 2 foto + score metrics (IMG Sign, AMP, Chain, Cosine) # Kanan: Compare (overlay/sign/chain/embedding) & Ablation (baca-ulang sliding window) # Ablation: bandingkan HASIL BACA ULANG sliding window (asli vs terdampak-occlusion) # → tampilkan blok/window mana saja yang berubah, bukan cuma skor akhir import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk, ImageDraw import numpy as np import threading from collections import Counter # ── CONFIG ───────────────────────────────────────────────── CKPT_PATH = r"C:\PythonProj\img_bnn\checkpoints_sw357_conv10_imgsign\SW357_conv10_imgsign\best_model_epoch39_plateau.pth" WINDOW_SIZE = 11 THRESHOLD = 8 EMB_DIM = 1024 IMG_SIZE = 112 NEUTRAL_LEN = 29 REWARD_RATE = 0.3 PUNISH_RATE = 1.0 MASK_COLOR = 128 SOFT_BETA = 5.0 # sharpness tanh agreement per-dimensi (soft sign+magnitude) SOFT_GATE_SHARPNESS= 15.0 # sharpness sigmoid gate di sekitar THRESHOLD (landai) BG= "#0a0e1a"; CARD= "#111827"; BORDER="#1e293b" BLUE= "#6366f1"; GREEN= "#10b981"; ORANGE="#f59e0b" PURPLE="#a855f7"; TEAL= "#14b8a6"; RED= "#ef4444" YELLOW="#fbbf24"; WHITE= "#ffffff"; SUB= "#64748b"; TEXT="#e2e8f0" REGION_COLS = [ "#60a5fa","#34d399","#f59e0b","#f472b6","#a78bfa", "#fb923c","#22d3ee","#e879f9","#facc15","#6ee7b7" ] REGIONS = { "Mata Kiri" : (25, 20, 50, 55), "Mata Kanan" : (25, 57, 50, 90), "Semua Mata" : (18, 12, 52, 100), "Hidung" : (50, 35, 75, 77), "Mulut" : (72, 25, 95, 87), "Dahi" : (4, 18, 27, 94), "Rahang Kiri" : (72, 3, 112, 45), "Rahang Kanan": (72, 67, 112, 109), "Bagian Atas" : (0, 0, 56, 112), "Bagian Bawah": (56, 0, 112, 112), } CUSTOM_KEY = "Custom (6 Titik)" # ── MODEL LOAD ───────────────────────────────────────────── try: import torch, torch.nn as nn, torch.nn.functional as F TORCH_OK = True except: 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: _mtcnn = None; MTCNN_OK = False _model = None; _device = "cpu" if TORCH_OK: class _SW(nn.Module): def __init__(self): super().__init__() self.fc = nn.Sequential(nn.Linear(240,64),nn.ReLU(True),nn.Linear(64,32)) def forward(self, x): B,C,H,W=x.shape; diffs=[] for ws in [3,5,7]: p=ws//2; xp=F.pad(x,[p,p,p,p],mode='reflect') pat=xp.unfold(2,ws,1).unfold(3,ws,1) d=x.unsqueeze(-1).unsqueeze(-1)-pat m=torch.ones(ws,ws,dtype=torch.bool,device=x.device); m[ws//2,ws//2]=False diffs.append(d[:,:,:,:,m]) d=torch.cat(diffs,-1); B,C,H,W,N=d.shape o=self.fc(d.permute(0,2,3,1,4).reshape(B*H*W,C*N)) return o.reshape(B,H,W,-1).permute(0,3,1,2) class _Net(nn.Module): def __init__(self): super().__init__() self.sw1=_SW(); 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))) for i in range(2,11): x=F.relu(getattr(self,f'bn{i}')(getattr(self,f'conv{i}')(x))) return self.bn(self.fc(self.gap(x).view(x.size(0),-1))) import os if os.path.exists(CKPT_PATH): try: _device="cuda" if torch.cuda.is_available() else "cpu" _model=_Net().to(_device) st=torch.load(CKPT_PATH,map_location="cpu",weights_only=False) if isinstance(st,dict) and "model" in st: st=st["model"] _model.load_state_dict(st); _model.eval() print(f"✓ IMGNet loaded device={_device}") except Exception as e: print(f"✗ {e}"); _model=None # ── UTILS ────────────────────────────────────────────────── def get_emb(arr): if _model and TORCH_OK: try: t=torch.from_numpy(arr.astype(np.float32)/255.0).permute(2,0,1).unsqueeze(0).to(_device) with torch.no_grad(): return _model(t).squeeze(0).cpu().numpy() except: pass np.random.seed(int(arr.sum())%2**31) e=np.random.randn(EMB_DIM).astype(np.float32) return e/(np.linalg.norm(e)+1e-8) def load_face(path): img=Image.open(path).convert("RGB") if MTCNN_OK and _mtcnn: try: face=_mtcnn(img) if face is not None: return np.clip(face.permute(1,2,0).numpy(),0,255).astype(np.uint8) except: pass return np.array(img.resize((IMG_SIZE,IMG_SIZE),Image.BILINEAR)) def occlude(arr, name): out=arr.copy(); r1,c1,r2,c2=REGIONS[name] out[r1:r2,c1:c2]=MASK_COLOR; return out def polygon_to_mask(points, size=IMG_SIZE): """points: list of (x,y) dalam ruang 112x112 (x=kolom, y=baris). Return boolean mask HxW — True = di dalam polygon.""" img=Image.new('L',(size,size),0) ImageDraw.Draw(img).polygon(points,fill=255) return np.array(img)>0 def occlude_mask(arr, mask): if mask is None: return arr out=arr.copy(); out[mask]=MASK_COLOR; return out def img_sign_score(e1,e2): n=len(e1)-WINDOW_SIZE+1 return sum(1 for i in range(n) if sum(1 for j in range(WINDOW_SIZE) if (e1[i+j]>=0)==(e2[i+j]>=0))>=THRESHOLD)/max(n,1) def amp_score(e1,e2): n=len(e1)-WINDOW_SIZE+1; tot=0.0 for i in range(n): w1,w2=e1[i:i+WINDOW_SIZE],e2[i:i+WINDOW_SIZE] s1=np.where(w1>=0,1,-1).astype(np.int8); s2=np.where(w2>=0,1,-1).astype(np.int8) if int(np.sum(s1==s2))>=THRESHOLD: a1,a2=np.mean(np.abs(w1)),np.mean(np.abs(w2)) tot+=max(0.0,1-abs(a1-a2)/max(a1,a2,1e-6)) return tot/max(n,1) def chain_score(e1,e2): n=len(e1)-WINDOW_SIZE+1 flags=[int(np.sum(np.where(e1[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8)== np.where(e2[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8)))>=THRESHOLD for i in range(n)] total=sum(flags); sg=total/max(n,1) nc=0; ic=False for f in flags: if f and not ic: nc+=1; ic=True elif not f: ic=False if nc==0 or total==0: return 0.0,0,0.0 ac=total/nc; diff=ac-NEUTRAL_LEN score=sg+(REWARD_RATE*diff if diff>=0 else PUNISH_RATE*diff)/100 return float(np.clip(score,0,1)),nc,ac def cosine(e1,e2): return float(np.dot(e1,e2)/(np.linalg.norm(e1)*np.linalg.norm(e2)+1e-8)) def lerp_color(t, c1="#ef4444", c2="#10b981"): 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}" # ── ABLATION: BACA ULANG SLIDING WINDOW ──────────────────── def window_sign_match(e1, e2): """Baca ulang tiap sliding window (window_size=11, threshold=8) antara e1 dan e2. Return array boolean per window: True = window MASIH cocok (>=THRESHOLD bit searah), False = window ini BERUBAH akibat oklusi. e1 = embedding asli, e2 = embedding foto yang sudah di-occlude.""" n=len(e1)-WINDOW_SIZE+1 out=np.zeros(n,dtype=bool) for i in range(n): s1=np.where(e1[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8) s2=np.where(e2[i:i+WINDOW_SIZE]>=0,1,-1).astype(np.int8) out[i]=int(np.sum(s1==s2))>=THRESHOLD return out def soft_window_match(e1, e2, beta=SOFT_BETA, gate_sharpness=SOFT_GATE_SHARPNESS): """Versi soft/differentiable dari window_sign_match + amp ratio (numpy port, tanpa torch), berdasarkan: agreement = (tanh(beta * E1 * E2) + 1) / 2 # per-dimensi soft_match = sliding-window-sum(agreement) # analog count 0..WINDOW_SIZE gate = sigmoid(gate_sharpness*(soft_match - THRESHOLD + 0.5)) amp1/amp2 = sliding-window-mean(|E1|), sliding-window-mean(|E2|) Tidak ada cliff seperti window_sign_match: 'agreement' juga mempertimbangkan magnitude, dan 'gate' transisi mulus di sekitar THRESHOLD alih-alih lompat. Return (gate, amp_ratio, combined) — semua array sepanjang n_win, TANPA di-gating oleh hasil hard-threshold (beda dari window_amp_sim).""" def window_sum(arr, w): c=np.cumsum(np.insert(arr,0,0.0)) return c[w:]-c[:-w] prod=e1.astype(np.float64)*e2.astype(np.float64) agreement=(np.tanh(beta*prod)+1)/2 soft_match=window_sum(agreement, WINDOW_SIZE) gate=1.0/(1.0+np.exp(-gate_sharpness*(soft_match-THRESHOLD+0.5))) amp1=window_sum(np.abs(e1.astype(np.float64)), WINDOW_SIZE)/WINDOW_SIZE amp2=window_sum(np.abs(e2.astype(np.float64)), WINDOW_SIZE)/WINDOW_SIZE amp_ratio=np.clip(1.0-np.abs(amp1-amp2)/np.maximum(np.maximum(amp1,amp2),1e-6),0.0,1.0) combined=gate*amp_ratio return gate, amp_ratio, combined def changed_segments(blocks): """Kelompokkan window yang BERUBAH (False) menjadi segmen kontinu. Return list of (start_idx, length) — dipakai untuk breakdown, bukan cuma angka akhir.""" segs=[]; i=0; n=len(blocks) while i",_on_lf_configure) def _on_canvas_configure(event): lf_canvas.itemconfig(lf_window,width=event.width) lf_canvas.bind("",_on_canvas_configure) def _on_mousewheel(event): lf_canvas.yview_scroll(int(-1*(event.delta/120)),"units") def _bind_wheel(_): lf_canvas.bind_all("",_on_mousewheel) def _unbind_wheel(_): lf_canvas.unbind_all("") lf_canvas.bind("",_bind_wheel) lf_canvas.bind("",_unbind_wheel) # Upload buttons tk.Label(lf,text="FOTO",font=("Courier",10,"bold"),bg=CARD,fg=BLUE).pack(pady=(8,4)) btn_f=tk.Frame(lf,bg=CARD); btn_f.pack(fill="x",padx=8) tk.Button(btn_f,text="Upload Foto 1",command=self.upload1, bg=BLUE,fg=WHITE,font=("Courier",9,"bold"), relief="flat",pady=4,cursor="hand2").pack(side="left",expand=True,fill="x",padx=2) tk.Button(btn_f,text="Upload Foto 2",command=self.upload2, bg=GREEN,fg=WHITE,font=("Courier",9,"bold"), relief="flat",pady=4,cursor="hand2").pack(side="left",expand=True,fill="x",padx=2) # Preview row prev=tk.Frame(lf,bg=CARD); prev.pack(fill="x",padx=6,pady=4) prev.grid_columnconfigure(0,weight=1); prev.grid_columnconfigure(1,weight=1) for col,label,color,attr in [(0,"Foto 1",BLUE,"c1"),(1,"Foto 2",GREEN,"c2")]: f=tk.Frame(prev,bg=CARD); f.grid(row=0,column=col,padx=2) tk.Label(f,text=label,font=("Courier",8,"bold"),bg=CARD,fg=color).pack() c=tk.Canvas(f,width=120,height=120,bg="#050810", highlightthickness=1,highlightbackground=BORDER); c.pack() setattr(self,attr,c) # Mode tabs tk.Label(lf,text="MODE",font=("Courier",9,"bold"),bg=CARD,fg=SUB).pack(pady=(6,2)) mf=tk.Frame(lf,bg=CARD); mf.pack(fill="x",padx=8) for val,label,col in [("compare","COMPARE",TEAL),("ablation","ABLATION",PURPLE)]: tk.Radiobutton(mf,text=label,variable=self.mode,value=val, bg=CARD,fg=col,selectcolor=CARD, font=("Courier",9,"bold"), command=self._refresh).pack(side="left",padx=6) # Indikator region aktif — selalu kelihatan di atas (nggak ikut ke-scroll) # biar nggak ambigu region mana yang benar-benar dipilih. self.region_active_lbl=tk.Label(lf,text="Region aktif: —", font=("Courier",8,"bold"),bg="#0a0e1a",fg=YELLOW, anchor="w",padx=6,pady=3) self.region_active_lbl.pack(fill="x",padx=8,pady=(2,4)) # Metrics (compare mode) self.metric_f=tk.Frame(lf,bg=CARD); self.metric_f.pack(fill="x",padx=8,pady=4) self.m_sign =self._mbox(self.metric_f,"IMG SIGN",GREEN) self.m_amp =self._mbox(self.metric_f,"AMP IMG", ORANGE) self.m_chain =self._mbox(self.metric_f,"CHAIN", TEAL) self.m_cos =self._mbox(self.metric_f,"COSINE", PURPLE) # Verdict self.verdict_lbl=tk.Label(lf,text="—", font=("Courier",18,"bold"),bg=CARD,fg=SUB, pady=6,highlightthickness=2,highlightbackground=BORDER) self.verdict_lbl.pack(fill="x",padx=8,pady=4) # Chain detail self.chain_lbl=tk.Label(lf,text="",font=("Courier",8),bg=CARD,fg=SUB, justify="left",wraplength=250) self.chain_lbl.pack(padx=10) # Ablation region selector tk.Label(lf,text="REGION OKLUASI",font=("Courier",9,"bold"), bg=CARD,fg=ORANGE).pack(pady=(10,2)) for i,name in enumerate(REGIONS): col=REGION_COLS[i%len(REGION_COLS)] tk.Radiobutton(lf,text=name,variable=self.sel,value=name, bg=CARD,fg=TEXT,selectcolor=CARD, font=("Courier",9), command=self._on_region).pack(anchor="w",padx=16,pady=1) tk.Radiobutton(lf,text=CUSTOM_KEY,variable=self.sel,value=CUSTOM_KEY, bg=CARD,fg=PURPLE,selectcolor=CARD, font=("Courier",9,"bold"), command=self._on_region).pack(anchor="w",padx=16,pady=(4,1)) # ── Ablation display toggles ──────────────────────── tk.Label(lf,text="TAMPILAN BACA-ULANG WINDOW",font=("Courier",9,"bold"), bg=CARD,fg=YELLOW).pack(pady=(10,2)) self.v_show_asli = tk.BooleanVar(value=True) # garis biru = embedding asli self.v_show_terdampak = tk.BooleanVar(value=True) # garis merah = embedding ter-occlude self.v_show_blocks = tk.BooleanVar(value=True) # bar blok hijau/merah per window self.v_show_delta = tk.BooleanVar(value=True) # sinyal delta besar (spike) tk.Checkbutton(lf,text="Garis Asli (Biru)",variable=self.v_show_asli, bg=CARD,fg=BLUE,selectcolor=CARD,activebackground=CARD, activeforeground=BLUE,font=("Courier",9,"bold"), command=self._on_region).pack(anchor="w",padx=16,pady=1) tk.Checkbutton(lf,text="Garis Terdampak (Merah)",variable=self.v_show_terdampak, bg=CARD,fg=RED,selectcolor=CARD,activebackground=CARD, activeforeground=RED,font=("Courier",9,"bold"), command=self._on_region).pack(anchor="w",padx=16,pady=1) tk.Checkbutton(lf,text="Bar Blok Berubah (Hijau/Merah)",variable=self.v_show_blocks, bg=CARD,fg=YELLOW,selectcolor=CARD,activebackground=CARD, activeforeground=YELLOW,font=("Courier",9,"bold"), command=self._on_region).pack(anchor="w",padx=16,pady=1) tk.Checkbutton(lf,text="Tandai Delta Besar (Spike)",variable=self.v_show_delta, bg=CARD,fg=ORANGE,selectcolor=CARD,activebackground=CARD, activeforeground=ORANGE,font=("Courier",9,"bold"), command=self._on_region).pack(anchor="w",padx=16,pady=1) # Ablation breakdown (bukan cuma skor akhir) self.abl_lbl=tk.Label(lf,text="",font=("Courier",8),bg=CARD,fg=TEXT, justify="left",wraplength=250) self.abl_lbl.pack(padx=10,pady=6) def _mbox(self,parent,label,color): f=tk.Frame(parent,bg="#0a0e1a",highlightthickness=1,highlightbackground=BORDER) f.pack(side="left",expand=True,fill="both",padx=2,pady=2) tk.Label(f,text=label,font=("Courier",6,"bold"),bg="#0a0e1a",fg=color).pack(pady=(4,0)) lbl=tk.Label(f,text="—",font=("Courier",13,"bold"),bg="#0a0e1a",fg=color) lbl.pack(pady=(0,4)); return lbl def _build_right(self,parent): rf=tk.Frame(parent,bg=CARD,highlightthickness=1,highlightbackground=BORDER) rf.grid(row=0,column=1,sticky="nsew") # ── COMPARE MODE canvases ────────────────────────── self.cmp_frame=tk.Frame(rf,bg=CARD); self.cmp_frame.pack(fill="both",expand=True) tk.Label(self.cmp_frame, text="Embedding: Biru=Foto1 Hijau=Foto2 — window aktif di-highlight", font=("Courier",9,"bold"),bg=CARD,fg=TEAL).pack(pady=(6,1)) self.c_emb=tk.Canvas(self.cmp_frame,bg="#050810",height=180, highlightthickness=1,highlightbackground=BORDER) self.c_emb.pack(fill="x",padx=8,pady=2) tk.Label(self.cmp_frame, text="Sign match per window (hijau=match ≥8/11, merah=tidak)", font=("Courier",9,"bold"),bg=CARD,fg=GREEN).pack(pady=(4,1)) self.c_sign=tk.Canvas(self.cmp_frame,bg="#050810",height=80, highlightthickness=1,highlightbackground=BORDER) self.c_sign.pack(fill="x",padx=8,pady=2) tk.Label(self.cmp_frame, text="Chain pattern (rantai match kontinu — panjang rantai = kualitas kecocokan)", font=("Courier",9,"bold"),bg=CARD,fg=TEAL).pack(pady=(4,1)) self.c_chain=tk.Canvas(self.cmp_frame,bg="#050810",height=70, highlightthickness=1,highlightbackground=BORDER) self.c_chain.pack(fill="x",padx=8,pady=2) # Embedding bars foto 1 dan 2 emb_row=tk.Frame(self.cmp_frame,bg=CARD) emb_row.pack(fill="x",padx=8,pady=(4,2)) emb_row.grid_columnconfigure(0,weight=1) emb_row.grid_columnconfigure(1,weight=1) lf1=tk.Frame(emb_row,bg=CARD); lf1.grid(row=0,column=0,sticky="ew",padx=(0,4)) tk.Label(lf1,text="Embedding Foto 1 (1024D)",font=("Courier",8,"bold"), bg=CARD,fg=BLUE).pack() self.c_emb1=tk.Canvas(lf1,bg="#050810",height=60, highlightthickness=1,highlightbackground=BORDER) self.c_emb1.pack(fill="x") lf2=tk.Frame(emb_row,bg=CARD); lf2.grid(row=0,column=1,sticky="ew",padx=(4,0)) tk.Label(lf2,text="Embedding Foto 2 (1024D)",font=("Courier",8,"bold"), bg=CARD,fg=GREEN).pack() self.c_emb2=tk.Canvas(lf2,bg="#050810",height=60, highlightthickness=1,highlightbackground=BORDER) self.c_emb2.pack(fill="x") # ── ABLATION MODE canvases ───────────────────────── self.abl_frame=tk.Frame(rf,bg=CARD); # tidak di-pack dulu # Kanvas gambar polygon custom (6 titik) — hanya di-pack saat # region terpilih == CUSTOM_KEY, disisipkan di atas panel hasil self.custom_draw_frame=tk.Frame(self.abl_frame,bg=CARD) tk.Label(self.custom_draw_frame, text="OKLUSI CUSTOM — klik 6 titik di foto (urut, otomatis nutup jadi polygon). " "Foto1 & Foto2 digambar TERPISAH — pilih tab di bawah.", font=("Courier",9,"bold"),bg=CARD,fg=PURPLE).pack(pady=(6,2)) toggle_row=tk.Frame(self.custom_draw_frame,bg=CARD); toggle_row.pack(pady=(0,4)) self.btn_custom_p1=tk.Button(toggle_row,text="✎ Gambar Foto 1", command=lambda:self._switch_custom_photo(1), font=("Courier",9,"bold"),relief="flat",padx=10,cursor="hand2") self.btn_custom_p1.pack(side="left",padx=4) self.btn_custom_p2=tk.Button(toggle_row,text="✎ Gambar Foto 2", command=lambda:self._switch_custom_photo(2), font=("Courier",9,"bold"),relief="flat",padx=10,cursor="hand2") self.btn_custom_p2.pack(side="left",padx=4) draw_row=tk.Frame(self.custom_draw_frame,bg=CARD); draw_row.pack() self.custom_scale=3 cs=IMG_SIZE*self.custom_scale self.c_custom_draw=tk.Canvas(draw_row,width=cs,height=cs,bg="#050810", highlightthickness=1,highlightbackground=BORDER, cursor="crosshair") self.c_custom_draw.pack(padx=4,pady=2) self.c_custom_draw.bind("", self._on_custom_click) btn_row=tk.Frame(self.custom_draw_frame,bg=CARD); btn_row.pack(pady=(2,6)) tk.Button(btn_row,text="Reset Titik",command=self._reset_custom_points, bg=RED,fg=WHITE,font=("Courier",9,"bold"),relief="flat", padx=8,cursor="hand2").pack(side="left",padx=4) self.btn_apply_custom=tk.Button(btn_row,text="Terapkan Oklusi", command=self._apply_custom_occlusion, bg=PURPLE,fg=WHITE,font=("Courier",9,"bold"),relief="flat", padx=8,cursor="hand2",state="disabled") self.btn_apply_custom.pack(side="left",padx=4) self.custom_status_lbl=tk.Label(btn_row,text="0/6 titik", font=("Courier",9,"bold"),bg=CARD,fg=SUB) self.custom_status_lbl.pack(side="left",padx=8) self._update_custom_toggle_style() self.abl_d1_title=tk.Label(self.abl_frame, text="Foto1 — Baca Ulang Sliding Window: Asli vs Terdampak", font=("Courier",9,"bold"),bg=CARD,fg=WHITE) self.abl_d1_title.pack(pady=(6,1)) self.c_d1=tk.Canvas(self.abl_frame,bg="#050810",height=90, highlightthickness=1,highlightbackground=BORDER) self.c_d1.pack(fill="x",padx=8,pady=2) self.c_blk1=tk.Canvas(self.abl_frame,bg="#050810",height=50, highlightthickness=1,highlightbackground=BORDER) self.c_blk1.pack(fill="x",padx=8,pady=(0,6)) self.abl_d2_title=tk.Label(self.abl_frame, text="Foto2 — Baca Ulang Sliding Window: Asli vs Terdampak", font=("Courier",9,"bold"),bg=CARD,fg=WHITE) self.abl_d2_title.pack(pady=(4,1)) self.c_d2=tk.Canvas(self.abl_frame,bg="#050810",height=90, highlightthickness=1,highlightbackground=BORDER) self.c_d2.pack(fill="x",padx=8,pady=2) self.c_blk2=tk.Canvas(self.abl_frame,bg="#050810",height=50, highlightthickness=1,highlightbackground=BORDER) self.c_blk2.pack(fill="x",padx=8,pady=(0,6)) tk.Label(self.abl_frame, text="Perbandingan region — % window yang berubah kalau region ini di-occlude (Biru=Foto1 Hijau=Foto2)", font=("Courier",9,"bold"),bg=CARD,fg=WHITE).pack(pady=(4,1)) self.c_multi=tk.Canvas(self.abl_frame,bg="#050810",height=150, highlightthickness=1,highlightbackground=BORDER) self.c_multi.pack(fill="x",padx=8,pady=(2,6)) tk.Label(self.abl_frame, text="Konsistensi blok berubah: Foto1 vs Foto2 pada region terpilih — blok merah di lokasi SAMA = region ini konsisten di-encode walau beda orang", font=("Courier",9,"bold"),bg=CARD,fg=WHITE).pack(pady=(4,1)) self.c_consist=tk.Canvas(self.abl_frame,bg="#050810",height=110, highlightthickness=1,highlightbackground=BORDER) self.c_consist.pack(fill="x",padx=8,pady=(2,6)) # Bind resize agar grafik re-draw saat ukuran berubah for cv in [self.c_emb,self.c_sign,self.c_chain, self.c_emb1,self.c_emb2, self.c_d1,self.c_d2,self.c_blk1,self.c_blk2, self.c_multi,self.c_consist]: cv.bind("", lambda e: self.after(50, self._redraw_all)) # Default: show compare self.cmp_frame.pack(fill="both",expand=True) # ── UPLOAD ──────────────────────────────────────────── def _redraw_all(self): """Re-draw semua grafik yang aktif setelah window resize""" if self.mode.get()=="compare": self._draw_compare() else: self._on_region() self._draw_multi() def _get_w(self, canvas, fallback=1200): """Get actual canvas width after rendering""" w = canvas.winfo_width() return w if w > 10 else fallback def upload1(self): path=filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")]) if not path: return self.arr1=load_face(path); self.e1=get_emb(self.arr1) self._show(self.arr1,self.c1,120) self.dc1={} threading.Thread(target=self._precompute,args=(1,),daemon=True).start() self._refresh() def upload2(self): path=filedialog.askopenfilename(filetypes=[("Image","*.jpg *.jpeg *.png *.bmp")]) if not path: return self.arr2=load_face(path); self.e2=get_emb(self.arr2) self._show(self.arr2,self.c2,120) self.dc2={} threading.Thread(target=self._precompute,args=(2,),daemon=True).start() self._refresh() def _show(self,arr,canvas,size,region=None): img=Image.fromarray(arr.astype(np.uint8)).resize((size,size),Image.NEAREST) if region: draw=ImageDraw.Draw(img); r1,c1,r2,c2=REGIONS[region] sc=size/IMG_SIZE draw.rectangle([c1*sc,r1*sc,c2*sc,r2*sc],outline=ORANGE,width=2) tk_img=ImageTk.PhotoImage(img) canvas.delete("all"); canvas.create_image(0,0,anchor="nw",image=tk_img) canvas.image=tk_img def _precompute(self,which): """Untuk tiap region: occlude foto, hitung ulang embedding, lalu BACA ULANG sliding window (window_sign_match) antara embedding asli vs embedding ter-occlude. Hasilnya array boolean per window (blocks), bukan kurva delta.""" arr=self.arr1 if which==1 else self.arr2 emb=self.e1 if which==1 else self.e2 dc =self.dc1 if which==1 else self.dc2 for name in REGIONS: occ=occlude(arr,name); eo=get_emb(occ) blocks=window_sign_match(emb,eo) # True=window tetap, False=window berubah dc[name]={"emb":eo,"blocks":blocks} if self.custom_mask.get(which) is not None: self._compute_custom_for(which) self.after(0,self._draw_multi) self.after(0,self._on_region) # ── REFRESH MODE ───────────────────────────────────── def _refresh(self): mode=self.mode.get() if mode=="compare": self.abl_frame.pack_forget() self.cmp_frame.pack(fill="both",expand=True) self._draw_compare() else: self.cmp_frame.pack_forget() self.abl_frame.pack(fill="both",expand=True) self._on_region() # ── COMPARE DRAWS ───────────────────────────────────── def _draw_compare(self): if self.e1 is None or self.e2 is None: return e1,e2=self.e1,self.e2 # Scores sg=img_sign_score(e1,e2); ap=amp_score(e1,e2) cs,nc,ac=chain_score(e1,e2); co=cosine(e1,e2) self.m_sign.config(text=f"{sg:.3f}") self.m_amp.config(text=f"{ap:.3f}") self.m_chain.config(text=f"{cs:.3f}") self.m_cos.config(text=f"{co:.3f}") self.chain_lbl.config( text=f"Chains: {nc} AvgLen: {ac:.1f} (neutral={NEUTRAL_LEN})") thr=0.79; npass=sum([sg>=thr,ap>=thr,cs>=thr]) if npass>=2: self.verdict_lbl.config(text="✅ MATCH",fg=WHITE,bg="#064e3b", highlightbackground=GREEN) elif npass==1: self.verdict_lbl.config(text="⚠️ UNCERTAIN",fg=WHITE,bg="#78350f", highlightbackground=ORANGE) else: self.verdict_lbl.config(text="❌ DIFFERENT",fg=WHITE,bg="#450a0a", highlightbackground=RED) # Embedding bar self._draw_emb_bars() self._draw_sign_pattern() self._draw_chain_pattern() self._draw_emb_single(self.c_emb1,self.e1,BLUE,"Foto 1") self._draw_emb_single(self.c_emb2,self.e2,GREEN,"Foto 2") def _draw_emb_bars(self): c=self.c_emb; c.delete("all") W=self._get_w(c); H=180 n=len(self.e1); bw=W/n; mid=H//2 c.create_line(0,mid,W,mid,fill=BORDER,width=1,dash=(3,2)) for i in range(n): x0=i*bw; x1=x0+bw-0.3 v1=float(self.e1[i]); h1=abs(v1)*(mid-4) if v1>=0: c.create_rectangle(x0,mid-h1,x1,mid,fill=BLUE,outline="") else: c.create_rectangle(x0,mid,x1,mid+h1,fill=BLUE,outline="") v2=float(self.e2[i]); h2=abs(v2)*(mid-4)*0.7 if v2>=0: c.create_rectangle(x0,mid-h2,x1,mid,fill=GREEN,outline="",stipple="gray25") else: c.create_rectangle(x0,mid,x1,mid+h2,fill=GREEN,outline="",stipple="gray25") c.create_text(4,4,anchor="nw", text="Biru=Foto1 Hijau=Foto2 (stipple) — dimensi yang searah = sign cocok", font=("Courier",7),fill=SUB) def _draw_emb_single(self,canvas,emb,color,label): """Grafik embedding individual (1024D) untuk satu foto — dipakai di kedua canvas bawah.""" canvas.delete("all") if emb is None: W=self._get_w(canvas); H=60 canvas.create_text(W//2,H//2,text=f"Upload {label}...", font=("Courier",9),fill=SUB) return W=self._get_w(canvas); H=60 n=len(emb); bw=W/n; mid=H//2 canvas.create_line(0,mid,W,mid,fill=BORDER,width=1,dash=(3,2)) for i in range(n): x0=i*bw; x1=x0+bw-0.3 v=float(emb[i]); h=abs(v)*(mid-3) if v>=0: canvas.create_rectangle(x0,mid-h,x1,mid,fill=color,outline="") else: canvas.create_rectangle(x0,mid,x1,mid+h,fill=color,outline="") canvas.create_text(4,2,anchor="nw", text=f"{label} min={emb.min():.2f} max={emb.max():.2f} mean={emb.mean():.3f}", font=("Courier",7),fill=SUB) def _draw_sign_pattern(self): c=self.c_sign; c.delete("all") W=self._get_w(c); H=80 e1,e2=self.e1,self.e2; n=len(e1)-WINDOW_SIZE+1; bw=W/n for i in range(n): mc=sum(1 for j in range(WINDOW_SIZE) if (e1[i+j]>=0)==(e2[i+j]>=0)) ratio=mc/WINDOW_SIZE; h=ratio*(H-4) col=GREEN if mc>=THRESHOLD else RED x0=i*bw c.create_rectangle(x0,H-h,x0+bw-0.3,H,fill=col,outline="") c.create_line(0,H-(THRESHOLD/WINDOW_SIZE)*(H-4), W,H-(THRESHOLD/WINDOW_SIZE)*(H-4), fill=YELLOW,width=1,dash=(4,2)) c.create_text(4,4,anchor="nw", text=f"Sign match ratio per window (thr={THRESHOLD}/{WINDOW_SIZE} = garis kuning)", font=("Courier",7),fill=SUB) def _draw_chain_pattern(self): c=self.c_chain; c.delete("all") W=self._get_w(c); H=70 e1,e2=self.e1,self.e2; n=len(e1)-WINDOW_SIZE+1; bw=W/n in_chain=False; chain_start=0; chain_num=0 for i in range(n): mc=sum(1 for j in range(WINDOW_SIZE) if (e1[i+j]>=0)==(e2[i+j]>=0)) match=mc>=THRESHOLD; x0=i*bw if match: c.create_rectangle(x0,10,x0+bw-0.3,H-10,fill=TEAL,outline="") if not in_chain: chain_start=i; in_chain=True; chain_num+=1 c.create_line(x0,4,x0,H-4,fill=WHITE,width=1) c.create_text(x0+1,6,anchor="nw", text=str(chain_num),font=("Courier",6),fill=WHITE) else: in_chain=False c.create_text(4,4,anchor="nw", text=f"Chain pattern — teal=match, garis putih=awal chain baru, angka=nomor chain", font=("Courier",7),fill=SUB) # ── ABLATION: BACA ULANG SLIDING WINDOW ─────────────── def _switch_custom_photo(self, which): self.custom_active_photo=which self._update_custom_toggle_style() self._draw_custom_canvas_bg() def _update_custom_toggle_style(self): active=self.custom_active_photo p1_done=self.custom_mask.get(1) is not None p2_done=self.custom_mask.get(2) is not None self.btn_custom_p1.config( bg=(BLUE if active==1 else CARD), fg=WHITE if active==1 else BLUE, relief=("sunken" if active==1 else "flat"), text=("✎ Gambar Foto 1 ✓" if p1_done else "✎ Gambar Foto 1")) self.btn_custom_p2.config( bg=(GREEN if active==2 else CARD), fg=WHITE if active==2 else GREEN, relief=("sunken" if active==2 else "flat"), text=("✎ Gambar Foto 2 ✓" if p2_done else "✎ Gambar Foto 2")) def _draw_custom_canvas_bg(self): """Gambar ulang background (foto yg lagi aktif di-edit) di kanvas gambar polygon.""" self.c_custom_draw.delete("all") cs=IMG_SIZE*self.custom_scale which=self.custom_active_photo arr=self.arr1 if which==1 else self.arr2 if arr is not None: img=Image.fromarray(arr.astype(np.uint8)).resize((cs,cs),Image.NEAREST) tkimg=ImageTk.PhotoImage(img) self.c_custom_draw.create_image(0,0,anchor="nw",image=tkimg) self.c_custom_draw.image=tkimg else: self.c_custom_draw.create_text(cs//2,cs//2,text=f"Upload Foto {which} dulu", font=("Courier",10),fill=SUB) self._redraw_custom_points() def _redraw_custom_points(self): self.c_custom_draw.delete("pt") sc=self.custom_scale which=self.custom_active_photo pts_orig=self.custom_points[which] pts=[(x*sc,y*sc) for (x,y) in pts_orig] col=BLUE if which==1 else GREEN for i,(x,y) in enumerate(pts): self.c_custom_draw.create_oval(x-4,y-4,x+4,y+4,fill=col,outline=WHITE,tags="pt") self.c_custom_draw.create_text(x+8,y-8,text=str(i+1),fill=WHITE, font=("Courier",8,"bold"),tags="pt") for i in range(len(pts)-1): x0,y0=pts[i]; x1,y1=pts[i+1] self.c_custom_draw.create_line(x0,y0,x1,y1,fill=col,width=2,tags="pt") if len(pts)==6: x0,y0=pts[0]; x5,y5=pts[5] self.c_custom_draw.create_line(x5,y5,x0,y0,fill=col,width=2,dash=(4,2),tags="pt") self.custom_status_lbl.config( text=f"Foto{which}: {len(pts_orig)}/6 titik") self.btn_apply_custom.config(state=("normal" if len(pts_orig)==6 else "disabled")) def _on_custom_click(self, event): which=self.custom_active_photo if len(self.custom_points[which])>=6: return sc=self.custom_scale ox=min(max(event.x/sc,0),IMG_SIZE-1) oy=min(max(event.y/sc,0),IMG_SIZE-1) self.custom_points[which].append((ox,oy)) self._redraw_custom_points() def _reset_custom_points(self): which=self.custom_active_photo self.custom_points[which]=[] self._redraw_custom_points() def _compute_custom_for(self, which): """Hitung ulang embedding+blocks utk region CUSTOM_KEY satu foto, pakai self.custom_mask[which] milik foto itu sendiri (independen dari foto satunya). Aman dipanggil dari thread background (cuma nulis ke dict, tidak sentuh widget).""" mask=self.custom_mask.get(which) if mask is None: return arr=self.arr1 if which==1 else self.arr2 emb=self.e1 if which==1 else self.e2 dc =self.dc1 if which==1 else self.dc2 if arr is None or emb is None: return occ=occlude_mask(arr,mask) eo=get_emb(occ) dc[CUSTOM_KEY]={"emb":eo,"blocks":window_sign_match(emb,eo)} def _apply_custom_occlusion(self): which=self.custom_active_photo pts=self.custom_points[which] if len(pts)!=6: return self.custom_mask[which]=polygon_to_mask(pts) self._compute_custom_for(which) self._update_custom_toggle_style() self._draw_multi() self._on_region() def _on_region(self,*_): if self.mode.get()!="ablation": return name=self.sel.get() self.region_active_lbl.config(text=f"Region aktif: {name}") if name==CUSTOM_KEY: self.custom_draw_frame.pack(fill="x",padx=8,pady=(2,4),before=self.abl_d1_title) self._draw_custom_canvas_bg() if self.arr1 is not None: self._show(occlude_mask(self.arr1,self.custom_mask.get(1)),self.c1,120) if self.arr2 is not None: self._show(occlude_mask(self.arr2,self.custom_mask.get(2)),self.c2,120) else: self.custom_draw_frame.pack_forget() if self.arr1 is not None: self._show(occlude(self.arr1,name),self.c1,120,region=name) if self.arr2 is not None: self._show(occlude(self.arr2,name),self.c2,120,region=name) self._draw_window_reread(name) self._draw_consistency(name) def _draw_window_reread(self,name): """Untuk tiap foto: gambar embedding asli vs terdampak (garis), lalu di bawahnya bar blok per sliding window (hijau=window tetap, merah=window berubah). Breakdown ditampilkan (jumlah window berubah, jumlah segmen, rata-rata panjang segmen) — bukan cuma satu angka skor akhir.""" show_asli = self.v_show_asli.get() show_terdampak = self.v_show_terdampak.get() show_blocks = self.v_show_blocks.get() show_delta = self.v_show_delta.get() pairs = [ (self.c_d1, self.c_blk1, self.dc1, self.e1, self.abl_d1_title, 1), (self.c_d2, self.c_blk2, self.dc2, self.e2, self.abl_d2_title, 2), ] for line_canvas, blk_canvas, dc, orig_emb, title_lbl, which in pairs: line_canvas.delete("all"); blk_canvas.delete("all") Wl=self._get_w(line_canvas); Hl=90 Wb=self._get_w(blk_canvas) if name not in dc or orig_emb is None: msg=("Gambar 6 titik & klik Terapkan Oklusi dulu" if name==CUSTOM_KEY else f"Menghitung Foto{which}...") line_canvas.create_text(Wl//2,Hl//2,text=msg, font=("Courier",9),fill=SUB) continue occ_emb = dc[name]["emb"] blocks = dc[name]["blocks"] # True=tetap, False=berubah n_dim = len(orig_emb) n_win = len(blocks) # ── Hitung delta per-dimensi & spike besar ── delta = orig_emb.astype(np.float64) - occ_emb.astype(np.float64) abs_delta = np.abs(delta) mu, sd = float(abs_delta.mean()), float(abs_delta.std()) spike_thr = mu + 1.5*sd if sd > 1e-9 else abs_delta.max()+1 spike_idx = np.where(abs_delta >= spike_thr)[0] d_max = float(abs_delta.max()) if abs_delta.size else 1e-8 # ── Baris atas: nilai embedding asli vs terdampak, full 1024D ── mid=Hl/2 g_max=max(float(np.abs(orig_emb).max()),float(np.abs(occ_emb).max()))+1e-8 bw_d=Wl/n_dim # spike bands digambar dulu (di belakang garis) — makin tinggi delta # makin pekat oranye, tanda arah: naik keatas jika asli>terdampak, # kebawah jika terdampak>asli if show_delta: for i in spike_idx: x0=i*bw_d t=min(1.0,(abs_delta[i]-spike_thr)/max(d_max-spike_thr,1e-8)) band_col = lerp_color(0.15+0.85*t, c1="#3a2a00", c2=YELLOW) line_canvas.create_rectangle(x0,4,x0+bw_d+0.6,Hl-4, fill=band_col,outline="") line_canvas.create_line(0,mid,Wl,mid,fill=BORDER,width=1,dash=(3,2)) if show_asli: pts=[(i*bw_d, mid-(orig_emb[i]/g_max)*(mid-4)) for i in range(n_dim)] for i in range(len(pts)-1): line_canvas.create_line(pts[i][0],pts[i][1],pts[i+1][0],pts[i+1][1], fill=BLUE,width=1) if show_terdampak: pts=[(i*bw_d, mid-(occ_emb[i]/g_max)*(mid-4)) for i in range(n_dim)] for i in range(len(pts)-1): line_canvas.create_line(pts[i][0],pts[i][1],pts[i+1][0],pts[i+1][1], fill=RED,width=1) # marker segitiga di puncak tiap spike: ▲ oranye kalau asli>terdampak # (delta positif, dimensi "turun" akibat oklusi), ▼ ungu kalau # terdampak>asli (delta negatif, dimensi "naik" akibat oklusi) if show_delta: for i in spike_idx: xc=i*bw_d+bw_d/2 yo=mid-(orig_emb[i]/g_max)*(mid-4) yt=mid-(occ_emb[i]/g_max)*(mid-4) y_ext = min(yo,yt)-6 # titik ekstrem paling atas if delta[i] >= 0: # asli lebih tinggi/lebih positif -> segitiga naik, oranye line_canvas.create_polygon( xc,y_ext-6, xc-4,y_ext, xc+4,y_ext, fill=ORANGE, outline="") else: y_ext2 = max(yo,yt)+6 line_canvas.create_polygon( xc,y_ext2+6, xc-4,y_ext2, xc+4,y_ext2, fill=PURPLE, outline="") n_spike=len(spike_idx) line_canvas.create_text(4,2,anchor="nw", text=f"Foto{which} [{name}] Biru=Asli Merah=Terdampak " f"Kuning=zona delta besar ▲/▼=spike ({n_spike} dim, thr≈{spike_thr:.3f})", font=("Courier",7),fill=SUB) # ── Baris bawah: hasil BACA ULANG tiap sliding window, di-encode # pakai AMPLITUDO (persis rumus amp_score) — tinggi & warna bar # = amp_sim (hijau tinggi=amplitudo mirip, merah pendek=jomplang # atau sign gagal). Titik oranye = window sign-nya MATCH tapi # amplitudonya anomali (di atas ambang adaptif mean+1.5*std, # dihitung hanya dari window yang match). ── n_changed=int(np.sum(~blocks)) segs=changed_segments(blocks) avg_len=(n_changed/len(segs)) if segs else 0.0 gate,amp_ratio,combined=soft_window_match(orig_emb,occ_emb) # dipakai utk bar visual (soft) amp_sim=np.where(blocks,amp_ratio,0.0) # basis anomali (hard-gated), diturunkan dari amp_ratio matched_idx=np.where(blocks)[0] if matched_idx.size>0: amp_dev=1-amp_sim[matched_idx] mu_a,sd_a=float(amp_dev.mean()),float(amp_dev.std()) amp_thr=mu_a+1.5*sd_a if sd_a>1e-9 else float(amp_dev.max())+1.0 avg_amp=float(amp_sim[matched_idx].mean()) anomaly_idx=[i for i in matched_idx if (1-amp_sim[i])>=amp_thr] else: amp_thr=2.0; avg_amp=0.0; anomaly_idx=[] if show_blocks: Hb=50; bw_w=Wb/n_win for i in range(n_win): x0=i*bw_w; sim=combined[i] h=max(2.0,sim*(Hb-4)) col=lerp_color(sim,c1=RED,c2=GREEN) blk_canvas.create_rectangle(x0,Hb-h,x0+bw_w+0.5,Hb,fill=col,outline="") for i in anomaly_idx: xc=i*bw_w+bw_w/2; h=max(2.0,combined[i]*(Hb-4)) blk_canvas.create_oval(xc-3,Hb-h-9,xc+3,Hb-h-3,fill=ORANGE,outline="") blk_canvas.create_text(Wb-4,2,anchor="ne", text="tinggi/warna=soft gate×amp •=anomali amplitudo (hard)", font=("Courier",6),fill=SUB) blk_canvas.create_text(4,2,anchor="nw", text=f"Berubah:{n_changed}/{n_win}({n_changed/max(n_win,1)*100:.0f}%) " f"seg:{len(segs)} avglen:{avg_len:.1f} | " f"AMP avg:{avg_amp:.2f} anomali:{len(anomaly_idx)}", font=("Courier",7,"bold"),fill=WHITE) title_lbl.config( text=f"Foto{which} [{name}] — Baca Ulang Sliding Window " f"(berubah: {n_changed}/{n_win} = {n_changed/max(n_win,1)*100:.1f}% " f"spike delta: {n_spike} amp avg: {avg_amp:.2f} amp anomali: {len(anomaly_idx)})") def _draw_multi(self): """Bandingkan semua region: berapa persen window yang berubah kalau region tsb ditutup. Biru=Foto1, Hijau=Foto2 — makin tinggi bar makin sensitif region itu terhadap oklusi.""" c=self.c_multi; c.delete("all") if not self.dc1 and not self.dc2: return W=self._get_w(c); H=150 names=list(REGIONS.keys()) if CUSTOM_KEY in self.dc1 or CUSTOM_KEY in self.dc2: names=names+[CUSTOM_KEY] n=len(names) slot=W/n; bar_w=slot*0.32; base=H-28 c.create_line(0,base,W,base,fill=BORDER) for idx,name in enumerate(names): xc=idx*slot+slot/2 col=REGION_COLS[idx%len(REGION_COLS)] if name in self.dc1: r1=np.sum(~self.dc1[name]["blocks"])/len(self.dc1[name]["blocks"]) h1=r1*base c.create_rectangle(xc-bar_w,base-h1,xc,base,fill=BLUE,outline="") c.create_text(xc-bar_w/2,base-h1-2,text=f"{r1*100:.0f}", anchor="s",font=("Courier",6),fill=BLUE) if name in self.dc2: r2=np.sum(~self.dc2[name]["blocks"])/len(self.dc2[name]["blocks"]) h2=r2*base c.create_rectangle(xc,base-h2,xc+bar_w,base,fill=GREEN,outline="") c.create_text(xc+bar_w/2,base-h2-2,text=f"{r2*100:.0f}", anchor="s",font=("Courier",6),fill=GREEN) c.create_text(xc,base+4,text=name.split()[0],anchor="n", font=("Courier",6),fill=col) c.create_text(W-4,4,anchor="ne", text="Biru=Foto1 Hijau=Foto2 (angka=% window berubah per region)", font=("Courier",7),fill=SUB) def _draw_consistency(self,name): """Bandingkan blok yang berubah pada region terpilih antara Foto1 dan Foto2 (bukan kurva delta yang di-smooth). Konsistensi diukur dengan Jaccard overlap dari posisi window yang sama-sama berubah.""" c=self.c_consist; c.delete("all") W=self._get_w(c); H=110 if name not in self.dc1 or name not in self.dc2: c.create_text(W//2,H//2,text="Tunggu precompute...", font=("Courier",9),fill=SUB); return b1=self.dc1[name]["blocks"]; b2=self.dc2[name]["blocks"] n=min(len(b1),len(b2)); bw=W/n row1_top,row1_bot = 8,44 row2_top,row2_bot = 54,90 for i in range(n): x0=i*bw c.create_rectangle(x0,row1_top,x0+bw+0.5,row1_bot, fill=(RED if not b1[i] else "#0f2418"),outline="") c.create_rectangle(x0,row2_top,x0+bw+0.5,row2_bot, fill=(ORANGE if not b2[i] else "#0f2418"),outline="") changed1=~b1[:n]; changed2=~b2[:n] inter=int(np.sum(changed1 & changed2)) union=int(np.sum(changed1 | changed2)) jacc=inter/union if union>0 else 0.0 col_j=GREEN if jacc>0.5 else (YELLOW if jacc>0.2 else RED) c.create_text(4,H-2,anchor="sw", text=f"Atas=Foto1(merah=berubah) Bawah=Foto2(oranye=berubah) overlap={inter}/{union}", font=("Courier",7),fill=SUB) c.create_text(W-4,4,anchor="ne", text=f"Konsistensi (Jaccard): {jacc:.3f}", font=("Courier",9,"bold"),fill=col_j) sim1=img_sign_score(self.e1,self.dc1[name]["emb"]) if self.e1 is not None else 0 sim2=img_sign_score(self.e2,self.dc2[name]["emb"]) if self.e2 is not None else 0 n1=int(np.sum(changed1)); n2=int(np.sum(changed2)) self.abl_lbl.config( text=f"[{name}]\n" f"Foto1: {n1}/{n} window berubah ({n1/max(n,1)*100:.1f}%) sign-sim akhir={sim1:.4f}\n" f"Foto2: {n2}/{n} window berubah ({n2/max(n,1)*100:.1f}%) sign-sim akhir={sim2:.4f}\n" f"Overlap posisi berubah (Jaccard): {jacc:.3f}\n" f"({'Konsisten ✓' if jacc>0.4 else 'Tidak konsisten ✗'})", fg=GREEN if jacc>0.4 else RED) if __name__=="__main__": app=App() app.after(200, app._redraw_all) # trigger setelah window render app.mainloop()