chinese-hershey-font
Convert Chinese Characters to Single-Line Fonts using Computer Vision
Fixes AI pixel art images, video, or sprite web uploads
git clone https://github.com/KennethJAllen/proper-pixel-art.gitKennethJAllen/proper-pixel-art|
Noisy, high resolution |
→ |
Clean, true-resolution pixel art |
Converts noisy, high-resolution pixel-art-style images (from generative models or low-quality web uploads) into clean, true-resolution assets. Such images often have a non-uniform grid and random artifacts, so standard downsampling fails — the usual alternatives are naive downscaling or redrawing the asset pixel by pixel. This tool automates the recovery instead. Videos and GIFs are supported too.
The algorithm is robust. It performs well for images that are already approximately aligned to a grid.
Here are a few examples. A mesh is computed, where each cell corresponds to one pixel.
|
Noisy, High Resolution |
Mesh |
True Pixel Resolution |
|
Noisy, High Resolution |
Mesh |
True Pixel Resolution |
|
Noisy, High Resolution |
Mesh |
True Pixel Resolution |
|
Noisy, High Resolution |
Mesh |
True Pixel Resolution |
pip install proper-pixel-art # CLI and Python API pip install "proper-pixel-art[web]" # Include the local web UI
Or with uv:
uv add proper-pixel-art # CLI and Python API uv add proper-pixel-art --extra web # Include the local web UI
git clone git@github.com:KennethJAllen/proper-pixel-art.git cd proper-pixel-art uv sync --extra web
uvx can run the tools directly:
uvx --from "proper-pixel-art" ppa <input_path> # CLI uvx --from "proper-pixel-art[web]" ppa-web # Local web UI
First, obtain a source pixel-art-style image (e.g. from a generative model such as OpenAI's gpt-image-2, or a web upload of pixel art). See scripts/README.md for a helper script that generates source images from a prompt.
The examples below assume you installed via
pip installoruv add(commands are on yourPATH). If you installed from source withuv sync, prefix each command withuv run(e.g.uv run ppa ...).
Try it live in your browser, no install required, on Hugging Face Spaces.
To run the same interface locally:
ppa-web # Opens http://127.0.0.1:7860
ppa <input_path> -o <output_path> -c <num_colors> -s <result_scale> [-t]
| Option | Description |
|---|---|
| INPUT (positional) | Source image, video, or GIF in pixel-art style |
-o, --output <path> |
Output directory or file path for result. (default: '.') |
-c, --colors <int> |
Number of colors for output (1-256). Use 0 to skip quantization and preserve all colors. May need to try a few different values. (default 0) |
-s, --scale-result <int> |
Width/height of each "pixel" in the output. 1 = no scaling. (default: 1) |
-t, --transparent |
Output with transparent background. (default: off) |
-u, --initial-upscale <int> |
Initial image upscale factor. Increasing this may help detect pixel edges. (default 2) |
-w, --pixel-width <int> |
Width of the pixels in the input image. Use 0 to determine it automatically. (default: 0) |
--config <path> |
YAML config file of pixelation parameters. Flags passed explicitly override values in the file. (default: none) |
--intermediate-dir <path> |
Directory to save images visualizing intermediate algorithm steps. Useful for development. (default: none) |
ppa assets/blob/blob.png -c 16 -s 25
Tip:
--colors(-c) is the parameter most likely to need tuning — try a few values if the result looks off.
Video and GIF inputs are recognized by extension, so the same ppa command pixelates animations too (e.g. from video models such as Sora). The result stays consistent frame-to-frame with no flicker — see how the algorithm handles animations for details.
ppa <input.mp4|input.gif> -o <output_path> -c <num_colors>
The output format follows the output extension (e.g. -o out.mp4 converts a GIF to MP4), and all the pixelation options and --config from the table above apply.
Two extra options apply to video/GIF inputs (they are ignored for images):
| Option | Description |
|---|---|
-f, --format <mp4|gif> |
Output format. (default: inferred from output, then input, extension) |
-n, --sample-frames <int> |
Frames sampled for mesh and palette detection. (default: 8) |
The ppa-video command is a deprecated alias for ppa, kept for compatibility.
GIF input is decoded with full frame compositing (variable-size delta frames, per-frame durations, and transparency are preserved). GIF output uses a single global palette.
MP4 output is encoded near-losslessly (libx264 at CRF 1) via PyAV, whose wheels bundle the FFmpeg libraries — so quality doesn't depend on what's installed on your machine. Note for anyone redistributing a bundled app: PyAV's wheels ship a GPL build of FFmpeg.
For Python developers who want to integrate this tool into their own code.
from PIL import Image
from proper_pixel_art import pixelate
image = Image.open('path/to/input.png')
result = pixelate(image, num_colors=16)
result.save('path/to/output.png')
Videos and GIFs have their own entry point:
from proper_pixel_art.video import pixelate_video
pixelate_video('input.mp4', 'output.gif', num_colors=16)
The keyword arguments mirror the CLI options table above: num_colors (-c), scale_result (-s), transparent_background (-t), initial_upscale_factor (-u), pixel_width (-w), and intermediate_dir (--intermediate-dir). In addition:
image : PIL.Image.Image — the image to pixelate.config : PixelateConfig | None — a bundle of every tunable parameter, loaded with PixelateConfig.from_yaml(path) (see Configuration file). Explicit arguments override matching values in config.A PIL image with true pixel resolution and quantized colors.
All tunable parameters can be collected in a YAML file so you can fine-tune the algorithm without changing code — including the deeper mesh-detection (Canny, Hough, line clustering) and color (alpha/transparency thresholds, quantization method, color binning) settings not exposed as CLI flags or direct arguments. See config.example.yaml for the full list of keys with their defaults. Any key you omit falls back to the default, so partial files are fine.
from PIL import Image
from proper_pixel_art import pixelate
from proper_pixel_art.config import PixelateConfig
config = PixelateConfig.from_yaml('config.yaml')
result = pixelate(Image.open('input.png'), config=config)
From the CLI, pass --config. Flags given explicitly override values from the file:
ppa input.png --config config.yaml # use the file ppa input.png --config config.yaml -c 8 # but override num_colors to 8
This tool can also be used to convert real images to pixel art: first request a pixelated version of the original image from GPT-4o, then use the tool to get the true pixel-resolution image. The mountain at the top of this README was made this way, starting from this photo:
|
Original photo |
→ |
True-resolution pixel art |
Here's a step-by-step overview, applied to this GPT-4o-generated blob:
Trim the edges of the image and replace mostly-transparent pixels (alpha below 50%) with a background color.
Upscale by a factor of 2 using nearest neighbor.
Find edges of the pixel art using Canny edge detection.
Quantize the original image to a small number of colors (see the num_colors tuning note above).
In each cell specified by the mesh, choose the most common color in the cell as the color for the pixel. Recreate the original image with one pixel per cell.
The same algorithm generalizes to animations by exploiting the fact that every frame shares one underlying pixel grid and palette. Rather than solving frames independently (which would make the mesh and colors jitter frame-to-frame), the mesh-detection steps (1–6) and color quantization (step 7) are run once over a sample of frames to fix a single grid and shared palette. That grid and palette are then applied to snap every frame (step 8), so the whole animation resolves to consistent, true pixel resolution.
more like this
Convert Chinese Characters to Single-Line Fonts using Computer Vision
🎥🤟 8 minimalistic templates for tfjs mediapipe handpose and facemesh
TachiSnap — Pixel Snapper for animation pixel artists. Rust + WebAssembly client-side tool for cleaning up AI-generated…
search projects, people, and tags