Show the generator what to copy, not what to replace

The reference sheet is what it imitates, so building that sheet out of the very
sprites I was trying to change is why the front-facing player came back
front-facing and the side-on runner came back side-on. A batch can now name
`ref_only` — the frames it looks at — separately from the frames it replaces:
the back view is drawn from the one genuine back-view frame, the charging runner
from a front-facing zombie, and each brute wound pair from its own A frame so the
injury cannot wander between the two.

Also stopped colour-matching a REPOSE. Fitting a differently-posed frame onto the
old one's colour statistics is what greyed out the serve shorts; when the pose
changes the generator's own palette is the better answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-31 22:36:13 +02:00
1 parent 01498eddad
commit 67a410e707
4 files changed
+96 -16

No files matched your search

+19 -6
View File
@@ -61,10 +61,18 @@ def density(path):
return round(float(((d > 40) & solid[:, :-1]).sum() / max(1, solid.sum())), 3)
def touches_edge(sheet):
"""A CLIPPED figure, not a speck of noise: a run of 8+ ink pixels along a border. The strict
version rejected sheets over single stray pixels and cost several regenerations."""
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 run(line):
n = best = 0
for v in line:
n = n + 1 if v else 0
best = max(best, n)
return best
return max(run(fg[0]), run(fg[-1]), run(fg[:, 0]), run(fg[:, -1])) >= 8
def colour_match(src, dst_name):
"""Fit the new frame onto the palette statistics of the sprite it replaces, so a redrawn body
@@ -83,7 +91,9 @@ def run(name, spec, dry=False):
os.makedirs(SCRATCH, exist_ok=True)
sprites = spec['sprites']
lead = spec.get('ref') # generated for scale, thrown away after
ref = ref_sheet(([lead] if lead else []) + sprites, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0))
shown = spec.get('ref_only') or (([lead] if lead else []) + sprites)
if spec.get('ref_only'): lead = None # ref_only sheets return exactly the frames asked for
ref = ref_sheet(shown, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0))
sheet = f'{SCRATCH}/{name}_hi.png'
prompt = f"{spec['subject']} {STYLE}"
if dry:
@@ -106,12 +116,15 @@ def run(name, spec, dry=False):
if lead: outs = outs[1:] # the scale reference is not installed
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:
# Two jobs, two bars. A coarse->fine REDRAW has to be measurably finer (1.25x, not 1.5x: a sheet
# that already has blood on it starts high, so a genuinely finer wounded body gains less). A
# REPOSE — turning the player around, pointing the runner at the camera, matching a wound between
# two frames — is already fine art and only has to come back no worse.
bar = 1.25 if spec.get('check', 'finer') == 'finer' else 0.85
if sum(after) / len(after) < sum(before) / len(before) * bar:
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)
if spec.get('check', 'finer') == 'finer': colour_match(o, s)
Image.open(o).save(os.path.join(HERE, s + '.png'))
print(f'[{name}] installed. detail {before} -> {after}')
return True