/ascii-image-generator

Generates ASCII art from a given image.

Primary LanguageJavaMIT LicenseMIT

GitHub tag (latest SemVer) Code Build Tool GitHub

icon ascii-image-generator

Generates ASCII art from a given image.

Table of contents
Demo
Features
Quick start
Documentation (with Screenshots)
Motivation
Development

Demo

Original ASCII art
./demo/demo.jpg ./demo/demo_ascii.png
"Wallpaper: The Way" by OiMax
is licensed with CC BY 2.0.
Screenshot of the
resulting text file

Features

  • Supported image formats: bmp, gif, jpg, jpeg, png, tiff, wbmp
  • Independent width and height pre-scaling
  • Multiple conversion parameters
    • Interpolation types for pre-scaling the image: Bicubic, Bilinear, Nearest Neighbor
    • Rounding method for quantization: Ceil, Floor, Round
    • Character variation: 10 characters, 70 characters

Quick start

Requirements

The program requires at least the installation of Java 8 or higher to run Gradle.

How to run

To start the application simply run the following command from the command line.

Linux / macOS Windows
./gradlew run .\gradlew.bat run

Documentation

"Choose image file" dialog

Choose an image file in one of the supported formats to start.

Supported formats: bmp, gif, jpg, jpeg, png, tiff, wbmp

./screenshots/01.png

"Scaling factors" dialog

Change the factors for pre-scaling the input image. It may be useful to change the aspect ratio of the input image since pixels are converted 1:1 into characters and the font you are displaying the ASCII image with is likely to not have an aspect ratio of 1:1. You can also take a tradeoff in image resolution and therefore reduce the resulting file size.

Input range for width and height: 5-500% (Default: 100%)
Half height: Set the height to one half the width resulting in a 2:1 ratio.
Proportional: Set the height to the width resulting in a 1:1 ratio.

./screenshots/02.png

"Image conversion methods" dialog

Change the appearance of the resulting ASCII image by changing the following settings to influence the conversion algorithm.

Interpolation type for image scaling:
Bicubic: Uses Bicubic interpolation for scaling. This results in a smoother image. This takes 16 pixels into account.
Bilinear (default): Uses Bilinear interpolation for scaling. This is good for most purposes but may lead to some interpolation artifacts. This takes 4 pixels into account.
Nearest Neighbor: Uses Nearest-neighbor interpolation for scaling. This is useful for pixel art and preserves the blocky details.
Rounding method for quantization:
Ceil: Use Math::ceil for rounding values in the quantization process. This leads to more white details and reduces almost black details.
Floor: Use Math::floor for rounding values in the quantization process. This leads to more black details and reduces almost white details.
Round (default): Use Math::round for rounding values in the quantization process. This leads to an even distribution between almost white and almost black details.
Character variation in resulting image:
10 characters (default): Uses the character sequence @%#*+=-:. to represent the different levels of grey in the ASCII image.
70 characters: Uses the character sequence $@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. to represent the different levels of grey in the ASCII image.

./screenshots/03.png

"Success" dialog

The image has been successfully converted and the resulting file name of the ASCII image is indicated.

./screenshots/04.png

Motivation

There were many aspects that motivated me to start this project and to program some things the way they are now:

  1. Create awesome ASCII art with a simple algorithm.
  2. Recreate a known grayscale algorithm from mathematical notation in self-describing code referencing the given notation (after realizing that an algorithm like that is not as simple as taking the average of all color channels).
  3. Work with some low level bit operations in Java.
  4. Explore the usage of Java Swing in a small side project in contrast to JavaFX.
  5. Explore the benefits and drawbacks of using java.util.Optional instead of null as a return value.

Development

Gradle Tasks

Task Description
run Runs this project as a JVM application.
debug Runs this project as a JVM application with debugging enabled.
- add program arguments with -Pargs="..."
- attach a remote debugger via port localhost:5005
showJavadoc Opens the generated Javadoc API documentation in the default browser.

ARGB32 color model

The color of a pixel is represented as a 32-bit integer as descibed by the ARGB32 color model. The 32 bits represent 4 channels of 8 bits each where the first one is alpha (A) and the three remaining channels are for red (R), green (G) and blue (B). The range of values for each channel is based on the sample length. So in this case the 8 bit wide channels allow for values ranging from 0 to 255 in decimal or 00 to FF in hexadecimal notation. 00 means black (in case of channels R, G or B) or fully transparent (in case of the alpha channel A). FF means full brightness of the color or fully opaque.

PixelSamples32bppRGBA.png

To get the different channel values out of a full representation of a pixel color some bit manipulation is needed. In Java this may look like the following:

// Get a pixel sample
BufferedImage image;
int pixel = image.getRGB(0, 0); // 0xff306090 (#FF306090)

// Extract the channel values
int alpha = pixel >>> 24; // 0xff (255)
int red = (pixel >>> 16) & 0xff; // 0x30 (48)
int green = (pixel >>> 8) & 0xff; // 0x60 (96)
int blue = pixel & 0xff; // 0x90 (144)

A more detailed explanation with visualized bit operations steps:

pixel        = [11111111 00110000 01100000 10010000]
pixel >>> 24 = [00000000 00000000 00000000 11111111] = alpha

pixel        = [11111111 00110000 01100000 10010000]
pixel >>> 16 = [00000000 00000000 11111111 00110000]
             & [00000000 00000000 00000000 11111111]
               [00000000 00000000 00000000 00110000] = red

pixel        = [11111111 00110000 01100000 10010000]
pixel >>>  8 = [00000000 11111111 00110000 01100000]
             & [00000000 00000000 00000000 11111111]
               [00000000 00000000 00000000 01100000] = green

pixel        = [11111111 00110000 01100000 10010000]
             & [00000000 00000000 00000000 11111111]
               [00000000 00000000 00000000 10010000] = blue

Grayscale algorithm

In order to convert a color that is typically gamma-compressed and therefore represented in a nonlinear color model to a grayscale representation with preserved luminance several steps for calculation are necessary. The algorithm used in this application is based on this article about "colorimetric conversion to grayscale".

At first the gamma-compression must be removed by expanding it. After that the color is in a linear colorspace and a weighted sum of the color channel values gives the overall linear luminance. The final step is to convert it back to a gamma-compressed color and set all color channels to this value. The color is now converted to grayscale. Implementation details may be seen in the Grayscale.java class.

Character sequences for ASCII art

This generator uses different characters to represent the brightness of pixels. Some characters are more dense and cover more area than others and therefore are used for darker pixels. Some characters such as a simple dot or a space are used for brighter pixels. This application makes use of two known character sequences with a length (or variation in levels of grey) of 10 and 70 characters. They are ordered from dark to bright and each includes a space character for white pixels.

10 characters: @%#*+=-:. 
70 characters: $@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'.