Project_WL/AgentScripts/WL814x_metrics.py

198 lines
7.6 KiB
Python
Raw Normal View History

# WL-814x 측정기 — 렌더 PNG + 마스크 PNG 를 읽어 합격선 6항목을 잰다.
# python AgentScripts/WL814x_metrics.py <raw폴더> [파일접두어필터]
#
# 정의(보고서와 동일)
# · 서로 다른 색 비율 = 고유색 수 / 캐릭터 영역 픽셀 수 × 100
# · 부위당 색 단계 = 그 부위의 색을 RGB 거리 16 이내로 묶었을 때, 면적 90 % 를 덮는 군집 수
# · 진한 점(눈) = 얼굴 데칼 마스크 안에서 L < 피부평균L × 0.75 인 픽셀 수 (814v 와 동일)
# · Hue 오차 = 부위별 채도가중 원형평균 Hue 의 기준 대비 차 (부호 있는 각도)
import sys, os, glob, json
import numpy as np
from PIL import Image
RAW = sys.argv[1] if len(sys.argv) > 1 else "Screenshots_WL/WL814x/raw"
FILT = sys.argv[2] if len(sys.argv) > 2 else ""
PARTS = ["all", "body3", "Body_m05", "Arm_m05", "Leg_M05", "Head", "Hair05", "Face"]
def load(p):
return np.asarray(Image.open(p).convert("RGB"), dtype=np.uint8)
def masks():
m = {}
for p in PARTS:
f = os.path.join(RAW, "mask_%s.png" % p)
if os.path.exists(f):
m[p] = load(f)[:, :, 0] > 127
return m
def lum(px):
return 0.299 * px[:, 0] + 0.587 * px[:, 1] + 0.114 * px[:, 2]
def hsv(px):
a = px.astype(np.float32)
mx = a.max(1); mn = a.min(1); d = mx - mn
h = np.zeros(len(a), np.float32)
nz = d > 1e-6
r, g, b = a[:, 0], a[:, 1], a[:, 2]
i = nz & (mx == r); h[i] = ((g[i] - b[i]) / d[i]) % 6
i = nz & (mx == g) & ~(mx == r); h[i] = (b[i] - r[i]) / d[i] + 2
i = nz & (mx == b) & ~(mx == r) & ~(mx == g); h[i] = (r[i] - g[i]) / d[i] + 4
h *= 60.0
s = np.where(mx > 1e-6, d / np.maximum(mx, 1e-6), 0)
return h, s, mx
def mean_hue(px):
h, s, v = hsv(px)
w = np.where(s > 0.08, s, 0.0)
if w.sum() < 1e-6:
return float("nan")
x = (w * np.cos(np.radians(h))).sum(); y = (w * np.sin(np.radians(h))).sum()
return (np.degrees(np.arctan2(y, x)) + 360.0) % 360.0
def dhue(a, b):
d = (b - a + 180.0) % 360.0 - 180.0
return d
def steps(px, thr=16.0, cover=0.90):
"""색을 RGB 거리 thr 이내로 묶어 면적 cover 를 덮는 군집 수."""
if len(px) == 0:
return 0, 0
key = (px[:, 0].astype(np.int32) << 16) | (px[:, 1].astype(np.int32) << 8) | px[:, 2].astype(np.int32)
u, cnt = np.unique(key, return_counts=True)
order = np.argsort(-cnt)
u, cnt = u[order], cnt[order]
cols = np.stack([(u >> 16) & 255, (u >> 8) & 255, u & 255], 1).astype(np.float32)
cent = []; wts = []
for i in range(len(u)):
c = cols[i]; w = cnt[i]
best = -1; bd = 1e9
for j, cc in enumerate(cent):
d = np.linalg.norm(cc - c)
if d < bd:
bd = d; best = j
if best >= 0 and bd < thr:
n = wts[best] + w
cent[best] = (cent[best] * wts[best] + c * w) / n
wts[best] = n
else:
cent.append(c.copy()); wts.append(float(w))
if len(cent) > 400:
break
wts = np.array(wts); o = np.argsort(-wts); wts = wts[o]
tot = len(px); acc = 0; k = 0
for w in wts:
acc += w; k += 1
if acc >= cover * tot:
break
return k, len(u)
def measure(img, msk, ref_hue=None, skinL=None):
px = img[msk]
n = len(px)
if n == 0:
return None
key = (px[:, 0].astype(np.int32) << 16) | (px[:, 1].astype(np.int32) << 8) | px[:, 2].astype(np.int32)
uc = len(np.unique(key))
st, _ = steps(px)
L = lum(px)
h_, s_, v_ = hsv(px)
r = dict(px=n, colors=uc, ratio=100.0 * uc / n, steps=st,
lmin=float(L.min()), lmax=float(L.max()), hue=mean_hue(px),
sat=float(s_.mean()), val=float(v_.mean()), lmean=float(L.mean()))
if ref_hue is not None and not np.isnan(r["hue"]) and not np.isnan(ref_hue):
r["dhue"] = float(dhue(ref_hue, r["hue"]))
if skinL:
r["dark"] = int((L < skinL * 0.75).sum())
return r
def main():
M = masks()
files = sorted(glob.glob(os.path.join(RAW, "*.png")))
files = [f for f in files if not os.path.basename(f).startswith("mask_")]
if FILT:
files = [f for f in files if os.path.basename(f).startswith(FILT)]
# 🔴 색상/채도 기준은 언제나 「지금」 상태(i1_a_base_now) 로 고정한다.
base = os.path.join(RAW, "i1_a_base_now.png")
if os.path.exists(base):
files = [base] + [f for f in files if os.path.abspath(f) != os.path.abspath(base)]
ref = {}
rows = []
for f in files:
img = load(f)
name = os.path.splitext(os.path.basename(f))[0]
# 피부 밝기 기준 = Head 마스크에서 Face 를 뺀 영역 평균 L
skinL = None
if "Head" in M and "Face" in M:
hm = M["Head"] & ~M["Face"]
if hm.sum() > 0:
skinL = float(lum(img[hm]).mean())
rec = {"name": name, "skinL": skinL}
for p in PARTS:
if p not in M:
continue
r = measure(img, M[p], ref.get(p), skinL if p == "Face" else None)
if r is None:
continue
if p not in ref and not np.isnan(r["hue"]):
ref[p] = r["hue"]
# 눈 크기(얼굴 왼쪽 절반의 진한 점 바운딩박스)
if p == "Face" and skinL:
ys, xs = np.where(M[p])
x0, x1 = xs.min(), xs.max()
half = (x0 + x1) // 2
sub = M[p].copy(); sub[:, half:] = False
idx = np.where(sub)
if len(idx[0]):
Ls = lum(img[sub])
d = Ls < skinL * 0.75
if d.sum() > 0:
yy = idx[0][d]; xx = idx[1][d]
r["eye"] = "%dx%d" % (xx.max() - xx.min() + 1, yy.max() - yy.min() + 1)
rec[p] = r
rows.append(rec)
hdr = "%-28s %6s %6s %5s | %s | %s | %s" % (
"render", "전체색", "비율%", "단계",
"색:몸통/피부/머리칼/얼굴", "단계:몸통/피부/머리칼", "얼굴 진한점/눈")
print(hdr); print("-" * 118)
for r in rows:
a = r.get("all"); b3 = r.get("body3"); hd = r.get("Head"); hr = r.get("Hair05"); fc = r.get("Face")
def c(x): return x["colors"] if x else 0
def s(x): return x["steps"] if x else 0
print("%-28s %6d %6.1f %5d | %5d/%4d/%4d/%4d | %6d/%3d/%3d | %4s / %s" % (
r["name"], a["colors"], a["ratio"], a["steps"],
c(b3), c(hd), c(hr), c(fc), s(b3), s(hd), s(hr),
fc.get("dark", "-") if fc else "-", fc.get("eye", "-") if fc else "-"))
print()
print("%-26s | %-22s | %-22s | %s" % ("render", "dHue 몸통/피부/머리칼", "채도S 몸통/피부/머리칼", "밝기L 몸통/피부/머리칼"))
b0 = rows[0]
for r in rows:
def g(p):
x = r.get(p)
return ("%+5.1f" % x["dhue"]) if (x and "dhue" in x) else " ref"
def s(p):
x = r.get(p); y = b0.get(p)
if not x: return " -"
return "%.2f" % x["sat"] + ("(%+.2f)" % (x["sat"] - y["sat"]) if y and r is not b0 else "")
def lv(p):
x = r.get(p); y = b0.get(p)
if not x: return " -"
return "%3.0f" % x["lmean"] + ("(%+3.0f)" % (x["lmean"] - y["lmean"]) if y and r is not b0 else "")
print("%-26s | %s %s %s | %s %s %s | %s %s %s" % (
r["name"], g("body3"), g("Head"), g("Hair05"),
s("body3"), s("Head"), s("Hair05"), lv("body3"), lv("Head"), lv("Hair05")))
with open(os.path.join(RAW, "..", "metrics.json"), "w", encoding="utf-8") as fp:
json.dump(rows, fp, ensure_ascii=False, indent=1, default=float)
main()