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 situations.
This library is uniquely correct from a math and quality perspective. It linearizes the image, and color comparisons are done with human luminance perception in mind (channel weighting). Few-to-no other libraries do this.
It supports images that make use of the alpha channel, AKA transparency.
Make sure to set your browser zoom to 100% to view these images properly
Original | Dithering Algorithm |
---|---|
Floyd-Steinberg (black and white palette) | |
Floyd-Steinberg (red, green, yellow, black) | |
- 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 that affects each pixel individually
- Using
- Error diffusion dithering
- Simple 2D
- Floyd-Steinberg, False Floyd-Steinberg
- Jarvis-Judice-Ninke
- Atkinson
- Stucki
- Burkes
- Sierra/Sierra3, Sierra2, Sierra2-4A/Sierra-Lite
- Steven Pigeon
- 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.
In your project, run
go get github.com/makeworld-the-better-one/dither/v2@latest
You can import it as "github.com/makeworld-the-better-one/dither/v2"
and use it as dither
.
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
// If it can't then a dithered copy will be returned.
img = d.Dither(img)
// 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.
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.
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.
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. But sometimes you want to scale the image up after dithering, to make the dithering effect more obvious for aesthetic purposes.
So 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 still produce moiré patterns, unless you're scaling by a multiple of the original dimensions. So when scaling up, you should be scaling by 2x or 3x, rather than a non-integer like 1.34x.
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.
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
Playing with the strength of the matrix might also be useful. The example above is at full strength, but sometimes that's too noisy. The code for 80% strength looks like this:
d.Matrix = dither.ErrorDiffusionStrength(dither.FloydSteinberg, 0.8)
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.
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.
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.
If the palette is grayscale, the input image should be converted to grayscale first to get accurate results.
All the [][]uint
matrices are supposed to be applied with PixelMapperFromMatrix
.
Images with transparency are only supported in v2.2.0 and after.
This library does not dither in the alpha channel or support transparent palettes. Instead it just keeps track of the alpha channel, and the dithered image returned will always have the exact same alpha values for each pixel. This allows for dithering of images with transparent parts.
Dithering images with semi-transparent pixels will also work, but is not as useful, because the output image will appear to have colors that are not in the palette, due to whatever background image you use.
- didder - a powerful CLI dithering tool
- wasm-palette-converter - dithering images to a color palette, in the browser
- 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 general-purpose dithering using WASM
- A GUI desktop application
image/draw
has adraw.Drawer
calledFloydSteinberg
- https://github.com/brouxco/dithering
- https://github.com/esimov/dithergo
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.
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.