A fast, correct image dithering library in Go.

dither

go reportcard Go Reference

dither is a library for dithering images in Go. It has many dithering algorithms built-in, and allows you to specify your own. Correctness is a top priority, as well as performance. It is designed to work well on its own, but also implements interfaces from the standard library, so that it can be integrated easily in a wide variety of situtations.

It does not support images that make use of the alpha channel, AKA transparency.

Original Dithering Algorithm
Floyd-Steinberg (black and white palette)
Floyd-Steinberg (red, green, yellow, black)

Types of dithering supported

  • Random noise (in grayscale and RGB)
  • Ordered Dithering
    • Bayer matrix of any size (as long as dimensions are powers of two)
    • Clustered-dot - many different preprogrammed matrices
    • Some unusual horizontal or vertical line matrices
    • Yours?
      • Using PixelMapperFromMatrix, this library can dither using any matrix
      • If you need more freedom, PixelMapper, can be used to implement any method of dithering affects each pixel individually
  • Error diffusion dithering
    • Simple 2D
    • Floyd-Steinberg
    • Jarvis-Judice-Ninke
    • Atkinson
    • Stucki
    • Burkes
    • Sierra/Sierra3, Sierra2, Sierra2-4A/Sierra-Lite
    • Yours? Custom error diffusion matrices can be used by the library.

More methods of dithering are being worked on, such as Riemersma, Yuliluoma, and blue noise.

Install

In your project, run

go get github.com/makeworld-the-better-one/dither/v2@latest
go mod tidy

You can import it as "github.com/makeworld-the-better-one/dither/v2" and use it as dither.

Usage

Here's a simple example using Floyd-Steinberg dithering.

img := // Get image.Image from somewhere

// These are the colors we want in our output image
palette := []color.Color{
    color.Black,
    color.White,
    // You can put any colors you want
}

// Create ditherer
d := dither.NewDitherer(palette)
d.Matrix = dither.FloydSteinberg

// Dither the image, attempting to modify the existing image
// It returns nil if the image was modified, otherwise it returns a dithered copy
if tmp := d.Dither(img); tmp != nil {
    img = tmp
}

// Now use img - save it as PNG, display it on the screen, etc

If you always want to dither a copy of the image, you can use DitherCopy instead.

Here's how you create a Ditherer that does Bayer dithering. Note how d.Mapper is used instead of d.Matrix.

d := dither.NewDitherer(palette)
d.Mapper = dither.Bayer(8, 8, 1.0) // 8x8 Bayer matrix at 100% strength

Here's how you create a Ditherer that does clustered-dot dithering - dithering with a predefined matrix.

d := dither.NewDitherer(palette)
d.Mapper = dither.PixelMapperFromMatrix(dither.ClusteredDotDiagonal8x8)

See the docs for more.

More Examples

Sometimes you can't dither using the above code. These examples show how you can use this library in those situations.

If you're interested in what specific algorithms look like, you can check out the tests output folder.

Performance

Operations that only affect each pixel individually are parallelized, using runtime.GOMAXPROCS(0) which defaults to the number of CPUs. This applies to any PixelMapper (aka Ditherer.Mapper) but not to an ErrorDiffusionMatrix (aka Ditherer.Matrix), as the latter is inherently sequential.

Scaling images

A dithered output image will only look right at 100% size. As you scale down, the image will immediately get darker, and strange grid-like artifacts will appear, known as a moiré pattern. This is due to how dithered images work, and is not something this library can fix.

The best thing to do is to scale the input image to the exact size you want before using this library.

As for scaling the dithered output image up (above 100%), that will only look fine if you use nearest-neighbor scaling - the kind of scaling that produces pixelated results. Otherwise the dither pixel values will be blurred and averaged, which will mess things up. And even once you're using that, it will only not darken the image after it's been scaled to 200% or above. In between 100% and 200% will still alter the image brightness.

Encoding output

Dithered images require that their pixel values be stored exactly. This means they must be encoded to a lossless format. PNG is almost always the best choice, as it is widely supported and takes up the least space. GIF is also acceptable, as long as the palette is 256 colors or less. The GIF format is also useful if you are dithering an animation. APNG is more efficient for animation, but has no Go stdlib support, and less support in non-browser environments.

The WebP format also works for both static images and animation, but it must be a lossless WebP, not a lossy one.

What method should I use?

Generally, using Floyd-Steinberg serpentine dithering will produce the best results. The code would be:

d := dither.NewDitherer(yourPalette)
d.Matrix = dither.FloydSteinberg
d.Serpentine = true

The main reason for using any other dithering algorithm would be

  • Aesthetics - dithering can be a cool image effect, and different methods will look different
  • Speed - error diffusion dithering is sequential and therefore single-threaded. But ordered dithering, like using Bayer, will use all available CPUs, which is much faster.

How do I get the palette?

Sometimes the palette isn't an option, as it might determined by the hardware. Many e-ink screens can only display black and white for example, and so your palette is chosen for you.

But in most cases you have all the colors available, and so you have to pick the ones that represent your image best. This is called color quantization.

I might end up writing another library that implements some common algorithms for this, like median cut. But there are some libraries that exist already. joshdk/quantize looks like the best one, although there is also this one.

Tips

Some general tips for working with the library.

Any returned PixelMappers should be cached and re-used. There is no point in regenerating them, it just wastes resources.

Any PixelMappers that mention "grayscale" or have "grayscale" in their name are completely safe to use with color input images, and even color palettes. It's just that they always return grayscale colors. If you want grayscale output, you should definitely be using those functions over the color ones.

All the [][]uint matrices are supposed to be applied with PixelMapperFromMatrix.

Projects using dither

  • I'm working on a CLI tool - for experimentation, automated dithering, and software pipelines
  • Your project? Build something fun, show how cool dithering can be! Some ideas / things I'd love to see:
    • A client-side web app for dithering using WASM
    • A GUI desktop application

Similar libraries

The largest problem with all of these libraries is that they don't linearize the image colors before dithering, which produces incorrect results. They also only support error diffusion dithering.

License

This library is under the Mozilla Public License 2.0. Similar to the LGPL, this means you can use this library in your project, even if it's proprietary. But any changes you make to the library's code must be released publicly. Crucially, this license allows for statically linking this library.

See LICENSE for details, and my blog post on why you should use the MPL over the LGPL for Go code.

Comments
  • Palette order affects output

    Palette order affects output

    This issue was originally made for the didder repo, so didder commands are referenced.


    This is a strange and important bug.

    These are the two commands:

    didder -i input.png -o test.png  -p 'black red white' edm FloydSteinberg
    didder -i input.png -o test2.png -p 'black white red' edm FloydSteinberg
    

    input.png is this image:

    table

    Here are the respective outputs:

    test test2

    Obviously, they should be exactly the same. This only occurs with edm and does not depend on the matrix used. This does not seem to occur with the upstream dither library, indicating the problem is with the didder code.

  • Support images with alpha channel

    Support images with alpha channel

    Thanks to @/Shrinks99

    This will not do dithering in the alpha channel, but it will properly handle images with non-opaque pixels.

    • Leave fully transparent pixels as they are
    • Partially transparent pixels (example)
      • I have a premult color, a=0.5
      • I take the RGB and dither it, and end up with a palette color (with implicit a=1)
      • I multiply the palette color by 0.5
      • I store the multiplied color with a=0.5

    DitherPaletted wouldn't support this, as this would require breaking the guarantee that the returned image has the same palette as the Ditherer.

  • Update Bayer recommendations

    Update Bayer recommendations

    From the Bayer docs:

    As for color images, after my own experimentation, I've determined that everything I said above about grayscale images still applies. Stick to 1.0 and Bayer sizes above 4x4 if you can, and changing the strength still changes contrast as described above.

    This is false.

    For example, here is a color image:

    KodakTestImage

    Here is 16x16 Bayer with an 8-bit sRGB grayscale palette of 0, 156, 213, 255. 100% strength.

    100

    The second image is obviously being made lighter despite being above 4x4 in the recommendations.

  • Burkes dithering matrix might be wrong

    Burkes dithering matrix might be wrong

  • Support strength value for error diffusion

    Support strength value for error diffusion

    As demonstrated here, it's possible to have a strength value for error diffusion too. This can be added as a struct field in Ditherer, where the default value of 0 indicates 100% strength. This will not break compatibility.

  • Pigeon dithering

    Pigeon dithering

    It's odd this type of dithering is referred to using a full name of its creator when all other dithering types are referred to only by the surname of the creator.

    How about just Pigeon?

    This is mentioned/proposed in the comments of the page that you link to.

  • Dithering with a grayscale palette has

    Dithering with a grayscale palette has "unexpected" results with color image input

    "unexpected" is in quotes because the math is sound, but the result is almost never what you want. This is a property of dithering, and is not related to the library code.

    The bad output can be very severe with error diffusion dithering, but ordered dithering still can have issues too.

    The solution is to always make an image grayscale before dithering with a grayscale palette. The library should have a convenience function for this, as well as explain it in the README. Should the library detect grayscale palettes and make the image grayscale automatically when dithering? Probably not, the user should still have the option to.

    Example

    Original image:

    KodakTestImage

    Original image but made to black and white:

    KodakTestImage-gray

    Floyd-Steinberg dithering the color image with an 8-bit sRGB grayscale palette of 0, 156, 213, 255. 100% strength:

    fss

    And the same, but dithering the black and white image:

    fss-gray-before

  • Clustered-dot dithering matrix generation

    Clustered-dot dithering matrix generation

    Hi, I saw the post in your blog re dithering, where you asked if one know how to do clustered dithering to contact you. Dithering you mentioned is usally called halftone dithering and have long history in silk printing and offset printing. You can find many resources on it on the net, but what you have to do is sample your image withe some frequency in 256 levels of gray, and then replace each sample by 16x16 matrix of pixels representing 256 levels of gray. It can be also be done with lines instead of dots resulting in line raster, where you vary width of the line with intensity. It is also not bad idea to do bluring and unsharp masking before sampling which is equivalent of applying lowpas 2d filter before sampling as in Niquist theorem where you first filter the signal before sampling to avoid artifacts. If you want to do quick experiment, just replace each pixel with 8x8 pixels representing black dots on white background for levels below 50% and white dots on black background for leves above 50%. If you need any help feel free to contact me at robert.aleksic at gmail.com. cheers robby

    p.s. if you want to do it in color, you separate your image to components like CMYK for ofset printing and then apply all four images one on on top of the other, with varying angles of raster to obtaine nice rosette like here: https://cdn2.hubspot.net/hubfs/2296165/Imported_Blog_Media/CMKdot-1.jpg. Black component is created to avoid usage of to much ink and preserve gray balance, but that is another topic - color calibration.

Related tags
Image - This repository holds supplementary Go image librariesThis repository holds supplementary Go image libraries

Go Images This repository holds supplementary Go image libraries. Download/Insta

Jan 5, 2022
darkroom - An image proxy with changeable storage backends and image processing engines with focus on speed and resiliency.
darkroom - An image proxy with changeable storage backends and image processing engines with focus on speed and resiliency.

Darkroom - Yet Another Image Proxy Introduction Darkroom combines the storage backend and the image processor and acts as an Image Proxy on your image

Dec 6, 2022
Easily customizable Social image (or Open graph image) generator

fancycard Easily customizable Social image (or Open graph image) generator Built with Go, Gin, GoQuery and Chromedp Build & Run Simply, Clone this rep

Jan 14, 2022
An API which allows you to upload an image and responds with the same image, stripped of EXIF data

strip-metadata This is an API which allows you to upload an image and responds with the same image, stripped of EXIF data. How to run You need to have

Nov 25, 2021
Imgpreview - Tiny image previews for HTML while the original image is loading
Imgpreview - Tiny image previews for HTML while the original image is loading

imgpreview This is a Go program that generates tiny blurry previews for images t

May 22, 2022
Go package for fast high-level image processing powered by libvips C library

bimg Small Go package for fast high-level image processing using libvips via C bindings, providing a simple programmatic API. bimg was designed to be

Jan 2, 2023
A lightning fast image processing and resizing library for Go

govips A lightning fast image processing and resizing library for Go This package wraps the core functionality of libvips image processing library by

Jan 8, 2023
Fast, simple, scalable, Docker-ready HTTP microservice for high-level image processing

imaginary Fast HTTP microservice written in Go for high-level image processing backed by bimg and libvips. imaginary can be used as private or public

Jan 3, 2023
Fast Image Convolutions (Gaussian) Blur

Usage package main import ( "image" "image/jpeg" "os" "github.com/0xc0d/ficblur" ) func main() { imageFile, err := os.Open("img.jpeg") panicN

Feb 5, 2022
Image processing library and rendering toolkit for Go.

blend Image processing library and rendering toolkit for Go. (WIP) Installation: This library is compatible with Go1. go get github.com/phrozen/blend

Nov 11, 2022
go library for image programming (merge, crop, resize, watermark, animate, ease, transit)
go library for image programming (merge, crop, resize, watermark, animate, ease, transit)

Result Terminal Code mergi -t TT -i https://raw.githubusercontent.com/ashleymcnamara/gophers/master/Facepalm_Gopher.png -r "131 131" -i https://raw.gi

Jan 6, 2023
Content aware image resize library
Content aware image resize library

Caire is a content aware image resize library based on Seam Carving for Content-Aware Image Resizing paper. How does it work An energy map (edge detec

Jan 2, 2023
ColorX is a library to determine the most prominent color in an image written in golang

ColorX is a library to determine the most prominent color in an image. ColorX doesn't use any sort of complex algorithms to calculate the prominent color, it simply loops over the image pixels and returns the color that occurs the most.

Nov 11, 2021
A library for basic image processing in Go.
A library for basic image processing in Go.

Imaging Package imaging provides basic image processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.). All the image process

Nov 26, 2021
A library for basic image processing in Go.
A library for basic image processing in Go.

Imaging Package imaging provides basic image processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.). All the image process

Nov 26, 2021
A Go-language library for the automatic generation of image collages.

CollageCreator is a Go-language library for the automatic generation of image collages.

Jan 29, 2022
Image processing algorithms in pure Go
Image processing algorithms in pure Go

bild A collection of parallel image processing algorithms in pure Go. The aim of this project is simplicity in use and development over absolute high

Jan 6, 2023
Decode embedded EXIF meta data from image files.

goexif Provides decoding of basic exif and tiff encoded data. Still in alpha - no guarantees. Suggestions and pull requests are welcome. Functionality

Dec 17, 2022
Imaging is a simple image processing package for Go
Imaging is a simple image processing package for Go

Imaging Package imaging provides basic image processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.). All the image process

Dec 30, 2022