3d8825f5a9
REGISTRY rewritten around the central rule: a character folder is born only when a body ships to ariki-game (<character>_base_v<NN> = ship ordinal). lena_nude dissolves accordingly: - characters/female/lena_base_v01/ — SHIPPED 2026-08-10: AccuRig GLB carrier, T-pose/rig FBX + JSON, previews, frozen README - characters/work/lena/ — the live lane: recipes 01-47 (incl. new 36-47: refill/sheets/clay/despeckle/musculature/spin/AccuRig export/graft/pose QC), masters (athletic_v04 blend + textures, accurig blend), lane-history README - hires_claude/hires_work intermediates (blends, logs, probes) pruned Supporting docs: AGENTS.md, working-files rule, rig-graft plan addendum, originals README, prune_lane.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
# Stage 35 (read-only): does the approved concept turnaround describe OUR body?
|
|
#
|
|
# blender --background --python 35_silhouette.py -- <concept_front.png> <our_front.png>
|
|
#
|
|
# This decides how the turnaround can be used. If the silhouettes match, the concept renders could
|
|
# be projected onto the mesh as a texture source. If they do not, they are a style and tone
|
|
# reference only — and quietly projecting them would smear one body's paint onto another's shape.
|
|
# Widths are normalised by figure height, so framing and resolution cancel.
|
|
import bpy, sys, os
|
|
import numpy as np
|
|
|
|
argv = sys.argv[sys.argv.index("--") + 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]
|
|
|
|
|
|
def profile(p):
|
|
A = load(p)
|
|
sat = A.max(axis=2) - A.min(axis=2)
|
|
m = (sat > 0.06) & (A.max(axis=2) > 0.15)
|
|
ys, xs = np.nonzero(m)
|
|
y0, y1 = ys.min(), ys.max()
|
|
Hf = float(y1 - y0)
|
|
cx = xs.mean()
|
|
out = []
|
|
for t in np.arange(0.02, 1.0, 0.02):
|
|
r = int(round(y0 + t * Hf))
|
|
row = np.nonzero(m[r])[0]
|
|
if len(row) < 2:
|
|
out.append((t, np.nan, np.nan))
|
|
continue
|
|
# full extent (includes arms where they are out), and the CENTRAL run (the torso/leg)
|
|
full = (row.max() - row.min()) / Hf
|
|
# central run: walk out from the pixel nearest the body's x centre
|
|
k = row[np.argmin(np.abs(row - cx))]
|
|
lo = k
|
|
while lo - 1 in set(row.tolist()) if False else (lo - 1 >= 0 and m[r, lo - 1]):
|
|
lo -= 1
|
|
hi = k
|
|
while hi + 1 < m.shape[1] and m[r, hi + 1]:
|
|
hi += 1
|
|
out.append((t, full, (hi - lo) / Hf))
|
|
return np.array(out), Hf
|
|
|
|
|
|
A, ha = profile(argv[0])
|
|
B, hb = profile(argv[1])
|
|
print(f"concept figure height {ha:.0f} px ours {hb:.0f} px")
|
|
print("\n t concept_full ours_full concept_core ours_core core_diff")
|
|
diffs = []
|
|
for (t, fa, ca), (_, fb, cb) in zip(A, B):
|
|
if np.isnan(ca) or np.isnan(cb):
|
|
continue
|
|
d = cb - ca
|
|
# the central run is only meaningful where it is a single body part:
|
|
# below the arms (t>0.30) and above the ankles
|
|
flag = ""
|
|
if 0.30 < t < 0.95:
|
|
diffs.append(d)
|
|
if abs(d) > 0.02:
|
|
flag = " <-- differs"
|
|
print(f" {t:.2f} {fa:9.3f} {fb:9.3f} {ca:10.3f} {cb:9.3f} {d:+8.3f}{flag}")
|
|
if diffs:
|
|
diffs = np.array(diffs)
|
|
print(f"\nCORE WIDTH (torso/legs, t 0.30-0.95), ours minus concept, as fraction of height:")
|
|
print(f" mean {diffs.mean():+.4f} mean|d| {np.abs(diffs).mean():.4f} "
|
|
f"max|d| {np.abs(diffs).max():.4f}")
|
|
print(f" bands differing by >2% of height: {int((np.abs(diffs)>0.02).sum())} of {len(diffs)}")
|
|
print("SILHOUETTE_DONE")
|