scripts/pixel_art.py
scripts/pixel_art.pyBrowse 7 files
1,712 tokens
5,811 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1"""Pixel art converter — Floyd-Steinberg dithering with preset or named palette.2 3Named hardware palettes (NES, GameBoy, PICO-8, C64, etc.) ported from4pixel-art-studio (MIT) — see ATTRIBUTION.md.5 6Usage (import):7 from pixel_art import pixel_art8 pixel_art("in.png", "out.png", preset="arcade")9 pixel_art("in.png", "out.png", preset="nes")10 pixel_art("in.png", "out.png", palette="PICO_8", block=6)11 12Usage (CLI):13 python pixel_art.py in.png out.png --preset nes14"""15 16from PIL import Image, ImageEnhance, ImageOps17 18try:19 from .palettes import PALETTES, build_palette_image20except ImportError:21 from palettes import PALETTES, build_palette_image22 23 24PRESETS = {25 # ── Original presets (adaptive palette) ─────────────────────────────26 "arcade": {27 "contrast": 1.8, "color": 1.5, "sharpness": 1.2,28 "posterize_bits": 5, "block": 8, "palette": 16,29 },30 "snes": {31 "contrast": 1.6, "color": 1.4, "sharpness": 1.2,32 "posterize_bits": 6, "block": 4, "palette": 32,33 },34 # ── Hardware-accurate presets (named palette) ───────────────────────35 "nes": {36 "contrast": 1.5, "color": 1.4, "sharpness": 1.2,37 "posterize_bits": 6, "block": 8, "palette": "NES",38 },39 "gameboy": {40 "contrast": 1.5, "color": 1.0, "sharpness": 1.2,41 "posterize_bits": 6, "block": 8, "palette": "GAMEBOY_ORIGINAL",42 },43 "gameboy_pocket": {44 "contrast": 1.5, "color": 1.0, "sharpness": 1.2,45 "posterize_bits": 6, "block": 8, "palette": "GAMEBOY_POCKET",46 },47 "pico8": {48 "contrast": 1.6, "color": 1.3, "sharpness": 1.2,49 "posterize_bits": 6, "block": 6, "palette": "PICO_8",50 },51 "c64": {52 "contrast": 1.6, "color": 1.3, "sharpness": 1.2,53 "posterize_bits": 6, "block": 8, "palette": "C64",54 },55 "apple2": {56 "contrast": 1.8, "color": 1.4, "sharpness": 1.2,57 "posterize_bits": 5, "block": 10, "palette": "APPLE_II_HI",58 },59 "teletext": {60 "contrast": 1.8, "color": 1.5, "sharpness": 1.2,61 "posterize_bits": 5, "block": 10, "palette": "TELETEXT",62 },63 "mspaint": {64 "contrast": 1.6, "color": 1.4, "sharpness": 1.2,65 "posterize_bits": 6, "block": 8, "palette": "MICROSOFT_WINDOWS_PAINT",66 },67 "mono_green": {68 "contrast": 1.8, "color": 0.0, "sharpness": 1.2,69 "posterize_bits": 5, "block": 6, "palette": "MONO_GREEN",70 },71 "mono_amber": {72 "contrast": 1.8, "color": 0.0, "sharpness": 1.2,73 "posterize_bits": 5, "block": 6, "palette": "MONO_AMBER",74 },75 # ── Artistic palette presets ────────────────────────────────────────76 "neon": {77 "contrast": 1.8, "color": 1.6, "sharpness": 1.2,78 "posterize_bits": 5, "block": 6, "palette": "NEON_CYBER",79 },80 "pastel": {81 "contrast": 1.2, "color": 1.3, "sharpness": 1.1,82 "posterize_bits": 6, "block": 6, "palette": "PASTEL_DREAM",83 },84}85 86 87def pixel_art(input_path, output_path, preset="arcade", **overrides):88 """Convert an image to retro pixel art.89 90 Args:91 input_path: path to source image92 output_path: path to save the resulting PNG93 preset: one of PRESETS (arcade, snes, nes, gameboy, pico8, c64, ...)94 **overrides: optionally override any preset field. In particular:95 palette: int (adaptive N colors) OR str (named palette from PALETTES)96 block: int pixel block size97 contrast / color / sharpness / posterize_bits: numeric enhancers98 99 Returns:100 The resulting PIL.Image.101 """102 if preset not in PRESETS:103 raise ValueError(104 f"Unknown preset {preset!r}. Choose from: {sorted(PRESETS)}"105 )106 cfg = {**PRESETS[preset], **overrides}107 108 img = Image.open(input_path).convert("RGB")109 110 img = ImageEnhance.Contrast(img).enhance(cfg["contrast"])111 img = ImageEnhance.Color(img).enhance(cfg["color"])112 img = ImageEnhance.Sharpness(img).enhance(cfg["sharpness"])113 img = ImageOps.posterize(img, cfg["posterize_bits"])114 115 w, h = img.size116 block = cfg["block"]117 small = img.resize(118 (max(1, w // block), max(1, h // block)),119 Image.NEAREST,120 )121 122 # Quantize AFTER downscale so Floyd-Steinberg aligns with final pixel grid.123 pal = cfg["palette"]124 if isinstance(pal, str):125 # Named hardware/artistic palette126 pal_img = build_palette_image(pal)127 quantized = small.quantize(palette=pal_img, dither=Image.FLOYDSTEINBERG)128 else:129 # Adaptive N-color palette (original behavior)130 quantized = small.quantize(colors=int(pal), dither=Image.FLOYDSTEINBERG)131 132 result = quantized.resize((w, h), Image.NEAREST)133 result.save(output_path, "PNG")134 return result135 136 137def main():138 import argparse139 p = argparse.ArgumentParser(description="Convert image to pixel art.")140 p.add_argument("input")141 p.add_argument("output")142 p.add_argument("--preset", default="arcade", choices=sorted(PRESETS))143 p.add_argument("--palette", default=None,144 help=f"Override palette: int or name from {sorted(PALETTES)}")145 p.add_argument("--block", type=int, default=None)146 args = p.parse_args()147 148 overrides = {}149 if args.palette is not None:150 try:151 overrides["palette"] = int(args.palette)152 except ValueError:153 overrides["palette"] = args.palette154 if args.block is not None:155 overrides["block"] = args.block156 157 pixel_art(args.input, args.output, preset=args.preset, **overrides)158 print(f"Wrote {args.output}")159 160 161if __name__ == "__main__":162 main()163