dither/quantizer.go

144 lines
3.5 KiB
Go
Raw Normal View History

2021-01-10 03:35:31 +00:00
package main
import (
"image"
"image/color"
"math"
"math/rand"
)
// provided x, y, and color at location, return a color
type quantizerFunction func(int, int, color.Color) color.Color
// apply sequentially applies a quantizing function to an image and returns the result
func apply(i image.Image, f quantizerFunction) image.Image {
out := image.NewRGBA(image.Rect(0, 0, i.Bounds().Max.X, i.Bounds().Max.Y))
b := out.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
out.Set(x, y, f(x, y, i.At(x, y)))
}
}
return out
}
// noOp just clones colors from one image to another, to validate file handling.
func noOp(_, _ int, c color.Color) color.Color {
return c
}
// naiveBW smashes each pixel to black or white based on lumosity.
func naiveBW(_, _ int, c color.Color) color.Color {
l := luminence(c)
if l > 0.5 {
return color.White
}
return color.Black
}
// randomNoise injects random noise into the quantization step
func randomNoise(_, _ int, c color.Color) color.Color {
l := luminence(c)
if (l + rand.Float64() - 0.5) > 0.5 {
return color.White
}
return color.Black
}
// bayer dithering applies a "value map" to our brightness range, instead of messing with the luminence itself.
// the basic one is a matrix of
// 0, 2
// 1, 3
// normalized by the number of cells in the matrix (i.e. divided by 4, in this case) and then compared to the luminosity.
// it takes the current coordinates as input to find your location in the (tiled) matrix.
type bayer struct {
side int
matrix map[coord]float64
}
type coord struct {
x, y int
}
func (b *bayer) valueAt(x, y int) float64 {
return b.matrix[coord{x: x % b.side, y: y % b.side}]
}
// I could do this recursively but don't feel like it
func newBayer(level int) *bayer {
if level == 1 {
return &bayer{
side: 4,
matrix: map[coord]float64{
{0, 0}: 0 / 16.0,
{1, 0}: 8 / 16.0,
{0, 1}: 12 / 16.0,
{1, 1}: 4 / 16.0,
{2, 0}: 2 / 16.0,
{3, 0}: 10 / 16.0,
{2, 1}: 14 / 16.0,
{3, 1}: 6 / 16.0,
{0, 2}: 3 / 16.0,
{1, 2}: 11 / 16.0,
{0, 3}: 15 / 16.0,
{1, 3}: 7 / 16.0,
{2, 2}: 1 / 16.0,
{3, 2}: 9 / 16.0,
{2, 3}: 13 / 16.0,
{3, 3}: 5 / 16.0,
},
}
}
return &bayer{
side: 2,
matrix: map[coord]float64{
{0, 0}: 0 / 4.0,
{1, 0}: 2 / 4.0,
{0, 1}: 3 / 4.0,
{1, 1}: 1 / 4.0,
},
}
}
func bayerDithering(level int, invert bool) quantizerFunction {
b := newBayer(level)
return func(x int, y int, c color.Color) color.Color {
l := luminence(c)
v := b.valueAt(x, y)
if invert {
if l > 1-v {
return color.White
}
return color.Black
}
if l > v {
return color.White
}
return color.Black
}
}
// 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
}
// seems to result in super-dark images right now.
func sRGBtoXYZ(u float64) float64 {
if u <= 0.04045 {
return (25 * u) / 323
}
return math.Pow(((200*u)+11)/211, 2.4)
}