ONE palette gate replaces four feature-picked ones
Oliver, on the brute's fall frames: "now the fall zombie has a different skin tone.. man this is all so tiresome." Both halves were right. The skin IS twenty points lighter across that sheet, and the tiresome part is mine: I had been adding one hand-picked gate per drift I discovered — redness, then hue, then `changed`, then no-grids — which is a loop with no end, because naming features is not the same as comparing pictures. `kit` names four: shirt redness, shorts brightness, shorts hue, near-white fraction. Every character breaks a different one. The brute's shirt has no red in it at all, so its reference redness was 0 and blood scored 70-100 against a tolerance meant for a garment. A body face down shows a fraction of the shirt it shows standing, so two of the four move with POSE. And nothing in that list ever measured SKIN, which is exactly how a brute's flesh drifted across a whole sheet with six gates green. The palette gate asks the only question that generalises: are the big colour areas of this frame the same colours as the anchor's? Top SIX by area — the garments and the skin. Proportions ignored, so a pose change is invisible; blood is never in the top six, so a fresh wound cannot look like drift. CALIBRATED BEFORE IT WAS WIRED IN, which is the discipline I skipped all day. Frames known to be right score 0.0-8.5; the drifts we could actually see score 12.0-15.9. The threshold is 10, in the gap. Three formulations were measured and the two that overlapped were thrown away. And every sheet of a variant is now anchored to that variant's FIRST sheet, not to the stage above it. Chaining let a legal hop compound into a drift nobody authorised — the shorts walking -9 to -19 down the wound ladder was that, and so was this. Its first verdict on the existing brute: br_play is the character, and every other sheet sits 15 to 48 away from it. It would have caught all of this on the first roll. Also in: the GRID guard from the same session — the valley-cut rescue would slice a 2x2 sheet into four frames of a torso above someone else's boots, turning a loud failure into a silent one. Asked per cut PIECE, because a body is vertically continuous and a piece with a gap in it caught two of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df454c5d92
commit
0e92c72457
33 files changed
+312
-19
No files matched your search
+81
-9
@@ -12,7 +12,7 @@
|
||||
# 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 json, os, shutil, subprocess, sys
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
@@ -147,6 +147,13 @@ def kit(path):
|
||||
# NOTE the fourth tolerance: kit() returns four numbers and kit_off zips them against this, so a
|
||||
# three-long tuple would silently skip the hue check rather than fail — the same shape of no-op the
|
||||
# read-only floodfill in _fine.py was. Every caller passing its own `palette_tol` needs four too.
|
||||
# A measure only means something if the garment it measures EXISTS. The brute wears a pale grey-green
|
||||
# work shirt with no red in it at all, so the reference's redness is 0 and every roll with a spot of
|
||||
# blood on it scored 70-100 — twenty-six points of tolerance against a number that was never
|
||||
# measuring a shirt. Below this floor `redness` is reading BLOOD, which is supposed to change between
|
||||
# wound stages, so the axis is skipped rather than fought. The tolerances in a batch were calibrated
|
||||
# on a red-shirted zombie; a variant inherits them and must not inherit their assumptions.
|
||||
RED_FLOOR = 40
|
||||
def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)):
|
||||
"""Is this roll wearing the right kit? Generative rolls drift the palette EVERY TIME and nothing
|
||||
else here catches it: colour_match only runs for `check: finer`, and it is a global mean/sd fit
|
||||
@@ -154,9 +161,10 @@ def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)):
|
||||
headband, the toss shipped twice in pale grey shorts, and a swing pair shipped in brown shoes.
|
||||
Compared against a canonical frame that is NOT part of the batch (o_idle1 for the opponent)."""
|
||||
ref = kit(os.path.join(HERE, ref_name + '.png'))
|
||||
skip = [ref[0] < RED_FLOOR, False, False, False] # no red garment -> redness means nothing
|
||||
for o in outs:
|
||||
got = kit(o)
|
||||
if any(abs(g - r) > t for g, r, t in zip(got, ref, tol)):
|
||||
if any(abs(g - r) > t for i, (g, r, t) in enumerate(zip(got, ref, tol)) if not skip[i]):
|
||||
return (f'kit drifted vs {ref_name}: redness/shorts/bright {got} vs {ref} '
|
||||
f'(tolerance {tol})')
|
||||
return None
|
||||
@@ -208,11 +216,18 @@ def changed_off(outs, refs, want):
|
||||
for o, r in zip(outs, refs):
|
||||
rp = os.path.join(HERE, r + '.png')
|
||||
if not os.path.exists(rp): continue
|
||||
_, _, ma, mb, _ = _masks(o, rp)
|
||||
d = _d(ma, mb)
|
||||
if d < want:
|
||||
_, _, ma, mb, h = _masks(o, rp)
|
||||
# WHOLE BODY *OR* UPPER BODY, whichever moved more. An arm is a body-relative amount of
|
||||
# silhouette: on the lean club pro it is 12-38% of him, on a brute twice his width it is 9%,
|
||||
# so a threshold calibrated on one rejects a perfectly good amputation on the other. Above
|
||||
# the hip is where an arm lives, and there the same removal reads roughly twice as loud —
|
||||
# the stride gate splits at the same line for the same reason.
|
||||
hip = int(h * STRIDE_HIP)
|
||||
d, up = _d(ma, mb), _d(ma[:hip], mb[:hip])
|
||||
if max(d, up) < want:
|
||||
return (f'{os.path.basename(o)} is {r} redrawn — only {d:.3f} of the silhouette differs '
|
||||
f'(want >= {want}). The thing that was supposed to change did not change.')
|
||||
f'({up:.3f} above the hip, want >= {want}). '
|
||||
f'The thing that was supposed to change did not change.')
|
||||
return None
|
||||
|
||||
def stride_off(outs, pairs):
|
||||
@@ -227,6 +242,41 @@ def stride_off(outs, pairs):
|
||||
f'want ~1.00 / <={STRIDE_TOP} / >={STRIDE_LEG})')
|
||||
return None
|
||||
|
||||
# ---- THE PALETTE GATE, which replaces four hand-picked ones. `kit` names features — shirt redness,
|
||||
# shorts brightness, shorts hue, near-white fraction — and every character breaks a different one:
|
||||
# the brute's shirt has no red in it, a body face down shows a fraction of the shirt it shows
|
||||
# standing, and NOTHING in that list ever measured SKIN, which is how a brute's flesh got twenty
|
||||
# points lighter across its fall sheet without a single gate noticing.
|
||||
#
|
||||
# This asks the only question that generalises: are the big colour areas of this frame the same
|
||||
# colours as the anchor's? Top SIX by area — the garments and the skin. Proportions are ignored, so a
|
||||
# pose change is invisible; blood is never in the top six, so a fresh wound cannot look like drift.
|
||||
# Calibrated on frames known to be right (0.0-8.5) against the drifts we could see (12.0-15.9), so
|
||||
# the default sits in the gap. One number, no per-character tuning.
|
||||
def dom_colours(path, n=6, q=8, floor=0.005):
|
||||
a = np.array(Image.open(path).convert('RGBA')).astype(int)
|
||||
px = a[..., :3][a[..., 3] > 128]
|
||||
if not len(px): return np.zeros((0, 3)), np.zeros(0)
|
||||
u, c = np.unique((px // q) * q, axis=0, return_counts=True)
|
||||
k = c / c.sum() >= floor
|
||||
u, c = u[k], c[k] / c[k].sum()
|
||||
o = np.argsort(-c)[:n]
|
||||
return u[o], c[o] / c[o].sum()
|
||||
|
||||
def palette_off(outs, anchor, want):
|
||||
ap = os.path.join(HERE, anchor + '.png')
|
||||
if not os.path.exists(ap): return None
|
||||
ua, _ = dom_colours(ap)
|
||||
if not len(ua): return None
|
||||
for o in outs:
|
||||
uo, wo = dom_colours(o)
|
||||
if not len(uo): continue
|
||||
d = float((np.sqrt(((uo[:, None, :] - ua[None, :, :]) ** 2).sum(2)).min(1) * wo).sum())
|
||||
if d > want:
|
||||
return (f'{os.path.basename(o)} is not painted in {anchor}\'s colours — its main areas sit '
|
||||
f'{d:.1f} away (want <= {want}). Something the size of a garment or the skin changed.')
|
||||
return None
|
||||
|
||||
def not_magenta(sheet):
|
||||
"""The generator sometimes ignores the magenta and paints its own background. That was written
|
||||
off as harmless — the key uses the corner colour — right up until it returned a GREEN sheet and
|
||||
@@ -300,6 +350,15 @@ def run(name, spec, dry=False):
|
||||
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
|
||||
if lead: outs = outs[1:] # the scale reference is not installed
|
||||
# A CHAINED BATCH'S BASELINE IS ITS REFERENCE, MEASURED NOW. Seeding every placeholder up front
|
||||
# meant a wound stage was scored against a copy of the MASTER — art two steps better than the
|
||||
# stage it actually follows — and three brute batches were rejected for not out-detailing a
|
||||
# sheet they were never drawn from.
|
||||
refs0 = spec.get('ref_only') or []
|
||||
if len(refs0) == len(sprites):
|
||||
for r, t in zip(refs0, sprites):
|
||||
tp, rp = (os.path.join(HERE, n + '.png') for n in (t, r))
|
||||
if not os.path.exists(tp) and os.path.exists(rp): shutil.copy(rp, tp)
|
||||
before = [density(os.path.join(HERE, s + '.png')) for s in sprites]
|
||||
after = [density(o) for o in outs]
|
||||
# Two jobs, two bars. A coarse->fine REDRAW has to be measurably finer (1.25x, not 1.5x: a sheet
|
||||
@@ -315,7 +374,11 @@ def run(name, spec, dry=False):
|
||||
bar = {'finer': 1.25, 'same': 0.85, 'loose': 0.7, 'any': 0.0}[spec.get('check', 'finer')]
|
||||
if sum(after) / len(after) < sum(before) / len(before) * bar:
|
||||
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
|
||||
if spec.get('palette_ref'):
|
||||
# the palette gate supersedes the feature-picked kit gate wherever a batch declares an anchor
|
||||
if spec.get('palette_anchor'):
|
||||
bad = palette_off(outs, spec['palette_anchor'], spec.get('palette_max', 10))
|
||||
if bad: print(f'[{name}] REJECTED: {bad}'); return False
|
||||
elif spec.get('palette_ref'):
|
||||
bad = kit_off(outs, spec['palette_ref'], tuple(spec.get('palette_tol', (12, 10, 1.5))))
|
||||
if bad: print(f'[{name}] REJECTED: {bad}'); return False
|
||||
if spec.get('stride'):
|
||||
@@ -363,11 +426,20 @@ if __name__ == '__main__':
|
||||
for a in args: # frames merged, or a limb off the canvas
|
||||
if a.startswith('--tries='): tries = int(a.split('=')[1])
|
||||
names = list(B) if '--all' in args else [a for a in args if not a.startswith('--')]
|
||||
bad = []
|
||||
bad, stale = [], set()
|
||||
for n in names:
|
||||
# A CHAINED BATCH IS DRAWN FROM THE STAGE ABOVE IT, so if that stage failed its reference is
|
||||
# still a placeholder and whatever comes back is drawn from the wrong body. The brute's
|
||||
# headless sheet installed cleanly off an arm-off stage that never landed — three perfect
|
||||
# frames of a two-armed corpse. Skipping is the only honest answer; running is worse than
|
||||
# failing, because it SUCCEEDS.
|
||||
blocked = [r for r in (B[n].get('ref_only') or []) if r in stale]
|
||||
if blocked:
|
||||
print(f'[{n}] SKIPPED: drawn from {", ".join(blocked)}, which did not land')
|
||||
bad.append(n); stale.update(B[n]['sprites']); continue
|
||||
for attempt in range(1, tries + 1):
|
||||
if run(n, B[n], dry): break
|
||||
print(f'[{n}] attempt {attempt}/{tries} rejected')
|
||||
else:
|
||||
bad.append(n)
|
||||
bad.append(n); stale.update(B[n]['sprites'])
|
||||
print('\nFAILED BATCHES (re-run):', ' '.join(bad) if bad else 'none')
|
||||
Reference in new issue
Block a user