DitherPunk.jl
A dithering & digital halftoning package inspired by Lucas Pope's Obra Dinn and Surma's blogpost of the same name.
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)

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())

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())

ColorSchemes.jl
Predefined color schemes from ColorSchemes.jl can also be used.
using ColorSchemes
dither(img, ColorSchemes.PuOr_7)

dither(img, Bayer(), ColorSchemes.berlin)

Type ColorSchemes.<TAB>
to get color scheme suggestions!
Automatic color schemes
DitherPunk also allows you to generate optimized color schemes. Simply pass the size of the desired color palette:
dither(img, 8)

dither(img, Bayer(), 8)

Extra glitchiness
If you want glitchy artefacts in your art, use an ErrorDiffusion
method and set the keyword argument clamp_error=false
.
dither(img, FloydSteinberg(), [blue, white, red]; clamp_error=false)

Have fun playing around with different combinations of algorithms, color schemes and color difference metrics!
dither(img, Atkinson(), ColorSchemes.julia; metric=DE_CMC(), clamp_error=false)

Braille
It is also possible to dither images directly to Braille-characters using braille()
. The interface is the same as for binary dithering with dither
:
img = imresize(img; ratio=1//3)
braille(img)
⡪⡪⡪⢒⢕⢕⠪⡪⡒⢕⢕⠪⡪⡒⢕⢕⠪⡪⡒⢕⢕⠕⡪⡪⡒⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⢕⠕⢕⢕⠪⡪⡪⢒⢕⠕
⡪⡪⡪⢕⢕⠥⠭⡪⡪⢕⢕⡩⡪⡪⣑⢕⠭⡪⠪⢕⢕⡩⡪⡪⣊⢕⢕⢕⠥⢕⢕⣒⢕⢕⣒⢕⢕⢕⠥⢕⢕⢕⢕⠕⢕⢕⣒⢕⢕⢕⢒⢕⢕⠥⢕⠭⢕⢕⢍⡪⡪⣑⢕⢍
⡪⡪⡪⠵⢕⡭⠭⡪⡪⣒⢕⠪⡪⡪⡢⢕⠭⡪⡩⢕⢕⡪⡪⠪⡢⢕⢕⢕⢕⢕⢕⣒⢕⢕⣒⢕⢕⢕⢕⢕⢕⢕⢕⠭⢕⢕⣒⢕⢕⢕⢕⢕⢕⠭⢕⢕⢕⢕⢕⠬⡪⣒⢕⣑
⡪⡪⡪⠭⢕⡪⠭⡪⡪⣒⢕⠭⡪⡪⡪⢕⠭⡪⡪⢕⢕⡪⡪⠭⡪⢕⢕⢕⣑⡐⡱⣒⢕⣕⡲⣕⢕⢕⢕⢕⢕⢕⢕⠭⢕⢕⣒⢕⢕⢕⢕⢕⢕⠭⢕⢕⢕⢕⢕⢕⡪⣒⢕⢔
⡪⡪⠮⠭⢕⠮⠭⡪⡪⣒⢕⠭⡪⡪⡪⢕⠭⡪⡪⢕⢕⡪⡪⠭⡪⣕⠵⠕⠿⠠⠪⡪⣓⡪⠮⣒⢕⢕⢕⢕⣕⢕⢕⢝⢕⢕⣒⢕⢕⣕⢕⢕⢕⠭⢕⢕⢕⢕⢕⢕⡪⣒⢕⢕
⡪⡭⢭⠭⣓⢭⠭⣪⠭⣒⢕⢝⡪⡪⡪⣓⠭⡪⡪⢕⢕⠮⡪⢭⡪⣒⠭⠍⢄⠡⡘⣜⣒⣚⠭⡲⢕⢭⢕⠵⣒⢕⠭⣕⢕⣕⢕⢕⠵⣒⢕⠵⢕⢝⢕⢕⢕⣕⢕⢕⡪⣒⢕⢕
⢭⡪⣓⠭⡲⢕⡭⡲⠭⣪⢕⢭⡪⡪⡭⡲⢝⡪⡭⢭⢕⢭⡪⣓⡪⣪⠭⣽⣿⣷⡪⣒⠮⣒⢝⡪⣓⠵⢕⡫⡲⠭⡭⡲⢕⡲⢕⡭⡭⣖⠵⡭⣓⢕⢕⣓⢕⡲⢕⢕⡪⠮⢕⢕
⢵⡪⣎⡫⣚⢕⢮⡪⡫⡲⢕⡳⡪⣚⡪⠮⣓⠭⣪⣓⢕⣓⣪⣪⣚⣒⢝⣾⣽⣽⣑⠵⢭⣪⢕⡭⡪⣫⢕⢭⣚⢭⣚⣚⠵⢝⣕⢝⡪⠮⡭⣪⣒⢭⢕⢕⢕⢭⢕⢕⢭⠭⢕⢕
⣕⠮⠮⢮⢕⡳⠵⡪⠱⠩⢓⠍⢎⠢⢉⠩⡪⣝⡲⣪⠵⣕⢖⢖⢮⡪⡭⣿⣿⣿⣪⡫⣕⡲⢕⠮⣝⣒⡭⣕⡵⣕⣎⣎⠯⣕⠮⢕⡭⡭⣪⡲⡲⠵⢕⢭⣓⢕⣕⣕⢵⣙⡪⣕
⣪⢝⢭⣓⠵⠭⠑⠌⡊⡊⡢⡑⢅⠕⠔⠵⠐⡐⠙⢖⠽⡲⡭⣕⣓⢮⣺⣿⣿⣟⢮⢪⡪⣞⢭⢝⣪⢲⠭⢖⢮⢕⣕⠮⣝⠲⢭⣓⣚⡪⡲⡪⣚⢭⣓⢕⢕⡭⣒⢕⢕⠮⣪⣒
⢕⢭⣓⡪⡑⢍⡋⢏⢓⢌⠢⢌⡲⣀⣝⡸⣲⡈⡂⡑⢝⠮⠮⠵⣕⡳⣺⣿⣿⣿⣪⠵⢭⡲⠵⢭⣪⣓⣝⣕⢧⣓⢖⠽⣜⢭⣓⠮⡲⠭⠮⣚⢖⢕⢖⢕⣓⡪⣪⢕⡭⡭⡲⣒
⣳⡨⣒⡠⢌⣔⢬⢄⣂⡂⢌⢖⠌⡶⣐⠏⡮⡪⣘⠮⢐⢈⠂⡍⣔⠨⠔⢝⢿⣿⡷⡝⡑⡉⠍⡂⠢⠂⠢⢑⠑⡵⢭⡫⢖⡳⣒⡫⣝⢭⡫⣪⢕⣓⡭⠵⢕⡭⡲⢕⠮⡪⣚⣒
⠢⠕⠢⢊⣑⢌⢪⠕⠔⠬⠡⡩⢉⠔⡐⡡⢐⠨⢘⠭⠦⢔⢵⢫⡻⡳⢧⣻⣿⣿⣻⣒⢌⡨⢍⡢⣑⡑⡑⡄⡵⠬⡒⢌⠕⢌⠢⠕⢔⠱⢊⢒⢍⢒⢌⠫⡑⣊⠪⡑⢍⠪⢒⠒
⠨⠨⠊⠐⠔⠈⠂⡒⡉⢌⢊⢌⢐⢌⠐⡐⡁⠊⡒⡪⢊⡢⡑⡡⠪⠬⢑⢔⠒⢜⢌⡒⠕⡪⣊⣒⡲⠬⢱⢜⡪⠩⠨⢔⢕⠥⡑⢅⠣⡑⡡⢅⡢⢥⣒⢮⠮⡲⠭⡒⢕⢪⣂⢍
⡈⢂⡡⡑⣁⠅⢕⠔⢌⠢⡡⣂⠢⡢⡑⢌⠔⠥⢊⢌⠄⡨⡨⠢⢑⢊⢒⡢⢌⡢⡢⡒⠭⠬⠢⢖⠔⡪⣕⠕⠭⡑⢬⢪⢓⠦⡊⠦⢑⠬⣘⠨⠬⣒⡲⣕⢕⡲⢭⣐⣑⡨⠔⡢
⡊⡒⠬⠨⢄⠍⠢⠊⡂⠅⢂⠢⡡⢢⢪⡓⡪⢕⢕⠥⣕⢕⣪⣋⡪⣒⢖⢒⢕⡪⡰⣊⠭⣒⢭⣓⠮⢫⠪⡍⡡⠈⢅⠁⠜⡉⠌⡨⢕⢝⡢⠭⢍⣒⡪⢵⢕⠭⡕⢎⢎⠮⠭⡪
⠈⠨⡠⢕⢕⡪⣉⢉⢄⡫⣒⣑⣐⡢⣓⢮⣚⢭⣓⡫⡪⠕⢖⠰⡩⠢⡣⢕⢅⢍⢒⠐⢊⠒⡡⠨⠊⠢⢁⠂⠢⢁⢂⠨⢄⠢⠢⢑⢁⢑⠌⠍⠢⢂⢊⠡⠐⠐⠐⠁⢂⠊⠊⠢
⡪⡒⠬⡒⡒⠔⢔⢕⠧⣚⣒⢕⠲⠩⡨⠦⡲⣑⢕⢊⢊⢉⢄⠩⡨⢑⢌⠄⢂⠂⢂⠈⠄⢐⠀⠅⠄⠡⢠⣐⣔⣒⣒⢭⡲⣬⣥⣡⣐⣤⣦⣅⣡⣂⡠⣐⣌⣄⠡⠥⠠⢂⠱⣷
⢒⣊⡪⠬⢒⡡⠣⠭⠪⠐⠄⠡⠡⠁⠌⣐⣈⡐⠡⠡⢐⠡⢐⠊⡐⢐⠀⠊⡀⢌⢀⡢⡲⠦⠦⢖⠝⢝⠫⢕⠕⢝⢚⢛⠫⡑⠕⠕⠝⠩⢋⢛⠫⢛⣙⠫⢛⠻⠭⢚⡒⠦⠵⠲
⢑⠒⠌⠔⠀⠄⢈⠐⡈⠐⡈⠌⠢⢁⠑⡐⠡⡘⡢⡑⡂⡂⠦⠲⠪⠍⠭⡑⡒⡑⠅⡪⠒⠥⡩⠢⠪⠄⠥⡡⠨⡐⠄⢅⢢⠢⠥⣑⢌⠢⠡⡐⢌⢄⠢⡨⢔⡨⡨⢐⠌⠔⠌⡊
⢐⢐⠔⠐⠌⢐⢄⡂⣂⢅⣊⢌⠌⠄⠍⠌⠌⠠⡐⡀⠄⠅⡂⠅⠅⠕⠌⠌⠔⢌⠩⡐⡑⡡⠂⠕⡁⢕⠡⠢⠡⢒⢑⢑⢐⠍⠅⢒⠡⡉⠅⠍⡂⠅⡉⠄⡂⢂⢂⠡⠢⠡⢑⠨
⠒⠑⠒⠓⠙⠒⠑⠙⠙⠉⠐⠐⠈⠈⠈⠊⠐⠈⠐⠀⠁⠁⠂⠁⠁⠑⠑⠁⠁⠁⠂⠒⠐⠀⠑⠁⠂⠑⠊⠈⠊⠐⠐⠐⠐⠈⠈⠂⠂⠂⠁⠁⠂⠁⠂⠁⠈⠐⠐⠈⠈⠈⠐⠈
Depending on the color of the Unicode characters and the background, you might also want to invert
the output:
braille(img, Bayer(); invert=true)
⡪⡪⣪⡪⣪⡪⡪⡪⣪⡪⣪⡪⣪⡪⡪⡪⣪⡪⣪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⣪⡪⣪⡪⣪⡪
⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⣪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⣪⡪
⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪
⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⠮⡫⡮⡪⡪⡊⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪
⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡢⡪⡢⣪⣀⣺⣢⡪⡢⡪⡢⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪
⡢⡪⡢⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⣺⣾⡺⣦⡪⡪⡪⡢⡪⡪⡪⡢⡪⡢⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪⡪
⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡊⡢⡊⡢⡪⡢⡪⡢⡪⡢⡪⡢⡊⠀⠈⣪⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡊⡢⡊⡢⡪⡢⡪⡢⡪⡢⡪⡪⡪⡪⡪⡪⡪
⡢⡪⡪⡪⡢⡪⡢⡪⡢⡪⡢⡊⣢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⠂⠂⠂⣪⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡊⡢⡊⡢⡊⡢⡊⡢⡪⡪⡪⡢⡪⡢⡪⡢⡪⡪⡪⡪⡪
⡢⡪⡢⡪⡢⡪⡂⣪⣪⣪⣢⣪⣪⣺⣶⣺⣢⡊⡢⡊⡢⡪⡢⡊⡢⡊⡢⠀⠀⠀⡠⡪⡢⡪⡢⡪⡢⡊⡢⡊⡂⡊⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡪⡪⡪⡪⡢⡪⡪⡪⡢⡪
⡢⡊⡢⡪⡢⡪⣪⣺⣺⣺⣪⣺⣪⣺⣺⣚⣮⣺⣦⡊⡢⡊⡢⡪⡢⡊⡢⠀⠀⠀⡠⡪⡢⡪⡢⡪⡢⡊⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡢⡢⡪⡢⡪
⣢⡪⡢⣪⣪⡺⣨⣸⣨⡺⣪⡺⡪⡻⡪⡊⡪⡺⣾⡺⣢⣊⣢⡊⡢⡊⡂⠀⠀⠀⡠⡪⡢⡊⡢⡪⡢⡊⡢⡊⡢⡊⡢⡊⡢⡊⡢⡊⡢⡊⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡪⡪
⡪⡺⡪⡚⡺⡺⡚⡺⡺⡺⡺⡋⣺⡊⡪⣺⣢⣲⡪⡊⣮⣺⣮⡺⡪⣺⣪⡢⡀⠀⠀⣪⣦⡺⣮⣺⣮⣺⣦⣺⣦⡊⡢⡊⡢⡊⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪⡢⡪
⣢⣺⣪⣺⡪⡲⣢⣺⣪⣺⣪⡲⣦⣺⣪⣺⣺⣻⣦⡪⣪⡛⡊⡀⣠⡊⡚⡂⡀⡀⡠⡺⣪⡫⡪⡺⣪⡺⣪⡺⡊⡪⣪⡪⣪⡺⣪⣺⣪⣪⣢⣪⣢⣺⣢⡪⣢⣺⣢⣺⣢⣺⣪⣪
⣾⣺⣾⣾⣺⣺⣺⣺⣺⣺⣪⡺⣺⣺⣾⣺⣾⣺⣪⡪⣪⣺⣪⣺⣪⣺⣪⣪⣪⣢⣢⣪⣪⡪⡪⡪⣂⣺⡪⡪⡢⣪⣺⡺⣪⣺⣪⡺⣪⣺⡪⡺⡪⡺⡪⡊⣪⡪⣪⡪⣪⡊⣪⣺
⣾⣺⡾⣺⣺⣺⣪⣺⣪⣺⣪⡺⣪⣺⣺⣺⣪⡺⣪⡺⣺⡺⣪⡺⣮⣺⣪⡺⣪⡺⣪⡪⣪⣪⣪⡪⣪⡪⡢⡺⣪⣺⡺⡢⡢⡚⣪⡺⣪⡺⣪⣺⣪⡪⡪⡊⡪⡪⡪⡺⡮⡺⣪⡺
⣪⡪⣪⣺⣺⣺⣪⣺⣪⣺⣺⡺⣺⡪⡪⣪⣪⡪⡪⡺⡪⡪⡢⡺⡢⡪⣪⡪⡢⡺⣪⡪⣪⡺⡪⡊⣢⡪⣢⡺⣺⣺⣪⣺⣦⣺⣾⡫⡢⡢⡪⡪⣢⡪⣢⡊⣪⣪⣢⣪⣪⡊⣢⡪
⣾⣾⣺⡫⣪⡪⣢⣺⣺⡪⡪⡺⡺⡪⡢⡊⡢⡊⡢⡪⣢⣪⡪⣪⡢⡺⣪⡪⣺⣪⣪⣾⣮⣪⣾⣺⣾⣺⣾⣺⣾⣺⣾⣺⣺⣺⣺⡺⣾⣺⣪⣺⣪⣺⣶⣺⣾⣻⣾⣺⣾⣺⣾⣺
⣪⣫⣪⡪⣪⣺⡪⡪⣢⡪⣢⡊⣪⣺⣲⣚⡢⡪⡢⡪⣢⣺⣺⡺⣪⣺⣺⣺⣾⣺⣾⣾⣾⣿⣾⣺⣾⣻⡾⡻⡺⠫⡪⡪⡊⡚⡪⠺⡺⠚⠚⠺⠺⠺⡾⡻⠺⠻⣺⣻⣾⣻⣮⡈
⣪⡺⡪⣺⣪⣺⡮⣪⣢⣺⣺⣺⣺⣻⣾⡺⡾⣺⣾⣺⣺⣺⣺⣺⣾⡺⣾⣾⣾⡺⣾⡺⣪⡙⣊⣫⣢⣪⣠⡢⣢⣪⣠⣢⣠⣢⣪⣪⣢⣢⣢⣢⣢⡢⣢⣢⣢⣠⣲⣢⣺⣚⣪⣊
⣢⣺⣺⣺⣾⣾⣾⣾⣾⣿⣾⣺⣺⣺⣾⣺⣾⣪⣪⣺⣪⣻⣪⣫⣢⣲⣢⡪⣪⣺⣪⣪⣪⣺⣪⣺⣪⣺⣺⣺⣺⣺⣺⣺⣺⣺⣺⡺⣺⣺⣺⡺⣺⡺⣺⡺⣪⡺⣺⣺⣺⣺⣪⣺
⣾⣻⣪⣻⣾⡺⣺⡺⡺⡺⡾⡺⣺⣺⣺⣺⣾⣺⡾⣻⣾⣺⣾⣺⣺⣺⣺⣺⣪⣺⣺⣺⣺⣺⣪⣺⣮⡺⣪⣪⣺⣺⣮⣪⣪⣺⣪⣺⣮⣺⣺⣺⣪⣺⣾⣺⣺⣺⣺⣺⣪⣺⣾⣺
⠊⠊⠈⠊⠊⠊⠊⠂⠂⠒⠚⠛⠚⠛⠚⠚⠚⠚⠚⠚⠚⠚⠚⠚⠚⠚⠊⠚⠚⠚⠚⠊⠊⠚⠊⠚⠚⠊⠚⠚⠚⠚⠚⠚⠚⠚⠊⠚⠊⠚⠚⠚⠊⠚⠚⠚⠚⠚⠊⠚⠚⠚⠚⠚