Colour is arithmetic: match each frame onto the anchor instead of asking for it
The character-as-image fix landed and did NOT close the gap, which is the answer I promised to report rather than paper over. The brute's serve sheet came back with his build, his garments and his DARK TONES PIXEL-IDENTICAL to the anchor — (56,56,32) both — and his big pale shirt two shades brighter, (200,200,184) against (176,176,152). The model was looking straight at the character. It held identity perfectly and re-lit the largest flat area anyway, because every call re-renders and re-rendering re-lights. No prompt fixes that. Six gates spent a day trying to make a generator do arithmetic. So the arithmetic happens here now. Pair each of a frame's dominant colours with its nearest anchor colour, fit one gain+offset per channel across those pairs, apply it to every pixel. Pairs further apart than NEW_COLOUR are dropped from the FIT — colours the anchor genuinely has not got, i.e. blood on a fresh wound — but they still RECEIVE the transform, so a wound stays the right red relative to the body it is on. The gain is clamped so one odd pair cannot posterise a frame. PROVEN ON THE REJECTED FRAME BEFORE SPENDING A SINGLE CALL, which is the discipline I skipped all day: the frame that measured 20.0 away passes after correction, its shirt pulled from (200,200,184) to (176,176,160). And the order is now CORRECT, THEN CHECK — the palette gate is a safety net for what arithmetic could not fix, not a tripwire for what it can. The model this leaves: the generator, given references -> identity, pose, composition the pipeline, deterministically -> exact colour the gates -> layout accidents only Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
525f017636
commit
a58e8b18aa
11 files changed
+81
-9
No files matched your search
+38
-6
@@ -65,6 +65,7 @@ def build(name, frames, scale=2, gap=90, pad=70):
|
||||
# master: the reference specifies eleven poses that already work, and the prompt only says who.
|
||||
import json, shutil, collections
|
||||
|
||||
# THE FIRST SHEET describes the character in WORDS, because at that point no picture of it exists.
|
||||
RESTYLE = (
|
||||
"The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE "
|
||||
"SHEET as a COMPLETELY DIFFERENT ZOMBIE: {who} "
|
||||
@@ -75,6 +76,28 @@ RESTYLE = (
|
||||
"There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and "
|
||||
"the air are empty and the poses are the same as if a ball were there. "
|
||||
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
|
||||
|
||||
# ...AND EVERY SHEET AFTER IT DESCRIBES THE CHARACTER WITH A PICTURE OF IT. This is the whole fix.
|
||||
# The brute's three sheets each restyled from the master independently and each re-derived "pale
|
||||
# grey-green work shirt" from the same paragraph, so the game got three different brutes and the
|
||||
# palette gate — correctly — refused two of them. A paragraph is a lossy specification of a
|
||||
# character; the character is not.
|
||||
#
|
||||
# It is also the pattern that already worked: the racket zombie's own play sheet was generated from
|
||||
# [z_c1, o_idle1, o_run1, o_swing1] — the character as an IMAGE, the poses as IMAGES — in one call,
|
||||
# first time. I had it, and then did not use it for variants.
|
||||
RESTYLE_KNOWN = (
|
||||
"The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for "
|
||||
"who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that "
|
||||
"character exactly. "
|
||||
"The REMAINING reference images are {n} POSES, and are there ONLY to show what the body is doing "
|
||||
"— never who it is. "
|
||||
"Draw THE CHARACTER FROM THE FIRST IMAGE in each of those {n} poses, in the same order, at the "
|
||||
"same size and the same distance from the camera, all facing the same way. The character never "
|
||||
"changes; only the pose does. "
|
||||
"There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and "
|
||||
"the air are empty and the poses are the same as if a ball were there. "
|
||||
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
|
||||
STRIDE_NOTE = ("In particular frames 2 and 3 must stay what they are in the reference: ONE DRAWING "
|
||||
"SHOWN TWICE with only the legs different — everything above the hips identical "
|
||||
"between them, the legs clearly swapped, and the two frames NOT identical copies. ")
|
||||
@@ -117,12 +140,16 @@ def variant(key, who):
|
||||
seed = lambda src, dst: shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png'))
|
||||
made = []
|
||||
|
||||
def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False):
|
||||
def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False, lead=False):
|
||||
# `lead` means the first reference is THE CHARACTER and not a pose — it is shown to the
|
||||
# generator and is not one of the frames asked for, so it is skipped when pairing references
|
||||
# to outputs (both for seeding and for the run-time baseline in _regen).
|
||||
pose_refs = refs[1:] if lead else refs
|
||||
# Only the sheets drawn off the MASTER are seeded here. A chained wound stage is seeded by
|
||||
# _regen at run time, from whatever its reference has become by then — seeding it now would
|
||||
# score it against a copy of the master, two steps better than the stage it follows.
|
||||
if newart:
|
||||
for src, dst in zip(refs, sprites):
|
||||
for src, dst in zip(pose_refs, sprites):
|
||||
seed(src, dst)
|
||||
spec = dict(sprites=sprites, ref_only=list(refs), check='loose', anchor=0, subject=subject)
|
||||
spec['newart' if newart else 'state'] = True
|
||||
@@ -139,11 +166,16 @@ def variant(key, who):
|
||||
print(f'{name:16s} <- {refs[0]}… {", ".join(sprites)}')
|
||||
|
||||
K = 'z_' + key
|
||||
# --- the whole body, restyled off the master
|
||||
# --- the whole body. The PLAY sheet invents the character from the words; serve and fall are then
|
||||
# drawn from the character it produced (K+'1' leads their reference list) plus the master's poses.
|
||||
# That ordering is why the three must be run in the order printed at the end.
|
||||
for part, frames in SHEETS.items():
|
||||
batch(f'{key}_{part}', [K + f[3:] for f in frames], frames,
|
||||
RESTYLE.format(n=len(frames), who=who) + (STRIDE_NOTE if part == 'play' else ''),
|
||||
stride=[[1, 2]] if part == 'play' else None, newart=True)
|
||||
first = part == 'play'
|
||||
refs = frames if first else [K + '1'] + frames
|
||||
batch(f'{key}_{part}', [K + f[3:] for f in frames], refs,
|
||||
(RESTYLE.format(n=len(frames), who=who) + STRIDE_NOTE) if first
|
||||
else RESTYLE_KNOWN.format(n=len(frames)),
|
||||
stride=[[1, 2]] if first else None, newart=True, lead=not first)
|
||||
|
||||
# --- the two wound stages, each chained off the one above it
|
||||
WALK = ['2', '3', '4'] # shuffle A, shuffle B, forehand — the wound rows' three frames
|
||||
|
||||
Reference in new issue
Block a user