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