Pixel Art Studio
The pixel-art node that knows the model didn't make pixel art — it makes this
- frames
- pixel_art
There's a well-earned community consensus on AI pixel art, and it's that diffusion models don't produce it - they produce something that looks like it from across the room: off-grid, blurry, hundreds of colors, inconsistent pixel sizes. Real pixel art is a deterministic post-process: grid-aware downscale, palette quantization, then clean upscaling. Pixel Art Studio is that entire pipeline in one node, and it's built for the case the consensus usually ignores - doing it to a video sequence without the result boiling and flickering frame to frame.
If you've ever rendered something in a pixel-art style and zoomed in to find your "pixels" aren't uniform or your palette is 300 colors, this is the fix. Feed it frames, get back clean, crisp, limited-palette pixel art.
How it works
It's three stages fused, all with temporal stability bolted on:
- Block reduction. Each block of
pixel_size×pixel_sizepixels is collapsed to one color via a per-block winner-take-all vote over 32 color bins - dominant-color voting, not averaging, which is exactly what the KB's pixel-art doctrine says or you get mush.kernel_size(default 5, equal topixel_size) controls the sampling window: equal means exact tiling, larger overlaps neighboring blocks for a softer, chunkier look. - Global palette. A k-means palette is fit once across all frames on the reduced blocks (256/128/64/48 colors), so the palette can't flicker frame to frame. Every frame's art pixels snap to the nearest palette entry in Oklab, with per-pixel temporal hysteresis so a static pixel doesn't pop a pixel thicker/thinner when its color drifts across a palette boundary.
- Output sizing. Optional
width/height(0 = input size), resized with nearest-to-integer-multiple + box down so the pixel grid stays uniform at fractional scales.
The video-first thinking shows everywhere: stability (0–1) is temporal hysteresis on the block vote - a block keeps its previous winner unless clearly beaten. And loop does a hidden warm-up pass so looping footage doesn't snap at the last-to-first wrap. Turn loop off for non-looping video.
Inputs that matter
frames(IMAGE) - can be a single image or a video batch.pixel_size(default 5) - the chunky-ness. Higher = chunkier.num_colors(256/128/64/48) - the palette budget. 256 keeps it painterly; 48 reads as properly retro.edge_style-hard(default) = crisp sprite edges with binary alpha, matching hand-drawn assets;soft= area-averaged, antialiased look. For sprites,hard.stability(default 1) - video smoothness. 0 disables hysteresis.loop(default true) - seamless loops; disable for non-looping footage.alpha_threshold(default 0.58) - for alpha input; where "50% opaque" sits in the graded coverage.
Output is pixel_art (IMAGE), same batch as input. Frames with an embedded alpha channel come out with one.
Installing it
Part of Link Comfy Nodes:
cd ComfyUI/custom_nodes
git clone https://github.com/Mister-Link/link-comfy-nodes
cd link-comfy-nodes
pip install -r requirements.txt
or ComfyUI Manager → "Link Comfy Nodes", restart. The palette stage needs scikit-learn (it's in requirements.txt), which you won't have from stock ComfyUI - that's the most common install stumble.
Common issues
- Import error about
sklearn- install the pack's requirements; scikit-learn doesn't come with ComfyUI. - Stills look fine but video shimmers - raise
stabilitytoward 1. The hysteresis is what kills the boil. - Looping footage has a visible seam -
loopdefaults to true for a reason; if you turned it off, turn it back on. Conversely, non-looping footage withloopon gets a warm-up pass that wastes a little time for no benefit. - Pixels aren't uniform at fractional output sizes - the node handles it internally, but if you set a
width/heightthat isn't a clean multiple, expect the grid to be nearest-integer rather than perfectly square.
One thing it won't do is invent a period-correct palette like PICO-8's 16 colors - you get k-means at your chosen budget, not a named hardware palette. For everything else - grid, palette, stability - this is the single node that replaces the three-step dance most people string together manually.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| frames | IMAGE | — | |
| pixel_size | INT | 51–128 | — |
| kernel_size | INT | 51–128 | Sampling window per block. Equal to pixel_size = exact tiling; larger overlaps neighboring blocks for a softer, chunkier look. |
| num_colors | COMBO | 256 | Global palette size, fit once across all frames on the reduced blocks (no per-frame palette flicker). |
| edge_style | COMBO | hard | hard = crisp pixel-art edges: the art grid is reduced by nearest sampling (no invented in-between colors along contours) and output alpha is snapped to fully opaque/transparent, matching hand-drawn sprite edges. soft = area-averaged reduce and graded alpha (antialiased look). |
| alpha_threshold | FLOAT | 0.580–1 | Coverage level treated as ~50% opacity for the graded output alpha. 0 = raw coverage. |
| stability | FLOAT | 1.000–1 | Temporal hysteresis on the block vote (video): previous winner keeps a block unless clearly beaten. 0 disables. |
| loop | BOOLEAN | true | Treat the batch as a seamless loop: every temporal mechanism (vote hysteresis, palette hysteresis, alpha trigger) gets a hidden warm-up pass so frame 1 starts from the end-of-sequence state -- no snap at the last-to-first wrap. Disable for non-looping footage. |
| widthopt | INT | 00–8192 | Output width. 0 = input width. One of width/height set = keep aspect. |
| heightopt | INT | 00–8192 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| pixel_art | IMAGE | — |