111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
# WL-814x2 팔레트 「최종 화면 색」 역보정 (닫힌 루프)
|
||
# 측정 지점을 알베도가 아니라 **Play 화면 픽셀**로 옮긴다.
|
||
# 칸마다 원본색 O(부위 보정 역산) · 화면색 S(측정) 를 비교해
|
||
# 목표 T = O × (밝기 S / 밝기 O) 가 되도록 팔레트 칸 색에 채널별 이득을 곱한다.
|
||
# python AgentScripts/WL814x2_calib.py <측정에쓸 렌더이름> [apply]
|
||
import sys, os, json
|
||
import numpy as np
|
||
from PIL import Image
|
||
|
||
RAW = 'Screenshots_WL/WL814x/2nd/raw'
|
||
PAL = 'Assets/WL/Look/Character/Textures/WLPalette_FINAL.png'
|
||
TGT = 'AgentScripts/WL814x2_target.json'
|
||
GAIN = {'M05': (0.80, 1.00), 'Skin': (0.86, 0.93), 'Hair': (0.85, 0.93)}
|
||
PARTMAP = {'Body_m05': 'M05', 'Arm_m05': 'M05', 'Leg_M05': 'M05', 'Head': 'Skin', 'Hair05': 'Hair'}
|
||
REN = sys.argv[1] if len(sys.argv) > 1 else 'a_now'
|
||
APPLY = len(sys.argv) > 2 and sys.argv[2] == 'apply'
|
||
|
||
|
||
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 dh(a, b):
|
||
return (b - a + 540) % 360 - 180
|
||
|
||
|
||
idm = L('idmap'); ma = L('mask_all')[:, :, 0] > 127
|
||
palimg = np.array(Image.open(PAL).convert('RGB'), dtype=np.uint8)
|
||
S = palimg.shape[0]; G = S // 8
|
||
idref = np.array([[9 + j * 15, 240 - j * 11, 40 + j * 7] for j in range(G * G)], np.float32)
|
||
flat = idm.reshape(-1, 3).astype(np.float32)
|
||
d2 = ((flat[:, None, :] - idref[None, :, :]) ** 2).sum(2)
|
||
near = d2.argmin(1).reshape(idm.shape[:2])
|
||
kid = np.where((d2.min(1).reshape(idm.shape[:2]) < 400) & (idm.sum(2) > 12), near, -1)
|
||
|
||
masks = {}
|
||
for p in PARTMAP:
|
||
f = RAW + '/mask_' + p + '.png'
|
||
if os.path.exists(f):
|
||
masks[p] = L('mask_' + p)[:, :, 0] > 127
|
||
|
||
# 목표색(원본) — 최초 1회만 저장하고 이후 회차에서는 고정해 쓴다
|
||
if os.path.exists(TGT):
|
||
target = {int(k): v for k, v in json.load(open(TGT)).items()}
|
||
else:
|
||
target = {}
|
||
for j in range(G * G):
|
||
gx, gy = j % G, j // G
|
||
p = palimg[gy * 8 + 4, gx * 8 + 4].astype(np.float32)
|
||
sel = ma & (kid == j)
|
||
if sel.sum() < 200:
|
||
target[j] = [float(x) for x in p]; continue
|
||
part = max(((k, int((sel & v).sum())) for k, v in masks.items()), key=lambda t: t[1])[0]
|
||
gs, gv = GAIN[PARTMAP[part]]
|
||
mx = p.max() / 255.0; mn = p.min() / 255.0
|
||
s = (mx - mn) / mx if mx > 1e-6 else 0
|
||
s2 = min(s / gs, 1.0); v2 = min(mx / gv, 1.0)
|
||
# H 유지 · S,V 만 역보정
|
||
import colorsys
|
||
h = hue(p) / 360.0
|
||
r, g, b = colorsys.hsv_to_rgb(h, s2, v2)
|
||
target[j] = [float(r * 255), float(g * 255), float(b * 255)]
|
||
json.dump({str(k): v for k, v in target.items()}, open(TGT, 'w'))
|
||
|
||
img = L(REN)
|
||
newpal = palimg.copy()
|
||
print('%3s %-10s %7s | %-16s %-16s %-16s | %-16s %6s -> %6s' %
|
||
('칸', '부위', 'px', '원본 O', '팔레트 P', '화면 S', '새 팔레트 P\'', 'dHue', 'dHue예상'))
|
||
rows = []
|
||
for j in range(G * G):
|
||
gx, gy = j % G, j // G
|
||
p = palimg[gy * 8 + 4, gx * 8 + 4].astype(np.float32)
|
||
sel = ma & (kid == j)
|
||
n = int(sel.sum())
|
||
if n < 200:
|
||
continue
|
||
part = max(((k, int((sel & v).sum())) for k, v in masks.items()), key=lambda t: t[1])[0]
|
||
o = np.array(target[j], np.float32)
|
||
s = img[sel].mean(0).astype(np.float32)
|
||
t = o * (max(lum(s), 1.0) / max(lum(o), 1.0)) # 같은 밝기에서의 「원본 색조」
|
||
gain = np.clip(t / np.maximum(s, 4.0), 0.55, 1.8)
|
||
np_ = np.clip(p * gain, 0, 255)
|
||
rows.append((j, part, n, o, p, s, t, np_))
|
||
print('%3d %-10s %7d | %-16s %-16s %-16s | %-16s %+6.1f -> %+6.1f' % (
|
||
j, part, 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(t))))
|
||
if APPLY:
|
||
newpal[gy * 8:gy * 8 + 8, gx * 8:gx * 8 + 8] = np.round(np_).astype(np.uint8)
|
||
|
||
err = [abs(dh(hue(r[3]), hue(r[5]))) for r in rows]
|
||
w = np.array([r[2] for r in rows], np.float64)
|
||
print('화면 Hue 오차: 면적가중 평균 %.1f° · 단순평균 %.1f° · 최대 %.1f°' %
|
||
((np.array(err) * w).sum() / w.sum(), np.mean(err), np.max(err)))
|
||
if APPLY:
|
||
Image.fromarray(newpal).save(PAL)
|
||
print('팔레트 갱신 →', PAL)
|