# WL-814x3 — 최종 수치표 + 필수 캡처 4종 import json import numpy as np from PIL import Image, ImageDraw RAW='Screenshots_WL/WL814x/3rd/raw'; OUT='Screenshots_WL/WL814x/3rd' def L(n): return np.asarray(Image.open(RAW+'/'+n+'.png').convert('RGB'),dtype=np.uint8) 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 G=np.array(Image.open('Assets/WL/Look/Character/Textures/WLPalette_FINAL.png')).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()} NAME={8:'머리칼(주)',4:'머리칼(밝은단)',12:'머리칼(하이라이트)',2:'흰 튜닉',14:'부츠 흰면',1:'가죽 갑옷',13:'가죽(어두운)',3:'부츠 갈색',15:'붉은 바지',9:'붉은 천',10:'벨트',11:'팔 소매',0:'머리 피부',5:'다리 피부'} m10=L('mask_all10')[:,:,0]>127 print('%-16s %7s | %-16s %-16s %8s | %-16s %8s'%('부위','px','원본 RGB','2차(보라 앰비언트)','dHue','3차(중성+보정)','dHue')) ch0=[];ch1=[];w=[] for j in sorted(NAME): sel=ma&(kid==j); n=int(sel.sum()) if n<800: continue o=tgt[j]; a=L('pre_v0_purple')[sel].mean(0); b=L('f_final')[sel].mean(0) ea=dh(hue(o),hue(a)); eb=dh(hue(o),hue(b)) print('%-16s %7d | %-16s %-16s %+8.1f | %-16s %+8.1f'%(NAME[j],n,tuple(int(x) for x in o), tuple(int(x) for x in a),ea,tuple(int(x) for x in b),eb)) if sat(o)>0.12: ch0.append(abs(ea)); ch1.append(abs(eb)); w.append(n) w=np.array(w,float); ch0=np.array(ch0); ch1=np.array(ch1) print('유채색 가중 dHue: 2차 %.1f°(최대 %.1f) → 3차 %.1f°(최대 %.1f)'%((ch0*w).sum()/w.sum(),ch0.max(),(ch1*w).sum()/w.sum(),ch1.max())) for v,lbl in [('nc_gray','보정전(중성 앰비언트)'),('s0_now','3차 최종')]: g=L(v+'_g')[m10]; k=(g[:,0].astype(np.int32)<<16)|(g[:,1].astype(np.int32)<<8)|g[:,2].astype(np.int32) u=len(np.unique(k)); print('%s 게임 크기: 캐릭터 %d px · 고유색 %d · 서로 다른 색 %.1f %%'%(lbl,m10.sum(),u,100.0*u/m10.sum())) # 캡처 ys,xs=np.where(ma|maF); x0,x1,y0,y1=xs.min()-24,xs.max()+25,ys.min()-24,ys.max()+25 def crop(n): return Image.open(RAW+'/'+n+'.png').convert('RGB').crop((x0,y0,x1,y1)) def grid(out,items,scale=1.0): ims=[(t,crop(n)) for t,n in items] if scale!=1.0: ims=[(t,im.resize((int(im.width*scale),int(im.height*scale)),Image.NEAREST)) for t,im in ims] W=sum(i.width for _,i in ims)+18*(len(ims)-1); H=ims[0][1].height+26 o=Image.new('RGB',(W,H),(22,22,26)); d=ImageDraw.Draw(o); x=0 for t,im in ims: o.paste(im,(x,26)); d.text((x+3,7),t,fill=(240,232,190)); x+=im.width+18 o.save(OUT+'/'+out); print('saved',out,o.size) grid('d_compare.png',[('2차 (보라 앰비언트)','pre_v0_purple'),('3차 최종 (중성 앰비언트 + 보정)','s0_now')]) grid('d_ambient_steps.png',[('보라(기존)','pre_v0_purple'),('중성 회색','pre_v1_gray'),('옅은 청회','v2_cool'),('옅은 난색','v3_warm')],0.72) def wide(out,items): ims=[(t,Image.open(RAW+'/'+n+'.png').convert('RGB').resize((405,720))) for t,n in items] W=sum(i.width for _,i in ims)+16*(len(ims)-1); H=ims[0][1].height+26 o=Image.new('RGB',(W,H),(22,22,26)); d=ImageDraw.Draw(o); x=0 for t,im in ims: o.paste(im,(x,26)); d.text((x+3,7),t,fill=(240,232,190)); x+=im.width+16 o.save(OUT+'/'+out); print('saved',out,o.size) wide('d_key_bg.png',[('기존 키라이트','w1_gray'),('키 (1,.95,.90)','w4_gray_keysoft'),('키 순백 (1,1,1)','w5_gray_keywhite')]) Image.open(RAW+'/f_wide.png').convert('RGB').save(OUT+'/d_final_wide.png') print('saved d_final_wide.png')