HomeArticlesGenerative Art

Computational Photo Art: ASCII, Stippling and Dithering Explained

From luminance-ranked character sets to weighted Voronoi stippling and Floyd-Steinberg error diffusion — how a photo becomes algorithmic art.

mysimulator teamUpdated June 2026≈ 8 min read▶ Open the simulation

Every technique starts with the same number: luminance

Underneath ASCII art, photomosaics, stippling and dithering is the same first step: reduce a colour pixel to a single brightness value. The eye is far more sensitive to green than to red or blue, so a naive average of the three channels looks wrong — the standard formula weights them unevenly, close to L = 0.299R + 0.587G + 0.114B (the ITU-R BT.601 luma coefficients used by broadcast video). Every algorithm below operates on this single derived channel, then decides separately how to render each luminance value as ink, dots or characters.

live demo · error-diffusion noise standing in for a dithered image● LIVE

ASCII art: ranking glyphs by ink coverage

An ASCII converter does not try to match letter shapes to image features — it matches ink coverage. Render each candidate character to a small bitmap, count the fraction of dark pixels, and sort the character set by that fraction: a space has zero coverage, a period a little, and @ or # nearly full coverage. Each image cell's average luminance is then mapped onto that ranked list. Because monospace fonts are roughly twice as tall as they are wide, cells are typically sampled as rectangles rather than squares so the output does not look vertically squashed.

Photomosaics: nearest neighbour in colour space

A photomosaic replaces every tile of the source image with a small thumbnail chosen from a library, selected to match that tile's average colour. Precompute the average RGB of every library image once, then for each tile in the target run a nearest-neighbour search over that colour space. A common refinement mixes a translucent layer of the original tile's colour over the chosen thumbnail — this pulls the thumbnail closer to the exact target colour and hides visible seams between tiles that are only approximate matches.

Stippling: Lloyd's algorithm makes dots look hand-placed

Weighted Voronoi stippling (Secord, 2002) scatters points with density proportional to image darkness, then repeatedly relaxes them: build the Voronoi diagram of the current points, move each point to the darkness-weighted centroid of its own cell, and repeat.

repeat N times:
  cells = voronoi(points)
  for each cell c, point p in cells:
      p.x, p.y = weighted_centroid(c, darkness_field)   // Lloyd relaxation

This is Lloyd's algorithm, the same iterative relaxation used to build centroidal Voronoi tessellations for mesh generation and k-means clustering. After a handful of iterations the points spread into a blue-noise-like pattern — evenly spaced with no visible clumping or grid alignment — while still concentrating where the image is dark, which is exactly what a pen-and-ink stipple artist does by eye.

Dithering: Floyd-Steinberg error diffusion

Reducing an image to two tones (pure black and white) by simply rounding each pixel destroys the mid-tones. Floyd-Steinberg dithering (1976) fixes this by treating rounding as lossy and pushing the loss forward: after quantizing a pixel, the difference between its true value and the value used is distributed to unprocessed neighbours with fixed weights.

old = pixel[x][y]
new = round_to_palette(old)
pixel[x][y] = new
err = old - new
pixel[x+1][y  ] += err * 7/16
pixel[x-1][y+1] += err * 3/16
pixel[x  ][y+1] += err * 5/16
pixel[x+1][y+1] += err * 1/16

Scanned left to right, top to bottom, this pushes 16 sixteenths of the rounding error onward every pixel, so no error is created or destroyed — only redistributed — and the local average brightness converges on the original. Ordered dithering (a fixed Bayer threshold matrix tiled across the image) is the alternative: faster and free of the sequential dependency, but it produces a visible periodic pattern instead of the organic-looking noise error diffusion gives.

Frequently asked questions

Why does Floyd-Steinberg dithering look better than just rounding each pixel?

Rounding every pixel independently to the nearest available shade throws away information — a pixel that should be 40% grey becomes either fully black or fully white with no memory of the 40%. Error diffusion carries that leftover 40% (or -60%) forward into neighbouring pixels, so the average brightness over any region is preserved even though each individual pixel is pure black or white.

How is stippling different from just scattering random dots by brightness?

Naive random placement clumps dots by chance, which reads as noisy rather than tonal. Weighted Voronoi stippling instead relaxes dot positions with Lloyd's algorithm so that each dot sits at the centroid of its own weighted cell — this evens out the spacing (blue-noise-like) while dot density still tracks image darkness, which is what makes it look hand-drawn rather than random.

Why do ASCII art converters use character density rather than actual letter shapes?

Because at the distance and font size ASCII art is normally viewed, the eye reads coverage — how much ink a glyph puts down in its cell — rather than individual letterforms. Ranking a character set by average pixel coverage (space, then punctuation, then letters, then symbols like @ and #) and mapping luminance onto that ranked list reproduces tonal gradients faithfully, which shape-matching alone would not.

Try it live

Everything above runs in your browser — open Computational Photo Art and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.

▶ Open Computational Photo Art simulation

What did you find?

Add reproduction steps (optional)