clean up command-line parsing and add some error-diffusion experiments

This commit is contained in:
David 2021-01-17 12:24:45 -05:00
parent 94d1e465c9
commit 301ee00b69
2 changed files with 155 additions and 35 deletions

View File

@ -69,3 +69,52 @@ func colorBayer(level int, p color.Palette) quantizerFunction {
return p[p.Index(rc)] return p[p.Index(rc)]
} }
} }
func colorError(errMap map[coord]float64, d diffusion, palette color.Palette) quantizerFunction {
r := float64(len(palette))
return func(x int, y int, c color.Color) color.Color {
p := coord{x: x, y: y}
rc := permuteColor(c, uint8(r*errMap[p]))
delete(errMap, p) // don't let the error map grow too big
nc := palette[palette.Index(rc)]
l := luminence(c) - luminence(nc)
applyError(d, l, p, errMap)
return nc
}
}
func simpleColorErrorDiffusion() 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 colorError(errMap, d, sixteencolors)
}
func colorJarvisJudiceNinke() 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 colorError(errMap, d, sixteencolors)
}

141
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
@ -32,25 +33,73 @@ func apply(i image.Image, f quantizerFunction) image.Image {
return out return out
} }
type ditherOpts struct {
raw string
level int
invert bool
}
func (o *ditherOpts) Set(input string) error {
for _, p := range input {
switch p {
case 'n':
o.invert = true
case '0':
o.level = 0
case '1':
o.level = 1
default:
return fmt.Errorf("unknown option %s", string(p))
}
}
return nil
}
func (o *ditherOpts) String() string {
return o.raw
}
func main() { func main() {
if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" { ditherOpts := &ditherOpts{}
fmt.Printf(`usage: %s <path/to/image.ext> <dither_option> <path/to/output.png>
Supported dither options are: noop; naive; randomnoise; bayer{0,1}; bayer{0,1}n filenameFlag := flag.String("in", "", "Input filename. PNG or JPG formats accepted.")
Supported input image formats are jpg, png verboseFlag := flag.Bool("v", false, "Include diagnostic log lines.")
`, os.Args[0]) paletteFlag := flag.String("p", "bw",
`Palette to use. Defaults to 'bw': black-and-white.
Pass 'sixteen' for the classic sixteen-color palette.
Not all dither options support palettes; unsupported ditherers will ignore this flag.`)
dithererFlag := flag.String("d", "naive",
`Ditherer to use. Defaults to 'naive', which just flattens to nearest color.
Other options: 'noise', 'bayer', 'error', 'floydsteinberg', 'jjn', 'atkinson'.`)
flag.Var(ditherOpts, "o",
`Supply options for the selected ditherer.
Currently only relevant to the bayer ditherer, which defaults to 'level 0, non-inverted'.
To change: '1' applies a level-one bayer diffuser; '1n' would invert it, '0' would be the default.`)
outputFlag := flag.String("out", "output.png", "Output filename. Outputs in PNG format.")
flag.Parse()
// extract all those pointers
filename := *filenameFlag
ditherer := *dithererFlag
palette := *paletteFlag
outfile := *outputFlag
verbose := *verboseFlag
if filename == "" || outfile == "" {
fmt.Printf("Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
os.Exit(0) os.Exit(0)
} }
filename := os.Args[1]
ditherer := os.Args[2]
outfile := os.Args[3]
i, codex, err := loadImage(filename) i, codex, err := loadImage(filename)
if err != nil { if err != nil {
fmt.Printf("error loading %s; %v\n", filename, err) fmt.Printf("error loading %s; %v\n", filename, err)
os.Exit(255) os.Exit(255)
} }
fmt.Printf("loaded %s using %s\n", filename, codex) if verbose {
fmt.Printf("loaded %s using %s\n", filename, codex)
}
t := time.Now() t := time.Now()
@ -59,33 +108,50 @@ func main() {
case "noop": case "noop":
new = apply(i, noOp) new = apply(i, noOp)
case "naive": case "naive":
new = apply(i, naiveBW) if palette == "bw" {
case "palette": new = apply(i, naiveBW)
new = apply(i, naivePalette(sixteencolors)) } else if palette == "sixteen" {
case "randomnoise": new = apply(i, naivePalette(sixteencolors))
}
case "noise":
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
new = apply(i, randomNoise) if palette == "bw" {
case "noisepalette": new = apply(i, randomNoise)
rand.Seed(time.Now().UnixNano()) } else if palette == "sixteen" {
new = apply(i, randomNoisePalette(sixteencolors)) new = apply(i, randomNoisePalette(sixteencolors))
case "bayer0": } else {
new = apply(i, bayerDithering(0, false)) fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette)
case "bayer0p": os.Exit(2)
new = apply(i, colorBayer(0, sixteencolors)) }
case "bayer0n": case "bayer":
new = apply(i, bayerDithering(0, true)) if palette == "bw" {
case "bayer1": new = apply(i, bayerDithering(ditherOpts.level, ditherOpts.invert))
new = apply(i, bayerDithering(1, false)) } else if palette == "sixteen" {
case "bayer1n": new = apply(i, colorBayer(ditherOpts.level, sixteencolors))
new = apply(i, bayerDithering(1, true)) } else {
case "bayer1p": fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette)
new = apply(i, colorBayer(1, sixteencolors)) os.Exit(2)
case "simpleerror": }
new = apply(i, simpleErrorDiffusion()) case "error":
if palette == "bw" {
new = apply(i, simpleErrorDiffusion())
} else if palette == "sixteen" {
new = apply(i, simpleColorErrorDiffusion())
} else {
fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette)
os.Exit(2)
}
case "floydsteinberg": case "floydsteinberg":
new = apply(i, floydSteinberg()) new = apply(i, floydSteinberg())
case "jjn": case "jjn":
new = apply(i, jarvisJudiceNinke()) if palette == "bw" {
new = apply(i, jarvisJudiceNinke())
} else if palette == "sixteen" {
new = apply(i, colorJarvisJudiceNinke())
} else {
fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette)
os.Exit(2)
}
case "atkinson": case "atkinson":
new = apply(i, atkinson()) new = apply(i, atkinson())
default: default:
@ -93,14 +159,19 @@ func main() {
os.Exit(2) os.Exit(2)
} }
fmt.Printf("dithering took %s\n", time.Since(t)) if verbose {
fmt.Printf("dithering took %s\n", time.Since(t))
}
err = saveImage(outfile, new) err = saveImage(outfile, new)
if err != nil { if err != nil {
fmt.Printf("error saving %s; %v\n", outfile, err) fmt.Printf("error saving %s; %v\n", outfile, err)
os.Exit(255) os.Exit(255)
} }
fmt.Printf("saved output to %s\n", outfile)
if verbose {
fmt.Printf("saved output to %s\n", outfile)
}
} }
func loadImage(filename string) (image.Image, string, error) { func loadImage(filename string) (image.Image, string, error) {