hermes-agent/optional-skills/creative/pixel-art/SKILL.md
Teknium bbc8499e8c refactor(creative): consolidate pixel-art skills into single preset-based skill
Merges pixel-art-arcade and pixel-art-snes into one pixel-art skill with
named presets (arcade, snes) + parametric overrides. The underlying
pipeline was already identical across both variants — only palette size,
block size, and enhancement strength differed. A single preset-based
function is easier to discover, maintain, and extend (adding a new era
like gameboy or nes is just another preset dict).

Contributor authorship preserved on original additive commit.
2026-04-19 11:57:51 -07:00

5.9 KiB

name description version author license metadata
pixel-art Convert images into retro pixel art using named presets (arcade, snes) with Floyd-Steinberg dithering. Arcade is bold and chunky; SNES is cleaner with more detail retention. 1.2.0 dodo-reach MIT
hermes
tags category
creative
pixel-art
arcade
snes
retro
image
creative

Pixel Art

Convert any image into retro-style pixel art. One function with named presets that select different aesthetics:

  • arcade — 16-color palette, 8px blocks. Bold, chunky, high-impact. 80s/90s arcade cabinet feel.
  • snes — 32-color palette, 4px blocks. Cleaner 16-bit console look with more detail retention.

The core pipeline is identical across presets — what changes is palette size, block size, and the strength of contrast/color/posterize pre-processing. All presets use Floyd-Steinberg dithering applied AFTER downscale so error diffusion aligns with the final pixel grid.

When to Use

  • User wants retro pixel art from a source image
  • Posters, album covers, social posts, sprites, characters, backgrounds
  • Subject can tolerate aggressive simplification (arcade) or benefits from retained detail (snes)

Preset Picker

Preset Palette Block Best for
arcade 16 colors 8px Posters, hero images, bold covers, simple subjects
snes 32 colors 4px Characters, sprites, detailed illustrations, photos

Default is arcade for maximum stylistic punch. Switch to snes when the subject has detail worth preserving.

Procedure

  1. Pick a preset (arcade or snes) based on the aesthetic you want.
  2. Boost contrast, color, and sharpness using the preset's enhancement values.
  3. Lightly posterize the image to simplify tonal regions before quantization.
  4. Downscale to w // block by h // block with Image.NEAREST.
  5. Quantize the reduced image to the preset's palette size with Floyd-Steinberg dithering.
  6. Upscale back to the original size with Image.NEAREST.
  7. Save the output as PNG.

Code

from PIL import Image, ImageEnhance, ImageOps

PRESETS = {
    "arcade": {
        "contrast": 1.8,
        "color": 1.5,
        "sharpness": 1.2,
        "posterize_bits": 5,
        "block": 8,
        "palette": 16,
    },
    "snes": {
        "contrast": 1.6,
        "color": 1.4,
        "sharpness": 1.2,
        "posterize_bits": 6,
        "block": 4,
        "palette": 32,
    },
}


def pixel_art(input_path, output_path, preset="arcade", **overrides):
    """
    Convert an image to retro pixel art.

    Args:
        input_path: path to source image
        output_path: path to save the resulting PNG
        preset: "arcade" or "snes"
        **overrides: optionally override any preset field
                     (contrast, color, sharpness, posterize_bits, block, palette)

    Returns:
        The resulting PIL.Image.
    """
    if preset not in PRESETS:
        raise ValueError(
            f"Unknown preset {preset!r}. Choose from: {sorted(PRESETS)}"
        )

    cfg = {**PRESETS[preset], **overrides}

    img = Image.open(input_path).convert("RGB")

    # Stylistic boost — stronger for smaller palettes
    img = ImageEnhance.Contrast(img).enhance(cfg["contrast"])
    img = ImageEnhance.Color(img).enhance(cfg["color"])
    img = ImageEnhance.Sharpness(img).enhance(cfg["sharpness"])

    # Light posterization separates tonal regions before quantization
    img = ImageOps.posterize(img, cfg["posterize_bits"])

    w, h = img.size
    block = cfg["block"]
    small = img.resize(
        (max(1, w // block), max(1, h // block)),
        Image.NEAREST,
    )

    # Quantize AFTER downscaling so dithering aligns with the final pixel grid
    quantized = small.quantize(
        colors=cfg["palette"], dither=Image.FLOYDSTEINBERG
    )
    result = quantized.resize((w, h), Image.NEAREST)

    result.save(output_path, "PNG")
    return result

Example Usage

# Bold arcade look (default)
pixel_art("/path/to/image.jpg", "/path/to/arcade.png")

# Cleaner SNES look with more detail
pixel_art("/path/to/image.jpg", "/path/to/snes.png", preset="snes")

# Override individual parameters — e.g. tighter palette with SNES block size
pixel_art(
    "/path/to/image.jpg",
    "/path/to/custom.png",
    preset="snes",
    palette=16,
)

Why This Order Works

Floyd-Steinberg dithering distributes quantization error to adjacent pixels. Applying it AFTER downscaling keeps that error diffusion aligned with the reduced pixel grid, so each dithered pixel maps cleanly to a final enlarged block. Quantizing before downscaling wastes the dithering pattern on full-resolution detail that disappears during resize.

A light posterization step before downscaling improves separation between tonal regions, which helps photographic inputs read as stylized pixel art instead of simple pixelated photos.

Stronger pre-processing (higher contrast/color) pairs with smaller palettes because fewer colors have to carry the whole image. SNES runs softer enhancements because 32 colors can represent gradients and mid-tones directly.

Pitfalls

  • arcade 8px blocks are aggressive and can destroy fine detail — use snes for subjects that need retention
  • Busy photographs can become noisy under snes because the larger palette preserves small variations — use arcade to flatten them
  • Very small source images (<~100px wide) may collapse under 8px blocks. max(1, w // block) guards against zero dimensions, but output will be visually degenerate.
  • Fractional overrides for block or palette will break quantization — keep them as positive integers.

Verification

Output is correct if:

  • A PNG file is created at the output path
  • The image shows clear square pixel blocks at the preset's block size
  • Dithering is visible in gradients
  • The palette is limited to approximately the preset's color count
  • The overall look matches the targeted era (arcade or SNES)

Dependencies

  • Python 3
  • Pillow
pip install Pillow