diff --git a/README.md b/README.md index 7bec301..a4cda23 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ This repo implements a number of the dithering methods detailed on >8) + i, + uint8(g>>8) + i, + uint8(b>>8) + i, + uint8(a >> 8), + } +} + +// naivePalette smashes each pixel to its closest color in the pallette. +func naivePalette(p color.Palette) quantizerFunction { + return func(_, _ int, c color.Color) color.Color { + return p[p.Index(c)] + } +} + +// randomNoisePalette injects random noise into the quantization step +func randomNoisePalette(p color.Palette) quantizerFunction { + return func(_, _ int, c color.Color) color.Color { + // the randomization here is tuned for the sixteen-color palette for now. + // I think the proper theory here is probably "only try and randomize within one palette swatch in either direction". + // it might be possible to instead permute the color selected _from the palette_ (i.e. modify the result of p.Index(c))... + // ...but I think for that to work you'd need a proper "ordering" for the colors. + noise := rand.Intn(64) - 32 + if noise < 0 { + noise = 0 + } + rc := permuteColor(c, uint8(noise)) + return p[p.Index(rc)] + } +} + +// color permutation algo: https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm +func colorBayer(level int, p color.Palette) quantizerFunction { + b := newBayer(level) + r := len(p) + return func(x, y int, c color.Color) color.Color { + v := float64(r) * (b.valueAt(x, y) - 0.5) + rc := permuteColor(c, uint8(v)) + return p[p.Index(rc)] + } +} diff --git a/example/cricket.jpg b/example/cricket.jpg new file mode 100644 index 0000000..baa49ed Binary files /dev/null and b/example/cricket.jpg differ diff --git a/main.go b/main.go index 8130549..7d676d6 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "image" + "image/color" _ "image/jpeg" "image/png" "math/rand" @@ -10,6 +11,27 @@ import ( "time" ) +type coord struct { + x, y int +} + +// 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 +} + func main() { if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" { fmt.Printf(`usage: %s @@ -38,17 +60,26 @@ func main() { new = apply(i, noOp) case "naive": new = apply(i, naiveBW) + case "palette": + new = apply(i, naivePalette(sixteencolors)) case "randomnoise": rand.Seed(time.Now().UnixNano()) new = apply(i, randomNoise) + case "noisepalette": + rand.Seed(time.Now().UnixNano()) + new = apply(i, randomNoisePalette(sixteencolors)) case "bayer0": new = apply(i, bayerDithering(0, false)) + case "bayer0p": + new = apply(i, colorBayer(0, sixteencolors)) case "bayer0n": new = apply(i, bayerDithering(0, true)) case "bayer1": new = apply(i, bayerDithering(1, false)) case "bayer1n": new = apply(i, bayerDithering(1, true)) + case "bayer1p": + new = apply(i, colorBayer(1, sixteencolors)) case "simpleerror": new = apply(i, simpleErrorDiffusion()) case "floydsteinberg": diff --git a/quantizer.go b/quantizer.go index 248b999..9ab3236 100644 --- a/quantizer.go +++ b/quantizer.go @@ -1,28 +1,10 @@ package main import ( - "image" "image/color" "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 @@ -57,10 +39,6 @@ type bayer struct { 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}] }