diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e55a82..bbbdada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.0.0] - 2021-02-13 ### Added - Added `ErrorDiffusionStrength` to set the strength of error diffusion dithering (#4) +- `RoundClamp` function for making your own `PixelMappers` that round correctly + +### Changed +- All linear RGB values are represented using `uint16` instead of `uint8` now, because 8-bits is not enough to accurately hold a linearized value. This is a breaking change, hence the new major version. + +### Fixed +- Rounding is no longer biased, because ties are rounded to the nearest even number ## [1.0.0] - 2021-02-11 diff --git a/README.md b/README.md index 73f7843..13e78fc 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,12 @@ More methods of dithering are being worked on, such as Riemersma, Yuliluoma, and In your project, run ``` -go get github.com/makeworld-the-better-one/dither@latest +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. @@ -153,8 +155,6 @@ you should definitely be using those functions over the color ones. All the `[][]uint` matrices are supposed to be applied with `PixelMapperFromMatrix`. -If you ever find yourself multiplying a number by `255.0` (for example to scale a float in the range [0, 1]), make sure to add 0.5 before returning. Only by doing this can you get a correct answer in all cases. For example, `255.0 * n` should become `255.0 * n + 0.5`. - ## Projects using `dither` diff --git a/color_spaces.go b/color_spaces.go index 594cbb6..3594590 100644 --- a/color_spaces.go +++ b/color_spaces.go @@ -7,43 +7,43 @@ import ( // linearize1 linearizes an R, G, or B channel value from an sRGB color. // Must be in the range [0, 1]. -func linearize1(v float32) float32 { +func linearize1(v float64) float64 { if v <= 0.04045 { return v / 12.92 } - return float32(math.Pow((float64(v)+0.055)/1.055, 2.4)) + return math.Pow((v+0.055)/1.055, 2.4) } -func linearize65535to255(i uint16) uint8 { - v := float32(i) / 65535.0 - return uint8(linearize1(v)*255.0 + 0.5) +func linearize65535(i uint16) uint16 { + v := float64(i) / 65535.0 + return uint16(math.RoundToEven(linearize1(v) * 65535.0)) } -func linearize255(i uint8) uint8 { - v := float32(i) / 255.0 - return uint8(linearize1(v)*255.0 + 0.5) +func linearize255to65535(i uint8) uint16 { + v := float64(i) / 255.0 + return uint16(math.RoundToEven(linearize1(v) * 65535.0)) } // toLinearRGB converts a non-linear sRGB color to a linear RGB color space. -func toLinearRGB(c color.Color) (uint8, uint8, uint8) { +func toLinearRGB(c color.Color) (uint16, uint16, uint16) { // Optimize for different color types switch v := c.(type) { case color.Gray: - g := linearize255(v.Y) + g := linearize255to65535(v.Y) return g, g, g case color.Gray16: - g := linearize65535to255(v.Y) + g := linearize65535(v.Y) return g, g, g case color.NRGBA: - return linearize255(v.R), linearize255(v.G), linearize255(v.B) + return linearize255to65535(v.R), linearize255to65535(v.G), linearize255to65535(v.B) case color.NRGBA64: - return linearize65535to255(v.R), linearize65535to255(v.G), linearize65535to255(v.B) + return linearize65535(v.R), linearize65535(v.G), linearize65535(v.B) case color.RGBA: - return linearize255(v.R), linearize255(v.G), linearize255(v.B) + return linearize255to65535(v.R), linearize255to65535(v.G), linearize255to65535(v.B) case color.RGBA64: - return linearize65535to255(v.R), linearize65535to255(v.G), linearize65535to255(v.B) + return linearize65535(v.R), linearize65535(v.G), linearize65535(v.B) } r, g, b, _ := c.RGBA() - return linearize65535to255(uint16(r)), linearize65535to255(uint16(g)), linearize65535to255(uint16(b)) + return linearize65535(uint16(r)), linearize65535(uint16(g)), linearize65535(uint16(b)) } diff --git a/dither.go b/dither.go index e7760e5..bb7f53e 100644 --- a/dither.go +++ b/dither.go @@ -53,7 +53,7 @@ type Ditherer struct { palette []color.Color // linearPalette holds all the palette colors, but in linear RGB space. - linearPalette [][3]uint8 + linearPalette [][3]uint16 } // NewDitherer creates a new Ditherer that uses a copy of the provided palette. @@ -70,10 +70,10 @@ func NewDitherer(palette []color.Color) *Ditherer { copy(d.palette, palette) // Create linear RGB version of the palette - d.linearPalette = make([][3]uint8, len(d.palette)) + d.linearPalette = make([][3]uint16, len(d.palette)) for i := range d.linearPalette { r, g, b := toLinearRGB(d.palette[i]) - d.linearPalette[i] = [3]uint8{r, g, b} + d.linearPalette[i] = [3]uint16{r, g, b} } return d @@ -104,20 +104,20 @@ func (d *Ditherer) GetPalette() []color.Color { return p } -func sqDiff(v1 uint8, v2 uint8) uint16 { +func sqDiff(v1 uint16, v2 uint16) uint32 { // This optimization is copied from Go stdlib, see // https://github.com/golang/go/blob/go1.15.7/src/image/color/color.go#L314 - d := uint16(v1) - uint16(v2) + d := uint32(v1) - uint32(v2) return (d * d) >> 2 } // closestColor returns the index of the color in the palette that's closest to // the provided one, using Euclidean distance in linear RGB space. The provided // RGB values must be linear RGB. -func (d *Ditherer) closestColor(r, g, b uint8) int { +func (d *Ditherer) closestColor(r, g, b uint16) int { // Go through each color and find the closest one - color, best := 0, uint16(math.MaxUint16) + color, best := 0, uint32(math.MaxUint32) for i, c := range d.linearPalette { // Euclidean distance, but the square root part is removed dist := sqDiff(r, c[0]) + sqDiff(g, c[1]) + sqDiff(b, c[2]) @@ -189,16 +189,16 @@ func (d *Ditherer) Dither(src image.Image) image.Image { // Store linear values here instead of converting back and forth and storing // sRGB values inside the image. // Pointers are used to differentiate between a zero value and an unset value - lins := make([][][3]*uint8, b.Dy()) + lins := make([][][3]*uint16, b.Dy()) for i := 0; i < len(lins); i++ { - lins[i] = make([][3]*uint8, b.Dx()) + lins[i] = make([][3]*uint16, b.Dx()) } // Setters and getters for that linear storage - linearSet := func(x, y int, r, g, b uint8) { - lins[y][x] = [3]*uint8{&r, &g, &b} + linearSet := func(x, y int, r, g, b uint16) { + lins[y][x] = [3]*uint16{&r, &g, &b} } - linearAt := func(x, y int) (uint8, uint8, uint8) { + linearAt := func(x, y int) (uint16, uint16, uint16) { c := lins[y][x] if c[0] == nil { // This pixel hasn't been linearized yet @@ -225,11 +225,16 @@ func (d *Ditherer) Dither(src image.Image) image.Image { new := d.linearPalette[newColorIdx] // Quant errors in each channel - er, eg, eb := int16(oldR)-int16(new[0]), int16(oldG)-int16(new[1]), int16(oldB)-int16(new[2]) + er, eg, eb := int32(oldR)-int32(new[0]), int32(oldG)-int32(new[1]), int32(oldB)-int32(new[2]) // Diffuse error in two dimensions for yy := range d.Matrix { for xx := range d.Matrix[yy] { + if d.Matrix[yy][xx] == 0 { + // Skip, because it won't affect anything + continue + } + // Get the coords of the pixel the error is being applied to deltaX, deltaY := d.Matrix.Offset(xx, yy, curPx) if d.Serpentine && y%2 == 0 { @@ -247,12 +252,9 @@ func (d *Ditherer) Dither(src image.Image) image.Image { r, g, b := linearAt(pxX, pxY) linearSet(pxX, pxY, - // +0.5 is important to prevent pure white from being quantized to black due - // to the addition of error being rounded. This can be seen if you remove - // the +0.5 and test Floyd-Steinberg dithering on the gradient.png image. - clamp(float32(r)+float32(er)*d.Matrix[yy][xx]+0.5), - clamp(float32(g)+float32(eg)*d.Matrix[yy][xx]+0.5), - clamp(float32(b)+float32(eb)*d.Matrix[yy][xx]+0.5), + RoundClamp(float32(r)+float32(er)*d.Matrix[yy][xx]), + RoundClamp(float32(g)+float32(eg)*d.Matrix[yy][xx]), + RoundClamp(float32(b)+float32(eb)*d.Matrix[yy][xx]), ) } } @@ -328,15 +330,16 @@ func (d *Ditherer) DitherPalettedConfig(src image.Image) (*image.Paletted, image } } -// clamp clamps i to the interval [0, 255]. -func clamp(i float32) uint8 { +// 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. +func RoundClamp(i float32) uint16 { if i < 0 { return 0 } - if i > 255 { - return 255 + if i > 65535 { + return 65535 } - return uint8(i) + return uint16(math.RoundToEven(float64(i))) } // copyImage copies src's pixels into dst. diff --git a/examples/gif_animation.go b/examples/gif_animation.go index b4da935..41558aa 100644 --- a/examples/gif_animation.go +++ b/examples/gif_animation.go @@ -10,7 +10,7 @@ import ( _ "image/png" // For frame decoding "os" - "github.com/makeworld-the-better-one/dither" + "github.com/makeworld-the-better-one/dither/v2" ) const numFrames = 20 diff --git a/examples/gif_image.go b/examples/gif_image.go index c9af1d7..10bba12 100644 --- a/examples/gif_image.go +++ b/examples/gif_image.go @@ -10,7 +10,7 @@ import ( _ "image/png" // Imported for decoding of the input image "os" - "github.com/makeworld-the-better-one/dither" + "github.com/makeworld-the-better-one/dither/v2" ) func main() { diff --git a/examples/output/gif_animation.gif b/examples/output/gif_animation.gif index c8a664b..57a032c 100644 Binary files a/examples/output/gif_animation.gif and b/examples/output/gif_animation.gif differ diff --git a/examples/output/gif_image.gif b/examples/output/gif_image.gif index 95a7d55..d72ace5 100644 Binary files a/examples/output/gif_image.gif and b/examples/output/gif_image.gif differ diff --git a/go.mod b/go.mod index c3e6480..ba11e35 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/makeworld-the-better-one/dither +module github.com/makeworld-the-better-one/dither/v2 go 1.15 diff --git a/images/output/ClusteredDot4x4.png b/images/output/ClusteredDot4x4.png index b8b1081..e9840b7 100644 Binary files a/images/output/ClusteredDot4x4.png and b/images/output/ClusteredDot4x4.png differ diff --git a/images/output/ClusteredDot6x6.png b/images/output/ClusteredDot6x6.png index 1c5e10a..29a8da8 100644 Binary files a/images/output/ClusteredDot6x6.png and b/images/output/ClusteredDot6x6.png differ diff --git a/images/output/ClusteredDot6x6_2.png b/images/output/ClusteredDot6x6_2.png index 16416a0..c290f0b 100644 Binary files a/images/output/ClusteredDot6x6_2.png and b/images/output/ClusteredDot6x6_2.png differ diff --git a/images/output/ClusteredDot6x6_3.png b/images/output/ClusteredDot6x6_3.png index 99eef03..7e8db59 100644 Binary files a/images/output/ClusteredDot6x6_3.png and b/images/output/ClusteredDot6x6_3.png differ diff --git a/images/output/ClusteredDot8x8.png b/images/output/ClusteredDot8x8.png index 8a5642f..044fcb6 100644 Binary files a/images/output/ClusteredDot8x8.png and b/images/output/ClusteredDot8x8.png differ diff --git a/images/output/ClusteredDotDiagonal16x16_gradient.png b/images/output/ClusteredDotDiagonal16x16_gradient.png index f6f8bd0..f850414 100644 Binary files a/images/output/ClusteredDotDiagonal16x16_gradient.png and b/images/output/ClusteredDotDiagonal16x16_gradient.png differ diff --git a/images/output/ClusteredDotDiagonal6x6.png b/images/output/ClusteredDotDiagonal6x6.png index 18a1e96..825e5e3 100644 Binary files a/images/output/ClusteredDotDiagonal6x6.png and b/images/output/ClusteredDotDiagonal6x6.png differ diff --git a/images/output/ClusteredDotDiagonal8x8.png b/images/output/ClusteredDotDiagonal8x8.png index e325e9a..b8a6fef 100644 Binary files a/images/output/ClusteredDotDiagonal8x8.png and b/images/output/ClusteredDotDiagonal8x8.png differ diff --git a/images/output/ClusteredDotDiagonal8x8_2.png b/images/output/ClusteredDotDiagonal8x8_2.png index cf18ce4..3fc33a7 100644 Binary files a/images/output/ClusteredDotDiagonal8x8_2.png and b/images/output/ClusteredDotDiagonal8x8_2.png differ diff --git a/images/output/ClusteredDotDiagonal8x8_3.png b/images/output/ClusteredDotDiagonal8x8_3.png index 988ac3a..c5949fe 100644 Binary files a/images/output/ClusteredDotDiagonal8x8_3.png and b/images/output/ClusteredDotDiagonal8x8_3.png differ diff --git a/images/output/ClusteredDotHorizontalLine.png b/images/output/ClusteredDotHorizontalLine.png index 87f4c24..2aff1f4 100644 Binary files a/images/output/ClusteredDotHorizontalLine.png and b/images/output/ClusteredDotHorizontalLine.png differ diff --git a/images/output/ClusteredDotSpiral5x5.png b/images/output/ClusteredDotSpiral5x5.png index e902f06..cc2579d 100644 Binary files a/images/output/ClusteredDotSpiral5x5.png and b/images/output/ClusteredDotSpiral5x5.png differ diff --git a/images/output/ClusteredDotVerticalLine.png b/images/output/ClusteredDotVerticalLine.png index 4947b20..e7dd1c8 100644 Binary files a/images/output/ClusteredDotVerticalLine.png and b/images/output/ClusteredDotVerticalLine.png differ diff --git a/images/output/Horizontal3x5.png b/images/output/Horizontal3x5.png index 8b20956..a0823cd 100644 Binary files a/images/output/Horizontal3x5.png and b/images/output/Horizontal3x5.png differ diff --git a/images/output/Vertical5x3.png b/images/output/Vertical5x3.png index 79fed9e..b96e234 100644 Binary files a/images/output/Vertical5x3.png and b/images/output/Vertical5x3.png differ diff --git a/images/output/bayer_16x16_gradient.png b/images/output/bayer_16x16_gradient.png index 508fbb9..068da4d 100644 Binary files a/images/output/bayer_16x16_gradient.png and b/images/output/bayer_16x16_gradient.png differ diff --git a/images/output/bayer_16x16_red-green-black.png b/images/output/bayer_16x16_red-green-black.png index 11183e4..49cdd24 100644 Binary files a/images/output/bayer_16x16_red-green-black.png and b/images/output/bayer_16x16_red-green-black.png differ diff --git a/images/output/bayer_16x16_red-green-yellow-black.png b/images/output/bayer_16x16_red-green-yellow-black.png index 1b0de7c..fe50417 100644 Binary files a/images/output/bayer_16x16_red-green-yellow-black.png and b/images/output/bayer_16x16_red-green-yellow-black.png differ diff --git a/images/output/bayer_16x8_gradient.png b/images/output/bayer_16x8_gradient.png index 742f4e3..6faaa75 100644 Binary files a/images/output/bayer_16x8_gradient.png and b/images/output/bayer_16x8_gradient.png differ diff --git a/images/output/bayer_2x2_gradient.png b/images/output/bayer_2x2_gradient.png index 614d2de..acc2007 100644 Binary files a/images/output/bayer_2x2_gradient.png and b/images/output/bayer_2x2_gradient.png differ diff --git a/images/output/bayer_4x4_gradient.png b/images/output/bayer_4x4_gradient.png index 5b0df85..682a793 100644 Binary files a/images/output/bayer_4x4_gradient.png and b/images/output/bayer_4x4_gradient.png differ diff --git a/images/output/bayer_8x8_gradient.png b/images/output/bayer_8x8_gradient.png index 979ad96..cb71879 100644 Binary files a/images/output/bayer_8x8_gradient.png and b/images/output/bayer_8x8_gradient.png differ diff --git a/images/output/edm_atkinson.png b/images/output/edm_atkinson.png index f01d924..555c443 100644 Binary files a/images/output/edm_atkinson.png and b/images/output/edm_atkinson.png differ diff --git a/images/output/edm_floyd-steinberg.png b/images/output/edm_floyd-steinberg.png index aef3681..fd0598c 100644 Binary files a/images/output/edm_floyd-steinberg.png and b/images/output/edm_floyd-steinberg.png differ diff --git a/images/output/edm_floyd-steinberg_serpentine.png b/images/output/edm_floyd-steinberg_serpentine.png index 6b77722..8a6a0a2 100644 Binary files a/images/output/edm_floyd-steinberg_serpentine.png and b/images/output/edm_floyd-steinberg_serpentine.png differ diff --git a/images/output/edm_floyd-steinberg_strength_02.png b/images/output/edm_floyd-steinberg_strength_02.png index efdfe73..b2bba85 100644 Binary files a/images/output/edm_floyd-steinberg_strength_02.png and b/images/output/edm_floyd-steinberg_strength_02.png differ diff --git a/images/output/edm_jarvis-judice-ninke.png b/images/output/edm_jarvis-judice-ninke.png index 305c103..e45b58c 100644 Binary files a/images/output/edm_jarvis-judice-ninke.png and b/images/output/edm_jarvis-judice-ninke.png differ diff --git a/images/output/edm_peppers_atkinson_red-green-black.png b/images/output/edm_peppers_atkinson_red-green-black.png index 64356ab..4c079e9 100644 Binary files a/images/output/edm_peppers_atkinson_red-green-black.png and b/images/output/edm_peppers_atkinson_red-green-black.png differ diff --git a/images/output/edm_peppers_atkinson_red-green-yellow-black.png b/images/output/edm_peppers_atkinson_red-green-yellow-black.png index 9820f24..2b8b810 100644 Binary files a/images/output/edm_peppers_atkinson_red-green-yellow-black.png and b/images/output/edm_peppers_atkinson_red-green-yellow-black.png differ diff --git a/images/output/edm_peppers_floyd-steinberg_red-green-black.png b/images/output/edm_peppers_floyd-steinberg_red-green-black.png index 2ff23ba..6a438ba 100644 Binary files a/images/output/edm_peppers_floyd-steinberg_red-green-black.png and b/images/output/edm_peppers_floyd-steinberg_red-green-black.png differ diff --git a/images/output/edm_peppers_floyd-steinberg_red-green-yellow-black.png b/images/output/edm_peppers_floyd-steinberg_red-green-yellow-black.png index 27b6275..fe0aaf0 100644 Binary files a/images/output/edm_peppers_floyd-steinberg_red-green-yellow-black.png and b/images/output/edm_peppers_floyd-steinberg_red-green-yellow-black.png differ diff --git a/images/output/edm_peppers_jarvis-judice-ninke_red-green-black.png b/images/output/edm_peppers_jarvis-judice-ninke_red-green-black.png index f74898f..4a6d824 100644 Binary files a/images/output/edm_peppers_jarvis-judice-ninke_red-green-black.png and b/images/output/edm_peppers_jarvis-judice-ninke_red-green-black.png differ diff --git a/images/output/edm_peppers_jarvis-judice-ninke_red-green-yellow-black.png b/images/output/edm_peppers_jarvis-judice-ninke_red-green-yellow-black.png index 3acfe5f..d279f34 100644 Binary files a/images/output/edm_peppers_jarvis-judice-ninke_red-green-yellow-black.png and b/images/output/edm_peppers_jarvis-judice-ninke_red-green-yellow-black.png differ diff --git a/images/output/edm_peppers_simpled2d_red-green-black.png b/images/output/edm_peppers_simpled2d_red-green-black.png index 10534c7..b74e44b 100644 Binary files a/images/output/edm_peppers_simpled2d_red-green-black.png and b/images/output/edm_peppers_simpled2d_red-green-black.png differ diff --git a/images/output/edm_peppers_simpled2d_red-green-yellow-black.png b/images/output/edm_peppers_simpled2d_red-green-yellow-black.png index ce47b8e..f86105d 100644 Binary files a/images/output/edm_peppers_simpled2d_red-green-yellow-black.png and b/images/output/edm_peppers_simpled2d_red-green-yellow-black.png differ diff --git a/images/output/edm_simple2d.png b/images/output/edm_simple2d.png index da5c9af..e075775 100644 Binary files a/images/output/edm_simple2d.png and b/images/output/edm_simple2d.png differ diff --git a/images/output/edm_simple2d_serpentine.png b/images/output/edm_simple2d_serpentine.png index c328509..15e6dc7 100644 Binary files a/images/output/edm_simple2d_serpentine.png and b/images/output/edm_simple2d_serpentine.png differ diff --git a/images/output/random_noise_grayscale.png b/images/output/random_noise_grayscale.png index 43177e7..0dfb423 100644 Binary files a/images/output/random_noise_grayscale.png and b/images/output/random_noise_grayscale.png differ diff --git a/images/output/random_noise_rgb_red-green-black.png b/images/output/random_noise_rgb_red-green-black.png index d12bfad..2bfd7eb 100644 Binary files a/images/output/random_noise_rgb_red-green-black.png and b/images/output/random_noise_rgb_red-green-black.png differ diff --git a/images/output/random_noise_rgb_red-green-yellow-black.png b/images/output/random_noise_rgb_red-green-yellow-black.png index 80c8639..94e835c 100644 Binary files a/images/output/random_noise_rgb_red-green-yellow-black.png and b/images/output/random_noise_rgb_red-green-yellow-black.png differ diff --git a/ordered_ditherers.go b/ordered_ditherers.go index cae73cc..4af07b3 100644 --- a/ordered_ditherers.go +++ b/ordered_ditherers.go @@ -219,11 +219,12 @@ var ClusteredDotVerticalLine = OrderedDitherMatrix{ var ClusteredDot8x8 = OrderedDitherMatrix{ Matrix: [][]uint{ // For some reason the values in the book were in the range of 0-64 - // instead of 0-63. + // instead of 0-63. I changed the 64 value to 63, so that pure black + // didn't end up with occasional white dots. {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, 64, 62, 54, 42, 21}, + {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}, diff --git a/pixelmappers.go b/pixelmappers.go index b84f64b..2de6d53 100644 --- a/pixelmappers.go +++ b/pixelmappers.go @@ -14,10 +14,11 @@ import ( // // 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. +// 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. -type PixelMapper func(x, y int, r, g, b uint8) (uint8, uint8, uint8) +type PixelMapper func(x, y int, r, g, b uint16) (uint16, uint16, uint16) // RandomNoiseGrayscale returns a PixelMapper that adds random noise to the // color before returning. This is the simplest form of dithering. @@ -49,17 +50,17 @@ type PixelMapper func(x, y int, r, g, b uint8) (uint8, uint8, uint8) // not wrapped. Basically, don't worry about the values of your min and max // distorting the image in an unexpected way. func RandomNoiseGrayscale(min, max float32) PixelMapper { - return PixelMapper(func(x, y int, r, g, b uint8) (uint8, uint8, uint8) { + return PixelMapper(func(x, y int, r, g, b uint16) (uint16, uint16, uint16) { // These values were taken from Wikipedia: // https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale // 0.2126, 0.7152, 0.0722 - // Then multiplied by 255, to scale them for uint8 color. - // Note that 54 + 183 + 19 = 256. + // Then multiplied by 65535, to scale them for 16-bit color. + // Note that 13933 + 46871 + 4732 = 65536 // // Basically, this takes linear RGB and gives a linear gray. - gray := (54*uint16(r) + 183*uint16(g) + 19*uint16(b) + 1<<7) >> 8 + gray := (13933*uint32(r) + 46871*uint32(g) + 4732*uint32(b) + 1<<15) >> 16 - new := clamp(float32(gray) + 255.0*(rand.Float32()*(max-min)+min) + 0.5) + new := RoundClamp(float32(gray) + 65535.0*(rand.Float32()*(max-min)+min)) return new, new, new }) } @@ -74,10 +75,10 @@ func RandomNoiseGrayscale(min, max float32) PixelMapper { // See RandomNoiseGrayscale for more details about values and how this function // works. func RandomNoiseRGB(minR, maxR, minG, maxG, minB, maxB float32) PixelMapper { - return PixelMapper(func(x, y int, r, g, b uint8) (uint8, uint8, uint8) { - return clamp(float32(r) + 255.0*(rand.Float32()*(maxR-minR)+minR) + 0.5), - clamp(float32(g) + 255.0*(rand.Float32()*(maxG-minG)+minG) + 0.5), - clamp(float32(b) + 255.0*(rand.Float32()*(maxB-minB)+minB) + 0.5) + return PixelMapper(func(x, y int, r, g, b uint16) (uint16, uint16, uint16) { + return RoundClamp(float32(r) + 65535.0*(rand.Float32()*(maxR-minR)+minR)), + RoundClamp(float32(g) + 65535.0*(rand.Float32()*(maxG-minG)+minG)), + RoundClamp(float32(b) + 65535.0*(rand.Float32()*(maxB-minB)+minB)) }) } @@ -165,14 +166,18 @@ func bayerMatrix(xdim, ydim uint) [][]uint { // and returns a value that can be added to a color instead of thresholded. // // scale is the number that's multiplied at the end, usually you want this to be -// 255 to scale to match the color value range. value is the cell of the matrix. +// 65535 to scale to match the color value range. value is the cell of the matrix. // max is the divisor of the cell value, usually this is the product of the matrix // dimensions. func convThresholdToAddition(scale float32, value uint, max uint) float32 { // See: // https://en.wikipedia.org/wiki/Ordered_dithering // https://en.wikipedia.org/wiki/Talk:Ordered_dithering#Sources - return scale * (float32(value+1.0)/float32(max) - 0.5) + + // 0.50000006 is next possible float32 value after 0.5. This is to correct + // a rounding error that occurs when the number is exactly 0.5, which results + // in pure black being dithered when it should be left alone. + return scale * (float32(value+1.0)/float32(max) - 0.50000006) } // Bayer returns a PixelMapper that applies a Bayer matrix with the specified size. @@ -191,8 +196,8 @@ func convThresholdToAddition(scale float32, value uint, max uint) float32 { // Source: // https://bisqwit.iki.fi/story/howto/dither/jy/#Appendix%202ThresholdMatrix // -// strength should be in the range [-1, 1]. It is multiplied with 255, which is -// then multiplied with the matrix. +// 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. @@ -281,7 +286,7 @@ func Bayer(x, y uint, strength float32) PixelMapper { } // Create precalculated matrix - scale := 255.0 * strength + scale := 65535.0 * strength max := x * y precalc := make([][]float32, y) @@ -292,10 +297,10 @@ func Bayer(x, y uint, strength float32) PixelMapper { } } - return PixelMapper(func(xx, yy int, r, g, b uint8) (uint8, uint8, uint8) { - return clamp(float32(r) + precalc[yy%int(y)][xx%int(x)]), - clamp(float32(g) + precalc[yy%int(y)][xx%int(x)]), - clamp(float32(b) + precalc[yy%int(y)][xx%int(x)]) + return PixelMapper(func(xx, yy int, r, g, b uint16) (uint16, uint16, uint16) { + return RoundClamp(float32(r) + precalc[yy%int(y)][xx%int(x)]), + RoundClamp(float32(g) + precalc[yy%int(y)][xx%int(x)]), + RoundClamp(float32(b) + precalc[yy%int(y)][xx%int(x)]) }) } @@ -313,7 +318,7 @@ func Bayer(x, y uint, strength float32) PixelMapper { func PixelMapperFromMatrix(odm OrderedDitherMatrix, strength float32) PixelMapper { ydim := len(odm.Matrix) xdim := len(odm.Matrix[0]) - scale := 255.0 * strength + scale := 65535.0 * strength // Create precalculated matrix precalc := make([][]float32, ydim) @@ -324,9 +329,9 @@ func PixelMapperFromMatrix(odm OrderedDitherMatrix, strength float32) PixelMappe } } - return PixelMapper(func(xx, yy int, r, g, b uint8) (uint8, uint8, uint8) { - return clamp(float32(r) + precalc[yy%ydim][xx%xdim]), - clamp(float32(g) + precalc[yy%ydim][xx%xdim]), - clamp(float32(b) + precalc[yy%ydim][xx%xdim]) + return PixelMapper(func(xx, yy int, r, g, b uint16) (uint16, uint16, uint16) { + return RoundClamp(float32(r) + precalc[yy%ydim][xx%xdim]), + RoundClamp(float32(g) + precalc[yy%ydim][xx%xdim]), + RoundClamp(float32(b) + precalc[yy%ydim][xx%xdim]) }) }