# WL-814x3 — 팔레트 칸을 「최종 화면에서 원하는 색이 나오도록」 역산한다. # 화면색 S = 팔레트 P ⊙ m (조명은 채널별 곱 · 포스트 영향 0.1° 실측) → P' = P ⊙ (T / S) # T = 원본색 O 를 S 의 밝기로 맞춘 것 (색조만 되돌리고 명암은 조명이 정한 대로 둔다) import json, sys import numpy as np from PIL import Image RAW='Screenshots_WL/WL814x/3rd/raw' PAL='Assets/WL/Look/Character/Textures/WLPalette_FINAL.png' REN=sys.argv[1]; APPLY=len(sys.argv)>2 and sys.argv[2]=='apply' CLAMP=float(sys.argv[3]) if len(sys.argv)>3 else 1.35 DAMP=float(sys.argv[4]) if len(sys.argv)>4 else 0.55 # 감쇠: 모델이 근사라 한 번에 다 가면 발산한다 def L(n): return np.asarray(Image.open(RAW+'/'+n+'.png').convert('RGB'),dtype=np.uint8) def lum(c): return 0.299*c[0]+0.587*c[1]+0.114*c[2] def hue(c): r,g,b=[float(x) for x in c]; mx,mn=max(r,g,b),min(r,g,b); d=mx-mn if d<1e-6: return 0.0 if mx==r: h=((g-b)/d)%6 elif mx==g: h=(b-r)/d+2 else: h=(r-g)/d+4 return h*60.0 def sat(c): mx=max(c); return (mx-min(c))/mx if mx>1e-6 else 0 def dh(a,b): return (b-a+540)%360-180 idm=L('idmap'); maF=L('mask_Face')[:,:,0]>127; ma=(L('mask_all')[:,:,0]>127)&~maF pal=np.array(Image.open(PAL).convert('RGB'),dtype=np.uint8); G=pal.shape[0]//8 idref=np.array([[9+j*15,240-j*11,40+j*7] for j in range(G*G)],np.float32) fl=idm.reshape(-1,3).astype(np.float32); d2=((fl[:,None,:]-idref[None,:,:])**2).sum(2) kid=np.where((d2.min(1).reshape(idm.shape[:2])<400)&(idm.sum(2)>12), d2.argmin(1).reshape(idm.shape[:2]), -1) tgt={int(k):np.array(v,np.float32) for k,v in json.load(open('AgentScripts/WL814x2_target.json')).items()} im=L(REN); newpal=pal.copy() print('%3s %7s | %-16s %-16s %-16s | %-16s %7s %7s'%('칸','px','원본 O','팔레트 P','화면 S','새 팔레트 P\'','dHue','예상')) e0=[];e1=[];w=[] for j in range(G*G): gx,gy=j%G,j//G sel=ma&(kid==j); n=int(sel.sum()) if n<800: continue p=pal[gy*8+4,gx*8+4].astype(np.float64); o=tgt[j].astype(np.float64) s=im[sel].mean(0).astype(np.float64) t=o*(max(lum(s),1.0)/max(lum(o),1.0)) g=np.clip(t/np.maximum(s,6.0),1.0/CLAMP,CLAMP) g=np.power(g,DAMP) # 감쇠 g=g/max(lum(g),1e-6) # 밝기는 유지, 색조만 g=np.clip(g,1.0/CLAMP,CLAMP) np_=p*g if np_.max()>248: np_=np_*248.0/np_.max() np_=np.clip(np_,4,255) pred=np.clip(s*(np_/np.maximum(p,1.0)),0,255) print('%3d %7d | %-16s %-16s %-16s | %-16s %+7.1f %+7.1f'%(j,n,tuple(int(x) for x in o),tuple(int(x) for x in p), tuple(int(x) for x in s),tuple(int(x) for x in np_),dh(hue(o),hue(s)),dh(hue(o),hue(pred)))) if sat(o)>0.12: e0.append(abs(dh(hue(o),hue(s)))); e1.append(abs(dh(hue(o),hue(pred)))); w.append(n) if APPLY: newpal[gy*8:gy*8+8,gx*8:gx*8+8]=np.round(np_).astype(np.uint8) w=np.array(w,float); e0=np.array(e0); e1=np.array(e1) print('유채색 가중 dHue %.1f°(최대 %.1f) → 예상 %.1f°(최대 %.1f)'%((e0*w).sum()/w.sum(),e0.max(),(e1*w).sum()/w.sum(),e1.max())) if APPLY: Image.fromarray(newpal).save(PAL); print('팔레트 갱신 →',PAL)