├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── atkinson.go ├── burkes.go ├── floydsteinberg.go ├── main.go ├── sierra2.go ├── sierra3.go ├── sierra_lite.go └── stucki.go ├── dither_color.go ├── dither_mono.go ├── ditherer.go ├── go.mod ├── input ├── Lenna.png ├── david.jpg ├── flower.jpg ├── gopher.jpg └── portal.jpg └── output ├── color ├── Atkinson.png ├── Burkes.png ├── FloydSteinberg.png ├── Sierra-2.png ├── Sierra-3.png ├── Sierra-Lite.png └── Stucki.png └── mono ├── Atkinson.png ├── Burkes.png ├── FloydSteinberg.png ├── Sierra-2.png ├── Sierra-3.png ├── Sierra-Lite.png └── Stucki.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: esimov -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | strategy: 13 | matrix: 14 | go-version: [~1.14, ~1.13, ~1.12, ~1.11] 15 | runs-on: ubuntu-latest 16 | env: 17 | GO111MODULE: "on" 18 | steps: 19 | - name: Install Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Download Go modules 28 | run: go mod download 29 | 30 | - name: Build 31 | run: go build -o /dev/null -v ./cmd/main.go -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Endre Simó 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dithergo 2 | [![CI](https://github.com/esimov/dithergo/workflows/CI/badge.svg)](https://github.com/esimov/dithergo/actions) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/esimov/dithergo.svg)](https://pkg.go.dev/github.com/esimov/dithergo) 4 | [![license](https://img.shields.io/github/license/esimov/dithergo)](./LICENSE) 5 | 6 | Dithergo is a simple Go library implementing various [dithering](https://en.wikipedia.org/wiki/Dither) algorithm to produce halftone images. It supports color and monochrome image outputs. 7 | 8 | The library implements the following dithering methods: ***Floyd Steinberg, Atkinson, Burkes, Stucki, Sierra-2, Sierra-3, Sierra-Lite***. All of these algorithms have something in common: they diffuse the error in two dimensions, but they always push the error forward, never backward. 9 | 10 | We can represent this with the following diagram: 11 | 12 | X 7 5 13 | 3 5 7 5 3 14 | 1 3 5 3 1 15 | 16 | (1/48) 17 | 18 | where `X` represent the current pixel processed. The fraction at the bottom represents the divisor for the error. Above is the the `Floyd-Steinberg` dithering algorithm which can be transposed into the following Go code: 19 | 20 | ```go 21 | ditherers = []dither.Dither{ 22 | dither.Dither{ 23 | "FloydSteinberg", 24 | dither.Settings{ 25 | [][]float32{ 26 | []float32{ 0.0, 0.0, 0.0, 7.0 / 48.0, 5.0 / 48.0 }, 27 | []float32{ 3.0 / 48.0, 5.0 / 48.0, 7.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0 }, 28 | []float32{ 1.0 / 48.0, 3.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0, 1.0 / 48.0 }, 29 | }, 30 | float32(multiplier), 31 | }, 32 | }, 33 | } 34 | ``` 35 | 36 | You can plug in any dithering algorithm, so the library can be further extended. 37 | 38 | ### Installation 39 | 40 | `$ go get -u -v github.com/esimov/dithergo` 41 | 42 | ### Running 43 | 44 | Type `go run cmd/main.go --help` to check all the supported commands. The library supports the following commands: 45 | 46 | ``` 47 | Usage of commands: 48 | -e string 49 | Generates & exports the color and greyscale mode halftone images. 50 | Options: 'all', 'color', 'mono' (default "all") 51 | -em float 52 | Error multiplier (default 1.18) 53 | -o string 54 | Output folder 55 | -t Option to export the tresholded image (default true) 56 | 57 | ``` 58 | You can run all of the supported dithering algorithms at once, or you can run a specific one from the `cmd` directory. 59 | 60 | ### Results: 61 | | Input | 62 | |:--:| 63 | || 64 | 65 | The below images are generated with the default options using Michelangelo's David statue as sample image. 66 | 67 | | Color | Monochrome | 68 | |:--:|:--:| 69 | | | | 70 | Atkinson | Atkinson | 71 | | | | 72 | Burkes | Burkes | 73 | | | | 74 | Floyd-Steinberg | Floyd-Steinberg | 75 | | | | 76 | Sierra-2 | Sierra-2 | 77 | | | | 78 | Sierra-3 | Sierra-3 | 79 | | | | 80 | Sierra-Lite | Sierra-Lite | 81 | | | | 82 | Stucki | Stucki | 83 | 84 | ## Author 85 | 86 | * Endre Simo ([@simo_endre](https://twitter.com/simo_endre)) 87 | 88 | ## License 89 | 90 | Copyright © 2018 Endre Simo 91 | 92 | This software is distributed under the MIT license found in the LICENSE file. 93 | -------------------------------------------------------------------------------- /cmd/atkinson.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Atkinson", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 1.0 / 8.0, 1.0 / 8.0 }, 13 | []float32{ 1.0 / 8.0, 1.0 / 8.0, 1.0 / 8.0, 0.0 }, 14 | []float32{ 0.0, 1.0 / 8.0, 0.0, 0.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/burkes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Burkes", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 8.0 / 32.0, 4.0 / 32.0 }, 13 | []float32{ 2.0 / 32.0, 4.0 / 32.0, 8.0 / 32.0, 4.0 / 32.0, 2.0 / 32.0 }, 14 | []float32{ 0.0, 0.0, 0.0, 0.0, 0.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/floydsteinberg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "FloydSteinberg", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 7.0 / 48.0, 5.0 / 48.0 }, 13 | []float32{ 3.0 / 48.0, 5.0 / 48.0, 7.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0 }, 14 | []float32{ 1.0 / 48.0, 3.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0, 1.0 / 48.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "FloydSteinberg", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 7.0 / 48.0, 5.0 / 48.0 }, 13 | []float32{ 3.0 / 48.0, 5.0 / 48.0, 7.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0 }, 14 | []float32{ 1.0 / 48.0, 3.0 / 48.0, 5.0 / 48.0, 3.0 / 48.0, 1.0 / 48.0 }, 15 | }, 16 | }, 17 | }, 18 | dither.Dither{ 19 | "Stucki", 20 | dither.Settings{ 21 | [][]float32{ 22 | []float32{ 0.0, 0.0, 0.0, 8.0 / 42.0, 4.0 / 42.0 }, 23 | []float32{ 2.0 / 42.0, 4.0 / 42.0, 8.0 / 42.0, 4.0 / 42.0, 2.0 / 42.0 }, 24 | []float32{ 1.0 / 42.0, 2.0 / 42.0, 4.0 / 42.0, 2.0 / 42.0, 1.0 / 42.0 }, 25 | }, 26 | }, 27 | }, 28 | dither.Dither{ 29 | "Atkinson", 30 | dither.Settings{ 31 | [][]float32{ 32 | []float32{ 0.0, 0.0, 1.0 / 8.0, 1.0 / 8.0 }, 33 | []float32{ 1.0 / 8.0, 1.0 / 8.0, 1.0 / 8.0, 0.0 }, 34 | []float32{ 0.0, 1.0 / 8.0, 0.0, 0.0 }, 35 | }, 36 | }, 37 | }, 38 | dither.Dither{ 39 | "Burkes", 40 | dither.Settings{ 41 | [][]float32{ 42 | []float32{ 0.0, 0.0, 0.0, 8.0 / 32.0, 4.0 / 32.0 }, 43 | []float32{ 2.0 / 32.0, 4.0 / 32.0, 8.0 / 32.0, 4.0 / 32.0, 2.0 / 32.0 }, 44 | []float32{ 0.0, 0.0, 0.0, 0.0, 0.0 }, 45 | }, 46 | }, 47 | }, 48 | dither.Dither{ 49 | "Sierra-3", 50 | dither.Settings{ 51 | [][]float32{ 52 | []float32{ 0.0, 0.0, 0.0, 5.0 / 32.0, 3.0 / 32.0 }, 53 | []float32{ 2.0 / 32.0, 4.0 / 32.0, 5.0 / 32.0, 4.0 / 32.0, 2.0 / 32.0 }, 54 | []float32{ 0.0, 2.0 / 32.0, 3.0 / 32.0, 2.0 / 32.0, 0.0 }, 55 | }, 56 | }, 57 | }, 58 | dither.Dither{ 59 | "Sierra-2", 60 | dither.Settings{ 61 | [][]float32{ 62 | []float32{ 0.0, 0.0, 0.0, 4.0 / 16.0, 3.0 / 16.0 }, 63 | []float32{ 1.0 / 16.0, 2.0 / 16.0, 3.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0 }, 64 | []float32{ 0.0, 0.0, 0.0, 0.0, 0.0 }, 65 | }, 66 | }, 67 | }, 68 | dither.Dither{ 69 | "Sierra-Lite", 70 | dither.Settings{ 71 | [][]float32{ 72 | []float32{ 0.0, 0.0, 2.0 / 4.0 }, 73 | []float32{ 1.0 / 4.0, 1.0 / 4.0, 0.0 }, 74 | []float32{ 0.0, 0.0, 0.0 }, 75 | }, 76 | }, 77 | }, 78 | } 79 | 80 | func main() { 81 | dither.Process(ditherers) 82 | } -------------------------------------------------------------------------------- /cmd/sierra2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Sierra-2", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 4.0 / 16.0, 3.0 / 16.0 }, 13 | []float32{ 1.0 / 16.0, 2.0 / 16.0, 3.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0 }, 14 | []float32{ 0.0, 0.0, 0.0, 0.0, 0.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/sierra3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Sierra-3", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 5.0 / 32.0, 3.0 / 32.0 }, 13 | []float32{ 2.0 / 32.0, 4.0 / 32.0, 5.0 / 32.0, 4.0 / 32.0, 2.0 / 32.0 }, 14 | []float32{ 0.0, 2.0 / 32.0, 3.0 / 32.0, 2.0 / 32.0, 0.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/sierra_lite.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Sierra-Lite", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 2.0 / 4.0 }, 13 | []float32{ 1.0 / 4.0, 1.0 / 4.0, 0.0 }, 14 | []float32{ 0.0, 0.0, 0.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /cmd/stucki.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/esimov/dithergo" 5 | ) 6 | 7 | var ditherers []dither.Dither = []dither.Dither{ 8 | dither.Dither{ 9 | "Stucki", 10 | dither.Settings{ 11 | [][]float32{ 12 | []float32{ 0.0, 0.0, 0.0, 8.0 / 42.0, 4.0 / 42.0 }, 13 | []float32{ 2.0 / 42.0, 4.0 / 42.0, 8.0 / 42.0, 4.0 / 42.0, 2.0 / 42.0 }, 14 | []float32{ 1.0 / 42.0, 2.0 / 42.0, 4.0 / 42.0, 2.0 / 42.0, 1.0 / 42.0 }, 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | func main() { 21 | dither.Process(ditherers) 22 | } -------------------------------------------------------------------------------- /dither_color.go: -------------------------------------------------------------------------------- 1 | package dither 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | type Settings struct { 9 | Filter [][]float32 10 | } 11 | 12 | type Dither struct { 13 | Type string 14 | Settings 15 | } 16 | 17 | func (dither Dither) Color(input image.Image, errorMultiplier float32) image.Image { 18 | bounds := input.Bounds() 19 | img := image.NewRGBA(bounds) 20 | for x := bounds.Min.X; x < bounds.Dx(); x++ { 21 | for y := bounds.Min.Y; y < bounds.Dy(); y++ { 22 | pixel := input.At(x, y) 23 | img.Set(x, y, pixel) 24 | } 25 | } 26 | dx, dy := img.Bounds().Dx(), img.Bounds().Dy() 27 | 28 | // Prepopulate multidimensional slices 29 | redErrors := make([][]float32, dx) 30 | greenErrors := make([][]float32, dx) 31 | blueErrors := make([][]float32, dx) 32 | for x := 0; x < dx; x++ { 33 | redErrors[x] = make([]float32, dy) 34 | greenErrors[x] = make([]float32, dy) 35 | blueErrors[x] = make([]float32, dy) 36 | for y := 0; y < dy; y++ { 37 | redErrors[x][y] = 0 38 | greenErrors[x][y] = 0 39 | blueErrors[x][y] = 0 40 | } 41 | } 42 | 43 | var qrr, qrg, qrb float32 44 | for x := 0; x < dx; x++ { 45 | for y := 0; y < dy; y++ { 46 | r32, g32, b32, a := img.At(x, y).RGBA() 47 | r, g, b := float32(uint8(r32)), float32(uint8(g32)), float32(uint8(b32)) 48 | r -= redErrors[x][y] * errorMultiplier 49 | g -= greenErrors[x][y] * errorMultiplier 50 | b -= blueErrors[x][y] * errorMultiplier 51 | 52 | // Diffuse the error of each calculation to the neighboring pixels 53 | if r < 128 { 54 | qrr = -r 55 | r = 0 56 | } else { 57 | qrr = 255 - r 58 | r = 255 59 | } 60 | if g < 128 { 61 | qrg = -g 62 | g = 0 63 | } else { 64 | qrg = 255 - g 65 | g = 255 66 | } 67 | if b < 128 { 68 | qrb = -b 69 | b = 0 70 | } else { 71 | qrb = 255 - b 72 | b = 255 73 | } 74 | img.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}) 75 | 76 | // Diffuse error in two dimension 77 | ydim := len(dither.Filter) - 1 78 | xdim := len(dither.Filter[0]) / 2 79 | for xx := 0; xx < ydim+1; xx++ { 80 | for yy := -xdim; yy <= xdim-1; yy++ { 81 | if y+yy < 0 || dy <= y+yy || x+xx < 0 || dx <= x+xx { 82 | continue 83 | } 84 | // Adds the error of the previous pixel to the current pixel 85 | redErrors[x+xx][y+yy] += qrr * dither.Filter[xx][yy+ydim] 86 | greenErrors[x+xx][y+yy] += qrg * dither.Filter[xx][yy+ydim] 87 | blueErrors[x+xx][y+yy] += qrb * dither.Filter[xx][yy+ydim] 88 | } 89 | } 90 | } 91 | } 92 | return img 93 | } 94 | -------------------------------------------------------------------------------- /dither_mono.go: -------------------------------------------------------------------------------- 1 | package dither 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | func (dither Dither) Monochrome(input image.Image, errorMultiplier float32) image.Image { 9 | bounds := input.Bounds() 10 | img := image.NewGray(bounds) 11 | for x := bounds.Min.X; x < bounds.Dx(); x++ { 12 | for y := bounds.Min.Y; y < bounds.Dy(); y++ { 13 | pixel := input.At(x, y) 14 | img.Set(x, y, pixel) 15 | } 16 | } 17 | dx, dy := img.Bounds().Dx(), img.Bounds().Dy() 18 | 19 | // Prepopulate multidimensional slice 20 | errors := make([][]float32, dx) 21 | for x := 0; x < dx; x++ { 22 | errors[x] = make([]float32, dy) 23 | for y := 0; y < dy; y++ { 24 | errors[x][y] = 0 25 | } 26 | } 27 | 28 | for x := 0; x < dx; x++ { 29 | for y := 0; y < dy; y++ { 30 | pix := float32(img.GrayAt(x, y).Y) 31 | pix -= errors[x][y] * errorMultiplier 32 | 33 | var quantError float32 34 | // Diffuse the error of each calculation to the neighboring pixels 35 | if pix < 128 { 36 | quantError = -pix 37 | pix = 0 38 | } else { 39 | quantError = 255 - pix 40 | pix = 255 41 | } 42 | 43 | img.SetGray(x, y, color.Gray{Y: uint8(pix)}) 44 | 45 | // Diffuse error in two dimension 46 | ydim := len(dither.Filter) - 1 47 | xdim := len(dither.Filter[0]) / 2 48 | for xx := 0; xx < ydim+1; xx++ { 49 | for yy := -xdim; yy <= xdim-1; yy++ { 50 | if y+yy < 0 || dy <= y+yy || x+xx < 0 || dx <= x+xx { 51 | continue 52 | } 53 | // Adds the error of the previous pixel to the current pixel 54 | errors[x+xx][y+yy] += quantError * dither.Filter[xx][yy+ydim] 55 | } 56 | } 57 | } 58 | } 59 | return img 60 | } 61 | -------------------------------------------------------------------------------- /ditherer.go: -------------------------------------------------------------------------------- 1 | package dither 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | _ "image/jpeg" 9 | "image/png" 10 | "log" 11 | "os" 12 | "time" 13 | ) 14 | 15 | type file struct { 16 | name string 17 | } 18 | 19 | // Command line flags 20 | var ( 21 | outputDir string 22 | colorType string 23 | treshold bool 24 | multiplier float64 25 | cmd flag.FlagSet 26 | ) 27 | 28 | // Open reads the source image 29 | func (file *file) Open() (image.Image, error) { 30 | f, err := os.Open(file.name) 31 | if err != nil { 32 | return nil, err 33 | } 34 | defer f.Close() 35 | 36 | img, _, err := image.Decode(f) 37 | return img, err 38 | } 39 | 40 | // Grayscale converts an image to grayscale mode 41 | func (file *file) Grayscale(input image.Image) (*image.Gray, error) { 42 | bounds := input.Bounds() 43 | gray := image.NewGray(bounds) 44 | 45 | for x := bounds.Min.X; x < bounds.Dx(); x++ { 46 | for y := bounds.Min.Y; y < bounds.Dy(); y++ { 47 | pixel := input.At(x, y) 48 | gray.Set(x, y, pixel) 49 | } 50 | } 51 | return gray, nil 52 | } 53 | 54 | // tresholdDithering creates a tresholded image 55 | func (file *file) tresholdDithering(input *image.Gray) (*image.Gray, error) { 56 | var ( 57 | bounds = input.Bounds() 58 | dithered = image.NewGray(bounds) 59 | dx = bounds.Dx() 60 | dy = bounds.Dy() 61 | ) 62 | 63 | for x := 0; x < dx; x++ { 64 | for y := 0; y < dy; y++ { 65 | pixel := input.GrayAt(x, y) 66 | threshold := func(pixel color.Gray) color.Gray { 67 | if pixel.Y > 123 { 68 | return color.Gray{Y: 255} 69 | } 70 | return color.Gray{Y: 0} 71 | } 72 | 73 | dithered.Set(x, y, threshold(pixel)) 74 | } 75 | } 76 | output, err := os.Create(outputDir + "/treshold.png") 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer output.Close() 81 | err = png.Encode(output, dithered) 82 | 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | return dithered, nil 87 | } 88 | 89 | // Process parses the command line inputs and calls the defined dithering method 90 | func Process(ditherers []Dither) { 91 | cmd = *flag.NewFlagSet("commands", flag.ExitOnError) 92 | cmd.StringVar(&outputDir, "o", "", "Output folder") 93 | cmd.StringVar(&colorType, "e", "all", "Generates & exports the color and greyscale mode halftone images. Options: 'all', 'color', 'mono'") 94 | cmd.BoolVar(&treshold, "t", true, "Option to export the tresholded image") 95 | cmd.Float64Var(&multiplier, "em", 1.18, "Error multiplier") 96 | 97 | log.SetPrefix("dithergo: ") 98 | log.SetFlags(0) 99 | 100 | cmd.Usage = func() { 101 | fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) 102 | cmd.PrintDefaults() 103 | } 104 | 105 | if len(os.Args) == 1 { 106 | fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) 107 | os.Exit(0) 108 | } 109 | 110 | if os.Args[1] == "--help" || os.Args[1] == "-h" { 111 | fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) 112 | cmd.PrintDefaults() 113 | os.Exit(0) 114 | } 115 | 116 | // Parse flags before to use them 117 | cmd.Parse(os.Args[2:]) 118 | 119 | if len(cmd.Args()) > 0 { 120 | cmd.Usage() 121 | log.Printf("missing input file.") 122 | os.Exit(0) 123 | } 124 | 125 | if len(outputDir) == 0 { 126 | cmd.Usage() 127 | log.Printf("missing output directory.") 128 | os.Exit(0) 129 | } 130 | 131 | input := &file{name: string(os.Args[1])} 132 | img, err := input.Open() 133 | if err != nil { 134 | log.Fatalf("Input error: %v", err) 135 | } 136 | 137 | // Channel to signal the completion event 138 | done := make(chan struct{}) 139 | 140 | fmt.Print("Rendering image...") 141 | now := time.Now() 142 | progress(done) 143 | 144 | // Run the ditherer method 145 | func(input *file, done chan struct{}) { 146 | if cmd.Parsed() { 147 | if _, err := os.Stat(outputDir); os.IsNotExist(err) { 148 | os.Mkdir(outputDir, os.ModePerm) 149 | } 150 | _ = os.Mkdir(outputDir+"/color", os.ModePerm) 151 | _ = os.Mkdir(outputDir+"/mono", os.ModePerm) 152 | 153 | if treshold { 154 | gray, _ := input.Grayscale(img) 155 | input.tresholdDithering(gray) 156 | } 157 | 158 | for _, ditherer := range ditherers { 159 | dc := ditherer.Color(img, float32(multiplier)) 160 | dg := ditherer.Monochrome(img, float32(multiplier)) 161 | cex := outputDir + "/color/" 162 | gex := outputDir + "/mono/" 163 | 164 | switch colorType { 165 | case "all": 166 | generateOutput(ditherer, dc, cex) 167 | generateOutput(ditherer, dg, gex) 168 | case "color": 169 | generateOutput(ditherer, dc, cex) 170 | case "mono": 171 | generateOutput(ditherer, dg, gex) 172 | } 173 | } 174 | done <- struct{}{} 175 | } 176 | }(input, done) 177 | 178 | since := time.Since(now) 179 | fmt.Println("\nDone✓") 180 | fmt.Printf("Rendered in: %.2fs\n", since.Seconds()) 181 | } 182 | 183 | // generateOutput render the generated image 184 | func generateOutput(dither Dither, img image.Image, exportDir string) { 185 | output, err := os.Create(exportDir + dither.Type + ".png") 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | defer output.Close() 190 | 191 | err = png.Encode(output, img) 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | } 196 | 197 | // progress visualize the rendering progress 198 | func progress(done chan struct{}) { 199 | ticker := time.NewTicker(time.Millisecond * 200) 200 | 201 | go func() { 202 | for { 203 | select { 204 | case <-ticker.C: 205 | fmt.Print(".") 206 | case <-done: 207 | ticker.Stop() 208 | } 209 | } 210 | }() 211 | } 212 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/esimov/dithergo 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /input/Lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/input/Lenna.png -------------------------------------------------------------------------------- /input/david.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/input/david.jpg -------------------------------------------------------------------------------- /input/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/input/flower.jpg -------------------------------------------------------------------------------- /input/gopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/input/gopher.jpg -------------------------------------------------------------------------------- /input/portal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/input/portal.jpg -------------------------------------------------------------------------------- /output/color/Atkinson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Atkinson.png -------------------------------------------------------------------------------- /output/color/Burkes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Burkes.png -------------------------------------------------------------------------------- /output/color/FloydSteinberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/FloydSteinberg.png -------------------------------------------------------------------------------- /output/color/Sierra-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Sierra-2.png -------------------------------------------------------------------------------- /output/color/Sierra-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Sierra-3.png -------------------------------------------------------------------------------- /output/color/Sierra-Lite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Sierra-Lite.png -------------------------------------------------------------------------------- /output/color/Stucki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/color/Stucki.png -------------------------------------------------------------------------------- /output/mono/Atkinson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Atkinson.png -------------------------------------------------------------------------------- /output/mono/Burkes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Burkes.png -------------------------------------------------------------------------------- /output/mono/FloydSteinberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/FloydSteinberg.png -------------------------------------------------------------------------------- /output/mono/Sierra-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Sierra-2.png -------------------------------------------------------------------------------- /output/mono/Sierra-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Sierra-3.png -------------------------------------------------------------------------------- /output/mono/Sierra-Lite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Sierra-Lite.png -------------------------------------------------------------------------------- /output/mono/Stucki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esimov/dithergo/7f9ddf55e84884b02360fd2ddad64753992e71bb/output/mono/Stucki.png --------------------------------------------------------------------------------