From fb163851cff40b76fb36a8494bb7871f71a38e08 Mon Sep 17 00:00:00 2001 From: "Anthony N. Simon" Date: Thu, 1 Sep 2016 00:27:40 +0200 Subject: [PATCH] Extract neighbors (#18) * Extract get neighbors operation * Add Dilate and Erode functions * Refactor and add doc * Switch to radius based spatial filters * Add dilate and erode to README --- README.md | 11 ++++++- effect/effect.go | 75 +++++++++++++++++++++++++++++++++++-------- effect/effect_test.go | 14 ++++---- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1dc5d29..8905970 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,11 @@ func main() { ## Effect import "github.com/anthonynsimon/bild/effect" +### Dilate + result := effect.Dilate(img, 3) + +![example](https://anthonynsimon.github.io/projects/bild/dilate.jpg) + ### Edge Detection result := effect.EdgeDetection(img, 1.0) @@ -147,6 +152,11 @@ func main() { ![example](https://anthonynsimon.github.io/projects/bild/emboss.jpg) +### Erode + result := effect.Erode(img, 3) + +![example](https://anthonynsimon.github.io/projects/bild/erode.jpg) + ### Grayscale result := effect.Grayscale(img) @@ -172,7 +182,6 @@ func main() { ![example](https://anthonynsimon.github.io/projects/bild/sharpen.jpg) - ### Sobel result := effect.Sobel(img) diff --git a/effect/effect.go b/effect/effect.go index 914f3dc..1a7665f 100644 --- a/effect/effect.go +++ b/effect/effect.go @@ -169,19 +169,67 @@ func Sobel(src image.Image) *image.RGBA { } // Median returns a new image in which each pixel is the median of it's neighbors. -// Size sets the amount of neighbors to be searched. -func Median(img image.Image, size int) *image.RGBA { +// The parameter radius corresponds to the radius of the neighbor area to be searched, +// for example a radius of R will result in a search window length of 2R+1 for each dimension. +func Median(img image.Image, radius float64) *image.RGBA { + fn := func(neighbors []color.RGBA) color.RGBA { + util.SortRGBA(neighbors, 0, len(neighbors)-1) + return neighbors[len(neighbors)/2] + } + + result := spatialFilter(img, radius, fn) + + return result +} + +// Dilate picks the local maxima from the neighbors of each pixel and returns the resulting image. +// The parameter radius corresponds to the radius of the neighbor area to be searched, +// for example a radius of R will result in a search window length of 2R+1 for each dimension. +func Dilate(img image.Image, radius float64) *image.RGBA { + fn := func(neighbors []color.RGBA) color.RGBA { + util.SortRGBA(neighbors, 0, len(neighbors)-1) + return neighbors[len(neighbors)-1] + } + + result := spatialFilter(img, radius, fn) + + return result +} + +// Erode picks the local minima from the neighbors of each pixel and returns the resulting image. +// The parameter radius corresponds to the radius of the neighbor area to be searched, +// for example a radius of R will result in a search window length of 2R+1 for each dimension. +func Erode(img image.Image, radius float64) *image.RGBA { + fn := func(neighbors []color.RGBA) color.RGBA { + util.SortRGBA(neighbors, 0, len(neighbors)-1) + return neighbors[0] + } + + result := spatialFilter(img, radius, fn) + + return result +} + +// spatialFilter goes through each pixel on an image collecting it's neighbors and picking one +// based on the function provided. The resulting image is then returned. +// The parameter radius corresponds to the radius of the neighbor area to be searched, +// for example a radius of R will result in a search window length of 2R+1 for each dimension. +// The parameter pickerFn is the function that receives the list of neighbors and returns the selected +// neighbor to be used for the resulting image. +func spatialFilter(img image.Image, radius float64, pickerFn func(neighbors []color.RGBA) color.RGBA) *image.RGBA { bounds := img.Bounds() src := clone.AsRGBA(img) - if size <= 0 { + if radius <= 0 { return src } + kernelSize := int(2*radius + 1 + 0.5) + dst := image.NewRGBA(bounds) w, h := bounds.Dx(), bounds.Dy() - neighborsCount := size * size + neighborsCount := kernelSize * kernelSize parallel.Line(h, func(start, end int) { for y := start; y < end; y++ { @@ -189,10 +237,10 @@ func Median(img image.Image, size int) *image.RGBA { neighbors := make([]color.RGBA, neighborsCount) i := 0 - for ky := 0; ky < size; ky++ { - for kx := 0; kx < size; kx++ { - ix := x - size/2 + kx - iy := y - size/2 + ky + for ky := 0; ky < kernelSize; ky++ { + for kx := 0; kx < kernelSize; kx++ { + ix := x - kernelSize/2 + kx + iy := y - kernelSize/2 + ky if ix < 0 { ix = 0 @@ -217,14 +265,13 @@ func Median(img image.Image, size int) *image.RGBA { } } - util.SortRGBA(neighbors, 0, neighborsCount-1) - median := neighbors[neighborsCount/2] + c := pickerFn(neighbors) pos := y*dst.Stride + x*4 - dst.Pix[pos+0] = median.R - dst.Pix[pos+1] = median.G - dst.Pix[pos+2] = median.B - dst.Pix[pos+3] = median.A + dst.Pix[pos+0] = c.R + dst.Pix[pos+1] = c.G + dst.Pix[pos+2] = c.B + dst.Pix[pos+3] = c.A } } }) diff --git a/effect/effect_test.go b/effect/effect_test.go index 29320b6..715ea2a 100644 --- a/effect/effect_test.go +++ b/effect/effect_test.go @@ -337,12 +337,12 @@ func TestEmboss(t *testing.T) { func TestMedian(t *testing.T) { cases := []struct { - size int + radius float64 value image.Image expected *image.RGBA }{ { - size: 0, + radius: 0, value: &image.RGBA{ Rect: image.Rect(0, 0, 3, 3), Stride: 3 * 4, @@ -363,7 +363,7 @@ func TestMedian(t *testing.T) { }, }, { - size: 3, + radius: 1, value: &image.RGBA{ Rect: image.Rect(0, 0, 3, 3), Stride: 3 * 4, @@ -384,7 +384,7 @@ func TestMedian(t *testing.T) { }, }, { - size: 2, + radius: 2, value: &image.RGBA{ Rect: image.Rect(0, 0, 3, 3), Stride: 3 * 4, @@ -400,16 +400,16 @@ func TestMedian(t *testing.T) { Pix: []uint8{ 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, - 0x0, 0xFF, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, + 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, }, }, }, } for _, c := range cases { - actual := Median(c.value, c.size) + actual := Median(c.value, c.radius) if !util.RGBAImageEqual(actual, c.expected) { - t.Errorf("%s: expected: %#v, actual: %#v", "Sobel", util.RGBAToString(c.expected), util.RGBAToString(actual)) + t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Sobel", util.RGBAToString(c.expected), util.RGBAToString(actual)) } } }