dither

package module
v2.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 20, 2023 License: MPL-2.0 Imports: 7 Imported by: 24

README

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.

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)

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 that affects each pixel individually
  • 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.

Install

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.

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
// 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.

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. 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.

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

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.

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.

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

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.

Projects using dither

  • 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

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.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Atkinson = ErrorDiffusionMatrix{
	{0, 0, 1.0 / 8, 1.0 / 8},
	{1.0 / 8, 1.0 / 8, 1.0 / 8, 0},
	{0, 1.0 / 8, 0, 0},
}
View Source
var Burkes = ErrorDiffusionMatrix{
	{0, 0, 0, 8.0 / 32, 4.0 / 32},
	{2.0 / 32, 4.0 / 32, 8.0 / 32, 4.0 / 32, 2.0 / 32},
}
View Source
var ClusteredDot4x4 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{12, 5, 6, 13},
		{4, 0, 1, 7},
		{11, 3, 2, 8},
		{15, 10, 9, 14},
	},
	Max: 16,
}

ClusteredDot4x4 comes from http://caca.zoy.org/study/part2.html

It is not diagonal, so the dots form a grid.

View Source
var ClusteredDot6x6 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{34, 29, 17, 21, 30, 35},
		{28, 14, 9, 16, 20, 31},
		{13, 8, 4, 5, 15, 19},
		{12, 3, 0, 1, 10, 18},
		{27, 7, 2, 6, 23, 24},
		{33, 26, 11, 22, 25, 32},
	},
	Max: 36,
}

ClusteredDot6x6 comes from Figure 5.9 of the book Digital Halftoning by Robert Ulichney. It can represent "37 levels of gray". It is not diagonal.

View Source
var ClusteredDot6x6_2 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{34, 25, 21, 17, 29, 33},
		{30, 13, 9, 5, 12, 24},
		{18, 6, 1, 0, 8, 20},
		{22, 10, 2, 3, 4, 16},
		{26, 14, 7, 11, 15, 28},
		{35, 31, 19, 23, 27, 32},
	},
	Max: 36,
}

ClusteredDot6x6_2 comes from https://archive.is/71e9G. On the webpage it is called "central white point" while ClusteredDot6x6 is called "clustered dots".

It is nearly identical to ClusteredDot6x6.

View Source
var ClusteredDot6x6_3 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{30, 22, 16, 21, 33, 35},
		{24, 11, 7, 9, 26, 28},
		{13, 5, 0, 2, 14, 19},
		{15, 3, 1, 4, 12, 18},
		{27, 8, 6, 10, 25, 29},
		{32, 20, 17, 23, 31, 34},
	},
	Max: 36,
}

ClusteredDot6x6_3 comes from https://archive.is/71e9G. On the webpage it is called "balanced centered point".

It is nearly identical to ClusteredDot6x6.

View Source
var ClusteredDot8x8 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{3, 9, 17, 27, 25, 15, 7, 1},
		{11, 29, 38, 46, 44, 36, 23, 5},
		{19, 40, 52, 58, 56, 50, 34, 13},
		{31, 48, 60, 63, 62, 54, 42, 21},
		{30, 47, 59, 63, 61, 53, 41, 20},
		{18, 39, 51, 57, 55, 49, 33, 12},
		{10, 28, 37, 45, 43, 35, 22, 4},
		{2, 8, 16, 26, 24, 14, 6, 0},
	},
	Max: 64,
}

ClusteredDot8x8 comes from Figure 1.5 of the book Modern Digital Halftoning, Second Edition, by Daniel L. Lau and Gonzalo R. Arce. It is like ClusteredDotDiagonal8x8, but is not diagonal. It can represent "65 gray-levels".

View Source
var ClusteredDotDiagonal16x16 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{63, 58, 50, 40, 41, 51, 59, 60, 64, 69, 77, 87, 86, 76, 68, 67},
		{57, 33, 27, 18, 19, 28, 34, 52, 70, 94, 100, 109, 108, 99, 93, 75},
		{49, 26, 13, 11, 12, 15, 29, 44, 78, 101, 114, 116, 115, 112, 98, 83},
		{39, 17, 4, 3, 2, 9, 20, 42, 87, 110, 123, 124, 125, 118, 107, 85},
		{38, 16, 5, 0, 1, 10, 21, 43, 89, 111, 122, 127, 126, 117, 106, 84},
		{48, 25, 8, 6, 7, 14, 30, 45, 79, 102, 119, 121, 120, 113, 97, 82},
		{56, 32, 24, 23, 22, 31, 35, 53, 71, 95, 103, 104, 105, 96, 92, 74},
		{62, 55, 47, 37, 36, 46, 54, 61, 65, 72, 80, 90, 91, 81, 73, 66},
		{64, 69, 77, 87, 86, 76, 68, 67, 63, 58, 50, 40, 41, 51, 59, 60},
		{70, 94, 100, 109, 108, 99, 93, 75, 57, 33, 27, 18, 19, 28, 34, 52},
		{78, 101, 114, 116, 115, 112, 98, 83, 49, 26, 13, 11, 12, 15, 29, 44},
		{87, 110, 123, 124, 125, 118, 107, 85, 39, 17, 4, 3, 2, 9, 20, 42},
		{89, 111, 122, 127, 126, 117, 106, 84, 38, 16, 5, 0, 1, 10, 21, 43},
		{79, 102, 119, 121, 120, 113, 97, 82, 48, 25, 8, 6, 7, 14, 30, 45},
		{71, 95, 103, 104, 105, 96, 92, 74, 56, 32, 24, 23, 22, 31, 35, 53},
		{65, 72, 80, 90, 91, 81, 73, 66, 62, 55, 47, 37, 36, 46, 54, 61},
	},
	Max: 128,
}

ClusteredDotDiagonal16x16 comes from Figure 5.4 of the book Digital Halftoning by Robert Ulichney. In the book it's called "M = 8". It can represent "129 levels of gray". Its dimensions are 16x16, but as a diagonal matrix it is 17x17. It is called "Diagonal" because the resulting dot pattern is at a 45 degree angle.

View Source
var ClusteredDotDiagonal6x6 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{8, 6, 7, 9, 11, 10},
		{5, 0, 1, 12, 17, 16},
		{4, 3, 2, 13, 14, 15},
		{9, 11, 10, 8, 6, 8},
		{12, 17, 16, 5, 0, 1},
		{13, 14, 15, 4, 3, 2},
	},
	Max: 18,
}

ClusteredDotDiagonal6x6 comes from Figure 5.4 of the book Digital Halftoning by Robert Ulichney. In the book it's called "M = 3". It can represent "19 levels of gray". Its dimensions are 6x6, but as a diagonal matrix it is 7x7. It is called "Diagonal" because the resulting dot pattern is at a 45 degree angle.

View Source
var ClusteredDotDiagonal8x8 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{24, 10, 12, 26, 35, 47, 49, 37},
		{8, 0, 2, 14, 45, 59, 61, 51},
		{22, 6, 4, 16, 43, 57, 63, 53},
		{30, 20, 18, 28, 33, 41, 55, 39},
		{34, 46, 48, 36, 25, 11, 13, 27},
		{44, 58, 60, 50, 9, 1, 3, 15},
		{42, 56, 62, 52, 23, 7, 5, 17},
		{32, 40, 54, 38, 31, 21, 19, 29},
	},
	Max: 64,
}

ClusteredDotDiagonal8x8 comes from http://caca.zoy.org/study/part2.html

They say it "mimics the halftoning techniques used by newspapers". It is called "Diagonal" because the resulting dot pattern is at a 45 degree angle.

View Source
var ClusteredDotDiagonal8x8_2 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{13, 11, 12, 15, 18, 20, 19, 16},
		{4, 3, 2, 9, 27, 28, 29, 22},
		{5, 0, 1, 10, 26, 31, 30, 21},
		{8, 6, 7, 14, 23, 25, 24, 17},
		{18, 20, 19, 16, 13, 11, 12, 15},
		{27, 28, 29, 22, 4, 3, 2, 9},
		{26, 31, 30, 21, 5, 0, 1, 10},
		{23, 25, 24, 17, 8, 6, 7, 14},
	},
	Max: 32,
}

ClusteredDotDiagonal8x8_2 comes from Figure 5.4 of the book Digital Halftoning by Robert Ulichney. In the book it's called "M = 4". It can represent "33 levels of gray". Its dimensionsare 8x8, but as a diagonal matrix it is 9x9. It is called "Diagonal" because the resulting dot pattern is at a 45 degree angle.

It is almost identical to ClusteredDotDiagonal8x8, but worse because it can represent fewer gray levels. There is not much point in using it.

View Source
var ClusteredDotDiagonal8x8_3 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{13, 9, 5, 12, 18, 22, 26, 19},
		{6, 1, 0, 8, 25, 30, 31, 23},
		{10, 2, 3, 4, 21, 29, 28, 27},
		{14, 7, 11, 15, 17, 24, 20, 16},
		{18, 22, 26, 19, 13, 9, 5, 12},
		{25, 30, 31, 23, 6, 1, 0, 8},
		{21, 29, 28, 27, 10, 2, 3, 4},
		{17, 24, 20, 16, 14, 7, 11, 15},
	},
	Max: 32,
}

ClusteredDotDiagonal8x8_3 comes from https://archive.is/71e9G. On the webpage it is called "diagonal ordered matrix with balanced centered points".

It is almost identical to ClusteredDotDiagonal8x8, but worse because it can represent fewer gray levels. There is not much point in using it.

It is called "Diagonal" because the resulting dot pattern is at a 45 degree angle.

View Source
var ClusteredDotHorizontalLine = OrderedDitherMatrix{
	Matrix: [][]uint{

		{35, 33, 31, 30, 32, 34},
		{23, 21, 19, 18, 20, 22},
		{11, 9, 7, 6, 8, 10},
		{5, 3, 1, 0, 2, 4},
		{17, 15, 13, 12, 14, 16},
		{29, 27, 25, 24, 26, 28},
	},
	Max: 36,
}

ClusteredDotHorizontalLine comes from Figure 5.13 of the book Digital Halftoning by Robert Ulichney. It can represent "37 levels of gray". Its dimensions are 6x6.

It "clusters pixels about horizontal lines".

View Source
var ClusteredDotSpiral5x5 = OrderedDitherMatrix{
	Matrix: [][]uint{

		{20, 21, 22, 23, 24},
		{19, 6, 7, 8, 9},
		{18, 5, 0, 1, 10},
		{17, 4, 3, 2, 11},
		{16, 15, 14, 13, 12},
	},
	Max: 25,
}

ClusteredDotSpiral5x5 comes from Figure 5.13 of the book Digital Halftoning by Robert Ulichney. It can represent "26 levels of gray". Its dimensions are 5x5.

Instead of alternating dark and light dots like the other clustered-dot matrices, the dark parts grow to fill the area.

View Source
var ClusteredDotVerticalLine = OrderedDitherMatrix{
	Matrix: [][]uint{
		{35, 23, 11, 5, 17, 29},
		{33, 21, 9, 3, 15, 27},
		{31, 19, 7, 1, 13, 25},
		{30, 18, 6, 0, 12, 24},
		{32, 20, 8, 2, 14, 26},
		{34, 22, 10, 4, 16, 28},
	},
	Max: 36,
}

ClusteredDotVerticalLine is my rotated version of ClusteredDotHorizontalLine.

View Source
var FalseFloydSteinberg = ErrorDiffusionMatrix{
	{0, 3.0 / 8},
	{3.0 / 8, 2.0 / 8},
}
View Source
var FloydSteinberg = ErrorDiffusionMatrix{
	{0, 0, 7.0 / 16},
	{3.0 / 16, 5.0 / 16, 1.0 / 16},
}
View Source
var Horizontal3x5 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{9, 10, 11},
		{3, 4, 5},
		{0, 1, 2},
		{6, 7, 8},
		{12, 13, 14},
	},
	Max: 15,
}

Horizontal3x5 is my rotated version of Vertical5x3.

View Source
var JarvisJudiceNinke = ErrorDiffusionMatrix{
	{0, 0, 0, 7.0 / 48, 5.0 / 48},
	{3.0 / 48, 5.0 / 48, 7.0 / 48, 5.0 / 48, 3.0 / 48},
	{1.0 / 48, 3.0 / 48, 5.0 / 48, 3.0 / 48, 1.0 / 48},
}
View Source
var Sierra = ErrorDiffusionMatrix{
	{0, 0, 0, 5.0 / 32, 3.0 / 32},
	{2.0 / 32, 4.0 / 32, 5.0 / 32, 4.0 / 32, 2.0 / 32},
	{0, 2.0 / 32, 3.0 / 32, 2.0 / 32, 0},
}
View Source
var Sierra2 = TwoRowSierra

Sierra2 is another name for TwoRowSierra

View Source
var Sierra2_4A = SierraLite

Sierra2_4A (usually written as Sierra2-4A) is another name for SierraLite.

View Source
var Sierra3 = Sierra

Sierra3 is another name for the original Sierra matrix.

View Source
var SierraLite = ErrorDiffusionMatrix{
	{0, 0, 2.0 / 4},
	{1.0 / 4, 1.0 / 4, 0},
}
View Source
var Simple2D = ErrorDiffusionMatrix{
	{0, 0.5},
	{0.5, 0},
}
View Source
var StevenPigeon = ErrorDiffusionMatrix{
	{0, 0, 0, 2.0 / 14, 1.0 / 14},
	{0, 2.0 / 14, 2.0 / 14, 2.0 / 14, 0},
	{1.0 / 14, 0, 1.0 / 14, 0, 1.0 / 14},
}

StevenPigeon is an error diffusion matrix developed by Steven Pigeon. Source: https://hbfs.wordpress.com/2013/12/31/dithering/

View Source
var Stucki = ErrorDiffusionMatrix{
	{0, 0, 0, 8.0 / 42, 4.0 / 42},
	{2.0 / 42, 4.0 / 42, 8.0 / 42, 4.0 / 42, 2.0 / 42},
	{1.0 / 42, 2.0 / 42, 4.0 / 42, 2.0 / 42, 1.0 / 42},
}
View Source
var TwoRowSierra = ErrorDiffusionMatrix{
	{0, 0, 0, 4.0 / 16, 3.0 / 16},
	{1.0 / 16, 2.0 / 16, 3.0 / 16, 2.0 / 16, 1.0 / 16},
}
View Source
var Vertical5x3 = OrderedDitherMatrix{
	Matrix: [][]uint{
		{9, 3, 0, 6, 12},
		{10, 4, 1, 7, 13},
		{11, 5, 2, 8, 14},
	},
	Max: 15,
}

Vertical5x3 comes from http://caca.zoy.org/study/part2.html

They say it "creates artistic vertical line artifacts".

Functions

func RoundClamp

func RoundClamp(i float32) uint16

RoundClamp clamps the number and rounds it, rounding ties to the nearest even number. This should be used if you're writing your own PixelMapper.

Types

type Ditherer

type Ditherer struct {

	// Matrix is the ErrorDiffusionMatrix for dithering.
	Matrix ErrorDiffusionMatrix

	// Mapper is the ColorMapper function for dithering.
	Mapper PixelMapper

	// Special is the special dithering algorithm that's being used. The default
	// value of 0 indicates that no special dithering algorithm is being used.
	Special SpecialDither

	// SingleThreaded controls whether the dithering happens sequentially or using
	// runtime.GOMAXPROCS(0) workers, which defaults to the number of CPUs.
	//
	// Note that error diffusion dithering (using Matrix) is sequential by nature
	// and so this field has no effect.
	//
	// Setting this to true is only useful in rare cases, like when numbers are
	// used sequentially in a PixelMapper, and the output must be deterministic.
	// Because otherwise the numbers will be retrieved in a different order each
	// time, as the goroutines call on the PixelMapper.
	SingleThreaded bool

	// Serpentine controls whether the error diffusion matrix is applied in a
	// serpentine manner, meaning that it goes right-to-left every other line.
	// This greatly reduces line-type artifacts. If a Mapper is being used this
	// field will have no effect.
	Serpentine bool
	// contains filtered or unexported fields
}

Ditherer dithers images according to the settings in the struct. It can be safely reused for many images, and used concurrently.

Some members of the struct are public. Those members can be changed in-between dithering images, if you would like to dither again. If you change those public methods while an image is being dithered, the output image will have problems, so only change in-between dithering.

You can only set one of Matrix, Mapper, or Special. Trying to dither when none or more than one of those are set will cause the function to panic.

All methods can handle images with transparency, unless otherwise specified. Read the docs before using!

func NewDitherer

func NewDitherer(palette []color.Color) *Ditherer

NewDitherer creates a new Ditherer that uses a copy of the provided palette. If the palette is empty or nil then nil will be returned. All palette colors should be opaque.

func (*Ditherer) Dither

func (d *Ditherer) Dither(src image.Image) image.Image

Dither dithers the provided image.

It will always try to change the provided image and return it, but if that is not possible it will return the dithered image as a copy.

In comparison to DitherCopy, this can greatly reduce memory usage, and is quicker because it usually won't copy the image at the beginning. It should be preferred if you don't need to keep the original image.

Cases where a copy will be are limited to: If the input image is *image.Paletted and the image's palette is different than the Ditherer's, or if the image can't be casted to draw.Image.

The returned image type when copied is *image.RGBA. But it may be different if the image wasn't copied.

func (*Ditherer) DitherConfig

func (d *Ditherer) DitherConfig(src draw.Image) (image.Image, image.Config)

DitherConfig is like Dither, but returns an image.Config as well.

func (*Ditherer) DitherCopy

func (d *Ditherer) DitherCopy(src image.Image) *image.RGBA

DitherCopy dithers a copy of the src image and returns it. The src image remains unchanged. If you don't need to keep the original image, use Dither.

func (*Ditherer) DitherCopyConfig

func (d *Ditherer) DitherCopyConfig(src image.Image) (*image.RGBA, image.Config)

DitherCopyConfig is like DitherCopy, but returns an image.Config as well.

func (*Ditherer) DitherPaletted

func (d *Ditherer) DitherPaletted(src image.Image) *image.Paletted

DitherPaletted dithers a copy of the src image and returns it as an *image.Paletted. The src image remains unchanged. If you don't need an *image.Paletted, using Dither or DitherCopy should be preferred.

The palette of the returned image is the same palette the ditherer uses internally -- it will be equal to the output of GetPalette().

If the Ditherer's palette has over 256 colors then the function will panic, because *image.Paletted does not allow for that.

DitherPaletted can't handle images with transparency.

func (*Ditherer) DitherPalettedConfig

func (d *Ditherer) DitherPalettedConfig(src image.Image) (*image.Paletted, image.Config)

DitherPalettedConfig is like DitherPaletted, but returns an image.Config as well.

DitherPalettedConfig can't handle images with transparency.

func (*Ditherer) Draw

func (d *Ditherer) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point)

Draw implements draw.Drawer. This means you can use a Ditherer in many places, such as for encoding GIFs.

Draw ignores whether dst has a palette or not, and just uses the internal Ditherer palette. If the dst image passed has a palette (i.e. is of the type *image.Paletted), and the palette is the not the same as the Ditherer's palette, it will panic.

func (*Ditherer) GetColorModel

func (d *Ditherer) GetColorModel() color.Model

GetColorModel returns a copy of the Ditherer's palette as a color.Model that finds the closest color using Euclidean distance in sRGB space.

func (*Ditherer) GetPalette

func (d *Ditherer) GetPalette() []color.Color

GetPalette returns a copy of the current palette being used by the Ditherer.

func (*Ditherer) Quantize

func (d *Ditherer) Quantize(p color.Palette, m image.Image) color.Palette

Quantize implements draw.Quantizer. It ignores the provided image and just returns the Ditherer's palette each time. This is useful for places that only allow you to set the palette through a draw.Quantizer, like the image/gif package.

This function will panic if the Ditherer's palette has more colors than the caller wants, which the caller indicates by cap(p).

It will also panic if there's already colors in the color.Palette provided to the func and not all of those colors are included in the Ditherer's palette. This is because the caller is indicating that certain colors must be in the palette, but the user who created the Ditherer does not want those colors.

type ErrorDiffusionMatrix

type ErrorDiffusionMatrix [][]float32

ErrorDiffusionMatrix holds the matrix for the error-diffusion type of dithering. An example of this would be Floyd-Steinberg or Atkinson.

Zero values can be used to represent pixels that have already been processed. The current pixel is assumed to be the right-most zero value in the top row.

func ErrorDiffusionStrength

func ErrorDiffusionStrength(edm ErrorDiffusionMatrix, strength float32) ErrorDiffusionMatrix

ErrorDiffusionStrength modifies an existing error diffusion matrix so that it will be applied with the specified strength.

strength is usually a value from 0 to 1.0, where 1.0 means 100% strength, and will not modify the matrix at all. It is inversely proportional to contrast - reducing the strength increases the contrast. It can be useful at values like 0.8 for reducing noise in the dithered image.

See the documentation for Bayer for more details.

func (ErrorDiffusionMatrix) CurrentPixel

func (e ErrorDiffusionMatrix) CurrentPixel() int

CurrentPixel returns the index the current pixel. The current pixel is assumed to be the right-most zero value in the top row. In all matrixes that I have seen, the current pixel is always in the middle, but this function exists just in case.

Therefore with an ErrorDiffusionMatrix named edm, the current pixel is at:

edm[0][edm.CurrentPixel()]

Usually you'll want to cache this value.

func (ErrorDiffusionMatrix) Offset

func (e ErrorDiffusionMatrix) Offset(x, y, curPx int) (int, int)

Offset will take the index of where you are in the matrix and return the offset from the current pixel. You have to pass the curPx value yourself to allow for caching, but it can be retrieved by calling CurrentPixel().

type OrderedDitherMatrix

type OrderedDitherMatrix struct {
	Matrix [][]uint `json:"matrix"`
	Max    uint     `json:"max"`
}

OrderedDitherMatrix is used to hold a matrix used for ordered dithering. This is useful if you find a matrix somewhere and would like to try it out. You can create this struct and then give it to PixelMapperFromMatrix.

The matrix must be rectangular - each slice inside the first one must be the same length.

Max is the value all the matrix values will be divided by. Usually this is the product of the dimensions of the matrix (x*y), or the greatest value in the matrix plus one. For diagonal matrices or other matrices with repeated values, it is the latter.

Leaving Max as 0 will cause a panic.

Matrix values should almost always range from 0 to Max-1. If the matrix you found ranges from 1 to Max, just subtract 1 from every value when programming it.

type PixelMapper

type PixelMapper func(x, y int, r, g, b uint16) (uint16, uint16, uint16)

PixelMapper is a function that takes the coordinate and color of a pixel, and returns a new color. That new color does not need to be part of any palette.

This is used for thresholding, random dithering, patterning, and ordered dithering - basically any dithering that can be applied to each pixel individually.

The provided RGB values are in the linear RGB space, and the returned values must be as well. All dithering operations should be happening in this space anyway, so this is done as a convenience. The RGB values are in the range [0, 65535], and must be returned in the same range.

It must be thread-safe, as it will be called concurrently.

func Bayer

func Bayer(x, y uint, strength float32) PixelMapper

Bayer returns a PixelMapper that applies a Bayer matrix with the specified size. Please read this entire documentation, and see my recommendations at the end, especially if you're dithering color images.

First off, cache the result of this function. It's not trivial to generate, and it can be re-used or used concurrently with no issues.

The provided dimensions of the bayer matrix can only be powers of 2, but they do not need to be the same. If they are not powers of two this function will panic.

There are currently two exceptions to this, which come from hand-derived Bayer matrices by Joel Yliluoma: 5x3, 3x5, 3x3. As he notes, "they can have a visibly uneven look, and thus are rarely worth using".

Source:

https://bisqwit.iki.fi/story/howto/dither/jy/#Appendix%202ThresholdMatrix

strength should be in the range [-1, 1]. It is multiplied with 65535 (the max color value), which is then multiplied with the matrix.

You can use this to change the amount the matrix is applied to the image, the "strength" of the dithering matrix. Usually just keeping it at 1.0 is fine.

The closer to zero stength is, the smaller the range of colors that will be dithered. Colors outside that range will just be quantized, and not have a Bayer matrix applied. To dither the entire color range, set it to 1.0.

Why would you want to shrink the dither range? Well Bayer matrixes are fundamentally biased to making the image brighter, increasing the value in each channel. This means that there might be darker parts that would be better off just quantized to the darkest color in your palette, instead of made lighter and dithered. By shrinking the dither range, you dither the colors that are more in the "middle", and let the darker and lighter ones just get quantized.

You might also want to reduce the strength to reduce noise in the image, as dithering doesn't produce smooth colored areas. Usually a value around 0.8 is good for this.

You can also make strength negative. If you know already that your image is dark, and so you don't want it to be made bright, then this is a better approach then shrinking the dither range. A negative strength flips the bias of the Bayer matrix, making it biased towards making images darker. To dither the entire color range but inverted, set strength to -1.0.

The closer to zero you get, the more similar the effect of the negative and positive strength become. This is because they are shrinking the dither range towards the same spot.

At Bayer sizes above 4x4, the brightness bias mostly disappears, and the difference between strength being -1.0 vs 1.0 is not really noticeable. Decreasing it below 1.0 or or above -1.0 will still shrink the dithering range, but instead of fixing some bias, it will just increase the contrast of the image.

Greater than 1 or less than -1 doesn't really make sense, so stay away from that range. It expands the range of the dithering outside the possible color range, so there won't be enough dithering patterns in the output image. The further from zero, the larger the range.

Going away from zero is similar to reducing contrast. If you go too far from zero, the whole image becomes gray.

RECOMMENDATIONS

For grayscale output, I would recommend 1.0 for lighter images, or -1.0 for darker images. If you cannot know beforehand, you may want to decrease that value, to reduce the risk of making dark images really bright. Try staying between 0.5 and 1.0.

If you're using a Bayer size larger than 4x4, just using 1.0 for strength should be fine for most kinds of grayscale images.

Color images are different. The Bayer matrix's bias to brightness applies to each RGB channel, and so the color of the image can become quite distorted at 1.0 strength. Several sites I have seen recommend 0.64 strength (written as 256/4), and from my own testing this is often a good value for color images. Do not default to 1.0 for Bayer dithering of color images.

Of course, experiment for yourself. And let me know if I'm wrong!

func PixelMapperFromMatrix

func PixelMapperFromMatrix(odm OrderedDitherMatrix, strength float32) PixelMapper

PixelMapperFromMatrix takes an OrderedDitherMatrix, and will return a PixelMapper. This is a simple way to make use of the clustered-dot matrices in this library, or to try out some matrix you found online.

Because a PixelMapper is returned, this can make the matrix usable in more situations than originally designed, like with color images and multi-color palettes.

See Bayer for a detailed explanation of strength. You can use this to change the amount the matrix is applied to the image, and to reduce noise. Usually you'll just want to set it to 1.0.

func RandomNoiseGrayscale

func RandomNoiseGrayscale(min, max float32) PixelMapper

RandomNoiseGrayscale returns a PixelMapper that adds random noise to the color before returning. This is the simplest form of dithering.

Non-grayscale colors will be converted to grayscale before the noise is added.

You must call rand.Seed before calling using the PixelMapper, otherwise the output will be the same each time. A simple way to initialize rand.Seed is:

rand.Seed(time.Now().UnixNano())

The noise added to each channel will be randomly chosen from within the range of min (inclusive) and max (exclusive). To simplify things, you can consider valid color values to range from 0 to 1. This means if you wanted the noise to shift the color through 50% of the color space at most, the min and max would be -0.5 and 0.5.

Statistically, -0.5 and 0.5 are the best values for random dithering, as they evenly dither colors. Using values closer to zero (like -0.2 and 0.2) will effectively reduce the contrast of the image, and values further from zero (like -0.7 and 0.7) will increase the contrast.

Making the min and max different values, like using -0.2 and 0.5 will make the image brighter or darker. In that example, the image will become brighter, as the randomness is more likely to land on the positive side and increase the color value.

If the noise puts the channel value too high or too low it will be clamped, not wrapped. Basically, don't worry about the values of your min and max distorting the image in an unexpected way.

func RandomNoiseRGB

func RandomNoiseRGB(minR, maxR, minG, maxG, minB, maxB float32) PixelMapper

RandomNoiseRGB is like RandomNoiseGrayscale but it adds randomness in the R, G, and B channels. It should not be used when you want a grayscale output image, ie when your palette is grayscale.

Most of the time you will want all the mins to be the same, and all the maxes to be the same.

See RandomNoiseGrayscale for more details about values and how this function works.

type SpecialDither

type SpecialDither int

SpecialDither is used to represent dithering algorithms that require custom code, because they cannot be represented by a PixelMapper or error diffusion matrix.

There are currently no SpecialDither options, but they will be added in the future.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL