# slice.py — chroma-key a generated sprite sheet on its corner color, split into # frames on column gaps, common-scale the sheet so the tallest frame = IDLE_TARGET_H, # save each frame to the given output paths. Usage: slice.py sheet.png out1.png out2.png ... import sys from PIL import Image IDLE_TARGET_H = 224 # 2x remaster: the player renders at ~170 DEVICE px (RES 3), so 112 was being upscaled KEY_D2 = 60 * 60 # squared RGB distance to the bg color that still counts as background MIN_GAP = 12 # empty columns that separate frames MIN_BLOB = 60 # despeckle: drop opaque islands smaller than this many pixels def main(sheet_path, out_paths, largest_only=False): im = Image.open(sheet_path).convert('RGBA') w, h = im.size px = im.load() # background = corner color (average the four corners that agree with the top-left) bg = px[0, 0][:3] for x in range(w): for y in range(h): r, g, b, a = px[x, y] if (r - bg[0]) ** 2 + (g - bg[1]) ** 2 + (b - bg[2]) ** 2 < KEY_D2 \ or (min(r, b) > g + 40): # de-fringe: magenta-hued halo pixels (sprites have no magenta) px[x, y] = (0, 0, 0, 0) # despeckle: flood-fill opaque components; wipe tiny ones (halos, dust). # largest_only additionally wipes everything but the biggest blob (stray fragments # from splitting a sheet whose frames touch — e.g. the brute's dragged pole). seen = [[False] * h for _ in range(w)] blobs = [] for x in range(w): for y in range(h): if seen[x][y] or px[x, y][3] == 0: continue stack, blob = [(x, y)], [] seen[x][y] = True while stack: cx, cy = stack.pop() blob.append((cx, cy)) for nx, ny in ((cx+1,cy),(cx-1,cy),(cx,cy+1),(cx,cy-1)): if 0 <= nx < w and 0 <= ny < h and not seen[nx][ny] and px[nx, ny][3] > 0: seen[nx][ny] = True; stack.append((nx, ny)) blobs.append(blob) keep = max(blobs, key=len) if (largest_only and blobs) else None for blob in blobs: if blob is keep: continue if largest_only or len(blob) < MIN_BLOB: for cx, cy in blob: px[cx, cy] = (0, 0, 0, 0) # segment on empty column runs cols = [any(px[x, y][3] for y in range(h)) for x in range(w)] frames, start, gap = [], None, 0 for x in range(w): if cols[x]: if start is None: start = x gap = 0 elif start is not None: gap += 1 if gap >= MIN_GAP: frames.append((start, x - gap + 1)); start = None if start is not None: frames.append((start, w)) crops = [] for x0, x1 in frames: c = im.crop((x0, 0, x1, h)) crops.append(c.crop(c.getbbox())) if len(crops) != len(out_paths): sys.exit(f'ERROR: found {len(crops)} frames, expected {len(out_paths)} ({sheet_path})') k = IDLE_TARGET_H / max(c.height for c in crops) # one scale per sheet keeps relative frame sizes for c, out in zip(crops, out_paths): c.resize((max(1, round(c.width * k)), max(1, round(c.height * k))), Image.NEAREST).save(out) print(f'{out}: {round(c.width * k)}x{round(c.height * k)}') if __name__ == '__main__': args = [a for a in sys.argv[1:] if a != '--largest'] main(args[0], args[1:], largest_only='--largest' in sys.argv)