From 301ee00b69e311b9c19213e12e2c774c97922208 Mon Sep 17 00:00:00 2001 From: David Ashby Date: Sun, 17 Jan 2021 12:24:45 -0500 Subject: [PATCH] clean up command-line parsing and add some error-diffusion experiments --- color_quant.go | 49 +++++++++++++++++ main.go | 141 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 155 insertions(+), 35 deletions(-) diff --git a/color_quant.go b/color_quant.go index 1724c87..d22bbb7 100644 --- a/color_quant.go +++ b/color_quant.go @@ -69,3 +69,52 @@ func colorBayer(level int, p color.Palette) quantizerFunction { 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) +} diff --git a/main.go b/main.go index 7d676d6..5fc6503 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "image" "image/color" @@ -32,25 +33,73 @@ func apply(i image.Image, f quantizerFunction) image.Image { 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() { - if len(os.Args) == 1 || len(os.Args) > 4 || os.Args[1] == "help" { - fmt.Printf(`usage: %s - Supported dither options are: noop; naive; randomnoise; bayer{0,1}; bayer{0,1}n - Supported input image formats are jpg, png -`, os.Args[0]) + ditherOpts := &ditherOpts{} + + filenameFlag := flag.String("in", "", "Input filename. PNG or JPG formats accepted.") + verboseFlag := flag.Bool("v", false, "Include diagnostic log lines.") + 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) } - 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) + if verbose { + fmt.Printf("loaded %s using %s\n", filename, codex) + } t := time.Now() @@ -59,33 +108,50 @@ func main() { case "noop": new = apply(i, noOp) case "naive": - new = apply(i, naiveBW) - case "palette": - new = apply(i, naivePalette(sixteencolors)) - case "randomnoise": + if palette == "bw" { + new = apply(i, naiveBW) + } else if palette == "sixteen" { + new = apply(i, naivePalette(sixteencolors)) + } + case "noise": 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()) + if palette == "bw" { + new = apply(i, randomNoise) + } else if palette == "sixteen" { + new = apply(i, randomNoisePalette(sixteencolors)) + } else { + fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette) + os.Exit(2) + } + case "bayer": + if palette == "bw" { + new = apply(i, bayerDithering(ditherOpts.level, ditherOpts.invert)) + } else if palette == "sixteen" { + new = apply(i, colorBayer(ditherOpts.level, sixteencolors)) + } else { + fmt.Printf("unknown palette option for %s ditherer: %s\n", ditherer, palette) + os.Exit(2) + } + 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": new = apply(i, floydSteinberg()) 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": new = apply(i, atkinson()) default: @@ -93,14 +159,19 @@ func main() { 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) if err != nil { fmt.Printf("error saving %s; %v\n", outfile, err) 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) {