181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
# 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)
|
||
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))
|
||
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)]
|
||
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("%-28s %8s %8s %8s %8s" % ("render", "dH몸통", "dH피부", "dH머리칼", "dH얼굴"))
|
||
for r in rows:
|
||
def g(p):
|
||
x = r.get(p)
|
||
return ("%+.1f" % x["dhue"]) if (x and "dhue" in x) else "ref"
|
||
print("%-28s %8s %8s %8s %8s" % (r["name"], g("body3"), g("Head"), g("Hair05"), g("Face")))
|
||
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()
|