Project_WL/AgentScripts/WL814x_metrics.py

198 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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()