Skip to content

Commit

Permalink
Extract neighbors (#18)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
anthonynsimon committed Aug 31, 2016
1 parent 1817a6b commit fb16385
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 22 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -172,7 +182,6 @@ func main() {

![example](https://anthonynsimon.github.io/projects/bild/sharpen.jpg)


### Sobel
result := effect.Sobel(img)

Expand Down
75 changes: 61 additions & 14 deletions effect/effect.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,30 +169,78 @@ 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++ {
for x := 0; x < w; x++ {

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
Expand All @@ -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
}
}
})
Expand Down
14 changes: 7 additions & 7 deletions effect/effect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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))
}
}
}
Expand Down

0 comments on commit fb16385

Please sign in to comment.