Paint by Numbers Preprocessor: K-Mean
The step that turns a photo into flat paint regions
- original_image
- hex_stack
- preprocessed_image
This is the workhorse of the pack: Paint by Numbers Preprocessor: K-Mean takes any image and flattens it into a limited number of clean, solid color regions - the base layer every paint-by-numbers sheet needs. The name is a slight lie (there's no neural network, no sampler, nothing AI about it), but the math is real: K-means clustering, same thing that powers posterization tools in photo editors.
It's the first step in the pack's three-node pipeline: preprocess → calculate numbers → overlay. Feed it a photo, get back a flat, posterized image. That flat image then splits two ways - into a lineart converter (any ControlNet lineart model) for the outlines, and into Paint by Numbers: Calculate numbers for the numbering.
How it works
Open the hood and it's a fairly straightforward CPU pipeline (all scikit-learn, numpy, and OpenCV - no GPU here):
- Denoise - a bilateral filter that smooths noise while preserving edges, so your color fields come out clean instead of speckled. If
noise_reduction_strengthis above 2, it layers on a light Gaussian blur for very noisy sources. - Blur - an optional Gaussian blur to merge tiny detail into bigger regions.
- Quantize - K-means clustering collapses the image to
num_colorsdistinct colors. If you've connected a hex stack, this step changes: it picks the most representative subset of your palette (capped atnum_colors) and snaps every pixel to the nearest palette color, instead of inventing colors. - Clean up - tiny regions below
min_region_sizeget replaced with their neighbors' median color, and a gentle morphological pass fills gaps and removes noise specks.
K-means is the slow part. It samples up to 50,000 pixels for clustering (so fitting is fast), but the final predict pass runs over every pixel - which is why the README recommends keeping the longest side at 3000px or under. Bigger images work, they just eat CPU.
The inputs that matter
All of these are "required" in the node, but you'll realistically tune a handful:
- original_image - whatever you want to turn into a paint-by-numbers base.
- num_colors (default 20, range 2–50) - the number of paint regions. This is the one you'll be fiddling with constantly. Too few and faces become abstract blobs; too many and it stops looking like a paint kit and starts looking like a noisy JPEG.
- hex_stack - optional. Wire in a Hex Color Stack and the quantizer locks to your palette instead of picking colors for you.
- min_region_size (default 800) - deletes small isolated regions. Confetti-like noise everywhere? Raise it.
- noise_reduction_strength and blur_radius - for cleaning up low-quality sources. Leave both at default until the output annoys you.
The others - color_intensity (0.5–2, punches saturation up or down before quantizing), bilateral_filter, morphological_cleanup - are fine at their defaults.
The output
One output: preprocessed_image. It's the posterized result, and it feeds two places - your lineart converter and the Calculate numbers node. That's the whole job.
Installing and troubleshooting
Same story as the whole pack: ComfyUI Manager, search "Painting-by-colors-generator", or:
cd ComfyUI/custom_nodes
git clone https://github.com/Chrisvenator/ComfyUI-Painting-by-colors-generator
then restart. No models to download - it's pure Python. The dependencies (scikit-learn, OpenCV, scipy, numpy) are already sitting in most ComfyUI installs, so Manager usually has nothing heavy to fetch.
If your output looks like noise confetti, raise min_region_size and noise_reduction_strength. If regions look muddy and blended, lower blur_radius. If the colors don't match your reference image, that's the color_intensity knob. And if you plugged in a hex stack and nothing seems to change, remember the stack is a cap - check that num_colors isn't bigger than the stack, or the preprocessor will just use all your colors.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| original_image | IMAGE | — | |
| blur_radius | FLOAT | 0.00–100 | — |
| color_intensity | FLOAT | 1.00.5–2 | — |
| noise_reduction_strength | FLOAT | 5.00–5 | — |
| bilateral_filter | BOOLEAN | true | — |
| morphological_cleanup | BOOLEAN | true | — |
| min_region_size | INT | 80010–1000 | — |
| hex_stackopt | HEX_STACK | — | |
| num_colorsopt | INT | 202–50 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| preprocessed_image | IMAGE | — |