Transforming an image to ASCII characters

Step 1: Image manipulation in the canvas

const canvas = document.createElement("canvas")
const canvas = document.getElementById(<source>)
if (!source) return null
canvas.setAttribute("width", source.width)
canvas.setAttribute("height", source.height)
const context = canvas.getContext("2d")
context.drawImage(source, 0, 0)
const image_data = context.getImageData(0, 0, source.width, source.height)
const raw_data = image_data.data

This snippet creates a <canvas> element and paints an image on it. The function getImageData returns the raw data for the image. It's an array containing 4 bytes per image pixel (the red, green, blue and alpha channel respectively). It's possible to modify the array (named raw_data in the snippet) to modify the image, like in the example below. For the purpose of converting to ASCII we don't need to modify it, but to read pixel color values from it to know what ASCII character to write.

In this example we have taken the RGB values and switched them up. The red has been moved to the green, the green to the blue, and the blue to the red. The result is a green-red costume, insted of a red-blue one.

Step 2: Getting the brightness and mapping the pixels

We are going to map every pixel to a specific ASCII character using it's brightness. Brightness can be calculated with the formula below, which returns a number from 0 to 1, (0 being a black pixel and 1 being a white pixel).

// Traditional grayscale value
const getGrayscale = color=> (color.red + color.green + color.blue) / (255 * 3)
// Brightness value adjusts better to the humans eye's perception of color
const getBrightness = color=> (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue) / 255
Spiderman (Grayscale)Spiderman (Brigthness)

We are going to calculate the brightness of each pixel to map it to it's corresponding ASCII character. In our case, we are going to use the following ASCII values for the different brightness levels:

 .,:;i1tfLCG08@

Please note that we are going to use a total of 15 characters, there is a whitespace for the brightest colors (it may be tough to spot). The mapping is actually pretty straightforward

// Traditional grayscale value
const getCharacter = brightness=> {
  const ASCII = " .,:;i1tfLCG08@"
  const end = ASCII.length - 1
  return ASCII.charAt(end - Math.round(brightness * end))
}

Step 3: Sampling the image

Now we have all the tools for converting an image to ASCII, we need to fix the contrast, calculate the brightness, and map it to it's equivalent character.

There is, however, a small issue here: pixels are square, characters are not, they are larger. To fix this, instead of mapping every row of pixels we are going to map every other row. The resulting image (see below) has the same width, but half the height compared to the original.

Spiderman (Sampled)

With this final touch, we are ready to extract the characters

Step 4: Putting it all together

Let's see the magic in action.

More concepts