206 lines
4.5 KiB
Go
206 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"image/color"
|
|
"math/rand"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
type diffusion struct {
|
|
matrix map[coord]float64
|
|
divisor float64
|
|
}
|
|
|
|
func applyError(diffusion diffusion, quantError float64, currentPixel coord, errMap map[coord]float64) {
|
|
for c, i := range diffusion.matrix {
|
|
target := coord{x: currentPixel.x + c.x, y: currentPixel.y + c.y}
|
|
errMap[target] = errMap[target] + (quantError * (i / diffusion.divisor))
|
|
}
|
|
}
|
|
|
|
func diffuser(errMap map[coord]float64, d diffusion) quantizerFunction {
|
|
return func(x int, y int, c color.Color) color.Color {
|
|
p := coord{x: x, y: y}
|
|
l := luminence(c) + errMap[p]
|
|
delete(errMap, p) // don't let the error map grow too big
|
|
if l > 0.5 {
|
|
applyError(d, l-1.0, p, errMap)
|
|
return color.White
|
|
}
|
|
applyError(d, l, p, errMap)
|
|
return color.Black
|
|
}
|
|
}
|
|
|
|
func simpleErrorDiffusion() quantizerFunction {
|
|
errMap := make(map[coord]float64)
|
|
d := diffusion{
|
|
divisor: 2.0,
|
|
matrix: map[coord]float64{
|
|
{x: 1, y: 0}: 1.0,
|
|
{x: 0, y: 1}: 1.0,
|
|
},
|
|
}
|
|
return diffuser(errMap, d)
|
|
}
|
|
|
|
func floydSteinberg() quantizerFunction {
|
|
errMap := make(map[coord]float64)
|
|
d := diffusion{
|
|
divisor: 16.0,
|
|
matrix: map[coord]float64{
|
|
{x: 1, y: 0}: 7.0,
|
|
{x: -1, y: 1}: 3.0,
|
|
{x: 0, y: 1}: 5.0,
|
|
{x: 1, y: 1}: 1.0,
|
|
},
|
|
}
|
|
return diffuser(errMap, d)
|
|
}
|
|
|
|
func jarvisJudiceNinke() quantizerFunction {
|
|
errMap := make(map[coord]float64)
|
|
d := diffusion{
|
|
divisor: 48.0,
|
|
matrix: map[coord]float64{
|
|
{x: 1, y: 0}: 7.0,
|
|
{x: 2, y: 0}: 5.0,
|
|
|
|
{x: -2, y: 1}: 3.0,
|
|
{x: -1, y: 1}: 5.0,
|
|
{x: 0, y: 1}: 7.0,
|
|
{x: 1, y: 1}: 5.0,
|
|
{x: 2, y: 1}: 3.0,
|
|
|
|
{x: -2, y: 2}: 1.0,
|
|
{x: -1, y: 2}: 3.0,
|
|
{x: 0, y: 2}: 5.0,
|
|
{x: 1, y: 2}: 3.0,
|
|
{x: 2, y: 2}: 1.0,
|
|
},
|
|
}
|
|
return diffuser(errMap, d)
|
|
}
|
|
|
|
func atkinson() quantizerFunction {
|
|
errMap := make(map[coord]float64)
|
|
d := diffusion{
|
|
divisor: 8.0,
|
|
matrix: map[coord]float64{
|
|
{x: 1, y: 0}: 1.0,
|
|
{x: 2, y: 0}: 1.0,
|
|
{x: -1, y: 1}: 1.0,
|
|
{x: 0, y: 1}: 1.0,
|
|
{x: 1, y: 1}: 1.0,
|
|
{x: 0, y: 2}: 1.0,
|
|
},
|
|
}
|
|
return diffuser(errMap, d)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
y, _, _ := color.RGBToYCbCr(nr.R, nr.G, nr.B)
|
|
return float64(y) / 255.0
|
|
}
|