├── .github └── workflows │ └── check.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── adjust ├── adjustment.go ├── adjustment_test.go ├── apply.go └── apply_test.go ├── assets └── img │ ├── add.jpg │ ├── boxblur.jpg │ ├── brightness.jpg │ ├── colorburn.jpg │ ├── colordodge.jpg │ ├── contrast.jpg │ ├── crop.jpg │ ├── darken.jpg │ ├── difference.jpg │ ├── dilate.jpg │ ├── divide.jpg │ ├── edgedetection.jpg │ ├── emboss.jpg │ ├── erode.jpg │ ├── exclusion.jpg │ ├── extractchannel.jpg │ ├── fliph.jpg │ ├── flipv.jpg │ ├── floodfill.jpg │ ├── gamma.jpg │ ├── gaussianblur.jpg │ ├── grayscale.jpg │ ├── histogram.png │ ├── hue.jpg │ ├── invert.jpg │ ├── lighten.jpg │ ├── linearburn.jpg │ ├── linearlight.jpg │ ├── logo.png │ ├── median.jpg │ ├── multiply.jpg │ ├── noisebinary.jpg │ ├── noisegaussian.jpg │ ├── noiseuniform.jpg │ ├── normal.jpg │ ├── opacity.jpg │ ├── original.jpg │ ├── overlay.jpg │ ├── perlin.jpg │ ├── resizebox.jpg │ ├── resizecatmullrom.jpg │ ├── resizegaussian.jpg │ ├── resizelanczos.jpg │ ├── resizelinear.jpg │ ├── resizemitchell.jpg │ ├── resizenearestneighbor.jpg │ ├── rotation01.gif │ ├── rotation02.gif │ ├── rotation03.gif │ ├── saturation.jpg │ ├── screen.jpg │ ├── sepia.jpg │ ├── sharpen.jpg │ ├── shearh.jpg │ ├── shearv.jpg │ ├── sobel.jpg │ ├── softlight.jpg │ ├── subtract.jpg │ ├── threshold.jpg │ ├── translate.jpg │ └── unsharpmask.jpg ├── benchmarks.txt ├── bin └── release ├── blend ├── blend.go └── blend_test.go ├── blur ├── blur.go └── blur_test.go ├── channel ├── channel.go └── channel_test.go ├── clone ├── clone.go └── clone_test.go ├── cmd ├── adjust.go ├── blend.go ├── blur.go ├── channel.go ├── effect.go ├── helpers.go ├── histogram.go ├── imgio.go ├── noise.go ├── root.go └── segment.go ├── convolution ├── convolution.go ├── convolution_test.go ├── kernel.go └── kernel_test.go ├── effect ├── effect.go └── effect_test.go ├── fcolor ├── rgbaf64.go └── rgbaf64_test.go ├── go.mod ├── go.sum ├── histogram ├── histogram.go └── histogram_test.go ├── imgio ├── io.go └── io_test.go ├── main.go ├── math ├── f64 │ ├── clamp.go │ └── clamp_test.go └── integer │ ├── helpers.go │ └── helpers_test.go ├── noise ├── noise.go └── noise_test.go ├── paint ├── fill.go └── fill_test.go ├── parallel ├── parallel.go └── parallel_test.go ├── perlin └── perlin.go ├── segment ├── thresholding.go └── thresholding_test.go ├── transform ├── filters.go ├── resize.go ├── resize_test.go ├── rotate.go ├── rotate_test.go ├── shear.go ├── shear_test.go ├── translate.go └── translate_test.go └── util ├── colormodel.go ├── colormodel_test.go ├── stack.go ├── stack_test.go ├── util.go └── util_test.go /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | check_build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | go-version: 16 | - '1.11' 17 | - '1.12' 18 | - '1.13' 19 | - '1.14' 20 | - '1.15' 21 | - '1.16' 22 | - '1.17' 23 | - '1.18' 24 | - '1.19' 25 | - '1.20' 26 | - '1.21' 27 | - '1.22' 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Setup Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: ${{ matrix.go-version }} 36 | 37 | - name: Test codebase 38 | run: make test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # Visual Studio Code files 5 | .vscode/* 6 | !.vscode/settings.json 7 | !.vscode/tasks.json 8 | !.vscode/launch.json 9 | 10 | #Jetbrains project files 11 | .idea/ 12 | 13 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 14 | *.o 15 | *.a 16 | *.so 17 | 18 | # Folders 19 | _obj 20 | _test 21 | 22 | # Architecture specific extensions/prefixes 23 | *.[568vq] 24 | [568vq].out 25 | 26 | *.cgo1.go 27 | *.cgo2.c 28 | _cgo_defun.c 29 | _cgo_gotypes.go 30 | _cgo_export.* 31 | 32 | _testmain.go 33 | 34 | *.exe 35 | *.test 36 | *.prof 37 | 38 | # Output of the go coverage tool, specifically when used with LiteIDE 39 | *.out 40 | 41 | # Dist 42 | dist 43 | bild 44 | 45 | # Tmp files 46 | tmp -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## next 4 | - 5 | 6 | ## 0.14.0 7 | - transform: reduce amount of calls to cos/sin by @sbinet in https://github.com/anthonynsimon/bild/pull/92 8 | - Fix: forward compat go mod by @anthonynsimon in https://github.com/anthonynsimon/bild/pull/102 9 | - Bump golang.org/x/image from 0.5.0 to 0.18.0 by @dependabot in https://github.com/anthonynsimon/bild/pull/101 10 | - Bump golang.org/x/image from 0.0.0-20190703141733-d6a02ce849c9 to 0.5.0 by @dependabot in https://github.com/anthonynsimon/bild/pull/96 11 | 12 | ## 0.13.0 13 | - [PR-85:](https://github.com/anthonynsimon/bild/pull/85) Up to 20% less allocations and 90% less bytes allocated. 14 | - Minor documentation improvements. 15 | 16 | ## 0.12.0 17 | - [PR-74:](https://github.com/anthonynsimon/bild/pull/74) Add Perlin noise function 18 | - [PR-77:](https://github.com/anthonynsimon/bild/pull/77) Performance improvements for the image adjustment function 19 | - [PR-81:](https://github.com/anthonynsimon/bild/pull/81) Make blend() exported to allow for custom blending implementations 20 | - [PR-82:](https://github.com/anthonynsimon/bild/pull/82) Fix rotate panic 21 | - Minor additional fixes and documentation improvements. 22 | 23 | ## 0.11.1 24 | - [PR-71:](https://github.com/anthonynsimon/bild/pull/71) Gaussian blur is up to ~20x faster. 25 | 26 | ## 0.11.0 27 | - bild now comes with a built-in CLI 28 | - Added extract multiple channels functionality 29 | - Minor fixes and performance improvements 30 | 31 | ## 0.10.0 32 | - New feature effect.UnsharpMask 33 | - Changed paint.FloodFill fuzz parameter to tolerance based. This is a - breaking change. 34 | 35 | 36 | ## 0.9.0 37 | - New feature paint.FloodFill 38 | - New feature transform.Translate 39 | 40 | ## 0.8.7 41 | - Significant performance optimisations for Resize, Rotate, Convolve and Spatial Filtering functions. Most effects and blurs are indirectly benefited from this. 42 | 43 | ## 0.7.0 44 | - New feature transform.Shear 45 | - New feature adjust.Hue and adjust.Saturation 46 | - New features effect.Dilate and effect.Erode 47 | 48 | ## 0.6.0 49 | - New noise package, now you can generate Binary, Uniform and Gaussian noise (colored and monochrome). 50 | 51 | ## 0.5.0 52 | - Major code refactor. Breaking changes as all APIs have been decentralised into sub-packages. 53 | 54 | ## 0.4.0 55 | - Initial open source release. 56 | - Release before major code refactor. Package bild contains all APIs in this release. 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Want to hack on the project? Any kind of contribution is welcome! 3 | Simply follow the next steps: 4 | 5 | - Fork the project. 6 | - Create a new branch. 7 | - Make your changes and write tests when practical. 8 | - Commit your changes to the new branch. 9 | - Send a pull request, it will be reviewed shortly. 10 | 11 | In case you want to add a feature, please create a new issue and briefly explain what the feature would consist of. For bugs or requests, before creating an issue please check if one has already been created for it. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2024 Anthony Simon 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG = github.com/anthonynsimon/bild 2 | VERSION ?= dev 3 | LDFLAGS = -ldflags "-X $(PKG)/cmd.Version=$(VERSION) -extldflags \"-static\"" 4 | MAC_LDFLAGS = -ldflags "-X $(PKG)/cmd.Version=$(VERSION)" 5 | 6 | deps: 7 | go get ./... 8 | 9 | install: 10 | go install $(MAC_LDFLAGS) 11 | 12 | test: deps 13 | go test ./... -timeout 60s $(LDFLAGS) -v 14 | 15 | cover: deps 16 | go test ./... -race -v -timeout 15s -coverprofile=coverage.out 17 | go tool cover -html=coverage.out 18 | 19 | fmt: 20 | go fmt ./... 21 | 22 | bench: deps 23 | go test $(LDFLAGS) -benchmem -bench=. -benchtime=5s ./... 24 | 25 | race: deps 26 | go test ./... -v -race -timeout 15s 27 | 28 | release: release-x64 release-mac 29 | 30 | ensure-dist: deps 31 | mkdir -p dist 32 | 33 | release-bin: ensure-dist 34 | GOOS=linux GOARCH=amd64 go build -o dist/bild $(LDFLAGS) 35 | 36 | release-x64: ensure-dist 37 | GOOS=linux GOARCH=amd64 go build -o dist/bild $(LDFLAGS) && cd dist && tar -czf bild_$(VERSION)_x64.tar.gz bild && rm bild 38 | 39 | release-x86: ensure-dist 40 | GOOS=linux GOARCH=386 go build -o dist/bild $(LDFLAGS) && cd dist && tar -czf bild_$(VERSION)_x86.tar.gz bild && rm bild 41 | 42 | release-mac: ensure-dist 43 | go build $(MAC_LDFLAGS) -o dist/bild && cd dist && tar -czf bild_$(VERSION)_mac.tar.gz bild && rm bild 44 | -------------------------------------------------------------------------------- /adjust/adjustment.go: -------------------------------------------------------------------------------- 1 | /*Package adjust provides basic color correction functions.*/ 2 | package adjust 3 | 4 | import ( 5 | "image" 6 | "image/color" 7 | "math" 8 | 9 | "github.com/anthonynsimon/bild/math/f64" 10 | "github.com/anthonynsimon/bild/util" 11 | ) 12 | 13 | // Brightness returns a copy of the image with the adjusted brightness. 14 | // Change is the normalized amount of change to be applied (range -1.0 to 1.0). 15 | func Brightness(src image.Image, change float64) *image.RGBA { 16 | lookup := make([]uint8, 256) 17 | 18 | for i := 0; i < 256; i++ { 19 | lookup[i] = uint8(f64.Clamp(float64(i)*(1+change), 0, 255)) 20 | } 21 | 22 | fn := func(c color.RGBA) color.RGBA { 23 | return color.RGBA{lookup[c.R], lookup[c.G], lookup[c.B], c.A} 24 | } 25 | 26 | img := Apply(src, fn) 27 | 28 | return img 29 | } 30 | 31 | // Gamma returns a gamma corrected copy of the image. Provided gamma param must be larger than 0. 32 | func Gamma(src image.Image, gamma float64) *image.RGBA { 33 | gamma = math.Max(0.00001, gamma) 34 | 35 | lookup := make([]uint8, 256) 36 | 37 | for i := 0; i < 256; i++ { 38 | lookup[i] = uint8(f64.Clamp(math.Pow(float64(i)/255, 1.0/gamma)*255, 0, 255)) 39 | } 40 | 41 | fn := func(c color.RGBA) color.RGBA { 42 | return color.RGBA{lookup[c.R], lookup[c.G], lookup[c.B], c.A} 43 | } 44 | 45 | img := Apply(src, fn) 46 | 47 | return img 48 | } 49 | 50 | // Contrast returns a copy of the image with its difference in high and low values adjusted by the change param. 51 | // Change is the normalized amount of change to be applied, in the range of -1.0 to 1.0. 52 | // If Change is set to 0.0, then the values remain the same, if it's set to 0.5, then all values will be moved 50% away from the middle value. 53 | func Contrast(src image.Image, change float64) *image.RGBA { 54 | lookup := make([]uint8, 256) 55 | 56 | for i := 0; i < 256; i++ { 57 | lookup[i] = uint8(f64.Clamp(((((float64(i)/255)-0.5)*(1+change))+0.5)*255, 0, 255)) 58 | } 59 | 60 | fn := func(c color.RGBA) color.RGBA { 61 | return color.RGBA{lookup[c.R], lookup[c.G], lookup[c.B], c.A} 62 | } 63 | 64 | img := Apply(src, fn) 65 | 66 | return img 67 | } 68 | 69 | // Hue adjusts the overall hue of the provided image and returns the result. 70 | // Parameter change is the amount of change to be applied and is of the range 71 | // -360 to 360. It corresponds to the hue angle in the HSL color model. 72 | func Hue(img image.Image, change int) *image.RGBA { 73 | fn := func(c color.RGBA) color.RGBA { 74 | h, s, l := util.RGBToHSL(c) 75 | h = float64((int(h) + change) % 360) 76 | outColor := util.HSLToRGB(h, s, l) 77 | outColor.A = c.A 78 | return outColor 79 | } 80 | 81 | return Apply(img, fn) 82 | } 83 | 84 | // Saturation adjusts the saturation of the image and returns the result. 85 | // Parameter change is the amount of change to be applied and is of the range 86 | // -1.0 to 1.0 (-1.0 being -100% and 1.0 being 100%). 87 | func Saturation(img image.Image, change float64) *image.RGBA { 88 | fn := func(c color.RGBA) color.RGBA { 89 | h, s, l := util.RGBToHSL(c) 90 | s = f64.Clamp(s*(1+change), 0.0, 1.0) 91 | outColor := util.HSLToRGB(h, s, l) 92 | outColor.A = c.A 93 | return outColor 94 | } 95 | 96 | return Apply(img, fn) 97 | } 98 | -------------------------------------------------------------------------------- /adjust/apply.go: -------------------------------------------------------------------------------- 1 | package adjust 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/anthonynsimon/bild/clone" 8 | "github.com/anthonynsimon/bild/parallel" 9 | ) 10 | 11 | // Apply returns a copy of the provided image after applying the provided color function to each pixel. 12 | func Apply(img image.Image, fn func(color.RGBA) color.RGBA) *image.RGBA { 13 | bounds := img.Bounds() 14 | dst := clone.AsRGBA(img) 15 | w, h := bounds.Dx(), bounds.Dy() 16 | 17 | parallel.Line(h, func(start, end int) { 18 | for y := start; y < end; y++ { 19 | for x := 0; x < w; x++ { 20 | dstPos := y*dst.Stride + x*4 21 | 22 | c := color.RGBA{} 23 | 24 | dr := &dst.Pix[dstPos+0] 25 | dg := &dst.Pix[dstPos+1] 26 | db := &dst.Pix[dstPos+2] 27 | da := &dst.Pix[dstPos+3] 28 | 29 | c.R = *dr 30 | c.G = *dg 31 | c.B = *db 32 | c.A = *da 33 | 34 | c = fn(c) 35 | 36 | *dr = c.R 37 | *dg = c.G 38 | *db = c.B 39 | *da = c.A 40 | } 41 | } 42 | }) 43 | 44 | return dst 45 | } 46 | -------------------------------------------------------------------------------- /adjust/apply_test.go: -------------------------------------------------------------------------------- 1 | package adjust 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/anthonynsimon/bild/math/f64" 9 | "github.com/anthonynsimon/bild/util" 10 | ) 11 | 12 | func TestApply(t *testing.T) { 13 | cases := []struct { 14 | desc string 15 | fn func(color.RGBA) color.RGBA 16 | value image.Image 17 | expected *image.RGBA 18 | }{ 19 | { 20 | desc: "no change", 21 | fn: func(c color.RGBA) color.RGBA { 22 | return c 23 | }, 24 | value: &image.RGBA{ 25 | Rect: image.Rect(0, 0, 2, 2), 26 | Stride: 2 * 4, 27 | Pix: []uint8{ 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | }, 31 | }, 32 | expected: &image.RGBA{ 33 | Rect: image.Rect(0, 0, 2, 2), 34 | Stride: 2 * 4, 35 | Pix: []uint8{ 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | }, 39 | }, 40 | }, 41 | { 42 | desc: "plus 128", 43 | fn: func(c color.RGBA) color.RGBA { 44 | return color.RGBA{c.R + 128, c.G + 128, c.B + 128, c.A + 128} 45 | }, 46 | value: &image.RGBA{ 47 | Rect: image.Rect(0, 0, 2, 2), 48 | Stride: 2 * 4, 49 | Pix: []uint8{ 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | }, 53 | }, 54 | expected: &image.RGBA{ 55 | Rect: image.Rect(0, 0, 2, 2), 56 | Stride: 2 * 4, 57 | Pix: []uint8{ 58 | 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 59 | 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 60 | }, 61 | }, 62 | }, 63 | { 64 | desc: "minus 64", 65 | fn: func(c color.RGBA) color.RGBA { 66 | return color.RGBA{c.R - 64, c.G - 64, c.B - 64, c.A - 64} 67 | }, 68 | value: &image.RGBA{ 69 | Rect: image.Rect(0, 0, 2, 2), 70 | Stride: 2 * 4, 71 | Pix: []uint8{ 72 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 73 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 74 | }, 75 | }, 76 | expected: &image.RGBA{ 77 | Rect: image.Rect(0, 0, 2, 2), 78 | Stride: 2 * 4, 79 | Pix: []uint8{ 80 | 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 81 | 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 82 | }, 83 | }, 84 | }, 85 | } 86 | 87 | for _, c := range cases { 88 | actual := Apply(c.value, c.fn) 89 | if !util.RGBAImageEqual(actual, c.expected) { 90 | t.Errorf("%s: expected: %#v, actual: %#v", "apply "+c.desc, c.expected, actual) 91 | } 92 | } 93 | } 94 | 95 | func TestClampFloat64(t *testing.T) { 96 | cases := []struct { 97 | value float64 98 | expected float64 99 | }{ 100 | {-1.0, 0.0}, 101 | {1.0, 1.0}, 102 | {0.5, 0.5}, 103 | {1.01, 1.0}, 104 | {255.0, 1.0}, 105 | } 106 | 107 | for _, c := range cases { 108 | actual := f64.Clamp(c.value, 0.0, 1.0) 109 | if actual != c.expected { 110 | t.Errorf("%s: expected: %#v, actual: %#v", "clampFloat64", c.expected, actual) 111 | } 112 | } 113 | } 114 | 115 | func BenchmarkApply(b *testing.B) { 116 | val := &image.RGBA{ 117 | Rect: image.Rect(0, 0, 2, 2), 118 | Stride: 2 * 4, 119 | Pix: []uint8{ 120 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 121 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 122 | }, 123 | } 124 | fn := func(c color.RGBA) color.RGBA { 125 | return color.RGBA{c.R - 64, c.G - 64, c.B - 64, c.A - 64} 126 | } 127 | b.ResetTimer() 128 | for i := 0; i < b.N; i++ { 129 | Apply(val, fn) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /assets/img/add.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/add.jpg -------------------------------------------------------------------------------- /assets/img/boxblur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/boxblur.jpg -------------------------------------------------------------------------------- /assets/img/brightness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/brightness.jpg -------------------------------------------------------------------------------- /assets/img/colorburn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/colorburn.jpg -------------------------------------------------------------------------------- /assets/img/colordodge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/colordodge.jpg -------------------------------------------------------------------------------- /assets/img/contrast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/contrast.jpg -------------------------------------------------------------------------------- /assets/img/crop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/crop.jpg -------------------------------------------------------------------------------- /assets/img/darken.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/darken.jpg -------------------------------------------------------------------------------- /assets/img/difference.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/difference.jpg -------------------------------------------------------------------------------- /assets/img/dilate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/dilate.jpg -------------------------------------------------------------------------------- /assets/img/divide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/divide.jpg -------------------------------------------------------------------------------- /assets/img/edgedetection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/edgedetection.jpg -------------------------------------------------------------------------------- /assets/img/emboss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/emboss.jpg -------------------------------------------------------------------------------- /assets/img/erode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/erode.jpg -------------------------------------------------------------------------------- /assets/img/exclusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/exclusion.jpg -------------------------------------------------------------------------------- /assets/img/extractchannel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/extractchannel.jpg -------------------------------------------------------------------------------- /assets/img/fliph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/fliph.jpg -------------------------------------------------------------------------------- /assets/img/flipv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/flipv.jpg -------------------------------------------------------------------------------- /assets/img/floodfill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/floodfill.jpg -------------------------------------------------------------------------------- /assets/img/gamma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/gamma.jpg -------------------------------------------------------------------------------- /assets/img/gaussianblur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/gaussianblur.jpg -------------------------------------------------------------------------------- /assets/img/grayscale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/grayscale.jpg -------------------------------------------------------------------------------- /assets/img/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/histogram.png -------------------------------------------------------------------------------- /assets/img/hue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/hue.jpg -------------------------------------------------------------------------------- /assets/img/invert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/invert.jpg -------------------------------------------------------------------------------- /assets/img/lighten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/lighten.jpg -------------------------------------------------------------------------------- /assets/img/linearburn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/linearburn.jpg -------------------------------------------------------------------------------- /assets/img/linearlight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/linearlight.jpg -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/logo.png -------------------------------------------------------------------------------- /assets/img/median.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/median.jpg -------------------------------------------------------------------------------- /assets/img/multiply.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/multiply.jpg -------------------------------------------------------------------------------- /assets/img/noisebinary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/noisebinary.jpg -------------------------------------------------------------------------------- /assets/img/noisegaussian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/noisegaussian.jpg -------------------------------------------------------------------------------- /assets/img/noiseuniform.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/noiseuniform.jpg -------------------------------------------------------------------------------- /assets/img/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/normal.jpg -------------------------------------------------------------------------------- /assets/img/opacity.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/opacity.jpg -------------------------------------------------------------------------------- /assets/img/original.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/original.jpg -------------------------------------------------------------------------------- /assets/img/overlay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/overlay.jpg -------------------------------------------------------------------------------- /assets/img/perlin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/perlin.jpg -------------------------------------------------------------------------------- /assets/img/resizebox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizebox.jpg -------------------------------------------------------------------------------- /assets/img/resizecatmullrom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizecatmullrom.jpg -------------------------------------------------------------------------------- /assets/img/resizegaussian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizegaussian.jpg -------------------------------------------------------------------------------- /assets/img/resizelanczos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizelanczos.jpg -------------------------------------------------------------------------------- /assets/img/resizelinear.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizelinear.jpg -------------------------------------------------------------------------------- /assets/img/resizemitchell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizemitchell.jpg -------------------------------------------------------------------------------- /assets/img/resizenearestneighbor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/resizenearestneighbor.jpg -------------------------------------------------------------------------------- /assets/img/rotation01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/rotation01.gif -------------------------------------------------------------------------------- /assets/img/rotation02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/rotation02.gif -------------------------------------------------------------------------------- /assets/img/rotation03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/rotation03.gif -------------------------------------------------------------------------------- /assets/img/saturation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/saturation.jpg -------------------------------------------------------------------------------- /assets/img/screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/screen.jpg -------------------------------------------------------------------------------- /assets/img/sepia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/sepia.jpg -------------------------------------------------------------------------------- /assets/img/sharpen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/sharpen.jpg -------------------------------------------------------------------------------- /assets/img/shearh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/shearh.jpg -------------------------------------------------------------------------------- /assets/img/shearv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/shearv.jpg -------------------------------------------------------------------------------- /assets/img/sobel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/sobel.jpg -------------------------------------------------------------------------------- /assets/img/softlight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/softlight.jpg -------------------------------------------------------------------------------- /assets/img/subtract.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/subtract.jpg -------------------------------------------------------------------------------- /assets/img/threshold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/threshold.jpg -------------------------------------------------------------------------------- /assets/img/translate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/translate.jpg -------------------------------------------------------------------------------- /assets/img/unsharpmask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonynsimon/bild/676ea017b21def2a0edeffc3190f4cef6d545784/assets/img/unsharpmask.jpg -------------------------------------------------------------------------------- /benchmarks.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: amd64 3 | 4 | pkg: github.com/anthonynsimon/bild/adjust 5 | BenchmarkApply-8 25142976 235 ns/op 112 B/op 3 allocs/op 6 | 7 | pkg: github.com/anthonynsimon/bild/convolution 8 | BenchmarkConvolve3-8 172 34613389 ns/op 8413690 B/op 8 allocs/op 9 | BenchmarkConvolve8-8 32 170457685 ns/op 8462640 B/op 8 allocs/op 10 | BenchmarkConvolve32-8 2 2603227291 ns/op 8659248 B/op 8 allocs/op 11 | BenchmarkConvolve64-8 1 10181854106 ns/op 8929584 B/op 8 allocs/op 12 | 13 | pkg: github.com/anthonynsimon/bild/effect 14 | BenchmarkMedian1-8 1005 6275448 ns/op 3687014 B/op 65544 allocs/op 15 | BenchmarkMedian4-8 26 226515953 ns/op 23628461 B/op 65550 allocs/op 16 | BenchmarkMedian8-8 2 2524265186 ns/op 84585780 B/op 65577 allocs/op 17 | 18 | pkg: github.com/anthonynsimon/bild/noise 19 | BenchmarkUniformMonochrome-8 194 30926449 ns/op 1048921 B/op 4 allocs/op 20 | BenchmarkUniformColored-8 67 87401332 ns/op 1048712 B/op 4 allocs/op 21 | 22 | pkg: github.com/anthonynsimon/bild/paint 23 | BenchmarkFloodFill-8 82 71237537 ns/op 24616143 B/op 259073 allocs/op 24 | 25 | pkg: github.com/anthonynsimon/bild/transform 26 | BenchmarkResizeTenth-8 54 106483274 ns/op 7374290 B/op 10 allocs/op 27 | BenchmarkResizeQuarter-8 52 116070611 ns/op 20971935 B/op 8 allocs/op 28 | BenchmarkResizeHalf-8 37 162314807 ns/op 50332324 B/op 8 allocs/op 29 | BenchmarkResize1x-8 267 23252871 ns/op 8389000 B/op 8 allocs/op 30 | BenchmarkResize2x-8 94 59867822 ns/op 25166217 B/op 8 allocs/op 31 | BenchmarkResize4x-8 31 188098593 ns/op 83886558 B/op 8 allocs/op 32 | BenchmarkResize8x-8 8 666375788 ns/op 301990720 B/op 9 allocs/op 33 | BenchmarkResize16x-8 2 2523312624 ns/op 1140851040 B/op 8 allocs/op 34 | BenchmarkRotation256-8 1586 3397452 ns/op 1310965 B/op 196613 allocs/op 35 | BenchmarkRotation512-8 438 13462494 ns/op 5243236 B/op 786437 allocs/op 36 | BenchmarkRotation1024-8 121 49059303 ns/op 20971951 B/op 3145733 allocs/op 37 | BenchmarkRotation2048-8 30 201259553 ns/op 83886562 B/op 12582917 allocs/op 38 | BenchmarkRotation4096-8 7 788848699 ns/op 335545040 B/op 50331653 allocs/op 39 | BenchmarkRotation8192-8 2 3243539872 ns/op 1342178584 B/op 201326597 allocs/op 40 | BenchmarkTranslate-8 6219 932677 ns/op 4194449 B/op 4 allocs/op -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # usage: bin/release 1.2.3 3 | 4 | set -e 5 | 6 | if [[ $1 == "" ]]; then 7 | echo "No release version set" 8 | exit 1 9 | fi 10 | 11 | RELEASE=$1 12 | 13 | if ! [[ $RELEASE =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then 14 | echo "Release tag does not match expected pattern: vMAJOR.MINOR.PATCH" 15 | exit 1 16 | fi 17 | 18 | if ! [[ -z $(git status -s) ]]; then 19 | echo "You have uncommited or staged changes on git, please commit them or stash them" 20 | exit 1 21 | fi 22 | 23 | echo "Running tests" 24 | make test 25 | 26 | echo "Tagging and pushing release to upstream" 27 | git tag $RELEASE -m "Release $RELEASE, please check the changelog for more details" 28 | 29 | VERSION=$RELEASE make release 30 | echo "Release binaries located in ./dist" 31 | 32 | git push origin master --follow-tags 33 | echo "Successfully pushed release $RELEASE to upstream, you might still need to uplaod the release assets" 34 | -------------------------------------------------------------------------------- /blur/blur.go: -------------------------------------------------------------------------------- 1 | /*Package blur provides image blurring functions.*/ 2 | package blur 3 | 4 | import ( 5 | "image" 6 | "math" 7 | 8 | "github.com/anthonynsimon/bild/clone" 9 | "github.com/anthonynsimon/bild/convolution" 10 | ) 11 | 12 | // Box returns a blurred (average) version of the image. 13 | // Radius must be larger than 0. 14 | func Box(src image.Image, radius float64) *image.RGBA { 15 | if radius <= 0 { 16 | return clone.AsRGBA(src) 17 | } 18 | 19 | length := int(math.Ceil(2*radius + 1)) 20 | k := convolution.NewKernel(length, length) 21 | 22 | for x := 0; x < length; x++ { 23 | for y := 0; y < length; y++ { 24 | k.Matrix[y*length+x] = 1 25 | } 26 | } 27 | 28 | return convolution.Convolve(src, k.Normalized(), &convolution.Options{Bias: 0, Wrap: false, KeepAlpha: false}) 29 | } 30 | 31 | // Gaussian returns a smoothly blurred version of the image using 32 | // a Gaussian function. Radius must be larger than 0. 33 | func Gaussian(src image.Image, radius float64) *image.RGBA { 34 | if radius <= 0 { 35 | return clone.AsRGBA(src) 36 | } 37 | 38 | // Create the 1-d gaussian kernel 39 | length := int(math.Ceil(2*radius + 1)) 40 | k := convolution.NewKernel(length, 1) 41 | for i, x := 0, -radius; i < length; i, x = i+1, x+1 { 42 | k.Matrix[i] = math.Exp(-(x * x / 4 / radius)) 43 | } 44 | normK := k.Normalized() 45 | 46 | // Perform separable convolution 47 | options := convolution.Options{Bias: 0, Wrap: false, KeepAlpha: false} 48 | result := convolution.Convolve(src, normK, &options) 49 | result = convolution.Convolve(result, normK.Transposed(), &options) 50 | 51 | return result 52 | } 53 | -------------------------------------------------------------------------------- /blur/blur_test.go: -------------------------------------------------------------------------------- 1 | package blur 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/util" 8 | ) 9 | 10 | func TestBoxBlur(t *testing.T) { 11 | cases := []struct { 12 | radius float64 13 | value image.Image 14 | expected *image.RGBA 15 | }{ 16 | { 17 | radius: 0.0, 18 | value: &image.RGBA{ 19 | Rect: image.Rect(0, 0, 3, 3), 20 | Stride: 3 * 4, 21 | Pix: []uint8{ 22 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 23 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 24 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 25 | }, 26 | }, 27 | expected: &image.RGBA{ 28 | Rect: image.Rect(0, 0, 3, 3), 29 | Stride: 3 * 4, 30 | Pix: []uint8{ 31 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 32 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 33 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 34 | }, 35 | }, 36 | }, 37 | { 38 | radius: 1.0, 39 | value: &image.RGBA{ 40 | Rect: image.Rect(0, 0, 3, 3), 41 | Stride: 3 * 4, 42 | Pix: []uint8{ 43 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 44 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 45 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 46 | }, 47 | }, 48 | expected: &image.RGBA{ 49 | Rect: image.Rect(0, 0, 3, 3), 50 | Stride: 3 * 4, 51 | Pix: []uint8{ 52 | 0xaa, 0xaa, 0xaa, 0xff, 0xb8, 0xb8, 0xb8, 0xff, 0xc6, 0xc6, 0xc6, 0xff, 53 | 0x55, 0x55, 0x55, 0xff, 0x71, 0x71, 0x71, 0xff, 0x8d, 0x8d, 0x8d, 0xff, 54 | 0x0, 0x0, 0x0, 0xff, 0x2a, 0x2a, 0x2a, 0xff, 0x55, 0x55, 0x55, 0xff, 55 | }, 56 | }, 57 | }, 58 | { 59 | radius: 1.0, 60 | value: &image.RGBA{ 61 | Rect: image.Rect(0, 0, 3, 3), 62 | Stride: 3 * 4, 63 | Pix: []uint8{ 64 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 65 | 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 66 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 67 | }, 68 | }, 69 | expected: &image.RGBA{ 70 | Rect: image.Rect(0, 0, 3, 3), 71 | Stride: 3 * 4, 72 | Pix: []uint8{ 73 | 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 74 | 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 75 | 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 0x1c, 0x00, 0x00, 0xff, 76 | }, 77 | }, 78 | }, 79 | } 80 | 81 | for _, c := range cases { 82 | actual := Box(c.value, c.radius) 83 | if !util.RGBAImageEqual(actual, c.expected) { 84 | t.Errorf("%s: expected: %#v, actual: %#v", "BoxBlur", c.expected, actual) 85 | } 86 | } 87 | } 88 | 89 | func TestGaussianBlur(t *testing.T) { 90 | cases := []struct { 91 | radius float64 92 | value image.Image 93 | expected *image.RGBA 94 | }{ 95 | { 96 | radius: 0.0, 97 | value: &image.RGBA{ 98 | Rect: image.Rect(0, 0, 3, 3), 99 | Stride: 3 * 4, 100 | Pix: []uint8{ 101 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 102 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 103 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 104 | }, 105 | }, 106 | expected: &image.RGBA{ 107 | Rect: image.Rect(0, 0, 3, 3), 108 | Stride: 3 * 4, 109 | Pix: []uint8{ 110 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 111 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 112 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 113 | }, 114 | }, 115 | }, 116 | { 117 | radius: 1.0, 118 | value: &image.RGBA{ 119 | Rect: image.Rect(0, 0, 3, 3), 120 | Stride: 3 * 4, 121 | Pix: []uint8{ 122 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 123 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 124 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 125 | }, 126 | }, 127 | expected: &image.RGBA{ 128 | Rect: image.Rect(0, 0, 3, 3), 129 | Stride: 3 * 4, 130 | Pix: []uint8{ 131 | 0xb1, 0xb1, 0xb1, 0xff, 0xbc, 0xbc, 0xbc, 0xff, 0xcc, 0xcc, 0xcc, 0xff, 132 | 0x4d, 0x4d, 0x4d, 0xff, 0x68, 0x68, 0x68, 0xff, 0x8b, 0x8b, 0x8b, 0xff, 133 | 0x0, 0x0, 0x0, 0xff, 0x25, 0x25, 0x25, 0xff, 0x58, 0x58, 0x58, 0xff, 134 | }, 135 | }, 136 | }, 137 | { 138 | radius: 1.0, 139 | value: &image.RGBA{ 140 | Rect: image.Rect(0, 0, 3, 3), 141 | Stride: 3 * 4, 142 | Pix: []uint8{ 143 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 144 | 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 145 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 146 | }, 147 | }, 148 | expected: &image.RGBA{ 149 | Rect: image.Rect(0, 0, 3, 3), 150 | Stride: 3 * 4, 151 | Pix: []uint8{ 152 | 0x17, 0x00, 0x00, 0xff, 0x1e, 0x00, 0x00, 0xff, 0x17, 0x00, 0x00, 0xff, 153 | 0x1e, 0x00, 0x00, 0xff, 0x26, 0x00, 0x00, 0xff, 0x1e, 0x00, 0x00, 0xff, 154 | 0x17, 0x00, 0x00, 0xff, 0x1e, 0x00, 0x00, 0xff, 0x17, 0x00, 0x00, 0xff, 155 | }, 156 | }, 157 | }, 158 | } 159 | 160 | for i, c := range cases { 161 | actual := Gaussian(c.value, c.radius) 162 | if !util.RGBAImageEqual(actual, c.expected) { 163 | t.Errorf("%s %d: expected: %#v, actual: %#v", "GaussianBlur", i, c.expected, actual) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /channel/channel.go: -------------------------------------------------------------------------------- 1 | /*Package channel provides image channel separation and manipulation functions.*/ 2 | package channel 3 | 4 | import ( 5 | "fmt" 6 | "image" 7 | 8 | "github.com/anthonynsimon/bild/clone" 9 | "github.com/anthonynsimon/bild/parallel" 10 | ) 11 | 12 | // Channel identifier for RGBA images 13 | type Channel int 14 | 15 | // Channel identifiers 16 | const ( 17 | Red = iota 18 | Green 19 | Blue 20 | Alpha 21 | ) 22 | 23 | var ( 24 | allChannels = []Channel{Red, Green, Blue, Alpha} 25 | ) 26 | 27 | // ExtractMultiple returns a RGBA image containing the values of the selected channels. 28 | // 29 | // Usage example: 30 | // 31 | // result := channel.ExtractMultiple(img, channel.Blue, channel.Alpha) 32 | func ExtractMultiple(img image.Image, channels ...Channel) *image.RGBA { 33 | for _, c := range channels { 34 | if c < 0 || 3 < c { 35 | panic(fmt.Sprintf("channel index '%v' out of bounds. Red: 0, Green: 1, Blue: 2, Alpha: 3", c)) 36 | } 37 | } 38 | 39 | dst := clone.AsRGBA(img) 40 | bounds := dst.Bounds() 41 | dstW, dstH := bounds.Dx(), bounds.Dy() 42 | 43 | if bounds.Empty() { 44 | return &image.RGBA{} 45 | } 46 | 47 | channelsToRemove := []Channel{} 48 | for _, channel := range allChannels { 49 | shouldRemove := true 50 | for _, enabled := range channels { 51 | if enabled == channel { 52 | shouldRemove = false 53 | break 54 | } 55 | } 56 | if shouldRemove { 57 | channelsToRemove = append(channelsToRemove, channel) 58 | } 59 | } 60 | 61 | parallel.Line(dstH, func(start, end int) { 62 | for y := start; y < end; y++ { 63 | for x := 0; x < dstW; x++ { 64 | pos := y*dst.Stride + x*4 65 | for _, c := range channelsToRemove { 66 | dst.Pix[pos+int(c)] = 0x00 67 | } 68 | } 69 | } 70 | }) 71 | 72 | return dst 73 | } 74 | 75 | // Extract returns a grayscale image containing the values of the selected channel. 76 | // 77 | // Usage example: 78 | // 79 | // result := channel.Extract(img, channel.Alpha) 80 | func Extract(img image.Image, c Channel) *image.Gray { 81 | if c < 0 || 3 < c { 82 | panic(fmt.Sprintf("channel index '%v' out of bounds. Red: 0, Green: 1, Blue: 2, Alpha: 3", c)) 83 | } 84 | 85 | src := clone.AsRGBA(img) 86 | bounds := src.Bounds() 87 | srcW, srcH := bounds.Dx(), bounds.Dy() 88 | 89 | if bounds.Empty() { 90 | return &image.Gray{} 91 | } 92 | 93 | dst := image.NewGray(bounds) 94 | 95 | parallel.Line(srcH, func(start, end int) { 96 | for y := start; y < end; y++ { 97 | for x := 0; x < srcW; x++ { 98 | srcPos := y*src.Stride + x*4 99 | dstPos := y*dst.Stride + x 100 | 101 | dst.Pix[dstPos] = src.Pix[srcPos+int(c)] 102 | } 103 | } 104 | }) 105 | 106 | return dst 107 | } 108 | -------------------------------------------------------------------------------- /channel/channel_test.go: -------------------------------------------------------------------------------- 1 | package channel 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/util" 8 | ) 9 | 10 | func TestExtractMultiple(t *testing.T) { 11 | cases := []struct { 12 | description string 13 | channels []Channel 14 | img image.Image 15 | expected *image.RGBA 16 | }{ 17 | { 18 | description: "red empty image", 19 | channels: []Channel{Red}, 20 | img: &image.RGBA{}, 21 | expected: &image.RGBA{}, 22 | }, 23 | { 24 | description: "red empty pix", 25 | channels: []Channel{Red}, 26 | img: &image.RGBA{ 27 | Rect: image.Rect(0, 0, 0, 0), 28 | Stride: 0 * 4, 29 | Pix: []uint8{}, 30 | }, 31 | expected: &image.RGBA{}, 32 | }, 33 | { 34 | description: "red single pixel", 35 | channels: []Channel{Red}, 36 | img: &image.RGBA{ 37 | Rect: image.Rect(0, 0, 1, 1), 38 | Stride: 1 * 4, 39 | Pix: []uint8{ 40 | 0x20, 0x60, 0x90, 0xFF, 41 | }, 42 | }, 43 | expected: &image.RGBA{ 44 | Rect: image.Rect(0, 0, 1, 1), 45 | Stride: 1, 46 | Pix: []uint8{ 47 | 0x20, 0x00, 0x00, 0x00, 48 | }}, 49 | }, 50 | { 51 | description: "green single pixel", 52 | channels: []Channel{Green}, 53 | img: &image.RGBA{ 54 | Rect: image.Rect(0, 0, 1, 1), 55 | Stride: 1 * 4, 56 | Pix: []uint8{ 57 | 0x20, 0x60, 0x90, 0xFF, 58 | }, 59 | }, 60 | expected: &image.RGBA{ 61 | Rect: image.Rect(0, 0, 1, 1), 62 | Stride: 1, 63 | Pix: []uint8{ 64 | 0x00, 0x60, 0x00, 0x00, 65 | }}, 66 | }, 67 | { 68 | description: "blue single pixel", 69 | channels: []Channel{Blue}, 70 | img: &image.RGBA{ 71 | Rect: image.Rect(0, 0, 1, 1), 72 | Stride: 1 * 4, 73 | Pix: []uint8{ 74 | 0x20, 0x60, 0x90, 0xFF, 75 | }, 76 | }, 77 | expected: &image.RGBA{ 78 | Rect: image.Rect(0, 0, 1, 1), 79 | Stride: 1, 80 | Pix: []uint8{ 81 | 0x00, 0x00, 0x90, 0x00, 82 | }}, 83 | }, 84 | { 85 | description: "alpha single pixel", 86 | channels: []Channel{Alpha}, 87 | img: &image.RGBA{ 88 | Rect: image.Rect(0, 0, 1, 1), 89 | Stride: 1 * 4, 90 | Pix: []uint8{ 91 | 0x20, 0x60, 0x90, 0xFF, 92 | }, 93 | }, 94 | expected: &image.RGBA{ 95 | Rect: image.Rect(0, 0, 1, 1), 96 | Stride: 1, 97 | Pix: []uint8{ 98 | 0x00, 0x00, 0x00, 0xFF, 99 | }}, 100 | }, 101 | { 102 | description: "multiple single pixel", 103 | channels: []Channel{Red, Alpha}, 104 | img: &image.RGBA{ 105 | Rect: image.Rect(0, 0, 1, 1), 106 | Stride: 1 * 4, 107 | Pix: []uint8{ 108 | 0x20, 0x60, 0x90, 0xFF, 109 | }, 110 | }, 111 | expected: &image.RGBA{ 112 | Rect: image.Rect(0, 0, 1, 1), 113 | Stride: 1, 114 | Pix: []uint8{ 115 | 0x20, 0x00, 0x00, 0xFF, 116 | }}, 117 | }, 118 | } 119 | 120 | for _, c := range cases { 121 | actual := ExtractMultiple(c.img, c.channels...) 122 | if !util.RGBAImageEqual(actual, c.expected) { 123 | t.Errorf("%s: expected: %#v, actual %#v", "Extract "+c.description, c.expected, actual) 124 | } 125 | } 126 | } 127 | 128 | func TestExtract(t *testing.T) { 129 | cases := []struct { 130 | description string 131 | channel Channel 132 | img image.Image 133 | expected *image.Gray 134 | }{ 135 | { 136 | description: "red empty image", 137 | channel: Red, 138 | img: &image.RGBA{}, 139 | expected: &image.Gray{}, 140 | }, 141 | { 142 | description: "red empty pix", 143 | channel: Red, 144 | img: &image.RGBA{ 145 | Rect: image.Rect(0, 0, 0, 0), 146 | Stride: 0 * 4, 147 | Pix: []uint8{}, 148 | }, 149 | expected: &image.Gray{}, 150 | }, 151 | { 152 | description: "red single pixel", 153 | channel: Red, 154 | img: &image.RGBA{ 155 | Rect: image.Rect(0, 0, 1, 1), 156 | Stride: 1 * 4, 157 | Pix: []uint8{ 158 | 0x20, 0x60, 0x90, 0xFF, 159 | }, 160 | }, 161 | expected: &image.Gray{ 162 | Rect: image.Rect(0, 0, 1, 1), 163 | Stride: 1, 164 | Pix: []uint8{ 165 | 0x20, 166 | }}, 167 | }, 168 | { 169 | description: "green single pixel", 170 | channel: Green, 171 | img: &image.RGBA{ 172 | Rect: image.Rect(0, 0, 1, 1), 173 | Stride: 1 * 4, 174 | Pix: []uint8{ 175 | 0x20, 0x60, 0x90, 0xFF, 176 | }, 177 | }, 178 | expected: &image.Gray{ 179 | Rect: image.Rect(0, 0, 1, 1), 180 | Stride: 1, 181 | Pix: []uint8{ 182 | 0x60, 183 | }}, 184 | }, 185 | { 186 | description: "blue single pixel", 187 | channel: Blue, 188 | img: &image.RGBA{ 189 | Rect: image.Rect(0, 0, 1, 1), 190 | Stride: 1 * 4, 191 | Pix: []uint8{ 192 | 0x20, 0x60, 0x90, 0xFF, 193 | }, 194 | }, 195 | expected: &image.Gray{ 196 | Rect: image.Rect(0, 0, 1, 1), 197 | Stride: 1, 198 | Pix: []uint8{ 199 | 0x90, 200 | }}, 201 | }, 202 | { 203 | description: "alpha single pixel", 204 | channel: Alpha, 205 | img: &image.RGBA{ 206 | Rect: image.Rect(0, 0, 1, 1), 207 | Stride: 1 * 4, 208 | Pix: []uint8{ 209 | 0x20, 0x60, 0x90, 0xFF, 210 | }, 211 | }, 212 | expected: &image.Gray{ 213 | Rect: image.Rect(0, 0, 1, 1), 214 | Stride: 1, 215 | Pix: []uint8{ 216 | 0xFF, 217 | }}, 218 | }, 219 | } 220 | 221 | for _, c := range cases { 222 | actual := Extract(c.img, c.channel) 223 | if !util.GrayImageEqual(actual, c.expected) { 224 | t.Errorf("%s: expected: %#v, actual %#v", "Extract "+c.description, c.expected, actual) 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /clone/clone.go: -------------------------------------------------------------------------------- 1 | /*Package clone provides image cloning function.*/ 2 | package clone 3 | 4 | import ( 5 | "image" 6 | "image/draw" 7 | 8 | "github.com/anthonynsimon/bild/parallel" 9 | ) 10 | 11 | // PadMethod is the method used to fill padded pixels. 12 | type PadMethod uint8 13 | 14 | const ( 15 | // NoFill leaves the padded pixels empty. 16 | NoFill = iota 17 | // EdgeExtend extends the closest edge pixel. 18 | EdgeExtend 19 | // EdgeWrap wraps around the pixels of an image. 20 | EdgeWrap 21 | ) 22 | 23 | // AsRGBA returns an RGBA copy of the supplied image. 24 | func AsRGBA(src image.Image) *image.RGBA { 25 | bounds := src.Bounds() 26 | img := image.NewRGBA(bounds) 27 | draw.Draw(img, bounds, src, bounds.Min, draw.Src) 28 | return img 29 | } 30 | 31 | // AsShallowRGBA tries to cast to image.RGBA to get reference. Otherwise makes a copy 32 | func AsShallowRGBA(src image.Image) *image.RGBA { 33 | if rgba, ok := src.(*image.RGBA); ok { 34 | return rgba 35 | } 36 | return AsRGBA(src) 37 | } 38 | 39 | // Pad returns an RGBA copy of the src image parameter with its edges padded 40 | // using the supplied PadMethod. 41 | // Parameter padX and padY correspond to the amount of padding to be applied 42 | // on each side. 43 | // Parameter m is the PadMethod to fill the new pixels. 44 | // 45 | // Usage example: 46 | // 47 | // result := Pad(img, 5,5, EdgeExtend) 48 | func Pad(src image.Image, padX, padY int, m PadMethod) *image.RGBA { 49 | var result *image.RGBA 50 | 51 | switch m { 52 | case EdgeExtend: 53 | result = extend(src, padX, padY) 54 | case NoFill: 55 | result = noFill(src, padX, padY) 56 | case EdgeWrap: 57 | result = wrap(src, padX, padY) 58 | default: 59 | result = extend(src, padX, padY) 60 | } 61 | 62 | return result 63 | } 64 | 65 | func noFill(img image.Image, padX, padY int) *image.RGBA { 66 | srcBounds := img.Bounds() 67 | paddedW, paddedH := srcBounds.Dx()+2*padX, srcBounds.Dy()+2*padY 68 | newBounds := image.Rect(0, 0, paddedW, paddedH) 69 | fillBounds := image.Rect(padX, padY, padX+srcBounds.Dx(), padY+srcBounds.Dy()) 70 | 71 | dst := image.NewRGBA(newBounds) 72 | draw.Draw(dst, fillBounds, img, srcBounds.Min, draw.Src) 73 | 74 | return dst 75 | } 76 | 77 | func extend(img image.Image, padX, padY int) *image.RGBA { 78 | dst := noFill(img, padX, padY) 79 | paddedW, paddedH := dst.Bounds().Dx(), dst.Bounds().Dy() 80 | 81 | parallel.Line(paddedH, func(start, end int) { 82 | for y := start; y < end; y++ { 83 | iy := y 84 | if iy < padY { 85 | iy = padY 86 | } else if iy >= paddedH-padY { 87 | iy = paddedH - padY - 1 88 | } 89 | 90 | for x := 0; x < paddedW; x++ { 91 | ix := x 92 | if ix < padX { 93 | ix = padX 94 | } else if x >= paddedW-padX { 95 | ix = paddedW - padX - 1 96 | } else if iy == y { 97 | // This only enters if we are not in a y-padded area or 98 | // x-padded area, so nothing to extend here. 99 | // So simply jump to the next padded-x index. 100 | x = paddedW - padX - 1 101 | continue 102 | } 103 | 104 | dstPos := y*dst.Stride + x*4 105 | edgePos := iy*dst.Stride + ix*4 106 | 107 | dst.Pix[dstPos+0] = dst.Pix[edgePos+0] 108 | dst.Pix[dstPos+1] = dst.Pix[edgePos+1] 109 | dst.Pix[dstPos+2] = dst.Pix[edgePos+2] 110 | dst.Pix[dstPos+3] = dst.Pix[edgePos+3] 111 | } 112 | } 113 | }) 114 | 115 | return dst 116 | } 117 | 118 | func wrap(img image.Image, padX, padY int) *image.RGBA { 119 | dst := noFill(img, padX, padY) 120 | paddedW, paddedH := dst.Bounds().Dx(), dst.Bounds().Dy() 121 | 122 | parallel.Line(paddedH, func(start, end int) { 123 | for y := start; y < end; y++ { 124 | iy := y 125 | if iy < padY { 126 | iy = (paddedH - padY) - ((padY - y) % (paddedH - padY*2)) 127 | } else if iy >= paddedH-padY { 128 | iy = padY - ((padY - y) % (paddedH - padY*2)) 129 | } 130 | 131 | for x := 0; x < paddedW; x++ { 132 | ix := x 133 | if ix < padX { 134 | ix = (paddedW - padX) - ((padX - x) % (paddedW - padX*2)) 135 | } else if ix >= paddedW-padX { 136 | ix = padX - ((padX - x) % (paddedW - padX*2)) 137 | } else if iy == y { 138 | // This only enters if we are not in a y-padded area or 139 | // x-padded area, so nothing to extend here. 140 | // So simply jump to the next padded-x index. 141 | x = paddedW - padX - 1 142 | continue 143 | } 144 | 145 | dstPos := y*dst.Stride + x*4 146 | edgePos := iy*dst.Stride + ix*4 147 | 148 | dst.Pix[dstPos+0] = dst.Pix[edgePos+0] 149 | dst.Pix[dstPos+1] = dst.Pix[edgePos+1] 150 | dst.Pix[dstPos+2] = dst.Pix[edgePos+2] 151 | dst.Pix[dstPos+3] = dst.Pix[edgePos+3] 152 | } 153 | } 154 | }) 155 | 156 | return dst 157 | } 158 | -------------------------------------------------------------------------------- /clone/clone_test.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/anthonynsimon/bild/util" 9 | ) 10 | 11 | func TestCloneAsRGBA(t *testing.T) { 12 | cases := []struct { 13 | desc string 14 | value image.Image 15 | expected *image.RGBA 16 | }{ 17 | { 18 | desc: "RGBA", 19 | value: &image.RGBA{ 20 | Rect: image.Rect(0, 0, 1, 2), 21 | Stride: 4, 22 | Pix: []uint8{ 23 | 0x80, 0x80, 0x80, 0x80, 24 | 0x80, 0x80, 0x80, 0x80, 25 | }, 26 | }, 27 | expected: &image.RGBA{ 28 | Rect: image.Rect(0, 0, 1, 2), 29 | Stride: 4, 30 | Pix: []uint8{ 31 | 0x80, 0x80, 0x80, 0x80, 32 | 0x80, 0x80, 0x80, 0x80, 33 | }, 34 | }, 35 | }, 36 | { 37 | desc: "RGBA64", 38 | value: &image.RGBA64{ 39 | Rect: image.Rect(0, 0, 1, 2), 40 | Stride: 8, 41 | Pix: []uint8{ 42 | 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 43 | 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 44 | }, 45 | }, 46 | expected: &image.RGBA{ 47 | Rect: image.Rect(0, 0, 1, 2), 48 | Stride: 4, 49 | Pix: []uint8{ 50 | 0x80, 0x80, 0x80, 0x80, 51 | 0x80, 0x80, 0x80, 0x80, 52 | }, 53 | }, 54 | }, 55 | { 56 | desc: "NRGBA", 57 | value: &image.NRGBA{ 58 | Rect: image.Rect(0, 0, 1, 2), 59 | Stride: 4, 60 | Pix: []uint8{ 61 | 0xFF, 0xFF, 0xFF, 0x80, 62 | 0xFF, 0xFF, 0xFF, 0x80, 63 | }, 64 | }, 65 | expected: &image.RGBA{ 66 | Rect: image.Rect(0, 0, 1, 2), 67 | Stride: 4, 68 | Pix: []uint8{ 69 | 0x80, 0x80, 0x80, 0x80, 70 | 0x80, 0x80, 0x80, 0x80, 71 | }, 72 | }, 73 | }, 74 | { 75 | desc: "NRGBA64", 76 | value: &image.NRGBA{ 77 | Rect: image.Rect(0, 0, 1, 2), 78 | Stride: 8, 79 | Pix: []uint8{ 80 | 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0x80, 81 | 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0x80, 82 | }, 83 | }, 84 | expected: &image.RGBA{ 85 | Rect: image.Rect(0, 0, 1, 2), 86 | Stride: 4, 87 | Pix: []uint8{ 88 | 0x80, 0x80, 0x80, 0x80, 89 | 0x80, 0x80, 0x80, 0x80, 90 | }, 91 | }, 92 | }, 93 | { 94 | desc: "Gray", 95 | value: &image.Gray{ 96 | Rect: image.Rect(0, 0, 1, 2), 97 | Stride: 2, 98 | Pix: []uint8{ 99 | 0x80, 0x80, 100 | 0x80, 0x80, 101 | }, 102 | }, 103 | expected: &image.RGBA{ 104 | Rect: image.Rect(0, 0, 1, 2), 105 | Stride: 4, 106 | Pix: []uint8{ 107 | 0x80, 0x80, 0x80, 0xFF, 108 | 0x80, 0x80, 0x80, 0xFF, 109 | }, 110 | }, 111 | }, 112 | { 113 | desc: "Gray16", 114 | value: &image.Gray16{ 115 | Rect: image.Rect(0, 0, 1, 2), 116 | Stride: 2, 117 | Pix: []uint8{ 118 | 0x80, 0x80, 119 | 0x80, 0x80, 120 | }, 121 | }, 122 | expected: &image.RGBA{ 123 | Rect: image.Rect(0, 0, 1, 2), 124 | Stride: 4, 125 | Pix: []uint8{ 126 | 0x80, 0x80, 0x80, 0xFF, 127 | 0x80, 0x80, 0x80, 0xFF, 128 | }, 129 | }, 130 | }, 131 | { 132 | desc: "Alpha", 133 | value: &image.Alpha{ 134 | Rect: image.Rect(0, 0, 1, 2), 135 | Stride: 1, 136 | Pix: []uint8{ 137 | 0x80, 138 | 0x80, 139 | }, 140 | }, 141 | expected: &image.RGBA{ 142 | Rect: image.Rect(0, 0, 1, 2), 143 | Stride: 4, 144 | Pix: []uint8{ 145 | 0x80, 0x80, 0x80, 0x80, 146 | 0x80, 0x80, 0x80, 0x80, 147 | }, 148 | }, 149 | }, 150 | { 151 | desc: "Alpha16", 152 | value: &image.Alpha16{ 153 | Rect: image.Rect(0, 0, 1, 2), 154 | Stride: 1, 155 | Pix: []uint8{ 156 | 0x80, 0x80, 157 | 0x80, 0x80, 158 | }, 159 | }, 160 | expected: &image.RGBA{ 161 | Rect: image.Rect(0, 0, 1, 2), 162 | Stride: 4, 163 | Pix: []uint8{ 164 | 0x80, 0x80, 0x80, 0x80, 165 | 0x80, 0x80, 0x80, 0x80, 166 | }, 167 | }, 168 | }, 169 | { 170 | desc: "Paletted", 171 | value: &image.Paletted{ 172 | Rect: image.Rect(0, 0, 1, 2), 173 | Stride: 1, 174 | Palette: color.Palette{ 175 | color.RGBA{0x00, 0x00, 0x00, 0x00}, 176 | color.RGBA{0x80, 0x80, 0x80, 0x80}, 177 | color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, 178 | }, 179 | Pix: []uint8{ 180 | 0x1, 0x2, 181 | }, 182 | }, 183 | expected: &image.RGBA{ 184 | Rect: image.Rect(0, 0, 1, 2), 185 | Stride: 4, 186 | Pix: []uint8{ 187 | 0x80, 0x80, 0x80, 0x80, 188 | 0xFF, 0xFF, 0xFF, 0xFF, 189 | }, 190 | }, 191 | }, 192 | } 193 | 194 | for _, c := range cases { 195 | actual := AsRGBA(c.value) 196 | if !util.RGBAImageEqual(actual, c.expected) { 197 | t.Errorf("%s: expected: %#v, actual: %#v", "CloneAsRGBA from "+c.desc, c.expected, actual) 198 | } 199 | } 200 | 201 | //shallow copy should work the same 202 | for _, c := range cases { 203 | actual := AsShallowRGBA(c.value) 204 | if !util.RGBAImageEqual(actual, c.expected) { 205 | t.Errorf("%s: expected: %#v, actual: %#v", "CloneAsRGBA from "+c.desc, c.expected, actual) 206 | } 207 | } 208 | } 209 | 210 | func TestShallowRGBAReturnRef(t *testing.T) { 211 | src := &image.RGBA{ 212 | Rect: image.Rect(0, 0, 1, 2), 213 | Stride: 4, 214 | Pix: []uint8{ 215 | 0x80, 0x80, 0x80, 0x80, 216 | 0xFF, 0xFF, 0xFF, 0xFF, 217 | }, 218 | } 219 | 220 | if copyRef := AsShallowRGBA(src); copyRef != src { 221 | t.Errorf("ShallowRGBA should return the same ref (src=%p, copy=%p)", src, copyRef) 222 | } 223 | } 224 | 225 | func TestPad(t *testing.T) { 226 | cases := []struct { 227 | desc string 228 | method PadMethod 229 | x, y int 230 | value image.Image 231 | expected *image.RGBA 232 | }{ 233 | { 234 | desc: "No fill", 235 | method: NoFill, 236 | x: 2, 237 | y: 1, 238 | value: &image.RGBA{ 239 | Rect: image.Rect(0, 0, 1, 2), 240 | Stride: 4, 241 | Pix: []uint8{ 242 | 0x80, 0x80, 0x80, 0xFF, 243 | 0x40, 0x40, 0x40, 0xFF, 244 | }, 245 | }, 246 | expected: &image.RGBA{ 247 | Rect: image.Rect(0, 0, 5, 4), 248 | Stride: 5 * 4, 249 | Pix: []uint8{ 250 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 251 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 252 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 253 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 254 | }, 255 | }, 256 | }, 257 | { 258 | desc: "Edge Extend", 259 | method: EdgeExtend, 260 | x: 1, 261 | y: 1, 262 | value: &image.RGBA{ 263 | Rect: image.Rect(0, 0, 2, 2), 264 | Stride: 2 * 4, 265 | Pix: []uint8{ 266 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 267 | 0x40, 0x40, 0x40, 0xFF, 0x10, 0x10, 0x10, 0xFF, 268 | }, 269 | }, 270 | expected: &image.RGBA{ 271 | Rect: image.Rect(0, 0, 4, 4), 272 | Stride: 4 * 4, 273 | Pix: []uint8{ 274 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x80, 0x80, 0x80, 0xFF, 275 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x80, 0x80, 0x80, 0xFF, 276 | 0x40, 0x40, 0x40, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x10, 0x10, 0x10, 0xFF, 0x10, 0x10, 0x10, 0xFF, 277 | 0x40, 0x40, 0x40, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x10, 0x10, 0x10, 0xFF, 0x10, 0x10, 0x10, 0xFF, 278 | }, 279 | }, 280 | }, 281 | { 282 | desc: "Edge Wrap", 283 | method: EdgeWrap, 284 | x: 1, 285 | y: 1, 286 | value: &image.RGBA{ 287 | Rect: image.Rect(0, 0, 2, 2), 288 | Stride: 4, 289 | Pix: []uint8{ 290 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 291 | 0x40, 0x40, 0x40, 0xFF, 0x10, 0x10, 0x10, 0xFF, 292 | }, 293 | }, 294 | expected: &image.RGBA{ 295 | Rect: image.Rect(0, 0, 4, 4), 296 | Stride: 4 * 4, 297 | Pix: []uint8{ 298 | 0x40, 0x40, 0x40, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x80, 0x80, 0x80, 0xFF, 299 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 300 | 0x40, 0x40, 0x40, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x80, 0x80, 0x80, 0xFF, 301 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 302 | }, 303 | }, 304 | }, 305 | } 306 | 307 | for _, c := range cases { 308 | actual := Pad(c.value, c.x, c.y, c.method) 309 | if !util.RGBAImageEqual(actual, c.expected) { 310 | t.Errorf("%s:\nexpected:%v\nactual:%v", "Pad "+c.desc, util.RGBAToString(c.expected), util.RGBAToString(actual)) 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /cmd/adjust.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/anthonynsimon/bild/adjust" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func brightness() *cobra.Command { 12 | var change float64 13 | 14 | var cmd = &cobra.Command{ 15 | Use: "brightness", 16 | Short: "adjust the relative brightness of an image", 17 | Args: cobra.ExactArgs(2), 18 | Example: "brightness --change 0.5 input.jpg output.jpg", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fin := args[0] 21 | fout := args[1] 22 | 23 | apply(fin, fout, func(img image.Image) (image.Image, error) { 24 | return adjust.Brightness(img, change), nil 25 | }) 26 | }} 27 | 28 | cmd.Flags().Float64VarP(&change, "change", "c", 0, "adjust change") 29 | 30 | return cmd 31 | } 32 | 33 | func contrast() *cobra.Command { 34 | var change float64 35 | 36 | var cmd = &cobra.Command{ 37 | Use: "contrast", 38 | Short: "adjust the relative contrast of an image", 39 | Args: cobra.ExactArgs(2), 40 | Example: "contrast --change 0.5 input.jpg output.jpg", 41 | Run: func(cmd *cobra.Command, args []string) { 42 | fin := args[0] 43 | fout := args[1] 44 | 45 | apply(fin, fout, func(img image.Image) (image.Image, error) { 46 | return adjust.Contrast(img, change), nil 47 | }) 48 | }} 49 | 50 | cmd.Flags().Float64VarP(&change, "change", "c", 0, "adjust change") 51 | 52 | return cmd 53 | } 54 | 55 | func gamma() *cobra.Command { 56 | var change float64 57 | 58 | var cmd = &cobra.Command{ 59 | Use: "gamma", 60 | Short: "adjust the gamma of an image", 61 | Args: cobra.ExactArgs(2), 62 | Example: "gamma --gamma 1.1 input.jpg output.jpg", 63 | Run: func(cmd *cobra.Command, args []string) { 64 | fin := args[0] 65 | fout := args[1] 66 | 67 | apply(fin, fout, func(img image.Image) (image.Image, error) { 68 | return adjust.Gamma(img, change), nil 69 | }) 70 | }} 71 | 72 | cmd.Flags().Float64VarP(&change, "gamma", "g", 0, "set the gamma of the image") 73 | 74 | return cmd 75 | } 76 | 77 | func hue() *cobra.Command { 78 | var change int 79 | 80 | var cmd = &cobra.Command{ 81 | Use: "hue", 82 | Short: "adjust the hue of an image", 83 | Args: cobra.ExactArgs(2), 84 | Example: "hue --change 170 input.jpg output.jpg", 85 | Run: func(cmd *cobra.Command, args []string) { 86 | fin := args[0] 87 | fout := args[1] 88 | 89 | apply(fin, fout, func(img image.Image) (image.Image, error) { 90 | return adjust.Hue(img, change), nil 91 | }) 92 | }} 93 | 94 | cmd.Flags().IntVarP(&change, "change", "c", 0, "adjust change") 95 | 96 | return cmd 97 | } 98 | 99 | func saturation() *cobra.Command { 100 | var change float64 101 | 102 | var cmd = &cobra.Command{ 103 | Use: "saturation", 104 | Short: "adjust the saturation of an image", 105 | Args: cobra.ExactArgs(2), 106 | Example: "saturation --change 170 input.jpg output.jpg", 107 | Run: func(cmd *cobra.Command, args []string) { 108 | fin := args[0] 109 | fout := args[1] 110 | 111 | apply(fin, fout, func(img image.Image) (image.Image, error) { 112 | return adjust.Saturation(img, change), nil 113 | }) 114 | }} 115 | 116 | cmd.Flags().Float64VarP(&change, "change", "c", 0, "adjust change") 117 | 118 | return cmd 119 | } 120 | 121 | func createAdjust() *cobra.Command { 122 | adjustCmd := &cobra.Command{ 123 | Use: "adjust", 124 | Short: "adjust basic image features like brightness or contrast", 125 | } 126 | 127 | adjustCmd.AddCommand(brightness()) 128 | adjustCmd.AddCommand(contrast()) 129 | adjustCmd.AddCommand(gamma()) 130 | adjustCmd.AddCommand(hue()) 131 | adjustCmd.AddCommand(saturation()) 132 | 133 | return adjustCmd 134 | } 135 | -------------------------------------------------------------------------------- /cmd/blend.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/anthonynsimon/bild/blend" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func buildBlendModeCommand(name string) *cobra.Command { 12 | var strength float64 13 | 14 | var cmd = &cobra.Command{ 15 | Use: name, 16 | Args: cobra.ExactArgs(3), 17 | Example: fmt.Sprintf("%s --strength 0.5 input1.jpg input2.jpg output.jpg", name), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fin1 := args[0] 20 | fin2 := args[1] 21 | fout := args[2] 22 | 23 | apply2(fin1, fin2, fout, func(img1, img2 image.Image) (image.Image, error) { 24 | switch name { 25 | case "add": 26 | return blend.Opacity(img1, blend.Add(img1, img2), strength), nil 27 | case "colorburn": 28 | return blend.Opacity(img1, blend.ColorBurn(img1, img2), strength), nil 29 | case "colordodge": 30 | return blend.Opacity(img1, blend.ColorDodge(img1, img2), strength), nil 31 | case "darken": 32 | return blend.Opacity(img1, blend.Darken(img1, img2), strength), nil 33 | case "difference": 34 | return blend.Opacity(img1, blend.Difference(img1, img2), strength), nil 35 | case "divide": 36 | return blend.Opacity(img1, blend.Divide(img1, img2), strength), nil 37 | case "exclusion": 38 | return blend.Opacity(img1, blend.Exclusion(img1, img2), strength), nil 39 | case "lighten": 40 | return blend.Opacity(img1, blend.Lighten(img1, img2), strength), nil 41 | case "linearburn": 42 | return blend.Opacity(img1, blend.LinearBurn(img1, img2), strength), nil 43 | case "linearLight": 44 | return blend.Opacity(img1, blend.LinearLight(img1, img2), strength), nil 45 | case "multiply": 46 | return blend.Opacity(img1, blend.Multiply(img1, img2), strength), nil 47 | case "normal": 48 | return blend.Opacity(img1, blend.Normal(img1, img2), strength), nil 49 | case "opacity": 50 | return blend.Opacity(img1, img2, strength), nil 51 | case "overlay": 52 | return blend.Opacity(img1, blend.Overlay(img1, img2), strength), nil 53 | case "screen": 54 | return blend.Opacity(img1, blend.Screen(img1, img2), strength), nil 55 | case "softlight": 56 | return blend.Opacity(img1, blend.SoftLight(img1, img2), strength), nil 57 | case "subtract": 58 | return blend.Opacity(img1, blend.Subtract(img1, img2), strength), nil 59 | } 60 | return blend.Opacity(img1, blend.Add(img1, img2), strength), nil 61 | }) 62 | }, 63 | } 64 | 65 | cmd.Flags().Float64VarP(&strength, "strength", "s", 1.0, "blend mode strength") 66 | 67 | return cmd 68 | } 69 | 70 | func createBlend() *cobra.Command { 71 | blendCmd := &cobra.Command{ 72 | Use: "blend", 73 | Short: "blend two images together", 74 | } 75 | 76 | blendCmd.AddCommand(buildBlendModeCommand("add")) 77 | blendCmd.AddCommand(buildBlendModeCommand("colorburn")) 78 | blendCmd.AddCommand(buildBlendModeCommand("colordodge")) 79 | blendCmd.AddCommand(buildBlendModeCommand("darken")) 80 | blendCmd.AddCommand(buildBlendModeCommand("difference")) 81 | blendCmd.AddCommand(buildBlendModeCommand("divide")) 82 | blendCmd.AddCommand(buildBlendModeCommand("exclusion")) 83 | blendCmd.AddCommand(buildBlendModeCommand("lighten")) 84 | blendCmd.AddCommand(buildBlendModeCommand("linearburn")) 85 | blendCmd.AddCommand(buildBlendModeCommand("linearLight")) 86 | blendCmd.AddCommand(buildBlendModeCommand("multiply")) 87 | blendCmd.AddCommand(buildBlendModeCommand("normal")) 88 | blendCmd.AddCommand(buildBlendModeCommand("opacity")) 89 | blendCmd.AddCommand(buildBlendModeCommand("overlay")) 90 | blendCmd.AddCommand(buildBlendModeCommand("screen")) 91 | blendCmd.AddCommand(buildBlendModeCommand("softlight")) 92 | blendCmd.AddCommand(buildBlendModeCommand("subtract")) 93 | 94 | return blendCmd 95 | } 96 | -------------------------------------------------------------------------------- /cmd/blur.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/anthonynsimon/bild/blur" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func box() *cobra.Command { 11 | var radius float64 12 | 13 | var cmd = &cobra.Command{ 14 | Use: "box", 15 | Short: "apply box blur to an input image", 16 | Args: cobra.ExactArgs(2), 17 | Example: "box --radius 0.5 input.jpg output.jpg", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fin := args[0] 20 | fout := args[1] 21 | 22 | apply(fin, fout, func(img image.Image) (image.Image, error) { 23 | return blur.Box(img, radius), nil 24 | }) 25 | }} 26 | 27 | cmd.Flags().Float64VarP(&radius, "radius", "r", 0, "the blur's radius") 28 | 29 | return cmd 30 | } 31 | 32 | func gaussian() *cobra.Command { 33 | var radius float64 34 | 35 | var cmd = &cobra.Command{ 36 | Use: "gaussian", 37 | Short: "apply gaussian blur to an input image", 38 | Args: cobra.ExactArgs(2), 39 | Example: "gaussian --radius 0.5 input.jpg output.jpg", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | fin := args[0] 42 | fout := args[1] 43 | 44 | apply(fin, fout, func(img image.Image) (image.Image, error) { 45 | return blur.Gaussian(img, radius), nil 46 | }) 47 | }} 48 | 49 | cmd.Flags().Float64VarP(&radius, "radius", "r", 0, "the blur's radius") 50 | 51 | return cmd 52 | } 53 | 54 | func createBlur() *cobra.Command { 55 | var blurCmd = &cobra.Command{ 56 | Use: "blur", 57 | Short: "blur an image using the specified method", 58 | } 59 | 60 | blurCmd.AddCommand(box()) 61 | blurCmd.AddCommand(gaussian()) 62 | 63 | return blurCmd 64 | } 65 | -------------------------------------------------------------------------------- /cmd/channel.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/anthonynsimon/bild/channel" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func extractChannel() *cobra.Command { 12 | var channels string 13 | 14 | var cmd = &cobra.Command{ 15 | Use: "extract", 16 | Short: "extracts RGBA channels from an input image", 17 | Args: cobra.ExactArgs(2), 18 | Example: "extract --channels rba input.jpg output.png", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fin := args[0] 21 | fout := args[1] 22 | 23 | // Apply takes care of resolving the destination encoder 24 | apply(fin, fout, func(img image.Image) (image.Image, error) { 25 | onlyChannels := []channel.Channel{} 26 | for _, c := range channels { 27 | switch c { 28 | case 'r': 29 | onlyChannels = append(onlyChannels, channel.Red) 30 | case 'g': 31 | onlyChannels = append(onlyChannels, channel.Green) 32 | case 'b': 33 | onlyChannels = append(onlyChannels, channel.Blue) 34 | case 'a': 35 | onlyChannels = append(onlyChannels, channel.Alpha) 36 | default: 37 | return nil, fmt.Errorf("unknown channel alias '%c'", c) 38 | } 39 | } 40 | 41 | result := channel.ExtractMultiple(img, onlyChannels...) 42 | return result, nil 43 | }) 44 | }} 45 | 46 | cmd.Flags().StringVarP(&channels, "channels", "c", "rgba", "the channels to include in the histogram") 47 | 48 | return cmd 49 | } 50 | 51 | func createChannel() *cobra.Command { 52 | var cmd = &cobra.Command{ 53 | Use: "channel", 54 | Short: "channel operations on images", 55 | } 56 | 57 | cmd.AddCommand(extractChannel()) 58 | 59 | return cmd 60 | } 61 | -------------------------------------------------------------------------------- /cmd/effect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/anthonynsimon/bild/effect" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func grayscale() *cobra.Command { 11 | var cmd = &cobra.Command{ 12 | Use: "grayscale", 13 | Aliases: []string{"gray"}, 14 | Short: "applies the grayscale effect", 15 | Args: cobra.ExactArgs(2), 16 | Example: "grayscale input.jpg output.png", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | fin := args[0] 19 | fout := args[1] 20 | 21 | // Apply takes care of resolving the destination encoder 22 | apply(fin, fout, func(img image.Image) (image.Image, error) { 23 | return effect.Grayscale(img), nil 24 | }) 25 | }} 26 | return cmd 27 | } 28 | 29 | func sepia() *cobra.Command { 30 | var cmd = &cobra.Command{ 31 | Use: "sepia", 32 | Short: "applies the sepia effect", 33 | Args: cobra.ExactArgs(2), 34 | Example: "sepia input.jpg output.png", 35 | Run: func(cmd *cobra.Command, args []string) { 36 | fin := args[0] 37 | fout := args[1] 38 | 39 | // Apply takes care of resolving the destination encoder 40 | apply(fin, fout, func(img image.Image) (image.Image, error) { 41 | return effect.Sepia(img), nil 42 | }) 43 | }} 44 | return cmd 45 | } 46 | 47 | func sharpen() *cobra.Command { 48 | var cmd = &cobra.Command{ 49 | Use: "sharpen", 50 | Aliases: []string{"sharp"}, 51 | Short: "applies the sharpen effect", 52 | Args: cobra.ExactArgs(2), 53 | Example: "sharpen input.jpg output.png", 54 | Run: func(cmd *cobra.Command, args []string) { 55 | fin := args[0] 56 | fout := args[1] 57 | 58 | // Apply takes care of resolving the destination encoder 59 | apply(fin, fout, func(img image.Image) (image.Image, error) { 60 | return effect.Sharpen(img), nil 61 | }) 62 | }} 63 | return cmd 64 | } 65 | 66 | func sobel() *cobra.Command { 67 | var cmd = &cobra.Command{ 68 | Use: "sobel", 69 | Short: "applies the sobel effect", 70 | Args: cobra.ExactArgs(2), 71 | Example: "sobel input.jpg output.png", 72 | Run: func(cmd *cobra.Command, args []string) { 73 | fin := args[0] 74 | fout := args[1] 75 | 76 | // Apply takes care of resolving the destination encoder 77 | apply(fin, fout, func(img image.Image) (image.Image, error) { 78 | return effect.Sobel(img), nil 79 | }) 80 | }} 81 | return cmd 82 | } 83 | 84 | func invert() *cobra.Command { 85 | var cmd = &cobra.Command{ 86 | Use: "invert", 87 | Short: "applies the invert effect", 88 | Args: cobra.ExactArgs(2), 89 | Example: "invert input.jpg output.png", 90 | Run: func(cmd *cobra.Command, args []string) { 91 | fin := args[0] 92 | fout := args[1] 93 | 94 | // Apply takes care of resolving the destination encoder 95 | apply(fin, fout, func(img image.Image) (image.Image, error) { 96 | return effect.Invert(img), nil 97 | }) 98 | }} 99 | return cmd 100 | } 101 | 102 | func median() *cobra.Command { 103 | var radius float64 104 | 105 | var cmd = &cobra.Command{ 106 | Use: "median", 107 | Short: "applies the median effect", 108 | Args: cobra.ExactArgs(2), 109 | Example: "median --radius 2.5 input.jpg output.png", 110 | Run: func(cmd *cobra.Command, args []string) { 111 | fin := args[0] 112 | fout := args[1] 113 | 114 | // Apply takes care of resolving the destination encoder 115 | apply(fin, fout, func(img image.Image) (image.Image, error) { 116 | return effect.Median(img, radius), nil 117 | }) 118 | }} 119 | 120 | cmd.Flags().Float64VarP(&radius, "radius", "r", 3, "the effect's radius") 121 | 122 | return cmd 123 | } 124 | 125 | func erode() *cobra.Command { 126 | var radius float64 127 | 128 | var cmd = &cobra.Command{ 129 | Use: "erode", 130 | Short: "applies the erode effect", 131 | Args: cobra.ExactArgs(2), 132 | Example: "erode --radius 0.5 input.jpg output.png", 133 | Run: func(cmd *cobra.Command, args []string) { 134 | fin := args[0] 135 | fout := args[1] 136 | 137 | // Apply takes care of resolving the destination encoder 138 | apply(fin, fout, func(img image.Image) (image.Image, error) { 139 | return effect.Erode(img, radius), nil 140 | }) 141 | }} 142 | 143 | cmd.Flags().Float64VarP(&radius, "radius", "r", 0.5, "the effect's radius") 144 | 145 | return cmd 146 | } 147 | 148 | func dilate() *cobra.Command { 149 | var radius float64 150 | 151 | var cmd = &cobra.Command{ 152 | Use: "dilate", 153 | Short: "applies the dilate effect", 154 | Args: cobra.ExactArgs(2), 155 | Example: "dilate --radius 0.5 input.jpg output.png", 156 | Run: func(cmd *cobra.Command, args []string) { 157 | fin := args[0] 158 | fout := args[1] 159 | 160 | // Apply takes care of resolving the destination encoder 161 | apply(fin, fout, func(img image.Image) (image.Image, error) { 162 | return effect.Dilate(img, radius), nil 163 | }) 164 | }} 165 | 166 | cmd.Flags().Float64VarP(&radius, "radius", "r", 0.5, "the effect's radius") 167 | 168 | return cmd 169 | } 170 | 171 | func edgedetection() *cobra.Command { 172 | var radius float64 173 | 174 | var cmd = &cobra.Command{ 175 | Use: "edgedetection", 176 | Short: "applies the edgedetection effect", 177 | Args: cobra.ExactArgs(2), 178 | Example: "edgedetection --radius 0.5 input.jpg output.png", 179 | Run: func(cmd *cobra.Command, args []string) { 180 | fin := args[0] 181 | fout := args[1] 182 | 183 | // Apply takes care of resolving the destination encoder 184 | apply(fin, fout, func(img image.Image) (image.Image, error) { 185 | return effect.EdgeDetection(img, radius), nil 186 | }) 187 | }} 188 | 189 | cmd.Flags().Float64VarP(&radius, "radius", "r", 0.5, "the effect's radius") 190 | 191 | return cmd 192 | } 193 | 194 | func emboss() *cobra.Command { 195 | var cmd = &cobra.Command{ 196 | Use: "emboss", 197 | Short: "applies the emboss effect", 198 | Args: cobra.ExactArgs(2), 199 | Example: "emboss input.jpg output.png", 200 | Run: func(cmd *cobra.Command, args []string) { 201 | fin := args[0] 202 | fout := args[1] 203 | 204 | // Apply takes care of resolving the destination encoder 205 | apply(fin, fout, func(img image.Image) (image.Image, error) { 206 | return effect.Emboss(img), nil 207 | }) 208 | }} 209 | 210 | return cmd 211 | } 212 | 213 | func unsharpmask() *cobra.Command { 214 | var radius float64 215 | var amount float64 216 | 217 | var cmd = &cobra.Command{ 218 | Use: "unsharpmask", 219 | Short: "applies the unsharpmask effect", 220 | Args: cobra.ExactArgs(2), 221 | Example: "unsharpmask input.jpg output.png", 222 | Run: func(cmd *cobra.Command, args []string) { 223 | fin := args[0] 224 | fout := args[1] 225 | 226 | // Apply takes care of resolving the destination encoder 227 | apply(fin, fout, func(img image.Image) (image.Image, error) { 228 | return effect.UnsharpMask(img, radius, amount), nil 229 | }) 230 | }} 231 | 232 | cmd.Flags().Float64VarP(&radius, "radius", "r", 25, "the effect's radius") 233 | cmd.Flags().Float64VarP(&radius, "amount", "a", 1, "the effect's amount") 234 | 235 | return cmd 236 | } 237 | 238 | func createEffect() *cobra.Command { 239 | var cmd = &cobra.Command{ 240 | Use: "effect", 241 | Short: "apply effects on images", 242 | } 243 | 244 | cmd.AddCommand(grayscale()) 245 | cmd.AddCommand(sepia()) 246 | cmd.AddCommand(sharpen()) 247 | cmd.AddCommand(sobel()) 248 | cmd.AddCommand(invert()) 249 | cmd.AddCommand(median()) 250 | cmd.AddCommand(erode()) 251 | cmd.AddCommand(dilate()) 252 | cmd.AddCommand(edgedetection()) 253 | cmd.AddCommand(emboss()) 254 | cmd.AddCommand(unsharpmask()) 255 | 256 | return cmd 257 | } 258 | -------------------------------------------------------------------------------- /cmd/helpers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/anthonynsimon/bild/imgio" 12 | ) 13 | 14 | var jpgExtensions = []string{".jpg", ".jpeg"} 15 | var pngExtensions = []string{".png"} 16 | var bmpExtensions = []string{".bmp"} 17 | 18 | var ( 19 | // ErrWrongSize is thrown when the provided size string does not match the expected form. 20 | errWrongSize = errors.New("size must be of form [width]x[height], i.e. 400x200") 21 | ) 22 | 23 | type size struct { 24 | Width int 25 | Height int 26 | } 27 | 28 | func resolveEncoder(outputfile string, defaultEncoding imgio.Encoder) imgio.Encoder { 29 | lower := strings.ToLower(outputfile) 30 | 31 | for _, ext := range jpgExtensions { 32 | if strings.HasSuffix(lower, ext) { 33 | return imgio.JPEGEncoder(100) 34 | } 35 | } 36 | 37 | for _, ext := range pngExtensions { 38 | if strings.HasSuffix(lower, ext) { 39 | return imgio.PNGEncoder() 40 | } 41 | } 42 | 43 | for _, ext := range bmpExtensions { 44 | if strings.HasSuffix(lower, ext) { 45 | return imgio.BMPEncoder() 46 | } 47 | } 48 | 49 | return defaultEncoding 50 | } 51 | 52 | func apply(fin, fout string, process func(image.Image) (image.Image, error)) { 53 | in, err := imgio.Open(fin) 54 | exitIfNotNil(err) 55 | 56 | result, err := process(in) 57 | exitIfNotNil(err) 58 | 59 | encoder := resolveEncoder(fout, imgio.PNGEncoder()) 60 | err = imgio.Save(fout, result, encoder) 61 | exitIfNotNil(err) 62 | } 63 | 64 | func apply2(fin1, fin2, fout string, process func(image.Image, image.Image) (image.Image, error)) { 65 | in1, err := imgio.Open(fin1) 66 | exitIfNotNil(err) 67 | 68 | in2, err := imgio.Open(fin2) 69 | exitIfNotNil(err) 70 | 71 | result, err := process(in1, in2) 72 | exitIfNotNil(err) 73 | 74 | encoder := resolveEncoder(fout, imgio.PNGEncoder()) 75 | err = imgio.Save(fout, result, encoder) 76 | exitIfNotNil(err) 77 | } 78 | 79 | func exitIfNotNil(err error) { 80 | if err != nil { 81 | fmt.Println(err) 82 | os.Exit(1) 83 | } 84 | } 85 | 86 | func parseSizeStr(sizestr string) (*size, error) { 87 | parts := strings.Split(sizestr, "x") 88 | if len(parts) != 2 { 89 | return nil, errWrongSize 90 | } 91 | 92 | w, err := strconv.Atoi(parts[0]) 93 | if err != nil || w < 0 { 94 | return nil, errWrongSize 95 | } 96 | 97 | h, err := strconv.Atoi(parts[1]) 98 | if err != nil || h < 0 { 99 | return nil, errWrongSize 100 | } 101 | 102 | return &size{ 103 | Width: w, 104 | Height: h, 105 | }, nil 106 | } 107 | -------------------------------------------------------------------------------- /cmd/histogram.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | 7 | "github.com/anthonynsimon/bild/channel" 8 | "github.com/anthonynsimon/bild/histogram" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newHisto() *cobra.Command { 13 | var channels string 14 | 15 | var cmd = &cobra.Command{ 16 | Use: "new", 17 | Short: "creates a RBG histogram from an input image", 18 | Args: cobra.ExactArgs(2), 19 | Example: "new --channels rgb input.jpg output.png", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | fin := args[0] 22 | fout := args[1] 23 | 24 | // Apply takes care of resolving the destination encoder 25 | apply(fin, fout, func(img image.Image) (image.Image, error) { 26 | onlyChannels := []channel.Channel{channel.Alpha} 27 | for _, c := range channels { 28 | switch c { 29 | case 'r': 30 | onlyChannels = append(onlyChannels, channel.Red) 31 | case 'g': 32 | onlyChannels = append(onlyChannels, channel.Green) 33 | case 'b': 34 | onlyChannels = append(onlyChannels, channel.Blue) 35 | default: 36 | return nil, fmt.Errorf("unknown channel alias '%c'", c) 37 | } 38 | } 39 | 40 | hist := histogram.NewRGBAHistogram(img) 41 | result := channel.ExtractMultiple(hist.Image(), onlyChannels...) 42 | return result, nil 43 | }) 44 | }} 45 | 46 | cmd.Flags().StringVarP(&channels, "channels", "c", "rgb", "the channels to include in the histogram") 47 | 48 | return cmd 49 | } 50 | 51 | func createHistogram() *cobra.Command { 52 | var cmd = &cobra.Command{ 53 | Use: "histogram", 54 | Short: "histogram operations on images", 55 | } 56 | 57 | cmd.AddCommand(newHisto()) 58 | 59 | return cmd 60 | } 61 | -------------------------------------------------------------------------------- /cmd/imgio.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func encode() *cobra.Command { 10 | var cmd = &cobra.Command{ 11 | Use: "encode", 12 | Short: "encodes the input image using the desired encoding set by the destination file extension", 13 | Args: cobra.ExactArgs(2), 14 | Example: "encode input.jpg output.png", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fin := args[0] 17 | fout := args[1] 18 | 19 | // Apply takes care of resolving the destination encoder 20 | apply(fin, fout, func(img image.Image) (image.Image, error) { 21 | return img, nil 22 | }) 23 | }} 24 | return cmd 25 | } 26 | 27 | func createImgio() *cobra.Command { 28 | var cmd = &cobra.Command{ 29 | Use: "imgio", 30 | Short: "i/o operations on images", 31 | } 32 | 33 | cmd.AddCommand(encode()) 34 | 35 | return cmd 36 | } 37 | -------------------------------------------------------------------------------- /cmd/noise.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/anthonynsimon/bild/imgio" 5 | "github.com/anthonynsimon/bild/noise" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func generateNoise() *cobra.Command { 10 | size := "" 11 | mono := false 12 | 13 | var cmd = &cobra.Command{ 14 | Use: "new", 15 | Short: "generates an image filled with noise", 16 | Args: cobra.ExactArgs(1), 17 | Example: "new -s 100x100 output.png", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fout := args[0] 20 | 21 | size, err := parseSizeStr(size) 22 | exitIfNotNil(err) 23 | 24 | result := noise.Generate(size.Width, size.Height, &noise.Options{ 25 | Monochrome: mono, 26 | }) 27 | 28 | encoder := resolveEncoder(fout, imgio.PNGEncoder()) 29 | err = imgio.Save(fout, result, encoder) 30 | exitIfNotNil(err) 31 | }} 32 | 33 | cmd.Flags().StringVarP(&size, "size", "s", "512x512", "the width and height of the output image") 34 | cmd.Flags().BoolVarP(&mono, "monochrome", "m", false, "output monochrome noise") 35 | 36 | return cmd 37 | } 38 | 39 | func createNoise() *cobra.Command { 40 | var cmd = &cobra.Command{ 41 | Use: "noise", 42 | Short: "noise generators", 43 | } 44 | 45 | cmd.AddCommand(generateNoise()) 46 | 47 | return cmd 48 | } 49 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // Version of bild's CLI, set by the compiler on release 8 | var Version string 9 | 10 | var rootCmd = &cobra.Command{ 11 | Use: "bild", 12 | Short: "A collection of parallel image processing algorithms in pure Go", 13 | Version: Version, 14 | } 15 | 16 | func init() { 17 | rootCmd.AddCommand(createAdjust()) 18 | rootCmd.AddCommand(createBlend()) 19 | rootCmd.AddCommand(createBlur()) 20 | rootCmd.AddCommand(createImgio()) 21 | rootCmd.AddCommand(createNoise()) 22 | rootCmd.AddCommand(createSegment()) 23 | rootCmd.AddCommand(createHistogram()) 24 | rootCmd.AddCommand(createChannel()) 25 | rootCmd.AddCommand(createEffect()) 26 | } 27 | 28 | // Execute starts the cli's root command 29 | func Execute() { 30 | err := rootCmd.Execute() 31 | exitIfNotNil(err) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/segment.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/anthonynsimon/bild/segment" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func threshold() *cobra.Command { 11 | var level uint8 12 | 13 | var cmd = &cobra.Command{ 14 | Use: "threshold", 15 | Short: "segment an image by a threshold", 16 | Args: cobra.ExactArgs(2), 17 | Example: "threshold --level 200 input.jpg output.jpg", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fin := args[0] 20 | fout := args[1] 21 | 22 | apply(fin, fout, func(img image.Image) (image.Image, error) { 23 | return segment.Threshold(img, level), nil 24 | }) 25 | }} 26 | 27 | cmd.Flags().Uint8VarP(&level, "level", "l", 128, "the level at which the segmenting threshold will be crossed") 28 | 29 | return cmd 30 | } 31 | 32 | func createSegment() *cobra.Command { 33 | var blurCmd = &cobra.Command{ 34 | Use: "segment", 35 | Short: "segment an image using the specified method", 36 | } 37 | 38 | blurCmd.AddCommand(threshold()) 39 | 40 | return blurCmd 41 | } 42 | -------------------------------------------------------------------------------- /convolution/convolution.go: -------------------------------------------------------------------------------- 1 | /*Package convolution provides the functionality to create and apply a kernel to an image.*/ 2 | package convolution 3 | 4 | import ( 5 | "image" 6 | "math" 7 | 8 | "github.com/anthonynsimon/bild/clone" 9 | "github.com/anthonynsimon/bild/parallel" 10 | ) 11 | 12 | // Options are the Convolve function parameters. 13 | // Bias is added to each RGB channel after convoluting. Range is -255 to 255. 14 | // Wrap sets if indices outside of image dimensions should be taken from the opposite side. 15 | // KeepAlpha sets if alpha should be convolved or kept from the source image. 16 | type Options struct { 17 | Bias float64 18 | Wrap bool 19 | KeepAlpha bool 20 | } 21 | 22 | // Convolve applies a convolution matrix (kernel) to an image with the supplied options. 23 | // 24 | // Usage example: 25 | // 26 | // result := Convolve(img, kernel, &Options{Bias: 0, Wrap: false}) 27 | func Convolve(img image.Image, k Matrix, o *Options) *image.RGBA { 28 | // Config the convolution 29 | bias := 0.0 30 | wrap := false 31 | keepAlpha := false 32 | if o != nil { 33 | wrap = o.Wrap 34 | bias = o.Bias 35 | keepAlpha = o.KeepAlpha 36 | } 37 | 38 | return execute(img, k, bias, wrap, keepAlpha) 39 | } 40 | 41 | func execute(img image.Image, k Matrix, bias float64, wrap, keepAlpha bool) *image.RGBA { 42 | // Kernel attributes 43 | lenX := k.MaxX() 44 | lenY := k.MaxY() 45 | radiusX := lenX / 2 46 | radiusY := lenY / 2 47 | 48 | // Pad the source image, basically pre-computing the pixels outside of image bounds 49 | var src *image.RGBA 50 | if wrap { 51 | src = clone.Pad(img, radiusX, radiusY, clone.EdgeWrap) 52 | } else { 53 | src = clone.Pad(img, radiusX, radiusY, clone.EdgeExtend) 54 | } 55 | 56 | // src bounds now includes padded pixels 57 | srcBounds := src.Bounds() 58 | srcW, srcH := srcBounds.Dx(), srcBounds.Dy() 59 | dst := image.NewRGBA(img.Bounds()) 60 | 61 | // To keep alpha we simply don't convolve it 62 | if keepAlpha { 63 | // Notice we can't use lenY since it will be larger than the actual padding pixels 64 | // as it includes the identity element 65 | parallel.Line(srcH-(radiusY*2), func(start, end int) { 66 | // Correct range so we don't iterate over the padded pixels on the main loop 67 | for y := start + radiusY; y < end+radiusY; y++ { 68 | for x := radiusX; x < srcW-radiusX; x++ { 69 | 70 | var r, g, b float64 71 | // Kernel has access to the padded pixels 72 | for ky := 0; ky < lenY; ky++ { 73 | iy := y - radiusY + ky 74 | 75 | for kx := 0; kx < lenX; kx++ { 76 | ix := x - radiusX + kx 77 | 78 | kvalue := k.At(kx, ky) 79 | ipos := iy*src.Stride + ix*4 80 | r += float64(src.Pix[ipos+0]) * kvalue 81 | g += float64(src.Pix[ipos+1]) * kvalue 82 | b += float64(src.Pix[ipos+2]) * kvalue 83 | } 84 | } 85 | 86 | // Map x and y indices to non-padded range 87 | pos := (y-radiusY)*dst.Stride + (x-radiusX)*4 88 | 89 | dst.Pix[pos+0] = uint8(math.Max(math.Min(r+bias, 255), 0)) 90 | dst.Pix[pos+1] = uint8(math.Max(math.Min(g+bias, 255), 0)) 91 | dst.Pix[pos+2] = uint8(math.Max(math.Min(b+bias, 255), 0)) 92 | dst.Pix[pos+3] = src.Pix[y*src.Stride+x*4+3] 93 | } 94 | } 95 | }) 96 | } else { 97 | // Notice we can't use lenY since it will be larger than the actual padding pixels 98 | // as it includes the identity element 99 | parallel.Line(srcH-(radiusY*2), func(start, end int) { 100 | // Correct range so we don't iterate over the padded pixels on the main loop 101 | for y := start + radiusY; y < end+radiusY; y++ { 102 | for x := radiusX; x < srcW-radiusX; x++ { 103 | 104 | var r, g, b, a float64 105 | // Kernel has access to the padded pixels 106 | for ky := 0; ky < lenY; ky++ { 107 | iy := y - radiusY + ky 108 | 109 | for kx := 0; kx < lenX; kx++ { 110 | ix := x - radiusX + kx 111 | 112 | kvalue := k.At(kx, ky) 113 | ipos := iy*src.Stride + ix*4 114 | r += float64(src.Pix[ipos+0]) * kvalue 115 | g += float64(src.Pix[ipos+1]) * kvalue 116 | b += float64(src.Pix[ipos+2]) * kvalue 117 | a += float64(src.Pix[ipos+3]) * kvalue 118 | } 119 | } 120 | 121 | // Map x and y indices to non-padded range 122 | pos := (y-radiusY)*dst.Stride + (x-radiusX)*4 123 | 124 | dst.Pix[pos+0] = uint8(math.Max(math.Min(r+bias, 255), 0)) 125 | dst.Pix[pos+1] = uint8(math.Max(math.Min(g+bias, 255), 0)) 126 | dst.Pix[pos+2] = uint8(math.Max(math.Min(b+bias, 255), 0)) 127 | dst.Pix[pos+3] = uint8(math.Max(math.Min(a, 255), 0)) 128 | } 129 | } 130 | }) 131 | } 132 | 133 | return dst 134 | } 135 | -------------------------------------------------------------------------------- /convolution/convolution_test.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/util" 8 | ) 9 | 10 | // benchResult is used to avoid having the compiler optimize the benchmark code calls 11 | var benchResult interface{} 12 | 13 | func TestConvolve(t *testing.T) { 14 | cases := []struct { 15 | options *Options 16 | kernel *Kernel 17 | value image.Image 18 | expected *image.RGBA 19 | }{ 20 | { 21 | options: &Options{Bias: 0, Wrap: false}, 22 | kernel: &Kernel{[]float64{}, 0, 0}, 23 | value: &image.RGBA{ 24 | Rect: image.Rect(0, 0, 3, 3), 25 | Stride: 3 * 4, 26 | Pix: []uint8{ 27 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 28 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 29 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 30 | }, 31 | }, 32 | expected: &image.RGBA{ 33 | Rect: image.Rect(0, 0, 3, 3), 34 | Stride: 3 * 4, 35 | Pix: []uint8{ 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | }, 40 | }, 41 | }, 42 | { 43 | options: &Options{Bias: 0, Wrap: false}, 44 | kernel: &Kernel{[]float64{ 45 | 0, 0, 0, 46 | 0, 1, 0, 47 | 0, 0, 0, 48 | }, 3, 3}, 49 | value: &image.RGBA{ 50 | Rect: image.Rect(0, 0, 3, 3), 51 | Stride: 3 * 4, 52 | Pix: []uint8{ 53 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x15, 0x15, 0x15, 0xFF, 54 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x35, 0x35, 0x35, 0xFF, 55 | 0x40, 0x40, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 56 | }, 57 | }, 58 | expected: &image.RGBA{ 59 | Rect: image.Rect(0, 0, 3, 3), 60 | Stride: 3 * 4, 61 | Pix: []uint8{ 62 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x15, 0x15, 0x15, 0xFF, 63 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x35, 0x35, 0x35, 0xFF, 64 | 0x40, 0x40, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 65 | }, 66 | }, 67 | }, 68 | { 69 | options: &Options{Bias: 0, Wrap: false}, 70 | kernel: &Kernel{[]float64{ 71 | 0, 0, 0, 72 | 0, 1, 1, 73 | 0, 0, 0, 74 | }, 3, 3}, 75 | value: &image.RGBA{ 76 | Rect: image.Rect(0, 0, 3, 3), 77 | Stride: 3 * 4, 78 | Pix: []uint8{ 79 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 80 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x40, 0x40, 0x40, 0xFF, 81 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 82 | }, 83 | }, 84 | expected: &image.RGBA{ 85 | Rect: image.Rect(0, 0, 3, 3), 86 | Stride: 3 * 4, 87 | Pix: []uint8{ 88 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 89 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x80, 0x80, 0x80, 0xFF, 90 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 91 | }, 92 | }, 93 | }, 94 | { 95 | options: &Options{Bias: 128, Wrap: false}, 96 | kernel: &Kernel{[]float64{ 97 | 0, 0, 0, 98 | 0, 1, 0, 99 | 0, 0, 0, 100 | }, 3, 3}, 101 | value: &image.RGBA{ 102 | Rect: image.Rect(0, 0, 3, 3), 103 | Stride: 3 * 4, 104 | Pix: []uint8{ 105 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 106 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0xFF, 107 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x20, 0x20, 0xFF, 108 | }, 109 | }, 110 | expected: &image.RGBA{ 111 | Rect: image.Rect(0, 0, 3, 3), 112 | Stride: 3 * 4, 113 | Pix: []uint8{ 114 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 115 | 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 0x80, 0x80, 0x80, 0xFF, 116 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0xA0, 0xA0, 0xFF, 117 | }, 118 | }, 119 | }, 120 | { 121 | options: &Options{Bias: 0, Wrap: false, KeepAlpha: true}, 122 | kernel: &Kernel{[]float64{ 123 | 1, 1, 1, 124 | 1, 1, 1, 125 | 1, 1, 1, 126 | }, 3, 3}, 127 | value: &image.RGBA{ 128 | Rect: image.Rect(0, 0, 3, 3), 129 | Stride: 3 * 4, 130 | Pix: []uint8{ 131 | 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 132 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0xFF, 133 | 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 134 | }, 135 | }, 136 | expected: &image.RGBA{ 137 | Rect: image.Rect(0, 0, 3, 3), 138 | Stride: 3 * 4, 139 | Pix: []uint8{ 140 | 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 141 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 142 | 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 143 | }, 144 | }, 145 | }, 146 | { 147 | options: &Options{Bias: 0, Wrap: false, KeepAlpha: true}, 148 | kernel: &Kernel{[]float64{ 149 | 0, 0, 0, 150 | 0, 0, 0, 151 | 0, 0, 0, 152 | }, 3, 3}, 153 | value: &image.RGBA{ 154 | Rect: image.Rect(0, 0, 3, 3), 155 | Stride: 3 * 4, 156 | Pix: []uint8{ 157 | 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 158 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0xFF, 159 | 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 160 | }, 161 | }, 162 | expected: &image.RGBA{ 163 | Rect: image.Rect(0, 0, 3, 3), 164 | Stride: 3 * 4, 165 | Pix: []uint8{ 166 | 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xFF, 167 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 168 | 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xFF, 169 | }, 170 | }, 171 | }, 172 | { 173 | options: &Options{Bias: 0, Wrap: true}, 174 | kernel: &Kernel{[]float64{ 175 | 1, 0, 0, 176 | 0, 0, 0, 177 | 0, 0, 0, 178 | }, 3, 3}, 179 | value: &image.RGBA{ 180 | Rect: image.Rect(0, 0, 3, 3), 181 | Stride: 3 * 4, 182 | Pix: []uint8{ 183 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 184 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 185 | 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 186 | }, 187 | }, 188 | expected: &image.RGBA{ 189 | Rect: image.Rect(0, 0, 3, 3), 190 | Stride: 3 * 4, 191 | Pix: []uint8{ 192 | 0x20, 0x20, 0x20, 0xff, 0x00, 0x00, 0x00, 0xff, 0x80, 0x80, 0x80, 0xff, 193 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 194 | 0x40, 0x40, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 195 | }, 196 | }, 197 | }, 198 | } 199 | 200 | for _, c := range cases { 201 | actual := Convolve(c.value, c.kernel, c.options) 202 | if !util.RGBAImageEqual(actual, c.expected) { 203 | t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Convolve", util.RGBAToString(c.expected), util.RGBAToString(actual)) 204 | } 205 | } 206 | } 207 | 208 | func BenchmarkConvolve3(b *testing.B) { 209 | benchConvolve(b, 1024, 1024, NewKernel(3, 3)) 210 | } 211 | 212 | func BenchmarkConvolve8(b *testing.B) { 213 | benchConvolve(b, 1024, 1024, NewKernel(8, 8)) 214 | } 215 | 216 | func BenchmarkConvolve32(b *testing.B) { 217 | benchConvolve(b, 1024, 1024, NewKernel(32, 32)) 218 | } 219 | 220 | func BenchmarkConvolve64(b *testing.B) { 221 | benchConvolve(b, 1024, 1024, NewKernel(64, 64)) 222 | } 223 | 224 | func benchConvolve(b *testing.B, w, h int, k *Kernel) { 225 | img := image.NewRGBA(image.Rect(0, 0, w, h)) 226 | b.ResetTimer() 227 | for n := 0; n < b.N; n++ { 228 | benchResult = Convolve(img, k, &Options{Wrap: false}) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /convolution/kernel.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Matrix interface. 9 | // At returns the matrix value at position x, y. 10 | // Normalized returns a new matrix with normalized values. 11 | // MaxX returns the horizontal length. 12 | // MaxY returns the vertical length. 13 | type Matrix interface { 14 | At(x, y int) float64 15 | Normalized() Matrix 16 | MaxX() int 17 | MaxY() int 18 | Transposed() Matrix 19 | } 20 | 21 | // NewKernel returns a kernel of the provided length. 22 | func NewKernel(width, height int) *Kernel { 23 | return &Kernel{make([]float64, width*height), width, height} 24 | } 25 | 26 | // Kernel to be used as a convolution matrix. 27 | type Kernel struct { 28 | Matrix []float64 29 | Width int 30 | Height int 31 | } 32 | 33 | // Normalized returns a new Kernel with normalized values. 34 | func (k *Kernel) Normalized() Matrix { 35 | sum := k.Absum() 36 | w := k.Width 37 | h := k.Height 38 | nk := NewKernel(w, h) 39 | 40 | // avoid division by 0 41 | if sum == 0 { 42 | sum = 1 43 | } 44 | 45 | for i := 0; i < w*h; i++ { 46 | nk.Matrix[i] = k.Matrix[i] / sum 47 | } 48 | 49 | return nk 50 | } 51 | 52 | // MaxX returns the horizontal length. 53 | func (k *Kernel) MaxX() int { 54 | return k.Width 55 | } 56 | 57 | // MaxY returns the vertical length. 58 | func (k *Kernel) MaxY() int { 59 | return k.Height 60 | } 61 | 62 | // At returns the matrix value at position x, y. 63 | func (k *Kernel) At(x, y int) float64 { 64 | return k.Matrix[y*k.Width+x] 65 | } 66 | 67 | // Transposed returns a new Kernel that has the columns as rows and vice versa 68 | func (k *Kernel) Transposed() Matrix { 69 | w := k.Width 70 | h := k.Height 71 | nk := NewKernel(h, w) 72 | 73 | for x := 0; x < w; x++ { 74 | for y := 0; y < h; y++ { 75 | nk.Matrix[x*h+y] = k.Matrix[y*w+x] 76 | } 77 | } 78 | 79 | return nk 80 | } 81 | 82 | // String returns the string representation of the matrix. 83 | func (k *Kernel) String() string { 84 | result := "" 85 | stride := k.MaxX() 86 | height := k.MaxY() 87 | for y := 0; y < height; y++ { 88 | result += fmt.Sprintf("\n") 89 | for x := 0; x < stride; x++ { 90 | result += fmt.Sprintf("%-8.4f", k.At(x, y)) 91 | } 92 | } 93 | return result 94 | } 95 | 96 | // Absum returns the absolute cumulative value of the kernel. 97 | func (k *Kernel) Absum() float64 { 98 | var sum float64 99 | for _, v := range k.Matrix { 100 | sum += math.Abs(v) 101 | } 102 | return sum 103 | } 104 | -------------------------------------------------------------------------------- /convolution/kernel_test.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import "testing" 4 | 5 | func TestNewKernel(t *testing.T) { 6 | cases := []struct { 7 | size int 8 | expected *Kernel 9 | }{ 10 | { 11 | size: 0, 12 | expected: &Kernel{[]float64{}, 0, 0}, 13 | }, 14 | { 15 | size: 1, 16 | expected: &Kernel{[]float64{ 17 | 0, 18 | 0, 19 | }, 1, 1}, 20 | }, 21 | { 22 | size: 2, 23 | expected: &Kernel{[]float64{ 24 | 0, 0, 25 | 0, 0, 26 | }, 2, 2}, 27 | }, 28 | { 29 | size: 3, 30 | expected: &Kernel{[]float64{ 31 | 0, 0, 0, 32 | 0, 0, 0, 33 | 0, 0, 0, 34 | }, 3, 3}, 35 | }, 36 | { 37 | size: 10, 38 | expected: &Kernel{[]float64{ 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49 | }, 10, 10}, 50 | }, 51 | } 52 | 53 | for _, c := range cases { 54 | actual := NewKernel(c.size, c.size) 55 | if !kernelEqual(actual, c.expected) { 56 | t.Errorf("%s: expected: %#v, actual: %#v", "NewKernel", c.expected, actual) 57 | } 58 | } 59 | } 60 | 61 | func TestAbsum(t *testing.T) { 62 | cases := []struct { 63 | kernel *Kernel 64 | expected float64 65 | }{ 66 | { 67 | expected: 0, 68 | kernel: NewKernel(0, 0), 69 | }, 70 | { 71 | expected: 10, 72 | kernel: &Kernel{[]float64{ 73 | 5, 0, 1, 74 | 0, 2, 0, 75 | 0, 2, 0, 76 | }, 3, 3}, 77 | }, 78 | { 79 | expected: 11, 80 | kernel: &Kernel{[]float64{ 81 | 4, 0, 1, 82 | 0, 1, 0, 83 | 1, 3, 1, 84 | }, 3, 3}, 85 | }, 86 | { 87 | expected: 34, 88 | kernel: &Kernel{[]float64{ 89 | 20, 0, 2, 90 | 0, -9, 0, 91 | -2, 0, -1, 92 | }, 3, 3}, 93 | }, 94 | { 95 | expected: 11, 96 | kernel: &Kernel{[]float64{ 97 | 0, 0, 0, -1, 98 | 0, 9, 0, 0, 99 | 0, 0, 0, -1, 100 | 0, 0, 0, 0, 101 | }, 4, 4}, 102 | }, 103 | } 104 | 105 | for _, c := range cases { 106 | actual := c.kernel.Absum() 107 | if actual != c.expected { 108 | t.Errorf("%s: expected: %#v, actual: %#v", "KernelAbSum", c.expected, actual) 109 | } 110 | } 111 | } 112 | 113 | func TestKernelAt(t *testing.T) { 114 | cases := []struct { 115 | x, y int 116 | kernel *Kernel 117 | expected float64 118 | }{ 119 | { 120 | x: 0, 121 | y: 0, 122 | expected: 5, 123 | kernel: &Kernel{[]float64{ 124 | 5, 0, 1, 125 | 0, 2, 0, 126 | 0, 2, 0, 127 | }, 3, 3}, 128 | }, 129 | { 130 | x: 2, 131 | y: 1, 132 | expected: -2, 133 | kernel: &Kernel{[]float64{ 134 | 4, -7, 1, 135 | -11, 1, -2, 136 | 1, 3, 1, 137 | }, 3, 3}, 138 | }, 139 | { 140 | x: 2, 141 | y: 2, 142 | expected: -1, 143 | kernel: &Kernel{[]float64{ 144 | 20, 0, 2, 145 | 0, -9, 0, 146 | -2, 0, -1, 147 | }, 3, 3}, 148 | }, 149 | { 150 | x: 3, 151 | y: 2, 152 | expected: -1, 153 | kernel: &Kernel{[]float64{ 154 | 0, 0, 0, -1, 155 | 0, 9, 0, 0, 156 | 0, 0, 0, -1, 157 | 0, 0, 92, 0, 158 | }, 4, 4}, 159 | }, 160 | } 161 | 162 | for _, c := range cases { 163 | actual := c.kernel.At(c.x, c.y) 164 | if actual != c.expected { 165 | t.Errorf("%s: expected: %#v, actual: %#v", "KernelAt", c.expected, actual) 166 | } 167 | } 168 | } 169 | 170 | func TestKernelNormalized(t *testing.T) { 171 | cases := []struct { 172 | desc string 173 | kernel *Kernel 174 | expected *Kernel 175 | }{ 176 | { 177 | desc: "all zero", 178 | kernel: &Kernel{[]float64{ 179 | 0, 0, 0, 180 | 0, 0, 0, 181 | 0, 0, 0, 182 | }, 3, 3}, 183 | expected: &Kernel{[]float64{ 184 | 0, 0, 0, 185 | 0, 0, 0, 186 | 0, 0, 0, 187 | }, 3, 3}, 188 | }, 189 | { 190 | desc: "one element", 191 | kernel: &Kernel{[]float64{ 192 | 0, 0, 0, 193 | 0, 1, 0, 194 | 0, 0, 0, 195 | }, 3, 3}, 196 | expected: &Kernel{[]float64{ 197 | 0, 0, 0, 198 | 0, 1, 0, 199 | 0, 0, 0, 200 | }, 3, 3}, 201 | }, 202 | { 203 | desc: "sum 3", 204 | kernel: &Kernel{[]float64{ 205 | 0, 0, 0, 206 | 1, 1, 0, 207 | 0, 0, 1, 208 | }, 3, 3}, 209 | expected: &Kernel{[]float64{ 210 | 0, 0, 0, 211 | 1.0 / 3, 1.0 / 3, 0, 212 | 0, 0, 1.0 / 3, 213 | }, 3, 3}, 214 | }, 215 | { 216 | desc: "sum 4", 217 | kernel: &Kernel{[]float64{ 218 | 0, 0, 0, 219 | 1, -2, 0, 220 | 0, 0, 1, 221 | }, 3, 3}, 222 | expected: &Kernel{[]float64{ 223 | 0, 0, 0, 224 | 1.0 / 4, -2.0 / 4, 0, 225 | 0, 0, 1.0 / 4, 226 | }, 3, 3}, 227 | }, 228 | { 229 | desc: "sum 5", 230 | kernel: &Kernel{[]float64{ 231 | 0, 0, 0, 232 | 1, -2, 0, 233 | -1, 0, 1, 234 | }, 3, 3}, 235 | expected: &Kernel{[]float64{ 236 | 0, 0, 0, 237 | 1.0 / 5, -2.0 / 5, 0, 238 | -1.0 / 5, 0, 1.0 / 5, 239 | }, 3, 3}, 240 | }, 241 | { 242 | desc: "single negative element", 243 | kernel: &Kernel{[]float64{ 244 | 0, 0, 0, 245 | 0, -1, 0, 246 | 0, 0, 0, 247 | }, 3, 3}, 248 | expected: &Kernel{[]float64{ 249 | 0, 0, 0, 250 | 0, -1, 0, 251 | 0, 0, 0, 252 | }, 3, 3}, 253 | }, 254 | } 255 | 256 | for _, c := range cases { 257 | actual := c.kernel.Normalized() 258 | if !kernelEqual(actual.(*Kernel), c.expected) { 259 | t.Errorf("%s: expected: %#v, actual: %#v", "KernelNormalized "+c.desc, c.expected, actual) 260 | } 261 | } 262 | } 263 | 264 | func TestKernelTransposed(t *testing.T) { 265 | cases := []struct { 266 | desc string 267 | kernel *Kernel 268 | expected *Kernel 269 | }{ 270 | { 271 | desc: "all zero", 272 | kernel: &Kernel{[]float64{ 273 | 0, 0, 0, 274 | 0, 0, 0, 275 | 0, 0, 0, 276 | }, 3, 3}, 277 | expected: &Kernel{[]float64{ 278 | 0, 0, 0, 279 | 0, 0, 0, 280 | 0, 0, 0, 281 | }, 3, 3}, 282 | }, 283 | { 284 | desc: "one element", 285 | kernel: &Kernel{[]float64{ 286 | 0, 0, 0, 287 | 0, 1, 0, 288 | 0, 0, 0, 289 | }, 3, 3}, 290 | expected: &Kernel{[]float64{ 291 | 0, 0, 0, 292 | 0, 1, 0, 293 | 0, 0, 0, 294 | }, 3, 3}, 295 | }, 296 | { 297 | desc: "diagonal", 298 | kernel: &Kernel{[]float64{ 299 | 1, 0, 0, 300 | 0, 1, 0, 301 | 0, 0, 1, 302 | }, 3, 3}, 303 | expected: &Kernel{[]float64{ 304 | 1, 0, 0, 305 | 0, 1, 0, 306 | 0, 0, 1, 307 | }, 3, 3}, 308 | }, 309 | { 310 | desc: "3x2", 311 | kernel: &Kernel{[]float64{ 312 | 1, 1, 1, 313 | 0, 1, 0, 314 | }, 3, 2}, 315 | expected: &Kernel{[]float64{ 316 | 1, 0, 317 | 1, 1, 318 | 1, 0, 319 | }, 2, 3}, 320 | }, 321 | { 322 | desc: "5x1", 323 | kernel: &Kernel{[]float64{ 324 | 1, 1, 1, 0, 1, 325 | }, 5, 1}, 326 | expected: &Kernel{[]float64{ 327 | 1, 328 | 1, 329 | 1, 330 | 0, 331 | 1, 332 | }, 1, 5}, 333 | }, 334 | } 335 | 336 | for i, c := range cases { 337 | actual := c.kernel.Transposed() 338 | if !kernelEqual(actual.(*Kernel), c.expected) { 339 | t.Errorf("%s case #%d: expected: %#v, actual: %#v", "KernelTransposed "+c.desc, i, c.expected, actual) 340 | } 341 | } 342 | } 343 | 344 | func TestKernelString(t *testing.T) { 345 | cases := []struct { 346 | kernel *Kernel 347 | expected string 348 | }{ 349 | { 350 | kernel: &Kernel{[]float64{ 351 | 0, 0, 0, 352 | 0, -1, 0, 353 | 0, 0, 0, 354 | }, 3, 3}, 355 | expected: "\n0.0000 0.0000 0.0000 \n0.0000 -1.0000 0.0000 \n0.0000 0.0000 0.0000 ", 356 | }, 357 | { 358 | kernel: &Kernel{[]float64{ 359 | -2.75, 0, 0, 360 | 0, -1, 0, 361 | 0, 0, 92.32579, 362 | }, 3, 3}, 363 | expected: "\n-2.7500 0.0000 0.0000 \n0.0000 -1.0000 0.0000 \n0.0000 0.0000 92.3258 ", 364 | }, 365 | } 366 | 367 | for _, c := range cases { 368 | actual := c.kernel.String() 369 | if actual != c.expected { 370 | t.Errorf("%s: expected: %#v, actual: %#v", "KernelString", c.expected, actual) 371 | } 372 | } 373 | } 374 | 375 | func kernelEqual(a, b *Kernel) bool { 376 | if a.Matrix == nil && b.Matrix == nil { 377 | return true 378 | } 379 | 380 | if a.MaxX() != b.MaxX() || a.MaxY() != b.MaxY() { 381 | return false 382 | } 383 | 384 | for x := 0; x < a.MaxX(); x++ { 385 | for y := 0; y < a.MaxY(); y++ { 386 | if a.Matrix[y*a.MaxX()+x] != b.Matrix[y*b.MaxX()+x] { 387 | return false 388 | } 389 | } 390 | } 391 | 392 | return true 393 | } 394 | -------------------------------------------------------------------------------- /fcolor/rgbaf64.go: -------------------------------------------------------------------------------- 1 | /*Package fcolor provides a basic RGBAF64 color type.*/ 2 | package fcolor 3 | 4 | import "github.com/anthonynsimon/bild/math/f64" 5 | 6 | // RGBAF64 represents an RGBA color using the range 0.0 to 1.0 with a float64 for each channel. 7 | type RGBAF64 struct { 8 | R, G, B, A float64 9 | } 10 | 11 | // NewRGBAF64 returns a new RGBAF64 color based on the provided uint8 values. 12 | // uint8 value 0 maps to 0, 128 to 0.5 and 255 to 1.0. 13 | func NewRGBAF64(r, g, b, a uint8) RGBAF64 { 14 | return RGBAF64{float64(r) / 255, float64(g) / 255, float64(b) / 255, float64(a) / 255} 15 | } 16 | 17 | // Clamp limits the channel values of the RGBAF64 color to the range 0.0 to 1.0. 18 | func (c *RGBAF64) Clamp() { 19 | c.R = f64.Clamp(c.R, 0, 1) 20 | c.G = f64.Clamp(c.G, 0, 1) 21 | c.B = f64.Clamp(c.B, 0, 1) 22 | c.A = f64.Clamp(c.A, 0, 1) 23 | } 24 | -------------------------------------------------------------------------------- /fcolor/rgbaf64_test.go: -------------------------------------------------------------------------------- 1 | package fcolor 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestNewRGBA(t *testing.T) { 9 | cases := []struct { 10 | value [4]uint8 11 | expected RGBAF64 12 | }{ 13 | { 14 | value: [4]uint8{0, 0, 0, 0}, 15 | expected: RGBAF64{0, 0, 0, 0}, 16 | }, 17 | { 18 | value: [4]uint8{255, 128, 64, 32}, 19 | expected: RGBAF64{1.0, 0.5, 0.25, 0.125}, 20 | }, 21 | { 22 | value: [4]uint8{10, 20, 30, 40}, 23 | expected: RGBAF64{0.04, 0.078, 0.12, 0.16}, 24 | }, 25 | { 26 | value: [4]uint8{255, 255, 255, 255}, 27 | expected: RGBAF64{1.0, 1.0, 1.0, 1.0}, 28 | }, 29 | } 30 | 31 | for _, c := range cases { 32 | actual := NewRGBAF64(c.value[0], c.value[1], c.value[2], c.value[3]) 33 | if !rgbaf64Equal(actual, c.expected, 0.01) { 34 | t.Errorf("%s: expected: %#v, actual: %#v", "NewRGBAF6", c.expected, actual) 35 | } 36 | } 37 | } 38 | 39 | func TestClamp(t *testing.T) { 40 | cases := []struct { 41 | value RGBAF64 42 | expected RGBAF64 43 | }{ 44 | { 45 | value: RGBAF64{0, 0, 0, 0}, 46 | expected: RGBAF64{0, 0, 0, 0}, 47 | }, 48 | { 49 | value: RGBAF64{10.0, 0.55, -0.25, 1.125}, 50 | expected: RGBAF64{1.0, 0.55, 0.0, 1.0}, 51 | }, 52 | { 53 | value: RGBAF64{1.04, 0.078, -0.12, 1.01}, 54 | expected: RGBAF64{1.0, 0.078, 0.0, 1.0}, 55 | }, 56 | { 57 | value: RGBAF64{1.0, 1.0, 1.0, 1.0}, 58 | expected: RGBAF64{1.0, 1.0, 1.0, 1.0}, 59 | }, 60 | } 61 | 62 | for _, c := range cases { 63 | c.value.Clamp() 64 | if !rgbaf64Equal(c.value, c.expected, 0.01) { 65 | t.Errorf("%s: expected: %#v, actual: %#v", "NewRGBAF6", c.expected, c.value) 66 | } 67 | } 68 | } 69 | 70 | func rgbaf64Equal(a, b RGBAF64, maxDiff float64) bool { 71 | if math.Abs(a.R-b.R) > maxDiff { 72 | return false 73 | } 74 | if math.Abs(a.G-b.G) > maxDiff { 75 | return false 76 | } 77 | if math.Abs(a.B-b.B) > maxDiff { 78 | return false 79 | } 80 | if math.Abs(a.A-b.A) > maxDiff { 81 | return false 82 | } 83 | return true 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anthonynsimon/bild 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/spf13/cobra v0.0.5 7 | golang.org/x/image v0.18.0 8 | ) 9 | 10 | require ( 11 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 12 | github.com/spf13/pflag v1.0.3 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 10 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 11 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 12 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 13 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 14 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 15 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 18 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 19 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 20 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 21 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 22 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 23 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 24 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 25 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 26 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 27 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 28 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 29 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 30 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 31 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 32 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 36 | -------------------------------------------------------------------------------- /histogram/histogram.go: -------------------------------------------------------------------------------- 1 | /*Package histogram provides basic histogram types and functions to analyze RGBA images.*/ 2 | package histogram 3 | 4 | import ( 5 | "image" 6 | 7 | "github.com/anthonynsimon/bild/clone" 8 | ) 9 | 10 | // RGBAHistogram holds a sub-histogram per RGBA channel. 11 | // Each channel histogram contains 256 bins (8-bit color depth per channel). 12 | type RGBAHistogram struct { 13 | R Histogram 14 | G Histogram 15 | B Histogram 16 | A Histogram 17 | } 18 | 19 | // Histogram holds a variable length slice of bins, which keeps track of sample counts. 20 | type Histogram struct { 21 | Bins []int 22 | } 23 | 24 | // Max returns the highest count found in the histogram bins. 25 | func (h *Histogram) Max() int { 26 | var max int 27 | if len(h.Bins) > 0 { 28 | max = h.Bins[0] 29 | for i := 1; i < len(h.Bins); i++ { 30 | if h.Bins[i] > max { 31 | max = h.Bins[i] 32 | } 33 | } 34 | } 35 | return max 36 | } 37 | 38 | // Min returns the lowest count found in the histogram bins. 39 | func (h *Histogram) Min() int { 40 | var min int 41 | if len(h.Bins) > 0 { 42 | min = h.Bins[0] 43 | for i := 1; i < len(h.Bins); i++ { 44 | if h.Bins[i] < min { 45 | min = h.Bins[i] 46 | } 47 | } 48 | } 49 | return min 50 | } 51 | 52 | // Cumulative returns a new Histogram in which each bin is the cumulative 53 | // value of its previous bins 54 | func (h *Histogram) Cumulative() *Histogram { 55 | binCount := len(h.Bins) 56 | out := Histogram{make([]int, binCount)} 57 | 58 | if binCount > 0 { 59 | out.Bins[0] = h.Bins[0] 60 | } 61 | 62 | for i := 1; i < binCount; i++ { 63 | out.Bins[i] = out.Bins[i-1] + h.Bins[i] 64 | } 65 | 66 | return &out 67 | } 68 | 69 | // Image returns a grayscale image representation of the Histogram. 70 | // The width and height of the image will be equivalent to the number of Bins in the Histogram. 71 | func (h *Histogram) Image() *image.Gray { 72 | dstW, dstH := len(h.Bins), len(h.Bins) 73 | dst := image.NewGray(image.Rect(0, 0, dstW, dstH)) 74 | 75 | max := h.Max() 76 | if max == 0 { 77 | max = 1 78 | } 79 | 80 | for x := 0; x < dstW; x++ { 81 | value := ((h.Bins[x] << 16 / max) * dstH) >> 16 82 | // Fill from the bottom up 83 | for y := dstH - 1; y > dstH-value-1; y-- { 84 | dst.Pix[y*dst.Stride+x] = 0xFF 85 | } 86 | } 87 | return dst 88 | } 89 | 90 | // NewRGBAHistogram constructs a RGBAHistogram out of the provided image. 91 | // A sub-histogram is created per RGBA channel with 256 bins each. 92 | func NewRGBAHistogram(img image.Image) *RGBAHistogram { 93 | src := clone.AsRGBA(img) 94 | 95 | binCount := 256 96 | r := Histogram{make([]int, binCount)} 97 | g := Histogram{make([]int, binCount)} 98 | b := Histogram{make([]int, binCount)} 99 | a := Histogram{make([]int, binCount)} 100 | 101 | for y := 0; y < src.Bounds().Dy(); y++ { 102 | for x := 0; x < src.Bounds().Dx(); x++ { 103 | pos := y*src.Stride + x*4 104 | r.Bins[src.Pix[pos+0]]++ 105 | g.Bins[src.Pix[pos+1]]++ 106 | b.Bins[src.Pix[pos+2]]++ 107 | a.Bins[src.Pix[pos+3]]++ 108 | } 109 | } 110 | 111 | return &RGBAHistogram{R: r, G: g, B: b, A: a} 112 | } 113 | 114 | // Cumulative returns a new RGBAHistogram in which each bin is the cumulative 115 | // value of its previous bins per channel. 116 | func (h *RGBAHistogram) Cumulative() *RGBAHistogram { 117 | binCount := len(h.R.Bins) 118 | 119 | r := Histogram{make([]int, binCount)} 120 | g := Histogram{make([]int, binCount)} 121 | b := Histogram{make([]int, binCount)} 122 | a := Histogram{make([]int, binCount)} 123 | 124 | out := RGBAHistogram{R: r, G: g, B: b, A: a} 125 | 126 | if binCount > 0 { 127 | out.R.Bins[0] = h.R.Bins[0] 128 | out.G.Bins[0] = h.G.Bins[0] 129 | out.B.Bins[0] = h.B.Bins[0] 130 | out.A.Bins[0] = h.A.Bins[0] 131 | } 132 | 133 | for i := 1; i < binCount; i++ { 134 | out.R.Bins[i] = out.R.Bins[i-1] + h.R.Bins[i] 135 | out.G.Bins[i] = out.G.Bins[i-1] + h.G.Bins[i] 136 | out.B.Bins[i] = out.B.Bins[i-1] + h.B.Bins[i] 137 | out.A.Bins[i] = out.A.Bins[i-1] + h.A.Bins[i] 138 | } 139 | 140 | return &out 141 | } 142 | 143 | // Image returns an RGBA image representation of the RGBAHistogram. 144 | // An image width of 256 represents the 256 Bins per channel and the 145 | // image height of 256 represents the max normalized histogram value per channel. 146 | // Each RGB channel from the histogram is mapped to its corresponding channel in the image, 147 | // so that for example if the red channel is extracted from the image, it corresponds to the 148 | // red channel histogram. 149 | func (h *RGBAHistogram) Image() *image.RGBA { 150 | if len(h.R.Bins) != 256 || len(h.G.Bins) != 256 || 151 | len(h.B.Bins) != 256 || len(h.A.Bins) != 256 { 152 | panic("RGBAHistogram bins length not equal to 256") 153 | } 154 | 155 | dstW, dstH := 256, 256 156 | dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) 157 | 158 | maxR := h.R.Max() 159 | if maxR == 0 { 160 | maxR = 1 161 | } 162 | maxG := h.G.Max() 163 | if maxG == 0 { 164 | maxG = 1 165 | } 166 | maxB := h.B.Max() 167 | if maxB == 0 { 168 | maxB = 1 169 | } 170 | 171 | for x := 0; x < dstW; x++ { 172 | binHeightR := ((h.R.Bins[x] << 16 / maxR) * dstH) >> 16 173 | binHeightG := ((h.G.Bins[x] << 16 / maxG) * dstH) >> 16 174 | binHeightB := ((h.B.Bins[x] << 16 / maxB) * dstH) >> 16 175 | // Fill from the bottom up 176 | for y := dstH - 1; y >= 0; y-- { 177 | pos := y*dst.Stride + x*4 178 | iy := dstH - 1 - y 179 | 180 | if iy < binHeightR { 181 | dst.Pix[pos+0] = 0xFF 182 | } 183 | if iy < binHeightG { 184 | dst.Pix[pos+1] = 0xFF 185 | } 186 | if iy < binHeightB { 187 | dst.Pix[pos+2] = 0xFF 188 | } 189 | dst.Pix[pos+3] = 0xFF 190 | } 191 | } 192 | 193 | return dst 194 | } 195 | -------------------------------------------------------------------------------- /imgio/io.go: -------------------------------------------------------------------------------- 1 | /*Package imgio provides basic image file input/output.*/ 2 | package imgio 3 | 4 | import ( 5 | "image" 6 | "image/jpeg" 7 | "image/png" 8 | "io" 9 | "os" 10 | 11 | "golang.org/x/image/bmp" 12 | ) 13 | 14 | // Encoder encodes the provided image and writes it 15 | type Encoder func(io.Writer, image.Image) error 16 | 17 | // Open loads and decodes an image from a file and returns it. 18 | // 19 | // Usage example: 20 | // 21 | // // Decodes an image from a file with the given filename 22 | // // returns an error if something went wrong 23 | // img, err := Open("exampleName") 24 | func Open(filename string) (image.Image, error) { 25 | f, err := os.Open(filename) 26 | if err != nil { 27 | return nil, err 28 | } 29 | defer f.Close() 30 | 31 | img, _, err := image.Decode(f) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return img, nil 37 | } 38 | 39 | // JPEGEncoder returns an encoder to JPEG given the argument 'quality' 40 | func JPEGEncoder(quality int) Encoder { 41 | return func(w io.Writer, img image.Image) error { 42 | return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) 43 | } 44 | } 45 | 46 | // PNGEncoder returns an encoder to PNG 47 | func PNGEncoder() Encoder { 48 | return func(w io.Writer, img image.Image) error { 49 | return png.Encode(w, img) 50 | } 51 | } 52 | 53 | // BMPEncoder returns an encoder to BMP 54 | func BMPEncoder() Encoder { 55 | return func(w io.Writer, img image.Image) error { 56 | return bmp.Encode(w, img) 57 | } 58 | } 59 | 60 | // Save creates a file and writes to it an image using the provided encoder. 61 | // 62 | // Usage example: 63 | // 64 | // // Save an image to a file in PNG format, 65 | // // returns an error if something went wrong 66 | // err := Save("exampleName", img, imgio.JPEGEncoder(100)) 67 | func Save(filename string, img image.Image, encoder Encoder) error { 68 | // filename = strings.TrimSuffix(filename, filepath.Ext(filename)) 69 | f, err := os.Create(filename) 70 | if err != nil { 71 | return err 72 | } 73 | defer f.Close() 74 | return encoder(f, img) 75 | } 76 | -------------------------------------------------------------------------------- /imgio/io_test.go: -------------------------------------------------------------------------------- 1 | package imgio 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestEncode(t *testing.T) { 11 | cases := []struct { 12 | format string 13 | encoder Encoder 14 | value image.Image 15 | }{ 16 | { 17 | format: "png", 18 | encoder: PNGEncoder(), 19 | value: &image.RGBA{ 20 | Rect: image.Rect(0, 0, 3, 3), 21 | Stride: 3 * 4, 22 | Pix: []uint8{ 23 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 24 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 25 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 26 | }, 27 | }, 28 | }, 29 | { 30 | format: "jpg,jpeg", 31 | encoder: JPEGEncoder(95), 32 | value: &image.RGBA{ 33 | Rect: image.Rect(0, 0, 3, 3), 34 | Stride: 3 * 4, 35 | Pix: []uint8{ 36 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 37 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 38 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 39 | }, 40 | }, 41 | }, 42 | { 43 | format: "bmp", 44 | encoder: BMPEncoder(), 45 | value: &image.RGBA{ 46 | Rect: image.Rect(0, 0, 3, 3), 47 | Stride: 3 * 4, 48 | Pix: []uint8{ 49 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 50 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 51 | 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 52 | }, 53 | }, 54 | }, 55 | } 56 | 57 | for _, c := range cases { 58 | buf := bytes.Buffer{} 59 | c.encoder(&buf, c.value) 60 | _, outFormat, err := image.Decode(&buf) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | if !strings.Contains(c.format, outFormat) { 65 | t.Errorf("%s: expected: %#v, actual: %#v", "Encoder", c.format, outFormat) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/anthonynsimon/bild/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /math/f64/clamp.go: -------------------------------------------------------------------------------- 1 | /*Package f64 provides helper functions for the float64 type.*/ 2 | package f64 3 | 4 | // Clamp returns the value if it fits within the parameters min and max. 5 | // Otherwise returns the closest boundary parameter value. 6 | func Clamp(value, min, max float64) float64 { 7 | if value > max { 8 | return max 9 | } 10 | if value < min { 11 | return min 12 | } 13 | return value 14 | } 15 | -------------------------------------------------------------------------------- /math/f64/clamp_test.go: -------------------------------------------------------------------------------- 1 | package f64 2 | 3 | import "testing" 4 | 5 | func TestClamp(t *testing.T) { 6 | cases := []struct { 7 | value, min, max, expected float64 8 | }{ 9 | { 10 | value: 0.0, 11 | min: 0.0, 12 | max: 1.0, 13 | expected: 0.0, 14 | }, 15 | { 16 | value: 0.0, 17 | min: 1.0, 18 | max: 1.5, 19 | expected: 1.0, 20 | }, 21 | { 22 | value: 200.0, 23 | min: 0.0, 24 | max: 1.0, 25 | expected: 1.0, 26 | }, 27 | { 28 | value: -4561200.0, 29 | min: 0.0, 30 | max: 1.0, 31 | expected: 0.0, 32 | }, 33 | } 34 | 35 | for _, c := range cases { 36 | actual := Clamp(c.value, c.min, c.max) 37 | if actual != c.expected { 38 | t.Errorf("f64.Clamp: expected: %v actual: %v", c.expected, actual) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /math/integer/helpers.go: -------------------------------------------------------------------------------- 1 | /*Package integer provides helper functions for the integer type.*/ 2 | package integer 3 | 4 | // Min returns the parameter with the lowest value. 5 | func Min(a, b int) int { 6 | if a < b { 7 | return a 8 | } 9 | return b 10 | } 11 | 12 | // Max returns the parameter with the highest value. 13 | func Max(a, b int) int { 14 | if a > b { 15 | return a 16 | } 17 | return b 18 | } 19 | -------------------------------------------------------------------------------- /math/integer/helpers_test.go: -------------------------------------------------------------------------------- 1 | package integer 2 | 3 | import "testing" 4 | 5 | func TestMin(t *testing.T) { 6 | cases := []struct { 7 | a, b, expected int 8 | }{ 9 | { 10 | a: 0, 11 | b: 0, 12 | expected: 0, 13 | }, 14 | { 15 | a: 1, 16 | b: 1, 17 | expected: 1, 18 | }, 19 | { 20 | a: -1, 21 | b: 1, 22 | expected: -1, 23 | }, 24 | { 25 | a: 1, 26 | b: -1, 27 | expected: -1, 28 | }, 29 | { 30 | a: 10, 31 | b: 2, 32 | expected: 2, 33 | }, 34 | } 35 | 36 | for _, c := range cases { 37 | actual := Min(c.a, c.b) 38 | if actual != c.expected { 39 | t.Errorf("Min: expected: %v actual: %v", c.expected, actual) 40 | } 41 | } 42 | } 43 | 44 | func TestMax(t *testing.T) { 45 | cases := []struct { 46 | a, b, expected int 47 | }{ 48 | { 49 | a: 0, 50 | b: 0, 51 | expected: 0, 52 | }, 53 | { 54 | a: 1, 55 | b: 1, 56 | expected: 1, 57 | }, 58 | { 59 | a: -1, 60 | b: 1, 61 | expected: 1, 62 | }, 63 | { 64 | a: 1, 65 | b: -1, 66 | expected: 1, 67 | }, 68 | { 69 | a: 10, 70 | b: 2, 71 | expected: 10, 72 | }, 73 | } 74 | 75 | for _, c := range cases { 76 | actual := Max(c.a, c.b) 77 | if actual != c.expected { 78 | t.Errorf("Max: expected: %v actual: %v", c.expected, actual) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /noise/noise.go: -------------------------------------------------------------------------------- 1 | package noise 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math/rand" 7 | "time" 8 | 9 | "github.com/anthonynsimon/bild/parallel" 10 | "github.com/anthonynsimon/bild/perlin" 11 | ) 12 | 13 | // Fn is a noise function that generates values between 0 and 255. 14 | type Fn func() uint8 15 | 16 | var ( 17 | // Uniform distribution noise function. 18 | Uniform Fn 19 | // Binary distribution noise function. 20 | Binary Fn 21 | // Gaussian distribution noise function. 22 | Gaussian Fn 23 | ) 24 | 25 | func init() { 26 | Uniform = func() uint8 { 27 | return uint8(rand.Intn(256)) 28 | } 29 | Binary = func() uint8 { 30 | return 0xFF * uint8(rand.Intn(2)) 31 | } 32 | Gaussian = func() uint8 { 33 | return uint8(rand.NormFloat64()*32.0 + 128.0) 34 | } 35 | } 36 | 37 | // Options to configure the noise generation. 38 | type Options struct { 39 | // NoiseFn is a noise function that will be called for each pixel 40 | // on the image being generated. 41 | NoiseFn Fn 42 | // Monochrome sets if the resulting image is grayscale or colored, 43 | // the latter meaning that each RGB channel was filled with different values. 44 | Monochrome bool 45 | } 46 | 47 | // GeneratePerlin outputs the perlin image of given height and width and freqency 48 | func GeneratePerlin(width, height int, frequency float64) *image.RGBA { 49 | alpha, beta, n := 2., 2., 3 50 | 51 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 52 | p := perlin.NewPerlin(alpha, beta, n, rand.Int63()) 53 | 54 | for x := 0.; x < float64(height); x++ { 55 | for y := 0.; y < float64(width); y++ { 56 | t := p.Noise2D((x/10)*frequency, (y/10)*frequency) 57 | img.Set(int(x), int(y), color.NRGBA{ 58 | R: uint8((t + 1) * 126), 59 | G: uint8((t + 1) * 126), 60 | B: uint8((t + 1) * 126), 61 | A: 255, 62 | }) 63 | } 64 | } 65 | 66 | return img 67 | } 68 | 69 | // Generate returns an image of the parameter width and height filled 70 | // with the values from a noise function. 71 | // If no options are provided, defaults will be used. 72 | func Generate(width, height int, o *Options) *image.RGBA { 73 | dst := image.NewRGBA(image.Rect(0, 0, width, height)) 74 | 75 | // Get options or defaults 76 | noiseFn := Uniform 77 | monochrome := false 78 | if o != nil { 79 | if o.NoiseFn != nil { 80 | noiseFn = o.NoiseFn 81 | } 82 | monochrome = o.Monochrome 83 | } 84 | 85 | rand.Seed(time.Now().UTC().UnixNano()) 86 | 87 | if monochrome { 88 | fillMonochrome(dst, noiseFn) 89 | } else { 90 | fillColored(dst, noiseFn) 91 | } 92 | 93 | return dst 94 | } 95 | 96 | func fillMonochrome(img *image.RGBA, noiseFn Fn) { 97 | width, height := img.Bounds().Dx(), img.Bounds().Dy() 98 | parallel.Line(height, func(start, end int) { 99 | for y := start; y < end; y++ { 100 | for x := 0; x < width; x++ { 101 | pos := y*img.Stride + x*4 102 | v := noiseFn() 103 | 104 | img.Pix[pos+0] = v 105 | img.Pix[pos+1] = v 106 | img.Pix[pos+2] = v 107 | img.Pix[pos+3] = 0xFF 108 | } 109 | } 110 | }) 111 | } 112 | 113 | func fillColored(img *image.RGBA, noiseFn Fn) { 114 | width, height := img.Bounds().Dx(), img.Bounds().Dy() 115 | parallel.Line(height, func(start, end int) { 116 | for y := start; y < end; y++ { 117 | for x := 0; x < width; x++ { 118 | pos := y*img.Stride + x*4 119 | 120 | img.Pix[pos+0] = noiseFn() 121 | img.Pix[pos+1] = noiseFn() 122 | img.Pix[pos+2] = noiseFn() 123 | img.Pix[pos+3] = 0xFF 124 | } 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /noise/noise_test.go: -------------------------------------------------------------------------------- 1 | package noise 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/histogram" 8 | ) 9 | 10 | // benchResult is used to avoid having the compiler optimize the benchmark code calls 11 | var benchResult interface{} 12 | 13 | func TestMonochromeNoise(t *testing.T) { 14 | cases := []struct { 15 | w, h int 16 | o *Options 17 | }{ 18 | { 19 | w: 200, 20 | h: 200, 21 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 22 | }, 23 | { 24 | w: 512, 25 | h: 512, 26 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 27 | }, 28 | { 29 | w: 900, 30 | h: 200, 31 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 32 | }, 33 | { 34 | w: 5, 35 | h: 1000, 36 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 37 | }, 38 | } 39 | 40 | for _, c := range cases { 41 | result := Generate(c.w, c.h, c.o) 42 | checkPixels(result, isMonochrome, true, "UniformNoiseMonochrome: color not monochrome.", 0, t) 43 | } 44 | } 45 | 46 | func TestColorNoise(t *testing.T) { 47 | cases := []struct { 48 | w, h int 49 | o *Options 50 | }{ 51 | { 52 | w: 200, 53 | h: 200, 54 | o: &Options{NoiseFn: Uniform, Monochrome: false}, 55 | }, 56 | { 57 | w: 512, 58 | h: 512, 59 | o: &Options{NoiseFn: Uniform, Monochrome: false}, 60 | }, 61 | { 62 | w: 900, 63 | h: 200, 64 | o: &Options{NoiseFn: Uniform, Monochrome: false}, 65 | }, 66 | { 67 | w: 5, 68 | h: 1000, 69 | o: &Options{NoiseFn: Uniform, Monochrome: false}, 70 | }, 71 | } 72 | 73 | for _, c := range cases { 74 | result := Generate(c.w, c.h, c.o) 75 | checkPixels(result, isMonochrome, false, "ColorNoise: color is monochrome.", c.w*c.h/10, t) 76 | } 77 | } 78 | 79 | func TestUniformNoise(t *testing.T) { 80 | cases := []struct { 81 | w, h int 82 | o *Options 83 | }{ 84 | { 85 | w: 200, 86 | h: 200, 87 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 88 | }, 89 | { 90 | w: 512, 91 | h: 512, 92 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 93 | }, 94 | { 95 | w: 900, 96 | h: 200, 97 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 98 | }, 99 | { 100 | w: 5, 101 | h: 1000, 102 | o: &Options{NoiseFn: Uniform, Monochrome: true}, 103 | }, 104 | } 105 | 106 | for _, c := range cases { 107 | result := Generate(c.w, c.h, c.o) 108 | 109 | hist := histogram.NewRGBAHistogram(result).Cumulative() 110 | 111 | for i := 1; i < len(hist.R.Bins); i++ { 112 | // Fail if cumulative histogram does not follow a positive linear slope 113 | if hist.R.Bins[i] <= hist.R.Bins[i-1] || hist.G.Bins[i] <= hist.G.Bins[i-1] || hist.B.Bins[i] <= hist.B.Bins[i-1] { 114 | t.Errorf("UniformNoise: non uniform distribution.") 115 | break 116 | } 117 | } 118 | } 119 | } 120 | 121 | func TestBinaryNoise(t *testing.T) { 122 | cases := []struct { 123 | w, h int 124 | o *Options 125 | }{ 126 | { 127 | w: 200, 128 | h: 200, 129 | o: &Options{NoiseFn: Binary, Monochrome: true}, 130 | }, 131 | { 132 | w: 512, 133 | h: 512, 134 | o: &Options{NoiseFn: Binary, Monochrome: true}, 135 | }, 136 | { 137 | w: 900, 138 | h: 200, 139 | o: &Options{NoiseFn: Binary, Monochrome: true}, 140 | }, 141 | { 142 | w: 5, 143 | h: 1000, 144 | o: &Options{NoiseFn: Binary, Monochrome: true}, 145 | }, 146 | } 147 | 148 | for _, c := range cases { 149 | result := Generate(c.w, c.h, c.o) 150 | 151 | hist := histogram.NewRGBAHistogram(result) 152 | 153 | binCount := 0 154 | for i := 0; i < len(hist.R.Bins); i++ { 155 | if hist.R.Bins[i] != 0x00 && hist.G.Bins[i] != 0x00 && hist.B.Bins[i] != 0x00 { 156 | binCount++ 157 | } 158 | } 159 | 160 | if binCount != 2 { 161 | t.Errorf("BinaryNoise: non binary distribution.") 162 | break 163 | } 164 | } 165 | } 166 | 167 | func BenchmarkUniformMonochrome(b *testing.B) { 168 | for n := 0; n < b.N; n++ { 169 | benchResult = Generate(512, 512, &Options{NoiseFn: Uniform, Monochrome: true}) 170 | } 171 | } 172 | 173 | func BenchmarkUniformColored(b *testing.B) { 174 | for n := 0; n < b.N; n++ { 175 | benchResult = Generate(512, 512, &Options{NoiseFn: Uniform, Monochrome: false}) 176 | } 177 | } 178 | 179 | // checkPixels goes through each pixel in the image, extracting the RGBA channels and passing it through the 180 | // provided test function. If the result of the function and the expected bool don't match, then fail the test 181 | // with the provided message. Tolerance is the error count permitted. 182 | func checkPixels(img *image.RGBA, fn func(r, g, b, a uint8) bool, expected bool, failMsg string, tolerance int, t *testing.T) { 183 | errorCount := 0 184 | checkLoop: 185 | for x := 0; x < img.Bounds().Dx(); x++ { 186 | for y := 0; y < img.Bounds().Dy(); y++ { 187 | pos := y*img.Stride + x*4 188 | if fn(img.Pix[pos+0], img.Pix[pos+1], img.Pix[pos+2], img.Pix[pos+3]) != expected { 189 | errorCount++ 190 | if errorCount > tolerance { 191 | t.Errorf(failMsg) 192 | break checkLoop 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | func isMonochrome(r, g, b, a uint8) bool { 200 | if r == g && g == b { 201 | return true 202 | } 203 | return false 204 | } 205 | -------------------------------------------------------------------------------- /paint/fill.go: -------------------------------------------------------------------------------- 1 | /*Package paint provides functions to edit a group of pixels on an image.*/ 2 | package paint 3 | 4 | import ( 5 | "image" 6 | "image/color" 7 | "math" 8 | 9 | "github.com/anthonynsimon/bild/clone" 10 | "github.com/anthonynsimon/bild/util" 11 | ) 12 | 13 | type fillPoint struct { 14 | X, Y int 15 | MarkedFromBelow bool 16 | MarkedFromAbove bool 17 | PreviousFillEdgeLeft int 18 | PreviousFillEdgeRight int 19 | } 20 | 21 | // FloodFill fills a area of the image with a provided color and returns the new image. 22 | // Parameter sp is the starting point of the fill. 23 | // Parameter c is the fill color. 24 | // Parameter t is the tolerance and is of the range 0 to 255. It represents the max amount of 25 | // difference between colors for them to be considered similar. 26 | func FloodFill(img image.Image, sp image.Point, c color.Color, t uint8) *image.RGBA { 27 | var st util.Stack 28 | var point fillPoint 29 | visited := make(map[int]bool) 30 | im := clone.AsRGBA(img) 31 | 32 | maxX := im.Bounds().Dx() - 1 33 | maxY := im.Bounds().Dy() - 1 34 | if sp.X > maxX || sp.X < 0 || sp.Y > maxY || sp.Y < 0 { 35 | return im 36 | } 37 | 38 | tSquared := math.Pow(float64(t), 2) 39 | matchColor := color.NRGBAModel.Convert(im.At(sp.X, sp.Y)).(color.NRGBA) 40 | 41 | st.Push(fillPoint{sp.X, sp.Y, true, true, 0, 0}) 42 | 43 | // loop until there are no more points remaining 44 | for st.Len() > 0 { 45 | point = st.Pop().(fillPoint) 46 | pixOffset := im.PixOffset(point.X, point.Y) 47 | 48 | if !visited[pixOffset] { 49 | im.Set(point.X, point.Y, c) 50 | visited[pixOffset] = true 51 | 52 | // fill left side 53 | xpos := point.X 54 | for { 55 | xpos-- 56 | if xpos < 0 { 57 | xpos = 0 58 | break 59 | } 60 | pixOffset = im.PixOffset(xpos, point.Y) 61 | if isColorMatch(im, pixOffset, matchColor, tSquared) { 62 | im.Set(xpos, point.Y, c) 63 | visited[pixOffset] = true 64 | } else { 65 | break 66 | } 67 | } 68 | 69 | leftFillEdge := xpos - 1 70 | if leftFillEdge < 0 { 71 | leftFillEdge = 0 72 | } 73 | 74 | // fill right side 75 | xpos = point.X 76 | for { 77 | xpos++ 78 | if xpos > maxX { 79 | break 80 | } 81 | 82 | pixOffset = im.PixOffset(xpos, point.Y) 83 | if isColorMatch(im, pixOffset, matchColor, tSquared) { 84 | im.Set(xpos, point.Y, c) 85 | visited[pixOffset] = true 86 | } else { 87 | break 88 | } 89 | } 90 | rightFillEdge := xpos + 1 91 | if rightFillEdge > maxX { 92 | rightFillEdge = maxX 93 | } 94 | 95 | // skip every second check for pixels above and below 96 | skipCheckAbove := false 97 | skipCheckBelow := false 98 | 99 | // check pixels above/below the fill line 100 | for x := leftFillEdge; x <= rightFillEdge; x++ { 101 | outOfPreviousRange := x >= point.PreviousFillEdgeRight || x <= point.PreviousFillEdgeLeft 102 | 103 | if skipCheckBelow { 104 | skipCheckBelow = !skipCheckBelow 105 | } else { 106 | if point.MarkedFromBelow || outOfPreviousRange { 107 | if point.Y > 0 { 108 | pixOffset = im.PixOffset(x, point.Y-1) 109 | if !visited[pixOffset] && isColorMatch(im, pixOffset, matchColor, tSquared) { 110 | skipCheckBelow = true 111 | st.Push(fillPoint{x, (point.Y - 1), true, false, leftFillEdge, rightFillEdge}) 112 | } 113 | } 114 | } 115 | } 116 | 117 | if skipCheckAbove { 118 | skipCheckAbove = !skipCheckAbove 119 | } else { 120 | if point.MarkedFromAbove || outOfPreviousRange { 121 | if point.Y < maxY { 122 | 123 | pixOffset = im.PixOffset(x, point.Y+1) 124 | if !visited[pixOffset] && isColorMatch(im, pixOffset, matchColor, tSquared) { 125 | skipCheckAbove = true 126 | st.Push(fillPoint{x, (point.Y + 1), false, true, leftFillEdge, rightFillEdge}) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | return im 136 | } 137 | 138 | func isColorMatch(im *image.RGBA, pos int, mc color.NRGBA, tSquared float64) bool { 139 | rDiff := float64(mc.R) - float64(im.Pix[pos+0]) 140 | gDiff := float64(mc.G) - float64(im.Pix[pos+1]) 141 | bDiff := float64(mc.B) - float64(im.Pix[pos+2]) 142 | aDiff := float64(mc.A) - float64(im.Pix[pos+3]) 143 | 144 | distanceR := math.Max(rDiff*rDiff, math.Pow(rDiff-aDiff, 2)) 145 | distanceG := math.Max(gDiff*gDiff, math.Pow(gDiff-aDiff, 2)) 146 | distanceB := math.Max(bDiff*bDiff, math.Pow(bDiff-aDiff, 2)) 147 | distance := distanceR + distanceG + distanceB 148 | 149 | return distance <= tSquared 150 | } 151 | -------------------------------------------------------------------------------- /paint/fill_test.go: -------------------------------------------------------------------------------- 1 | package paint 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/anthonynsimon/bild/util" 9 | ) 10 | 11 | func TestFloodFill(t *testing.T) { 12 | cases := []struct { 13 | startPoint image.Point 14 | fillColor color.Color 15 | tolerance uint8 16 | value image.Image 17 | expected *image.RGBA 18 | }{ 19 | { 20 | tolerance: 0, 21 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 22 | startPoint: image.Point{0, 0}, 23 | value: &image.RGBA{ 24 | Rect: image.Rect(0, 0, 3, 3), 25 | Stride: 3 * 4, 26 | Pix: []uint8{ 27 | 0xFF, 0xFF, 0xFF, 0x00, 0x10, 0x10, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 28 | 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x10, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 29 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 30 | }, 31 | }, 32 | expected: &image.RGBA{ 33 | Rect: image.Rect(0, 0, 3, 3), 34 | Stride: 3 * 4, 35 | Pix: []uint8{ 36 | 0xAA, 0xAA, 0xAA, 0xAA, 0x10, 0x10, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 37 | 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x10, 0x10, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 38 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 39 | }, 40 | }, 41 | }, 42 | { 43 | tolerance: 0, 44 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 45 | startPoint: image.Point{0, 0}, 46 | value: &image.RGBA{ 47 | Rect: image.Rect(0, 0, 3, 3), 48 | Stride: 3 * 4, 49 | Pix: []uint8{ 50 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x88, 51 | 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 52 | 0xCC, 0xCC, 0xCC, 0xCC, 0xBB, 0xBB, 0xBB, 0xBB, 0xAA, 0xAA, 0xAA, 0xAA, 53 | }, 54 | }, 55 | expected: &image.RGBA{ 56 | Rect: image.Rect(0, 0, 3, 3), 57 | Stride: 3 * 4, 58 | Pix: []uint8{ 59 | 0xAA, 0xAA, 0xAA, 0xAA, 0x0, 0x0, 0x0, 0x0, 0x88, 0x88, 0x88, 0x88, 60 | 0xDD, 0xDD, 0xDD, 0xDD, 0x0, 0x0, 0x0, 0x0, 0x99, 0x99, 0x99, 0x99, 61 | 0xCC, 0xCC, 0xCC, 0xCC, 0xBB, 0xBB, 0xBB, 0xBB, 0xAA, 0xAA, 0xAA, 0xAA, 62 | }, 63 | }, 64 | }, 65 | { 66 | tolerance: 128, 67 | fillColor: color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, 68 | startPoint: image.Point{0, 0}, 69 | value: &image.RGBA{ 70 | Rect: image.Rect(0, 0, 3, 3), 71 | Stride: 3 * 4, 72 | Pix: []uint8{ 73 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x88, 74 | 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 75 | 0xCC, 0xCC, 0xCC, 0xCC, 0x20, 0x20, 0x20, 0x20, 0xAA, 0xAA, 0xAA, 0xAA, 76 | }, 77 | }, 78 | expected: &image.RGBA{ 79 | Rect: image.Rect(0, 0, 3, 3), 80 | Stride: 3 * 4, 81 | Pix: []uint8{ 82 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x88, 83 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 84 | 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x20, 0x20, 0x20, 0xAA, 0xAA, 0xAA, 0xAA, 85 | }, 86 | }, 87 | }, 88 | { 89 | tolerance: 255, 90 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 91 | startPoint: image.Point{1, 0}, 92 | value: &image.RGBA{ 93 | Rect: image.Rect(0, 0, 3, 3), 94 | Stride: 3 * 4, 95 | Pix: []uint8{ 96 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x88, 97 | 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x99, 0x99, 0x99, 0x99, 98 | 0xCC, 0xCC, 0xCC, 0xCC, 0xBB, 0xBB, 0xBB, 0xBB, 0xFF, 0xFF, 0xFF, 0xAA, 99 | }, 100 | }, 101 | expected: &image.RGBA{ 102 | Rect: image.Rect(0, 0, 3, 3), 103 | Stride: 3 * 4, 104 | Pix: []uint8{ 105 | 0xFF, 0xFF, 0xFF, 0xFF, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 106 | 0xDD, 0xDD, 0xDD, 0xDD, 0xAA, 0xAA, 0xAA, 0xAA, 0x99, 0x99, 0x99, 0x99, 107 | 0xCC, 0xCC, 0xCC, 0xCC, 0xBB, 0xBB, 0xBB, 0xBB, 0xFF, 0xFF, 0xFF, 0xAA, 108 | }, 109 | }, 110 | }, 111 | { 112 | tolerance: 128, 113 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 114 | startPoint: image.Point{1, 0}, 115 | value: &image.RGBA{ 116 | Rect: image.Rect(0, 0, 3, 3), 117 | Stride: 3 * 4, 118 | Pix: []uint8{ 119 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 120 | 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 121 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 122 | }, 123 | }, 124 | expected: &image.RGBA{ 125 | Rect: image.Rect(0, 0, 3, 3), 126 | Stride: 3 * 4, 127 | Pix: []uint8{ 128 | 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 129 | 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 130 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 131 | }, 132 | }, 133 | }, 134 | { 135 | tolerance: 255, 136 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 137 | startPoint: image.Point{1, 0}, 138 | value: &image.RGBA{ 139 | Rect: image.Rect(0, 0, 3, 3), 140 | Stride: 3 * 4, 141 | Pix: []uint8{ 142 | 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 143 | 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x80, 144 | 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 145 | }, 146 | }, 147 | expected: &image.RGBA{ 148 | Rect: image.Rect(0, 0, 3, 3), 149 | Stride: 3 * 4, 150 | Pix: []uint8{ 151 | 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 152 | 0xFF, 0xFF, 0x00, 0x80, 0xAA, 0xAA, 0xAA, 0xAA, 0xFF, 0x00, 0xFF, 0x80, 153 | 0xFF, 0xFF, 0x01, 0xFF, 0xAA, 0xAA, 0xAA, 0xAA, 0xFF, 0x01, 0xFF, 0xFF, 154 | }, 155 | }, 156 | }, 157 | { 158 | tolerance: 0, 159 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 160 | startPoint: image.Point{2, 0}, 161 | value: &image.RGBA{ 162 | Rect: image.Rect(0, 0, 1, 1), 163 | Stride: 1 * 4, 164 | Pix: []uint8{ 165 | 0x00, 0x00, 0x00, 0x00, 166 | }, 167 | }, 168 | expected: &image.RGBA{ 169 | Rect: image.Rect(0, 0, 1, 1), 170 | Stride: 1 * 4, 171 | Pix: []uint8{ 172 | 0x00, 0x00, 0x00, 0x00, 173 | }, 174 | }, 175 | }, 176 | { 177 | tolerance: 0, 178 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 179 | startPoint: image.Point{0, 2}, 180 | value: &image.RGBA{ 181 | Rect: image.Rect(0, 0, 1, 1), 182 | Stride: 1 * 4, 183 | Pix: []uint8{ 184 | 0x00, 0x00, 0x00, 0x00, 185 | }, 186 | }, 187 | expected: &image.RGBA{ 188 | Rect: image.Rect(0, 0, 1, 1), 189 | Stride: 1 * 4, 190 | Pix: []uint8{ 191 | 0x00, 0x00, 0x00, 0x00, 192 | }, 193 | }, 194 | }, 195 | { 196 | tolerance: 0, 197 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 198 | startPoint: image.Point{-1, 0}, 199 | value: &image.RGBA{ 200 | Rect: image.Rect(0, 0, 1, 1), 201 | Stride: 1 * 4, 202 | Pix: []uint8{ 203 | 0x00, 0x00, 0x00, 0x00, 204 | }, 205 | }, 206 | expected: &image.RGBA{ 207 | Rect: image.Rect(0, 0, 1, 1), 208 | Stride: 1 * 4, 209 | Pix: []uint8{ 210 | 0x00, 0x00, 0x00, 0x00, 211 | }, 212 | }, 213 | }, 214 | { 215 | tolerance: 0, 216 | fillColor: color.RGBA{0xAA, 0xAA, 0xAA, 0xAA}, 217 | startPoint: image.Point{0, -1}, 218 | value: &image.RGBA{ 219 | Rect: image.Rect(0, 0, 1, 1), 220 | Stride: 1 * 4, 221 | Pix: []uint8{ 222 | 0x00, 0x00, 0x00, 0x00, 223 | }, 224 | }, 225 | expected: &image.RGBA{ 226 | Rect: image.Rect(0, 0, 1, 1), 227 | Stride: 1 * 4, 228 | Pix: []uint8{ 229 | 0x00, 0x00, 0x00, 0x00, 230 | }, 231 | }, 232 | }, 233 | } 234 | 235 | for _, c := range cases { 236 | actual := FloodFill(c.value, c.startPoint, c.fillColor, c.tolerance) 237 | if !util.RGBAImageEqual(actual, c.expected) { 238 | t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Flood Fill", util.RGBAToString(c.expected), util.RGBAToString(actual)) 239 | } 240 | } 241 | } 242 | 243 | func BenchmarkFloodFill(b *testing.B) { 244 | 245 | img := image.NewRGBA(image.Rect(0, 0, 500, 500)) 246 | 247 | for n := 0; n < b.N; n++ { 248 | FloodFill(img, image.Point{0, 0}, color.RGBA{128, 0, 128, 128}, 128) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /parallel/parallel.go: -------------------------------------------------------------------------------- 1 | /*Package parallel provides helper functions for the dispatching of parallel jobs.*/ 2 | package parallel 3 | 4 | import ( 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | func init() { 10 | runtime.GOMAXPROCS(runtime.NumCPU()) 11 | } 12 | 13 | // Line dispatches a parameter fn into multiple goroutines by splitting the parameter length 14 | // by the number of available CPUs and assigning the length parts into each fn. 15 | func Line(length int, fn func(start, end int)) { 16 | procs := runtime.GOMAXPROCS(0) 17 | counter := length 18 | partSize := length / procs 19 | if procs <= 1 || partSize <= procs { 20 | fn(0, length) 21 | } else { 22 | var wg sync.WaitGroup 23 | for counter > 0 { 24 | start := counter - partSize 25 | end := counter 26 | if start < 0 { 27 | start = 0 28 | } 29 | counter -= partSize 30 | wg.Add(1) 31 | go func() { 32 | defer wg.Done() 33 | fn(start, end) 34 | }() 35 | } 36 | 37 | wg.Wait() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /parallel/parallel_test.go: -------------------------------------------------------------------------------- 1 | package parallel 2 | 3 | import "testing" 4 | 5 | func TestParallelize(t *testing.T) { 6 | for n := 0; n < 1024; n++ { 7 | data := make([]bool, n) 8 | 9 | Line(len(data), func(start, end int) { 10 | for i := start; i < end; i++ { 11 | data[i] = !data[i] 12 | } 13 | }) 14 | 15 | for _, d := range data { 16 | if !d { 17 | t.Errorf("Test parallelize failed. Failure at n = %v", n) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /perlin/perlin.go: -------------------------------------------------------------------------------- 1 | // file is taken from aquilax/go-perlin 2 | 3 | // Package perlin provides coherent noise function over 1, 2 or 3 dimensions 4 | // This code is go adaptagion based on C implementation that can be found here: 5 | // http://git.gnome.org/browse/gegl/tree/operations/common/perlin/perlin.c 6 | // (original copyright Ken Perlin) 7 | package perlin 8 | 9 | import ( 10 | "math" 11 | "math/rand" 12 | ) 13 | 14 | // General constants 15 | const ( 16 | B = 0x100 17 | N = 0x1000 18 | BM = 0xff 19 | ) 20 | 21 | // Perlin is the noise generator 22 | type Perlin struct { 23 | alpha float64 24 | beta float64 25 | n int 26 | 27 | p [B + B + 2]int 28 | g3 [B + B + 2][3]float64 29 | g2 [B + B + 2][2]float64 30 | g1 [B + B + 2]float64 31 | } 32 | 33 | // NewPerlin creates new Perlin noise generator 34 | // In what follows "alpha" is the weight when the sum is formed. 35 | // Typically it is 2, As this approaches 1 the function is noisier. 36 | // "beta" is the harmonic scaling/spacing, typically 2, n is the 37 | // number of iterations and seed is the math.rand seed value to use 38 | func NewPerlin(alpha, beta float64, n int, seed int64) *Perlin { 39 | return NewPerlinRandSource(alpha, beta, n, rand.NewSource(seed)) 40 | } 41 | 42 | // NewPerlinRandSource creates new Perlin noise generator 43 | // In what follows "alpha" is the weight when the sum is formed. 44 | // Typically it is 2, As this approaches 1 the function is noisier. 45 | // "beta" is the harmonic scaling/spacing, typically 2, n is the 46 | // number of iterations and source is source of pseudo-random int64 values 47 | func NewPerlinRandSource(alpha, beta float64, n int, source rand.Source) *Perlin { 48 | var p Perlin 49 | var i int 50 | 51 | p.alpha = alpha 52 | p.beta = beta 53 | p.n = n 54 | 55 | r := rand.New(source) 56 | 57 | for i = 0; i < B; i++ { 58 | p.p[i] = i 59 | p.g1[i] = float64((r.Int()%(B+B))-B) / B 60 | 61 | for j := 0; j < 2; j++ { 62 | p.g2[i][j] = float64((r.Int()%(B+B))-B) / B 63 | } 64 | 65 | normalize2(&p.g2[i]) 66 | 67 | for j := 0; j < 3; j++ { 68 | p.g3[i][j] = float64((r.Int()%(B+B))-B) / B 69 | } 70 | normalize3(&p.g3[i]) 71 | } 72 | 73 | for ; i > 0; i-- { 74 | k := p.p[i] 75 | j := r.Int() % B 76 | p.p[i] = p.p[j] 77 | p.p[j] = k 78 | } 79 | 80 | for i := 0; i < B+2; i++ { 81 | p.p[B+i] = p.p[i] 82 | p.g1[B+i] = p.g1[i] 83 | for j := 0; j < 2; j++ { 84 | p.g2[B+i][j] = p.g2[i][j] 85 | } 86 | for j := 0; j < 3; j++ { 87 | p.g3[B+i][j] = p.g3[i][j] 88 | } 89 | } 90 | 91 | return &p 92 | } 93 | 94 | func normalize2(v *[2]float64) { 95 | s := math.Sqrt(v[0]*v[0] + v[1]*v[1]) 96 | v[0] = v[0] / s 97 | v[1] = v[1] / s 98 | } 99 | 100 | func normalize3(v *[3]float64) { 101 | s := math.Sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]) 102 | v[0] = v[0] / s 103 | v[1] = v[1] / s 104 | v[2] = v[2] / s 105 | } 106 | 107 | func at2(rx, ry float64, q [2]float64) float64 { 108 | return rx*q[0] + ry*q[1] 109 | } 110 | 111 | func at3(rx, ry, rz float64, q [3]float64) float64 { 112 | return rx*q[0] + ry*q[1] + rz*q[2] 113 | } 114 | 115 | func sCurve(t float64) float64 { 116 | return t * t * (3. - 2.*t) 117 | } 118 | 119 | func lerp(t, a, b float64) float64 { 120 | return a + t*(b-a) 121 | } 122 | 123 | func (p *Perlin) noise1(arg float64) float64 { 124 | var vec [1]float64 125 | vec[0] = arg 126 | 127 | t := vec[0] + N 128 | bx0 := int(t) & BM 129 | bx1 := (bx0 + 1) & BM 130 | rx0 := t - float64(int(t)) 131 | rx1 := rx0 - 1. 132 | 133 | sx := sCurve(rx0) 134 | u := rx0 * p.g1[p.p[bx0]] 135 | v := rx1 * p.g1[p.p[bx1]] 136 | 137 | return lerp(sx, u, v) 138 | } 139 | 140 | func (p *Perlin) noise2(vec [2]float64) float64 { 141 | 142 | t := vec[0] + N 143 | bx0 := int(t) & BM 144 | bx1 := (bx0 + 1) & BM 145 | rx0 := t - float64(int(t)) 146 | rx1 := rx0 - 1. 147 | 148 | t = vec[1] + N 149 | by0 := int(t) & BM 150 | by1 := (by0 + 1) & BM 151 | ry0 := t - float64(int(t)) 152 | ry1 := ry0 - 1. 153 | 154 | i := p.p[bx0] 155 | j := p.p[bx1] 156 | 157 | b00 := p.p[i+by0] 158 | b10 := p.p[j+by0] 159 | b01 := p.p[i+by1] 160 | b11 := p.p[j+by1] 161 | 162 | sx := sCurve(rx0) 163 | sy := sCurve(ry0) 164 | 165 | q := p.g2[b00] 166 | u := at2(rx0, ry0, q) 167 | q = p.g2[b10] 168 | v := at2(rx1, ry0, q) 169 | a := lerp(sx, u, v) 170 | 171 | q = p.g2[b01] 172 | u = at2(rx0, ry1, q) 173 | q = p.g2[b11] 174 | v = at2(rx1, ry1, q) 175 | b := lerp(sx, u, v) 176 | 177 | return lerp(sy, a, b) 178 | } 179 | 180 | func (p *Perlin) noise3(vec [3]float64) float64 { 181 | t := vec[0] + N 182 | bx0 := int(t) & BM 183 | bx1 := (bx0 + 1) & BM 184 | rx0 := t - float64(int(t)) 185 | rx1 := rx0 - 1. 186 | 187 | t = vec[1] + N 188 | by0 := int(t) & BM 189 | by1 := (by0 + 1) & BM 190 | ry0 := t - float64(int(t)) 191 | ry1 := ry0 - 1. 192 | 193 | t = vec[2] + N 194 | bz0 := int(t) & BM 195 | bz1 := (bz0 + 1) & BM 196 | rz0 := t - float64(int(t)) 197 | rz1 := rz0 - 1. 198 | 199 | i := p.p[bx0] 200 | j := p.p[bx1] 201 | 202 | b00 := p.p[i+by0] 203 | b10 := p.p[j+by0] 204 | b01 := p.p[i+by1] 205 | b11 := p.p[j+by1] 206 | 207 | t = sCurve(rx0) 208 | sy := sCurve(ry0) 209 | sz := sCurve(rz0) 210 | 211 | q := p.g3[b00+bz0] 212 | u := at3(rx0, ry0, rz0, q) 213 | q = p.g3[b10+bz0] 214 | v := at3(rx1, ry0, rz0, q) 215 | a := lerp(t, u, v) 216 | 217 | q = p.g3[b01+bz0] 218 | u = at3(rx0, ry1, rz0, q) 219 | q = p.g3[b11+bz0] 220 | v = at3(rx1, ry1, rz0, q) 221 | b := lerp(t, u, v) 222 | 223 | c := lerp(sy, a, b) 224 | 225 | q = p.g3[b00+bz1] 226 | u = at3(rx0, ry0, rz1, q) 227 | q = p.g3[b10+bz1] 228 | v = at3(rx1, ry0, rz1, q) 229 | a = lerp(t, u, v) 230 | 231 | q = p.g3[b01+bz1] 232 | u = at3(rx0, ry1, rz1, q) 233 | q = p.g3[b11+bz1] 234 | v = at3(rx1, ry1, rz1, q) 235 | b = lerp(t, u, v) 236 | 237 | d := lerp(sy, a, b) 238 | 239 | return lerp(sz, c, d) 240 | } 241 | 242 | // Noise1D generates 1-dimensional Perlin Noise value 243 | func (p *Perlin) Noise1D(x float64) float64 { 244 | var scale float64 = 1 245 | var sum float64 246 | px := x 247 | 248 | for i := 0; i < p.n; i++ { 249 | val := p.noise1(px) 250 | sum += val / scale 251 | scale *= p.alpha 252 | px *= p.beta 253 | } 254 | return sum 255 | } 256 | 257 | // Noise2D Generates 2-dimensional Perlin Noise value 258 | func (p *Perlin) Noise2D(x, y float64) float64 { 259 | var scale float64 = 1 260 | var sum float64 261 | var px [2]float64 262 | 263 | px[0] = x 264 | px[1] = y 265 | 266 | for i := 0; i < p.n; i++ { 267 | val := p.noise2(px) 268 | sum += val / scale 269 | scale *= p.alpha 270 | px[0] *= p.beta 271 | px[1] *= p.beta 272 | } 273 | return sum 274 | } 275 | 276 | // Noise3D Generates 3-dimensional Perlin Noise value 277 | func (p *Perlin) Noise3D(x, y, z float64) float64 { 278 | var scale float64 = 1 279 | var sum float64 280 | var px [3]float64 281 | 282 | if z < 0.0000 { 283 | return p.Noise2D(x, y) 284 | } 285 | px[0] = x 286 | px[1] = y 287 | px[2] = z 288 | 289 | for i := 0; i < p.n; i++ { 290 | val := p.noise3(px) 291 | sum += val / scale 292 | scale *= p.alpha 293 | px[0] *= p.beta 294 | px[1] *= p.beta 295 | px[2] *= p.beta 296 | } 297 | return sum 298 | } 299 | -------------------------------------------------------------------------------- /segment/thresholding.go: -------------------------------------------------------------------------------- 1 | /*Package segment provides basic image segmentation and clusterring methods.*/ 2 | package segment 3 | 4 | import ( 5 | "image" 6 | "image/color" 7 | 8 | "github.com/anthonynsimon/bild/clone" 9 | "github.com/anthonynsimon/bild/util" 10 | ) 11 | 12 | // Threshold returns a grayscale image in which values from the param img that are 13 | // smaller than the param level are set to black and values larger than or equal to 14 | // it are set to white. 15 | // Level must be of the range 0 to 255. 16 | func Threshold(img image.Image, level uint8) *image.Gray { 17 | src := clone.AsRGBA(img) 18 | bounds := src.Bounds() 19 | 20 | dst := image.NewGray(bounds) 21 | 22 | for y := 0; y < bounds.Dy(); y++ { 23 | for x := 0; x < bounds.Dx(); x++ { 24 | srcPos := y*src.Stride + x*4 25 | dstPos := y*dst.Stride + x 26 | 27 | c := src.Pix[srcPos : srcPos+4] 28 | r := util.Rank(color.RGBA{c[0], c[1], c[2], c[3]}) 29 | 30 | // transparent pixel is always white 31 | if c[0] == 0 && c[1] == 0 && c[2] == 0 && c[3] == 0 { 32 | dst.Pix[dstPos] = 0xFF 33 | continue 34 | } 35 | 36 | if uint8(r) >= level { 37 | dst.Pix[dstPos] = 0xFF 38 | } else { 39 | dst.Pix[dstPos] = 0x00 40 | } 41 | } 42 | } 43 | 44 | return dst 45 | } 46 | -------------------------------------------------------------------------------- /segment/thresholding_test.go: -------------------------------------------------------------------------------- 1 | package segment 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/util" 8 | ) 9 | 10 | func TestThreshold(t *testing.T) { 11 | cases := []struct { 12 | level uint8 13 | img image.Image 14 | expected *image.Gray 15 | }{ 16 | { 17 | level: 0, 18 | img: &image.RGBA{ 19 | Rect: image.Rect(0, 0, 2, 2), 20 | Stride: 2 * 4, 21 | Pix: []uint8{ 22 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 23 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0x80, 24 | }, 25 | }, 26 | expected: &image.Gray{ 27 | Rect: image.Rect(0, 0, 2, 2), 28 | Stride: 2, 29 | Pix: []uint8{ 30 | 0xFF, 0xFF, 31 | 0xFF, 0xFF, 32 | }, 33 | }, 34 | }, 35 | { 36 | level: 128, 37 | img: &image.RGBA{ 38 | Rect: image.Rect(0, 0, 2, 2), 39 | Stride: 2 * 4, 40 | Pix: []uint8{ 41 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 42 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0x80, 43 | }, 44 | }, 45 | expected: &image.Gray{ 46 | Rect: image.Rect(0, 0, 2, 2), 47 | Stride: 2, 48 | Pix: []uint8{ 49 | 0x00, 0xFF, 50 | 0xFF, 0x00, 51 | }, 52 | }, 53 | }, 54 | { 55 | level: 255, 56 | img: &image.RGBA{ 57 | Rect: image.Rect(0, 0, 2, 2), 58 | Stride: 2 * 4, 59 | Pix: []uint8{ 60 | 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 61 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0x80, 62 | }, 63 | }, 64 | expected: &image.Gray{ 65 | Rect: image.Rect(0, 0, 2, 2), 66 | Stride: 2, 67 | Pix: []uint8{ 68 | 0x00, 0xFF, 69 | 0xFF, 0x00, 70 | }, 71 | }, 72 | }, 73 | { 74 | level: 127, 75 | img: &image.RGBA{ 76 | Rect: image.Rect(0, 0, 2, 2), 77 | Stride: 2 * 4, 78 | Pix: []uint8{ 79 | 0x80, 0x80, 0x80, 0xFF, 0xC0, 0xC0, 0xC0, 0xFF, 80 | 0x40, 0x40, 0x40, 0x40, 0x80, 0x80, 0x80, 0x80, 81 | }, 82 | }, 83 | expected: &image.Gray{ 84 | Rect: image.Rect(0, 0, 2, 2), 85 | Stride: 2, 86 | Pix: []uint8{ 87 | 0xFF, 0xFF, 88 | 0x00, 0xFF, 89 | }, 90 | }, 91 | }, 92 | } 93 | 94 | for _, c := range cases { 95 | actual := Threshold(c.img, c.level) 96 | if !util.GrayImageEqual(actual, c.expected) { 97 | t.Errorf("%s: expected: %v actual: %v", "Threshold", c.expected, actual) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /transform/filters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package transform provides basic image transformation functions, such as resizing, rotation and flipping. 3 | It includes a variety of resampling filters to handle interpolation in case that upsampling or downsampling is required. 4 | */ 5 | package transform 6 | 7 | import "math" 8 | 9 | // ResampleFilter is used to evaluate sample points and interpolate between them. 10 | // Support is the number of points required by the filter per 'side'. 11 | // For example, a support of 1.0 means that the filter will get pixels on 12 | // positions -1 and +1 away from it. 13 | // Fn is the resample filter function to evaluate the samples. 14 | type ResampleFilter struct { 15 | Support float64 16 | Fn func(x float64) float64 17 | } 18 | 19 | // NearestNeighbor resampling filter assigns to each point the sample point nearest to it. 20 | var NearestNeighbor ResampleFilter 21 | 22 | // Box resampling filter, only let pass values in the x < 0.5 range from sample. 23 | // It produces similar results to the Nearest Neighbor method. 24 | var Box ResampleFilter 25 | 26 | // Linear resampling filter interpolates linearly between the two nearest samples per dimension. 27 | var Linear ResampleFilter 28 | 29 | // Gaussian resampling filter interpolates using a Gaussian function between the two nearest 30 | // samples per dimension. 31 | var Gaussian ResampleFilter 32 | 33 | // MitchellNetravali resampling filter interpolates between the four nearest samples per dimension. 34 | var MitchellNetravali ResampleFilter 35 | 36 | // CatmullRom resampling filter interpolates between the four nearest samples per dimension. 37 | var CatmullRom ResampleFilter 38 | 39 | // Lanczos resampling filter interpolates between the six nearest samples per dimension. 40 | var Lanczos ResampleFilter 41 | 42 | func init() { 43 | NearestNeighbor = ResampleFilter{ 44 | Support: 0, 45 | Fn: nil, 46 | } 47 | Box = ResampleFilter{ 48 | Support: 0.5, 49 | Fn: func(x float64) float64 { 50 | if math.Abs(x) < 0.5 { 51 | return 1 52 | } 53 | return 0 54 | }, 55 | } 56 | Linear = ResampleFilter{ 57 | Support: 1.0, 58 | Fn: func(x float64) float64 { 59 | x = math.Abs(x) 60 | if x < 1.0 { 61 | return 1.0 - x 62 | } 63 | return 0 64 | }, 65 | } 66 | Gaussian = ResampleFilter{ 67 | Support: 1.0, 68 | Fn: func(x float64) float64 { 69 | x = math.Abs(x) 70 | if x < 1.0 { 71 | exp := 2.0 72 | x *= 2.0 73 | y := math.Pow(0.5, math.Pow(x, exp)) 74 | base := math.Pow(0.5, math.Pow(2, exp)) 75 | return (y - base) / (1 - base) 76 | } 77 | return 0 78 | }, 79 | } 80 | MitchellNetravali = ResampleFilter{ 81 | Support: 2.0, 82 | Fn: func(x float64) float64 { 83 | b := 1.0 / 3 84 | c := 1.0 / 3 85 | var w [4]float64 86 | x = math.Abs(x) 87 | 88 | if x < 1.0 { 89 | w[0] = 0 90 | w[1] = 6 - 2*b 91 | w[2] = (-18 + 12*b + 6*c) * x * x 92 | w[3] = (12 - 9*b - 6*c) * x * x * x 93 | } else if x <= 2.0 { 94 | w[0] = 8*b + 24*c 95 | w[1] = (-12*b - 48*c) * x 96 | w[2] = (6*b + 30*c) * x * x 97 | w[3] = (-b - 6*c) * x * x * x 98 | } else { 99 | return 0 100 | } 101 | 102 | return (w[0] + w[1] + w[2] + w[3]) / 6 103 | }, 104 | } 105 | CatmullRom = ResampleFilter{ 106 | Support: 2.0, 107 | Fn: func(x float64) float64 { 108 | b := 0.0 109 | c := 0.5 110 | var w [4]float64 111 | x = math.Abs(x) 112 | 113 | if x < 1.0 { 114 | w[0] = 0 115 | w[1] = 6 - 2*b 116 | w[2] = (-18 + 12*b + 6*c) * x * x 117 | w[3] = (12 - 9*b - 6*c) * x * x * x 118 | } else if x <= 2.0 { 119 | w[0] = 8*b + 24*c 120 | w[1] = (-12*b - 48*c) * x 121 | w[2] = (6*b + 30*c) * x * x 122 | w[3] = (-b - 6*c) * x * x * x 123 | } else { 124 | return 0 125 | } 126 | 127 | return (w[0] + w[1] + w[2] + w[3]) / 6 128 | }, 129 | } 130 | Lanczos = ResampleFilter{ 131 | Support: 3.0, 132 | Fn: func(x float64) float64 { 133 | x = math.Abs(x) 134 | if x == 0 { 135 | return 1.0 136 | } else if x < 3.0 { 137 | return (3.0 * math.Sin(math.Pi*x) * math.Sin(math.Pi*(x/3.0))) / (math.Pi * math.Pi * x * x) 138 | } 139 | return 0.0 140 | }, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /transform/resize.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "image" 5 | "math" 6 | 7 | "github.com/anthonynsimon/bild/clone" 8 | "github.com/anthonynsimon/bild/math/f64" 9 | "github.com/anthonynsimon/bild/parallel" 10 | ) 11 | 12 | // Resize returns a new image with its size adjusted to the new width and height. The filter 13 | // param corresponds to the Resampling Filter to be used when interpolating between the sample points. 14 | // 15 | // Usage example: 16 | // 17 | // result := transform.Resize(img, 800, 600, transform.Linear) 18 | func Resize(img image.Image, width, height int, filter ResampleFilter) *image.RGBA { 19 | if width <= 0 || height <= 0 || img.Bounds().Empty() { 20 | return image.NewRGBA(image.Rect(0, 0, 0, 0)) 21 | } 22 | 23 | src := clone.AsShallowRGBA(img) 24 | var dst *image.RGBA 25 | 26 | // NearestNeighbor is a special case, it's faster to compute without convolution matrix. 27 | if filter.Support <= 0 { 28 | dst = nearestNeighbor(src, width, height) 29 | } else { 30 | dst = resampleHorizontal(src, width, filter) 31 | dst = resampleVertical(dst, height, filter) 32 | } 33 | 34 | return dst 35 | } 36 | 37 | // Crop returns a new image which contains the intersection between the rect and the image provided as params. 38 | // Only the intersection is returned. If a rect larger than the image is provided, no fill is done to 39 | // the 'empty' area. 40 | // 41 | // Usage example: 42 | // 43 | // result := transform.Crop(img, image.Rect(0, 0, 512, 256)) 44 | func Crop(img image.Image, rect image.Rectangle) *image.RGBA { 45 | src := clone.AsShallowRGBA(img) 46 | return clone.AsRGBA(src.SubImage(rect)) 47 | } 48 | 49 | func resampleHorizontal(src *image.RGBA, width int, filter ResampleFilter) *image.RGBA { 50 | srcWidth, srcHeight := src.Bounds().Dx(), src.Bounds().Dy() 51 | srcStride := src.Stride 52 | 53 | delta := float64(srcWidth) / float64(width) 54 | // Scale must be at least 1. Special case for image size reduction filter radius. 55 | scale := math.Max(delta, 1.0) 56 | 57 | dst := image.NewRGBA(image.Rect(0, 0, width, srcHeight)) 58 | dstStride := dst.Stride 59 | 60 | filterRadius := math.Ceil(scale * filter.Support) 61 | 62 | parallel.Line(srcHeight, func(start, end int) { 63 | for y := start; y < end; y++ { 64 | for x := 0; x < width; x++ { 65 | // value of x from src 66 | ix := (float64(x)+0.5)*delta - 0.5 67 | istart, iend := int(ix-filterRadius+0.5), int(ix+filterRadius) 68 | 69 | if istart < 0 { 70 | istart = 0 71 | } 72 | if iend >= srcWidth { 73 | iend = srcWidth - 1 74 | } 75 | 76 | var r, g, b, a float64 77 | var sum float64 78 | for kx := istart; kx <= iend; kx++ { 79 | 80 | srcPos := y*srcStride + kx*4 81 | // normalize the sample position to be evaluated by the filter 82 | normPos := (float64(kx) - ix) / scale 83 | fValue := filter.Fn(normPos) 84 | 85 | r += float64(src.Pix[srcPos+0]) * fValue 86 | g += float64(src.Pix[srcPos+1]) * fValue 87 | b += float64(src.Pix[srcPos+2]) * fValue 88 | a += float64(src.Pix[srcPos+3]) * fValue 89 | sum += fValue 90 | } 91 | 92 | dstPos := y*dstStride + x*4 93 | dst.Pix[dstPos+0] = uint8(f64.Clamp((r/sum)+0.5, 0, 255)) 94 | dst.Pix[dstPos+1] = uint8(f64.Clamp((g/sum)+0.5, 0, 255)) 95 | dst.Pix[dstPos+2] = uint8(f64.Clamp((b/sum)+0.5, 0, 255)) 96 | dst.Pix[dstPos+3] = uint8(f64.Clamp((a/sum)+0.5, 0, 255)) 97 | } 98 | } 99 | }) 100 | 101 | return dst 102 | } 103 | 104 | func resampleVertical(src *image.RGBA, height int, filter ResampleFilter) *image.RGBA { 105 | srcWidth, srcHeight := src.Bounds().Dx(), src.Bounds().Dy() 106 | srcStride := src.Stride 107 | 108 | delta := float64(srcHeight) / float64(height) 109 | scale := math.Max(delta, 1.0) 110 | 111 | dst := image.NewRGBA(image.Rect(0, 0, srcWidth, height)) 112 | dstStride := dst.Stride 113 | 114 | filterRadius := math.Ceil(scale * filter.Support) 115 | 116 | parallel.Line(height, func(start, end int) { 117 | for y := start; y < end; y++ { 118 | iy := (float64(y)+0.5)*delta - 0.5 119 | 120 | istart, iend := int(iy-filterRadius+0.5), int(iy+filterRadius) 121 | 122 | if istart < 0 { 123 | istart = 0 124 | } 125 | if iend >= srcHeight { 126 | iend = srcHeight - 1 127 | } 128 | 129 | for x := 0; x < srcWidth; x++ { 130 | var r, g, b, a float64 131 | var sum float64 132 | for ky := istart; ky <= iend; ky++ { 133 | 134 | srcPos := ky*srcStride + x*4 135 | normPos := (float64(ky) - iy) / scale 136 | fValue := filter.Fn(normPos) 137 | 138 | r += float64(src.Pix[srcPos+0]) * fValue 139 | g += float64(src.Pix[srcPos+1]) * fValue 140 | b += float64(src.Pix[srcPos+2]) * fValue 141 | a += float64(src.Pix[srcPos+3]) * fValue 142 | sum += fValue 143 | } 144 | 145 | dstPos := y*dstStride + x*4 146 | dst.Pix[dstPos+0] = uint8(f64.Clamp((r/sum)+0.5, 0, 255)) 147 | dst.Pix[dstPos+1] = uint8(f64.Clamp((g/sum)+0.5, 0, 255)) 148 | dst.Pix[dstPos+2] = uint8(f64.Clamp((b/sum)+0.5, 0, 255)) 149 | dst.Pix[dstPos+3] = uint8(f64.Clamp((a/sum)+0.5, 0, 255)) 150 | } 151 | } 152 | }) 153 | 154 | return dst 155 | } 156 | 157 | func nearestNeighbor(src *image.RGBA, width, height int) *image.RGBA { 158 | srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() 159 | srcStride := src.Stride 160 | 161 | dst := image.NewRGBA(image.Rect(0, 0, width, height)) 162 | dstStride := dst.Stride 163 | 164 | dx := float64(srcW) / float64(width) 165 | dy := float64(srcH) / float64(height) 166 | 167 | for y := 0; y < height; y++ { 168 | for x := 0; x < width; x++ { 169 | pos := y*dstStride + x*4 170 | ipos := int((float64(y)+0.5)*dy)*srcStride + int((float64(x)+0.5)*dx)*4 171 | 172 | dst.Pix[pos+0] = src.Pix[ipos+0] 173 | dst.Pix[pos+1] = src.Pix[ipos+1] 174 | dst.Pix[pos+2] = src.Pix[ipos+2] 175 | dst.Pix[pos+3] = src.Pix[ipos+3] 176 | } 177 | } 178 | 179 | return dst 180 | } 181 | -------------------------------------------------------------------------------- /transform/rotate.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math" 7 | 8 | "github.com/anthonynsimon/bild/clone" 9 | "github.com/anthonynsimon/bild/parallel" 10 | ) 11 | 12 | // RotationOptions are the rotation parameters 13 | // ResizeBounds set to false will keep the original image bounds, cutting any 14 | // pixels that go past it when rotating. 15 | // Pivot is the point of anchor for the rotation. Default of center is used if a nil is passed. 16 | // If ResizeBounds is set to true, a center pivot will always be used. 17 | type RotationOptions struct { 18 | ResizeBounds bool 19 | Pivot *image.Point 20 | } 21 | 22 | // Rotate returns a rotated image by the provided angle using the pivot as an anchor. 23 | // Parameters angle is in degrees and it's applied clockwise. 24 | // Default parameters are used if a nil *RotationOptions is passed. 25 | // 26 | // Usage example: 27 | // 28 | // // Rotate 90.0 degrees clockwise, preserving the image size and the pivot point at the top left corner 29 | // result := transform.Rotate(img, 90.0, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{0, 0}}) 30 | func Rotate(img image.Image, angle float64, options *RotationOptions) *image.RGBA { 31 | src := clone.AsShallowRGBA(img) 32 | srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() 33 | 34 | supersample := false 35 | absAngle := int(math.Abs(angle) + 0.5) 36 | if absAngle%360 == 0 { 37 | // Return early if nothing to do 38 | return src 39 | } else if absAngle%90 != 0 { 40 | // Supersampling is required for non-special angles 41 | // Special angles = 90, 180, 270... 42 | supersample = true 43 | } 44 | 45 | // Config defaults 46 | resizeBounds := false 47 | // Default pivot position is center of image 48 | pivotX, pivotY := float64(srcW/2), float64(srcH/2) 49 | // Get options if provided 50 | if options != nil { 51 | resizeBounds = options.ResizeBounds 52 | if options.Pivot != nil { 53 | pivotX, pivotY = float64(options.Pivot.X), float64(options.Pivot.Y) 54 | } 55 | } 56 | 57 | if supersample { 58 | // Supersample, currently hard set to 2x 59 | srcW, srcH = srcW*2, srcH*2 60 | src = Resize(src, srcW, srcH, NearestNeighbor) 61 | pivotX, pivotY = pivotX*2, pivotY*2 62 | } 63 | 64 | // Convert to radians, positive degree maps to clockwise rotation 65 | angleRadians := -angle * (math.Pi / 180) 66 | 67 | var dstW, dstH int 68 | var sin, cos = math.Sincos(angleRadians) 69 | if resizeBounds { 70 | // Reserve larger size in destination image for full image bounds rotation 71 | // If not preserving size, always take image center as pivot 72 | pivotX, pivotY = float64(srcW)/2, float64(srcH)/2 73 | 74 | a := math.Abs(float64(srcW) * sin) 75 | b := math.Abs(float64(srcW) * cos) 76 | c := math.Abs(float64(srcH) * sin) 77 | d := math.Abs(float64(srcH) * cos) 78 | 79 | dstW, dstH = int(c+b+0.5), int(a+d+0.5) 80 | } else { 81 | dstW, dstH = srcW, srcH 82 | } 83 | dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) 84 | 85 | // Calculate offsets in case entire image is being displayed 86 | // Otherwise areas clipped by rotation won't be available 87 | offsetX := (dstW - srcW) / 2 88 | offsetY := (dstH - srcH) / 2 89 | 90 | parallel.Line(srcH, func(start, end int) { 91 | // Correct range to include the pixels visible in new bounds 92 | // Note that cannot be done in parallelize function input height, otherwise ranges would overlap 93 | yStart := int((float64(start)/float64(srcH))*float64(dstH)) - offsetY 94 | yEnd := int((float64(end)/float64(srcH))*float64(dstH)) - offsetY 95 | xStart := -offsetX 96 | xEnd := srcW + offsetX 97 | 98 | for y := yStart; y < yEnd; y++ { 99 | dy := float64(y) - pivotY + 0.5 100 | for x := xStart; x < xEnd; x++ { 101 | dx := float64(x) - pivotX + 0.5 102 | 103 | ix := int((cos*dx - sin*dy + pivotX)) 104 | iy := int((sin*dx + cos*dy + pivotY)) 105 | 106 | if ix < 0 || ix >= srcW || iy < 0 || iy >= srcH { 107 | continue 108 | } 109 | 110 | red, green, blue, alpha := src.At(ix, iy).RGBA() 111 | 112 | dst.Set(x+offsetX, y+offsetY, color.RGBA64{ 113 | R: uint16(red), 114 | G: uint16(green), 115 | B: uint16(blue), 116 | A: uint16(alpha), 117 | }) 118 | } 119 | } 120 | }) 121 | 122 | if supersample { 123 | // Downsample to original bounds as part of the Supersampling 124 | dst = Resize(dst, dstW/2, dstH/2, Linear) 125 | } 126 | 127 | return dst 128 | } 129 | 130 | // FlipH returns a horizontally flipped version of the image. 131 | func FlipH(img image.Image) *image.RGBA { 132 | bounds := img.Bounds() 133 | src := clone.AsShallowRGBA(img) 134 | dst := image.NewRGBA(bounds) 135 | w, h := dst.Bounds().Dx(), dst.Bounds().Dy() 136 | 137 | parallel.Line(h, func(start, end int) { 138 | for y := start; y < end; y++ { 139 | for x := 0; x < w; x++ { 140 | iy := y * dst.Stride 141 | pos := iy + (x * 4) 142 | flippedX := w - x - 1 143 | flippedPos := iy + (flippedX * 4) 144 | 145 | dst.Pix[pos+0] = src.Pix[flippedPos+0] 146 | dst.Pix[pos+1] = src.Pix[flippedPos+1] 147 | dst.Pix[pos+2] = src.Pix[flippedPos+2] 148 | dst.Pix[pos+3] = src.Pix[flippedPos+3] 149 | } 150 | } 151 | }) 152 | 153 | return dst 154 | } 155 | 156 | // FlipV returns a vertically flipped version of the image. 157 | func FlipV(img image.Image) *image.RGBA { 158 | bounds := img.Bounds() 159 | src := clone.AsShallowRGBA(img) 160 | dst := image.NewRGBA(bounds) 161 | w, h := dst.Bounds().Dx(), dst.Bounds().Dy() 162 | 163 | parallel.Line(h, func(start, end int) { 164 | for y := start; y < end; y++ { 165 | for x := 0; x < w; x++ { 166 | pos := y*dst.Stride + (x * 4) 167 | flippedY := h - y - 1 168 | flippedPos := flippedY*dst.Stride + (x * 4) 169 | 170 | dst.Pix[pos+0] = src.Pix[flippedPos+0] 171 | dst.Pix[pos+1] = src.Pix[flippedPos+1] 172 | dst.Pix[pos+2] = src.Pix[flippedPos+2] 173 | dst.Pix[pos+3] = src.Pix[flippedPos+3] 174 | } 175 | } 176 | }) 177 | 178 | return dst 179 | } 180 | -------------------------------------------------------------------------------- /transform/shear.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "image" 5 | "math" 6 | 7 | "github.com/anthonynsimon/bild/clone" 8 | "github.com/anthonynsimon/bild/parallel" 9 | ) 10 | 11 | // ShearH applies a shear linear transformation along the horizontal axis, 12 | // the parameter angle is the shear angle to be applied. 13 | // The transformation will be applied with the center of the image as the pivot. 14 | func ShearH(img image.Image, angle float64) *image.RGBA { 15 | src := clone.AsShallowRGBA(img) 16 | srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() 17 | 18 | // Supersample, currently hard set to 2x 19 | srcW, srcH = srcW*2, srcH*2 20 | src = Resize(src, srcW, srcH, NearestNeighbor) 21 | 22 | // Calculate shear factor 23 | kx := math.Tan(angle * (math.Pi / 180)) 24 | 25 | dstW, dstH := srcW+int(float64(srcH)*math.Abs(kx)), srcH 26 | dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) 27 | 28 | pivotX := float64(dstW) / 2 29 | pivotY := float64(dstH) / 2 30 | 31 | // Calculate offset since we are resizing the bounds to 32 | // fit the sheared image. 33 | dx := (dstW - srcW) / 2 34 | dy := (dstH - srcH) / 2 35 | 36 | parallel.Line(dstH, func(start, end int) { 37 | for y := start; y < end; y++ { 38 | for x := 0; x < dstW; x++ { 39 | // Move positions to revolve around pivot 40 | ix := x - int(pivotX) - dx 41 | iy := y - int(pivotY) - dy 42 | 43 | // Apply linear transformation 44 | ix = ix + int(float64(iy)*kx) 45 | 46 | // Move positions back to image coordinates 47 | ix += int(pivotX) 48 | iy += int(pivotY) 49 | 50 | if ix < 0 || ix >= srcW || iy < 0 || iy >= srcH { 51 | continue 52 | } 53 | 54 | srcPos := iy*src.Stride + ix*4 55 | dstPos := y*dst.Stride + x*4 56 | 57 | dst.Pix[dstPos+0] = src.Pix[srcPos+0] 58 | dst.Pix[dstPos+1] = src.Pix[srcPos+1] 59 | dst.Pix[dstPos+2] = src.Pix[srcPos+2] 60 | dst.Pix[dstPos+3] = src.Pix[srcPos+3] 61 | } 62 | } 63 | }) 64 | 65 | // Downsample to original bounds as part of the Supersampling 66 | dst = Resize(dst, dstW/2, dstH/2, Linear) 67 | 68 | return dst 69 | } 70 | 71 | // ShearV applies a shear linear transformation along the vertical axis, 72 | // the parameter angle is the shear angle to be applied. 73 | // The transformation will be applied with the center of the image as the pivot. 74 | func ShearV(img image.Image, angle float64) *image.RGBA { 75 | src := clone.AsRGBA(img) 76 | srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() 77 | 78 | // Supersample, currently hard set to 2x 79 | srcW, srcH = srcW*2, srcH*2 80 | src = Resize(src, srcW, srcH, NearestNeighbor) 81 | 82 | // Calculate shear factor 83 | ky := math.Tan(angle * (math.Pi / 180)) 84 | 85 | dstW, dstH := srcW, srcH+int(float64(srcW)*math.Abs(ky)) 86 | dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) 87 | 88 | pivotX := float64(dstW) / 2 89 | pivotY := float64(dstH) / 2 90 | 91 | // Calculate offset since we are resizing the bounds to 92 | // fit the sheared image. 93 | dx := (dstW - srcW) / 2 94 | dy := (dstH - srcH) / 2 95 | 96 | parallel.Line(dstH, func(start, end int) { 97 | for y := start; y < end; y++ { 98 | for x := 0; x < dstW; x++ { 99 | // Move positions to revolve around pivot 100 | ix := x - int(pivotX) - dx 101 | iy := y - int(pivotY) - dy 102 | 103 | // Apply linear transformation 104 | iy = iy + int(float64(ix)*ky) 105 | 106 | // Move positions back to image coordinates 107 | ix += int(pivotX) 108 | iy += int(pivotY) 109 | 110 | if ix < 0 || ix >= srcW || iy < 0 || iy >= srcH { 111 | continue 112 | } 113 | 114 | srcPos := iy*src.Stride + ix*4 115 | dstPos := y*dst.Stride + x*4 116 | 117 | dst.Pix[dstPos+0] = src.Pix[srcPos+0] 118 | dst.Pix[dstPos+1] = src.Pix[srcPos+1] 119 | dst.Pix[dstPos+2] = src.Pix[srcPos+2] 120 | dst.Pix[dstPos+3] = src.Pix[srcPos+3] 121 | } 122 | } 123 | }) 124 | 125 | // Downsample to original bounds as part of the Supersampling 126 | dst = Resize(dst, dstW/2, dstH/2, Linear) 127 | 128 | return dst 129 | } 130 | -------------------------------------------------------------------------------- /transform/translate.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/anthonynsimon/bild/clone" 7 | "github.com/anthonynsimon/bild/parallel" 8 | ) 9 | 10 | // Translate repositions a copy of the provided image by dx on the x-axis and 11 | // by dy on the y-axis and returns the result. The bounds from the provided image 12 | // will be kept. 13 | // A positive dx value moves the image towards the right and a positive dy value 14 | // moves the image upwards. 15 | func Translate(img image.Image, dx, dy int) *image.RGBA { 16 | src := clone.AsShallowRGBA(img) 17 | 18 | if dx == 0 && dy == 0 { 19 | return src 20 | } 21 | 22 | w, h := src.Bounds().Dx(), src.Bounds().Dy() 23 | dst := image.NewRGBA(src.Bounds()) 24 | 25 | parallel.Line(h, func(start, end int) { 26 | for y := start; y < end; y++ { 27 | for x := 0; x < w; x++ { 28 | ix, iy := x-dx, y+dy 29 | 30 | if ix < 0 || ix >= w || iy < 0 || iy >= h { 31 | continue 32 | } 33 | 34 | srcPos := iy*src.Stride + ix*4 35 | dstPos := y*src.Stride + x*4 36 | 37 | copy(dst.Pix[dstPos:dstPos+4], src.Pix[srcPos:srcPos+4]) 38 | } 39 | } 40 | }) 41 | 42 | return dst 43 | } 44 | -------------------------------------------------------------------------------- /transform/translate_test.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/anthonynsimon/bild/util" 8 | ) 9 | 10 | // benchResult is used to avoid having the compiler optimize the benchmark code calls 11 | var benchResult interface{} 12 | 13 | func BenchmarkTranslate(b *testing.B) { 14 | img := image.NewRGBA(image.Rect(0, 0, 1024, 1024)) 15 | b.ResetTimer() 16 | for n := 0; n < b.N; n++ { 17 | benchResult = Translate(img, 512, 512) 18 | } 19 | } 20 | 21 | func TestTranslate(t *testing.T) { 22 | cases := []struct { 23 | name string 24 | dx int 25 | dy int 26 | img *image.RGBA 27 | expected *image.RGBA 28 | }{ 29 | { 30 | name: "empty with translation", 31 | dx: 2, 32 | dy: 2, 33 | img: &image.RGBA{ 34 | Stride: 0, 35 | Rect: image.Rect(0, 0, 0, 0), 36 | Pix: []uint8{}, 37 | }, 38 | expected: &image.RGBA{ 39 | Stride: 0, 40 | Rect: image.Rect(0, 0, 0, 0), 41 | Pix: []uint8{}, 42 | }, 43 | }, 44 | { 45 | name: "empty no translation", 46 | dx: 0, 47 | dy: 0, 48 | img: &image.RGBA{ 49 | Stride: 0, 50 | Rect: image.Rect(0, 0, 0, 0), 51 | Pix: []uint8{}, 52 | }, 53 | expected: &image.RGBA{ 54 | Stride: 0, 55 | Rect: image.Rect(0, 0, 0, 0), 56 | Pix: []uint8{}, 57 | }, 58 | }, 59 | { 60 | name: "no translation", 61 | dx: 0, 62 | dy: 0, 63 | img: &image.RGBA{ 64 | Stride: 2 * 4, 65 | Rect: image.Rect(0, 0, 2, 2), 66 | Pix: []uint8{ 67 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 68 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 69 | }, 70 | }, 71 | expected: &image.RGBA{ 72 | Stride: 2 * 4, 73 | Rect: image.Rect(0, 0, 2, 2), 74 | Pix: []uint8{ 75 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 76 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 77 | }, 78 | }, 79 | }, 80 | { 81 | name: "dx +1", 82 | dx: 1, 83 | dy: 0, 84 | img: &image.RGBA{ 85 | Stride: 2 * 4, 86 | Rect: image.Rect(0, 0, 2, 2), 87 | Pix: []uint8{ 88 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 89 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 90 | }, 91 | }, 92 | expected: &image.RGBA{ 93 | Stride: 2 * 4, 94 | Rect: image.Rect(0, 0, 2, 2), 95 | Pix: []uint8{ 96 | 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 97 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0xFF, 98 | }, 99 | }, 100 | }, 101 | { 102 | name: "dy +1", 103 | dx: 0, 104 | dy: 1, 105 | img: &image.RGBA{ 106 | Stride: 2 * 4, 107 | Rect: image.Rect(0, 0, 2, 2), 108 | Pix: []uint8{ 109 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 110 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 111 | }, 112 | }, 113 | expected: &image.RGBA{ 114 | Stride: 2 * 4, 115 | Rect: image.Rect(0, 0, 2, 2), 116 | Pix: []uint8{ 117 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 118 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | }, 120 | }, 121 | }, 122 | { 123 | name: "dx +1 dy +1", 124 | dx: 1, 125 | dy: 1, 126 | img: &image.RGBA{ 127 | Stride: 2 * 4, 128 | Rect: image.Rect(0, 0, 2, 2), 129 | Pix: []uint8{ 130 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 131 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 132 | }, 133 | }, 134 | expected: &image.RGBA{ 135 | Stride: 2 * 4, 136 | Rect: image.Rect(0, 0, 2, 2), 137 | Pix: []uint8{ 138 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0xFF, 139 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 140 | }, 141 | }, 142 | }, 143 | { 144 | name: "dx -1", 145 | dx: -1, 146 | dy: 0, 147 | img: &image.RGBA{ 148 | Stride: 2 * 4, 149 | Rect: image.Rect(0, 0, 2, 2), 150 | Pix: []uint8{ 151 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 152 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 153 | }, 154 | }, 155 | expected: &image.RGBA{ 156 | Stride: 2 * 4, 157 | Rect: image.Rect(0, 0, 2, 2), 158 | Pix: []uint8{ 159 | 0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0x00, 160 | 0x20, 0x20, 0x20, 0xFF, 0x00, 0x00, 0x00, 0x00, 161 | }, 162 | }, 163 | }, 164 | { 165 | name: "dy -3", 166 | dx: 0, 167 | dy: -3, 168 | img: &image.RGBA{ 169 | Stride: 2 * 4, 170 | Rect: image.Rect(0, 0, 2, 2), 171 | Pix: []uint8{ 172 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0xFF, 173 | 0x80, 0x80, 0x80, 0xFF, 0x20, 0x20, 0x20, 0xFF, 174 | }, 175 | }, 176 | expected: &image.RGBA{ 177 | Stride: 2 * 4, 178 | Rect: image.Rect(0, 0, 2, 2), 179 | Pix: []uint8{ 180 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 181 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 182 | }, 183 | }, 184 | }, 185 | } 186 | 187 | for _, c := range cases { 188 | result := Translate(c.img, c.dx, c.dy) 189 | if !util.RGBAImageEqual(result, c.expected) { 190 | t.Errorf("%s:\nexpected:%v\nactual:%v", "Translate "+c.name, util.RGBAToString(c.expected), util.RGBAToString(result)) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /util/colormodel.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | "github.com/anthonynsimon/bild/math/f64" 8 | ) 9 | 10 | // RGBToHSL converts from RGB to HSL color model. 11 | // Parameter c is the RGBA color and must implement the color.RGBA interface. 12 | // Returned values h, s and l correspond to the hue, saturation and lightness. 13 | // The hue is of range 0 to 360 and the saturation and lightness are of range 0.0 to 1.0. 14 | func RGBToHSL(c color.RGBA) (float64, float64, float64) { 15 | r, g, b := float64(c.R)/255, float64(c.G)/255, float64(c.B)/255 16 | max := math.Max(r, math.Max(g, b)) 17 | min := math.Min(r, math.Min(g, b)) 18 | delta := max - min 19 | 20 | var h, s, l float64 21 | l = (max + min) / 2 22 | 23 | // Achromatic 24 | if delta <= 0 { 25 | return h, s, l 26 | } 27 | 28 | // Should it be smaller than or equals instead? 29 | if l < 0.5 { 30 | s = delta / (max + min) 31 | } else { 32 | s = delta / (2 - max - min) 33 | } 34 | 35 | if r >= max { 36 | h = (g - b) / delta 37 | } else if g >= max { 38 | h = (b-r)/delta + 2 39 | } else { 40 | h = (r-g)/delta + 4 41 | } 42 | 43 | h *= 60 44 | if h < 0 { 45 | h += 360 46 | } 47 | 48 | return h, s, l 49 | } 50 | 51 | // HSLToRGB converts from HSL to RGB color model. 52 | // Parameter h is the hue and its range is from 0 to 360 degrees. 53 | // Parameter s is the saturation and its range is from 0.0 to 1.0. 54 | // Parameter l is the lightness and its range is from 0.0 to 1.0. 55 | func HSLToRGB(h, s, l float64) color.RGBA { 56 | 57 | var r, g, b float64 58 | if s == 0 { 59 | r = l 60 | g = l 61 | b = l 62 | } else { 63 | var temp0, temp1 float64 64 | if l < 0.5 { 65 | temp0 = l * (1 + s) 66 | } else { 67 | temp0 = (l + s) - (s * l) 68 | } 69 | temp1 = 2*l - temp0 70 | 71 | h /= 360 72 | 73 | hueFn := func(v float64) float64 { 74 | if v < 0 { 75 | v++ 76 | } else if v > 1 { 77 | v-- 78 | } 79 | 80 | if v < 1.0/6.0 { 81 | return temp1 + (temp0-temp1)*6*v 82 | } 83 | if v < 1.0/2.0 { 84 | return temp0 85 | } 86 | if v < 2.0/3.0 { 87 | return temp1 + (temp0-temp1)*(2.0/3.0-v)*6 88 | } 89 | return temp1 90 | } 91 | 92 | r = hueFn(h + 1.0/3.0) 93 | g = hueFn(h) 94 | b = hueFn(h - 1.0/3.0) 95 | 96 | } 97 | 98 | outR := uint8(f64.Clamp(r*255+0.5, 0, 255)) 99 | outG := uint8(f64.Clamp(g*255+0.5, 0, 255)) 100 | outB := uint8(f64.Clamp(b*255+0.5, 0, 255)) 101 | 102 | return color.RGBA{outR, outG, outB, 0xFF} 103 | } 104 | 105 | // RGBToHSV converts from RGB to HSV color model. 106 | // Parameter c is the RGBA color and must implement the color.RGBA interface. 107 | // Returned values h, s and v correspond to the hue, saturation and value. 108 | // The hue is of range 0 to 360 and the saturation and value are of range 0.0 to 1.0. 109 | func RGBToHSV(c color.RGBA) (h, s, v float64) { 110 | r, g, b := float64(c.R)/255, float64(c.G)/255, float64(c.B)/255 111 | 112 | max := math.Max(r, math.Max(g, b)) 113 | min := math.Min(r, math.Min(g, b)) 114 | v = max 115 | delta := max - min 116 | 117 | // Avoid division by zero 118 | if max > 0 { 119 | s = delta / max 120 | } else { 121 | h = 0 122 | s = 0 123 | return 124 | } 125 | 126 | // Achromatic 127 | if max == min { 128 | h = 0 129 | return 130 | } 131 | 132 | if r >= max { 133 | h = (g - b) / delta 134 | } else if g >= max { 135 | h = (b-r)/delta + 2 136 | } else { 137 | h = (r-g)/delta + 4 138 | } 139 | 140 | h *= 60 141 | if h < 0 { 142 | h += 360 143 | } 144 | 145 | return 146 | } 147 | 148 | // HSVToRGB converts from HSV to RGB color model. 149 | // Parameter h is the hue and its range is from 0 to 360 degrees. 150 | // Parameter s is the saturation and its range is from 0.0 to 1.0. 151 | // Parameter v is the value and its range is from 0.0 to 1.0. 152 | func HSVToRGB(h, s, v float64) color.RGBA { 153 | var i, f, p, q, t float64 154 | 155 | // Achromatic 156 | if s == 0 { 157 | outV := uint8(f64.Clamp(v*255+0.5, 0, 255)) 158 | return color.RGBA{outV, outV, outV, 0xFF} 159 | } 160 | 161 | h /= 60 162 | i = math.Floor(h) 163 | f = h - i 164 | p = v * (1 - s) 165 | q = v * (1 - s*f) 166 | t = v * (1 - s*(1-f)) 167 | 168 | var r, g, b float64 169 | switch i { 170 | case 0: 171 | r = v 172 | g = t 173 | b = p 174 | case 1: 175 | r = q 176 | g = v 177 | b = p 178 | case 2: 179 | r = p 180 | g = v 181 | b = t 182 | case 3: 183 | r = p 184 | g = q 185 | b = v 186 | case 4: 187 | r = t 188 | g = p 189 | b = v 190 | default: 191 | r = v 192 | g = p 193 | b = q 194 | } 195 | 196 | outR := uint8(f64.Clamp(r*255+0.5, 0, 255)) 197 | outG := uint8(f64.Clamp(g*255+0.5, 0, 255)) 198 | outB := uint8(f64.Clamp(b*255+0.5, 0, 255)) 199 | return color.RGBA{outR, outG, outB, 0xFF} 200 | } 201 | -------------------------------------------------------------------------------- /util/colormodel_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestRGBToHSV(t *testing.T) { 10 | cases := []struct { 11 | input color.RGBA 12 | expected [3]float64 13 | }{ 14 | { 15 | input: color.RGBA{45, 166, 115, 255}, 16 | expected: [3]float64{155, 0.73, 0.65}, 17 | }, 18 | { 19 | input: color.RGBA{0, 255, 0, 255}, 20 | expected: [3]float64{120, 1, 1}, 21 | }, 22 | { 23 | input: color.RGBA{242, 220, 97, 255}, 24 | expected: [3]float64{51, 0.6, 0.95}, 25 | }, 26 | { 27 | input: color.RGBA{10, 10, 10, 255}, 28 | expected: [3]float64{0, 0.0, 0.04}, 29 | }, 30 | { 31 | input: color.RGBA{255, 255, 255, 255}, 32 | expected: [3]float64{0, 0.0, 1.0}, 33 | }, 34 | { 35 | input: color.RGBA{0, 0, 0, 255}, 36 | expected: [3]float64{0, 0.0, 0.0}, 37 | }, 38 | { 39 | input: color.RGBA{255, 0, 0, 255}, 40 | expected: [3]float64{0, 1.0, 1.0}, 41 | }, 42 | { 43 | input: color.RGBA{255, 0, 255, 255}, 44 | expected: [3]float64{300, 1.0, 1.0}, 45 | }, 46 | } 47 | 48 | for _, c := range cases { 49 | h, s, v := RGBToHSV(c.input) 50 | h = math.Floor(h + 0.5) 51 | s = math.Floor((s*100)+0.5) / 100 52 | v = math.Floor((v*100)+0.5) / 100 53 | if h != c.expected[0] || s != c.expected[1] || v != c.expected[2] { 54 | t.Errorf("RGBToHSV failed: expected: %#v, actual: %#v, %#v, %#v", c.expected, h, s, v) 55 | } 56 | } 57 | } 58 | 59 | func TestHSVToRGB(t *testing.T) { 60 | cases := []struct { 61 | input [3]float64 62 | expected color.RGBA 63 | }{ 64 | { 65 | input: [3]float64{155, 0.73, 0.65}, 66 | expected: color.RGBA{45, 166, 115, 255}, 67 | }, 68 | { 69 | input: [3]float64{120, 1, 1}, 70 | expected: color.RGBA{0, 255, 0, 255}, 71 | }, 72 | { 73 | input: [3]float64{51, 0.6, 0.95}, 74 | expected: color.RGBA{242, 220, 97, 255}, 75 | }, 76 | { 77 | input: [3]float64{0, 0.0, 0.04}, 78 | expected: color.RGBA{10, 10, 10, 255}, 79 | }, 80 | { 81 | input: [3]float64{0, 0.0, 1.0}, 82 | expected: color.RGBA{255, 255, 255, 255}, 83 | }, 84 | { 85 | input: [3]float64{0, 0.0, 0.0}, 86 | expected: color.RGBA{0, 0, 0, 255}, 87 | }, 88 | { 89 | input: [3]float64{0, 1.0, 1.0}, 90 | expected: color.RGBA{255, 0, 0, 255}, 91 | }, 92 | { 93 | input: [3]float64{300, 1.0, 1.0}, 94 | expected: color.RGBA{255, 0, 255, 255}, 95 | }, 96 | } 97 | 98 | for _, c := range cases { 99 | actual := HSVToRGB(c.input[0], c.input[1], c.input[2]) 100 | if actual != c.expected { 101 | t.Errorf("HSVToRGB failed: expected: %#v, actual: %#v", c.expected, actual) 102 | } 103 | } 104 | } 105 | 106 | func TestRGBToHSL(t *testing.T) { 107 | cases := []struct { 108 | input color.RGBA 109 | expected [3]float64 110 | }{ 111 | { 112 | input: color.RGBA{45, 166, 115, 255}, 113 | expected: [3]float64{155, 0.57, 0.41}, 114 | }, 115 | { 116 | input: color.RGBA{0, 255, 0, 255}, 117 | expected: [3]float64{120, 1, 0.5}, 118 | }, 119 | { 120 | input: color.RGBA{242, 220, 97, 255}, 121 | expected: [3]float64{51, 0.85, 0.66}, 122 | }, 123 | { 124 | input: color.RGBA{10, 10, 10, 255}, 125 | expected: [3]float64{0, 0.0, 0.04}, 126 | }, 127 | { 128 | input: color.RGBA{255, 255, 255, 255}, 129 | expected: [3]float64{0, 0.0, 1.0}, 130 | }, 131 | { 132 | input: color.RGBA{0, 0, 0, 255}, 133 | expected: [3]float64{0, 0.0, 0.0}, 134 | }, 135 | { 136 | input: color.RGBA{255, 0, 0, 255}, 137 | expected: [3]float64{0, 1.0, 0.5}, 138 | }, 139 | { 140 | input: color.RGBA{0, 0, 255, 255}, 141 | expected: [3]float64{240, 1.0, 0.5}, 142 | }, 143 | { 144 | input: color.RGBA{255, 0, 255, 255}, 145 | expected: [3]float64{300, 1.0, 0.5}, 146 | }, 147 | } 148 | 149 | for _, c := range cases { 150 | h, s, l := RGBToHSL(c.input) 151 | h = math.Floor(h + 0.5) 152 | s = math.Floor((s*100)+0.5) / 100 153 | l = math.Floor((l*100)+0.5) / 100 154 | if h != c.expected[0] || s != c.expected[1] || l != c.expected[2] { 155 | t.Errorf("RGBToHSL failed: expected: %#v, actual: %#v, %#v, %#v", c.expected, h, s, l) 156 | } 157 | } 158 | } 159 | 160 | func TestHSLToRGB(t *testing.T) { 161 | cases := []struct { 162 | input [3]float64 163 | expected color.RGBA 164 | }{ 165 | { 166 | input: [3]float64{155, 0.57, 0.41}, 167 | expected: color.RGBA{0x2d, 0xa4, 0x72, 0xff}, 168 | }, 169 | { 170 | input: [3]float64{120, 1, 0.5}, 171 | expected: color.RGBA{0, 255, 0, 255}, 172 | }, 173 | { 174 | input: [3]float64{51, 0.85, 0.66}, 175 | expected: color.RGBA{0xf2, 0xdc, 0x5f, 0xff}, 176 | }, 177 | { 178 | input: [3]float64{0, 0.0, 0.04}, 179 | expected: color.RGBA{10, 10, 10, 255}, 180 | }, 181 | { 182 | input: [3]float64{0, 0.0, 1.0}, 183 | expected: color.RGBA{255, 255, 255, 255}, 184 | }, 185 | { 186 | input: [3]float64{0, 0.0, 0.0}, 187 | expected: color.RGBA{0, 0, 0, 255}, 188 | }, 189 | { 190 | input: [3]float64{0, 1.0, 0.5}, 191 | expected: color.RGBA{255, 0, 0, 255}, 192 | }, 193 | { 194 | input: [3]float64{240, 1.0, 0.5}, 195 | expected: color.RGBA{0, 0, 255, 255}, 196 | }, 197 | { 198 | input: [3]float64{300, 1.0, 0.5}, 199 | expected: color.RGBA{255, 0, 255, 255}, 200 | }, 201 | } 202 | 203 | for _, c := range cases { 204 | actual := HSLToRGB(c.input[0], c.input[1], c.input[2]) 205 | if actual != c.expected { 206 | t.Errorf("HSLToRGB failed: expected: %#v, actual: %#v", c.expected, actual) 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /util/stack.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Stack implementation for arbitrary data types with Push(), Pop() and Len() functions 4 | type Stack struct { 5 | top *stackElement 6 | size int 7 | } 8 | 9 | type stackElement struct { 10 | value interface{} 11 | next *stackElement 12 | } 13 | 14 | // Len returns the size of stack 15 | func (s *Stack) Len() int { 16 | return s.size 17 | } 18 | 19 | // Push a new value onto the stack 20 | func (s *Stack) Push(value interface{}) { 21 | s.top = &stackElement{value, s.top} 22 | s.size++ 23 | } 24 | 25 | // Pop the most recently pushed value from the stack 26 | func (s *Stack) Pop() interface{} { 27 | if s.size > 0 { 28 | value := s.top.value 29 | s.top = s.top.next 30 | s.size-- 31 | return value 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /util/stack_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStack(t *testing.T) { 8 | var st Stack 9 | 10 | floatElement := 12.0 11 | stringElement := "el" 12 | intElement := 3 13 | 14 | st.Push(floatElement) 15 | st.Push(stringElement) 16 | st.Push(intElement) 17 | 18 | if 3 != st.Len() { 19 | t.Fatalf("Expected stack length of %d, got %d", 3, st.Len()) 20 | } 21 | 22 | pop1 := st.Pop() 23 | pop2 := st.Pop() 24 | pop3 := st.Pop() 25 | noElementsLeft := 0 == st.Len() 26 | 27 | if intElement != pop1 { 28 | t.Fatalf("Expected element to be %d, got %d", intElement, pop1) 29 | } 30 | if stringElement != pop2 { 31 | t.Fatalf("Expected element to be %s, got %s", stringElement, pop2) 32 | } 33 | if floatElement != pop3 { 34 | t.Fatalf("Expected element to be %6.2f, got %6.2f", floatElement, pop3) 35 | } 36 | if !noElementsLeft { 37 | t.Fatalf("Expected noElementsLeft to be %t, got %t", true, noElementsLeft) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | /*Package util provides various helper functions for the package bild.*/ 2 | package util 3 | 4 | import ( 5 | "fmt" 6 | "image" 7 | "image/color" 8 | ) 9 | 10 | // SortRGBA sorts a slice of RGBA values. 11 | // Parameter min and max correspond to the start and end slice indices 12 | // that determine the range to be sorted. 13 | func SortRGBA(data []color.RGBA, min, max int) { 14 | if min > max { 15 | return 16 | } 17 | p := partitionRGBASlice(data, min, max) 18 | SortRGBA(data, min, p-1) 19 | SortRGBA(data, p+1, max) 20 | } 21 | 22 | func partitionRGBASlice(data []color.RGBA, min, max int) int { 23 | pivot := data[max] 24 | i := min 25 | r := srank(pivot) 26 | for j := min; j < max; j++ { 27 | if srank(data[j]) <= r { 28 | data[i], data[j] = data[j], data[i] 29 | i++ 30 | } 31 | } 32 | data[i], data[max] = data[max], data[i] 33 | return i 34 | } 35 | 36 | func srank(c color.RGBA) uint { 37 | return uint(c.R)<<3 + uint(c.G)<<6 + uint(c.B)<<1 38 | } 39 | 40 | // Rank a color based on a color perception heuristic. 41 | func Rank(c color.RGBA) float64 { 42 | return float64(c.R)*0.3 + float64(c.G)*0.6 + float64(c.B)*0.1 43 | } 44 | 45 | // RGBAToString returns a string representation of the Hex values contained in an image.RGBA. 46 | func RGBAToString(img *image.RGBA) string { 47 | var result string 48 | result += fmt.Sprintf("\nBounds: %v", img.Bounds()) 49 | result += fmt.Sprintf("\nStride: %v", img.Stride) 50 | for y := 0; y < img.Bounds().Dy(); y++ { 51 | result += "\n" 52 | for x := 0; x < img.Bounds().Dx(); x++ { 53 | pos := y*img.Stride + x*4 54 | result += fmt.Sprintf("%#X, ", img.Pix[pos+0]) 55 | result += fmt.Sprintf("%#X, ", img.Pix[pos+1]) 56 | result += fmt.Sprintf("%#X, ", img.Pix[pos+2]) 57 | result += fmt.Sprintf("%#X, ", img.Pix[pos+3]) 58 | } 59 | } 60 | result += "\n" 61 | return result 62 | } 63 | 64 | // RGBASlicesEqual returns true if the parameter RGBA color slices a and b match 65 | // or false if otherwise. 66 | func RGBASlicesEqual(a, b []color.RGBA) bool { 67 | if a == nil && b == nil { 68 | return true 69 | } 70 | 71 | if len(a) != len(b) { 72 | return false 73 | } 74 | 75 | for i := range a { 76 | if a[i] != b[i] { 77 | return false 78 | } 79 | } 80 | 81 | return true 82 | } 83 | 84 | // GrayImageEqual returns true if the parameter images a and b match 85 | // or false if otherwise. 86 | func GrayImageEqual(a, b *image.Gray) bool { 87 | if !a.Rect.Eq(b.Rect) { 88 | return false 89 | } 90 | 91 | for i := 0; i < len(a.Pix); i++ { 92 | if a.Pix[i] != b.Pix[i] { 93 | return false 94 | } 95 | } 96 | return true 97 | } 98 | 99 | // RGBAImageEqual returns true if the parameter images a and b match 100 | // or false if otherwise. 101 | func RGBAImageEqual(a, b *image.RGBA) bool { 102 | if !a.Rect.Eq(b.Rect) { 103 | return false 104 | } 105 | 106 | for y := 0; y < a.Bounds().Dy(); y++ { 107 | for x := 0; x < a.Bounds().Dx(); x++ { 108 | pos := y*a.Stride + x*4 109 | if a.Pix[pos+0] != b.Pix[pos+0] { 110 | return false 111 | } 112 | if a.Pix[pos+1] != b.Pix[pos+1] { 113 | return false 114 | } 115 | if a.Pix[pos+2] != b.Pix[pos+2] { 116 | return false 117 | } 118 | if a.Pix[pos+3] != b.Pix[pos+3] { 119 | return false 120 | } 121 | } 122 | } 123 | return true 124 | } 125 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math" 7 | "testing" 8 | ) 9 | 10 | func TestQuickSortRGBA(t *testing.T) { 11 | cases := []struct { 12 | value []color.RGBA 13 | expected []color.RGBA 14 | }{ 15 | { 16 | value: []color.RGBA{{0, 0, 0, 0}}, 17 | expected: []color.RGBA{{0, 0, 0, 0}}, 18 | }, 19 | { 20 | value: []color.RGBA{{1, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}, 21 | expected: []color.RGBA{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 0, 0, 0}}, 22 | }, 23 | { 24 | value: []color.RGBA{{255, 0, 128, 0}, {255, 255, 0, 0}, {0, 0, 0, 0}}, 25 | expected: []color.RGBA{{0, 0, 0, 0}, {255, 0, 128, 0}, {255, 255, 0, 0}}, 26 | }, 27 | { 28 | value: []color.RGBA{{255, 255, 128, 0}, {255, 255, 0, 0}, {0, 0, 0, 0}}, 29 | expected: []color.RGBA{{0, 0, 0, 0}, {255, 255, 0, 0}, {255, 255, 128, 0}}, 30 | }, 31 | } 32 | 33 | for _, c := range cases { 34 | SortRGBA(c.value, 0, len(c.value)-1) 35 | if !RGBASlicesEqual(c.value, c.expected) { 36 | t.Errorf("%s: expected: %#v, actual: %#v", "SortRGBA", c.expected, c.value) 37 | } 38 | } 39 | } 40 | 41 | func TestRank(t *testing.T) { 42 | cases := []struct { 43 | value color.RGBA 44 | expected float64 45 | }{ 46 | { 47 | value: color.RGBA{0, 0, 0, 0}, 48 | expected: 0, 49 | }, 50 | { 51 | value: color.RGBA{255, 255, 255, 255}, 52 | expected: 255, 53 | }, 54 | { 55 | value: color.RGBA{128, 128, 128, 255}, 56 | expected: 128, 57 | }, 58 | { 59 | value: color.RGBA{128, 64, 32, 255}, 60 | expected: 80, 61 | }, 62 | } 63 | 64 | for _, c := range cases { 65 | actual := math.Ceil(Rank(c.value)) 66 | if actual != c.expected { 67 | t.Errorf("%s: expected: %#v, actual: %#v", "rank", c.expected, actual) 68 | } 69 | } 70 | } 71 | 72 | func TestRGBASlicesEqual(t *testing.T) { 73 | cases := []struct { 74 | a []color.RGBA 75 | b []color.RGBA 76 | expected bool 77 | }{ 78 | { 79 | a: []color.RGBA{}, 80 | b: []color.RGBA{}, 81 | expected: true, 82 | }, 83 | { 84 | a: []color.RGBA{{}}, 85 | b: []color.RGBA{{}}, 86 | expected: true, 87 | }, 88 | { 89 | a: []color.RGBA{{255, 140, 10, 0}}, 90 | b: []color.RGBA{{255, 140, 10, 0}}, 91 | expected: true, 92 | }, 93 | { 94 | a: []color.RGBA{{255, 128, 10, 0}}, 95 | b: []color.RGBA{{255, 140, 10, 0}}, 96 | expected: false, 97 | }, 98 | { 99 | a: []color.RGBA{{}}, 100 | b: []color.RGBA{{255, 140, 10, 0}}, 101 | expected: false, 102 | }, 103 | { 104 | a: []color.RGBA{}, 105 | b: []color.RGBA{{255, 140, 10, 0}}, 106 | expected: false, 107 | }, 108 | } 109 | 110 | for _, c := range cases { 111 | actual := RGBASlicesEqual(c.a, c.b) 112 | if actual != c.expected { 113 | t.Errorf("%s: expected: %v actual: %v", "RGBASlicesEqual", c.expected, actual) 114 | } 115 | } 116 | } 117 | func TestGrayImageEqual(t *testing.T) { 118 | cases := []struct { 119 | a *image.Gray 120 | b *image.Gray 121 | expected bool 122 | }{ 123 | { 124 | a: &image.Gray{ 125 | Rect: image.Rect(0, 0, 3, 2), 126 | Stride: 3, 127 | Pix: []uint8{ 128 | 0xFF, 0xFF, 0xFF, 129 | 0xFF, 0xFF, 0xFF, 130 | }, 131 | }, 132 | b: &image.Gray{ 133 | Rect: image.Rect(0, 0, 3, 2), 134 | Stride: 3, 135 | Pix: []uint8{ 136 | 0xFF, 0x00, 0xFF, 137 | 0xFF, 0xFF, 0xFF, 138 | }, 139 | }, 140 | expected: false, 141 | }, 142 | { 143 | a: &image.Gray{ 144 | Rect: image.Rect(0, 0, 3, 2), 145 | Stride: 3, 146 | Pix: []uint8{ 147 | 0xFF, 0xFF, 0xFF, 148 | 0xFF, 0xFF, 0xFF, 149 | }, 150 | }, 151 | b: &image.Gray{ 152 | Rect: image.Rect(0, 0, 2, 2), 153 | Stride: 2, 154 | Pix: []uint8{ 155 | 0xFF, 0xFF, 156 | 0xFF, 0xFF, 157 | }, 158 | }, 159 | expected: false, 160 | }, 161 | { 162 | a: &image.Gray{ 163 | Rect: image.Rect(0, 0, 2, 2), 164 | Stride: 2, 165 | Pix: []uint8{ 166 | 0xFF, 0xFF, 167 | 0xFF, 0xFF, 168 | }, 169 | }, 170 | b: &image.Gray{ 171 | Rect: image.Rect(0, 0, 2, 2), 172 | Stride: 2, 173 | Pix: []uint8{ 174 | 0xFF, 0xFF, 175 | 0xFF, 0xFF, 176 | }, 177 | }, 178 | expected: true, 179 | }, 180 | { 181 | a: &image.Gray{}, 182 | b: &image.Gray{}, 183 | expected: true, 184 | }, 185 | { 186 | a: &image.Gray{}, 187 | b: &image.Gray{ 188 | Rect: image.Rect(0, 0, 2, 2), 189 | Stride: 2, 190 | Pix: []uint8{ 191 | 0xFF, 0xFF, 192 | 0xFF, 0xFF, 193 | }, 194 | }, 195 | expected: false, 196 | }, 197 | } 198 | 199 | for _, c := range cases { 200 | actual := GrayImageEqual(c.a, c.b) 201 | if actual != c.expected { 202 | t.Errorf("%s: expected: %v actual: %v", "GrayImageEqual", c.expected, actual) 203 | } 204 | } 205 | } 206 | 207 | func TestRGBAImageEqual(t *testing.T) { 208 | cases := []struct { 209 | a *image.RGBA 210 | b *image.RGBA 211 | expected bool 212 | }{ 213 | { 214 | a: &image.RGBA{ 215 | Rect: image.Rect(0, 0, 3, 2), 216 | Stride: 3 * 4, 217 | Pix: []uint8{ 218 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 219 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 220 | }, 221 | }, 222 | b: &image.RGBA{ 223 | Rect: image.Rect(0, 0, 3, 2), 224 | Stride: 3 * 4, 225 | Pix: []uint8{ 226 | 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 227 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 228 | }, 229 | }, 230 | expected: false, 231 | }, 232 | { 233 | a: &image.RGBA{ 234 | Rect: image.Rect(0, 0, 3, 2), 235 | Stride: 3 * 4, 236 | Pix: []uint8{ 237 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 238 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 239 | }, 240 | }, 241 | b: &image.RGBA{ 242 | Rect: image.Rect(0, 0, 2, 2), 243 | Stride: 2 * 4, 244 | Pix: []uint8{ 245 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 246 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 247 | }, 248 | }, 249 | expected: false, 250 | }, 251 | { 252 | a: &image.RGBA{ 253 | Rect: image.Rect(0, 0, 3, 2), 254 | Stride: 3 * 4, 255 | Pix: []uint8{ 256 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 257 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 258 | }, 259 | }, 260 | b: &image.RGBA{ 261 | Rect: image.Rect(0, 0, 3, 2), 262 | Stride: 3 * 4, 263 | Pix: []uint8{ 264 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 265 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 266 | }, 267 | }, 268 | expected: true, 269 | }, 270 | { 271 | a: &image.RGBA{}, 272 | b: &image.RGBA{}, 273 | expected: true, 274 | }, 275 | { 276 | a: &image.RGBA{}, 277 | b: &image.RGBA{ 278 | Rect: image.Rect(0, 0, 2, 2), 279 | Stride: 2, 280 | Pix: []uint8{ 281 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 282 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 283 | }, 284 | }, 285 | expected: false, 286 | }, 287 | { 288 | a: &image.RGBA{ 289 | Rect: image.Rect(0, 0, 1, 1), 290 | Stride: 1 * 4, 291 | Pix: []uint8{ 292 | 0x00, 0x00, 0x00, 0x00, 293 | }, 294 | }, 295 | b: &image.RGBA{ 296 | Rect: image.Rect(0, 0, 1, 1), 297 | Stride: 1 * 4, 298 | Pix: []uint8{ 299 | 0xAA, 0x00, 0x00, 0x00, 300 | }, 301 | }, 302 | expected: false, 303 | }, 304 | { 305 | a: &image.RGBA{ 306 | Rect: image.Rect(0, 0, 1, 1), 307 | Stride: 1 * 4, 308 | Pix: []uint8{ 309 | 0x00, 0x00, 0x00, 0x00, 310 | }, 311 | }, 312 | b: &image.RGBA{ 313 | Rect: image.Rect(0, 0, 1, 1), 314 | Stride: 1 * 4, 315 | Pix: []uint8{ 316 | 0x00, 0x00, 0xAA, 0x00, 317 | }, 318 | }, 319 | expected: false, 320 | }, 321 | { 322 | a: &image.RGBA{ 323 | Rect: image.Rect(0, 0, 1, 1), 324 | Stride: 1 * 4, 325 | Pix: []uint8{ 326 | 0x00, 0x00, 0x00, 0x00, 327 | }, 328 | }, 329 | b: &image.RGBA{ 330 | Rect: image.Rect(0, 0, 1, 1), 331 | Stride: 1 * 4, 332 | Pix: []uint8{ 333 | 0x00, 0x00, 0x00, 0xAA, 334 | }, 335 | }, 336 | expected: false, 337 | }, 338 | } 339 | 340 | for _, c := range cases { 341 | actual := RGBAImageEqual(c.a, c.b) 342 | if actual != c.expected { 343 | t.Errorf("%s: expected: %v actual: %v", "RGBAImageEqual", c.expected, actual) 344 | } 345 | } 346 | } 347 | --------------------------------------------------------------------------------