Files
animation/characters/female/lena_nude/hires_claude/34_target_tone.py
T

81 lines
3.0 KiB
Python
Raw Normal View History

# Stage 34 (read-only): measure the APPROVED concept turnaround against our body, in the same
# units, so "uniform skin" stops being an adjective.
#
# blender --background --python 34_target_tone.py -- <concept_dir> <our_render.png>
#
# Both inputs are renders of a body on a flat grey background, so the comparison is like for like.
# Redness (r-g) is the useful statistic: it barely moves with lighting, so a body lit by one light
# should hold it nearly constant. Lena's Tripo texture does not — her feet measure 50% redder than
# her belly — and the question is what the approved reference does.
import bpy, sys, os
import numpy as np
argv = sys.argv[sys.argv.index("--") + 1:]
CONCEPT_DIR, OURS = argv[0], argv[1]
def load(p):
im = bpy.data.images.load(p)
w, h = im.size
b = np.empty(w * h * 4, dtype=np.float32)
im.pixels.foreach_get(b)
a = b.reshape(h, w, 4)[:, :, :3].copy()
bpy.data.images.remove(im)
return a[::-1] # Blender rows are bottom-up; flip to image order
def body_mask(A):
# background is flat and desaturated; skin is not
sat = A.max(axis=2) - A.min(axis=2)
return (sat > 0.06) & (A.max(axis=2) > 0.15)
def report(name, A):
m = body_mask(A)
if m.sum() < 500:
print(f"{name}: no body found")
return
ys, xs = np.nonzero(m)
y0, y1 = ys.min(), ys.max()
rg = (A[:, :, 0] - A[:, :, 1])
luma = A.mean(axis=2)
print(f"\n=== {name} === {m.sum()} body pixels, rows {y0}..{y1}")
print(f" WHOLE BODY r-g mean {rg[m].mean():.3f} std {rg[m].std():.3f} "
f"luma mean {luma[m].mean():.3f}")
# slice by height: 0 = top of the figure (head), 1 = bottom (feet)
print(" by height band (0=head .. 1=feet):")
rows = []
for i in range(10):
a = y0 + (y1 - y0) * i / 10.0
b = y0 + (y1 - y0) * (i + 1) / 10.0
band = np.zeros_like(m)
band[int(a):int(b) + 1, :] = True
mm = m & band
if mm.sum() < 200:
continue
rows.append((i / 10.0, rg[mm].mean(), luma[mm].mean(), int(mm.sum())))
print(f" {i/10.0:.1f}-{(i+1)/10.0:.1f} r-g {rg[mm].mean():.3f} "
f"luma {luma[mm].mean():.3f} n={int(mm.sum())}")
if rows:
arr = np.array([r[1] for r in rows])
print(f" SPREAD of r-g across height bands: {arr.max()-arr.min():.3f} "
f"(min {arr.min():.3f} at {rows[int(np.argmin(arr))][0]:.1f}, "
f"max {arr.max():.3f} at {rows[int(np.argmax(arr))][0]:.1f})")
return rg[m], luma[m]
res = {}
for f in sorted(os.listdir(CONCEPT_DIR)):
if f.lower().endswith(".png"):
res[f] = report("CONCEPT " + f, load(os.path.join(CONCEPT_DIR, f)))
ours = report("OURS " + os.path.basename(OURS), load(OURS))
print("\n" + "=" * 72)
print("VERDICT — how much does redness vary over the body?")
for k, v in res.items():
if v:
print(f" concept {k:<32} r-g std {v[0].std():.4f}")
if ours:
print(f" ours {os.path.basename(OURS):<32} r-g std {ours[0].std():.4f}")
print("TARGET_TONE_DONE")