Automate the redraw, and run the first eight batches through it
assets/_regen.py plus a batch file: build a reference sheet from the sprites in the repo, ask for a HIGH-resolution redraw, slice with _fine.py, colour-match each frame onto the one it replaces, install. The point of the whole thing is the style block — the old prompts specified coarse art and got it. It refuses to install a bad sheet. Wrong frame count, any figure touching a canvas edge (that is what clipped z_f1 in the first place), or a redraw that is not measurably finer than what it replaces, and the batch is left alone with a reason printed. Re-running a batch is the fix; three of the first eleven needed it. The bar is 1.25x inner-edge density rather than 1.5x, because a sheet that already has blood on it starts high and a genuinely finer wounded body still scores a smaller gain. Done so far: both shamblers and their wound skins, the brute's walk and its two sets of wound stages, the steward and his wound skins, the turned CPU. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2bf63d546
commit
0c499b60b5
36 files changed
+233
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# _regen.py — redraw a batch of sprites at real pixel-art resolution, end to end.
|
||||
#
|
||||
# python3 assets/_regen.py shambler_a # one batch
|
||||
# python3 assets/_regen.py --list # what batches exist
|
||||
# python3 assets/_regen.py --all # everything not already done
|
||||
#
|
||||
# Per batch it: builds a reference sheet from the sprites in the repo, asks the generator to redraw
|
||||
# them at HIGH art resolution (the style block below is the whole point — the previous prompts asked
|
||||
# for "big chunky pixel blocks", which is a specification for coarse art), slices with _fine.py,
|
||||
# colour-matches each frame onto what it replaces, and installs — but only if the sheet passes its
|
||||
# checks: right number of frames, nothing touching a canvas edge (that is what clipped z_f1), and a
|
||||
# real gain in inner-edge density (the number that tracks "is the drawing actually finer").
|
||||
#
|
||||
# Failures leave the sprites alone and print why, so a batch can just be re-run.
|
||||
import json, os, subprocess, sys
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SCRATCH = os.environ.get('SPLAT_SCRATCH', '/tmp/splatter-regen')
|
||||
GEN = os.path.expanduser('~/.claude/skills/generate-image/scripts/generate_image.py')
|
||||
PY = os.environ.get('SPLAT_PY', sys.executable)
|
||||
|
||||
STYLE = (
|
||||
"Redraw them as HIGH-RESOLUTION pixel art. The art pixels must be SMALL: build each figure out "
|
||||
"of roughly 90 to 110 pixels of height instead of the reference's 40, so there is room for real "
|
||||
"detail, and spend it — a face with teeth, sunken eyes and a jawline; individual fingers; "
|
||||
"clothing with visible seams, hems, folds and frayed torn edges; laced shoes; four or five "
|
||||
"distinct shades per material with fine hatched shading where the light falls away. Keep every "
|
||||
"pose, every body proportion, every height, every facing and every wound EXACTLY as in the "
|
||||
"reference — only the drawing resolution and the amount of fine detail change. Keep the same "
|
||||
"colours. Hard black outline around each figure. The background must be ONE FLAT solid magenta "
|
||||
"#FF00FF across the entire canvas — no scenery, no gradient, no vignette. Keep at least 200 "
|
||||
"pixels of empty magenta between figures and at least 150 pixels of empty magenta at every edge "
|
||||
"of the canvas: no figure or limb may touch or run off an edge. No shadow, no ground line, no text."
|
||||
)
|
||||
|
||||
def batches():
|
||||
with open(os.path.join(HERE, '_regen_batches.json')) as f:
|
||||
return json.load(f)
|
||||
|
||||
# ---------------------------------------------------------------- helpers ----
|
||||
def ref_sheet(names, out, scale=2.0, gap=220, pad=150):
|
||||
ims = [Image.open(os.path.join(HERE, n + '.png')).convert('RGBA') for n in names]
|
||||
ims = [i.resize((max(1, int(i.width * scale)), max(1, int(i.height * scale))), Image.NEAREST) for i in ims]
|
||||
w = sum(i.width for i in ims) + gap * (len(ims) - 1) + pad * 2
|
||||
h = max(i.height for i in ims) + pad * 2
|
||||
s = Image.new('RGBA', (w, h), (255, 0, 255, 255))
|
||||
x = pad
|
||||
for i in ims:
|
||||
s.paste(i, (x, h - pad - i.height), i); x += i.width + gap
|
||||
s.save(out)
|
||||
return out
|
||||
|
||||
def density(path):
|
||||
"""Inner-edge density: how much detail lives INSIDE the silhouette. Coarse art ~0.15, the
|
||||
redrawn runner ~0.45. This is the number that matched Oliver's eye."""
|
||||
a = np.array(Image.open(path).convert('RGBA'))
|
||||
solid = a[..., 3] > 128
|
||||
d = np.abs(np.diff(a[..., :3].astype(int), axis=1)).sum(2)
|
||||
return round(float(((d > 40) & solid[:, :-1]).sum() / max(1, solid.sum())), 3)
|
||||
|
||||
def touches_edge(sheet):
|
||||
a = np.array(Image.open(sheet).convert('RGB')).astype(int)
|
||||
bg = a[0, 0]
|
||||
fg = (((a - bg) ** 2).sum(2) > 3600) & ~(np.minimum(a[..., 0], a[..., 2]) > a[..., 1] + 40)
|
||||
return bool(fg[0].any() or fg[-1].any() or fg[:, 0].any() or fg[:, -1].any())
|
||||
|
||||
def colour_match(src, dst_name):
|
||||
"""Fit the new frame onto the palette statistics of the sprite it replaces, so a redrawn body
|
||||
does not change colour halfway through a walk cycle."""
|
||||
op = lambda x: x[..., 3] > 200
|
||||
old = np.array(Image.open(os.path.join(HERE, dst_name + '.png')).convert('RGBA'), dtype=float)
|
||||
new = np.array(Image.open(src).convert('RGBA'), dtype=float)
|
||||
po, pn = old[op(old)][:, :3], new[op(new)][:, :3]
|
||||
m = op(new)
|
||||
px = (new[m][:, :3] - pn.mean(0)) / np.maximum(pn.std(0), 1e-6) * po.std(0) + po.mean(0)
|
||||
new[m] = np.concatenate([np.clip(px, 0, 255), new[m][:, 3:]], axis=1)
|
||||
Image.fromarray(new.astype('uint8'), 'RGBA').save(src)
|
||||
|
||||
# ------------------------------------------------------------------- run ----
|
||||
def run(name, spec, dry=False):
|
||||
os.makedirs(SCRATCH, exist_ok=True)
|
||||
sprites = spec['sprites']
|
||||
ref = ref_sheet(sprites, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0))
|
||||
sheet = f'{SCRATCH}/{name}_hi.png'
|
||||
prompt = f"{spec['subject']} {STYLE}"
|
||||
if dry:
|
||||
print(f'[{name}] would generate {len(sprites)} frames: {", ".join(sprites)}'); return True
|
||||
print(f'[{name}] generating {len(sprites)} frames...')
|
||||
r = subprocess.run([PY, GEN, prompt, '--input', ref, '--output', sheet],
|
||||
capture_output=True, text=True, cwd=os.path.dirname(HERE))
|
||||
if not os.path.exists(sheet):
|
||||
print(f'[{name}] FAILED: generator returned no image\n{r.stdout[-400:]}'); return False
|
||||
if touches_edge(sheet):
|
||||
print(f'[{name}] REJECTED: a figure runs off the canvas edge — re-run'); return False
|
||||
outs = [f'{SCRATCH}/{name}_{i}.png' for i in range(len(sprites))]
|
||||
cut = ['--cut', spec['cut']] if spec.get('cut') else []
|
||||
r = subprocess.run([PY, os.path.join(HERE, '_fine.py'), sheet, *outs,
|
||||
'--anchor', str(spec.get('anchor', 0)), '--h', str(spec.get('h', 224)), *cut],
|
||||
capture_output=True, text=True)
|
||||
print(r.stdout.strip() or r.stderr.strip())
|
||||
if r.returncode or not all(os.path.exists(o) for o in outs):
|
||||
print(f'[{name}] REJECTED: slicing failed (frames merged?) — re-run or add "cut"'); return False
|
||||
before = [density(os.path.join(HERE, s + '.png')) for s in sprites]
|
||||
after = [density(o) for o in outs]
|
||||
# 1.25x, not 1.5x: a sheet that already has blood on it starts with a high edge density, so a
|
||||
# genuinely finer redraw of a wounded body scores a smaller gain than a clean one.
|
||||
if sum(after) / len(after) < sum(before) / len(before) * 1.25:
|
||||
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
|
||||
for o, s in zip(outs, sprites):
|
||||
colour_match(o, s)
|
||||
Image.open(o).save(os.path.join(HERE, s + '.png'))
|
||||
print(f'[{name}] installed. detail {before} -> {after}')
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
B = batches()
|
||||
if not args or '--list' in args:
|
||||
for k, v in B.items(): print(f'{k:16s} {len(v["sprites"]):2d} frames {", ".join(v["sprites"])}')
|
||||
sys.exit()
|
||||
dry = '--dry' in args
|
||||
names = list(B) if '--all' in args else [a for a in args if not a.startswith('--')]
|
||||
bad = [n for n in names if not run(n, B[n], dry)]
|
||||
print('\nFAILED BATCHES (re-run):', ' '.join(bad) if bad else 'none')
|
||||
Reference in new issue
Block a user