start working out how the color versions of these work...

This commit is contained in:
David 2021-01-12 20:19:48 -05:00
parent 25240c2f0a
commit 4c0678cf77
5 changed files with 107 additions and 22 deletions

View File

@ -5,3 +5,5 @@ This repo implements a number of the dithering methods detailed on <https://surm
light.png, light-hires.jpg, dark.png, and dark-hires.jpg are from Surma's post, used under CC BY-NC-SA 4.0. light.png, light-hires.jpg, dark.png, and dark-hires.jpg are from Surma's post, used under CC BY-NC-SA 4.0.
david.png is taken from the wikipedia article on dithering, where it was released into the public domain. david.png is taken from the wikipedia article on dithering, where it was released into the public domain.
I took cricket.jpg.

74
color_quant.go Normal file
View File

@ -0,0 +1,74 @@
package main
import (
"image/color"
"math/rand"
)
var bwpallette = color.Palette{
color.Black,
color.White,
}
var sixteencolors = color.Palette{
color.RGBA{0, 0, 0, 255}, // black
color.RGBA{0, 0, 127, 255}, // navy
color.RGBA{0, 0, 255, 255}, // blue
color.RGBA{0, 127, 0, 255}, // green
color.RGBA{0, 255, 0, 255}, // lime
color.RGBA{127, 0, 0, 255}, // maroon
color.RGBA{255, 0, 0, 255}, // red
color.RGBA{0, 127, 127, 255}, // teal
color.RGBA{127, 0, 127, 255}, // purple
color.RGBA{127, 127, 0, 255}, // olive
color.RGBA{0, 255, 255, 255}, // aqua
color.RGBA{255, 0, 255, 255}, // fuchsia
color.RGBA{255, 255, 0, 255}, // yellow
color.RGBA{127, 127, 127, 255}, // gray
color.RGBA{192, 192, 192, 255}, // silver
color.RGBA{255, 255, 255, 255}, // white
}
func permuteColor(c color.Color, i uint8) color.Color {
r, g, b, a := c.RGBA()
return color.RGBA{
uint8(r>>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)]
}
}

BIN
example/cricket.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

31
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"image" "image"
"image/color"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"math/rand" "math/rand"
@ -10,6 +11,27 @@ import (
"time" "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() { 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>
@ -38,17 +60,26 @@ func main() {
new = apply(i, noOp) new = apply(i, noOp)
case "naive": case "naive":
new = apply(i, naiveBW) new = apply(i, naiveBW)
case "palette":
new = apply(i, naivePalette(sixteencolors))
case "randomnoise": case "randomnoise":
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
new = apply(i, randomNoise) new = apply(i, randomNoise)
case "noisepalette":
rand.Seed(time.Now().UnixNano())
new = apply(i, randomNoisePalette(sixteencolors))
case "bayer0": case "bayer0":
new = apply(i, bayerDithering(0, false)) new = apply(i, bayerDithering(0, false))
case "bayer0p":
new = apply(i, colorBayer(0, sixteencolors))
case "bayer0n": case "bayer0n":
new = apply(i, bayerDithering(0, true)) new = apply(i, bayerDithering(0, true))
case "bayer1": case "bayer1":
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 "bayer1p":
new = apply(i, colorBayer(1, sixteencolors))
case "simpleerror": case "simpleerror":
new = apply(i, simpleErrorDiffusion()) new = apply(i, simpleErrorDiffusion())
case "floydsteinberg": case "floydsteinberg":

View File

@ -1,28 +1,10 @@
package main package main
import ( import (
"image"
"image/color" "image/color"
"math/rand" "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. // noOp just clones colors from one image to another, to validate file handling.
func noOp(_, _ int, c color.Color) color.Color { func noOp(_, _ int, c color.Color) color.Color {
return c return c
@ -57,10 +39,6 @@ type bayer struct {
matrix map[coord]float64 matrix map[coord]float64
} }
type coord struct {
x, y int
}
func (b *bayer) valueAt(x, y int) float64 { func (b *bayer) valueAt(x, y int) float64 {
return b.matrix[coord{x: x % b.side, y: y % b.side}] return b.matrix[coord{x: x % b.side, y: y % b.side}]
} }