davideisinger.com

My personal website
Log | Files | Refs | README

surma-dev-e4sfuv.txt (30214B)


      1 [1]← Back to home
      2 
      3 Ditherpunk — The article I wish I had about monochrome image dithering
      4 
      5 2021-01-04
      6 
      7 I always loved the visual aesthetic of dithering but never knew how it’s done.
      8 So I did some research. This article may contain traces of nostalgia and none
      9 of Lena.
     10 
     11 How did I get here? (You can skip this)
     12 
     13 I am late to the party, but I finally played [2]“Return of the Obra Dinn”, the
     14 most recent game by [3]Lucas Pope of [4]“Papers Please” fame. Obra Dinn is a
     15 story puzzler that I can only recommend, but what piqued my curiosity as a
     16 software engineer is that it is a 3D game (using the [5]Unity game engine) but
     17 rendered using only 2 colors with dithering. Apparently, this has been dubbed
     18 “Ditherpunk”, and I love that.
     19 
     20 [obradinn] Screenshot of “Return of the Obra Dinn”.
     21 
     22 Dithering, so my original understanding, was a technique to place pixels using
     23 only a few colors from a palette in a clever way to trick your brain into
     24 seeing many colors. Like in the picture, where you probably feel like there are
     25 multiple brightness levels when in fact there’s only two: Full brightness and
     26 black.
     27 
     28 The fact that I have never seen a 3D game with dithering like this probably
     29 stems from the fact that color palettes are mostly a thing of the past. You may
     30 remember running Windows 95 with 16 colors and playing games like “Monkey
     31 Island” on it.
     32 
     33 [win95] Windows 95 configured to use 16 colors. Now spend hours trying to find
     34 the right floppy disk with the drivers to get the “256 colors” or, gasp, “True
     35 Color” show up. [monkeyisland16] Screenshot of “The Secret of Monkey Island”
     36 using 16 colors.
     37 
     38 For a long time now, however, we have had 8 bits per channel per pixel,
     39 allowing each pixel on your screen to assume one of 16 million colors. With HDR
     40 and wide gamut on the horizon, things are moving even further away to ever
     41 requiring any form of dithering. And yet Obra Dinn used it anyway and rekindled
     42 a long forgotten love for me. Knowing a tiny bit about dithering from my work
     43 on [6]Squoosh, I was especially impressed with Obra Dinn’s ability to keep the
     44 dithering stable while I moved and rotated the camera through 3D space and I
     45 wanted to understand how it all worked.
     46 
     47 As it turns out, Lucas Pope wrote a [7]forum post where he explains which
     48 dithering techniques he uses and how he applies them to 3D space. He put
     49 extensive work into making the dithering stable when camera movements occur.
     50 Reading that forum post kicked me down the rabbit hole, which this blog post
     51 tries to summarize.
     52 
     53 Dithering
     54 
     55 What is Dithering?
     56 
     57 According to Wikipedia, “Dither is an intentionally applied form of noise used
     58 to randomize quantization error”, and is a technique not only limited to
     59 images. It is actually a technique used to this day on audio recordings, but
     60 that is yet another rabbit hole to fall into another time. Let’s dissect that
     61 definition in the context of images. First up: Quantization.
     62 
     63 Quantization
     64 
     65 Quantization is the process of mapping a large set of values to a smaller,
     66 usually finite, set of values. For the remainder of this article, I am going to
     67 use two images as examples:
     68 
     69 [dark-original] Example image #1: A black-and-white photograph of San
     70 Francisco’s Golden Gate Bridge, downscaled to 400x267 ([8]higher resolution).
     71 [light-original] Example image #2: A black-and-white photograph of San
     72 Francisco’s Bay Bridge, downscaled to 253x400 ([9]higher resolution).
     73 
     74 Both black-and-white photos use 256 different shades of gray. If we wanted to
     75 use fewer colors — for example just black and white to achieve monochromaticity
     76 — we have to change every pixel to be either pure black or pure white. In this
     77 scenario, the colors black and white are called our “color palette” and the
     78 process of changing pixels that do not use a color from the palette is called
     79 “quantization”. Because not all colors from the original images are in the
     80 color palette, this will inevitably introduce an error called the “quantization
     81 error”. The naïve solution is to quantizer each pixel to the color in the
     82 palette that is closest to the pixel’s original color.
     83 
     84     Note: Defining which colors are “close to each other” is open to
     85     interpretation and depends on how you measure the distance between two
     86     colors. I suppose ideally we’d measure distance in a psycho-visual way, but
     87     most of the articles I found simply used the euclidian distance in the RGB
     88     cube, i.e. Δred2+Δgreen2+Δblue2\sqrt{\Delta\text{red}^2 + \Delta\text
     89     {green}^2 + \Delta\text{blue}^2}Δred2+Δgreen2+Δblue2​.
     90 
     91 With our palette only consisting of black and white, we can use the brightness
     92 of a pixel to decide which color to quantize to. A brightness of 0 means black,
     93 a brightness of 1 means white, everything else is in-between, ideally
     94 correlating with human perception such that a brightness of 0.5 is a nice
     95 mid-gray. To quantize a given color, we only need to check if the color’s
     96 brightness is greater or less than 0.5 and quantize to white and black
     97 respectively. Applying this quantization to the image above yields an...
     98 unsatisfying result.
     99 
    100 grayscaleImage.mapSelf(brightness =>
    101   brightness > 0.5
    102     ? 1.0
    103     : 0.0
    104 );
    105 
    106     Note: The code samples in this article are real but built on top of a
    107     helper class GrayImageF32N0F8 I wrote for the [10]demo of this article.
    108     It’s similar to the web’s [11]ImageData, but uses Float32Array, only has
    109     one color channel, represents values between 0.0 and 1.0 and has a whole
    110     bunch of helper functions. The source code is available in [12]the lab.
    111 
    112 [dark-quantized] [light-quantized] Each pixel has been quantized to the either
    113 black or white depending on its brightness.
    114 
    115 Gamma
    116 
    117 I had finished writing this article and just wanted to “quickly” look what a
    118 black-to-white gradient looks like with the different dithering algorithms. The
    119 results showed me that I failed to consider the thing that always becomes a
    120 problem when working with images: color spaces. I had written the sentence
    121 “ideally correlating with human perception” without actually following it
    122 myself.
    123 
    124 My [13]demo is implemented using web technologies, most notably <canvas> and
    125 ImageData, which are — at the time of writing — specified to use [14]sRGB. It’s
    126 an old color space specification (from 1996) whose value-to-color mapping was
    127 modeled to mirror the behavior of CRT monitors. While barely anyone uses CRTs
    128 these days, it’s still considered the “safe” color space that gets correctly
    129 displayed on every display. As such, it is the default on the web platform.
    130 However, sRGB is not linear, meaning that (0.5,0.5,0.5)(0.5, 0.5, 0.5)(0.5,0.5,
    131 0.5) in sRGB is not the color a human sees when you mix 50% of (0,0,0)(0, 0, 0)
    132 (0,0,0) and (1,1,1)(1, 1, 1)(1,1,1). Instead, it’s the color you get when you
    133 pump half the power of full white through your Cathode-Ray Tube (CRT).
    134 
    135 [gradient-srgb] A gradient and how it looks when dithered in sRGB color space.
    136 
    137 
    138     Warning: I set image-rendering: pixelated; on most of the images in this
    139     article. This allows you to zoom in and truly see the pixels. However, on
    140     devices with fraction devicePixelRatio, this might introduce artifacts. If
    141     in doubt, open the image separate in a new tab.
    142 
    143 As this image shows, the dithered gradient gets bright way too quickly. If we
    144 want 0.5 be the color in the middle of pure black and white (as perceived by a
    145 human), we need to convert from sRGB to linear RGB space, which can be done
    146 with a process called “gamma correction”. Wikipedia lists the following
    147 formulas to convert between sRGB and linear RGB.
    148 
    149 srgbToLinear(b)={b12.92b≤0.04045(b+0.0551.055)γotherwiselinearToSrgb(b)=
    150 {12.92⋅bb≤0.00313081.055⋅b1γ−0.055otherwise(γ=2.4)\begin{array}{rcl} \text
    151 {srgbToLinear}(b) & = & \left\{\begin{array}{ll} \frac{b}{12.92} & b \le
    152 0.04045 \\ \left( \frac{b + 0.055}{1.055}\right)^{\gamma} & \text{otherwise} \
    153 end{array}\right.\\ \text{linearToSrgb}(b) & = & \left\{\begin{array}{ll} 12.92
    154 \cdot b & b \le 0.0031308 \\ 1.055 \cdot b^\frac{1}{\gamma} - 0.055 & \text
    155 {otherwise} \end{array}\right.\\ (\gamma = 2.4) \end{array}\\ srgbToLinear(b)
    156 linearToSrgb(b)(γ=2.4)​==​{12.92b​(1.055b+0.055​)γ​b≤0.04045otherwise​{12.92⋅b1
    157 .055⋅bγ1​−0.055​b≤0.0031308otherwise​​
    158 
    159 Formulas to convert between sRGB and linear RGB color space. What beauties they
    160 are 🙄. So intuitive.
    161 
    162 With these conversions in place, dithering produces (more) accurate results:
    163 
    164 [gradient-linear] A gradient and how it looks when dithered in linear RGB color
    165 space.
    166 
    167 Random noise
    168 
    169 Back to Wikipedia’s definition of dithering: “Intentionally applied form of
    170 noise used to randomize quantization error”. We got the quantization down, and
    171 now it says to add noise. Intentionally.
    172 
    173 Instead of quantizing each pixel directly, we add noise with a value between
    174 -0.5 and 0.5 to each pixel. The idea is that some pixels will now be quantized
    175 to the “wrong” color, but how often that happens depends on the pixel’s
    176 original brightness. Black will always remain black, white will always remain
    177 white, a mid-gray will be dithered to black roughly 50% of the time.
    178 Statistically, the overall quantization error is reduced and our brains are
    179 eager to do the rest and help you see the, uh, big picture.
    180 
    181 grayscaleImage.mapSelf(brightness =>
    182   brightness + (Math.random() - 0.5) > 0.5
    183     ? 1.0
    184     : 0.0
    185 );
    186 
    187 [dark-random] [light-random] Random noise [-0.5; 0.5] has been added to each
    188 pixel before quantization.
    189 
    190 I found this quite surprising! It is by no means good — video games from the
    191 90s have shown us that we can do better — but this is a very low effort and
    192 quick way to get more detail into a monochrome image. And if I was to take
    193 “dithering” literally, I’d end my article here. But there’s more…
    194 
    195 Ordered Dithering
    196 
    197 Instead of talking about what kind of noise to add to an image before
    198 quantizing it, we can also change our perspective and talk about adjusting the
    199 quantization threshold.
    200 
    201 // Adding noise
    202 grayscaleImage.mapSelf(brightness =>
    203   brightness + Math.random() - 0.5 > 0.5
    204     ? 1.0
    205     : 0.0
    206 );
    207 
    208 // Adjusting the threshold
    209 grayscaleImage.mapSelf(brightness =>
    210   brightness > Math.random()
    211     ? 1.0
    212     : 0.0
    213 );
    214 
    215 In the context of monochrome dithering, where the quantization threshold is
    216 0.5, these two approaches are equivalent:
    217 
    218 brightness+rand()−0.5>0.5⇔brightness>1.0−rand()⇔brightness>rand()\begin{array}
    219 {} & \mathrm{brightness} + \mathrm{rand}() - 0.5 & > & 0.5 \\ \Leftrightarrow &
    220 \mathrm{brightness} & > & 1.0 - \mathrm{rand}() \\ \Leftrightarrow & \mathrm
    221 {brightness} &>& \mathrm{rand}() \end{array} ⇔⇔​brightness+rand()−0.5brightnes
    222 sbrightness​>>>​0.51.0−rand()rand()​
    223 
    224 The upside of this approach is that we can talk about a “threshold map”.
    225 Threshold maps can be visualized to make it easier to reason about why a
    226 resulting image looks the way it does. They can also be precomputed and reused,
    227 which makes the dithering process deterministic and parallelizable per pixel.
    228 As a result, the dithering can happen on the GPU as a shader. This is what Obra
    229 Dinn does! There are a couple of different approaches to generating these
    230 threshold maps, but all of them introduce some kind of order to the noise that
    231 is added to the image, hence the name “ordered dithering”.
    232 
    233 The threshold map for the random dithering above, literally a map full of
    234 random thresholds, is also called “white noise”. The name comes from a term in
    235 signal processing where every frequency has the same intensity, just like in
    236 white light.
    237 
    238 [whitenoise] The threshold map for O.G. dithering is, by definition, white
    239 noise.
    240 
    241 Bayer Dithering
    242 
    243 “Bayer dithering” uses a Bayer matrix as the threshold map. They are named
    244 after Bryce Bayer, inventor of the [15]Bayer filter, which is in use to this
    245 day in digital cameras. Each pixel on the sensor can only detect brightness,
    246 but by cleverly arranging colored filters in front of the individual pixels, we
    247 can reconstruct color images through [16]demosaicing. The pattern for the
    248 filters is the same pattern used in Bayer dithering.
    249 
    250 Bayer matrices come in various sizes which I ended up calling “levels”. Bayer
    251 Level 0 is 2×22 \times 22×2 matrix. Bayer Level 1 is a 4×44 \times 44×4 matrix.
    252 Bayer Level nnn is a 2n+1×2n+12^{n+1} \times 2^{n+1}2n+1×2n+1 matrix. A level 
    253 nnn matrix can be recursively calculated from level n−1n-1n−1 (although
    254 Wikipedia also lists an [17]per-cell algorithm). If your image happens to be
    255 bigger than your bayer matrix, you can tile the threshold map.
    256 
    257 Bayer(0)=(0231)\begin{array}{rcl} \text{Bayer}(0) & = & \left( \begin{array}
    258 {cc} 0 & 2 \\ 3 & 1 \\ \end{array} \right) \\ \end{array} Bayer(0)​=​(03​21​)​
    259 
    260 Bayer(n)=(4⋅Bayer(n−1)+04⋅Bayer(n−1)+24⋅Bayer(n−1)+34⋅Bayer(n−1)+1)\begin
    261 {array}{c} \text{Bayer}(n) = \\ \left( \begin{array}{cc} 4 \cdot \text{Bayer}
    262 (n-1) + 0 & 4 \cdot \text{Bayer}(n-1) + 2 \\ 4 \cdot \text{Bayer}(n-1) + 3 & 4
    263 \cdot \text{Bayer}(n-1) + 1 \\ \end{array} \right) \end{array} Bayer(n)=(4⋅
    264 Bayer(n−1)+04⋅Bayer(n−1)+3​4⋅Bayer(n−1)+24⋅Bayer(n−1)+1​)​
    265 
    266 Recursive definition of Bayer matrices.
    267 
    268 A level nnn Bayer matrix contains the numbers 000 to 22n+22^{2n+2}22n+2. Once
    269 you normalize the Bayer matrix, i.e. divide by 22n+22^{2n+2}22n+2, you can use
    270 it as a threshold map:
    271 
    272 const bayer = generateBayerLevel(level);
    273 grayscaleImage.mapSelf((brightness, { x, y }) =>
    274   brightness > bayer.valueAt(x, y, { wrap: true })
    275     ? 1.0
    276     : 0.0
    277 );
    278 
    279 One thing to note: Bayer dithering using matrices as defined above will render
    280 an image lighter than it originally was. For example: An area where every pixel
    281 has a brightness of 1255=0.4%\frac{1}{255} = 0.4\%2551​=0.4%, a level 0 Bayer
    282 matrix of size 2×22\times22×2 will make one out of the four pixels white,
    283 resulting in an average brightness of 25%25\%25%. This error gets smaller with
    284 higher Bayer levels, but a fundamental bias remains.
    285 
    286 [bayerbias] The almost-black areas in the image are getting noticeably
    287 brighter.
    288 
    289 In our dark test image, the sky is not pure black and made significantly
    290 brighter when using Bayer Level 0. While it gets better with higher levels, an
    291 alternative solution is to flip the bias and make images render darker by
    292 inverting the way we use the Bayer matrix:
    293 
    294 const bayer = generateBayerLevel(level);
    295 grayscaleImage.mapSelf((brightness, { x, y }) =>
    296   //           👇
    297   brightness > 1 - bayer.valueAt(x, y, { wrap: true })
    298     ? 1.0
    299     : 0.0
    300 );
    301 
    302 I have used the original Bayer definition for the light image and the inverted
    303 version for the dark image. I personally found Level 1 and 3 the most
    304 aesthetically pleasing.
    305 
    306 [dark-bayer0] [light-bayer0] Bayer Dithering Level 0. [dark-bayer1]
    307 [light-bayer1] Bayer Dithering Level 1. [dark-bayer2] [light-bayer2] Bayer
    308 Dithering Level 2. [dark-bayer3] [light-bayer3] Bayer Dithering Level 3.
    309 
    310 Blue noise
    311 
    312 Both white noise and Bayer dithering have drawbacks, of course. Bayer
    313 dithering, for example, is very structured and will look quite repetitive,
    314 especially at lower levels. White noise is random, meaning that there
    315 inevitably will be clusters of bright pixels and voids of darker pixels in the
    316 threshold map. This can be made more obvious by squinting or, if that is too
    317 much work for you, through blurring the threshold map algorithmically. These
    318 clusters and voids can affect the output of the dithering process negatively.
    319 If darker areas of the image fall into one of the clusters, details will get
    320 lost in the dithered output (and vice-versa for brighter areas falling into
    321 voids).
    322 
    323 [whitenoiseblur] Clear clusters and voids remain visible even after applying a
    324 Gaussian blur (σ = 1.5).
    325 
    326 There is a variant of noise called “blue noise”, that addresses this issue. It
    327 is called blue noise because higher frequencies have higher intensities
    328 compared to the lower frequencies, just like blue light. By removing or
    329 dampening the lower frequencies, cluster and voids become less pronounced. Blue
    330 noise dithering is just as fast to apply to an image as white noise dithering —
    331 it’s just a threshold map in the end — but generating blue noise is a bit
    332 harder and expensive.
    333 
    334 The most common algorithm to generate blue noise seems to be the
    335 “void-and-cluster method” by [18]Robert Ulichney. Here is the [19]original
    336 whitepaper. I found the way the algorithm is described quite unintuitive and,
    337 now that I have implemented it, I am convinced it is explained in an
    338 unnecessarily abstract fashion. But it is quite clever!
    339 
    340 The algorithm is based on the idea that you can find a pixel that is part of
    341 cluster or a void by applying a [20]Gaussian Blur to the image and finding the
    342 brightest (or darkest) pixel in the blurred image respectively. After
    343 initializing a black image with a couple of randomly placed white pixels, the
    344 algorithm proceeds to continuously swap cluster pixels and void pixels to
    345 spread the white pixels out as evenly as possible. Afterwards, every pixel gets
    346 a number between 0 and n (where n is the total number of pixels) according to
    347 their importance for forming clusters and voids. For more details, see the [21]
    348 paper.
    349 
    350 My implementation works fine but is not very fast, as I didn’t spend much time
    351 optimizing. It takes about 1 minute to generate a 64×64 blue noise texture on
    352 my 2018 MacBook, which is sufficient for these purposes. If something faster is
    353 needed, a promising optimization would be to apply the Gaussian Blur not in the
    354 spatial domain but in the frequency domain instead.
    355 
    356     Excursion: Of course knowing this nerd-sniped me into implementing it. The
    357     reason this optimization is so promising is because convolution (which is
    358     the underlying operation of a Gaussian blur) has to loop over each field of
    359     the Gaussian kernel for each pixel in the image. However, if you convert
    360     both the image as well as the Gaussian kernel to the frequency domain
    361     (using one of the many Fast Fourier Transform algorithms), convolution
    362     becomes an element-wise multiplication. Since my targeted blue noise size
    363     is a power of two, I could implement the well-explored [22]in-place variant
    364     of the Cooley-Tukey FFT algorithm. After [23]some initial hickups, it did
    365     end up cutting the blue noise generation time by 50%. I still wrote pretty
    366     garbage-y code, so there’s a lot more to room for optimizations.
    367 
    368 [bluenoiseblur] A 64×64 blue noise with a Gaussian blur applied (σ = 1.5). No
    369 clear structures remain.
    370 
    371 As blue noise is based on a Gaussian Blur, which is calculated on a torus (a
    372 fancy way of saying that Gaussian blur wraps around at the edges), blue noise
    373 will also tile seamlessly. So we can use the 64×64 blue noise and repeat it to
    374 cover the entire image. Blue noise dithering has a nice, even distribution
    375 without showing any obvious patterns, balancing rendering of details and
    376 organic look.
    377 
    378 [dark-bluenoise] [light-bluenoise] Blue noise dithering.
    379 
    380 Error diffusion
    381 
    382 All the previous techniques rely on the fact that quantization errors will
    383 statistically even out because the thresholds in the threshold maps are
    384 uniformly distributed. A different approach to quantization is the concept of
    385 error diffusion, which is most likely what you have read about if you have ever
    386 researched image dithering before. In this approach we don’t just quantize and
    387 hope that on average the quantization error remains negligible. Instead, we
    388 measure the quantization error and diffuse the error onto neighboring pixels,
    389 influencing how they will get quantized. We are effectively changing the image
    390 we want to dither as we go along. This makes the process inherently sequential.
    391 
    392     Foreshadowing: One big advantage of error diffusion algorithms that we
    393     won’t touch on in this post is that they can handle arbitrary color
    394     palettes, while ordered dithering requires your color palette to be evenly
    395     spaced. More on that another time.
    396 
    397 Almost all error diffusion ditherings that I am going to look at use a
    398 “diffusion matrix”, which defines how the quantization error from the current
    399 pixel gets distributed across the neighboring pixels. For these matrices it is
    400 often assumed that the image’s pixels are traversed top-to-bottom,
    401 left-to-right — the same way us westerners read text. This is important as the
    402 error can only be diffused to pixels that haven’t been quantized yet. If you
    403 find yourself traversing an image in a different order than the diffusion
    404 matrix assumes, flip the matrix accordingly.
    405 
    406 “Simple” 2D error diffusion
    407 
    408 The naïve approach to error diffusion shares the quantization error between the
    409 pixel below the current one and the one to the right, which can be described
    410 with the following matrix:
    411 
    412 (∗0.50.50)\left(\begin{array}{cc} * & 0.5 \\ 0.5 & 0 \\ \end{array} \right) (∗0
    413 .5​0.50​)
    414 
    415 Diffusion matrix that shares half the error to 2 neightboring pixels, * marking
    416 the current pixel.
    417 
    418 The diffusion algorithm visits each pixel in the image (in the right order!),
    419 quantizes the current pixel and measures the quantization error. Note that the
    420 quantization error is signed, i.e. it can be negative if the quantization made
    421 the pixel brighter than the original brightness value. We then add fractions of
    422 the quantization error to neighboring pixels as specified by the matrix. Rinse
    423 and repeat.
    424 
    425 Error diffusion visualized step by step.
    426 
    427 This animation is supposed to visualize the algorithm, but won’t be able to
    428 show that the dithered result resembles the original. 4×4 pixels are hardly
    429 enough do diffuse and average out quantization errors. But it does show that if
    430 a pixel is made brighter during quantization, neighboring pixels will be made
    431 darker to make up for it (and vice-versa).
    432 
    433 [dark-simple2d] [light-simple2d] Simple 2D Error Diffusion Dithering.
    434 
    435 However, the simplicity of the diffusion matrix is prone to generating
    436 patterns, like the line-like patterns you can see in the test images above.
    437 
    438 Floyd-Steinberg
    439 
    440 Floyd-Steinberg is arguably the most well-known error diffusion algorithm, if
    441 not even the most well-known dithering algorithm. It uses a more elaborate
    442 diffusion matrix to distribute the quantization error to all directly
    443 neighboring, unvisited pixels. The numbers are carefully chosen to prevent
    444 repeating patterns as much as possible.
    445 
    446 116⋅(∗7351)\frac{1}{16} \cdot \left(\begin{array} {} & * & 7 \\ 3 & 5 & 1 \\ \
    447 end{array} \right) 161​⋅(3​∗5​71​)
    448 
    449 Diffusion matrix by Robert W. Floyd and Louis Steinberg.
    450 
    451 Floyd-Steinberg is a big improvement as it prevents a lot of patterns from
    452 forming. However, larger areas with little texture can still end up looking a
    453 bit unorganic.
    454 
    455 [dark-floydsteinberg] [light-floydsteinberg] Floyd-Steinberg Error Diffusion
    456 Dithering.
    457 
    458 Jarvis-Judice-Ninke
    459 
    460 Jarvis, Judice and Ninke take an even bigger diffusion matrix, distributing the
    461 error to more pixels than just immediately neighboring ones.
    462 
    463 148⋅(∗753575313531)\frac{1}{48} \cdot \left(\begin{array} {} & {} & * & 7 & 5 \
    464 \ 3 & 5 & 7 & 5 & 3 \\ 1 & 3 & 5 & 3 & 1 \\ \end{array} \right) 481​⋅⎝⎛​31​53​∗
    465 75​753​531​⎠⎞​
    466 
    467 Diffusion matrix by J. F. Jarvis, C. N. Judice, and W. H. Ninke of Bell Labs.
    468 
    469 Using this diffusion matrix, patterns are even less likely to emerge. While the
    470 test images still show some line like patterns, they are much less distracting
    471 now.
    472 
    473 [dark-jarvisjudiceninke] [light-jarvisjudiceninke] Jarvis’, Judice’s and
    474 Ninke’s dithering.
    475 
    476 Atkinson Dither
    477 
    478 Atkinson dithering was developed at Apple by Bill Atkinson and gained notoriety
    479 on on early Macintosh computers.
    480 
    481 18⋅(∗111111)\frac{1}{8} \cdot \left(\begin{array}{} & * & 1 & 1 \\ 1 & 1 & 1 &
    482 \\ & 1 & & \\ \end{array} \right) 81​⋅⎝⎛​1​∗11​11​1​⎠⎞​
    483 
    484 Diffusion matrix by Bill Atkinson.
    485 
    486 It’s worth noting that the Atkinson diffusion matrix contains six ones, but is
    487 normalized using 18\frac{1}{8}81​, meaning it doesn’t diffuse the entire error
    488 to neighboring pixels, increasing the perceived contrast of the image.
    489 
    490 [dark-atkinson] [light-atkinson] Atkinson Dithering.
    491 
    492 Riemersma Dither
    493 
    494 To be completely honest, the Riemersma dither is something I stumbled upon by
    495 accident. I found an [24]in-depth article while I was researching the other
    496 dithering algorithms. It doesn’t seem to be widely known, but I really like the
    497 way it looks and the concept behind it. Instead of traversing the image
    498 row-by-row it traverses the image with a [25]Hilbert curve. Technically, any 
    499 [26]space-filling curve would do, but the Hilbert curve came recommended and is
    500 [27]rather easy to implement using generators. Through this it aims to take the
    501 best of both ordered dithering and error diffusion dithering: Limiting the
    502 number of pixels a single pixel can influence together with the organic look
    503 (and small memory footprint).
    504 
    505 [hilbertcurve] Visualization of the 256x256 Hilbert curve by making pixels
    506 brighter the later they are visited by the curve.
    507 
    508 The Hilbert curve has a “locality” property, meaning that the pixels that are
    509 close together on the curve are also close together in the picture. This way we
    510 don’t need to use an error diffusion matrix but rather a diffusion sequence of
    511 length nnn. To quantize the current pixel, the last nnn quantization errors are
    512 added to the current pixel with weights given in the diffusion sequence. In the
    513 article they use an exponential falloff for the weights — the previous pixel’s
    514 quantization error getting a weight of 1, the oldest quantization error in the
    515 list a small, chosen weight rrr. This results in the following formula for the 
    516 iiith weight:
    517 
    518 weight[i]=r−in−1\text{weight}[i] = r^{-\frac{i}{n-1}} weight[i]=r−n−1i​
    519 
    520 The article recommends r=116r = \frac{1}{16}r=161​ and a minimum list length of
    521 n=16n = 16n=16, but for my test image I found r=18r = \frac{1}{8}r=81​ and n=
    522 32n = 32n=32 to be better looking.
    523 
    524 [dark-riemersma] [light-riemersma]
    525 
    526 Riemersma dither with r=18r = \frac{1}{8}r=81​ and n=32n = 32n=32.
    527 
    528 The dithering looks extremely organic, almost as good as blue noise dithering.
    529 At the same time it is easier to implement than both of the previous ones. It
    530 is, however, still an error diffusion dithering algorithm, meaning it is
    531 sequential and not suitable to run on a GPU.
    532 
    533 💛 Blue noise, Bayer & Riemersma
    534 
    535 As a 3D game, Obra Dinn had to use ordered dithering to be able to run it as a
    536 shader. It uses both Bayer dithering and blue noise dithering which I also
    537 think are the most aesthetically pleasing choices. Bayer dithering shows a bit
    538 more structure while blue noise looks very natural and organic. I am also
    539 particularly fond of the Riemersma dither and I want to explore how it holds up
    540 when there are multiple colors in the palette.
    541 
    542 Obra Dinn uses blue noise dithering for most of the environment. People and
    543 other objects of interest are dithered using Bayer, which forms a nice visual
    544 contrast and makes them stand out without breaking the games overall aesthetic.
    545 Again, more on his reasoning as well his solution to handling camera movement
    546 in his [28]forum post.
    547 
    548 If you want to try different dithering algorithms on one of your own images,
    549 take a look at my [29]demo that I wrote to generate all the images in this blog
    550 post. Keep in mind that these are not the fastest. If you decide to throw your
    551 20 megapixel camera JPEG at this, it will take a while.
    552 
    553     Note: It seems I am hitting a de-opt in Safari. My blue noise generator
    554     takes ~30 second in Chrome, but takes >20 minutes Safari. It is
    555     considerably quicker in Safari Tech Preview.
    556 
    557 I am sure this super niche, but I enjoyed this rabbit hole. If you have any
    558 opinions or experiences with dithering, I’d love to hear them.
    559 
    560 Thanks & other sources
    561 
    562 Thanks to [30]Lucas Pope for his games and the visual inspiration.
    563 
    564 Thanks to [31]Christoph Peters for his excellent [32]article on blue noise
    565 generation.
    566 
    567 [33]← Back to home [surma]
    568 
    569 Surma
    570 
    571 DX at Shopify. Web Platform Advocate.
    572 Craving simplicity, finding it nowhere.
    573 “A bit of a ‘careless eager student’ archetype” according to HN.
    574 Internetrovert 🏳️‍🌈 He/him.
    575 
    576 [34] twitter [35] mastodon [36] github [37] instagram [38] keybase [39] podcast
    577 [40] rss [41]Licenses
    578 
    579 References:
    580 
    581 [1] https://surma.dev/
    582 [2] https://obradinn.com/
    583 [3] https://twitter.com/dukope
    584 [4] https://papersplea.se/
    585 [5] https://unity.com/
    586 [6] https://squoosh.app/
    587 [7] https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742
    588 [8] https://surma.dev/things/ditherpunk/dark-hires.jpg
    589 [9] https://surma.dev/things/ditherpunk/light-hires.jpg
    590 [10] https://surma.dev/lab/ditherpunk/lab
    591 [11] https://developer.mozilla.org/en-US/docs/Web/API/ImageData
    592 [12] https://surma.dev/lab/ditherpunk
    593 [13] https://surma.dev/lab/ditherpunk/lab
    594 [14] https://en.wikipedia.org/wiki/SRGB
    595 [15] https://en.wikipedia.org/wiki/Bayer_filter
    596 [16] https://en.wikipedia.org/wiki/Demosaicing
    597 [17] https://en.wikipedia.org/wiki/Ordered_dithering#Pre-calculated_threshold_maps
    598 [18] http://ulichney.com/
    599 [19] https://surma.dev/things/ditherpunk/bluenoise-1993.pdf
    600 [20] https://en.wikipedia.org/wiki/Gaussian_blur
    601 [21] https://surma.dev/things/ditherpunk/bluenoise-1993.pdf
    602 [22] https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms
    603 [23] https://twitter.com/DasSurma/status/1341203941904834561
    604 [24] https://www.compuphase.com/riemer.htm
    605 [25] https://en.wikipedia.org/wiki/Hilbert_curve
    606 [26] https://en.wikipedia.org/wiki/Space-filling_curve
    607 [27] https://twitter.com/DasSurma/status/1343569629369786368
    608 [28] https://forums.tigsource.com/index.php?topic=40832.msg1363742#msg1363742
    609 [29] https://surma.dev/lab/ditherpunk/lab
    610 [30] https://twitter.com/dukope
    611 [31] https://twitter.com/momentsincg
    612 [32] http://momentsingraphics.de/BlueNoise.html
    613 [33] https://surma.dev/
    614 [34] https://twitter.com/dassurma
    615 [35] https://mastodon.social/@surma
    616 [36] https://github.com/surma
    617 [37] https://instagram.com/dassurma
    618 [38] https://keybase.io/surma
    619 [39] https://http203.libsyn.com/
    620 [40] https://surma.dev/index.xml
    621 [41] https://surma.dev/licenses/