init
This commit is contained in:
commit
6086d357a1
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dither
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# ignore any test outputs
|
||||||
|
/*.png
|
BIN
example/dark-hires.jpg
Normal file
BIN
example/dark-hires.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
BIN
example/dark.png
Normal file
BIN
example/dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
example/light-hires.jpg
Normal file
BIN
example/light-hires.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
BIN
example/light.png
Normal file
BIN
example/light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
91
main.go
Normal file
91
main.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
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>
|
||||||
|
Supported dither options are: noop, naive, randomnoise
|
||||||
|
Supported input image formats are jpg, png
|
||||||
|
`, os.Args[0])
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := os.Args[1]
|
||||||
|
ditherer := os.Args[2]
|
||||||
|
outfile := os.Args[3]
|
||||||
|
|
||||||
|
i, codex, err := loadImage(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error loading %s; %v\n", filename, err)
|
||||||
|
os.Exit(255)
|
||||||
|
}
|
||||||
|
fmt.Printf("loaded %s using %s\n", filename, codex)
|
||||||
|
|
||||||
|
t := time.Now()
|
||||||
|
|
||||||
|
var new image.Image
|
||||||
|
switch ditherer {
|
||||||
|
case "noop":
|
||||||
|
new = apply(i, noOp)
|
||||||
|
case "naive":
|
||||||
|
new = apply(i, naiveBW)
|
||||||
|
case "randomnoise":
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
new = apply(i, randomNoise)
|
||||||
|
case "bayer0":
|
||||||
|
new = apply(i, bayerDithering(0, false))
|
||||||
|
case "bayer0n":
|
||||||
|
new = apply(i, bayerDithering(0, true))
|
||||||
|
case "bayer1":
|
||||||
|
new = apply(i, bayerDithering(1, false))
|
||||||
|
case "bayer1n":
|
||||||
|
new = apply(i, bayerDithering(1, true))
|
||||||
|
default:
|
||||||
|
fmt.Printf("unknown ditherer option: %s\n", ditherer)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("dithering took %s\n", time.Since(t))
|
||||||
|
|
||||||
|
err = saveImage(outfile, new)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error saving %s; %v\n", outfile, err)
|
||||||
|
os.Exit(255)
|
||||||
|
}
|
||||||
|
fmt.Printf("saved output to %s\n", outfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImage(filename string) (image.Image, string, error) {
|
||||||
|
r, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return image.Decode(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveImage(filename string, img image.Image) error {
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := png.Encode(f, img); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
143
quantizer.go
Normal file
143
quantizer.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user