Skip to main content

Generate paint-by-numbers artifacts from an input image.

Project description

paintify

paintify preview

paintify is a Python CLI for turning regular images into paint-by-numbers templates. It takes one input image and generates a printable outline, a colored preview, a numbered paint palette, and a machine-readable manifest with the exact settings used for the run.

The project is aimed at quick, reproducible template generation rather than professional vector tracing. It works best for photos or illustrations where the subject has clear shapes and enough contrast after resizing and smoothing.

Output Artifacts

Every run writes these files to the output directory:

  • outline.svg: a printable black-and-white SVG with region boundaries and paint numbers.
  • preview.png: a colored preview with boundaries and labels, useful for checking the result.
  • palette.json: the numbered paint palette as JSON.
  • manifest.json: reproducibility metadata, settings, palette, regions, and label positions.

You can inspect a generated set of these artifacts in example/, including the source image at example/input.jpg.

Installation

paintify supports Python 3.12 and newer.

Install the CLI from PyPI with uv:

uv tool install paintify

Then run:

paintify --help

You can also run it without a persistent install:

uvx paintify --help

Install For Development

uv sync
make hooks

make hooks requires the project to be inside a Git repository because prek installs Git hooks.

Usage

paintify input.png --output-dir out --difficulty easy --seed 42

--difficulty selects a preset (easy, medium, or hard). Presets are a convenient starting point, and every preset value can be overridden independently:

paintify input.png \
  --difficulty medium \
  --colors 18 \
  --max-size 900 \
  --min-region-size 24 \
  --smooth-radius 0.5 \
  --palette-file palette.json \
  --max-regions 500

Optional palette-file snapping keeps generated colors close to a fixed paint set:

paintify input.png --output-dir out --palette-file palette.json

The palette file is a JSON object with a colors list of hex strings:

{
  "colors": ["#d62828", "#277da1", "#fcbf49"]
}

Use --colors -1 with --palette-file when every color from the palette file may be used. Without --colors -1, the selected preset still caps the generated color count before snapping to the file. --colors -1 without --palette-file is invalid.

For fully manual settings, disable presets with --no-preset. In this mode paintify requires all generation settings to be provided. --palette-file remains optional:

paintify input.png \
  --no-preset \
  --colors 10 \
  --max-size 180 \
  --min-region-size 20 \
  --smooth-radius 0.9 \
  --max-regions 200

How It Works

paintify runs a deterministic image-processing pipeline:

  1. The input image is loaded with OpenCV and converted to RGB.
  2. The image is resized so its longest side is at most --max-size. This controls the working resolution and has the biggest impact on speed and region detail.
  3. A Gaussian blur with radius --smooth-radius is applied. More blur removes noise and produces larger, smoother regions; less blur preserves detail.
  4. Colors are reduced with deterministic weighted k-means. The algorithm first groups near-identical RGB values into small bins, counts how often each binned color appears, and clusters those unique colors instead of clustering every pixel. The --seed controls the initial cluster centers.
  5. Palette colors are compared in Lab color space. Lab distances usually match human color perception better than raw RGB distances.
  6. If --palette-file is used, each generated color is snapped to the nearest color from that file.
  7. Thin one-pixel strips are cleaned up before region construction.
  8. Adjacent pixels with the same palette color are split into connected regions.
  9. Small regions below --min-region-size are merged into nearby kept regions. If the result still has more than --max-regions, the smallest regions are removed one at a time and reassigned to neighbouring regions with palette-distance tie-breaking.
  10. Label positions are placed near the safest inner point of each region by measuring distance from the region boundary.
  11. The final document is rendered as SVG, PNG, and JSON artifacts.

The same input, settings, and seed should produce the same output.

Parameters

Option Meaning Practical effect
--difficulty Preset name: easy, medium, or hard. Higher difficulty keeps more colors, detail, and regions.
--no-preset Disable presets and require explicit settings. Useful for reproducible manual tuning.
--colors / -c Maximum number of paint colors before unused colors are compacted away. Use -1 only with --palette-file to allow every file color. More colors preserve detail but make painting harder.
--max-size Longest side of the working image in pixels. Larger values preserve detail but increase runtime and region count.
--min-region-size Minimum target region area in working pixels. Larger values remove tiny islands; smaller values keep fine details.
--smooth-radius Gaussian blur radius before color reduction. Larger values simplify noisy images; 0 disables smoothing.
--palette-file JSON object with a colors list of hex strings. Limits output colors to the nearest colors listed in the file.
--max-regions Maximum number of final numbered regions. Lower values simplify the template; higher values preserve detail.
--seed Deterministic seed for k-means initialization. Change it to try different color clustering while keeping settings fixed.
--output-dir / -o Directory for generated artifacts. Defaults to paintify-out.

Difficulty Presets

Difficulty Colors Max size Min region size Smooth radius Max regions
easy 15 768 40 0.6 300
medium 20 1024 20 0.4 700
hard 30 1280 12 0.25 1200

Use easy for simpler printable templates, medium for balanced results, and hard when the input has details you want to preserve.

JSON Outputs

palette.json

palette.json is the simplest output for paint preparation. It contains the final numbered palette after unused colors are removed and indices are compacted:

[
  {"index": 1, "hex": "#f2d7b6", "rgb": [242, 215, 182], "name": "xkcd:light peach"},
  {"index": 2, "hex": "#8b5a2b", "rgb": [139, 90, 43], "name": "xkcd:milk chocolate"}
]

Use it when you need a shopping/mixing list, want to map numbers to physical paints, or want another program to read the generated palette. name is the nearest CSS4 or XKCD color name from vendored Matplotlib color data; it is a descriptive approximation, not a paint catalog match.

manifest.json

manifest.json is for reproducibility and downstream processing. It records the input path, chosen difficulty, seed, resolved settings, image size, generated artifact names, full palette, region metadata, and label positions:

{
  "input": "input.png",
  "difficulty": "easy",
  "seed": 42,
  "settings": {
    "max_colors": 15,
    "max_size": 768,
    "min_region_size": 40,
    "smooth_radius": 0.6,
    "palette_file": null,
    "max_regions": 300
  },
  "image_size": {"width": 768, "height": 512},
  "artifacts": ["outline.svg", "preview.png", "palette.json", "manifest.json"],
  "palette": [
    {"index": 1, "hex": "#f2d7b6", "rgb": [242, 215, 182], "name": "xkcd:light peach"}
  ],
  "regions": [
    {
      "id": 1,
      "palette_index": 1,
      "area": 512,
      "bbox": [10, 8, 45, 33],
      "label": {"x": 27, "y": 19}
    }
  ]
}

Important fields:

  • settings: the fully resolved values after preset application and CLI overrides.
  • image_size: the working image dimensions after resizing.
  • palette: the same numbered colors as palette.json.
  • regions: each numbered shape, its palette number, pixel area, bounding box, and label position.
  • bbox: [min_x, min_y, max_x, max_y] in working-image coordinates.

Use manifest.json when you want to audit a generated template, compare runs, write custom renderers, or build UI around the paint-by-numbers data.

Tuning Tips

  • If the result has too many tiny shapes, increase --min-region-size, increase --smooth-radius, lower --max-regions, or use --difficulty easy.
  • If the result loses important details, increase --max-size, increase --colors, decrease --smooth-radius, or use --difficulty hard.
  • If the colors are hard to match with real paints, provide a paint list with --palette-file.
  • If the segmentation looks odd but the settings are good, change --seed to try another color clustering.

Development

make lint
make test

To run individual checks:

uv run ruff check src tests
uv run flake8 src tests
uv run ty check src tests
uv run pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

paintify-0.3.1.tar.gz (32.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

paintify-0.3.1-py3-none-any.whl (41.4 kB view details)

Uploaded Python 3

File details

Details for the file paintify-0.3.1.tar.gz.

File metadata

  • Download URL: paintify-0.3.1.tar.gz
  • Upload date:
  • Size: 32.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for paintify-0.3.1.tar.gz
Algorithm Hash digest
SHA256 0c7a2e522b05fcfca598642f8183727cfd95ce741ce4863d4ddcdd929e3ed2cd
MD5 2fdb78d491b10c3ff694fc36e899ebfc
BLAKE2b-256 797f5c5a45db26e1bb35e69ef7c11c48a9e30641feeec3cfc73170f39f7dde6c

See more details on using hashes here.

File details

Details for the file paintify-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: paintify-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 41.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for paintify-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ec3f977af7354b23b672eeea4c6750dd0fb9df4748d704a319d98fcd517ef35c
MD5 8593c062d44ebbe6fa790711afb72b94
BLAKE2b-256 e1e73b13d59d8a7370208c52a05156a4009ff7f76ebe9a2cfcec877a0650a01b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page