Nodes/ComfyUI PixelArt Detector/🎨PixelArt Palette Converter
ComfyUI Node

🎨PixelArt Palette Converter

Recolor any image into NES or Game Boy palette — and actually make it look good

By dimtoneff·Created 3 years ago·Updated 10 months ago· 429
🎨PixelArt Palette Converter
  • images
  • paletteList
  • IMAGE
â—„paletteGAMEBOYâ–ş
â—„resize_w512â–ş
â—„resize_h512â–ş
â—„resize_typecontainâ–ş
â—„pixelizeImage.quantizeâ–ş
â—„grid_pixelate_grid_scan_size2â–ş
â—„reduce_colors_before_palette_swapfalseâ–ş
â—„reduce_colors_methodImage.quantizeâ–ş
â—„reduce_colors_max_colors128â–ş
â—„apply_pixeldetector_max_colorstrueâ–ş
â—„image_quantize_reduce_methodMAXCOVERAGEâ–ş
â—„opencv_settingsOpenCV.kmeans: only when reducing is enabled. RANDOM_CENTERS: Fast but doesn't guarantee same labels for the same image. PP_CENTERS: Slow but will yield optimum and consistent results for same input image. attempts: to run criteria_max_iterations so it gets the best labels. Increasing this value will slow down the runtime a lot, but improves the colors! â–ş
â—„opencv_kmeans_centersRANDOM_CENTERSâ–ş
â—„opencv_kmeans_attempts10â–ş
â—„opencv_criteria_max_iterations10â–ş
â—„pycluster_kmeans_metricsEUCLIDEAN_SQUAREâ–ş
â—„cleanupClean up colors: Iterate and eliminate pixels while there was none left covering less than the 'cleanup_pixels_threshold' of the image. Optionally, enable the 'reduce colors' option, which runs before this cleanup. Good cleanup_threshold values: between .01 & .05â–ş
â—„cleanup_colorsfalseâ–ş
â—„cleanup_pixels_threshold0.020â–ş
â—„ditherâ–ľâ–ş

This is the flagship node of the ComfyUI PixelArt Detector pack: take any image, drop it onto a retro palette, and make the result actually look like it was drawn for a 1989 console instead of just "tinted with a color filter." A naive palette swap maps every pixel to its nearest palette color, which produces posterized noise and muddy bands. The Converter gets around that the way real games did - with color reduction, dithering, and cleanup, and it gives you knobs for all three.

What's under the hood

The node is based on Astropulse's pixeldetector code. The pipeline is: optionally resize → optionally reduce colors (with a choice of clustering algorithms) → swap to the target palette → optionally clean up rare colors → optionally dither. Resizing is smart about order: if your input is smaller than the target, it upscales before pixelizing; if it's bigger, it downscales after quantization. Set both W and H to 0 and resize is skipped entirely.

The palette swap itself uses Pillow's Image.quantize, which maps each pixel to the nearest color in the target palette - and the dither options (floyd-steinberg, or ordered bayer-2/4/8/16) fake intermediate shades with checkerboard patterns so gradients don't band.

The inputs that matter

This node has a lot of inputs, but you can ignore most of them at first. The ones you'll actually set:

  • palette - the embedded pick: NES or GAMEBOY. If you want the 50+ lospec palettes, that's the paletteList input from the Palette Loader or Palette Generator instead.
  • paletteList (optional) - a LIST of palettes. When it has more than one entry, the node renders a comparison grid of your image under every palette. Great for picking.
  • pixelize - the swap algorithm. Image.quantize is the fast default; Grid.pixelate scans pixel-by-pixel and is slow; NP.quantize is a NumPy-based variant that's also slower.
  • reduce_colors_before_palette_swap - off by default. Flip it on to cut the image to reduce_colors_max_colors before the palette swap, which gives the final result far less banding. This is the biggest quality lever in the node.
  • reduce_colors_method - which clustering does the reduction. Image.quantize (fast), OpenCV.kmeans (slow but picks good centers, with RANDOM_CENTERS vs the more consistent PP_CENTERS), or the pyclustering kmeans/kmedians.
  • dither - none or one of the ordered/Floyd-Steinberg options. A little bayer dither is what sells the "Game Boy photo" look.
  • cleanup_colors + cleanup_pixels_threshold - after swapping, deletes colors that cover less than the threshold fraction of the image. Good values are 0.01–0.05; lower threshold = more colors kept.

There's also image_quantize_reduce_method (MAXCOVERAGE is the author's recommendation for pixel art) and a pile of OpenCV/pyclustering tuning knobs. Leave those alone until you're chasing a specific look.

Output is a single IMAGE list - wire it to a Preview or Save node. Hook reduce_colors_before_palette_swap on, set the palette, pick a small bayer dither, and you're 80% of the way to a convincing conversion.

Installing the pack

This node ships in dimtoneff/ComfyUI-PixelArt-Detector. Install via ComfyUI Manager (search "ComfyUI PixelArt Detector") or:

cd ComfyUI/custom_nodes
git clone https://github.com/dimtoneff/ComfyUI-PixelArt-Detector

Restart ComfyUI. On first load it auto-installs opencv-python, scipy, pyclustering, Pillow and numpy. pyclustering is the fragile one - it has no wheels for newer Pythons, so the pack pulls a prebuilt wheel from the author's releases at startup. On x86_64 it just works; on Apple Silicon or unusual Python builds it may fail and you'll need to install it manually.

Gotchas

  • Bayer dither on a huge image is slow - it's per-pixel math. Downscale first.
  • Grid.pixelate with scan size 1 is brutally slow - it's pixel-by-pixel. The author says increasing the size speeds it up but kills quality; the other two methods are better defaults.
  • The embedded NES/GAMEBOY palettes only appear if you don't feed a paletteList. Wire a Loader in and the dropdown is ignored.
  • Old nodes embedded in workflows won't update in place - delete and re-add from the Image/PixelArt menu.
  • The opencv_settings and cleanup string widgets are just help text. Ignore them; the real toggles are the booleans below them.
Categoryimage/PixelArt🕹️

Inputs (22)

NameTypeDefaultDescription
imagesIMAGE—
paletteCOMBOGAMEBOY2 options: NES, GAMEBOY
resize_wINT5120–2048—
resize_hINT5120–2048—
resize_typeCOMBOcontain3 options: contain, fit, stretch
pixelizeCOMBOImage.quantize3 options: Image.quantize, Grid.pixelate, NP.quantize
grid_pixelate_grid_scan_sizeINT21–32—
reduce_colors_before_palette_swapBOOLEANfalse—
reduce_colors_methodCOMBOImage.quantize4 options: Image.quantize, OpenCV.kmeans.reduce, Pycluster.kmeans.reduce, Pycluster.kmedians.reduce
reduce_colors_max_colorsINT1281–256—
apply_pixeldetector_max_colorsBOOLEANtrue—
image_quantize_reduce_methodCOMBOMAXCOVERAGE3 options: MAXCOVERAGE, MEDIANCUT, FASTOCTREE
opencv_settingsSTRINGOpenCV.kmeans: only when reducing is enabled. RANDOM_CENTERS: Fast but doesn't guarantee same labels for the same image. PP_CENTERS: Slow but will yield optimum and consistent results for same input image. attempts: to run criteria_max_iterations so it gets the best labels. Increasing this value will slow down the runtime a lot, but improves the colors! —
opencv_kmeans_centersCOMBORANDOM_CENTERS2 options: RANDOM_CENTERS, PP_CENTERS
opencv_kmeans_attemptsINT101–150—
opencv_criteria_max_iterationsINT101–150—
pycluster_kmeans_metricsCOMBOEUCLIDEAN_SQUARE6 options: EUCLIDEAN, EUCLIDEAN_SQUARE, MANHATTAN, CHEBYSHEV, CANBERRA, CHI_SQUARE
cleanupSTRINGClean up colors: Iterate and eliminate pixels while there was none left covering less than the 'cleanup_pixels_threshold' of the image. Optionally, enable the 'reduce colors' option, which runs before this cleanup. Good cleanup_threshold values: between .01 & .05—
cleanup_colorsBOOLEANfalse—
cleanup_pixels_thresholdFLOAT0.0200.001–1—
ditherCOMBO6 options: none, floyd-steinberg, bayer-2, bayer-4, bayer-8, bayer-16
paletteListoptLIST—

Outputs (1)

NameTypeDescription
IMAGEIMAGE—