diff --git a/main.go b/main.go index efe94e8..034c9f2 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( func main() { if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" { fmt.Printf(`usage: %s - Supported dither options are: noop, naive, randomnoise + Supported dither options are: noop; naive; randomnoise; bayer{0,1}; bayer{0,1}n Supported input image formats are jpg, png `, os.Args[0]) os.Exit(0) @@ -49,6 +49,8 @@ func main() { new = apply(i, bayerDithering(1, false)) case "bayer1n": new = apply(i, bayerDithering(1, true)) + case "simpleerror": + new = apply(i, simpleErrorDiffusion()) default: fmt.Printf("unknown ditherer option: %s\n", ditherer) os.Exit(2) diff --git a/quantizer.go b/quantizer.go index 6a22f17..16d8b85 100644 --- a/quantizer.go +++ b/quantizer.go @@ -3,7 +3,6 @@ package main import ( "image" "image/color" - "math" "math/rand" ) @@ -123,21 +122,31 @@ func bayerDithering(level int, invert bool) quantizerFunction { } } -// That is, "relative luminance": https://en.wikipedia.org/wiki/Relative_luminance. -// go's color library doesn't give any information on what "color space" this RGBA is derived from. -// Although the results look decent without using the sRGB->LinearRGB/CIE XYZ transformation, -// it's probably subtly wrong. Will play around with it. -// In particular, images seem "brighter" than they're supposed to be. -func luminence(c color.Color) float64 { - r, g, b, _ := c.RGBA() - // we divide by 65535 because each RGB value from RGBA() returns in the range [0,65535) - return (0.2126*float64(r) + 0.7152*float64(g) + 0.0722*float64(b)) / 65535 +func simpleErrorDiffusion() quantizerFunction { + errMap := make(map[coord]float64) + return func(x int, y int, c color.Color) color.Color { + l := luminence(c) + errMap[coord{x: x, y: y}] + if l > 0.5 { + errMap[coord{x: x + 1, y: y}] = (l - 1.0) / 2.0 + errMap[coord{x: x, y: y + 1}] = (l - 1.0) / 2.0 + delete(errMap, coord{x: x, y: y}) + return color.White + } + errMap[coord{x: x + 1, y: y}] = l / 2.0 + errMap[coord{x: x, y: y + 1}] = l / 2.0 + delete(errMap, coord{x: x, y: y}) + return color.Black + } } -// seems to result in super-dark images right now. -func sRGBtoXYZ(u float64) float64 { - if u <= 0.04045 { - return (25 * u) / 323 +// That is, "relative luminance": https://en.wikipedia.org/wiki/Relative_luminance. +// go's color library doesn't give any information on what "color space" the RGBA is derived from, +// so we convert to Y'CbCr, which returns luminence directly as the Y component. +func luminence(c color.Color) float64 { + nr, ok := color.NRGBAModel.Convert(c).(color.NRGBA) + if !ok { + return 0 } - return math.Pow(((200*u)+11)/211, 2.4) + y, _, _ := color.RGBToYCbCr(nr.R, nr.G, nr.B) + return float64(y) / 255.0 } diff --git a/quantizer_test.go b/quantizer_test.go new file mode 100644 index 0000000..09c3208 --- /dev/null +++ b/quantizer_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "image/color" + "testing" +) + +func TestNoop(t *testing.T) { + c := noOp(0, 0, color.White) + r, g, b, a := c.RGBA() + if r != 0xffff || g != 0xffff || b != 0xffff || a != 0xffff { + t.Log("noop did not return input") + t.Fail() + } +} + +func TestLuminence(t *testing.T) { + l := luminence(color.White) + if l != 1 { + t.Logf("white input did not output 1, instead %f", l) + t.Fail() + } + l = luminence(color.Black) + if l != 0 { + t.Logf("black input did not output 0, instead %f", l) + t.Fail() + } + l = luminence(color.Gray16{0x8000}) + if l < 0.5 { + t.Logf("white-leaning gray input was not above 0.5, instead %f", l) + t.Fail() + } + l = luminence(color.Gray16{0x7fff}) + if l > 0.5 { + t.Logf("black-leaning gray input not below 0.5, instead %f", l) + t.Fail() + } +}