├── .gitignore ├── LICENSE ├── README.md ├── blend ├── blend.go └── blend_test.go ├── blur ├── blur.go └── blur_test.go ├── convolution ├── convolution.go ├── convolution_test.go └── kernel.go ├── doc.go ├── edgedetection ├── canny.go ├── canny_test.go ├── laplacian.go ├── laplacian_test.go ├── sobel.go └── sobel_test.go ├── effects ├── effects.go └── effects_test.go ├── generate ├── generate.go └── generate_test.go ├── go.mod ├── grayscale ├── grayscale.go └── grayscale_test.go ├── histogram ├── histogram.go └── histogram_test.go ├── imgio ├── io.go └── io_test.go ├── padding ├── padding.go └── padding_test.go ├── res ├── blur │ ├── grayBlur.jpg │ ├── grayGaussianBlur.jpg │ ├── rgbaBlur.jpg │ └── rgbaGaussianBlur.jpg ├── building.jpg ├── edge │ ├── cannygray.jpg │ ├── cannyrgba.jpg │ ├── horizontalSobelGray.png │ ├── horizontalSobelRGBA.png │ ├── laplacianGrayK4.png │ ├── laplacianGrayK8.png │ ├── laplacianRGBAK4.png │ ├── laplacianRGBAK8.png │ ├── sobelGray.png │ ├── sobelRGBA.png │ ├── verticalSobelGray.png │ └── verticalSobelRGBA.png ├── effects │ ├── embossGray.jpg │ ├── embossRGBA.jpg │ ├── invertedGray.jpg │ ├── invertedRGBA.jpg │ ├── pixelateGray.jpg │ ├── pixelateRGBA.jpg │ ├── sepia.jpg │ ├── sharpenGray.jpg │ └── sharpenRGBA.jpg ├── engine.png ├── generate │ ├── linearGradientHorizontal.jpg │ ├── linearGradientVertical.jpg │ ├── sigmoidalGradientHorizontal.jpg │ └── sigmoidalGradientVertical.jpg ├── girl.jpg ├── grayscale │ ├── cropped_gray.jpg │ ├── cropped_gray16.jpg │ ├── gray.jpg │ └── gray16.jpg ├── histogram │ ├── gray.jpg │ └── rgba.jpg ├── io │ ├── outputJPG.jpg │ └── outputPNG.png ├── padding │ ├── grayPaddingBorderConstant.jpg │ ├── grayPaddingBorderConstantDistortedAnchor.jpg │ ├── grayPaddingBorderReflect.jpg │ ├── grayPaddingBorderReplicate.jpg │ ├── rgbaPaddedBorderConstant.jpg │ ├── rgbaPaddedBorderReflect.jpg │ └── rgbaPaddedBorderReplicate.jpg ├── resize │ ├── grayResize0_5x.jpg │ ├── grayResize1_5x.jpg │ ├── grayResize2_5_and_3_5x.jpg │ ├── grayResize2x.jpg │ ├── grayResize_CatmullRom_0_5x.jpg │ ├── grayResize_CatmullRom_2x.jpg │ ├── grayResize_Lanczos_0_5x.jpg │ ├── grayResize_Lanczos_2x.jpg │ ├── grayResize_Linear_2x.jpg │ ├── rgbaResize0_5x.jpg │ ├── rgbaResize1_5x.jpg │ ├── rgbaResize2_5_and_3_5x.jpg │ ├── rgbaResize2x.jpg │ ├── rgbaResize_CatmullRom_0_5x.jpg │ ├── rgbaResize_CatmullRom_2x.jpg │ ├── rgbaResize_Lanczos_0_5x.jpg │ ├── rgbaResize_Lanczos_2x.jpg │ └── rgbaResize_Linear_2x.jpg ├── threshold │ ├── otsuThreshBin.jpg │ ├── otsuThreshBinCropped.jpg │ ├── otsuThreshBinInvCropped.jpg │ ├── otsuThreshToZeroCropped.jpg │ ├── otsuThreshToZeroInvCropped.jpg │ ├── otsuThreshTruncCropped.jpg │ ├── thresh16Bin.jpg │ ├── thresh16BinInv.jpg │ ├── thresh16ToZero.jpg │ ├── thresh16ToZeroInv.jpg │ ├── thresh16Trunc.jpg │ ├── threshBin.jpg │ ├── threshBinInv.jpg │ ├── threshToZero.jpg │ └── threshTrunc.jpg └── transform │ ├── roateGray22.jpg │ ├── roateGray45.jpg │ ├── roateGray90.jpg │ ├── roateRGBA22.jpg │ ├── roateRGBA45.jpg │ ├── roateRGBA45Anchor200.jpg │ ├── roateRGBA45Noresize.jpg │ └── roateRGBA90.jpg ├── resize ├── filter.go ├── filter_test.go ├── resize.go └── resize_test.go ├── threshold ├── threshold.go └── threshold_test.go ├── transform ├── transform.go └── transform_test.go └── utils ├── constants.go ├── helpers.go ├── parallelhelpers.go ├── parallelhelpers_test.go └── test_utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Goland 2 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Szilagyi Ervin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imger 2 | [![MIT License](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/anthonynsimon/bild/blob/master/LICENSE) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/Ernyoke/Imger)](https://goreportcard.com/report/github.com/Ernyoke/Imger) 4 | 5 | This repository contains a collection of image processing algorithms written in pure Go. 6 | 7 | ## Currently supported 8 | * IO (ImreadGray, ImreadGray16, ImreadRGBA, ImreadRGBA64, Imwrite). Supported extensions: jpg, jpeg, png 9 | * Grayscale 10 | * Blend (AddScalarToGray, AddGray, AddGrayWeighted) 11 | * Threshold (Binary, BinaryInv, Trunc, ToZero, ToZeroInv, Otsu) 12 | * Image padding (BorderConstant, BorderReplicate, BorderReflect) 13 | * Convolution 14 | * Blur (Average - Box, Gaussian) 15 | * Edge detection (Sobel, Laplacian, Canny) 16 | * Resize (Nearest Neighbour, Linear, Catmull-Rom, Lanczos) 17 | * Effects (Pixelate, Sepia, Emboss, Sharpen, Invert) 18 | * Transform (Rotate) 19 | 20 | ## Install 21 | ```bash 22 | go get -u github.com/ernyoke/imger@v1.0.0 23 | ``` 24 | 25 | ## Running the Tests 26 | 27 | ```bash 28 | go test ./... 29 | ``` 30 | 31 | ## License 32 | This project is under the MIT License. See the LICENSE file for the full license text. -------------------------------------------------------------------------------- /blend/blend.go: -------------------------------------------------------------------------------- 1 | package blend 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "image/color" 8 | ) 9 | 10 | // AddScalarToGray takes a grayscale image and adds an integer value to all pixels of the image. If the result 11 | // overflows uint8, the result will be clamped to max uint8 (255). 12 | // Example of usage: 13 | // 14 | // res := blend.AddScalarToGray(img, 56) 15 | // 16 | func AddScalarToGray(img *image.Gray, value int) *image.Gray { 17 | res := image.NewGray(img.Rect) 18 | utils.ParallelForEachPixel(img.Bounds().Size(), func(x, y int) { 19 | pixel := int(img.GrayAt(x, y).Y) 20 | pixel += value 21 | res.SetGray(x, y, color.Gray{Y: uint8(utils.ClampInt(pixel, utils.MinUint8, int(utils.MaxUint8)))}) 22 | }) 23 | return res 24 | } 25 | 26 | // AddGray accepts two grayscale images and adds their pixel values. If the result for a given position overflows uint8, 27 | // the result will be clamped to max uint8 (255). 28 | // Example of usage: 29 | // 30 | // res, err := blend.AddGray(gray1, gray2) 31 | // 32 | func AddGray(img1 *image.Gray, img2 *image.Gray) (*image.Gray, error) { 33 | size1 := img1.Bounds().Size() 34 | size2 := img2.Bounds().Size() 35 | if size1.X != size2.X || size1.Y != size2.Y { 36 | return nil, errors.New("the size of the two image does not match") 37 | } 38 | res := image.NewGray(img1.Bounds()) 39 | utils.ParallelForEachPixel(size1, func(x int, y int) { 40 | p1 := img1.GrayAt(x, y) 41 | p2 := img2.GrayAt(x, y) 42 | sum := utils.ClampInt(int(p1.Y)+int(p2.Y), utils.MinUint8, int(utils.MaxUint8)) 43 | res.SetGray(x, y, color.Gray{uint8(sum)}) 44 | }) 45 | return res, nil 46 | } 47 | 48 | // AddGrayWeighted accepts two grayscale images and adds their pixel values using the following equation: 49 | // res(x, y) = img1(x, y) * w1 + img2(x, y) * w2 50 | // If the result for a given position overflows uint8, the result will be clamped to max uint8 (255). 51 | // If the result for a given position is negative, then it will be clamped to 0. 52 | // Example of usage: 53 | // 54 | // res, err := blend.AddGrayWeighted(gray1, 0.25, gray2, 0.75) 55 | // 56 | func AddGrayWeighted(img1 *image.Gray, w1 float64, img2 *image.Gray, w2 float64) (*image.Gray, error) { 57 | size1 := img1.Bounds().Size() 58 | size2 := img2.Bounds().Size() 59 | if size1.X != size2.X || size1.Y != size2.Y { 60 | return nil, errors.New("the size of the two image does not match") 61 | } 62 | res := image.NewGray(img1.Bounds()) 63 | utils.ParallelForEachPixel(size1, func(x int, y int) { 64 | p1 := img1.GrayAt(x, y) 65 | p2 := img2.GrayAt(x, y) 66 | sum := utils.ClampF64(float64(p1.Y)*w1+float64(p2.Y)*w2, utils.MinUint8, float64(utils.MaxUint8)) 67 | res.SetGray(x, y, color.Gray{uint8(sum)}) 68 | }) 69 | return res, nil 70 | } 71 | -------------------------------------------------------------------------------- /blend/blend_test.go: -------------------------------------------------------------------------------- 1 | package blend 2 | 3 | import ( 4 | "github.com/ernyoke/imger/utils" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | func Test_AddScalarToGray(t *testing.T) { 10 | input := image.Gray{ 11 | Rect: image.Rect(0, 0, 3, 3), 12 | Stride: 3, 13 | Pix: []uint8{ 14 | 0xFF, 0x80, 0x56, 15 | 0x56, 0x80, 0xFD, 16 | 0x00, 0x1A, 0xBB, 17 | }, 18 | } 19 | expected := &image.Gray{ 20 | Rect: image.Rect(0, 0, 3, 3), 21 | Stride: 3, 22 | Pix: []uint8{ 23 | 0xFF, 0x8A, 0x60, 24 | 0x60, 0x8A, 0xFF, 25 | 0x0A, 0x24, 0xC5, 26 | }, 27 | } 28 | result := AddScalarToGray(&input, 10) 29 | utils.CompareGrayImages(t, expected, result) 30 | } 31 | 32 | func Test_AddGray(t *testing.T) { 33 | input1 := image.Gray{ 34 | Rect: image.Rect(0, 0, 3, 3), 35 | Stride: 3, 36 | Pix: []uint8{ 37 | 0xFF, 0x80, 0x56, 38 | 0xFD, 0xFD, 0xFD, 39 | 0x00, 0x00, 0xBB, 40 | }, 41 | } 42 | input2 := image.Gray{ 43 | Rect: image.Rect(0, 0, 3, 3), 44 | Stride: 3, 45 | Pix: []uint8{ 46 | 0xFF, 0x0A, 0x56, 47 | 0x01, 0x02, 0x03, 48 | 0x00, 0xFF, 0xBB, 49 | }, 50 | } 51 | expected := &image.Gray{ 52 | Rect: image.Rect(0, 0, 3, 3), 53 | Stride: 3, 54 | Pix: []uint8{ 55 | 0xFF, 0x8A, 0xAC, 56 | 0xFE, 0xFF, 0xFF, 57 | 0x00, 0xFF, 0xFF, 58 | }, 59 | } 60 | result, err := AddGray(&input1, &input2) 61 | if err != nil { 62 | t.Fatalf("Error should not be returned. Error value: %s", err) 63 | } 64 | utils.CompareGrayImages(t, expected, result) 65 | } 66 | 67 | func Test_AddGrayWeighted(t *testing.T) { 68 | input1 := image.Gray{ 69 | Rect: image.Rect(0, 0, 3, 3), 70 | Stride: 3, 71 | Pix: []uint8{ 72 | 0xFF, 0x80, 0x56, 73 | 0xFD, 0xFD, 0xFD, 74 | 0x00, 0x00, 0xBB, 75 | }, 76 | } 77 | input2 := image.Gray{ 78 | Rect: image.Rect(0, 0, 3, 3), 79 | Stride: 3, 80 | Pix: []uint8{ 81 | 0xFF, 0x0A, 0x56, 82 | 0x01, 0x02, 0x03, 83 | 0x00, 0xFF, 0xBB, 84 | }, 85 | } 86 | expected := &image.Gray{ 87 | Rect: image.Rect(0, 0, 3, 3), 88 | Stride: 3, 89 | Pix: []uint8{ 90 | 0xFF, 0x45, 0x56, 91 | 0x7F, 0x7F, 0x80, 92 | 0x00, 0x7F, 0xBB, 93 | }, 94 | } 95 | result, err := AddGrayWeighted(&input1, 0.5, &input2, 0.5) 96 | if err != nil { 97 | t.Fatalf("Error should not be returned. Error value: %s", err) 98 | } 99 | utils.CompareGrayImages(t, expected, result) 100 | } 101 | 102 | func Test_AddGrayWeightedError(t *testing.T) { 103 | input1 := image.Gray{ 104 | Rect: image.Rect(0, 0, 3, 3), 105 | Stride: 3, 106 | Pix: []uint8{ 107 | 0xFF, 0x80, 0x56, 108 | 0xFD, 0xFD, 0xFD, 109 | 0x00, 0x00, 0xBB, 110 | }, 111 | } 112 | input2 := image.Gray{ 113 | Rect: image.Rect(0, 0, 3, 2), 114 | Stride: 3, 115 | Pix: []uint8{ 116 | 0xFF, 0x0A, 0x56, 117 | 0x01, 0x02, 0x03, 118 | }, 119 | } 120 | _, err := AddGrayWeighted(&input1, 0.5, &input2, 0.5) 121 | if err != nil { 122 | if err.Error() != "the size of the two image does not match" { 123 | t.Fatalf("Invalid error message!") 124 | } 125 | } else { 126 | t.Fatalf("Should not reach this point") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /blur/blur.go: -------------------------------------------------------------------------------- 1 | package blur 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/convolution" 6 | "github.com/ernyoke/imger/padding" 7 | "image" 8 | "math" 9 | ) 10 | 11 | // BoxGray applies average blur to a grayscale image. The amount of bluring effect depends on the kernel size, where 12 | // both width and height can be specified. The anchor point specifies a point inside the kernel. The pixel value 13 | // will be updated after the convolution was done for the given area. 14 | // Border types supported: see convolution package. 15 | func BoxGray(img *image.Gray, kernelSize image.Point, anchor image.Point, border padding.Border) (*image.Gray, error) { 16 | kernel := generateBoxKernel(&kernelSize) 17 | return convolution.ConvolveGray(img, kernel.Normalize(), anchor, border) 18 | } 19 | 20 | // BoxRGBA applies average blur to an RGBA image. The amount of bluring effect depends on the kernel size, where 21 | // both width and height can be specified. The anchor point specifies a point inside the kernel. The pixel value 22 | // will be updated after the convolution was done for the given area. 23 | // Border types supported: see convolution package. 24 | func BoxRGBA(img *image.RGBA, kernelSize image.Point, anchor image.Point, border padding.Border) (*image.RGBA, error) { 25 | kernel := generateBoxKernel(&kernelSize) 26 | return convolution.ConvolveRGBA(img, kernel.Normalize(), anchor, border) 27 | } 28 | 29 | // GaussianBlurGray applies average blur to a grayscale image. The amount of bluring effect depends on the kernel radius 30 | // and sigma value. The anchor point specifies a point inside the kernel. The pixel value will be updated after the 31 | // convolution was done for the given area. For border types see convolution package. 32 | func GaussianBlurGray(img *image.Gray, radius float64, sigma float64, border padding.Border) (*image.Gray, error) { 33 | if radius <= 0 { 34 | return nil, errors.New("radius must be bigger then 0") 35 | } 36 | return convolution.ConvolveGray(img, generateGaussianKernel(radius, sigma).Normalize(), image.Point{X: int(math.Ceil(radius)), Y: int(math.Ceil(radius))}, border) 37 | } 38 | 39 | // GaussianBlurRGBA applies average blur to an RGBA image. The amount of bluring effect depends on the kernel radius 40 | // and sigma value. The anchor point specifies a point inside the kernel. The pixel value will be updated after the 41 | // convolution was done for the given area. For border types see convolution package. 42 | func GaussianBlurRGBA(img *image.RGBA, radius float64, sigma float64, border padding.Border) (*image.RGBA, error) { 43 | if radius <= 0 { 44 | return nil, errors.New("radius must be bigger then 0") 45 | } 46 | return convolution.ConvolveRGBA(img, generateGaussianKernel(radius, sigma).Normalize(), image.Point{X: int(math.Ceil(radius)), Y: int(math.Ceil(radius))}, border) 47 | } 48 | 49 | // ------------------------------------------------------------------------------------------------------- 50 | func generateBoxKernel(kernelSize *image.Point) *convolution.Kernel { 51 | kernel, _ := convolution.NewKernel(kernelSize.X, kernelSize.Y) 52 | for x := 0; x < kernelSize.X; x++ { 53 | for y := 0; y < kernelSize.Y; y++ { 54 | kernel.Set(x, y, 1.0/float64(kernelSize.X*kernelSize.Y)) 55 | } 56 | } 57 | return kernel 58 | } 59 | 60 | func generateGaussianKernel(radius float64, sigma float64) *convolution.Kernel { 61 | length := int(math.Ceil(2*radius + 1)) 62 | kernel, _ := convolution.NewKernel(length, length) 63 | for x := 0; x < length; x++ { 64 | for y := 0; y < length; y++ { 65 | kernel.Set(x, y, gaussianFunc(float64(x)-radius, float64(y)-radius, sigma)) 66 | } 67 | } 68 | return kernel 69 | } 70 | 71 | func gaussianFunc(x, y, sigma float64) float64 { 72 | sigSqr := sigma * sigma 73 | return (1.0 / (2 * math.Pi * sigSqr)) * math.Exp(-(x*x+y*y)/(2*sigSqr)) 74 | } 75 | -------------------------------------------------------------------------------- /blur/blur_test.go: -------------------------------------------------------------------------------- 1 | package blur 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/padding" 6 | "github.com/ernyoke/imger/utils" 7 | "image" 8 | "testing" 9 | ) 10 | 11 | // ---------------------------------Unit tests------------------------------------ 12 | func TestGrayGaussianBlurZeroRadius(t *testing.T) { 13 | input := image.RGBA{ 14 | Rect: image.Rect(0, 0, 3, 3), 15 | Stride: 3 * 4, 16 | Pix: []uint8{ 17 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 18 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 19 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 20 | }, 21 | } 22 | _, err := GaussianBlurRGBA(&input, 0, 6, padding.BorderReflect) 23 | if err != nil { 24 | //ok 25 | } else { 26 | t.Fatal("no error thrown") 27 | } 28 | } 29 | 30 | func TestGrayGaussianBlurOneRadius(t *testing.T) { 31 | input := image.Gray{ 32 | Rect: image.Rect(0, 0, 3, 3), 33 | Stride: 3, 34 | Pix: []uint8{ 35 | 0xFF, 0x80, 0x56, 36 | 0x56, 0x80, 0x69, 37 | 0xEE, 0x29, 0xBB, 38 | }, 39 | } 40 | expected := &image.Gray{ 41 | Rect: image.Rect(0, 0, 3, 3), 42 | Stride: 3, 43 | Pix: []uint8{ 44 | 0x47, 0x5A, 0x33, 45 | 0x64, 0x88, 0x4D, 46 | 0x3B, 0x59, 0x36, 47 | }, 48 | } 49 | result, err := GaussianBlurGray(&input, 1, 2, padding.BorderConstant) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | utils.CompareGrayImagesWithOffset(t, expected, result, 1) 54 | } 55 | 56 | func TestRGBAGaussianBlurOneRadius(t *testing.T) { 57 | input := image.RGBA{ 58 | Rect: image.Rect(0, 0, 3, 3), 59 | Stride: 3 * 4, 60 | Pix: []uint8{ 61 | 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x56, 0x56, 0x56, 0xFF, 62 | 0x56, 0x56, 0x56, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x69, 0x69, 0x69, 0xFF, 63 | 0xEE, 0xEE, 0xEE, 0xFF, 0x29, 0x29, 0x29, 0xFF, 0xBB, 0xBB, 0xBB, 0xFF, 64 | }, 65 | } 66 | expected := &image.RGBA{ 67 | Rect: image.Rect(0, 0, 3, 3), 68 | Stride: 3 * 4, 69 | Pix: []uint8{ 70 | 0x47, 0x47, 0x47, 0xFF, 0x5A, 0x5A, 0x5A, 0xFF, 0x33, 0x33, 0x33, 0xFF, 71 | 0x64, 0x64, 0x64, 0xFF, 0x88, 0x88, 0x88, 0xFF, 0x4D, 0x4D, 0x4D, 0xFF, 72 | 0x3B, 0x3B, 0x3B, 0xFF, 0x59, 0x59, 0x59, 0xFF, 0x36, 0x36, 0x36, 0xFF, 73 | }, 74 | } 75 | actual, err := GaussianBlurRGBA(&input, 1, 2, padding.BorderConstant) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | //utils.PrintRGBA(t, actual) 80 | utils.CompareRGBAImagesWithOffset(t, expected, actual, 1) 81 | } 82 | 83 | // --------------------------------------------------------------------------------- 84 | 85 | // -----------------------------Acceptance tests------------------------------------ 86 | func setupTestCaseGray(t *testing.T) *image.Gray { 87 | path := "../res/girl.jpg" 88 | img, err := imgio.ImreadGray(path) 89 | if err != nil { 90 | t.Errorf("Could not read image from path: %s", path) 91 | } 92 | return img 93 | } 94 | 95 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 96 | path := "../res/girl.jpg" 97 | img, err := imgio.ImreadRGBA(path) 98 | if err != nil { 99 | t.Errorf("Could not read image from path: %s", path) 100 | } 101 | return img 102 | } 103 | 104 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 105 | err := imgio.Imwrite(img, path) 106 | if err != nil { 107 | t.Errorf("Could not write image to path: %s", path) 108 | } 109 | } 110 | 111 | func Test_Acceptance_GrayBlurInt(t *testing.T) { 112 | gray := setupTestCaseGray(t) 113 | blured, _ := BoxGray(gray, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, padding.BorderReflect) 114 | tearDownTestCase(t, blured, "../res/blur/grayBlur.jpg") 115 | } 116 | 117 | func Test_Acceptance_RGBABlurInt(t *testing.T) { 118 | rgba := setupTestCaseRGBA(t) 119 | blured, _ := BoxRGBA(rgba, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, padding.BorderReflect) 120 | tearDownTestCase(t, blured, "../res/blur/rgbaBlur.jpg") 121 | } 122 | 123 | func Test_Acceptance_GrayGaussianBlurInt(t *testing.T) { 124 | gray := setupTestCaseGray(t) 125 | blured, _ := GaussianBlurGray(gray, 7, 6, padding.BorderReflect) 126 | tearDownTestCase(t, blured, "../res/blur/grayGaussianBlur.jpg") 127 | } 128 | 129 | func Test_Acceptance_RGBAGaussianBlurInt(t *testing.T) { 130 | rgba := setupTestCaseRGBA(t) 131 | blured, _ := GaussianBlurRGBA(rgba, 5, 500, padding.BorderReflect) 132 | tearDownTestCase(t, blured, "../res/blur/rgbaGaussianBlur.jpg") 133 | } 134 | 135 | // ---------------------------------------------------------------------------------- 136 | -------------------------------------------------------------------------------- /convolution/convolution.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import ( 4 | "github.com/ernyoke/imger/padding" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "image/color" 8 | ) 9 | 10 | // ConvolveGray applies a convolution matrix (kernel) to a grayscale image. 11 | // Example of usage: 12 | // 13 | // res, err := convolution.ConvolveGray(img, kernel, {1, 1}, BorderReflect) 14 | // 15 | // Note: the anchor represents a point inside the area of the kernel. After every step of the convolution the position 16 | // specified by the anchor point gets updated on the result image. 17 | func ConvolveGray(img *image.Gray, kernel *Kernel, anchor image.Point, border padding.Border) (*image.Gray, error) { 18 | kernelSize := kernel.Size() 19 | padded, error := padding.PaddingGray(img, kernelSize, anchor, border) 20 | if error != nil { 21 | return nil, error 22 | } 23 | originalSize := img.Bounds().Size() 24 | resultImage := image.NewGray(img.Bounds()) 25 | utils.ParallelForEachPixel(originalSize, func(x int, y int) { 26 | sum := float64(0) 27 | for ky := 0; ky < kernelSize.Y; ky++ { 28 | for kx := 0; kx < kernelSize.X; kx++ { 29 | pixel := padded.GrayAt(x+kx, y+ky) 30 | kE := kernel.At(kx, ky) 31 | sum += float64(pixel.Y) * kE 32 | } 33 | } 34 | sum = utils.ClampF64(sum, utils.MinUint8, float64(utils.MaxUint8)) 35 | resultImage.Set(x, y, color.Gray{uint8(sum)}) 36 | }) 37 | return resultImage, nil 38 | } 39 | 40 | // ConvolveRGBA applies a convolution matrix (kernel) to an RGBA image. 41 | // Example of usage: 42 | // 43 | // res, err := convolution.ConvolveRGBA(img, kernel, {1, 1}, BorderReflect) 44 | // 45 | // Note: the anchor represents a point inside the area of the kernel. After every step of the convolution the position 46 | // specified by the anchor point gets updated on the result image. 47 | func ConvolveRGBA(img *image.RGBA, kernel *Kernel, anchor image.Point, border padding.Border) (*image.RGBA, error) { 48 | kernelSize := kernel.Size() 49 | padded, err := padding.PaddingRGBA(img, kernelSize, anchor, border) 50 | if err != nil { 51 | return nil, err 52 | } 53 | originalSize := img.Bounds().Size() 54 | resultImage := image.NewRGBA(img.Bounds()) 55 | utils.ParallelForEachPixel(originalSize, func(x int, y int) { 56 | sumR, sumG, sumB := 0.0, 0.0, 0.0 57 | for kx := 0; kx < kernelSize.X; kx++ { 58 | for ky := 0; ky < kernelSize.Y; ky++ { 59 | pixel := padded.RGBAAt(x+kx, y+ky) 60 | sumR += float64(pixel.R) * kernel.At(kx, ky) 61 | sumG += float64(pixel.G) * kernel.At(kx, ky) 62 | sumB += float64(pixel.B) * kernel.At(kx, ky) 63 | } 64 | } 65 | sumR = utils.ClampF64(sumR, utils.MinUint8, float64(utils.MaxUint8)) 66 | sumG = utils.ClampF64(sumG, utils.MinUint8, float64(utils.MaxUint8)) 67 | sumB = utils.ClampF64(sumB, utils.MinUint8, float64(utils.MaxUint8)) 68 | rgba := img.RGBAAt(x, y) 69 | resultImage.Set(x, y, color.RGBA{uint8(sumR), uint8(sumG), uint8(sumB), rgba.A}) 70 | }) 71 | return resultImage, nil 72 | } 73 | -------------------------------------------------------------------------------- /convolution/convolution_test.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import ( 4 | "github.com/ernyoke/imger/padding" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "testing" 8 | ) 9 | 10 | // ---------------------------------Unit tests------------------------------------ 11 | func Test_ConvolveGray_0Kernel(t *testing.T) { 12 | gray := image.Gray{ 13 | Rect: image.Rect(0, 0, 3, 3), 14 | Stride: 3, 15 | Pix: []uint8{ 16 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 17 | }, 18 | } 19 | expected := image.Gray{ 20 | Rect: image.Rect(0, 0, 3, 3), 21 | Stride: 3, 22 | Pix: []uint8{ 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | }, 25 | } 26 | kernel := Kernel{[][]float64{ 27 | {0, 0, 0}, 28 | {0, 0, 0}, 29 | {0, 0, 0}, 30 | }, 3, 3} 31 | conv, _ := ConvolveGray(&gray, &kernel, image.Point{X: 1, Y: 1}, padding.BorderConstant) 32 | size := conv.Bounds().Size() 33 | utils.ForEachPixel(size, func(x, y int) { 34 | pExp := expected.GrayAt(x, y).Y 35 | pRes := conv.GrayAt(x, y).Y 36 | if pExp != pRes { 37 | t.Errorf("Expected: %d - Actual: %d at %d, %d", pExp, pRes, x, y) 38 | } 39 | }) 40 | } 41 | 42 | func Test_ConvolveGray_1Kernel(t *testing.T) { 43 | var gray image.Gray 44 | gray = image.Gray{ 45 | Rect: image.Rect(0, 0, 3, 3), 46 | Stride: 3, 47 | Pix: []uint8{ 48 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 49 | }, 50 | } 51 | var expected image.Gray 52 | expected = image.Gray{ 53 | Rect: image.Rect(0, 0, 3, 3), 54 | Stride: 3, 55 | Pix: []uint8{ 56 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 57 | }, 58 | } 59 | var kernel Kernel 60 | kernel = Kernel{[][]float64{ 61 | {0, 0, 0}, 62 | {0, 1, 0}, 63 | {0, 0, 0}, 64 | }, 3, 3} 65 | conv, _ := ConvolveGray(&gray, &kernel, image.Point{X: 1, Y: 1}, padding.BorderConstant) 66 | size := conv.Bounds().Size() 67 | utils.ForEachPixel(size, func(x, y int) { 68 | pExp := expected.GrayAt(x, y).Y 69 | pRes := conv.GrayAt(x, y).Y 70 | if pExp != pRes { 71 | t.Errorf("Expected: %d - Actual: %d at %d, %d", pExp, pRes, x, y) 72 | } 73 | }) 74 | } 75 | 76 | func Test_ConvoleGray_11Kernel(t *testing.T) { 77 | gray := image.Gray{ 78 | Rect: image.Rect(0, 0, 3, 3), 79 | Stride: 3, 80 | Pix: []uint8{ 81 | 0x80, 0xFF, 0xFF, 82 | 0xFF, 0x40, 0x40, 83 | 0xFF, 0xFF, 0x40, 84 | }, 85 | } 86 | expected := image.Gray{ 87 | Rect: image.Rect(0, 0, 3, 3), 88 | Stride: 3, 89 | Pix: []uint8{ 90 | 0xFF, 0xFF, 0xFF, 91 | 0xFF, 0xFF, 0x80, 92 | 0xFF, 0xFF, 0x40, 93 | }, 94 | } 95 | var kernel Kernel 96 | kernel = Kernel{[][]float64{ 97 | {0, 0, 0}, 98 | {0, 1, 1}, 99 | {0, 0, 0}, 100 | }, 3, 3} 101 | conv, _ := ConvolveGray(&gray, &kernel, image.Point{X: 1, Y: 1}, padding.BorderConstant) 102 | size := conv.Bounds().Size() 103 | utils.ForEachPixel(size, func(x, y int) { 104 | pExp := expected.GrayAt(x, y) 105 | pRes := conv.GrayAt(x, y) 106 | if pExp != pRes { 107 | t.Errorf("Expected: %d - Actual: %d at %d, %d", pExp, pRes, x, y) 108 | } 109 | }) 110 | } 111 | 112 | func Test_ConvoleRGBA_1Kernel(t *testing.T) { 113 | rgba := image.RGBA{ 114 | Rect: image.Rect(0, 0, 3, 3), 115 | Stride: 3 * 4, 116 | Pix: []uint8{ 117 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 118 | 0xFF, 0x01, 0x02, 0x03, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 119 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 120 | }, 121 | } 122 | expected := image.RGBA{ 123 | Rect: image.Rect(0, 0, 3, 3), 124 | Stride: 3 * 4, 125 | Pix: []uint8{ 126 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 127 | 0xFF, 0x01, 0x02, 0x03, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 128 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 129 | }, 130 | } 131 | var kernel Kernel 132 | kernel = Kernel{[][]float64{ 133 | {0, 0, 0}, 134 | {0, 1, 0}, 135 | {0, 0, 0}, 136 | }, 3, 3} 137 | conv, _ := ConvolveRGBA(&rgba, &kernel, image.Point{X: 1, Y: 1}, padding.BorderConstant) 138 | size := conv.Bounds().Size() 139 | utils.ForEachPixel(size, func(x, y int) { 140 | pExp := expected.RGBAAt(x, y) 141 | pRes := conv.RGBAAt(x, y) 142 | if pExp != pRes { 143 | t.Errorf("Expected: %d - Actual: %d at %d, %d", pExp, pRes, x, y) 144 | } 145 | }) 146 | } 147 | 148 | func Test_ConvoleRGBA_11Kernel(t *testing.T) { 149 | rgba := image.RGBA{ 150 | Rect: image.Rect(0, 0, 3, 3), 151 | Stride: 3 * 4, 152 | Pix: []uint8{ 153 | 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 154 | 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 155 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0x40, 156 | }, 157 | } 158 | expected := image.RGBA{ 159 | Rect: image.Rect(0, 0, 3, 3), 160 | Stride: 3 * 4, 161 | Pix: []uint8{ 162 | 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 163 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x80, 0x80, 0x80, 0x40, 164 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x40, 0x40, 0x40, 165 | }, 166 | } 167 | var kernel Kernel 168 | kernel = Kernel{[][]float64{ 169 | {0, 0, 0}, 170 | {0, 1, 1}, 171 | {0, 0, 0}, 172 | }, 3, 3} 173 | conv, _ := ConvolveRGBA(&rgba, &kernel, image.Point{X: 1, Y: 1}, padding.BorderConstant) 174 | size := conv.Bounds().Size() 175 | utils.ForEachPixel(size, func(x, y int) { 176 | pExp := expected.RGBAAt(x, y) 177 | pRes := conv.RGBAAt(x, y) 178 | if pExp != pRes { 179 | t.Errorf("Expected: %d - Actual: %d at %d, %d", pExp, pRes, x, y) 180 | } 181 | }) 182 | } 183 | 184 | // ------------------------------------------------------------------------------- 185 | -------------------------------------------------------------------------------- /convolution/kernel.go: -------------------------------------------------------------------------------- 1 | package convolution 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "math" 7 | ) 8 | 9 | // Matrix interface for the Kernel 10 | type Matrix interface { 11 | At(x, y int) float64 12 | } 13 | 14 | // Kernel is a 2 dimensional matrix used mainly for convolution. 15 | type Kernel struct { 16 | Content [][]float64 17 | Width int 18 | Height int 19 | } 20 | 21 | // NewKernel creates a new Kernel with the given width and height. The value for every position of the kernel is 0. 22 | func NewKernel(width int, height int) (*Kernel, error) { 23 | if width < 0 || height < 0 { 24 | return nil, errors.New("negative kernel size") 25 | } 26 | m := make([][]float64, height) 27 | for i := range m { 28 | m[i] = make([]float64, width) 29 | } 30 | return &Kernel{Content: m, Width: width, Height: height}, nil 31 | } 32 | 33 | // At returns a value from the position of {x, y} of a kernel. 34 | func (k *Kernel) At(x, y int) float64 { 35 | return k.Content[x][y] 36 | } 37 | 38 | // Set sets a value at a given {x, y} position 39 | func (k *Kernel) Set(x int, y int, value float64) { 40 | k.Content[x][y] = value 41 | } 42 | 43 | // Size returns the size of the kernel. The size is a type of image.Point containing the width and height of the kernel. 44 | func (k *Kernel) Size() image.Point { 45 | return image.Point{X: k.Width, Y: k.Height} 46 | } 47 | 48 | // AbSum returns the sum of every absolute value from a kernel. 49 | func (k *Kernel) AbSum() float64 { 50 | var sum float64 51 | for x := 0; x < k.Height; x++ { 52 | for y := 0; y < k.Width; y++ { 53 | sum += math.Abs(k.At(x, y)) 54 | } 55 | } 56 | return sum 57 | } 58 | 59 | // Normalize returns a normalized kernel where each value is divided by the absolute sum of the kernel. 60 | func (k *Kernel) Normalize() *Kernel { 61 | normalized, _ := NewKernel(k.Width, k.Height) 62 | sum := k.AbSum() 63 | if sum == 0 { 64 | sum = 1 65 | 66 | } 67 | for x := 0; x < k.Height; x++ { 68 | for y := 0; y < k.Width; y++ { 69 | normalized.Set(x, y, k.At(x, y)/sum) 70 | } 71 | } 72 | return normalized 73 | } 74 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package imger contains a collection of image processing algorithms written in pure Go. 2 | package imger 3 | -------------------------------------------------------------------------------- /edgedetection/canny.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/blur" 6 | "github.com/ernyoke/imger/grayscale" 7 | "github.com/ernyoke/imger/padding" 8 | "github.com/ernyoke/imger/utils" 9 | "image" 10 | "image/color" 11 | "math" 12 | ) 13 | 14 | // CannyGray computes the edges of a given grayscale image using the Canny edge detection algorithm. The returned image 15 | // is a grayscale image represented on 8 bits. 16 | func CannyGray(img *image.Gray, lower float64, upper float64, kernelSize uint) (*image.Gray, error) { 17 | 18 | // blur the image using Gaussian filter 19 | blurred, err := blur.GaussianBlurGray(img, float64(kernelSize), 1, padding.BorderConstant) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | // get vertical and horizontal edges using Sobel filter 25 | vertical, err := VerticalSobelGray(blurred, padding.BorderConstant) 26 | if err != nil { 27 | return nil, err 28 | } 29 | horizontal, err := HorizontalSobelGray(blurred, padding.BorderConstant) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // calculate the gradient values and orientation angles for each pixel 35 | g, theta, err := gradientAndOrientation(vertical, horizontal) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // "thin" the edges using non-max suppression procedure 41 | thinEdges := nonMaxSuppression(blurred, g, theta) 42 | 43 | // hysteresis 44 | hist := threshold(thinEdges, g, lower, upper) 45 | //_ = threshold(thinEdges, g, lower, upper) 46 | 47 | return hist, nil 48 | } 49 | 50 | // CannyRGBA computes the edges of a given RGBA image using the Canny edge detection algorithm. The returned image is a 51 | // grayscale image represented on 8 bits. 52 | func CannyRGBA(img *image.RGBA, lower float64, upper float64, kernelSize uint) (*image.Gray, error) { 53 | return CannyGray(grayscale.Grayscale(img), lower, upper, kernelSize) 54 | } 55 | 56 | func gradientAndOrientation(vertical *image.Gray, horizontal *image.Gray) ([][]float64, [][]float64, error) { 57 | size := vertical.Bounds().Size() 58 | theta := make([][]float64, size.X) 59 | g := make([][]float64, size.X) 60 | for x := 0; x < size.X; x++ { 61 | theta[x] = make([]float64, size.Y) 62 | g[x] = make([]float64, size.Y) 63 | err := errors.New("none") 64 | for y := 0; y < size.Y; y++ { 65 | px := float64(vertical.GrayAt(x, y).Y) 66 | py := float64(horizontal.GrayAt(x, y).Y) 67 | g[x][y] = math.Hypot(px, py) 68 | theta[x][y], err = orientation(math.Atan2(float64(vertical.GrayAt(x, y).Y), float64(horizontal.GrayAt(x, y).Y))) 69 | if err != nil { 70 | return nil, nil, err 71 | } 72 | } 73 | } 74 | return g, theta, nil 75 | } 76 | 77 | func isBetween(val float64, lowerBound float64, upperBound float64) bool { 78 | return val >= lowerBound && val < upperBound 79 | } 80 | 81 | func orientation(x float64) (float64, error) { 82 | angle := 180 * x / math.Pi 83 | if isBetween(angle, 0, 22.5) || isBetween(angle, -180, -157.5) { 84 | return 0, nil 85 | } 86 | if isBetween(angle, 157.5, 180) || isBetween(angle, -22.5, 0) { 87 | return 0, nil 88 | } 89 | if isBetween(angle, 22.5, 67.5) || isBetween(angle, -157.5, -112.5) { 90 | return 45, nil 91 | } 92 | if isBetween(angle, 67.5, 112.5) || isBetween(angle, -112.5, -67.5) { 93 | return 90, nil 94 | } 95 | if isBetween(angle, 112.5, 157.5) || isBetween(angle, -67.5, -22.5) { 96 | return 135, nil 97 | } 98 | return 0, errors.New("invalid angle") 99 | } 100 | 101 | func isBiggerThenNeighbours(val float64, neighbour1 float64, neighbour2 float64) bool { 102 | return val > neighbour1 && val > neighbour2 103 | } 104 | 105 | func nonMaxSuppression(img *image.Gray, g [][]float64, theta [][]float64) *image.Gray { 106 | size := img.Bounds().Size() 107 | thinEdges := image.NewGray(image.Rect(0, 0, size.X, size.Y)) 108 | utils.ParallelForEachPixel(size, func(x, y int) { 109 | isLocalMax := false 110 | if x > 0 && x < size.X-1 && y > 0 && y < size.Y-1 { 111 | switch theta[x][y] { 112 | case 45: 113 | if isBiggerThenNeighbours(g[x][y], g[x+1][y-1], g[x-1][y+1]) { 114 | isLocalMax = true 115 | } 116 | case 90: 117 | if isBiggerThenNeighbours(g[x][y], g[x+1][y], g[x-1][y]) { 118 | isLocalMax = true 119 | } 120 | case 135: 121 | if isBiggerThenNeighbours(g[x][y], g[x-1][y-1], g[x+1][y+1]) { 122 | isLocalMax = true 123 | } 124 | case 0: 125 | if isBiggerThenNeighbours(g[x][y], g[x][y+1], g[x][y-1]) { 126 | isLocalMax = true 127 | } 128 | } 129 | } 130 | if isLocalMax { 131 | thinEdges.SetGray(x, y, color.Gray{Y: utils.MaxUint8}) 132 | } 133 | }) 134 | return thinEdges 135 | } 136 | 137 | func threshold(img *image.Gray, g [][]float64, lowerBound float64, upperBound float64) *image.Gray { 138 | size := img.Bounds().Size() 139 | res := image.NewGray(image.Rect(0, 0, size.X, size.Y)) 140 | utils.ParallelForEachPixel(size, func(x int, y int) { 141 | p := img.GrayAt(x, y) 142 | if p.Y == utils.MaxUint8 { 143 | if g[x][y] < lowerBound { 144 | res.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 145 | } 146 | if g[x][y] > upperBound { 147 | res.SetGray(x, y, color.Gray{Y: utils.MaxUint8}) 148 | } 149 | } 150 | }) 151 | utils.ParallelForEachPixel(size, func(x int, y int) { 152 | p := img.GrayAt(x, y) 153 | if p.Y == utils.MaxUint8 && x > 0 && x < size.X-1 && y > 0 && y < size.Y-1 { 154 | if g[x][y] >= lowerBound && g[x][y] <= upperBound { 155 | if checkNeighbours(x, y, res) { 156 | res.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 157 | } 158 | } 159 | } 160 | }) 161 | return res 162 | } 163 | 164 | func checkNeighbours(x, y int, img *image.Gray) bool { 165 | return img.GrayAt(x-1, y-1).Y == utils.MaxUint8 || img.GrayAt(x-1, y).Y == utils.MaxUint8 || 166 | img.GrayAt(x-1, y+1).Y == utils.MaxUint8 || img.GrayAt(x, y-1).Y == utils.MaxUint8 || 167 | img.GrayAt(x, y+1).Y == utils.MaxUint8 || img.GrayAt(x+1, y-1).Y == utils.MaxUint8 || 168 | img.GrayAt(x+1, y).Y == utils.MaxUint8 || img.GrayAt(x+1, y+1).Y == utils.MaxUint8 169 | } 170 | -------------------------------------------------------------------------------- /edgedetection/canny_test.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | // -----------------------------Acceptance tests------------------------------------ 10 | func setupTestCaseGray(t *testing.T) *image.Gray { 11 | path := "../res/engine.png" 12 | img, err := imgio.ImreadGray(path) 13 | if err != nil { 14 | t.Errorf("Could not read image from path: %s", path) 15 | } 16 | return img 17 | } 18 | 19 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 20 | path := "../res/engine.png" 21 | img, err := imgio.ImreadRGBA(path) 22 | if err != nil { 23 | t.Errorf("Could not read image from path: %s", path) 24 | } 25 | return img 26 | } 27 | 28 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 29 | err := imgio.Imwrite(img, path) 30 | if err != nil { 31 | t.Errorf("Could not write image to path: %s", path) 32 | } 33 | } 34 | 35 | func Test_Acceptance_CannyGray(t *testing.T) { 36 | gray := setupTestCaseGray(t) 37 | cny, err := CannyGray(gray, 15, 45, 5) 38 | if err != nil { 39 | t.Fatalf("Should not reach this point!") 40 | } 41 | tearDownTestCase(t, cny, "../res/edge/cannygray.jpg") 42 | } 43 | 44 | func Test_Acceptance_CannyRGBA(t *testing.T) { 45 | rgba := setupTestCaseRGBA(t) 46 | cny, err := CannyRGBA(rgba, 15, 45, 5) 47 | if err != nil { 48 | t.Fatalf("Should not reach this point!") 49 | } 50 | tearDownTestCase(t, cny, "../res/edge/cannyrgba.jpg") 51 | } 52 | -------------------------------------------------------------------------------- /edgedetection/laplacian.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/convolution" 6 | "github.com/ernyoke/imger/grayscale" 7 | "github.com/ernyoke/imger/padding" 8 | "image" 9 | ) 10 | 11 | var kernel4 = convolution.Kernel{Content: [][]float64{ 12 | {0, 1, 0}, 13 | {1, -4, 1}, 14 | {0, 1, 0}, 15 | }, Width: 3, Height: 3} 16 | 17 | var kernel8 = convolution.Kernel{Content: [][]float64{ 18 | {1, 1, 1}, 19 | {1, -8, 1}, 20 | {1, 1, 1}, 21 | }, Width: 3, Height: 3} 22 | 23 | // LaplacianKernel - constant type for differentiating Laplacian kernels 24 | type LaplacianKernel int 25 | 26 | const ( 27 | // K4 Laplacian kernel: 28 | // {0, 1, 0}, 29 | // {1, -4, 1}, 30 | // {0, 1, 0}, 31 | K4 LaplacianKernel = iota 32 | // K8 Laplacian kernel: 33 | // {0, 1, 0}, 34 | // {1, -8, 1}, 35 | // {0, 1, 0}, 36 | K8 37 | ) 38 | 39 | // LaplacianGray applies Laplacian filter to a grayscale image. The kernel types are: K4 and K8 (see LaplacianKernel) 40 | // Example of usage: 41 | // 42 | // res, err := edgedetection.LaplacianGray(img, paddding.BorderReflect, edgedetection.K8) 43 | // 44 | func LaplacianGray(gray *image.Gray, border padding.Border, kernel LaplacianKernel) (*image.Gray, error) { 45 | var laplacianKernel convolution.Kernel 46 | switch kernel { 47 | case K4: 48 | laplacianKernel = kernel4 49 | case K8: 50 | laplacianKernel = kernel8 51 | default: 52 | return nil, errors.New("invalid kernel") 53 | } 54 | return convolution.ConvolveGray(gray, &laplacianKernel, image.Point{X: 1, Y: 1}, border) 55 | } 56 | 57 | // LaplacianRGBA applies Laplacian filter to an RGBA image. The kernel types are: K4 and K8 (see LaplacianKernel) 58 | // Example of usage: 59 | // 60 | // res, err := edgedetection.LaplacianRGBA(img, paddding.BorderReflect, edgedetection.K8) 61 | // 62 | func LaplacianRGBA(img *image.RGBA, border padding.Border, kernel LaplacianKernel) (*image.Gray, error) { 63 | gray := grayscale.Grayscale(img) 64 | return LaplacianGray(gray, border, kernel) 65 | } 66 | -------------------------------------------------------------------------------- /edgedetection/laplacian_test.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/padding" 6 | "image" 7 | "testing" 8 | ) 9 | 10 | // -----------------------------Acceptance tests------------------------------------ 11 | 12 | func setupTestCaseGrayLapl(t *testing.T) *image.Gray { 13 | path := "../res/engine.png" 14 | img, err := imgio.ImreadGray(path) 15 | if err != nil { 16 | t.Errorf("Could not read image from path: %s", path) 17 | } 18 | return img 19 | } 20 | 21 | func setupTestCaseRGBALapl(t *testing.T) *image.RGBA { 22 | path := "../res/engine.png" 23 | img, err := imgio.ImreadRGBA(path) 24 | if err != nil { 25 | t.Errorf("Could not read image from path: %s", path) 26 | } 27 | return img 28 | } 29 | 30 | func tearDownTestCaseLapl(t *testing.T, img image.Image, path string) { 31 | err := imgio.Imwrite(img, path) 32 | if err != nil { 33 | t.Errorf("Could not write image to path: %s", path) 34 | } 35 | } 36 | 37 | func Test_Acceptance_LaplacianGrayK4(t *testing.T) { 38 | gray := setupTestCaseGrayLapl(t) 39 | laplacian, _ := LaplacianGray(gray, padding.BorderReflect, K4) 40 | tearDownTestCaseLapl(t, laplacian, "../res/edge/laplacianGrayK4.png") 41 | } 42 | 43 | func Test_Acceptance_LaplacianGrayK8(t *testing.T) { 44 | gray := setupTestCaseGrayLapl(t) 45 | laplacian, _ := LaplacianGray(gray, padding.BorderReflect, K8) 46 | tearDownTestCaseLapl(t, laplacian, "../res/edge/laplacianGrayK8.png") 47 | } 48 | 49 | func Test_Acceptance_LaplacianRGBAK4(t *testing.T) { 50 | rgba := setupTestCaseRGBALapl(t) 51 | laplacian, _ := LaplacianRGBA(rgba, padding.BorderReflect, K4) 52 | tearDownTestCaseLapl(t, laplacian, "../res/edge/laplacianRGBAK4.png") 53 | } 54 | 55 | func Test_Acceptance_LaplacianRGBAK8(t *testing.T) { 56 | rgba := setupTestCaseRGBALapl(t) 57 | laplacian, _ := LaplacianRGBA(rgba, padding.BorderReflect, K8) 58 | tearDownTestCaseLapl(t, laplacian, "../res/edge/laplacianRGBAK8.png") 59 | } 60 | 61 | // --------------------------------------------------------------------------------- 62 | -------------------------------------------------------------------------------- /edgedetection/sobel.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "github.com/ernyoke/imger/blend" 5 | "github.com/ernyoke/imger/convolution" 6 | "github.com/ernyoke/imger/grayscale" 7 | "github.com/ernyoke/imger/padding" 8 | "image" 9 | ) 10 | 11 | var horizontalKernel = convolution.Kernel{Content: [][]float64{ 12 | {-1, 0, 1}, 13 | {-2, 0, 2}, 14 | {-1, 0, 1}, 15 | }, Width: 3, Height: 3} 16 | 17 | var verticalKernel = convolution.Kernel{Content: [][]float64{ 18 | {-1, -2, -1}, 19 | {0, 0, 0}, 20 | {1, 2, 1}, 21 | }, Width: 3, Height: 3} 22 | 23 | // HorizontalSobelGray applies the horizontal Sobel operator (horizontal kernel) to a grayscale image. The result 24 | // of the Sobel operator is a 2-dimensional map of the gradient at each point. 25 | // More information on the Sobel operator: https://en.wikipedia.org/wiki/Sobel_operator 26 | func HorizontalSobelGray(gray *image.Gray, border padding.Border) (*image.Gray, error) { 27 | return convolution.ConvolveGray(gray, &horizontalKernel, image.Point{X: 1, Y: 1}, border) 28 | } 29 | 30 | // VerticalSobelGray applies the vertical Sobel operator (vertical kernel) to a grayscale image. The result 31 | // of the Sobel operator is a 2-dimensional map of the gradient at each point. 32 | // More information on the Sobel operator: https://en.wikipedia.org/wiki/Sobel_operator 33 | func VerticalSobelGray(gray *image.Gray, border padding.Border) (*image.Gray, error) { 34 | return convolution.ConvolveGray(gray, &verticalKernel, image.Point{X: 1, Y: 1}, border) 35 | } 36 | 37 | // SobelGray combines the horizontal and the vertical gradients of a grayscale image. The result is grayscale image 38 | // which contains the high gradients ("edges") marked as white. 39 | func SobelGray(img *image.Gray, border padding.Border) (*image.Gray, error) { 40 | horizontal, error := HorizontalSobelGray(img, border) 41 | if error != nil { 42 | return nil, error 43 | } 44 | vertical, error := VerticalSobelGray(img, border) 45 | if error != nil { 46 | return nil, error 47 | } 48 | res, error := blend.AddGrayWeighted(horizontal, 0.5, vertical, 0.5) 49 | if error != nil { 50 | return nil, error 51 | } 52 | return res, nil 53 | } 54 | 55 | // HorizontalSobelRGBA applies the horizontal Sobel operator (horizontal kernel) to an RGGBA image. The result 56 | // of the Sobel operator is a 2-dimensional map of the gradient at each point. 57 | // More information on the Sobel operator: https://en.wikipedia.org/wiki/Sobel_operator 58 | func HorizontalSobelRGBA(img *image.RGBA, border padding.Border) (*image.Gray, error) { 59 | gray := grayscale.Grayscale(img) 60 | return convolution.ConvolveGray(gray, &horizontalKernel, image.Point{X: 1, Y: 1}, border) 61 | } 62 | 63 | // VerticalSobelRGBA applies the vertical Sobel operator (vertical kernel) to an RGBA image. The result 64 | // of the Sobel operator is a 2-dimensional map of the gradient at each point. 65 | // More information on the Sobel operator: https://en.wikipedia.org/wiki/Sobel_operator 66 | func VerticalSobelRGBA(img *image.RGBA, border padding.Border) (*image.Gray, error) { 67 | gray := grayscale.Grayscale(img) 68 | return convolution.ConvolveGray(gray, &verticalKernel, image.Point{X: 1, Y: 1}, border) 69 | } 70 | 71 | // SobelRGBA combines the horizontal and the vertical gradients of an RGBA image. The result is grayscale image 72 | // which contains the high gradients ("edges") marked as white. 73 | func SobelRGBA(img *image.RGBA, border padding.Border) (*image.Gray, error) { 74 | gray := grayscale.Grayscale(img) 75 | return SobelGray(gray, border) 76 | } 77 | -------------------------------------------------------------------------------- /edgedetection/sobel_test.go: -------------------------------------------------------------------------------- 1 | package edgedetection 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/padding" 6 | "image" 7 | "testing" 8 | ) 9 | 10 | // -----------------------------Acceptance tests------------------------------------ 11 | 12 | func setupTestCaseGraySobel(t *testing.T) *image.Gray { 13 | path := "../res/engine.png" 14 | img, err := imgio.ImreadGray(path) 15 | if err != nil { 16 | t.Errorf("Could not read image from path: %s", path) 17 | } 18 | return img 19 | } 20 | 21 | func setupTestCaseRGBASobel(t *testing.T) *image.RGBA { 22 | path := "../res/engine.png" 23 | img, err := imgio.ImreadRGBA(path) 24 | if err != nil { 25 | t.Errorf("Could not read image from path: %s", path) 26 | } 27 | return img 28 | } 29 | 30 | func tearDownTestCaseSobel(t *testing.T, img image.Image, path string) { 31 | err := imgio.Imwrite(img, path) 32 | if err != nil { 33 | t.Errorf("Could not write image to path: %s", path) 34 | } 35 | } 36 | 37 | func Test_Acceptance_HorizontalSobelGray(t *testing.T) { 38 | gray := setupTestCaseGraySobel(t) 39 | sobel, _ := HorizontalSobelGray(gray, padding.BorderReflect) 40 | tearDownTestCaseSobel(t, sobel, "../res/edge/horizontalSobelGray.png") 41 | } 42 | 43 | func Test_Acceptance_VerticalSobelGray(t *testing.T) { 44 | gray := setupTestCaseGraySobel(t) 45 | sobel, _ := VerticalSobelGray(gray, padding.BorderReflect) 46 | tearDownTestCaseSobel(t, sobel, "../res/edge/verticalSobelGray.png") 47 | } 48 | 49 | func Test_Acceptance_SobelGray(t *testing.T) { 50 | gray := setupTestCaseGraySobel(t) 51 | sobel, _ := SobelGray(gray, padding.BorderReflect) 52 | tearDownTestCaseSobel(t, sobel, "../res/edge/sobelGray.png") 53 | } 54 | 55 | func Test_Acceptance_HorizontalSobelRGBA(t *testing.T) { 56 | rgba := setupTestCaseRGBASobel(t) 57 | sobel, _ := HorizontalSobelRGBA(rgba, padding.BorderReflect) 58 | tearDownTestCaseSobel(t, sobel, "../res/edge/horizontalSobelRGBA.png") 59 | } 60 | 61 | func Test_Acceptance_VerticalSobelRGBA(t *testing.T) { 62 | rgba := setupTestCaseRGBASobel(t) 63 | sobel, _ := VerticalSobelRGBA(rgba, padding.BorderReflect) 64 | tearDownTestCaseSobel(t, sobel, "../res/edge/verticalSobelRGBA.png") 65 | } 66 | 67 | func Test_Acceptance_SobelRGBA(t *testing.T) { 68 | rgba := setupTestCaseRGBASobel(t) 69 | sobel, _ := SobelRGBA(rgba, padding.BorderReflect) 70 | tearDownTestCaseSobel(t, sobel, "../res/edge/sobelRGBA.png") 71 | } 72 | 73 | // --------------------------------------------------------------------------------- 74 | -------------------------------------------------------------------------------- /effects/effects.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/blend" 6 | "github.com/ernyoke/imger/convolution" 7 | "github.com/ernyoke/imger/grayscale" 8 | "github.com/ernyoke/imger/padding" 9 | "github.com/ernyoke/imger/resize" 10 | "github.com/ernyoke/imger/utils" 11 | "image" 12 | "image/color" 13 | ) 14 | 15 | var sharpenKernel = convolution.Kernel{Content: [][]float64{ 16 | {0, -1, 0}, 17 | {-1, 5, -1}, 18 | {0, -1, 0}, 19 | }, Width: 3, Height: 3} 20 | 21 | // PixelateGray enlarges the pixels of a grayscale image. The factor value specifies how much should be the pixels 22 | // enlarged. 23 | // Example of usage: 24 | // 25 | // res, err := effects.PixelateGray(img, 5.0) 26 | // 27 | func PixelateGray(img *image.Gray, factor float64) (*image.Gray, error) { 28 | if factor < 1.0 { 29 | return nil, errors.New("invalid factor, should be greater then 1.0") 30 | } 31 | fdown := 1.0 / factor 32 | downScaled, downscaleError := resize.ResizeGray(img, fdown, fdown, resize.InterNearest) 33 | if downscaleError != nil { 34 | return nil, downscaleError 35 | } 36 | upscaled, upscaleError := resize.ResizeGray(downScaled, factor, factor, resize.InterNearest) 37 | if upscaleError != nil { 38 | return nil, upscaleError 39 | } 40 | return upscaled, nil 41 | } 42 | 43 | // PixelateRGBA enlarges the pixels of a RGBA image. The factor value specifies how much should be the pixels enlarged. 44 | // Example of usage: 45 | // 46 | // res, err := effects.PixelateRGBA(img, 5.0) 47 | // 48 | func PixelateRGBA(img *image.RGBA, factor float64) (*image.RGBA, error) { 49 | if factor < 1.0 { 50 | return nil, errors.New("invalid factor, should be greater then 1.0") 51 | } 52 | fdown := 1.0 / factor 53 | downScaled, downscaleError := resize.ResizeRGBA(img, fdown, fdown, resize.InterNearest) 54 | if downscaleError != nil { 55 | return nil, downscaleError 56 | } 57 | upscaled, upscaleError := resize.ResizeRGBA(downScaled, factor, factor, resize.InterNearest) 58 | if upscaleError != nil { 59 | return nil, upscaleError 60 | } 61 | return upscaled, nil 62 | } 63 | 64 | // Sepia applies Sepia tone to an RGBA image. 65 | func Sepia(img *image.RGBA) *image.RGBA { 66 | res := image.NewRGBA(img.Rect) 67 | utils.ParallelForEachPixel(img.Bounds().Size(), func(x, y int) { 68 | pixel := img.RGBAAt(x, y) 69 | r := float64(pixel.R) 70 | g := float64(pixel.G) 71 | b := float64(pixel.B) 72 | 73 | resR := r*0.393 + g*0.769 + b*0.189 74 | resG := r*0.349 + g*0.686 + b*0.168 75 | resB := r*0.272 + g*0.534 + b*0.131 76 | resPixel := color.RGBA{R: uint8(utils.ClampF64(resR, utils.MinUint8, float64(utils.MaxUint8))), 77 | G: uint8(utils.ClampF64(resG, utils.MinUint8, float64(utils.MaxUint8))), 78 | B: uint8(utils.ClampF64(resB, utils.MinUint8, float64(utils.MaxUint8))), A: pixel.A} 79 | 80 | res.SetRGBA(x, y, resPixel) 81 | }) 82 | return res 83 | } 84 | 85 | // EmbossGray takes a grayscale image and returns a copy of the image in which each pixel has been replaced either by a 86 | // highlight or a shadow representation. 87 | func EmbossGray(img *image.Gray) (*image.Gray, error) { 88 | var kernel = convolution.Kernel{Content: [][]float64{ 89 | {-1, -1, 0}, 90 | {-1, 0, 1}, 91 | {0, 1, 1}, 92 | }, Width: 3, Height: 3} 93 | 94 | conv, err := convolution.ConvolveGray(img, &kernel, image.Point{X: 1, Y: 1}, padding.BorderReflect) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return blend.AddScalarToGray(conv, 128), nil 99 | } 100 | 101 | // EmbossRGBA takes an RGBA image and returns a grayscale image in which each pixel has been replaced either by a 102 | // highlight or a shadow representation. 103 | func EmbossRGBA(img *image.RGBA) (*image.Gray, error) { 104 | gray := grayscale.Grayscale(img) 105 | return EmbossGray(gray) 106 | } 107 | 108 | // SharpenGray takes a grayscale image and returns another grayscale image where each edge is added to the original 109 | // image. 110 | func SharpenGray(img *image.Gray) (*image.Gray, error) { 111 | return convolution.ConvolveGray(img, &sharpenKernel, image.Point{X: 1, Y: 1}, padding.BorderReflect) 112 | } 113 | 114 | // SharpenRGBA takes an RGBA image and returns another RGBA image where each edge is added to the original image. 115 | func SharpenRGBA(img *image.RGBA) (*image.RGBA, error) { 116 | return convolution.ConvolveRGBA(img, &sharpenKernel, image.Point{X: 1, Y: 1}, padding.BorderReflect) 117 | } 118 | 119 | // InvertGray takes a grayscale image and return its inverted grayscale image. 120 | func InvertGray(img *image.Gray) *image.Gray { 121 | size := img.Bounds().Size() 122 | inverted := image.NewGray(img.Rect) 123 | utils.ParallelForEachPixel(size, func(x, y int) { 124 | original := img.GrayAt(x, y).Y 125 | inverted.SetGray(x, y, color.Gray{Y: utils.MaxUint8 - original}) 126 | }) 127 | return inverted 128 | } 129 | 130 | // InvertRGBA takes an RGBA image and return its inverted RGBA image. 131 | func InvertRGBA(img *image.RGBA) *image.RGBA { 132 | size := img.Bounds().Size() 133 | inverted := image.NewRGBA(img.Rect) 134 | utils.ParallelForEachPixel(size, func(x, y int) { 135 | originalColor := img.RGBAAt(x, y) 136 | invertedColor := color.RGBA{R: utils.MaxUint8 - originalColor.R, 137 | G: utils.MaxUint8 - originalColor.G, 138 | B: utils.MaxUint8 - originalColor.B, 139 | A: originalColor.A} 140 | inverted.SetRGBA(x, y, invertedColor) 141 | }) 142 | return inverted 143 | } 144 | -------------------------------------------------------------------------------- /effects/effects_test.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "testing" 8 | ) 9 | 10 | // --------------------------------Unit tests--------------------------------------- 11 | func Test_Sepia(t *testing.T) { 12 | rgba := image.RGBA{ 13 | Rect: image.Rect(0, 0, 3, 1), 14 | Stride: 4, 15 | Pix: []uint8{ 16 | 0x01, 0x01, 0x01, 0xFF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x01, 0x01, 0xFF, 17 | }, 18 | } 19 | expected := image.RGBA{ 20 | Rect: image.Rect(0, 0, 3, 1), 21 | Stride: 4, 22 | Pix: []uint8{ 23 | 0x01, 0x01, 0x00, 0xFF, 0x01, 0x01, 0x00, 0xFF, 0x01, 0x01, 0x00, 0xFF, 24 | }, 25 | } 26 | actual := Sepia(&rgba) 27 | utils.CompareRGBAImages(t, &expected, actual) 28 | } 29 | 30 | func Test_InvertedGray(t *testing.T) { 31 | gray := image.Gray{ 32 | Rect: image.Rect(0, 0, 4, 1), 33 | Stride: 4, 34 | Pix: []uint8{ 35 | 0x00, 0xFF, 0x80, 0xAB, 36 | }, 37 | } 38 | expected := image.Gray{ 39 | Rect: image.Rect(0, 0, 4, 1), 40 | Stride: 4, 41 | Pix: []uint8{ 42 | 0xFF, 0x00, 0x7F, 0x54, 43 | }, 44 | } 45 | actual := InvertGray(&gray) 46 | utils.CompareGrayImages(t, &expected, actual) 47 | } 48 | 49 | func Test_InvertedRGBA(t *testing.T) { 50 | rgba := image.RGBA{ 51 | Rect: image.Rect(0, 0, 3, 1), 52 | Stride: 4, 53 | Pix: []uint8{ 54 | 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7F, 0xAB, 0xFF, 55 | }, 56 | } 57 | expected := image.RGBA{ 58 | Rect: image.Rect(0, 0, 3, 1), 59 | Stride: 4, 60 | Pix: []uint8{ 61 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x80, 0x54, 0xFF, 62 | }, 63 | } 64 | actual := InvertRGBA(&rgba) 65 | utils.CompareRGBAImages(t, &expected, actual) 66 | } 67 | 68 | // -----------------------------Acceptance tests------------------------------------ 69 | func setupTestCaseGray(t *testing.T) *image.Gray { 70 | path := "../res/girl.jpg" 71 | img, err := imgio.ImreadGray(path) 72 | if err != nil { 73 | t.Errorf("Could not read image from path: %s", path) 74 | } 75 | return img 76 | } 77 | 78 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 79 | path := "../res/girl.jpg" 80 | img, err := imgio.ImreadRGBA(path) 81 | if err != nil { 82 | t.Errorf("Could not read image from path: %s", path) 83 | } 84 | return img 85 | } 86 | 87 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 88 | err := imgio.Imwrite(img, path) 89 | if err != nil { 90 | t.Errorf("Could not write image to path: %s", path) 91 | } 92 | } 93 | 94 | func Test_Acceptance_PixelateGray(t *testing.T) { 95 | rgba := setupTestCaseGray(t) 96 | sepia, err := PixelateGray(rgba, 5) 97 | if err != nil { 98 | t.Fatalf("Should not reach this point!") 99 | } 100 | tearDownTestCase(t, sepia, "../res/effects/pixelateGray.jpg") 101 | } 102 | 103 | func Test_Acceptance_PixelateRGBA(t *testing.T) { 104 | rgba := setupTestCaseRGBA(t) 105 | sepia, err := PixelateRGBA(rgba, 5) 106 | if err != nil { 107 | t.Fatalf("Should not reach this point!") 108 | } 109 | tearDownTestCase(t, sepia, "../res/effects/pixelateRGBA.jpg") 110 | } 111 | 112 | func Test_Acceptance_Sepia(t *testing.T) { 113 | rgba := setupTestCaseRGBA(t) 114 | sepia := Sepia(rgba) 115 | tearDownTestCase(t, sepia, "../res/effects/sepia.jpg") 116 | } 117 | 118 | func Test_Acceptance_EmbossGray(t *testing.T) { 119 | gray := setupTestCaseGray(t) 120 | emboss, err := EmbossGray(gray) 121 | if err != nil { 122 | t.Fatalf("Should not reach this point!") 123 | } 124 | tearDownTestCase(t, emboss, "../res/effects/embossGray.jpg") 125 | } 126 | 127 | func Test_Acceptance_EmbossRGBA(t *testing.T) { 128 | rgba := setupTestCaseRGBA(t) 129 | emboss, err := EmbossRGBA(rgba) 130 | if err != nil { 131 | t.Fatalf("Should not reach this point!") 132 | } 133 | tearDownTestCase(t, emboss, "../res/effects/embossRGBA.jpg") 134 | } 135 | 136 | func Test_Acceptance_SharpenGray(t *testing.T) { 137 | gray := setupTestCaseGray(t) 138 | sharp, err := SharpenGray(gray) 139 | if err != nil { 140 | t.Fatalf("Should not reach this point!") 141 | } 142 | tearDownTestCase(t, sharp, "../res/effects/sharpenGray.jpg") 143 | } 144 | 145 | func Test_Acceptance_SharpenRGBA(t *testing.T) { 146 | rgba := setupTestCaseRGBA(t) 147 | sharp, err := SharpenRGBA(rgba) 148 | if err != nil { 149 | t.Fatalf("Should not reach this point!") 150 | } 151 | tearDownTestCase(t, sharp, "../res/effects/sharpenRGBA.jpg") 152 | } 153 | 154 | func Test_Acceptance_InvertGray(t *testing.T) { 155 | gray := setupTestCaseGray(t) 156 | inerted := InvertGray(gray) 157 | tearDownTestCase(t, inerted, "../res/effects/invertedGray.jpg") 158 | } 159 | 160 | func Test_Acceptance_InvertedRGBA(t *testing.T) { 161 | rgba := setupTestCaseRGBA(t) 162 | inverted := InvertRGBA(rgba) 163 | tearDownTestCase(t, inverted, "../res/effects/invertedRGBA.jpg") 164 | } 165 | -------------------------------------------------------------------------------- /generate/generate.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "math" 7 | ) 8 | 9 | // Direction constant - direction of the gradient from first color to the second color. 10 | type Direction int 11 | 12 | const ( 13 | // H - horizontal direction 14 | H Direction = iota 15 | // V - vertical direction 16 | V 17 | ) 18 | 19 | func normalize(value float64, min float64, max float64) float64 { 20 | lower := -6.0 21 | upper := 6.0 22 | norm := (value - min) / (max - min) 23 | return norm*(upper-lower) + lower 24 | } 25 | 26 | // LinearGradient generates a gradient image using a linear function. 27 | func LinearGradient(size image.Point, startColor color.RGBA, endColor color.RGBA, direction Direction) *image.RGBA { 28 | gradFunc := func(colorChannel uint8, percent float64) uint8 { 29 | return uint8(math.Floor(float64(colorChannel) * percent)) 30 | } 31 | res := image.NewRGBA(image.Rect(0, 0, size.X, size.Y)) 32 | switch direction { 33 | case V: 34 | step := 1.0 / float64(size.Y) 35 | percent := 0.0 36 | for y := 0; y < size.Y; y++ { 37 | c := color.RGBA{ 38 | R: gradFunc(startColor.R, 1.0-percent) + gradFunc(endColor.R, percent), 39 | G: gradFunc(startColor.G, 1.0-percent) + gradFunc(endColor.G, percent), 40 | B: gradFunc(startColor.B, 1.0-percent) + gradFunc(endColor.B, percent), 41 | A: 255, 42 | } 43 | percent += step 44 | for x := 0; x < size.X; x++ { 45 | res.SetRGBA(x, y, c) 46 | } 47 | } 48 | case H: 49 | step := 1.0 / float64(size.X) 50 | percent := 0.0 51 | for x := 0; x < size.X; x++ { 52 | c := color.RGBA{ 53 | R: gradFunc(startColor.R, -percent) + gradFunc(endColor.R, percent), 54 | G: gradFunc(startColor.G, -percent) + gradFunc(endColor.G, percent), 55 | B: gradFunc(startColor.B, -percent) + gradFunc(endColor.B, percent), 56 | A: 255, 57 | } 58 | percent += step 59 | for y := 0; y < size.Y; y++ { 60 | res.SetRGBA(x, y, c) 61 | } 62 | } 63 | } 64 | return res 65 | } 66 | 67 | // SigmoidalGradient generates a gradient image using the sigmoid ( f(x) = 1 / (1 + exp(-x)) ) function. 68 | func SigmoidalGradient(size image.Point, startColor color.RGBA, endColor color.RGBA, direction Direction) *image.RGBA { 69 | sigmoid := func(val float64) float64 { 70 | return 1.0 / (1.0 + math.Exp(-val)) 71 | } 72 | res := image.NewRGBA(image.Rect(0, 0, size.X, size.Y)) 73 | switch direction { 74 | case V: 75 | for y := 0; y < size.Y; y++ { 76 | percent := sigmoid(normalize(float64(y), 0, float64(size.Y))) 77 | c := color.RGBA{ 78 | R: uint8((1.0-percent)*float64(startColor.R)) + uint8(percent*float64(endColor.R)), 79 | G: uint8((1.0-percent)*float64(startColor.G)) + uint8(percent*float64(endColor.G)), 80 | B: uint8((1.0-percent)*float64(startColor.B)) + uint8(percent*float64(endColor.B)), 81 | A: 255, 82 | } 83 | for x := 0; x < size.X; x++ { 84 | res.SetRGBA(x, y, c) 85 | } 86 | } 87 | case H: 88 | for x := 0; x < size.X; x++ { 89 | percent := sigmoid(normalize(float64(x), 0, float64(size.X))) 90 | c := color.RGBA{ 91 | R: uint8((1.0-percent)*float64(startColor.R)) + uint8(percent*float64(endColor.R)), 92 | G: uint8((1.0-percent)*float64(startColor.G)) + uint8(percent*float64(endColor.G)), 93 | B: uint8((1.0-percent)*float64(startColor.B)) + uint8(percent*float64(endColor.B)), 94 | A: 255, 95 | } 96 | for y := 0; y < size.Y; y++ { 97 | res.SetRGBA(x, y, c) 98 | } 99 | } 100 | } 101 | return res 102 | } 103 | -------------------------------------------------------------------------------- /generate/generate_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "image" 6 | "image/color" 7 | "testing" 8 | ) 9 | 10 | // -----------------------------Acceptance tests------------------------------------ 11 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 12 | path := "../res/girl.jpg" 13 | img, err := imgio.ImreadRGBA(path) 14 | if err != nil { 15 | t.Errorf("Could not read image from path: %s", path) 16 | } 17 | return img 18 | } 19 | 20 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 21 | err := imgio.Imwrite(img, path) 22 | if err != nil { 23 | t.Errorf("Could not write image to path: %s", path) 24 | } 25 | } 26 | 27 | func Test_Acceptance_LinearGradientHorizontal(t *testing.T) { 28 | res := LinearGradient(image.Point{X: 500, Y: 200}, color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, H) 29 | tearDownTestCase(t, res, "../res/generate/linearGradientHorizontal.jpg") 30 | } 31 | 32 | func Test_Acceptance_LinearGradientVertical(t *testing.T) { 33 | res := LinearGradient(image.Point{X: 500, Y: 200}, color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, V) 34 | tearDownTestCase(t, res, "../res/generate/linearGradientVertical.jpg") 35 | } 36 | 37 | func Test_Acceptance_SigmoidalHorizontal(t *testing.T) { 38 | res := SigmoidalGradient(image.Point{X: 500, Y: 200}, color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, H) 39 | tearDownTestCase(t, res, "../res/generate/sigmoidalGradientHorizontal.jpg") 40 | } 41 | 42 | func Test_Acceptance_SigmoidalGradientVertical(t *testing.T) { 43 | res := SigmoidalGradient(image.Point{X: 500, Y: 200}, color.RGBA{R: 0, G: 0, B: 0, A: 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255}, V) 44 | tearDownTestCase(t, res, "../res/generate/sigmoidalGradientVertical.jpg") 45 | } 46 | 47 | // --------------------------------------------------------------------------------- 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ernyoke/imger 2 | 3 | go 1.18 4 | 5 | retract [v0.0.1, v0.9.9] -------------------------------------------------------------------------------- /grayscale/grayscale.go: -------------------------------------------------------------------------------- 1 | package grayscale 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/ernyoke/imger/utils" 8 | ) 9 | 10 | // Grayscale takes an image on any type and returns the equivalent grayscale image represented on 8 bits. 11 | func Grayscale(img image.Image) *image.Gray { 12 | gray := image.NewGray(img.Bounds()) 13 | size := img.Bounds().Size() 14 | offset := img.Bounds().Min 15 | utils.ParallelForEachPixel(size, func(x, y int) { 16 | gray.Set(x+offset.X, y+offset.Y, color.GrayModel.Convert(img.At(x+offset.X, y+offset.Y))) 17 | }) 18 | return gray 19 | } 20 | 21 | // Grayscale16 takes an image on any type and returns the equivalent grayscale image represented on 16 bits. 22 | func Grayscale16(img image.Image) *image.Gray16 { 23 | gray := image.NewGray16(img.Bounds()) 24 | size := img.Bounds().Size() 25 | offset := img.Bounds().Min 26 | utils.ParallelForEachPixel(size, func(x, y int) { 27 | gray.Set(x+offset.X, y+offset.Y, color.Gray16Model.Convert(img.At(x+offset.X, y+offset.Y))) 28 | }) 29 | return gray 30 | } 31 | -------------------------------------------------------------------------------- /grayscale/grayscale_test.go: -------------------------------------------------------------------------------- 1 | package grayscale 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/ernyoke/imger/imgio" 8 | ) 9 | 10 | // -----------------------------Acceptance tests------------------------------------ 11 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 12 | path := "../res/girl.jpg" 13 | img, err := imgio.ImreadRGBA(path) 14 | if err != nil { 15 | t.Errorf("Could not read image from path: %s", path) 16 | } 17 | return img 18 | } 19 | 20 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 21 | err := imgio.Imwrite(img, path) 22 | if err != nil { 23 | t.Errorf("Could not write image to path: %s", path) 24 | } 25 | } 26 | 27 | func Test_Acceptance_GrayScale(t *testing.T) { 28 | rgba := setupTestCaseRGBA(t) 29 | gray := Grayscale(rgba) 30 | tearDownTestCase(t, gray, "../res/grayscale/gray.jpg") 31 | } 32 | 33 | func Test_Acceptance_GrayScaleCropped(t *testing.T) { 34 | rgba := setupTestCaseRGBA(t) 35 | cropped := rgba.SubImage(image.Rect(100, 100, rgba.Bounds().Max.X-100, rgba.Bounds().Max.Y-100)) 36 | gray := Grayscale(cropped) 37 | tearDownTestCase(t, gray, "../res/grayscale/cropped_gray.jpg") 38 | } 39 | 40 | func Test_Acceptance_GrayScale16(t *testing.T) { 41 | rgba := setupTestCaseRGBA(t) 42 | gray := Grayscale16(rgba) 43 | tearDownTestCase(t, gray, "../res/grayscale/gray16.jpg") 44 | } 45 | 46 | func Test_Acceptance_GrayScale16Cropped(t *testing.T) { 47 | rgba := setupTestCaseRGBA(t) 48 | cropped := rgba.SubImage(image.Rect(100, 100, rgba.Bounds().Max.X-100, rgba.Bounds().Max.Y-100)) 49 | gray := Grayscale16(cropped) 50 | tearDownTestCase(t, gray, "../res/grayscale/cropped_gray16.jpg") 51 | } 52 | 53 | // --------------------------------------------------------------------------------- 54 | -------------------------------------------------------------------------------- /histogram/histogram.go: -------------------------------------------------------------------------------- 1 | package histogram 2 | 3 | import ( 4 | "github.com/ernyoke/imger/utils" 5 | "image" 6 | "image/color" 7 | ) 8 | 9 | const hsize = 256 10 | const channels = 3 11 | 12 | // HistogramGray computes the histogram for a grayscale image. Returns an array of 256 uint64 values containing 13 | // distribution of the pixel values. 14 | func HistogramGray(img *image.Gray) [hsize]uint64 { 15 | var res [hsize]uint64 16 | utils.ForEachGrayPixel(img, func(pixel color.Gray) { 17 | res[pixel.Y]++ 18 | }) 19 | return res 20 | } 21 | 22 | // HistogramRGBARed computes the histogram for red channel from an RGBA image. Returns an array of 256 uint64 values 23 | // containing distribution of the pixel values. 24 | func HistogramRGBARed(img *image.RGBA) [hsize]uint64 { 25 | var res [hsize]uint64 26 | utils.ForEachRGBARedPixel(img, func(red uint8) { 27 | res[red]++ 28 | }) 29 | return res 30 | } 31 | 32 | // HistogramRGBAGreen computes the histogram for green channel from an RGBA image. Returns an array of 256 uint64 values 33 | // containing distribution of the pixel values. 34 | func HistogramRGBAGreen(img *image.RGBA) [hsize]uint64 { 35 | var res [hsize]uint64 36 | utils.ForEachRGBAGreenPixel(img, func(green uint8) { 37 | res[green]++ 38 | }) 39 | return res 40 | } 41 | 42 | // HistogramRGBABlue computes the histogram for blue channel from an RGBA image. Returns an array of 256 uint64 values 43 | // containing distribution of the pixel values. 44 | func HistogramRGBABlue(img *image.RGBA) [hsize]uint64 { 45 | var res [hsize]uint64 46 | utils.ForEachRGBABluePixel(img, func(blue uint8) { 47 | res[blue]++ 48 | }) 49 | return res 50 | } 51 | 52 | // HistogramRGBA computes the histogram for a RGBA image. Returns an 2D (shape: [3][256]) array of uint64 values 53 | // containing distribution of color values from each RGBA channel. 54 | func HistogramRGBA(img *image.RGBA) [channels][hsize]uint64 { 55 | var res [channels][hsize]uint64 56 | utils.ForEachRGBAPixel(img, func(pixel color.RGBA) { 57 | res[0][pixel.R]++ 58 | res[1][pixel.G]++ 59 | res[2][pixel.B]++ 60 | }) 61 | return res 62 | } 63 | 64 | // DrawHistogramGray computes and draws the histogram of a grayscale image. The size of the image is 256*scale width 65 | // and 256*scale height. 66 | func DrawHistogramGray(img *image.Gray, size image.Point) *image.Gray { 67 | h := HistogramGray(img) 68 | normHist := normalizeHistogram(h, uint64(size.Y)) 69 | res := image.NewGray(image.Rect(0, 0, size.X, size.Y)) 70 | drawerFunc(size, func(i int) uint64 { 71 | return normHist[i] 72 | }, func(x, y int) { 73 | res.SetGray(x, y, color.Gray{Y: utils.MaxUint8}) 74 | }) 75 | return res 76 | } 77 | 78 | // DrawHistogramRGBA computes and draws the histogram of a RGBA image. The size of the image is 256*scale width and 79 | // 256*scale height. 80 | func DrawHistogramRGBA(img *image.RGBA, size image.Point) *image.RGBA { 81 | h := HistogramRGBA(img) 82 | normRHist := normalizeHistogram(h[0], uint64(size.Y)) 83 | normGHist := normalizeHistogram(h[1], uint64(size.Y)) 84 | normBHist := normalizeHistogram(h[2], uint64(size.Y)) 85 | res := image.NewRGBA(image.Rect(0, 0, size.X, size.Y)) 86 | drawerFunc(size, func(i int) uint64 { 87 | return normRHist[i] 88 | }, func(x, y int) { 89 | pix := res.RGBAAt(x, y) 90 | pix.R = utils.MaxUint8 91 | res.SetRGBA(x, y, pix) 92 | }) 93 | drawerFunc(size, func(i int) uint64 { 94 | return normGHist[i] 95 | }, func(x, y int) { 96 | pix := res.RGBAAt(x, y) 97 | pix.G = utils.MaxUint8 98 | res.SetRGBA(x, y, pix) 99 | }) 100 | drawerFunc(size, func(i int) uint64 { 101 | return normBHist[i] 102 | }, func(x, y int) { 103 | pix := res.RGBAAt(x, y) 104 | pix.B = utils.MaxUint8 105 | res.SetRGBA(x, y, pix) 106 | }) 107 | return res 108 | } 109 | 110 | //--------------------------------------------------------------------------------------------- 111 | func drawerFunc(size image.Point, getNormAt func(i int) uint64, setPixel func(x, y int)) { 112 | scaleX := float64(size.X) / float64(hsize) 113 | for i := 0; i < hsize; i++ { 114 | for width := int(float64(i) * scaleX); width < int((float64(i)+1.0)*scaleX); width++ { 115 | for height := size.Y; height >= size.Y-int(getNormAt(i)); height-- { 116 | setPixel(width, height) 117 | } 118 | } 119 | } 120 | } 121 | 122 | func normalizeHistogram(v [hsize]uint64, maxHeight uint64) [hsize]uint64 { 123 | max := utils.GetMax(v[:]) 124 | var norm [hsize]uint64 125 | for i := 0; i < len(v); i++ { 126 | norm[i] = v[i] * maxHeight / max 127 | } 128 | return norm 129 | } 130 | -------------------------------------------------------------------------------- /histogram/histogram_test.go: -------------------------------------------------------------------------------- 1 | package histogram 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | // --------------------------------Unit tests--------------------------------------- 10 | 11 | func Test_Histogram_GrayScale(t *testing.T) { 12 | gray := image.Gray{ 13 | Rect: image.Rect(0, 0, 3, 3), 14 | Stride: 3, 15 | Pix: []uint8{ 16 | 0x01, 0x02, 0x80, 0x80, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 17 | }, 18 | } 19 | hist := HistogramGray(&gray) 20 | for i, h := range hist { 21 | switch i { 22 | case 0x01: 23 | expected := uint64(1) 24 | if h != expected { 25 | t.Errorf("Histogram value for %d should be %d!", h, expected) 26 | } 27 | case 0x02: 28 | expected := uint64(2) 29 | if h != expected { 30 | t.Errorf("Histogram value for %d should be %d!", h, expected) 31 | } 32 | case 0x80: 33 | expected := uint64(2) 34 | if h != expected { 35 | t.Errorf("Histogram value for %d should be %d!", h, expected) 36 | } 37 | case 0xFF: 38 | expected := uint64(4) 39 | if h != expected { 40 | t.Errorf("Histogram value for %d should be %d!", h, expected) 41 | } 42 | default: 43 | expected := uint64(0) 44 | if h != expected { 45 | t.Errorf("Histogram value for %d should be %d!", h, expected) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func Test_Histogram_RGBA(t *testing.T) { 52 | rgba := image.RGBA{ 53 | Rect: image.Rect(0, 0, 3, 3), 54 | Stride: 3 * 4, 55 | Pix: []uint8{ 56 | 0x01, 0x01, 0x01, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0x80, 0x80, 0x80, 0xFF, 57 | 0x80, 0x80, 0x80, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 58 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 59 | }, 60 | } 61 | hist := HistogramRGBA(&rgba) 62 | for _, hc := range hist { 63 | for i, h := range hc { 64 | switch i { 65 | case 0x01: 66 | expected := uint64(1) 67 | if h != expected { 68 | t.Errorf("Histogram value for %d should be %d!", h, expected) 69 | } 70 | case 0x02: 71 | expected := uint64(2) 72 | if h != expected { 73 | t.Errorf("Histogram value for %d should be %d!", h, expected) 74 | } 75 | case 0x80: 76 | expected := uint64(2) 77 | if h != expected { 78 | t.Errorf("Histogram value for %d should be %d!", h, expected) 79 | } 80 | case 0xFF: 81 | expected := uint64(4) 82 | if h != expected { 83 | t.Errorf("Histogram value for %d should be %d!", h, expected) 84 | } 85 | default: 86 | expected := uint64(0) 87 | if h != expected { 88 | t.Errorf("Histogram value for %d should be %d!", h, expected) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | func Test_Histogram_RGBARed(t *testing.T) { 96 | rgba := image.RGBA{ 97 | Rect: image.Rect(0, 0, 3, 3), 98 | Stride: 3 * 4, 99 | Pix: []uint8{ 100 | 0x01, 0x01, 0x01, 0xFF, 0x02, 0x66, 0x06, 0xFF, 0x80, 0x80, 0x80, 0xFF, 101 | 0x80, 0x80, 0x80, 0xFF, 0x02, 0x12, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 102 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 103 | }, 104 | } 105 | hist := HistogramRGBARed(&rgba) 106 | for i, h := range hist { 107 | switch i { 108 | case 0x01: 109 | expected := uint64(1) 110 | if h != expected { 111 | t.Errorf("Histogram value for %d should be %d!", h, expected) 112 | } 113 | case 0x02: 114 | expected := uint64(2) 115 | if h != expected { 116 | t.Errorf("Histogram value for %d should be %d!", h, expected) 117 | } 118 | case 0x80: 119 | expected := uint64(2) 120 | if h != expected { 121 | t.Errorf("Histogram value for %d should be %d!", h, expected) 122 | } 123 | case 0xFF: 124 | expected := uint64(4) 125 | if h != expected { 126 | t.Errorf("Histogram value for %d should be %d!", h, expected) 127 | } 128 | default: 129 | expected := uint64(0) 130 | if h != expected { 131 | t.Errorf("Histogram value for %d should be %d!", h, expected) 132 | } 133 | } 134 | } 135 | } 136 | 137 | func Test_Histogram_RGBAGreen(t *testing.T) { 138 | rgba := image.RGBA{ 139 | Rect: image.Rect(0, 0, 3, 3), 140 | Stride: 3 * 4, 141 | Pix: []uint8{ 142 | 0x45, 0x01, 0x08, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0x80, 0x80, 0x80, 0xFF, 143 | 0x56, 0x80, 0x80, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 144 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 145 | }, 146 | } 147 | hist := HistogramRGBAGreen(&rgba) 148 | for i, h := range hist { 149 | switch i { 150 | case 0x01: 151 | expected := uint64(1) 152 | if h != expected { 153 | t.Errorf("Histogram value for %d should be %d!", h, expected) 154 | } 155 | case 0x02: 156 | expected := uint64(2) 157 | if h != expected { 158 | t.Errorf("Histogram value for %d should be %d!", h, expected) 159 | } 160 | case 0x80: 161 | expected := uint64(2) 162 | if h != expected { 163 | t.Errorf("Histogram value for %d should be %d!", h, expected) 164 | } 165 | case 0xFF: 166 | expected := uint64(4) 167 | if h != expected { 168 | t.Errorf("Histogram value for %d should be %d!", h, expected) 169 | } 170 | default: 171 | expected := uint64(0) 172 | if h != expected { 173 | t.Errorf("Histogram value for %d should be %d!", h, expected) 174 | } 175 | } 176 | } 177 | } 178 | 179 | func Test_Histogram_RGBABlue(t *testing.T) { 180 | rgba := image.RGBA{ 181 | Rect: image.Rect(0, 0, 3, 3), 182 | Stride: 3 * 4, 183 | Pix: []uint8{ 184 | 0x01, 0x11, 0x01, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0x80, 0x80, 0x80, 0xFF, 185 | 0x80, 0x81, 0x80, 0xFF, 0x02, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 186 | 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 187 | }, 188 | } 189 | hist := HistogramRGBABlue(&rgba) 190 | for i, h := range hist { 191 | switch i { 192 | case 0x01: 193 | expected := uint64(1) 194 | if h != expected { 195 | t.Errorf("Histogram value for %d should be %d!", h, expected) 196 | } 197 | case 0x02: 198 | expected := uint64(2) 199 | if h != expected { 200 | t.Errorf("Histogram value for %d should be %d!", h, expected) 201 | } 202 | case 0x80: 203 | expected := uint64(2) 204 | if h != expected { 205 | t.Errorf("Histogram value for %d should be %d!", h, expected) 206 | } 207 | case 0xFF: 208 | expected := uint64(4) 209 | if h != expected { 210 | t.Errorf("Histogram value for %d should be %d!", h, expected) 211 | } 212 | default: 213 | expected := uint64(0) 214 | if h != expected { 215 | t.Errorf("Histogram value for %d should be %d!", h, expected) 216 | } 217 | } 218 | } 219 | } 220 | 221 | // -----------------------------Acceptance tests------------------------------------ 222 | func setupTestCaseGray(t *testing.T) *image.Gray { 223 | path := "../res/girl.jpg" 224 | img, err := imgio.ImreadGray(path) 225 | if err != nil { 226 | t.Errorf("Could not read image from path: %s", path) 227 | } 228 | return img 229 | } 230 | 231 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 232 | path := "../res/girl.jpg" 233 | img, err := imgio.ImreadRGBA(path) 234 | if err != nil { 235 | t.Errorf("Could not read image from path: %s", path) 236 | } 237 | return img 238 | } 239 | 240 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 241 | err := imgio.Imwrite(img, path) 242 | if err != nil { 243 | t.Errorf("Could not write image to path: %s", path) 244 | } 245 | } 246 | 247 | func Test_Acceptance_DrawHistogram_GrayScale(t *testing.T) { 248 | gray := setupTestCaseGray(t) 249 | expectedSize := image.Point{X: 512, Y: 600} 250 | hist := DrawHistogramGray(gray, expectedSize) 251 | actualSize := hist.Bounds().Size() 252 | if actualSize.X != expectedSize.X && actualSize.Y != expectedSize.Y { 253 | t.Fatalf("Size of expected [%d %d] does not match size of actual [%d %d]", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 254 | } 255 | tearDownTestCase(t, hist, "../res/histogram/gray.jpg") 256 | } 257 | 258 | func Test_Acceptance_DrawHistogram_RGBA(t *testing.T) { 259 | rgba := setupTestCaseRGBA(t) 260 | expectedSize := image.Point{X: 512, Y: 600} 261 | hist := DrawHistogramRGBA(rgba, expectedSize) 262 | actualSize := hist.Bounds().Size() 263 | if actualSize.X != expectedSize.X && actualSize.Y != expectedSize.Y { 264 | t.Fatalf("Size of expected [%d %d] does not match size of actual [%d %d]", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 265 | } 266 | tearDownTestCase(t, hist, "../res/histogram/rgba.jpg") 267 | } 268 | -------------------------------------------------------------------------------- /imgio/io.go: -------------------------------------------------------------------------------- 1 | package imgio 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "image/draw" 7 | "image/jpeg" 8 | "image/png" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | // Reads and decodes image from a given path. Supported extensions are: jpg, jpeg, png 14 | func decode(path string) (image.Image, error) { 15 | file, err := os.Open(path) 16 | defer file.Close() 17 | if err != nil { 18 | return nil, err 19 | } 20 | extension := filepath.Ext(path) 21 | switch extension { 22 | case ".jpg": 23 | fallthrough 24 | case ".jpeg": 25 | return jpeg.Decode(file) 26 | case ".png": 27 | return png.Decode(file) 28 | } 29 | return nil, errors.New("unsupported extension") 30 | } 31 | 32 | // Encodes and writes image to the given path 33 | func encode(img image.Image, path string) error { 34 | file, err := os.Create(path) 35 | if err != nil { 36 | return err 37 | } 38 | defer file.Close() 39 | extension := filepath.Ext(path) 40 | switch extension { 41 | case ".jpg": 42 | fallthrough 43 | case ".jpeg": 44 | return jpeg.Encode(file, img, nil) 45 | case ".png": 46 | return png.Encode(file, img) 47 | } 48 | return errors.New("unsupported extension") 49 | } 50 | 51 | // ImreadGray reads the image from the given path and return a grayscale image. Returns an error if the path is not 52 | // readable or the specified resource does not exist. 53 | func ImreadGray(path string) (*image.Gray, error) { 54 | img, err := decode(path) 55 | if err != nil { 56 | return nil, err 57 | } 58 | bounds := img.Bounds() 59 | gray := image.NewGray(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 60 | draw.Draw(gray, bounds, img, bounds.Min, draw.Src) 61 | return gray, nil 62 | } 63 | 64 | // ImreadGray16 reads the image from the given path and return a grayscale16 image. Returns an error if the path is not 65 | // readable or the specified resource does not exist. 66 | func ImreadGray16(path string) (*image.Gray16, error) { 67 | img, err := decode(path) 68 | if err != nil { 69 | return nil, err 70 | } 71 | bounds := img.Bounds() 72 | gray16 := image.NewGray16(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 73 | draw.Draw(gray16, bounds, img, bounds.Min, draw.Src) 74 | return gray16, nil 75 | } 76 | 77 | // ImreadRGBA reads the image from the given path and return a RGBA image. Returns an error if the path is not readable 78 | // or the specified resource does not exist. 79 | func ImreadRGBA(path string) (*image.RGBA, error) { 80 | img, err := decode(path) 81 | if err != nil { 82 | return nil, err 83 | } 84 | bounds := img.Bounds() 85 | rgba := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 86 | draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) 87 | return rgba, nil 88 | } 89 | 90 | // ImreadRGBA64 reads the image from the given path and return a RGBA64 image. 91 | // Returns an error if the path is not readable or the specified resource does not exist. 92 | func ImreadRGBA64(path string) (*image.RGBA64, error) { 93 | img, err := decode(path) 94 | if err != nil { 95 | return nil, err 96 | } 97 | bounds := img.Bounds() 98 | rgba64 := image.NewRGBA64(image.Rect(0, 0, bounds.Dx(), bounds.Dy())) 99 | draw.Draw(rgba64, bounds, img, bounds.Min, draw.Src) 100 | return rgba64, nil 101 | } 102 | 103 | // Imwrite saves the image under the location specified by the "path" string. Returns an error if the location is 104 | // not writable. 105 | func Imwrite(img image.Image, path string) error { 106 | return encode(img, path) 107 | } 108 | -------------------------------------------------------------------------------- /imgio/io_test.go: -------------------------------------------------------------------------------- 1 | package imgio 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | // -----------------------------Acceptance tests------------------------------------ 9 | func Test_ImreadGray(t *testing.T) { 10 | path := "../res/girl.jpg" 11 | img, err := ImreadGray(path) 12 | if err != nil { 13 | t.Fatal("Could not read file!") 14 | } 15 | expectedSize := image.Point{X: 403, Y: 403} 16 | actualSize := img.Bounds().Size() 17 | if !(expectedSize.X == actualSize.X && expectedSize.Y == actualSize.Y) { 18 | t.Errorf("Expected size [%d %d] does not equal actual size [%d %d]", expectedSize.X, expectedSize.Y, 19 | actualSize.X, actualSize.Y) 20 | } 21 | } 22 | 23 | func Test_ImreadGray16(t *testing.T) { 24 | path := "../res/girl.jpg" 25 | img, err := ImreadGray16(path) 26 | if err != nil { 27 | t.Fatal("Could not read file!") 28 | } 29 | expectedSize := image.Point{X: 403, Y: 403} 30 | actualSize := img.Bounds().Size() 31 | if !(expectedSize.X == actualSize.X && expectedSize.Y == actualSize.Y) { 32 | t.Errorf("Expected size [%d %d] does not equal actual size [%d %d]", expectedSize.X, expectedSize.Y, 33 | actualSize.X, actualSize.Y) 34 | } 35 | } 36 | 37 | func Test_ImreadRGBA(t *testing.T) { 38 | path := "../res/girl.jpg" 39 | img, err := ImreadRGBA(path) 40 | if err != nil { 41 | t.Fatal("Could not read file!") 42 | } 43 | expectedSize := image.Point{X: 403, Y: 403} 44 | actualSize := img.Bounds().Size() 45 | if !(expectedSize.X == actualSize.X && expectedSize.Y == actualSize.Y) { 46 | t.Errorf("Expected size [%d %d] does not equal actual size [%d %d]", expectedSize.X, expectedSize.Y, 47 | actualSize.X, actualSize.Y) 48 | } 49 | } 50 | 51 | func Test_ImreadRGBA64(t *testing.T) { 52 | path := "../res/girl.jpg" 53 | img, err := ImreadRGBA64(path) 54 | if err != nil { 55 | t.Fatal("Could not read file!") 56 | } 57 | expectedSize := image.Point{X: 403, Y: 403} 58 | actualSize := img.Bounds().Size() 59 | if !(expectedSize.X == actualSize.X && expectedSize.Y == actualSize.Y) { 60 | t.Errorf("Expected size [%d %d] does not equal actual size [%d %d]", expectedSize.X, expectedSize.Y, 61 | actualSize.X, actualSize.Y) 62 | } 63 | } 64 | 65 | func Test_Imread_InexistentFile(t *testing.T) { 66 | path := "../res/inexistent.jpg" 67 | _, err := ImreadRGBA64(path) 68 | if err != nil { 69 | // ok 70 | return 71 | } 72 | t.Fatal("Should not reach this point!") 73 | } 74 | 75 | func Test_ImwriteJPG(t *testing.T) { 76 | path := "../res/girl.jpg" 77 | img, err := ImreadRGBA(path) 78 | if err != nil { 79 | t.Fatal("Could not read file!") 80 | } 81 | outPath := "../res/io/outputJPG.jpg" 82 | errOut := Imwrite(img, outPath) 83 | if errOut != nil { 84 | t.Fatalf("Could not write to this location: %s! Error: %s", outPath, errOut) 85 | } 86 | } 87 | 88 | func Test_ImwritePNG(t *testing.T) { 89 | path := "../res/girl.jpg" 90 | img, err := ImreadRGBA(path) 91 | if err != nil { 92 | t.Fatal("Could not read file!") 93 | } 94 | outPath := "../res/io/outputPNG.png" 95 | errOut := Imwrite(img, outPath) 96 | if errOut != nil { 97 | t.Fatalf("Could not write to this location: %s! Error: %s", outPath, errOut) 98 | } 99 | } 100 | 101 | func Test_Imwrite_InvalidExtension(t *testing.T) { 102 | path := "../res/girl.jpg" 103 | img, err := ImreadRGBA(path) 104 | if err != nil { 105 | t.Fatal("Could not read file!") 106 | } 107 | outPath := "../res/io/invalid.xxx" 108 | errOut := Imwrite(img, outPath) 109 | if errOut != nil { 110 | // ok 111 | return 112 | } 113 | t.Fatal("Should not reach this point!") 114 | } 115 | -------------------------------------------------------------------------------- /padding/padding.go: -------------------------------------------------------------------------------- 1 | package padding 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "image/color" 7 | ) 8 | 9 | // Border is an enum type for supported padding types 10 | type Border int 11 | 12 | const ( 13 | // BorderConstant - xxxabcdefghxxx - where x is a black ( color.Gray{0} ) pixel 14 | BorderConstant Border = iota 15 | // BorderReplicate - aaaabcdefghhhh - replicates the nearest pixel 16 | BorderReplicate 17 | // BorderReflect - cbabcdefgfed - reflects the nearest pixel group 18 | BorderReflect 19 | ) 20 | 21 | // Paddings struct holds the padding sizes for each padding 22 | type Paddings struct { 23 | // PaddingLeft is the size of the left padding 24 | PaddingLeft int 25 | // PaddingRight is the size of the right padding 26 | PaddingRight int 27 | // PaddingTop is the size of the top padding 28 | PaddingTop int 29 | // PaddingBottom is the size of the bottom padding 30 | PaddingBottom int 31 | } 32 | 33 | func topPaddingReplicate(img image.Image, p Paddings, setPixel func(int, int, color.Color)) { 34 | originalSize := img.Bounds().Size() 35 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 36 | firstPixel := img.At(x-p.PaddingLeft, p.PaddingTop) 37 | for y := 0; y < p.PaddingTop; y++ { 38 | setPixel(x, y, firstPixel) 39 | } 40 | } 41 | } 42 | 43 | func bottomPaddingReplicate(img image.Image, p Paddings, setPixel func(int, int, color.Color)) { 44 | originalSize := img.Bounds().Size() 45 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 46 | lastPixel := img.At(x-p.PaddingLeft, originalSize.Y-1) 47 | for y := p.PaddingTop + originalSize.Y; y < originalSize.Y+p.PaddingTop+p.PaddingBottom; y++ { 48 | setPixel(x, y, lastPixel) 49 | } 50 | } 51 | } 52 | 53 | func leftPaddingReplicate(img image.Image, padded image.Image, p Paddings, setPixel func(int, int, color.Color)) { 54 | originalSize := img.Bounds().Size() 55 | for y := 0; y < originalSize.Y+p.PaddingBottom+p.PaddingTop; y++ { 56 | firstPixel := padded.At(p.PaddingLeft, y) 57 | for x := 0; x < p.PaddingLeft; x++ { 58 | setPixel(x, y, firstPixel) 59 | } 60 | } 61 | } 62 | 63 | func rightPaddingReplicate(img image.Image, padded image.Image, p Paddings, setPixel func(int, int, color.Color)) { 64 | originalSize := img.Bounds().Size() 65 | for y := 0; y < originalSize.Y+p.PaddingBottom+p.PaddingTop; y++ { 66 | lastPixel := padded.At(originalSize.X+p.PaddingLeft-1, y) 67 | for x := originalSize.X + p.PaddingLeft; x < originalSize.X+p.PaddingLeft+p.PaddingRight; x++ { 68 | setPixel(x, y, lastPixel) 69 | } 70 | } 71 | } 72 | 73 | func topPaddingReflect(img image.Image, p Paddings, setPixel func(int, int, color.Color)) { 74 | originalSize := img.Bounds().Size() 75 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 76 | for y := 0; y < p.PaddingTop; y++ { 77 | pixel := img.At(x-p.PaddingLeft, p.PaddingTop-y) 78 | setPixel(x, y, pixel) 79 | } 80 | } 81 | } 82 | 83 | func bottomPaddingReflect(img image.Image, p Paddings, setPixel func(int, int, color.Color)) { 84 | originalSize := img.Bounds().Size() 85 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 86 | for y := p.PaddingTop + originalSize.Y; y < originalSize.Y+p.PaddingTop+p.PaddingBottom; y++ { 87 | pixel := img.At(x-p.PaddingLeft, originalSize.Y-(y-p.PaddingTop-originalSize.Y)-2) 88 | setPixel(x, y, pixel) 89 | } 90 | } 91 | } 92 | 93 | func leftPaddingReflect(img image.Image, padded image.Image, p Paddings, setPixel func(int, int, color.Color)) { 94 | originalSize := img.Bounds().Size() 95 | for y := 0; y < originalSize.Y+p.PaddingBottom+p.PaddingTop; y++ { 96 | for x := 0; x < p.PaddingLeft; x++ { 97 | pixel := padded.At(2*p.PaddingLeft-x, y) 98 | setPixel(x, y, pixel) 99 | } 100 | } 101 | } 102 | 103 | func rightPaddingReflect(img image.Image, padded image.Image, p Paddings, setPixel func(int, int, color.Color)) { 104 | originalSize := img.Bounds().Size() 105 | for y := 0; y < originalSize.Y+p.PaddingBottom+p.PaddingTop; y++ { 106 | for x := originalSize.X + p.PaddingLeft; x < originalSize.X+p.PaddingLeft+p.PaddingRight; x++ { 107 | pixel := padded.At(originalSize.X+p.PaddingLeft-(x-originalSize.X-p.PaddingLeft)-2, y) 108 | setPixel(x, y, pixel) 109 | } 110 | } 111 | } 112 | 113 | // PaddingGray appends padding to a given grayscale image. The size of the padding is calculated from the kernel size 114 | // and the anchor point. Supported border types are: BorderConstant, BorderReplicate, BorderReflect. 115 | // Example of usage: 116 | // 117 | // res, err := padding.PaddingGray(img, {5, 5}, {1, 1}, BorderReflect) 118 | // 119 | // Note: this will add a 1px padding for the top and left borders of the image and a 3px padding fot the bottom and 120 | // right borders of the image. 121 | func PaddingGray(img *image.Gray, kernelSize image.Point, anchor image.Point, border Border) (*image.Gray, error) { 122 | originalSize := img.Bounds().Size() 123 | p, error := calculatePaddings(kernelSize, anchor) 124 | if error != nil { 125 | return nil, error 126 | } 127 | rect := getRectangleFromPaddings(p, originalSize) 128 | padded := image.NewGray(rect) 129 | 130 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 131 | for y := p.PaddingTop; y < originalSize.Y+p.PaddingTop; y++ { 132 | padded.Set(x, y, img.GrayAt(x-p.PaddingLeft, y-p.PaddingTop)) 133 | } 134 | } 135 | 136 | switch border { 137 | case BorderConstant: 138 | // do nothing 139 | case BorderReplicate: 140 | topPaddingReplicate(img, p, func(x int, y int, pixel color.Color) { 141 | padded.Set(x, y, pixel) 142 | }) 143 | bottomPaddingReplicate(img, p, func(x int, y int, pixel color.Color) { 144 | padded.Set(x, y, pixel) 145 | }) 146 | leftPaddingReplicate(img, padded, p, func(x int, y int, pixel color.Color) { 147 | padded.Set(x, y, pixel) 148 | }) 149 | rightPaddingReplicate(img, padded, p, func(x int, y int, pixel color.Color) { 150 | padded.Set(x, y, pixel) 151 | }) 152 | case BorderReflect: 153 | topPaddingReflect(img, p, func(x int, y int, pixel color.Color) { 154 | padded.Set(x, y, pixel) 155 | }) 156 | bottomPaddingReflect(img, p, func(x int, y int, pixel color.Color) { 157 | padded.Set(x, y, pixel) 158 | }) 159 | leftPaddingReflect(img, padded, p, func(x int, y int, pixel color.Color) { 160 | padded.Set(x, y, pixel) 161 | }) 162 | rightPaddingReflect(img, padded, p, func(x int, y int, pixel color.Color) { 163 | padded.Set(x, y, pixel) 164 | }) 165 | default: 166 | return nil, errors.New("unknown border type") 167 | } 168 | return padded, nil 169 | } 170 | 171 | // PaddingRGBA appends padding to a given RGBA image. The size of the padding is calculated from the kernel size 172 | // and the anchor point. Supported border types are: BorderConstant, BorderReplicate, BorderReflect. 173 | // Example of usage: 174 | // 175 | // res, err := padding.PaddingRGBA(img, {5, 5}, {1, 1}, BorderReflect) 176 | // 177 | // Note: this will add a 1px padding for the top and left borders of the image and a 3px padding fot the bottom and 178 | // right borders of the image. 179 | func PaddingRGBA(img *image.RGBA, kernelSize image.Point, anchor image.Point, border Border) (*image.RGBA, error) { 180 | originalSize := img.Bounds().Size() 181 | p, error := calculatePaddings(kernelSize, anchor) 182 | if error != nil { 183 | return nil, error 184 | } 185 | rect := getRectangleFromPaddings(p, originalSize) 186 | padded := image.NewRGBA(rect) 187 | 188 | for x := p.PaddingLeft; x < originalSize.X+p.PaddingLeft; x++ { 189 | for y := p.PaddingTop; y < originalSize.Y+p.PaddingTop; y++ { 190 | padded.Set(x, y, img.RGBAAt(x-p.PaddingLeft, y-p.PaddingTop)) 191 | } 192 | } 193 | 194 | switch border { 195 | case BorderConstant: 196 | // do nothing 197 | case BorderReplicate: 198 | topPaddingReplicate(img, p, func(x int, y int, pixel color.Color) { 199 | padded.Set(x, y, pixel) 200 | }) 201 | bottomPaddingReplicate(img, p, func(x int, y int, pixel color.Color) { 202 | padded.Set(x, y, pixel) 203 | }) 204 | leftPaddingReplicate(img, padded, p, func(x int, y int, pixel color.Color) { 205 | padded.Set(x, y, pixel) 206 | }) 207 | rightPaddingReplicate(img, padded, p, func(x int, y int, pixel color.Color) { 208 | padded.Set(x, y, pixel) 209 | }) 210 | case BorderReflect: 211 | topPaddingReflect(img, p, func(x int, y int, pixel color.Color) { 212 | padded.Set(x, y, pixel) 213 | }) 214 | bottomPaddingReflect(img, p, func(x int, y int, pixel color.Color) { 215 | padded.Set(x, y, pixel) 216 | }) 217 | leftPaddingReflect(img, padded, p, func(x int, y int, pixel color.Color) { 218 | padded.Set(x, y, pixel) 219 | }) 220 | rightPaddingReflect(img, padded, p, func(x int, y int, pixel color.Color) { 221 | padded.Set(x, y, pixel) 222 | }) 223 | default: 224 | return nil, errors.New("unknown border type") 225 | } 226 | return padded, nil 227 | } 228 | 229 | // ------------------------------------------------------------------------------------------------------- 230 | func calculatePaddings(kernelSize image.Point, anchor image.Point) (Paddings, error) { 231 | var p Paddings 232 | if kernelSize.X < 0 || kernelSize.Y < 0 { 233 | return p, errors.New("negative size") 234 | } 235 | if anchor.X < 0 || anchor.Y < 0 { 236 | return p, errors.New("negative anchor value") 237 | } 238 | if anchor.X > kernelSize.X || anchor.Y > kernelSize.Y { 239 | return p, errors.New("anc" + "hor value outside of the kernel") 240 | } 241 | 242 | p = Paddings{PaddingLeft: anchor.X, PaddingRight: kernelSize.X - anchor.X - 1, PaddingTop: anchor.Y, PaddingBottom: kernelSize.Y - anchor.Y - 1} 243 | 244 | return p, nil 245 | } 246 | 247 | func getRectangleFromPaddings(p Paddings, imgSize image.Point) image.Rectangle { 248 | x := p.PaddingLeft + p.PaddingRight + imgSize.X 249 | y := p.PaddingTop + p.PaddingBottom + imgSize.Y 250 | return image.Rect(0, 0, x, y) 251 | } 252 | -------------------------------------------------------------------------------- /padding/padding_test.go: -------------------------------------------------------------------------------- 1 | package padding 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "testing" 8 | ) 9 | 10 | // ---------------------------------Unit tests-------------------------------------- 11 | func Test_GrayPaddingBorderConstant_1pxPadding(t *testing.T) { 12 | gray := image.Gray{ 13 | Rect: image.Rect(0, 0, 5, 3), 14 | Stride: 5, 15 | Pix: []uint8{ 16 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 17 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 18 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 19 | }, 20 | } 21 | expected := image.Gray{ 22 | Rect: image.Rect(0, 0, 7, 5), 23 | Stride: 7, 24 | Pix: []uint8{ 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 27 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 28 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | }, 31 | } 32 | paddingSize := image.Point{X: 3, Y: 3} 33 | anchor := image.Point{X: 1, Y: 1} 34 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderConstant) 35 | utils.CompareGrayImages(t, &expected, actual) 36 | } 37 | 38 | func Test_GrayPaddingBorderConstant_2pxPadding(t *testing.T) { 39 | gray := image.Gray{ 40 | Rect: image.Rect(0, 0, 5, 3), 41 | Stride: 5, 42 | Pix: []uint8{ 43 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 44 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 45 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 46 | }, 47 | } 48 | expected := image.Gray{ 49 | Rect: image.Rect(0, 0, 9, 7), 50 | Stride: 9, 51 | Pix: []uint8{ 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 55 | 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 56 | 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | }, 60 | } 61 | paddingSize := image.Point{X: 5, Y: 5} 62 | anchor := image.Point{X: 2, Y: 2} 63 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderConstant) 64 | //utils.PrintGray(t, actual) 65 | utils.CompareGrayImages(t, &expected, actual) 66 | } 67 | 68 | func Test_GrayPaddingBorderConstant_1_3pxPadding(t *testing.T) { 69 | gray := image.Gray{ 70 | Rect: image.Rect(0, 0, 5, 3), 71 | Stride: 5, 72 | Pix: []uint8{ 73 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 74 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 75 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 76 | }, 77 | } 78 | expected := image.Gray{ 79 | Rect: image.Rect(0, 0, 9, 7), 80 | Stride: 9, 81 | Pix: []uint8{ 82 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 0x00, 84 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 0x00, 85 | 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | }, 90 | } 91 | paddingSize := image.Point{X: 5, Y: 5} 92 | anchor := image.Point{X: 1, Y: 1} 93 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderConstant) 94 | //utils.PrintGray(t, actual) 95 | utils.CompareGrayImages(t, &expected, actual) 96 | } 97 | 98 | func Test_GrayPaddingBorderReplicate_1pxPadding(t *testing.T) { 99 | gray := image.Gray{ 100 | Rect: image.Rect(0, 0, 5, 3), 101 | Stride: 5, 102 | Pix: []uint8{ 103 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 104 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 105 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 106 | }, 107 | } 108 | expected := image.Gray{ 109 | Rect: image.Rect(0, 0, 7, 5), 110 | Stride: 7, 111 | Pix: []uint8{ 112 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 113 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 114 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 115 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 116 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 117 | }, 118 | } 119 | paddingSize := image.Point{X: 3, Y: 3} 120 | anchor := image.Point{X: 1, Y: 1} 121 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderReplicate) 122 | utils.CompareGrayImages(t, &expected, actual) 123 | } 124 | 125 | func Test_GrayPaddingBorderReplicate_2pxPadding(t *testing.T) { 126 | gray := image.Gray{ 127 | Rect: image.Rect(0, 0, 5, 3), 128 | Stride: 5, 129 | Pix: []uint8{ 130 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 131 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 132 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 133 | }, 134 | } 135 | expected := image.Gray{ 136 | Rect: image.Rect(0, 0, 9, 7), 137 | Stride: 9, 138 | Pix: []uint8{ 139 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 140 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 141 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 142 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 143 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 144 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 145 | 0xAA, 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 146 | }, 147 | } 148 | paddingSize := image.Point{X: 5, Y: 5} 149 | anchor := image.Point{X: 2, Y: 2} 150 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderReplicate) 151 | //utils.PrintGray(t, actual) 152 | utils.CompareGrayImages(t, &expected, actual) 153 | } 154 | 155 | func Test_GrayPaddingBorderReplicate_1_3pxPadding(t *testing.T) { 156 | gray := image.Gray{ 157 | Rect: image.Rect(0, 0, 5, 3), 158 | Stride: 5, 159 | Pix: []uint8{ 160 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 161 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 162 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 163 | }, 164 | } 165 | expected := image.Gray{ 166 | Rect: image.Rect(0, 0, 9, 7), 167 | Stride: 9, 168 | Pix: []uint8{ 169 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 170 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 171 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 172 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 173 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 174 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 175 | 0xAA, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 176 | }, 177 | } 178 | paddingSize := image.Point{X: 5, Y: 5} 179 | anchor := image.Point{X: 1, Y: 1} 180 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderReplicate) 181 | //utils.PrintGray(t, actual) 182 | utils.CompareGrayImages(t, &expected, actual) 183 | } 184 | 185 | func Test_GrayPaddingBorderReflect_1_3pxPadding(t *testing.T) { 186 | gray := image.Gray{ 187 | Rect: image.Rect(0, 0, 5, 3), 188 | Stride: 5, 189 | Pix: []uint8{ 190 | 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 191 | 0x11, 0xBB, 0xCC, 0xDD, 0xEE, 192 | 0x22, 0xBB, 0xCC, 0xDD, 0xEE, 193 | }, 194 | } 195 | expected := image.Gray{ 196 | Rect: image.Rect(0, 0, 9, 6), 197 | Stride: 9, 198 | Pix: []uint8{ 199 | 0xBB, 0x11, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 200 | 0xBB, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 201 | 0xBB, 0x11, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 202 | 0xBB, 0x22, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 203 | 0xBB, 0x11, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 204 | 0xBB, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xDD, 0xCC, 0xBB, 205 | }, 206 | } 207 | paddingSize := image.Point{X: 5, Y: 4} 208 | anchor := image.Point{X: 1, Y: 1} 209 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderReflect) 210 | //utils.PrintGray(t, actual) 211 | utils.CompareGrayImages(t, &expected, actual) 212 | } 213 | 214 | func Test_GrayPaddingBorderReflect_2pxPadding(t *testing.T) { 215 | gray := image.Gray{ 216 | Rect: image.Rect(0, 0, 4, 4), 217 | Stride: 4, 218 | Pix: []uint8{ 219 | 0xAA, 0xBB, 0xCC, 0xDD, 220 | 0x11, 0x11, 0x11, 0x11, 221 | 0x22, 0x22, 0x22, 0x22, 222 | 0x33, 0x33, 0x33, 0x33, 223 | }, 224 | } 225 | expected := image.Gray{ 226 | Rect: image.Rect(0, 0, 8, 8), 227 | Stride: 8, 228 | Pix: []uint8{ 229 | 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 230 | 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 231 | 0xCC, 0xBB, 0xAA, 0xBB, 0xCC, 0xDD, 0xCC, 0xBB, 232 | 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 233 | 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 234 | 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 235 | 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 236 | 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 237 | }, 238 | } 239 | paddingSize := image.Point{X: 5, Y: 5} 240 | anchor := image.Point{X: 2, Y: 2} 241 | actual, _ := PaddingGray(&gray, paddingSize, anchor, BorderReflect) 242 | //utils.PrintGray(t, actual) 243 | utils.CompareGrayImages(t, &expected, actual) 244 | } 245 | 246 | // --------------------------------------------------------------------------------- 247 | 248 | // -----------------------------Acceptance tests------------------------------------ 249 | func setupTestCaseGray(t *testing.T) *image.Gray { 250 | path := "../res/girl.jpg" 251 | img, err := imgio.ImreadGray(path) 252 | if err != nil { 253 | t.Errorf("Could not read image from path: %s", path) 254 | } 255 | return img 256 | } 257 | 258 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 259 | path := "../res/girl.jpg" 260 | img, err := imgio.ImreadRGBA(path) 261 | if err != nil { 262 | t.Errorf("Could not read image from path: %s", path) 263 | } 264 | return img 265 | } 266 | 267 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 268 | err := imgio.Imwrite(img, path) 269 | if err != nil { 270 | t.Errorf("Could not write image to path: %s", path) 271 | } 272 | } 273 | 274 | func Test_Acceptance_GrayPaddingBorderConstant(t *testing.T) { 275 | gray := setupTestCaseGray(t) 276 | padded, _ := PaddingGray(gray, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderConstant) 277 | tearDownTestCase(t, padded, "../res/padding/grayPaddingBorderConstant.jpg") 278 | } 279 | 280 | func Test_Acceptance_GrayPaddingBorderConstantDistortedAnchor(t *testing.T) { 281 | gray := setupTestCaseGray(t) 282 | padded, _ := PaddingGray(gray, image.Point{X: 50, Y: 50}, image.Point{X: 8, Y: 8}, BorderConstant) 283 | tearDownTestCase(t, padded, "../res/padding/grayPaddingBorderConstantDistortedAnchor.jpg") 284 | } 285 | 286 | func Test_Acceptance_GrayPaddingBorderReplicate(t *testing.T) { 287 | gray := setupTestCaseGray(t) 288 | padded, _ := PaddingGray(gray, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderReplicate) 289 | tearDownTestCase(t, padded, "../res/padding/grayPaddingBorderReplicate.jpg") 290 | } 291 | 292 | func Test_Acceptance_GrayPaddingBorderReflect(t *testing.T) { 293 | gray := setupTestCaseGray(t) 294 | padded, _ := PaddingGray(gray, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderReflect) 295 | tearDownTestCase(t, padded, "../res/padding/grayPaddingBorderReflect.jpg") 296 | } 297 | 298 | func Test_Acceptance_RGBAPaddingBorderConstant(t *testing.T) { 299 | rgba := setupTestCaseRGBA(t) 300 | padded, _ := PaddingRGBA(rgba, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderConstant) 301 | tearDownTestCase(t, padded, "../res/padding/rgbaPaddedBorderConstant.jpg") 302 | } 303 | 304 | func Test_Acceptance_RGBAPaddingBorderReplicate(t *testing.T) { 305 | rgba := setupTestCaseRGBA(t) 306 | padded, _ := PaddingRGBA(rgba, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderReplicate) 307 | tearDownTestCase(t, padded, "../res/padding/rgbaPaddedBorderReplicate.jpg") 308 | } 309 | 310 | func Test_Acceptance_RGBAPaddingBorderReflect(t *testing.T) { 311 | rgba := setupTestCaseRGBA(t) 312 | padded, _ := PaddingRGBA(rgba, image.Point{X: 15, Y: 15}, image.Point{X: 8, Y: 8}, BorderReflect) 313 | tearDownTestCase(t, padded, "../res/padding/rgbaPaddedBorderReflect.jpg") 314 | } 315 | 316 | // --------------------------------------------------------------------------------- 317 | -------------------------------------------------------------------------------- /res/blur/grayBlur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/blur/grayBlur.jpg -------------------------------------------------------------------------------- /res/blur/grayGaussianBlur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/blur/grayGaussianBlur.jpg -------------------------------------------------------------------------------- /res/blur/rgbaBlur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/blur/rgbaBlur.jpg -------------------------------------------------------------------------------- /res/blur/rgbaGaussianBlur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/blur/rgbaGaussianBlur.jpg -------------------------------------------------------------------------------- /res/building.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/building.jpg -------------------------------------------------------------------------------- /res/edge/cannygray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/cannygray.jpg -------------------------------------------------------------------------------- /res/edge/cannyrgba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/cannyrgba.jpg -------------------------------------------------------------------------------- /res/edge/horizontalSobelGray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/horizontalSobelGray.png -------------------------------------------------------------------------------- /res/edge/horizontalSobelRGBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/horizontalSobelRGBA.png -------------------------------------------------------------------------------- /res/edge/laplacianGrayK4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/laplacianGrayK4.png -------------------------------------------------------------------------------- /res/edge/laplacianGrayK8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/laplacianGrayK8.png -------------------------------------------------------------------------------- /res/edge/laplacianRGBAK4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/laplacianRGBAK4.png -------------------------------------------------------------------------------- /res/edge/laplacianRGBAK8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/laplacianRGBAK8.png -------------------------------------------------------------------------------- /res/edge/sobelGray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/sobelGray.png -------------------------------------------------------------------------------- /res/edge/sobelRGBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/sobelRGBA.png -------------------------------------------------------------------------------- /res/edge/verticalSobelGray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/verticalSobelGray.png -------------------------------------------------------------------------------- /res/edge/verticalSobelRGBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/edge/verticalSobelRGBA.png -------------------------------------------------------------------------------- /res/effects/embossGray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/embossGray.jpg -------------------------------------------------------------------------------- /res/effects/embossRGBA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/embossRGBA.jpg -------------------------------------------------------------------------------- /res/effects/invertedGray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/invertedGray.jpg -------------------------------------------------------------------------------- /res/effects/invertedRGBA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/invertedRGBA.jpg -------------------------------------------------------------------------------- /res/effects/pixelateGray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/pixelateGray.jpg -------------------------------------------------------------------------------- /res/effects/pixelateRGBA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/pixelateRGBA.jpg -------------------------------------------------------------------------------- /res/effects/sepia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/sepia.jpg -------------------------------------------------------------------------------- /res/effects/sharpenGray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/sharpenGray.jpg -------------------------------------------------------------------------------- /res/effects/sharpenRGBA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/effects/sharpenRGBA.jpg -------------------------------------------------------------------------------- /res/engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/engine.png -------------------------------------------------------------------------------- /res/generate/linearGradientHorizontal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/generate/linearGradientHorizontal.jpg -------------------------------------------------------------------------------- /res/generate/linearGradientVertical.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/generate/linearGradientVertical.jpg -------------------------------------------------------------------------------- /res/generate/sigmoidalGradientHorizontal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/generate/sigmoidalGradientHorizontal.jpg -------------------------------------------------------------------------------- /res/generate/sigmoidalGradientVertical.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/generate/sigmoidalGradientVertical.jpg -------------------------------------------------------------------------------- /res/girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/girl.jpg -------------------------------------------------------------------------------- /res/grayscale/cropped_gray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/grayscale/cropped_gray.jpg -------------------------------------------------------------------------------- /res/grayscale/cropped_gray16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/grayscale/cropped_gray16.jpg -------------------------------------------------------------------------------- /res/grayscale/gray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/grayscale/gray.jpg -------------------------------------------------------------------------------- /res/grayscale/gray16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/grayscale/gray16.jpg -------------------------------------------------------------------------------- /res/histogram/gray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/histogram/gray.jpg -------------------------------------------------------------------------------- /res/histogram/rgba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/histogram/rgba.jpg -------------------------------------------------------------------------------- /res/io/outputJPG.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/io/outputJPG.jpg -------------------------------------------------------------------------------- /res/io/outputPNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/io/outputPNG.png -------------------------------------------------------------------------------- /res/padding/grayPaddingBorderConstant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/grayPaddingBorderConstant.jpg -------------------------------------------------------------------------------- /res/padding/grayPaddingBorderConstantDistortedAnchor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/grayPaddingBorderConstantDistortedAnchor.jpg -------------------------------------------------------------------------------- /res/padding/grayPaddingBorderReflect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/grayPaddingBorderReflect.jpg -------------------------------------------------------------------------------- /res/padding/grayPaddingBorderReplicate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/grayPaddingBorderReplicate.jpg -------------------------------------------------------------------------------- /res/padding/rgbaPaddedBorderConstant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/rgbaPaddedBorderConstant.jpg -------------------------------------------------------------------------------- /res/padding/rgbaPaddedBorderReflect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/rgbaPaddedBorderReflect.jpg -------------------------------------------------------------------------------- /res/padding/rgbaPaddedBorderReplicate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/padding/rgbaPaddedBorderReplicate.jpg -------------------------------------------------------------------------------- /res/resize/grayResize0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize0_5x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize1_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize1_5x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize2_5_and_3_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize2_5_and_3_5x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize2x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize_CatmullRom_0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize_CatmullRom_0_5x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize_CatmullRom_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize_CatmullRom_2x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize_Lanczos_0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize_Lanczos_0_5x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize_Lanczos_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize_Lanczos_2x.jpg -------------------------------------------------------------------------------- /res/resize/grayResize_Linear_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/grayResize_Linear_2x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize0_5x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize1_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize1_5x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize2_5_and_3_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize2_5_and_3_5x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize2x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize_CatmullRom_0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize_CatmullRom_0_5x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize_CatmullRom_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize_CatmullRom_2x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize_Lanczos_0_5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize_Lanczos_0_5x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize_Lanczos_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize_Lanczos_2x.jpg -------------------------------------------------------------------------------- /res/resize/rgbaResize_Linear_2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/resize/rgbaResize_Linear_2x.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshBin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshBin.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshBinCropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshBinCropped.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshBinInvCropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshBinInvCropped.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshToZeroCropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshToZeroCropped.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshToZeroInvCropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshToZeroInvCropped.jpg -------------------------------------------------------------------------------- /res/threshold/otsuThreshTruncCropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/otsuThreshTruncCropped.jpg -------------------------------------------------------------------------------- /res/threshold/thresh16Bin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/thresh16Bin.jpg -------------------------------------------------------------------------------- /res/threshold/thresh16BinInv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/thresh16BinInv.jpg -------------------------------------------------------------------------------- /res/threshold/thresh16ToZero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/thresh16ToZero.jpg -------------------------------------------------------------------------------- /res/threshold/thresh16ToZeroInv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/thresh16ToZeroInv.jpg -------------------------------------------------------------------------------- /res/threshold/thresh16Trunc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/thresh16Trunc.jpg -------------------------------------------------------------------------------- /res/threshold/threshBin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/threshBin.jpg -------------------------------------------------------------------------------- /res/threshold/threshBinInv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/threshBinInv.jpg -------------------------------------------------------------------------------- /res/threshold/threshToZero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/threshToZero.jpg -------------------------------------------------------------------------------- /res/threshold/threshTrunc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/threshold/threshTrunc.jpg -------------------------------------------------------------------------------- /res/transform/roateGray22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateGray22.jpg -------------------------------------------------------------------------------- /res/transform/roateGray45.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateGray45.jpg -------------------------------------------------------------------------------- /res/transform/roateGray90.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateGray90.jpg -------------------------------------------------------------------------------- /res/transform/roateRGBA22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateRGBA22.jpg -------------------------------------------------------------------------------- /res/transform/roateRGBA45.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateRGBA45.jpg -------------------------------------------------------------------------------- /res/transform/roateRGBA45Anchor200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateRGBA45Anchor200.jpg -------------------------------------------------------------------------------- /res/transform/roateRGBA45Noresize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateRGBA45Noresize.jpg -------------------------------------------------------------------------------- /res/transform/roateRGBA90.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ernyoke/Imger/461615a440736ba2a37d444d32b906f66225c0fa/res/transform/roateRGBA90.jpg -------------------------------------------------------------------------------- /resize/filter.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import "math" 4 | 5 | // Filter - Interface for resampling filters 6 | type Filter interface { 7 | Interpolate(float64) float64 8 | GetS() float64 9 | } 10 | 11 | // Linear - Struct for Linear filter 12 | type Linear struct{} 13 | 14 | // NewLinear creates a new Linear filter 15 | func NewLinear() *Linear { 16 | return &Linear{} 17 | } 18 | 19 | // Interpolate returns the coefficient for x value using Linear interpolation 20 | func (r *Linear) Interpolate(x float64) float64 { 21 | x = math.Abs(x) 22 | if x < 1.0 { 23 | return 1.0 - x 24 | } 25 | return 0 26 | } 27 | 28 | // GetS returns the support value for Linear filter 29 | func (r *Linear) GetS() float64 { 30 | return 1.0 31 | } 32 | 33 | // CatmullRom - Struct for Catmull-Rom filter 34 | type CatmullRom struct{} 35 | 36 | // NewCatmullRom creates a new Catmull-Rom filter 37 | func NewCatmullRom() *CatmullRom { 38 | return &CatmullRom{} 39 | } 40 | 41 | // Interpolate returns the coefficient for x value using Catmull-Rom interpolation 42 | func (r *CatmullRom) Interpolate(x float64) float64 { 43 | b := 0.0 44 | c := 0.5 45 | x = math.Abs(x) 46 | 47 | if x < 1.0 { 48 | return (6 - 2*b + (-18+12*b+6*c)*math.Pow(x, 2) + (12-9*b-6*c)*math.Pow(x, 3)) / 6 49 | } else if x <= 2.0 { 50 | return (8*b + 24*c + (-12*b-48*c)*x + (6*b+30*c)*math.Pow(x, 2) + (-b-6*c)*math.Pow(x, 3)) / 6 51 | } 52 | return 0 53 | } 54 | 55 | // GetS returns the support value for Catmull-Rom filter 56 | func (r *CatmullRom) GetS() float64 { 57 | return 2.0 58 | } 59 | 60 | // Lanczos - struct for Lanczos filter 61 | type Lanczos struct{} 62 | 63 | // NewLanczos creates a new Lanczos filter 64 | func NewLanczos() *Lanczos { 65 | return &Lanczos{} 66 | } 67 | 68 | // Interpolate returns the coefficient for x value using Lanczos interpolation 69 | func (r *Lanczos) Interpolate(x float64) float64 { 70 | x = math.Abs(x) 71 | if x > 0.0 && x < 3.0 { 72 | return (3.0 * math.Sin(math.Pi*x) * math.Sin(math.Pi*(x/3.0))) / (math.Pi * math.Pi * x * x) 73 | } 74 | return 0.0 75 | } 76 | 77 | // GetS returns the support value for Lanczos filter 78 | func (r *Lanczos) GetS() float64 { 79 | return 3.0 80 | } 81 | -------------------------------------------------------------------------------- /resize/filter_test.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import ( 4 | "github.com/ernyoke/imger/utils" 5 | "testing" 6 | ) 7 | 8 | // --------------------------------Unit tests---------------------------------------- 9 | func Test_Linear_Positive_Valid(t *testing.T) { 10 | linear := NewLinear() 11 | expected := 0.5 12 | actual := linear.Interpolate(0.5) 13 | if !utils.IsEqualFloat64(expected, actual) { 14 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 15 | } 16 | } 17 | 18 | func Test_Linear_Negative_Valid(t *testing.T) { 19 | linear := NewLinear() 20 | expected := 0.5 21 | actual := linear.Interpolate(-0.5) 22 | if !utils.IsEqualFloat64(expected, actual) { 23 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 24 | } 25 | } 26 | 27 | func Test_Linear_Positive_Invalid(t *testing.T) { 28 | linear := NewLinear() 29 | expected := 0.0 30 | actual := linear.Interpolate(1.5) 31 | if !utils.IsEqualFloat64(expected, actual) { 32 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 33 | } 34 | } 35 | 36 | func Test_Linear_Negative_Invalid(t *testing.T) { 37 | linear := NewLinear() 38 | expected := 0.0 39 | actual := linear.Interpolate(-1.5) 40 | if !utils.IsEqualFloat64(expected, actual) { 41 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 42 | } 43 | } 44 | 45 | func Test_CatmullRom_Positive_Betwen0and1(t *testing.T) { 46 | catmullRom := NewCatmullRom() 47 | expected := 0.5625 48 | actual := catmullRom.Interpolate(0.5) 49 | if !utils.IsEqualFloat64(expected, actual) { 50 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 51 | } 52 | } 53 | 54 | func Test_CatmullRom_Negative_Betwen0and1(t *testing.T) { 55 | catmullRom := NewCatmullRom() 56 | expected := 0.5625 57 | actual := catmullRom.Interpolate(-0.5) 58 | if !utils.IsEqualFloat64(expected, actual) { 59 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 60 | } 61 | } 62 | 63 | func Test_CatmullRom_Positive_Betwen1and2(t *testing.T) { 64 | catmullRom := NewCatmullRom() 65 | expected := -0.0625 66 | actual := catmullRom.Interpolate(1.5) 67 | if !utils.IsEqualFloat64(expected, actual) { 68 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 69 | } 70 | } 71 | 72 | func Test_CatmullRom_Negative_Betwen1and2(t *testing.T) { 73 | catmullRom := NewCatmullRom() 74 | expected := -0.0625 75 | actual := catmullRom.Interpolate(-1.5) 76 | if !utils.IsEqualFloat64(expected, actual) { 77 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 78 | } 79 | } 80 | 81 | func Test_CatmullRom_Positive_Invalid(t *testing.T) { 82 | linear := NewLinear() 83 | expected := 0.0 84 | actual := linear.Interpolate(2.6) 85 | if !utils.IsEqualFloat64(expected, actual) { 86 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 87 | } 88 | } 89 | 90 | func Test_CatmullRom_Negative_Invalid(t *testing.T) { 91 | linear := NewLinear() 92 | expected := 0.0 93 | actual := linear.Interpolate(-2.6) 94 | if !utils.IsEqualFloat64(expected, actual) { 95 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 96 | } 97 | } 98 | 99 | func Test_Lanczos_Positive_Valid(t *testing.T) { 100 | lanczos := NewLanczos() 101 | expected := 0.60792710185 102 | actual := lanczos.Interpolate(0.5) 103 | if !utils.IsEqualFloat64(expected, actual) { 104 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 105 | } 106 | } 107 | 108 | func Test_Lanczos_Negative_Valid(t *testing.T) { 109 | lanczos := NewLanczos() 110 | expected := 0.60792710185 111 | actual := lanczos.Interpolate(-0.5) 112 | if !utils.IsEqualFloat64(expected, actual) { 113 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 114 | } 115 | } 116 | 117 | func Test_Lanczos_Positive_Invalid(t *testing.T) { 118 | lanczos := NewLanczos() 119 | expected := 0.0 120 | actual := lanczos.Interpolate(3.5) 121 | if !utils.IsEqualFloat64(expected, actual) { 122 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 123 | } 124 | } 125 | 126 | func Test_Lanczos_Negative_Invalid(t *testing.T) { 127 | lanczos := NewLanczos() 128 | expected := 0.0 129 | actual := lanczos.Interpolate(-3.5) 130 | if !utils.IsEqualFloat64(expected, actual) { 131 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 132 | } 133 | } 134 | 135 | func Test_Lanczos_0(t *testing.T) { 136 | lanczos := NewLanczos() 137 | expected := 0.0 138 | actual := lanczos.Interpolate(0.0) 139 | if !utils.IsEqualFloat64(expected, actual) { 140 | t.Errorf("Expected %f is not equal to actual: %f\n", expected, actual) 141 | } 142 | } 143 | 144 | // ---------------------------------------------------------------------------------- 145 | -------------------------------------------------------------------------------- /resize/resize.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // Interpolation method types 12 | type Interpolation int 13 | 14 | const ( 15 | // InterNearest - takes the nearest pixel. 16 | InterNearest Interpolation = iota 17 | // InterLinear - Linear interpolation between two pixels. More info: https://en.wikipedia.org/wiki/Linear_interpolation 18 | InterLinear 19 | // InterCatmullRom - Catmull-Rom resampling. More info: https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline 20 | InterCatmullRom 21 | // InterLanczos - Lanczos resampling. More info: https://en.wikipedia.org/wiki/Lanczos_resampling 22 | InterLanczos 23 | ) 24 | 25 | func resizeNearestGray(img *image.Gray, fx float64, fy float64) (*image.Gray, error) { 26 | oldSize := img.Bounds().Size() 27 | newSize := image.Point{X: int(float64(oldSize.X) * fx), Y: int(float64(oldSize.Y) * fy)} 28 | newImg := image.NewGray(image.Rect(0, 0, newSize.X, newSize.Y)) 29 | utils.ParallelForEachPixel(newSize, func(x int, y int) { 30 | oldXTemp := float64(x) / fx 31 | var oldX int 32 | if fraction := oldXTemp - float64(int(oldXTemp)); fraction >= 0.5 { 33 | oldX = int(oldXTemp + 1) 34 | } else { 35 | oldX = int(oldXTemp) 36 | } 37 | oldYTemp := float64(y) / fy 38 | var oldY int 39 | if fraction := oldYTemp - float64(int(oldYTemp)); fraction >= 0.5 { 40 | oldY = int(oldYTemp + 1) 41 | } else { 42 | oldY = int(oldYTemp) 43 | } 44 | newImg.SetGray(x, y, img.GrayAt(oldX, oldY)) 45 | }) 46 | return newImg, nil 47 | } 48 | 49 | func resizeLinearGray(img *image.Gray, fx float64, fy float64) (*image.Gray, error) { 50 | res, err := resizeHorizontalGray(img, fx, NewLinear()) 51 | if err != nil { 52 | return nil, err 53 | } 54 | res, err = resizeVerticalGray(res, fy, NewLinear()) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return res, nil 60 | } 61 | 62 | func resizeCatmullRomGray(img *image.Gray, fx float64, fy float64) (*image.Gray, error) { 63 | res, err := resizeHorizontalGray(img, fx, NewCatmullRom()) 64 | if err != nil { 65 | return nil, err 66 | } 67 | res, err = resizeVerticalGray(res, fy, NewCatmullRom()) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return res, nil 73 | } 74 | 75 | func resizeLanczosGray(img *image.Gray, fx float64, fy float64) (*image.Gray, error) { 76 | res, err := resizeHorizontalGray(img, fx, NewLanczos()) 77 | if err != nil { 78 | return nil, err 79 | } 80 | res, err = resizeVerticalGray(res, fy, NewLanczos()) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return res, nil 86 | } 87 | 88 | func resizeHorizontalGray(img *image.Gray, fx float64, filter Filter) (*image.Gray, error) { 89 | originalSize := img.Bounds().Size() 90 | newWidth := int(float64(originalSize.X) * fx) 91 | res := image.NewGray(image.Rect(0, 0, newWidth, originalSize.Y)) 92 | dfx := 1 / fx 93 | 94 | radius := math.Ceil(fx * filter.GetS()) 95 | for y := 0; y < originalSize.Y; y++ { 96 | for x := 0; x < newWidth; x++ { 97 | ix := (float64(x)+0.5)*dfx - 0.5 98 | start := utils.ClampInt(int(ix-radius+0.5), 0, originalSize.X) 99 | end := utils.ClampInt(int(ix+radius), 0, originalSize.X) 100 | var fPix float64 101 | var sum float64 102 | for i := start; i < end; i++ { 103 | filterValue := filter.Interpolate(float64(i)-ix) / fx 104 | pix := img.GrayAt(i, y) 105 | fPix += float64(pix.Y) * filterValue 106 | sum += filterValue 107 | } 108 | res.SetGray(x, y, color.Gray{uint8(utils.ClampF64(fPix/sum+0.5, 0, 255))}) 109 | } 110 | } 111 | return res, nil 112 | } 113 | 114 | func resizeVerticalGray(img *image.Gray, fy float64, filter Filter) (*image.Gray, error) { 115 | originalSize := img.Bounds().Size() 116 | newHeight := int(float64(originalSize.Y) * fy) 117 | res := image.NewGray(image.Rect(0, 0, originalSize.X, newHeight)) 118 | dfy := 1 / fy 119 | 120 | radius := math.Ceil(fy * filter.GetS()) 121 | for y := 0; y < newHeight; y++ { 122 | iy := (float64(y)+0.5)*dfy - 0.5 123 | start := utils.ClampInt(int(iy-radius+0.5), 0, originalSize.Y) 124 | end := utils.ClampInt(int(iy+radius), 0, originalSize.Y) 125 | for x := 0; x < originalSize.X; x++ { 126 | var sum float64 127 | var fPix float64 128 | for i := start; i < end; i++ { 129 | filterValue := filter.Interpolate(float64(i)-iy) / fy 130 | pix := img.GrayAt(x, i) 131 | fPix += float64(pix.Y) * filterValue 132 | sum += filterValue 133 | } 134 | res.SetGray(x, y, color.Gray{uint8(utils.ClampF64(fPix/sum+0.5, 0, 255))}) 135 | } 136 | } 137 | return res, nil 138 | } 139 | 140 | func resizeNearestRGBA(img *image.RGBA, fx float64, fy float64) (*image.RGBA, error) { 141 | oldSize := img.Bounds().Size() 142 | newSize := image.Point{X: int(float64(oldSize.X) * fx), Y: int(float64(oldSize.Y) * fy)} 143 | newImg := image.NewRGBA(image.Rect(0, 0, newSize.X, newSize.Y)) 144 | utils.ParallelForEachPixel(newSize, func(x int, y int) { 145 | oldXTemp := float64(x) / fx 146 | var oldX int 147 | if fraction := oldXTemp - float64(int(oldXTemp)); fraction >= 0.5 { 148 | oldX = int(oldXTemp + 1) 149 | } else { 150 | oldX = int(oldXTemp) 151 | } 152 | oldYTemp := float64(y) / fy 153 | var oldY int 154 | if fraction := oldYTemp - float64(int(oldYTemp)); fraction >= 0.5 { 155 | oldY = int(oldYTemp + 1) 156 | } else { 157 | oldY = int(oldYTemp) 158 | } 159 | newImg.SetRGBA(x, y, img.RGBAAt(oldX, oldY)) 160 | }) 161 | return newImg, nil 162 | } 163 | 164 | func resizeLinearRGBA(img *image.RGBA, fx float64, fy float64) (*image.RGBA, error) { 165 | res, err := resizeHorizontalRGBA(img, fx, NewLinear()) 166 | if err != nil { 167 | return nil, err 168 | } 169 | res, err = resizeVerticalRGBA(res, fy, NewLinear()) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return res, nil 175 | } 176 | 177 | func resizeCatmullRomRGBA(img *image.RGBA, fx float64, fy float64) (*image.RGBA, error) { 178 | res, err := resizeHorizontalRGBA(img, fx, NewCatmullRom()) 179 | if err != nil { 180 | return nil, err 181 | } 182 | res, err = resizeVerticalRGBA(res, fy, NewCatmullRom()) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | return res, nil 188 | } 189 | 190 | func resizeLanczosRGBA(img *image.RGBA, fx float64, fy float64) (*image.RGBA, error) { 191 | res, err := resizeHorizontalRGBA(img, fx, NewLanczos()) 192 | if err != nil { 193 | return nil, err 194 | } 195 | res, err = resizeVerticalRGBA(res, fy, NewLanczos()) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return res, nil 201 | } 202 | 203 | func resizeHorizontalRGBA(img *image.RGBA, fx float64, filter Filter) (*image.RGBA, error) { 204 | originalSize := img.Bounds().Size() 205 | newWidth := int(float64(originalSize.X) * fx) 206 | res := image.NewRGBA(image.Rect(0, 0, newWidth, originalSize.Y)) 207 | dfx := 1 / fx 208 | 209 | radius := math.Ceil(fx * filter.GetS()) 210 | for y := 0; y < originalSize.Y; y++ { 211 | for x := 0; x < newWidth; x++ { 212 | ix := (float64(x)+0.5)*dfx - 0.5 213 | start := utils.ClampInt(int(ix-radius+0.5), 0, originalSize.X) 214 | end := utils.ClampInt(int(ix+radius), 0, originalSize.X) 215 | var fPixR float64 216 | var fPixG float64 217 | var fPixB float64 218 | var fPixA float64 219 | var sum float64 220 | for i := start; i < end; i++ { 221 | filterValue := filter.Interpolate(float64(i)-ix) / fx 222 | pix := img.RGBAAt(i, y) 223 | fPixR += float64(pix.R) * filterValue 224 | fPixG += float64(pix.G) * filterValue 225 | fPixB += float64(pix.B) * filterValue 226 | fPixA += float64(pix.A) * filterValue 227 | sum += filterValue 228 | } 229 | res.SetRGBA(x, y, color.RGBA{R: uint8(utils.ClampF64(fPixR/sum+0.5, 0, 255)), 230 | G: uint8(utils.ClampF64(fPixG/sum+0.5, 0, 255)), 231 | B: uint8(utils.ClampF64(fPixB/sum+0.5, 0, 255)), 232 | A: uint8(utils.ClampF64(fPixA/sum+0.5, 0, 255))}) 233 | } 234 | } 235 | return res, nil 236 | } 237 | 238 | func resizeVerticalRGBA(img *image.RGBA, fy float64, filter Filter) (*image.RGBA, error) { 239 | originalSize := img.Bounds().Size() 240 | newHeight := int(float64(originalSize.Y) * fy) 241 | res := image.NewRGBA(image.Rect(0, 0, originalSize.X, newHeight)) 242 | dfy := 1 / fy 243 | 244 | radius := math.Ceil(fy * filter.GetS()) 245 | for y := 0; y < newHeight; y++ { 246 | iy := (float64(y)+0.5)*dfy - 0.5 247 | start := utils.ClampInt(int(iy-radius+0.5), 0, originalSize.Y) 248 | end := utils.ClampInt(int(iy+radius), 0, originalSize.Y) 249 | for x := 0; x < originalSize.X; x++ { 250 | var fPixR float64 251 | var fPixG float64 252 | var fPixB float64 253 | var fPixA float64 254 | var sum float64 255 | for i := start; i < end; i++ { 256 | filterValue := filter.Interpolate(float64(i)-iy) / fy 257 | pix := img.RGBAAt(x, i) 258 | fPixR += float64(pix.R) * filterValue 259 | fPixG += float64(pix.G) * filterValue 260 | fPixB += float64(pix.B) * filterValue 261 | fPixA += float64(pix.A) * filterValue 262 | sum += filterValue 263 | } 264 | res.SetRGBA(x, y, color.RGBA{R: uint8(utils.ClampF64(fPixR/sum+0.5, 0, 255)), 265 | G: uint8(utils.ClampF64(fPixG/sum+0.5, 0, 255)), 266 | B: uint8(utils.ClampF64(fPixB/sum+0.5, 0, 255)), 267 | A: uint8(utils.ClampF64(fPixA/sum+0.5, 0, 255))}) 268 | } 269 | } 270 | return res, nil 271 | } 272 | 273 | // ResizeGray resizes an grayscale (Gray) image. 274 | // Input parameters: rbga imaga which will be resized; fx, fy scaling factors, their value has to be a positive float, 275 | // the new size of the image will be computed as originalWidth * fx and originalHeight * fy; interpolation method, 276 | // currently the following methods are supported: InterNearest, InterLinear, InterCatmullRom, InterLanczos. 277 | // Example of usage: 278 | // 279 | // res, err := resize.ResizeGray(img, 2.5, 3.5, resize.InterLinear) 280 | // 281 | func ResizeGray(img *image.Gray, fx float64, fy float64, interpolation Interpolation) (*image.Gray, error) { 282 | if fx < 0 || fy < 0 { 283 | return nil, errors.New("scale value should be greater then 0") 284 | } 285 | switch interpolation { 286 | case InterNearest: 287 | return resizeNearestGray(img, fx, fy) 288 | case InterLinear: 289 | return resizeLinearGray(img, fx, fy) 290 | case InterCatmullRom: 291 | return resizeCatmullRomGray(img, fx, fy) 292 | case InterLanczos: 293 | return resizeLanczosGray(img, fx, fy) 294 | } 295 | return nil, errors.New("invalid interpolation method") 296 | } 297 | 298 | // ResizeRGBA resizes an RGBA image. 299 | // Input parameters: rbga imaga which will be resized; fx, fy scaling factors, their value has to be a positive float, 300 | // the new size of the image will be computed as originalWidth * fx and originalHeight * fy; interpolation method, 301 | // currently the following methods are supported: InterNearest, InterLinear, InterCatmullRom, InterLanczos. 302 | // Example of usage: 303 | // 304 | // res, err := resize.ResizeRGBA(img, 2.5, 3.5, resize.InterLinear) 305 | // 306 | func ResizeRGBA(img *image.RGBA, fx float64, fy float64, interpolation Interpolation) (*image.RGBA, error) { 307 | if fx < 0 || fy < 0 { 308 | return nil, errors.New("scale value should be greater then 0") 309 | } 310 | switch interpolation { 311 | case InterNearest: 312 | return resizeNearestRGBA(img, fx, fy) 313 | case InterLinear: 314 | return resizeLinearRGBA(img, fx, fy) 315 | case InterCatmullRom: 316 | return resizeCatmullRomRGBA(img, fx, fy) 317 | case InterLanczos: 318 | return resizeLanczosRGBA(img, fx, fy) 319 | } 320 | return nil, errors.New("invalid interpolation method") 321 | } 322 | -------------------------------------------------------------------------------- /resize/resize_test.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | // -----------------------------Acceptance tests------------------------------------ 10 | func setupTestCaseGray(t *testing.T) *image.Gray { 11 | path := "../res/girl.jpg" 12 | img, err := imgio.ImreadGray(path) 13 | if err != nil { 14 | t.Errorf("Could not read image from path: %s", path) 15 | } 16 | return img 17 | } 18 | 19 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 20 | path := "../res/girl.jpg" 21 | img, err := imgio.ImreadRGBA(path) 22 | if err != nil { 23 | t.Errorf("Could not read image from path: %s", path) 24 | } 25 | return img 26 | } 27 | 28 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 29 | err := imgio.Imwrite(img, path) 30 | if err != nil { 31 | t.Errorf("Could not write image to path: %s", path) 32 | } 33 | } 34 | 35 | func Test_Acceptance_GrayResize_NN_2X(t *testing.T) { 36 | fxy := 2.0 37 | gray := setupTestCaseGray(t) 38 | actual, _ := ResizeGray(gray, fxy, fxy, InterNearest) 39 | originalSize := gray.Bounds().Size() 40 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 41 | actualSize := actual.Bounds().Size() 42 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 43 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 44 | } 45 | tearDownTestCase(t, actual, "../res/resize/grayResize2x.jpg") 46 | } 47 | 48 | func Test_Acceptance_GrayResize_NN_1_5X(t *testing.T) { 49 | fxy := 1.5 50 | gray := setupTestCaseGray(t) 51 | actual, _ := ResizeGray(gray, fxy, fxy, InterNearest) 52 | originalSize := gray.Bounds().Size() 53 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 54 | actualSize := actual.Bounds().Size() 55 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 56 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 57 | } 58 | tearDownTestCase(t, actual, "../res/resize/grayResize1_5x.jpg") 59 | } 60 | 61 | func Test_Acceptance_GrayResize_NN_2_5_AND_3_5X(t *testing.T) { 62 | fx := 2.5 63 | fy := 3.5 64 | gray := setupTestCaseGray(t) 65 | actual, _ := ResizeGray(gray, fx, fy, InterNearest) 66 | originalSize := gray.Bounds().Size() 67 | expectedSize := image.Point{X: int(float64(originalSize.X) * fx), Y: int(float64(originalSize.Y) * fy)} 68 | actualSize := actual.Bounds().Size() 69 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 70 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 71 | } 72 | tearDownTestCase(t, actual, "../res/resize/grayResize2_5_and_3_5x.jpg") 73 | } 74 | 75 | func Test_Acceptance_GrayResize_NN_0_5X(t *testing.T) { 76 | fxy := 0.5 77 | gray := setupTestCaseGray(t) 78 | actual, _ := ResizeGray(gray, fxy, fxy, InterNearest) 79 | originalSize := gray.Bounds().Size() 80 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 81 | actualSize := actual.Bounds().Size() 82 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 83 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 84 | } 85 | tearDownTestCase(t, actual, "../res/resize/grayResize0_5x.jpg") 86 | } 87 | 88 | func Test_Acceptance_GrayResize_Linear_2X(t *testing.T) { 89 | fxy := 2.0 90 | gray := setupTestCaseGray(t) 91 | actual, _ := ResizeGray(gray, fxy, fxy, InterLinear) 92 | originalSize := gray.Bounds().Size() 93 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 94 | actualSize := actual.Bounds().Size() 95 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 96 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 97 | } 98 | tearDownTestCase(t, actual, "../res/resize/grayResize_Linear_2x.jpg") 99 | } 100 | 101 | func Test_Acceptance_GrayResize_CatmullRom_2X(t *testing.T) { 102 | fxy := 2.0 103 | gray := setupTestCaseGray(t) 104 | actual, _ := ResizeGray(gray, fxy, fxy, InterCatmullRom) 105 | originalSize := gray.Bounds().Size() 106 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 107 | actualSize := actual.Bounds().Size() 108 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 109 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 110 | } 111 | tearDownTestCase(t, actual, "../res/resize/grayResize_CatmullRom_2x.jpg") 112 | } 113 | 114 | func Test_Acceptance_GrayResize_CatmullRom_0_5X(t *testing.T) { 115 | fxy := 0.5 116 | gray := setupTestCaseGray(t) 117 | actual, _ := ResizeGray(gray, fxy, fxy, InterCatmullRom) 118 | originalSize := gray.Bounds().Size() 119 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 120 | actualSize := actual.Bounds().Size() 121 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 122 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 123 | } 124 | tearDownTestCase(t, actual, "../res/resize/grayResize_CatmullRom_0_5x.jpg") 125 | } 126 | 127 | func Test_Acceptance_GrayResize_Lanczos_2X(t *testing.T) { 128 | fxy := 2.0 129 | gray := setupTestCaseGray(t) 130 | actual, _ := ResizeGray(gray, fxy, fxy, InterLanczos) 131 | originalSize := gray.Bounds().Size() 132 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 133 | actualSize := actual.Bounds().Size() 134 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 135 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 136 | } 137 | tearDownTestCase(t, actual, "../res/resize/grayResize_Lanczos_2x.jpg") 138 | } 139 | 140 | func Test_Acceptance_GrayResize_Lanczos_0_5X(t *testing.T) { 141 | fxy := 0.5 142 | gray := setupTestCaseGray(t) 143 | actual, _ := ResizeGray(gray, fxy, fxy, InterLanczos) 144 | originalSize := gray.Bounds().Size() 145 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 146 | actualSize := actual.Bounds().Size() 147 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 148 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 149 | } 150 | tearDownTestCase(t, actual, "../res/resize/grayResize_Lanczos_0_5x.jpg") 151 | } 152 | 153 | func Test_Acceptance_RGBAResize_NN_2X(t *testing.T) { 154 | fxy := 2.0 155 | rgba := setupTestCaseRGBA(t) 156 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterNearest) 157 | originalSize := rgba.Bounds().Size() 158 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 159 | actualSize := actual.Bounds().Size() 160 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 161 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 162 | } 163 | tearDownTestCase(t, actual, "../res/resize/rgbaResize2x.jpg") 164 | } 165 | 166 | func Test_Acceptance_RGBAResize_NN_1_5X(t *testing.T) { 167 | fxy := 1.5 168 | rgba := setupTestCaseRGBA(t) 169 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterNearest) 170 | originalSize := rgba.Bounds().Size() 171 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 172 | actualSize := actual.Bounds().Size() 173 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 174 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 175 | } 176 | tearDownTestCase(t, actual, "../res/resize/rgbaResize1_5x.jpg") 177 | } 178 | 179 | func Test_Acceptance_RGBAResize_NN_2_5_AND_3_5X(t *testing.T) { 180 | fx := 2.5 181 | fy := 3.5 182 | rgba := setupTestCaseRGBA(t) 183 | actual, _ := ResizeRGBA(rgba, fx, fy, InterNearest) 184 | originalSize := rgba.Bounds().Size() 185 | expectedSize := image.Point{X: int(float64(originalSize.X) * fx), Y: int(float64(originalSize.Y) * fy)} 186 | actualSize := actual.Bounds().Size() 187 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 188 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 189 | } 190 | tearDownTestCase(t, actual, "../res/resize/rgbaResize2_5_and_3_5x.jpg") 191 | } 192 | 193 | func Test_Acceptance_RGBAResize_NN_0_5X(t *testing.T) { 194 | fxy := 0.5 195 | rgba := setupTestCaseRGBA(t) 196 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterNearest) 197 | originalSize := rgba.Bounds().Size() 198 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 199 | actualSize := actual.Bounds().Size() 200 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 201 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 202 | } 203 | tearDownTestCase(t, actual, "../res/resize/rgbaResize0_5x.jpg") 204 | } 205 | 206 | func Test_Acceptance_RGBAResize_Linear_2X(t *testing.T) { 207 | fxy := 2.0 208 | rgba := setupTestCaseRGBA(t) 209 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterLinear) 210 | originalSize := rgba.Bounds().Size() 211 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 212 | actualSize := actual.Bounds().Size() 213 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 214 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 215 | } 216 | tearDownTestCase(t, actual, "../res/resize/rgbaResize_Linear_2x.jpg") 217 | } 218 | 219 | func Test_Acceptance_RGBAResize_CatmullRom_2X(t *testing.T) { 220 | fxy := 2.0 221 | rgba := setupTestCaseRGBA(t) 222 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterCatmullRom) 223 | originalSize := rgba.Bounds().Size() 224 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 225 | actualSize := actual.Bounds().Size() 226 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 227 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 228 | } 229 | tearDownTestCase(t, actual, "../res/resize/rgbaResize_CatmullRom_2x.jpg") 230 | } 231 | 232 | func Test_Acceptance_RGBAResize_CatmullRom_0_5X(t *testing.T) { 233 | fxy := 0.5 234 | rgba := setupTestCaseRGBA(t) 235 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterCatmullRom) 236 | originalSize := rgba.Bounds().Size() 237 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 238 | actualSize := actual.Bounds().Size() 239 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 240 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 241 | } 242 | tearDownTestCase(t, actual, "../res/resize/rgbaResize_CatmullRom_0_5x.jpg") 243 | } 244 | 245 | func Test_Acceptance_RGBAResize_Lanczos_2X(t *testing.T) { 246 | fxy := 2.0 247 | rgba := setupTestCaseRGBA(t) 248 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterLanczos) 249 | originalSize := rgba.Bounds().Size() 250 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 251 | actualSize := actual.Bounds().Size() 252 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 253 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 254 | } 255 | tearDownTestCase(t, actual, "../res/resize/rgbaResize_Lanczos_2x.jpg") 256 | } 257 | 258 | func Test_Acceptance_RGBAResize_Lanczos_0_5X(t *testing.T) { 259 | fxy := 0.5 260 | rgba := setupTestCaseRGBA(t) 261 | actual, _ := ResizeRGBA(rgba, fxy, fxy, InterLanczos) 262 | originalSize := rgba.Bounds().Size() 263 | expectedSize := image.Point{X: int(float64(originalSize.X) * fxy), Y: int(float64(originalSize.Y) * fxy)} 264 | actualSize := actual.Bounds().Size() 265 | if expectedSize.X != actualSize.X || expectedSize.Y != actualSize.Y { 266 | t.Errorf("Expected size of [%d, %d] does not match actual size of [%d, %d]!", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 267 | } 268 | tearDownTestCase(t, actual, "../res/resize/rgbaResize_Lanczos_0_5x.jpg") 269 | } 270 | 271 | // ---------------------------------------------------------------------------------- 272 | -------------------------------------------------------------------------------- /threshold/threshold.go: -------------------------------------------------------------------------------- 1 | package threshold 2 | 3 | import ( 4 | "errors" 5 | "image" 6 | "image/color" 7 | 8 | "github.com/ernyoke/imger/histogram" 9 | "github.com/ernyoke/imger/utils" 10 | ) 11 | 12 | // Method is an enum type for global threshold methods 13 | type Method int 14 | 15 | const ( 16 | // ThreshBinary 17 | // _ 18 | // | maxVal if src(x, y) > thresh 19 | // dst(x, y) = | 20 | // | 0 otherwise 21 | // |_ 22 | ThreshBinary Method = iota 23 | // ThreshBinaryInv 24 | // _ 25 | // | 0 if src(x, y) > thresh 26 | // dst(x, y) = | 27 | // | maxVal otherwise 28 | // |_ 29 | ThreshBinaryInv 30 | // ThreshTrunc 31 | // _ 32 | // | thresh if src(x, y) > thresh 33 | // dst(x, y) = | 34 | // | src(x, y) otherwise 35 | // |_ 36 | ThreshTrunc 37 | // ThreshToZero 38 | // _ 39 | // | src(x, y) if src(x, y) > thresh 40 | // dst(x, y) = | 41 | // | 0 otherwise 42 | // |_ 43 | ThreshToZero 44 | // ThreshToZeroInv 45 | // _ 46 | // | 0 if src(x, y) > thresh 47 | // dst(x, y) = | 48 | // | src(x, y) otherwise 49 | // |_ 50 | ThreshToZeroInv 51 | ) 52 | 53 | // Threshold returns a 8 bit grayscale image as result which was segmented using one of the following methods: 54 | // ThreshBinary, ThreshBinaryInv, ThreshTrunc, ThreshToZero, ThreshToZeroInv 55 | func Threshold(img *image.Gray, t uint8, method Method) (*image.Gray, error) { 56 | var setPixel func(*image.Gray, int, int) 57 | switch method { 58 | case ThreshBinary: 59 | setPixel = func(gray *image.Gray, x int, y int) { 60 | pixel := img.GrayAt(x, y).Y 61 | if pixel < t { 62 | gray.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 63 | } else { 64 | gray.SetGray(x, y, color.Gray{Y: utils.MaxUint8}) 65 | } 66 | } 67 | case ThreshBinaryInv: 68 | setPixel = func(gray *image.Gray, x int, y int) { 69 | pixel := img.GrayAt(x, y).Y 70 | if pixel < t { 71 | gray.SetGray(x, y, color.Gray{Y: utils.MaxUint8}) 72 | } else { 73 | gray.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 74 | } 75 | } 76 | case ThreshTrunc: 77 | { 78 | setPixel = func(gray *image.Gray, x int, y int) { 79 | pixel := img.GrayAt(x, y).Y 80 | if pixel < t { 81 | gray.SetGray(x, y, color.Gray{Y: pixel}) 82 | } else { 83 | gray.SetGray(x, y, color.Gray{Y: t}) 84 | } 85 | } 86 | } 87 | case ThreshToZero: 88 | setPixel = func(gray *image.Gray, x int, y int) { 89 | pixel := img.GrayAt(x, y).Y 90 | if pixel < t { 91 | gray.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 92 | } else { 93 | gray.SetGray(x, y, color.Gray{Y: pixel}) 94 | } 95 | } 96 | case ThreshToZeroInv: 97 | setPixel = func(gray *image.Gray, x int, y int) { 98 | pixel := img.GrayAt(x, y).Y 99 | if pixel < t { 100 | gray.SetGray(x, y, color.Gray{Y: pixel}) 101 | } else { 102 | gray.SetGray(x, y, color.Gray{Y: utils.MinUint8}) 103 | } 104 | } 105 | default: 106 | return nil, errors.New("invalid threshold method") 107 | } 108 | return threshold(img, setPixel), nil 109 | } 110 | 111 | // Threshold16 returns a grayscale image represented on 16 bits as result which was segmented using one of the following 112 | // Methods: ThreshBinary, ThreshBinaryInv, ThreshTrunc, ThreshToZero, ThreshToZeroInv 113 | func Threshold16(img *image.Gray16, t uint16, method Method) (*image.Gray16, error) { 114 | var setPixel func(*image.Gray16, int, int) 115 | switch method { 116 | case ThreshBinary: 117 | setPixel = func(gray *image.Gray16, x int, y int) { 118 | pixel := img.Gray16At(x, y).Y 119 | if pixel < t { 120 | gray.SetGray16(x, y, color.Gray16{Y: utils.MinUint16}) 121 | } else { 122 | gray.SetGray16(x, y, color.Gray16{Y: utils.MaxUint16}) 123 | } 124 | } 125 | case ThreshBinaryInv: 126 | setPixel = func(gray *image.Gray16, x int, y int) { 127 | pixel := img.Gray16At(x, y).Y 128 | if pixel < t { 129 | gray.SetGray16(x, y, color.Gray16{Y: utils.MaxUint16}) 130 | } else { 131 | gray.SetGray16(x, y, color.Gray16{Y: utils.MinUint16}) 132 | } 133 | } 134 | case ThreshTrunc: 135 | { 136 | setPixel = func(gray *image.Gray16, x int, y int) { 137 | pixel := img.Gray16At(x, y).Y 138 | if pixel < t { 139 | gray.SetGray16(x, y, color.Gray16{Y: pixel}) 140 | } else { 141 | gray.SetGray16(x, y, color.Gray16{Y: t}) 142 | } 143 | } 144 | } 145 | case ThreshToZero: 146 | setPixel = func(gray *image.Gray16, x int, y int) { 147 | pixel := img.Gray16At(x, y).Y 148 | if pixel < t { 149 | gray.SetGray16(x, y, color.Gray16{Y: utils.MinUint16}) 150 | } else { 151 | gray.SetGray16(x, y, color.Gray16{Y: pixel}) 152 | } 153 | } 154 | case ThreshToZeroInv: 155 | setPixel = func(gray *image.Gray16, x int, y int) { 156 | pixel := img.Gray16At(x, y).Y 157 | if pixel < t { 158 | gray.SetGray16(x, y, color.Gray16{pixel}) 159 | } else { 160 | gray.SetGray16(x, y, color.Gray16{Y: utils.MinUint16}) 161 | } 162 | } 163 | default: 164 | return nil, errors.New("invalid threshold method") 165 | } 166 | return threshold16(img, setPixel), nil 167 | } 168 | 169 | // OtsuThreshold returns a grayscale image which was segmented using Otsu's adaptive thresholding method. 170 | // Methods: ThreshBinary, ThreshBinaryInv, ThreshTrunc, ThreshToZero, ThreshToZeroInv 171 | // More info about Otsu's method: https://en.wikipedia.org/wiki/Otsu%27s_method 172 | func OtsuThreshold(img *image.Gray, method Method) (*image.Gray, error) { 173 | return Threshold(img, otsuThresholdValue(img), method) 174 | } 175 | 176 | // ------------------------------------------------------------------------------------------------------- 177 | func threshold(img *image.Gray, setPixel func(*image.Gray, int, int)) *image.Gray { 178 | size := img.Bounds().Size() 179 | gray := image.NewGray(img.Bounds()) 180 | offset := img.Bounds().Min 181 | utils.ParallelForEachPixel(size, func(x, y int) { 182 | setPixel(gray, x+offset.X, y+offset.Y) 183 | }) 184 | return gray 185 | } 186 | 187 | func threshold16(img *image.Gray16, setPixel16 func(*image.Gray16, int, int)) *image.Gray16 { 188 | size := img.Bounds().Size() 189 | gray := image.NewGray16(img.Bounds()) 190 | offset := img.Bounds().Min 191 | utils.ParallelForEachPixel(size, func(x, y int) { 192 | setPixel16(gray, x+offset.X, y+offset.Y) 193 | }) 194 | return gray 195 | } 196 | 197 | func otsuThresholdValue(img *image.Gray) uint8 { 198 | hist := histogram.HistogramGray(img) 199 | size := img.Bounds().Size() 200 | totalNumberOfPixels := size.X * size.Y 201 | 202 | var sumHist float64 203 | for i, bin := range hist { 204 | sumHist += float64(uint64(i) * bin) 205 | } 206 | 207 | var sumBackground float64 208 | var weightBackground int 209 | var weightForeground int 210 | 211 | maxVariance := 0.0 212 | var thresh uint8 213 | for i, bin := range hist { 214 | weightBackground += int(bin) 215 | if weightBackground == 0 { 216 | continue 217 | } 218 | weightForeground = totalNumberOfPixels - weightBackground 219 | if weightForeground == 0 { 220 | break 221 | } 222 | 223 | sumBackground += float64(uint64(i) * bin) 224 | 225 | meanBackground := float64(sumBackground) / float64(weightBackground) 226 | meanForeground := (sumHist - sumBackground) / float64(weightForeground) 227 | 228 | variance := float64(weightBackground) * float64(weightForeground) * (meanBackground - meanForeground) * (meanBackground - meanForeground) 229 | 230 | if variance > maxVariance { 231 | maxVariance = variance 232 | thresh = uint8(i) 233 | } 234 | } 235 | return thresh 236 | } 237 | -------------------------------------------------------------------------------- /threshold/threshold_test.go: -------------------------------------------------------------------------------- 1 | package threshold 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "testing" 7 | 8 | "github.com/ernyoke/imger/imgio" 9 | ) 10 | 11 | // -----------------------------Acceptance tests------------------------------------ 12 | func setupTestCaseGray(t *testing.T) *image.Gray { 13 | path := "../res/girl.jpg" 14 | img, err := imgio.ImreadGray(path) 15 | if err != nil { 16 | t.Errorf("Could not read image from path: %s", path) 17 | } 18 | return img 19 | } 20 | 21 | func setupTestCaseOtsu(t *testing.T) *image.Gray { 22 | path := "../res/building.jpg" 23 | img, err := imgio.ImreadGray(path) 24 | if err != nil { 25 | t.Errorf("Could not read image from path: %s", path) 26 | } 27 | return img 28 | } 29 | 30 | func setupTestCaseGray16(t *testing.T) *image.Gray16 { 31 | path := "../res/girl.jpg" 32 | img, err := imgio.ImreadGray16(path) 33 | if err != nil { 34 | t.Errorf("Could not read image from path: %s", path) 35 | } 36 | return img 37 | } 38 | 39 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 40 | err := imgio.Imwrite(img, path) 41 | if err != nil { 42 | t.Errorf("Could not write image to path: %s", path) 43 | } 44 | } 45 | 46 | func Test_Acceptance_ThresholdBinray(t *testing.T) { 47 | gray := setupTestCaseGray(t) 48 | thresh, _ := Threshold(gray, 100, ThreshBinary) 49 | tearDownTestCase(t, thresh, "../res/threshold/threshBin.jpg") 50 | } 51 | 52 | func Test_Acceptance_ThresholdBinrayInv(t *testing.T) { 53 | gray := setupTestCaseGray(t) 54 | thresh, _ := Threshold(gray, 100, ThreshBinaryInv) 55 | tearDownTestCase(t, thresh, "../res/threshold/threshBinInv.jpg") 56 | } 57 | 58 | func Test_Acceptance_ThresholdTrunc(t *testing.T) { 59 | gray := setupTestCaseGray(t) 60 | thresh, _ := Threshold(gray, 100, ThreshTrunc) 61 | tearDownTestCase(t, thresh, "../res/threshold/threshTrunc.jpg") 62 | } 63 | 64 | func Test_Acceptance_ThresholdToZero(t *testing.T) { 65 | gray := setupTestCaseGray(t) 66 | thresh, _ := Threshold(gray, 100, ThreshToZero) 67 | tearDownTestCase(t, thresh, "../res/threshold/threshToZero.jpg") 68 | } 69 | 70 | func Test_Acceptance_ThresholdToZeroInv(t *testing.T) { 71 | gray := setupTestCaseGray(t) 72 | thresh, _ := Threshold(gray, 100, ThreshToZeroInv) 73 | tearDownTestCase(t, thresh, "../res/threshold/threshBin.jpg") 74 | } 75 | 76 | func Test_Acceptance_Threshold16Bin(t *testing.T) { 77 | gray := setupTestCaseGray16(t) 78 | thresh, _ := Threshold16(gray, 32000, ThreshBinary) 79 | tearDownTestCase(t, thresh, "../res/threshold/thresh16Bin.jpg") 80 | } 81 | 82 | func Test_Acceptance_Threshold16BinInv(t *testing.T) { 83 | gray := setupTestCaseGray16(t) 84 | thresh, _ := Threshold16(gray, 32000, ThreshBinaryInv) 85 | tearDownTestCase(t, thresh, "../res/threshold/thresh16BinInv.jpg") 86 | } 87 | 88 | func Test_Acceptance_Threshold16Trunc(t *testing.T) { 89 | gray := setupTestCaseGray16(t) 90 | thresh, _ := Threshold16(gray, 32000, ThreshTrunc) 91 | tearDownTestCase(t, thresh, "../res/threshold/thresh16Trunc.jpg") 92 | } 93 | 94 | func Test_Acceptance_Threshold16ToZero(t *testing.T) { 95 | gray := setupTestCaseGray16(t) 96 | thresh, _ := Threshold16(gray, 32000, ThreshToZero) 97 | tearDownTestCase(t, thresh, "../res/threshold/thresh16ToZero.jpg") 98 | } 99 | 100 | func Test_Acceptance_Threshold16ToZeroInv(t *testing.T) { 101 | gray := setupTestCaseGray16(t) 102 | thresh, _ := Threshold16(gray, 32000, ThreshToZeroInv) 103 | tearDownTestCase(t, thresh, "../res/threshold/thresh16ToZeroInv.jpg") 104 | } 105 | 106 | func Test_Acceptance_OtsuThreshold(t *testing.T) { 107 | gray := setupTestCaseOtsu(t) 108 | thresh, _ := OtsuThreshold(gray, ThreshBinary) 109 | tearDownTestCase(t, thresh, "../res/threshold/otsuThreshBin.jpg") 110 | } 111 | 112 | func Test_Acceptance_OtsuThreshold_Cropped(t *testing.T) { 113 | thresholdMethods := map[string]Method{ 114 | "Bin": ThreshBinary, 115 | "BinInv": ThreshBinaryInv, 116 | "Trunc": ThreshTrunc, 117 | "ToZero": ThreshToZero, 118 | "ToZeroInv": ThreshToZeroInv, 119 | } 120 | 121 | for name, meth := range thresholdMethods { 122 | t.Run(name, func(t *testing.T) { 123 | gray := setupTestCaseOtsu(t) 124 | cropped := gray.SubImage(image.Rect(100, 100, gray.Bounds().Max.X-100, gray.Bounds().Max.Y-100)) 125 | thresh, err := OtsuThreshold(cropped.(*image.Gray), meth) 126 | if err != nil { 127 | t.Fatalf("unexpected error: %v", err) 128 | } 129 | tearDownTestCase(t, thresh, fmt.Sprintf("../res/threshold/otsuThresh%sCropped.jpg", name)) 130 | }) 131 | } 132 | } 133 | 134 | //--------------------------------------------------------------------------------- 135 | -------------------------------------------------------------------------------- /transform/transform.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "errors" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "math" 8 | ) 9 | 10 | // RotateGray rotates a grayscale image counterclockwise with a given angle. The point which will represent the center 11 | // ot the rotation is specified by the anchor argument. The result image can have its original size or it can be 12 | // resized to fit in the area of the image. 13 | // Example of usage: 14 | // 15 | // res, err := transform.RotateGray(img, 90.0, {512, 512}, true) 16 | // 17 | func RotateGray(img *image.Gray, angle float64, anchor image.Point, resizeToFit bool) (*image.Gray, error) { 18 | size := img.Bounds().Size() 19 | if anchor.X < 0 || anchor.Y < 0 || anchor.X > size.X || anchor.Y > size.Y { 20 | return nil, errors.New("invalid anchor position") 21 | } 22 | radians := angleToRadians(angle) 23 | newSize := size 24 | if resizeToFit { 25 | newSize = computeFitSize(size, radians) 26 | } 27 | result := image.NewGray(image.Rect(0, 0, newSize.X, newSize.Y)) 28 | utils.ParallelForEachPixel(newSize, func(x, y int) { 29 | result.SetGray(x, y, img.GrayAt(getOriginalPixelPosition(x, y, radians, anchor, computeOffset(size, newSize)))) 30 | }) 31 | return result, nil 32 | } 33 | 34 | // RotateRGBA rotates an RGBA image counterclockwise with a given angle. The point which will represent the center 35 | // ot the rotation is specified by the anchor argument. The result image can have its original size or it can be 36 | // resized to fit in the area of the image. 37 | // Example of usage: 38 | // 39 | // res, err := transform.RotateGray(img, 90.0, {512, 512}, true) 40 | // 41 | func RotateRGBA(img *image.RGBA, angle float64, anchor image.Point, resizeToFit bool) (*image.RGBA, error) { 42 | size := img.Bounds().Size() 43 | if anchor.X < 0 || anchor.Y < 0 || anchor.X > size.X || anchor.Y > size.Y { 44 | return nil, errors.New("invalid anchor position") 45 | } 46 | radians := angleToRadians(angle) 47 | newSize := size 48 | if resizeToFit { 49 | newSize = computeFitSize(size, radians) 50 | } 51 | result := image.NewRGBA(image.Rect(0, 0, newSize.X, newSize.Y)) 52 | utils.ParallelForEachPixel(newSize, func(x, y int) { 53 | result.SetRGBA(x, y, img.RGBAAt(getOriginalPixelPosition(x, y, radians, anchor, computeOffset(size, newSize)))) 54 | }) 55 | return result, nil 56 | } 57 | 58 | func angleToRadians(angle float64) float64 { 59 | return angle * (math.Pi / 180) 60 | } 61 | 62 | func computeFitSize(size image.Point, radians float64) image.Point { 63 | a := math.Abs(float64(size.X) * math.Sin(radians)) 64 | b := math.Abs(float64(size.X) * math.Cos(radians)) 65 | c := math.Abs(float64(size.Y) * math.Sin(radians)) 66 | d := math.Abs(float64(size.Y) * math.Cos(radians)) 67 | return image.Point{X: int(c + b), Y: int(a + d)} 68 | } 69 | 70 | func computeOffset(size image.Point, fittingSize image.Point) image.Point { 71 | return image.Point{X: (fittingSize.X - size.X) / 2, Y: (fittingSize.Y - size.Y) / 2} 72 | } 73 | 74 | func getOriginalPixelPosition(x int, y int, radians float64, anchor image.Point, offset image.Point) (int, int) { 75 | dx := x - anchor.X - offset.X 76 | dy := y - anchor.Y - offset.Y 77 | originalX := int(math.Floor(math.Cos(radians)*float64(dx) - math.Sin(radians)*float64(dy) + float64(anchor.X))) 78 | originalY := int(math.Floor(math.Sin(radians)*float64(dx) + math.Cos(radians)*float64(dy) + float64(anchor.Y))) 79 | return originalX, originalY 80 | } 81 | -------------------------------------------------------------------------------- /transform/transform_test.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "github.com/ernyoke/imger/imgio" 5 | "github.com/ernyoke/imger/utils" 6 | "image" 7 | "math" 8 | "testing" 9 | ) 10 | 11 | // ---------------------------------Unit tests-------------------------------------- 12 | func Test_AngleToRadians0(t *testing.T) { 13 | expected := 0.0 14 | actual := angleToRadians(0) 15 | if !utils.IsEqualFloat64(actual, expected) { 16 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 17 | } 18 | } 19 | 20 | func Test_AngleToRadians30(t *testing.T) { 21 | expected := math.Pi / 6 22 | actual := angleToRadians(30) 23 | if !utils.IsEqualFloat64(actual, expected) { 24 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 25 | } 26 | } 27 | 28 | func Test_AngleToRadians45(t *testing.T) { 29 | expected := math.Pi / 4 30 | actual := angleToRadians(45) 31 | if !utils.IsEqualFloat64(actual, expected) { 32 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 33 | } 34 | } 35 | 36 | func Test_AngleToRadians60(t *testing.T) { 37 | expected := math.Pi / 3 38 | actual := angleToRadians(60) 39 | if !utils.IsEqualFloat64(actual, expected) { 40 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 41 | } 42 | } 43 | 44 | func Test_AngleToRadians90(t *testing.T) { 45 | expected := math.Pi / 2 46 | actual := angleToRadians(90) 47 | if !utils.IsEqualFloat64(actual, expected) { 48 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 49 | } 50 | } 51 | 52 | func Test_AngleToRadians180(t *testing.T) { 53 | expected := math.Pi 54 | actual := angleToRadians(180) 55 | if !utils.IsEqualFloat64(actual, expected) { 56 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 57 | } 58 | } 59 | 60 | func Test_AngleToRadians360(t *testing.T) { 61 | expected := math.Pi * 2 62 | actual := angleToRadians(360) 63 | if !utils.IsEqualFloat64(actual, expected) { 64 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 65 | } 66 | } 67 | 68 | func Test_AngleToRadiansNeg30(t *testing.T) { 69 | expected := -math.Pi / 6 70 | actual := angleToRadians(-30) 71 | if !utils.IsEqualFloat64(actual, expected) { 72 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 73 | } 74 | } 75 | 76 | func Test_AngleToRadiansNeg60(t *testing.T) { 77 | expected := -math.Pi / 3 78 | actual := angleToRadians(-60) 79 | if !utils.IsEqualFloat64(actual, expected) { 80 | t.Errorf("Expected value %f is not eqaul with actual value %f", expected, actual) 81 | } 82 | } 83 | 84 | func Test_ComputeFitSize90(t *testing.T) { 85 | initial := image.Point{X: 1024, Y: 768} 86 | angle := 90.0 87 | expected := image.Point{X: 768, Y: 1024} 88 | actual := computeFitSize(initial, angleToRadians(angle)) 89 | if expected != actual { 90 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 91 | } 92 | } 93 | 94 | func Test_ComputeFitSize45(t *testing.T) { 95 | initial := image.Point{X: 1024, Y: 768} 96 | angle := 45.0 97 | expected := image.Point{X: 1267, Y: 1267} 98 | actual := computeFitSize(initial, angleToRadians(angle)) 99 | if expected != actual { 100 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 101 | } 102 | } 103 | 104 | func Test_ComputeFitSize22_5(t *testing.T) { 105 | initial := image.Point{X: 1024, Y: 768} 106 | angle := 22.5 107 | expected := image.Point{X: 1239, Y: 1101} 108 | actual := computeFitSize(initial, angleToRadians(angle)) 109 | if expected != actual { 110 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 111 | } 112 | } 113 | 114 | func Test_ComputeFitSizeNeg22_5(t *testing.T) { 115 | initial := image.Point{X: 1024, Y: 768} 116 | angle := -22.5 117 | expected := image.Point{X: 1239, Y: 1101} 118 | actual := computeFitSize(initial, angleToRadians(angle)) 119 | if expected != actual { 120 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 121 | } 122 | } 123 | 124 | func Test_ComputeFitSizeNeg45(t *testing.T) { 125 | initial := image.Point{X: 1024, Y: 768} 126 | angle := -45.0 127 | expected := image.Point{X: 1267, Y: 1267} 128 | actual := computeFitSize(initial, angleToRadians(angle)) 129 | if expected != actual { 130 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 131 | } 132 | } 133 | 134 | func Test_ComputeOffset90(t *testing.T) { 135 | size := image.Point{X: 1024, Y: 768} 136 | radians := angleToRadians(90) 137 | expected := image.Point{X: -128, Y: 128} 138 | actual := computeOffset(size, computeFitSize(size, radians)) 139 | if expected != actual { 140 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 141 | } 142 | } 143 | 144 | func Test_ComputeOffset180(t *testing.T) { 145 | size := image.Point{X: 1024, Y: 768} 146 | radians := angleToRadians(180) 147 | expected := image.Point{X: 0, Y: 0} 148 | actual := computeOffset(size, computeFitSize(size, radians)) 149 | if expected != actual { 150 | t.Errorf("Expected value %s is not eqaul with actual value %s", expected, actual) 151 | } 152 | } 153 | 154 | func Test_GetOriginalPixelPosition90(t *testing.T) { 155 | x := 0 156 | y := 0 157 | size := image.Point{X: 1024, Y: 768} 158 | radians := angleToRadians(90) 159 | anchor := image.Point{X: 512, Y: 384} 160 | offset := computeOffset(size, computeFitSize(size, radians)) 161 | expectedX := 1024 162 | expectedY := 0 163 | actualX, actualY := getOriginalPixelPosition(x, y, radians, anchor, offset) 164 | if actualX != expectedX && actualY != expectedY { 165 | t.Errorf("Expected value [%d %d] is not eqaul with actual value [%d %d]", expectedX, expectedY, actualX, actualY) 166 | } 167 | } 168 | 169 | func Test_GetOriginalPixelPosition45(t *testing.T) { 170 | x := 0 171 | y := 0 172 | size := image.Point{X: 1024, Y: 768} 173 | radians := angleToRadians(45) 174 | anchor := image.Point{X: 512, Y: 384} 175 | offset := computeOffset(size, computeFitSize(size, radians)) 176 | expectedX := 512 177 | expectedY := -512 178 | actualX, actualY := getOriginalPixelPosition(x, y, radians, anchor, offset) 179 | if actualX != expectedX && actualY != expectedY { 180 | t.Errorf("Expected value [%d %d] is not eqaul with actual value [%d %d]", expectedX, expectedY, actualX, actualY) 181 | } 182 | } 183 | 184 | // -----------------------------Acceptance tests------------------------------------ 185 | func setupTestCaseGray(t *testing.T) *image.Gray { 186 | path := "../res/building.jpg" 187 | img, err := imgio.ImreadGray(path) 188 | if err != nil { 189 | t.Errorf("Could not read image from path: %s", path) 190 | } 191 | return img 192 | } 193 | 194 | func setupTestCaseRGBA(t *testing.T) *image.RGBA { 195 | path := "../res/building.jpg" 196 | img, err := imgio.ImreadRGBA(path) 197 | if err != nil { 198 | t.Errorf("Could not read image from path: %s", path) 199 | } 200 | return img 201 | } 202 | 203 | func tearDownTestCase(t *testing.T, img image.Image, path string) { 204 | err := imgio.Imwrite(img, path) 205 | if err != nil { 206 | t.Errorf("Could not write image to path: %s", path) 207 | } 208 | } 209 | 210 | func Test_Acceptance_RotateGray90(t *testing.T) { 211 | gray := setupTestCaseGray(t) 212 | actual, err := RotateGray(gray, 90, image.Point{X: 512, Y: 384}, true) 213 | if err != nil { 214 | t.Errorf("Should not throw error!") 215 | } 216 | tearDownTestCase(t, actual, "../res/transform/roateGray90.jpg") 217 | } 218 | 219 | func Test_Acceptance_RotateGray45(t *testing.T) { 220 | gray := setupTestCaseGray(t) 221 | actual, err := RotateGray(gray, 45, image.Point{X: 512, Y: 384}, true) 222 | if err != nil { 223 | t.Errorf("Should not throw error!") 224 | } 225 | tearDownTestCase(t, actual, "../res/transform/roateGray45.jpg") 226 | } 227 | 228 | func Test_Acceptance_RotateGray22(t *testing.T) { 229 | gray := setupTestCaseGray(t) 230 | actual, err := RotateGray(gray, 22, image.Point{X: 512, Y: 384}, true) 231 | if err != nil { 232 | t.Errorf("Should not throw error!") 233 | } 234 | tearDownTestCase(t, actual, "../res/transform/roateGray22.jpg") 235 | } 236 | 237 | func Test_Acceptance_RotateRGBA90(t *testing.T) { 238 | rgba := setupTestCaseRGBA(t) 239 | actual, err := RotateRGBA(rgba, 90, image.Point{X: 512, Y: 384}, true) 240 | if err != nil { 241 | t.Errorf("Should not throw error!") 242 | } 243 | tearDownTestCase(t, actual, "../res/transform/roateRGBA90.jpg") 244 | } 245 | 246 | func Test_Acceptance_RotateRGBA45(t *testing.T) { 247 | rgba := setupTestCaseRGBA(t) 248 | actual, err := RotateRGBA(rgba, 45, image.Point{X: 512, Y: 384}, true) 249 | if err != nil { 250 | t.Errorf("Should not throw error!") 251 | } 252 | tearDownTestCase(t, actual, "../res/transform/roateRGBA45.jpg") 253 | } 254 | 255 | func Test_Acceptance_RotateRGBA22(t *testing.T) { 256 | rgba := setupTestCaseRGBA(t) 257 | actual, err := RotateRGBA(rgba, 22, image.Point{X: 512, Y: 384}, true) 258 | if err != nil { 259 | t.Errorf("Should not throw error!") 260 | } 261 | tearDownTestCase(t, actual, "../res/transform/roateRGBA22.jpg") 262 | } 263 | 264 | func Test_Acceptance_RotateRGBA45Noresize(t *testing.T) { 265 | rgba := setupTestCaseRGBA(t) 266 | actual, err := RotateRGBA(rgba, 45, image.Point{X: 512, Y: 384}, false) 267 | if err != nil { 268 | t.Errorf("Should not throw error!") 269 | } 270 | tearDownTestCase(t, actual, "../res/transform/roateRGBA45Noresize.jpg") 271 | } 272 | -------------------------------------------------------------------------------- /utils/constants.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // MaxUint8 - maximum value which can be held in an uint8 4 | const MaxUint8 = ^uint8(0) 5 | 6 | // MinUint8 - minimum value which can be held in an uint8 7 | const MinUint8 = 0 8 | 9 | // MaxUint16 - maximum value which can be held in an uint16 10 | const MaxUint16 = ^uint16(0) 11 | 12 | // MinUint16 - minimum value which can be held in an uint8 13 | const MinUint16 = 0 14 | -------------------------------------------------------------------------------- /utils/helpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | // ForEachPixel loops through the image and calls f functions for each [x, y] position. 9 | func ForEachPixel(size image.Point, f func(x int, y int)) { 10 | for y := 0; y < size.Y; y++ { 11 | for x := 0; x < size.X; x++ { 12 | f(x, y) 13 | } 14 | } 15 | } 16 | 17 | // ForEachGrayPixel loops through the image and calls f functions for each gray pixel. 18 | func ForEachGrayPixel(img *image.Gray, f func(pixel color.Gray)) { 19 | ForEachPixel(img.Bounds().Size(), func(x, y int) { 20 | pixel := img.GrayAt(x, y) 21 | f(pixel) 22 | }) 23 | } 24 | 25 | // ForEachRGBAPixel loops through the image and calls f functions for each RGBA pixel. 26 | func ForEachRGBAPixel(img *image.RGBA, f func(pixel color.RGBA)) { 27 | ForEachPixel(img.Bounds().Size(), func(x, y int) { 28 | pixel := img.RGBAAt(x, y) 29 | f(pixel) 30 | }) 31 | } 32 | 33 | // ForEachRGBARedPixel loops through the image and calls f functions for red component of each RGBA pixel. 34 | func ForEachRGBARedPixel(img *image.RGBA, f func(r uint8)) { 35 | ForEachRGBAPixel(img, func(pixel color.RGBA) { 36 | f(pixel.R) 37 | }) 38 | } 39 | 40 | // ForEachRGBAGreenPixel loops through the image and calls f functions for green component of each RGBA pixel. 41 | func ForEachRGBAGreenPixel(img *image.RGBA, f func(r uint8)) { 42 | ForEachRGBAPixel(img, func(pixel color.RGBA) { 43 | f(pixel.G) 44 | }) 45 | } 46 | 47 | // ForEachRGBABluePixel loops through the image and calls f functions for blue component of each RGBA pixel. 48 | func ForEachRGBABluePixel(img *image.RGBA, f func(r uint8)) { 49 | ForEachRGBAPixel(img, func(pixel color.RGBA) { 50 | f(pixel.B) 51 | }) 52 | } 53 | 54 | // ForEachRGBAAlphaPixel loops through the image and calls f functions for alpha component of each RGBA pixel 55 | func ForEachRGBAAlphaPixel(img *image.RGBA, f func(r uint8)) { 56 | ForEachRGBAPixel(img, func(pixel color.RGBA) { 57 | f(pixel.A) 58 | }) 59 | } 60 | 61 | // ClampInt returns min if value is lesser then min, max if value is greater them max or value if the input value is 62 | // between min and max. 63 | func ClampInt(value int, min int, max int) int { 64 | if value < min { 65 | return min 66 | } else if value > max { 67 | return max 68 | } 69 | return value 70 | } 71 | 72 | // ClampF64 returns min if value is lesser then min, max if value is greater them max or value if the input value is 73 | // between min and max. 74 | func ClampF64(value float64, min float64, max float64) float64 { 75 | if value < min { 76 | return min 77 | } else if value > max { 78 | return max 79 | } 80 | return value 81 | } 82 | 83 | // GetMax returns the maximum value from a slice 84 | func GetMax(v []uint64) uint64 { 85 | max := v[0] 86 | for _, value := range v { 87 | if max < value { 88 | max = value 89 | } 90 | } 91 | return max 92 | } 93 | -------------------------------------------------------------------------------- /utils/parallelhelpers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "math" 6 | "runtime" 7 | "sync" 8 | ) 9 | 10 | // ParallelForEachPixel loops through the image and calls f functions for each [x, y] position. 11 | // The image is divided into N * N blocks, where N is the number of available processor threads. For each block a 12 | // parallel Goroutine is started. 13 | func ParallelForEachPixel(size image.Point, f func(x int, y int)) { 14 | procs := runtime.GOMAXPROCS(0) 15 | var waitGroup sync.WaitGroup 16 | for i := 0; i < procs; i++ { 17 | startX := i * int(math.Floor(float64(size.X)/float64(procs))) 18 | var endX int 19 | if i < procs-1 { 20 | endX = (i + 1) * int(math.Floor(float64(size.X)/float64(procs))) 21 | } else { 22 | endX = size.X 23 | } 24 | for j := 0; j < procs; j++ { 25 | startY := j * int(math.Floor(float64(size.Y)/float64(procs))) 26 | var endY int 27 | if j < procs-1 { 28 | endY = (j + 1) * int(math.Floor(float64(size.Y)/float64(procs))) 29 | } else { 30 | endY = size.Y 31 | } 32 | waitGroup.Add(1) 33 | go func(sX int, eX int, sY int, eY int) { 34 | defer waitGroup.Done() 35 | for x := sX; x < eX; x++ { 36 | for y := sY; y < eY; y++ { 37 | f(x, y) 38 | } 39 | } 40 | }(startX, endX, startY, endY) 41 | waitGroup.Wait() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /utils/parallelhelpers_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func Test_ParallelForEachPixel(t *testing.T) { 9 | const ( 10 | N = 777 11 | M = 999 12 | ) 13 | expected := [N][M]int{} 14 | for i := 0; i < N; i++ { 15 | for j := 0; j < M; j++ { 16 | expected[i][j] = i + j 17 | } 18 | } 19 | actual := [N][M]int{} 20 | ParallelForEachPixel(image.Point{X: N, Y: M}, func(x int, y int) { 21 | actual[x][y] = x + y 22 | }) 23 | for i := 0; i < N; i++ { 24 | for j := 0; j < M; j++ { 25 | if expected[i][j] != actual[i][j] { 26 | t.Errorf("Expected gray: %d - actual gray: %d at: %d %d", expected[i][j], actual[i][j], i, j) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/test_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | /* 4 | Utility functions used for testing, do not use this in the production code. 5 | */ 6 | import ( 7 | "fmt" 8 | "image" 9 | "math" 10 | "testing" 11 | ) 12 | 13 | // CompareGrayImages Compares two Gray images and prints out if there is a difference between the pixels 14 | func CompareGrayImages(t *testing.T, expected *image.Gray, actual *image.Gray) { 15 | expectedSize := expected.Bounds().Size() 16 | actualSize := actual.Bounds().Size() 17 | if !expectedSize.Eq(actualSize) { 18 | t.Fatalf("expected (size: %d %d) and actual (size: %d %d) have different sizes:", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 19 | } 20 | for x := 0; x < expected.Bounds().Size().X; x++ { 21 | for y := 0; y < expected.Bounds().Size().Y; y++ { 22 | c1 := expected.GrayAt(x, y) 23 | c2 := actual.GrayAt(x, y) 24 | if c1.Y != c2.Y { 25 | t.Errorf("Expected gray: %d - actual gray: %d at: %d %d", c1.Y, c2.Y, y, x) 26 | } 27 | } 28 | } 29 | } 30 | 31 | // CompareGrayImagesWithOffset Compares two Gray images within a given interval (pixel +/- offset) and prints out if there is a difference between the pixels 32 | func CompareGrayImagesWithOffset(t *testing.T, expected *image.Gray, actual *image.Gray, offset uint16) { 33 | expectedSize := expected.Bounds().Size() 34 | actualSize := actual.Bounds().Size() 35 | if !expectedSize.Eq(actualSize) { 36 | t.Fatalf("expected (size: %d %d) and actual (size: %d %d) have different sizes:", expectedSize.X, expectedSize.Y, actualSize.X, actualSize.Y) 37 | } 38 | for x := 0; x < expected.Bounds().Size().X; x++ { 39 | for y := 0; y < expected.Bounds().Size().Y; y++ { 40 | c1 := expected.GrayAt(x, y) 41 | c2 := actual.GrayAt(x, y) 42 | if uint16(c1.Y) >= uint16(c2.Y)-offset && uint16(c1.Y) <= uint16(c2.Y)+offset { 43 | continue 44 | } else { 45 | t.Errorf("Expected gray: %d - actual gray: %d at: %d %d", c1.Y, c2.Y, y, x) 46 | } 47 | } 48 | } 49 | } 50 | 51 | // CompareRGBAImages Compares two RGBA images and prints out if there is a difference between the pixels 52 | func CompareRGBAImages(t *testing.T, expected *image.RGBA, actual *image.RGBA) { 53 | if !expected.Bounds().Size().Eq(actual.Bounds().Size()) { 54 | t.Fatal("img1 and img2 have different sizes!") 55 | } 56 | for x := 0; x < expected.Bounds().Size().X; x++ { 57 | for y := 0; y < expected.Bounds().Size().Y; y++ { 58 | c1 := expected.RGBAAt(x, y) 59 | c2 := actual.RGBAAt(x, y) 60 | if c1.R != c2.R { 61 | t.Errorf("Expected red: %d - actual red: %d at: %d %d", c1.R, c2.R, y, x) 62 | } 63 | if c1.G != c2.G { 64 | t.Errorf("Expected green: %d - actual green: %d at: %d %d", c1.G, c2.G, y, x) 65 | } 66 | if c1.B != c2.B { 67 | t.Errorf("Expected blue: %d - actual blue: %d at: %d %d", c1.B, c2.B, y, x) 68 | } 69 | if c1.A != c2.A { 70 | t.Errorf("Expected alpha: %d - actual alpha: %d at: %d %d", c1.A, c2.A, y, x) 71 | } 72 | } 73 | } 74 | } 75 | 76 | // CompareRGBAImagesWithOffset Compares two RGBA images within a given interval (pixel +/- offset) and prints out if there is a difference between the pixels 77 | func CompareRGBAImagesWithOffset(t *testing.T, expected *image.RGBA, actual *image.RGBA, offset uint16) { 78 | if !expected.Bounds().Size().Eq(actual.Bounds().Size()) { 79 | t.Fatal("img1 and img2 have different sizes!") 80 | } 81 | for x := 0; x < expected.Bounds().Size().X; x++ { 82 | for y := 0; y < expected.Bounds().Size().Y; y++ { 83 | c1 := expected.RGBAAt(x, y) 84 | c2 := actual.RGBAAt(x, y) 85 | if uint16(c1.R) < uint16(c2.R)-offset || uint16(c1.R) > uint16(c2.R)+offset { 86 | t.Errorf("Expected red: %d - actual red: %d at: %d %d", c1.R, c2.R, y, x) 87 | } 88 | if uint16(c1.G) < uint16(c2.G)-offset || uint16(c1.G) > uint16(c2.G)+offset { 89 | t.Errorf("Expected green: %d - actual green: %d at: %d %d", c1.G, c2.G, y, x) 90 | } 91 | if uint16(c1.B) < uint16(c2.B)-offset || uint16(c1.B) > uint16(c2.B)+offset { 92 | t.Errorf("Expected blue: %d - actual blue: %d at: %d %d", c1.B, c2.B, y, x) 93 | } 94 | if uint16(c1.A) < uint16(c2.A)-offset || uint16(c1.A) > uint16(c2.A)+offset { 95 | t.Errorf("Expected alpha: %d - actual alpha: %d at: %d %d", c1.A, c2.A, y, x) 96 | } 97 | } 98 | } 99 | } 100 | 101 | // PrintGray Print out gray image pixels to console 102 | func PrintGray(t *testing.T, gray *image.Gray) { 103 | size := gray.Bounds().Size() 104 | for y := 0; y < size.Y; y++ { 105 | for x := 0; x < size.X; x++ { 106 | fmt.Printf("0x%x ", gray.GrayAt(x, y).Y) 107 | } 108 | fmt.Printf("\n") 109 | } 110 | } 111 | 112 | // PrintRGBA Print out gray image pixels to console 113 | func PrintRGBA(t *testing.T, rgba *image.RGBA) { 114 | size := rgba.Bounds().Size() 115 | for y := 0; y < size.Y; y++ { 116 | for x := 0; x < size.X; x++ { 117 | fmt.Printf("0x%x ", rgba.RGBAAt(x, y)) 118 | } 119 | fmt.Printf("\n") 120 | } 121 | } 122 | 123 | // IsEqualFloat64 Compares 2 float values and returns true if they are inside of the interval of [-eps, +eps] 124 | func IsEqualFloat64(x float64, y float64) bool { 125 | eps := 0.0000001 126 | return math.Abs(x-y) <= eps 127 | } 128 | --------------------------------------------------------------------------------