Img2ContourAscii
A command-line image-to-ASCII converter that picks characters based on shape, not just brightness. Edges and contours are followed accurately because each character is matched against the actual visual structure of the image region it occupies.
Credit
This is a Python port of an idea and algorithm created entirely by Alex Harri. The approach is documented in detail in his blog post:
ASCII characters are not pixels: a deep dive into ASCII rendering
The original TypeScript implementation is part of his website's open source repository:
Alex was not involved in this project. All credit for the core algorithm belongs to him.
AI Generated
This implementation was written by Claude (Anthropic) based on Alex Harri's blog post and reference TypeScript source. No code was written by hand.
How it works
The problem with brightness-only rendering
Traditional ASCII renderers assign a character to each grid cell based on average brightness alone — effectively treating characters as square pixels. This produces blurry edges because the shape of the character is ignored.
Shape vectors
Each ASCII character occupies a cell differently. T is dense at the top, L is dense along the left and bottom, / is dense diagonally. Alex Harri's approach captures this by defining six sampling circles arranged across each cell:
(●) (●) ← top row (staggered for better coverage)
(●) (●) ← middle row
(●) (●) ← bottom row
For each character in the alphabet, the fraction of ink inside each circle is measured and stored as a 6-dimensional shape vector. These are pre-computed in default.json (taken directly from Alex's repository).
Matching image cells to characters
The same six circles are sampled at each grid cell in the image to produce a 6D sampling vector. A nearest-neighbour search finds the character whose shape vector is closest — the character that best fits the image region's structure.
Contrast enhancement
Two contrast passes sharpen boundaries between regions:
-
Directional crunch — Ten additional circles sample just outside the current cell's boundary. If a neighbouring region is brighter, the corresponding internal component is pushed down, exaggerating the boundary shape and preventing staircase artefacts.
-
Global crunch — The sampling vector is normalised by its own maximum, raised to a power, then rescaled. Increases contrast between lighter and darker components without affecting uniform regions.
Performance
Sampling is fully vectorised with numpy — no Python pixel loops. Results are cached using a quantised lookup table (Alex's own approach from the appendix of his post), so repeated or similar cell values are O(1) dict hits after warmup. This makes live video viable on a Raspberry Pi 4.
Colour (extra)
The --color flag adds ANSI 24-bit colour codes to each character using the average colour of the corresponding image region.
Palette quantisation (1.1+)
The --palette-size N flag (which implies --color) builds a per-image palette of N colours via PIL's median-cut algorithm and snaps every cell's colour to its nearest palette entry. The output then contains at most N unique colours, picked to fit the image's actual content.
This is useful when the output is being fed into a downstream renderer that pays per colour change — e.g. terminal protocols that emit a fresh escape sequence on every cell — or when you simply want a stylised low-palette look. Adjacent cells frequently land on the same palette entry, which collapses long colour-escape runs.
--hysteresis F (default 0.0) layers a sticky-colour bias on top of the palette quantisation. After nearest-palette assignment, a left-to-right per-row pass swaps each cell's colour to the previous cell's colour when that previous palette entry is within (1 + F) of the nearest entry's distance. Higher values produce longer runs of identical colour at the cost of slightly less faithful per-cell hue. 0.5 is a reasonable starting point for natural photographs; 0.0 disables the bias entirely.
When --palette-size is omitted (or 0), colour output is identical to v1.0 — the original 16-step quantisation + saturation boost — so existing callers continue to work unchanged.
Installation
The image renderer is installable from this repo as a Python package
that drops a img2contourascii console command on your PATH and
exposes Renderer / convert() for use as a library.
# Latest from GitHub
pip install git+https://github.com/JamesM92/Img2contourascii
# Or, from a local clone
git clone https://github.com/JamesM92/Img2contourascii
pip install ./Img2contourascii
numpy and Pillow are pulled in automatically.
For video / webcam support (separate script, not part of the installed package — install only what you need):
pip install imageio # GIF and basic formats
pip install imageio[ffmpeg] # MP4, AVI, MKV, etc.
pip install picamera2 # Raspberry Pi Camera Module
Image rendering — img2contourascii CLI
img2contourascii <image> [options]
(The legacy invocation python Img2ContourAscii.py <image> [options]
still works — it's a one-line shim that calls into the installed
package, kept for backwards compatibility with existing scripts.)
| Option | Default | Description |
|---|---|---|
--cols N |
terminal width | Output width in characters |
--global-crunch F |
2.2 |
Global contrast exponent |
--directional-crunch F |
2.8 |
Directional contrast exponent |
--color |
off | ANSI 24-bit colour output |
--palette-size N (1.1+) |
unset | Limit colour output to N image-adaptive palette entries (median-cut). Implies --color. |
--hysteresis F (1.1+) |
0.0 |
When --palette-size is set, bias adjacent cells toward the same palette entry. 0.5 is a good starting point for photos. |
--invert |
off | Invert lightness (bright→sparse, dark→dense — useful for photos) |
--autocontrast |
off | Stretch luminance range to [0, 1] before rendering |
--char-ratio F |
1.3333 |
Cell height/width ratio — tune if output looks squished |
--exclude CHARS |
"" |
Characters to never use, e.g. --exclude "|$" |
-o [FILE] |
stdout | Write to file; omit filename to auto-generate |
Backwards compatibility: every existing flag still defaults to its v1.0 behaviour. Omit
--palette-sizeand the colour pipeline is byte-for-byte identical to earlier releases. The new options are purely additive.
Examples
# Basic render to terminal
img2contourascii photo.jpg
# Wider, colour, saved to file
img2contourascii photo.jpg --cols 120 --color -o
# Photo-friendly (dark areas become dense)
img2contourascii photo.jpg --invert --autocontrast
# Higher contrast
img2contourascii photo.jpg --global-crunch 3.0 --directional-crunch 3.5
# Stylised low-palette render — 16 image-adaptive colours, sticky
# enough that adjacent cells often share a colour. Smaller output
# bytes when fed through a downstream renderer.
img2contourascii photo.jpg --palette-size 16 --hysteresis 0.5
Python API
Renderer and the convert() shorthand can be imported directly for use in other scripts:
from img2contourascii import Renderer, convert
# One-shot helper — opens the image, renders, returns a string
text = convert("photo.jpg", cols=80, use_color=True, autocontrast=True)
# Reusable renderer — build once, call render_frame() per image/frame
import numpy as np
from PIL import Image
renderer = Renderer(cols=120, use_color=True, autocontrast=True)
img_arr = np.array(Image.open("photo.jpg").convert("RGB"), dtype=np.float32)
text = renderer.render_frame(img_arr)
# Low-palette render via the Renderer constructor (1.1+)
renderer = Renderer(
cols = 80,
use_color = True,
palette_size = 16, # adaptive 16-colour palette
hysteresis = 0.5, # sticky-colour bias, longer runs
)
text = renderer.render_frame(img_arr)
Video / webcam rendering — ContourAscii_Video.py
python ContourAscii_Video.py <file> [options] # video file or GIF
python ContourAscii_Video.py --webcam [options] # USB webcam
python ContourAscii_Video.py --picam [options] # Raspberry Pi Camera Module
| Option | Default | Description |
|---|---|---|
--fps F |
from file / 30 | Override playback FPS |
--loop |
off | Loop video / GIF continuously |
--device N |
0 |
Webcam device index |
--cam-width N |
640 |
Capture width for webcam / Pi Camera |
--cam-height N |
480 |
Capture height for webcam / Pi Camera |
| (all image options) | --cols, --color, --invert, --autocontrast, etc. |
Examples
# Play a video file
python ContourAscii_Video.py clip.mp4 --color
# Loop an animated GIF
python ContourAscii_Video.py animation.gif --loop
# Live webcam at 80 columns
python ContourAscii_Video.py --webcam --cols 80 --color
# Raspberry Pi Camera
python ContourAscii_Video.py --picam --cols 60 --color --invert
The video script imports Renderer from the installed img2contourascii package — no code is duplicated, and video dependencies are only needed if you actually use that script.
Examples
All still-image renders use --cols 60 --autocontrast.
The globe animation uses --cols 50 --autocontrast.
Each table row shows the plain render (top) and colour render (bottom) for each method.
Apple
| Original | Brightness only | Contour (this tool) |
source |
||
Animated comparison (brightness → brightness+colour → contour → contour+colour):
Photo: Red apple — Abhijit Tembhekar, CC BY 2.0, via Wikimedia Commons.
Cat
| Original | Brightness only | Contour (this tool) |
source |
||
Animated comparison (brightness → brightness+colour → contour → contour+colour):
Photo: Cat portrait. Public domain, via Wikimedia Commons.
Globe (animated source)
The source is a 24-frame spinning-globe GIF (512×512). Each frame is rendered independently and the output GIFs loop at the original frame rate.
| Source GIF | Brightness only | Contour (this tool) |
source |
||
Animation: Spinning globe — Wikiscient, CC BY-SA 3.0, based on NASA Visible Earth imagery (public domain), via Wikimedia Commons.
License
PolyForm Noncommercial License 1.0.0 — free for any noncommercial use (personal, educational, research, hobby projects, nonprofits). Commercial use requires a separate license.
For commercial licensing inquiries, open an issue at github.com/JamesM92/Img2ContourAscii/issues.
Metadata
Release files for img2contourascii 1.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| img2contourascii-1.1.2.tar.gz | 28.3 kB | With provenance | Details for img2contourascii-1.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| img2contourascii-1.1.2-py3-none-any.whl | Python 3 | none | any | Details for img2contourascii-1.1.2-py3-none-any.whl |
Total release size: 51.8 kB
Release files / img2contourascii-1.1.2.tar.gz
| Download URL | img2contourascii-1.1.2.tar.gz |
|---|---|
| Size | 28.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5d561de4ad8e26ad75d892487ab86ef78707c9475202d6cc2604474e8f58e08
|
|
BLAKE2b-256 checksum How to use checksums |
2e9ba1f01eb3f52fd29226009262c203514671f96546a2ad31b93010d243e2ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.
Transparency logRelease files / img2contourascii-1.1.2-py3-none-any.whl
| Download URL | img2contourascii-1.1.2-py3-none-any.whl |
|---|---|
| Size | 23.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
35b5c6417842cac4999b38b90329de66f67dbf751258b356d0f9a031259ac0f8
|
|
BLAKE2b-256 checksum How to use checksums |
73eea35deb6d2d44d0aeba62686c07daef762c72b0b82bafe93d58d51612f594
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.
Transparency logSecurity for img2contourascii 1.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for img2contourascii-1.1.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.