diff --git a/effect/effect.go b/effect/effect.go index 051a461..0417b54 100644 --- a/effect/effect.go +++ b/effect/effect.go @@ -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], @@ -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 diff --git a/effect/effect_test.go b/effect/effect_test.go index c663c6c..ff85e34 100644 --- a/effect/effect_test.go +++ b/effect/effect_test.go @@ -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)) } } } @@ -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) + } +} diff --git a/util/util.go b/util/util.go index 253968b..9f13f99 100644 --- a/util/util.go +++ b/util/util.go @@ -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