Take a snap!

Picture perfect

Hello viewers! How is everybody? Yes thanks, we’re great!

This is going to be a rather short blog post because we’re super busy at the moment, but what it doesn’t mean is it won’t be an interesting one! It’s a little frivolous, we admit. Here at 23Squared we were thinking about images, specifically those made of ASCII. Also known as ASCII art. How does one convert an image to ASCII?

At first we thought it would be quite tricky but actually, after some messing around, we found that it’s actually quite simple….

Step one – Read the image and store it in memory

Step two – Convert the image to greyscale (or grayscale, depending on where you hail from)

Step three – Iterate through every pixel of the image and replace it with the ‘correct’ ASCII character

Right. That’s good, glad we’ve got that cleared up. So, now on to the codey magicky stuff that actually makes everything happen! We opted to use Java for this one.

To load the image, the code is trivial –

public static BufferedImage loadImage(String imageURI) throws IOException {

        File imageFile = new File(imageURI);

        if(!imageFile.exists()) {
            throw new FileNotFoundException("Failed to find file " + imageURI);
        }

        BufferedImage image = ImageIO.read(imageFile);

        if(image == null) {
            throw new IllegalArgumentException("Failed to load " + imageURI + ". Not a valid image file format");
        }

        return image;
    }

Easy peasy. We’ve used java.awt.image.BufferedImage to store the image internally. It provides lots of useful functionality, making life easy. It may not be the most efficient method but that wasn’t one of our primary concerns when writing this program.

The next step is a bit more difficult…greyscaling the image. This step isn’t wholly necessary but we’ve found that the results were better for greyscaled images compared to non-greyscaled images.

After researching the greyscaling we discovered that there is, in fact, many ways to skin a cat. Some are fast and some are very, very slow. Some preserve transparency and some do not. The method we’re using here was the fastest (158ms) that preserved image transparency (see http://codehustler.org/blog/java-to-create-grayscale-images-icons/ for more info). This is how we did it –

public BufferedImage convertToGrayscale(BufferedImage image) {
    grayscaleOperation =
            new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
    return grayscaleOperation.filter(image, null);
}

Simple, kinda. What we’re doing is creating a filter which is based on the GRAY colour space built in to Java. We then pass the image into this filter and the filter operation performs a pixel-by-pixel conversion of the image into the colour space which it is built on, ie grey.

This gives us a grey image. Nice. It’s also possible to resize the image so that one doesn’t end up with an image that doesn’t render properly for standard out. This step isn’t necessary provided smallish images are used so we aren’t going to cover it (this is about building the simplest possible working program, MVP if ya’ like).

The final step is swapping the pixels for ASCII. The observant amongst you will have noticed that we said, ‘correct’ ASCII character earlier on. Well, what’s the correct character? Well, pull up a chair and listen…There’s (once again) more than one way to skin a cat.

The best way to achieve this is to scan the image in rectangular sections and calculate the average brightness for the region (R, G & B values summed and divided by 3). The average brightness for each pixel in the drawing set (ASCII characters) is then calculated and the character with the closest average brightness to the region in the image are considered ‘the same’ and then that ASCII character replaces that region.

One of the key decisions is the size of the region in the image, too big and the output picture will have a low resolution, too small and the output picture will be too complicated.

As we’re aiming for MVP we opted for a slightly different approach –

 public String convertToAscii(BufferedImage image) {

    StringBuilder imageBuilder = new StringBuilder();

    for(int y=0; y<image.getHeight(); y++) {

        for(int x=0; x<image.getWidth(); x++) {

            Color color = new Color(image.getRGB(x, y));
            int brightness = calculateBrightness(color);

            // Scale the brightness from 0 - 255 to 0 - 1
            float scaledBrightness = ((float)brightness / (float)256);

            // Move the value into the correct range for DRAWING_SET[]
            int drawingSetCharacterIndex = (int)((scaledBrightness * 10));

            imageBuilder.append(DRAWING_SET[drawingSetCharacterIndex]);

            // Append a space to maintain aspect ratio
            imageBuilder.append(Utils.SPACE);
        }
        
        imageBuilder.append("\n");
    }

    return imageBuilder.toString();
}

We perform the following process –

– Iterate through every pixel in the image

– Calculate its average brightness

– Scale the brightness to be in the range of the drawing set

– Lookup the character from the drawing set that should replace the pixel

The drawing set is simply an array of characters, ordered darkest to lightest –

private static final char[] DRAWING_SET = {'@', '%', '#', '*', '+', '=', '-', ':', '.', ' '};

The string returned from this method is the image represented in ASCII! Ta-dah!

We hope you, our avid readers found this interesting. That’s it for now…

~23Squared

P.S. No cats were actually skinned in the making of this (we bought them pre-skinned).