Skip to content

Commit

Permalink
Optimize spatial filters (#24)
Browse files Browse the repository at this point in the history
* Add bechmarks

* Add Dilate and Erode tests

* Improve performance of sort

* Use edge extension instead of conditions in loop
  • Loading branch information
anthonynsimon authored Sep 7, 2016
1 parent b9c83dd commit c86db79
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 30 deletions.
33 changes: 11 additions & 22 deletions effect/effect.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,44 +217,33 @@ func Erode(img image.Image, radius float64) *image.RGBA {
// 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 radius <= 0 {
return src
return clone.AsRGBA(img)
}

padding := int(radius + 0.5)
src := clone.Pad(img, padding, padding, clone.EdgeExtend)

kernelSize := int(2*radius + 1 + 0.5)

bounds := img.Bounds()
dst := image.NewRGBA(bounds)

w, h := bounds.Dx(), bounds.Dy()
neighborsCount := kernelSize * kernelSize

parallel.Line(h, func(start, end int) {
for y := start; y < end; y++ {
for x := 0; x < w; x++ {
for y := start + padding; y < end+padding; y++ {
for x := padding; x < w+padding; x++ {

neighbors := make([]color.RGBA, neighborsCount)
i := 0
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
} else if ix >= w {
ix = w - 1
}

if iy < 0 {
iy = 0
} else if iy >= h {
iy = h - 1
}
ix := x - kernelSize>>1 + kx
iy := y - kernelSize>>1 + ky

ipos := iy*dst.Stride + ix*4
ipos := iy*src.Stride + ix*4
neighbors[i] = color.RGBA{
R: src.Pix[ipos+0],
G: src.Pix[ipos+1],
Expand All @@ -267,7 +256,7 @@ func spatialFilter(img image.Image, radius float64, pickerFn func(neighbors []co

c := pickerFn(neighbors)

pos := y*dst.Stride + x*4
pos := (y-padding)*dst.Stride + (x-padding)*4
dst.Pix[pos+0] = c.R
dst.Pix[pos+1] = c.G
dst.Pix[pos+2] = c.B
Expand Down
181 changes: 180 additions & 1 deletion effect/effect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,165 @@ func TestMedian(t *testing.T) {
for _, c := range cases {
actual := Median(c.value, c.radius)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Sobel", util.RGBAToString(c.expected), util.RGBAToString(actual))
t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Median", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}

func TestDilate(t *testing.T) {
cases := []struct {
radius float64
value image.Image
expected *image.RGBA
}{
{
radius: 0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
},
},
},
{
radius: 1,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x40, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
},
},
},
{
radius: 2,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x40, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x0, 0xFF, 0x00, 0xFF, 0x0, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x0, 0xFF, 0x00, 0xFF, 0x0, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x0, 0xFF, 0x00, 0xFF, 0x0, 0xFF,
},
},
},
}

for _, c := range cases {
actual := Dilate(c.value, c.radius)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Dilate", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}

func TestErode(t *testing.T) {
cases := []struct {
radius float64
value image.Image
expected *image.RGBA
}{
{
radius: 0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
},
},
},
{
radius: 1,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x20, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x40, 0x00, 0x00, 0xFF, 0x40, 0x40, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0x40, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0x40, 0x00, 0x00, 0xFF, 0x20, 0x00, 0x00, 0xFF, 0x20, 0x00, 0x00, 0xFF,
},
},
},
{
radius: 2,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0x40, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0xFF, 0x60, 0x60, 0x60, 0xFF, 0x60, 0x60, 0x60, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
},
}

for _, c := range cases {
actual := Erode(c.value, c.radius)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected:%v\nactual:%v\n", "Erode", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}
Expand Down Expand Up @@ -488,3 +646,24 @@ func TestSharpen(t *testing.T) {
}
}
}

func BenchmarkMedian1(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
for n := 0; n < b.N; n++ {
Median(img, 1)
}
}

func BenchmarkMedian4(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
for n := 0; n < b.N; n++ {
Median(img, 4)
}
}

func BenchmarkMedian8(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
for n := 0; n < b.N; n++ {
Median(img, 8)
}
}
15 changes: 8 additions & 7 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ func SortRGBA(data []color.RGBA, min, max int) {
func partitionRGBASlice(data []color.RGBA, min, max int) int {
pivot := data[max]
i := min
r := srank(pivot)
for j := min; j < max; j++ {
if Rank(data[j]) <= Rank(pivot) {
temp := data[i]
data[i] = data[j]
data[j] = temp
if srank(data[j]) <= r {
data[i], data[j] = data[j], data[i]
i++
}
}
temp := data[i]
data[i] = data[max]
data[max] = temp
data[i], data[max] = data[max], data[i]
return i
}

func srank(c color.RGBA) uint {
return uint(c.R)<<3 + uint(c.G)<<6 + uint(c.B)<<1
}

// Rank a color based on a color perception heuristic.
func Rank(c color.RGBA) float64 {
return float64(c.R)*0.3 + float64(c.G)*0.6 + float64(c.B)*0.1
Expand Down

0 comments on commit c86db79

Please sign in to comment.