/ PYTHON
A complete, runnable script in about 25 lines. Pillow is the only dependency. Below the code, the two mistakes that make most first attempts look wrong.
Save this as ascii.py and run python ascii.py photo.jpg 100, where 100 is the output width in characters.
from PIL import Image
RAMP = "@#S08Xx+=-;:,. " # densest to lightest
def image_to_ascii(path, columns=100):
img = Image.open(path).convert("L") # 8-bit greyscale
# A monospace cell is roughly twice as tall as it is wide, so squash the
# row count or the result comes out stretched vertically.
w, h = img.size
rows = max(1, round(columns * h / w * 0.5))
img = img.resize((columns, rows))
px = img.load()
lo, hi = img.getextrema() # auto-levels: use the real range
span = (hi - lo) or 1
lines = []
for y in range(rows):
line = []
for x in range(columns):
n = (px[x, y] - lo) / span # 0.0 dark .. 1.0 bright
line.append(RAMP[min(len(RAMP) - 1, int((1 - n) * (len(RAMP) - 1)))])
lines.append("".join(line))
return "\n".join(lines)
if __name__ == "__main__":
import sys
print(image_to_ascii(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 100))pip install Pillow first if you do not have it.
Running it against a portrait at 64 columns gives this. It is text, so you can pipe it to a file or paste it straight into a terminal.
................................................................ .................................... .......................... ................................,............................... ...................... .........,.............................. .................... .,;--,..................................... ....................:+8888X:,:;,............. .................. ...................,x0S0SS#+:;++,............................... ...........,,......-0SSS#SS8;;+0-;,............................. .........,,,,.....,=00x=-=+x-::x+-=:. .......................... .........,,,,......;00++xX008=-;=-+=:........................... .........,,,,......,88x8=x=::;-=x=x=;........................... ........,,,,,.... ,xXxX0888X8SSXX=++;........................... .......,,,,,,,...;00+x8S###@@@#0Xx--:...,..... ................. .....,,,,,,,,,...-X+Xx+#@@@@@#S08+;:,..=:.,;,.....,,,,.......... .......,,,,,,,,...:8S0S#@@@#S008x=;:,.,x+=-:......,,,,,.,....... .....,,,,,,,,,,...;=+x8S####SS08Xx=:...=+-:.....,,,,,,,.,.......
A monospace glyph is roughly twice as tall as it is wide. Map one pixel row to one line of text and the picture comes out stretched to about double its proper height. The fix is the * 0.5 in the row calculation. If your terminal or the font you are pasting into has a different ratio, adjust that number rather than the column count.
Most photographs never reach pure black or pure white. Dividing raw pixel values by 255 wastes both ends of the ramp and everything collapses into the middle few characters. Reading the true range with getextrema() and normalising against it is what keeps a dim photo legible.
The ramp above assumes light characters on a dark terminal, so the brightest pixels get the densest characters. Printing on white paper or into a light-themed editor means you want the opposite: reverse the ramp, or the image comes out as a negative.
Keep the original RGB image alongside the greyscale one, then wrap each character in an ANSI escape: \x1b[38;2;{r};{g};{b}m. Reset with \x1b[0m at the end of every line, otherwise the colour bleeds into whatever prints next.
Draw the characters onto a new canvas with PIL.ImageDraw using a monospace TrueType font. Advance by the font's character width for x and its line height for y — do not measure each glyph, since in a monospace font they are all identical and measuring per glyph is slow.
The 15-character ramp above is a good default. A longer one such as the classic 70-character ramp gives smoother gradients but is more sensitive to which font you view it in, because the perceived density order of rare punctuation varies between typefaces.
The image to ASCII converter on the homepage does all of the above in your browser, with 40 character styles, colour output and PNG export. Nothing is uploaded — the conversion runs on your own device. There is also a text to ASCII art generator for turning words into banner lettering.
Pillow is the only dependency. Install it with pip install Pillow. The conversion itself is plain Python arithmetic over the pixel values, so nothing else is required.
Because a monospace character cell is about twice as tall as it is wide. If you map one pixel row to one text row the picture comes out roughly twice as tall as it should be. Multiply the row count by about 0.5 when resizing.
Order the characters from most ink to least, for example @#S08Xx+=-;:,. followed by a space. Which end maps to bright depends on your background: on a dark terminal the brightest pixels need the densest characters, on white paper it is the other way round.
Stretch the contrast before mapping. Take the actual minimum and maximum brightness in the image with getextrema() and normalise against that range rather than assuming 0 to 255.
Yes. The converter on the homepage does the same thing in your browser with 40 character styles, and nothing is uploaded.