ASCII art: character ramps and why brightness needs gamma

How pictures become characters, what a character ramp is, why averaging pixel values darkens fine detail, and how CIE lightness fixes it.

On this page

ASCII art looks like a toy, but turning a picture into characters is a real tone-reproduction problem: dozens of grey levels have to be mapped onto a handful of glyphs so that the result still looks like the picture. Most converters get one step subtly wrong, and the art comes out muddy. This article walks through the pipeline the image to ASCII art tool uses, with numbers from its code.

The pipeline

  1. Divide the picture into cells, one per character.
  2. Measure each cell's brightness.
  3. Map the brightness onto a character ramp, from dense to sparse.

Each step has a trap.

Step 1: cells are not square

A monospace character cell is roughly twice as tall as it is wide (about 0.5 for most fonts: Menlo, Consolas and Courier are all close). If a 100-column rendering of a square photo used 100 rows, it would come out twice as tall as the original. So the number of rows is the number of columns × (picture height ÷ width) × 0.5. A 400 × 200 picture at 100 columns becomes 100 × 25 characters.

Step 2: average light, not numbers

Pixel values on a screen are gamma-encoded (sRGB): the number 128 does not mean "half the light of 255". It means about 22% of it, because the encoding spends more numbers on dark tones, where our eyes are more sensitive. Converting a value to actual light ("linear") is roughly raising it to the power 2.2.

That matters as soon as a cell contains more than one tone, which is almost always: fine detail, hair, text, a dithered logo. Take a cell that is half black and half white pixels.

  • Averaging the sRGB numbers gives (0 + 255) / 2 = 127.5, a middle grey.
  • Averaging the light gives a relative luminance of 0.5: half of white's light, which looks much lighter than middle grey.

Stand back from a black-and-white checkerboard and you see a light grey, not a mid grey. The second number is the right one.

Step 3: map by lightness, not by light

Once you have the light (relative luminance Y), there is a second trap: mapping Y straight onto the ramp. The eye's response to light is roughly a cube root, so equal steps of Y do not look equal. CIE L, the lightness scale used in colour science, corrects for that: L = 116 · Y^(1/3) − 16 (with a linear part near black), from 0 for black to 100 for white. An 18% grey card, the photographer's middle grey, is L* 49.5.

The three methods side by side

With the 10-character ramp @%#*+=-:. (dense to sparse), here is what each method draws:

CellsRGB average, mapped as isLight average, mapped by YLight average, mapped by L*
flat grey 64#@#
flat grey 128=#=
flat grey 192:=:
½ black, ½ white==:
¾ black, ¼ white##=
¼ black, ¾ white::.

Reading the table:

  • Mapping luminance Y directly (middle column) makes every flat tone too dark: grey 128 gets a #, nearly the darkest symbol, because its luminance is only 0.216.
  • Mapping raw sRGB numbers (first column) handles flat tones well, since sRGB is roughly perceptual already, but gets every mixed cell wrong: a cell with a quarter of white pixels is drawn as dark as flat grey 64.
  • Averaging light, then mapping by L* (last column) gets both right. This is what the tool does.

The visible difference is in fine detail: strands of hair, thin lines, small text and edges keep their brightness instead of sinking into the shadows.

Choosing a ramp

A ramp is a list of characters ordered by how much ink they put in their cell.

  • 10 levels (@%#*+=-:. ) is the classic: clear, readable, works in any font.
  • 70 levels (Paul Bourke's ramp, starting $@B%8&WM#*oahk…) gives more tones, but neighbouring characters differ by tiny amounts of ink, so it reads as texture rather than shading. It shines at large widths.
  • Unicode blocks █▓▒░ give smooth shading with only five levels, if the font draws them full-height.

Common mistakes

  • Too narrow. Below about 60 columns, faces become unrecognisable. Use 80 to 120 for sharing, more for posters.
  • Forgetting the background. On a dark terminal, bright parts should get dense characters: invert the mapping.
  • Pasting into a proportional font. ASCII art only lines up in monospace. In chat apps, use a code block.
  • Low-contrast pictures. A misty landscape maps to three characters. Raise contrast, or crop to the subject.
  • Resizing the picture first in an editor that averages in sRGB. That reintroduces the darkening this whole pipeline avoids; give the converter the original.

Try it

Open a portrait in the image to ASCII art tool, set 100 columns and the 10-level ramp, then move the mid-tone (gamma) slider: above 1 darkens the middle of the ramp, below 1 lightens it. If you want the actual colours of the picture for a palette rather than for characters, the image colour picker reads them exactly.

Tools mentioned in this article

Back to the blog