start working out how the color versions of these work...
This commit is contained in:
parent
25240c2f0a
commit
4c0678cf77
@ -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
74
color_quant.go
Normal 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
BIN
example/cricket.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
31
main.go
31
main.go
@ -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":
|
||||||
|
22
quantizer.go
22
quantizer.go
@ -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}]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user