add simpleerror ditherer and begin building out tests
This commit is contained in:
parent
6086d357a1
commit
f4d8d20ce5
4
main.go
4
main.go
@ -13,7 +13,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" {
|
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>
|
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
|
Supported input image formats are jpg, png
|
||||||
`, os.Args[0])
|
`, os.Args[0])
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -49,6 +49,8 @@ func main() {
|
|||||||
new = apply(i, bayerDithering(1, false))
|
new = apply(i, bayerDithering(1, false))
|
||||||
case "bayer1n":
|
case "bayer1n":
|
||||||
new = apply(i, bayerDithering(1, true))
|
new = apply(i, bayerDithering(1, true))
|
||||||
|
case "simpleerror":
|
||||||
|
new = apply(i, simpleErrorDiffusion())
|
||||||
default:
|
default:
|
||||||
fmt.Printf("unknown ditherer option: %s\n", ditherer)
|
fmt.Printf("unknown ditherer option: %s\n", ditherer)
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
|
39
quantizer.go
39
quantizer.go
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -123,21 +122,31 @@ func bayerDithering(level int, invert bool) quantizerFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// That is, "relative luminance": https://en.wikipedia.org/wiki/Relative_luminance.
|
func simpleErrorDiffusion() quantizerFunction {
|
||||||
// go's color library doesn't give any information on what "color space" this RGBA is derived from.
|
errMap := make(map[coord]float64)
|
||||||
// Although the results look decent without using the sRGB->LinearRGB/CIE XYZ transformation,
|
return func(x int, y int, c color.Color) color.Color {
|
||||||
// it's probably subtly wrong. Will play around with it.
|
l := luminence(c) + errMap[coord{x: x, y: y}]
|
||||||
// In particular, images seem "brighter" than they're supposed to be.
|
if l > 0.5 {
|
||||||
func luminence(c color.Color) float64 {
|
errMap[coord{x: x + 1, y: y}] = (l - 1.0) / 2.0
|
||||||
r, g, b, _ := c.RGBA()
|
errMap[coord{x: x, y: y + 1}] = (l - 1.0) / 2.0
|
||||||
// we divide by 65535 because each RGB value from RGBA() returns in the range [0,65535)
|
delete(errMap, coord{x: x, y: y})
|
||||||
return (0.2126*float64(r) + 0.7152*float64(g) + 0.0722*float64(b)) / 65535
|
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.
|
// That is, "relative luminance": https://en.wikipedia.org/wiki/Relative_luminance.
|
||||||
func sRGBtoXYZ(u float64) float64 {
|
// go's color library doesn't give any information on what "color space" the RGBA is derived from,
|
||||||
if u <= 0.04045 {
|
// so we convert to Y'CbCr, which returns luminence directly as the Y component.
|
||||||
return (25 * u) / 323
|
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
38
quantizer_test.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user