DitherPunk.jl

A dithering & digital halftoning package inspired by Lucas Pope's Obra Dinn and Surma's blogpost of the same name.

Note

This package is part of a wider Julia-based image processing ecosystem. If you are starting out, then you may benefit from reading about some fundamental conventions that the ecosystem utilizes that are markedly different from how images are typically represented in OpenCV, MATLAB, ImageJ or Python.

Getting started

We start out by loading an image, in this case the lighthouse from TestImages.jl.

using DitherPunk
using Images
using TestImages

img = testimage("lighthouse")
img = imresize(img; ratio=1//2)

To apply binary dithering, we also need to convert the image to grayscale.

img_gray = convert.(Gray, img)
Preprocessing

Sharpening the image and adjusting the contrast can emphasize the effect of the algorithms. It is highly recommended to play around with algorithms such as those provided by ImageContrastAdjustment.jl

Binary dithering

Since we already turned the image to grayscale, we are ready to apply a dithering method. When no algorithm is specified as the second argument to dither, FloydSteinberg is used as the default method:

dither(img_gray)

This is equivalent to

dither(img_gray, FloydSteinberg())
Note

DitherPunk currently implements around 30 algorithms. Take a look at the Image Gallery and Gradient Gallery for examples of each method!

One of the implemented methods is Bayer, an ordered dithering algorithm that leads to characteristic cross-hatch patterns.

dither(img_gray, Bayer())

Bayer specifically can also be used with several "levels" of Bayer-matrices:

dither(img_gray, Bayer(3))

You can also specify the return type of the image:

dither(Gray{Float16}, img_gray, Bayer(3))
dither(Bool, img_gray, Bayer(3))
256×384 Matrix{Bool}:
 1  0  1  0  1  0  1  0  1  0  1  0  1  …  1  0  1  0  1  0  1  0  1  0  1  0
 0  1  0  1  0  1  0  1  0  1  0  1  0     0  1  0  1  0  1  0  1  0  1  0  1
 1  0  1  0  1  0  1  0  1  0  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  1  0  1  0  1  0  1  0  1  0  1  0     0  0  0  1  0  1  0  1  0  0  0  1
 1  0  1  0  1  0  1  0  1  0  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  1  0  1  0  1  0  1  0  1  0  1  0  …  0  1  0  1  0  1  0  1  0  1  0  1
 1  0  1  0  1  0  1  0  1  0  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  0  0  1  0  1  0  1  0  0  0  1  0     0  1  0  1  0  0  0  1  0  0  0  1
 1  0  1  0  1  0  1  0  1  0  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  1  0  1  0  1  0  1  0  1  0  1  0     0  1  0  1  0  1  0  1  0  1  0  1
 ⋮              ⋮              ⋮        ⋱           ⋮              ⋮        
 0  0  0  0  0  0  0  0  0  0  0  0  0     0  0  0  0  0  0  0  0  0  0  0  0
 1  0  1  0  1  0  1  0  1  0  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  0  0  0  0  1  0  0  0  1  0  0  0     0  0  0  0  0  1  0  0  0  1  0  0
 0  0  1  0  0  0  1  0  1  0  1  0  1  …  1  0  1  0  1  0  1  0  1  0  1  0
 0  0  0  0  0  0  0  1  0  1  0  0  0     0  0  0  0  0  0  0  0  0  0  0  0
 1  0  1  0  1  1  1  1  1  1  1  0  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  1  0  1  1  1  0  1  0  1  0  1  0     0  0  0  0  0  0  0  0  0  0  0  0
 1  0  1  0  1  0  1  1  1  1  1  1  1     1  0  1  0  1  0  1  0  1  0  1  0
 0  0  0  0  0  0  0  0  0  0  0  1  0  …  0  0  0  0  0  0  0  0  0  0  0  0

Color spaces

Depending on the method, dithering in sRGB color space can lead to results that are too bright. To obtain a dithered image that more closely matches the human perception of brightness, grayscale images can be converted to linear color space using the boolean keyword argument to_linear.

dither(img_gray; to_linear=true)
dither(img_gray, Bayer(); to_linear=true)

Separate-space dithering

All dithering algorithms in DitherPunk can also be applied to color images and will automatically apply channel-wise binary dithering.

dither(img)

Because the algorithm is applied once per channel, the output of this algorithm depends on the color type of input image. RGB is recommended, but feel free to experiment. Dithering is fun and you should be able to produce glitchy images if you want to!

dither(convert.(HSV, img), Bayer())

Dithering with custom colors

Let's assume we want to recreate an image by stacking a bunch of Rubik's cubes. Dithering algorithms are perfect for this task! We start out by defining a custom color scheme:

white = RGB{Float32}(1, 1, 1)
yellow = RGB{Float32}(1, 1, 0)
green = RGB{Float32}(0, 0.5, 0)
orange = RGB{Float32}(1, 0.5, 0)
red = RGB{Float32}(1, 0, 0)
blue = RGB{Float32}(0, 0, 1)

rubiks_colors = [white, yellow, green, orange, red, blue]

Currently, dithering in custom colors is limited to ErrorDiffusion and OrderedDither algorithms:

d = dither(img, rubiks_colors)

This looks much better than simply quantizing to the closest color:

dither(img, ClosestColor(), rubiks_colors)

The output from a color dithering algorithm is an IndirectArray, which contains a color palette

d.values

and indices onto this palette

d.index
256×384 Matrix{Int64}:
 6  3  6  1  3  6  3  6  1  3  6  1  3  …  6  1  3  6  3  1  6  3  1  3  6  3
 3  1  3  6  1  3  1  3  6  1  3  6  1     3  6  4  1  6  3  2  6  6  1  3  1
 1  6  1  3  6  4  6  1  3  6  4  3  6     1  3  6  3  1  6  6  2  3  6  4  6
 6  3  6  1  3  1  3  6  1  3  1  6  2     6  1  3  1  3  3  1  6  1  3  6  2
 3  1  3  6  4  6  1  3  6  1  6  3  6     2  6  1  6  6  1  3  3  6  1  3  6
 1  6  1  3  6  2  6  1  3  6  2  1  3  …  6  2  6  3  2  6  6  1  2  6  1  3
 6  2  6  1  3  6  2  6  3  1  6  3  1     3  6  2  6  1  3  3  6  6  3  3  6
 3  6  2  6  1  3  6  1  1  6  2  6  3     1  3  1  3  6  1  1  3  1  5  1  3
 1  3  6  2  6  1  3  6  3  3  6  1  6     3  6  6  1  3  6  3  1  3  1  6  1
 6  1  3  6  2  6  1  3  1  6  2  3  3     1  3  1  6  1  3  6  6  5  6  3  6
 ⋮              ⋮              ⋮        ⋱           ⋮              ⋮        
 3  6  3  3  5  3  6  3  5  3  5  3  5     3  6  3  4  6  3  3  6  5  6  6  3
 6  5  5  3  6  5  3  6  3  5  6  4  6     5  3  6  3  5  3  6  4  3  3  3  5
 3  3  6  3  3  3  5  3  5  3  3  3  3     3  6  4  3  6  3  5  3  6  5  6  3
 6  5  3  5  6  5  3  4  6  3  6  5  3  …  4  3  6  5  3  6  3  6  3  3  3  6
 5  3  6  3  5  3  6  1  3  5  3  5  6     3  6  3  3  6  4  3  3  5  6  5  3
 3  6  4  3  1  1  2  1  1  1  3  3  3     5  3  5  6  3  6  5  6  3  3  3  6
 1  2  1  1  1  6  1  3  5  1  6  4  6     3  6  3  3  5  3  3  3  6  5  6  3
 6  4  3  6  2  2  3  1  1  1  2  1  2     6  4  3  6  3  6  5  6  3  3  3  6
 3  6  3  5  6  6  5  6  3  3  6  3  6  …  3  6  6  5  6  3  3  3  5  6  5  3

This index Matrix can be used in creative ways. Take a look at ASCII dithering for an example!

An interesting effect can also be achieved by color dithering gray-scale images:

dither(img_gray, rubiks_colors)

You can also play around with perceptual color difference metrics from Colors.jl. For faster dithering, the metric DE_AB() can be used, which computes Euclidean distances in Lab color space:

using Colors
dither(img, rubiks_colors; metric=DE_AB())