add simpleerror ditherer and begin building out tests

This commit is contained in:
David 2021-01-11 18:01:42 -05:00
parent 6086d357a1
commit f4d8d20ce5
3 changed files with 65 additions and 16 deletions

View File

@ -13,7 +13,7 @@ import (
func main() {
if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" {
fmt.Printf(`usage: %s <path/to/image.ext> <dither_option> <path/to/output.png>
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)

View File

@ -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
}

38
quantizer_test.go Normal file
View File

@ -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()
}
}