Skip to content

Commit

Permalink
Merge pull request #71 from angeloskath/fast-gaussian-blur
Browse files Browse the repository at this point in the history
Make gaussian blur using separable convolution
  • Loading branch information
anthonynsimon authored Jul 31, 2019
2 parents 244b621 + f4dba95 commit 9db0319
Showing 5 changed files with 119 additions and 24 deletions.
27 changes: 13 additions & 14 deletions blur/blur.go
Original file line number Diff line number Diff line change
@@ -35,19 +35,18 @@ func Gaussian(src image.Image, radius float64) *image.RGBA {
return clone.AsRGBA(src)
}

// Create the 1-d gaussian kernel
length := int(math.Ceil(2*radius + 1))
k := convolution.NewKernel(length, length)

gaussianFn := func(x, y, sigma float64) float64 {
return math.Exp(-(x*x/sigma + y*y/sigma))
}

for x := 0; x < length; x++ {
for y := 0; y < length; y++ {
k.Matrix[y*length+x] = gaussianFn(float64(x)-radius, float64(y)-radius, 4*radius)

}
}

return convolution.Convolve(src, k.Normalized(), &convolution.Options{Bias: 0, Wrap: false, KeepAlpha: false})
k := convolution.NewKernel(length, 1)
for i, x := 0, -radius; i < length; i, x = i+1, x+1 {
k.Matrix[i] = math.Exp(-(x*x/4/radius))
}
normK := k.Normalized()

// Perform separable convolution
options := convolution.Options{Bias: 0, Wrap: false, KeepAlpha: false}
result := convolution.Convolve(src, normK, &options)
result = convolution.Convolve(result, normK.Transposed(), &options)

return result
}
10 changes: 5 additions & 5 deletions blur/blur_test.go
Original file line number Diff line number Diff line change
@@ -128,9 +128,9 @@ func TestGaussianBlur(t *testing.T) {
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xb1, 0xb1, 0xb1, 0xff, 0xbd, 0xbd, 0xbd, 0xff, 0xcc, 0xcc, 0xcc, 0xff,
0x4d, 0x4d, 0x4d, 0xff, 0x68, 0x68, 0x68, 0xff, 0x8b, 0x8b, 0x8b, 0xff,
0x0, 0x0, 0x0, 0xff, 0x26, 0x26, 0x26, 0xff, 0x59, 0x59, 0x59, 0xff,
0xb1, 0xb1, 0xb1, 0xff, 0xbc, 0xbc, 0xbc, 0xff, 0xcc, 0xcc, 0xcc, 0xff,
0x4d, 0x4d, 0x4d, 0xff, 0x68, 0x68, 0x68, 0xff, 0x8b, 0x8b, 0x8b, 0xff,
0x0, 0x0, 0x0, 0xff, 0x25, 0x25, 0x25, 0xff, 0x58, 0x58, 0x58, 0xff,
},
},
},
@@ -157,10 +157,10 @@ func TestGaussianBlur(t *testing.T) {
},
}

for _, c := range cases {
for i, c := range cases {
actual := Gaussian(c.value, c.radius)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s: expected: %#v, actual: %#v", "GaussianBlur", c.expected, actual)
t.Errorf("%s %d: expected: %#v, actual: %#v", "GaussianBlur", i, c.expected, actual)
}
}
}
16 changes: 16 additions & 0 deletions convolution/kernel.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ type Matrix interface {
Normalized() Matrix
MaxX() int
MaxY() int
Transposed() Matrix
}

// NewKernel returns a kernel of the provided length.
@@ -63,6 +64,21 @@ func (k *Kernel) At(x, y int) float64 {
return k.Matrix[y*k.Width+x]
}

// Transposed returns a new Kernel that has the columns as rows and vice versa
func (k *Kernel) Transposed() Matrix {
w := k.Width;
h := k.Height;
nk := NewKernel(h, w)

for x := 0; x<w; x++ {
for y := 0; y<h; y++ {
nk.Matrix[x*h + y] = k.Matrix[y*w + x];
}
}

return nk
}

// String returns the string representation of the matrix.
func (k *Kernel) String() string {
result := ""
80 changes: 80 additions & 0 deletions convolution/kernel_test.go
Original file line number Diff line number Diff line change
@@ -261,6 +261,86 @@ func TestKernelNormalized(t *testing.T) {
}
}

func TestKernelTransposed(t *testing.T) {
cases := []struct {
desc string
kernel *Kernel
expected *Kernel
}{
{
desc: "all zero",
kernel: &Kernel{[]float64{
0, 0, 0,
0, 0, 0,
0, 0, 0,
}, 3, 3},
expected: &Kernel{[]float64{
0, 0, 0,
0, 0, 0,
0, 0, 0,
}, 3, 3},
},
{
desc: "one element",
kernel: &Kernel{[]float64{
0, 0, 0,
0, 1, 0,
0, 0, 0,
}, 3, 3},
expected: &Kernel{[]float64{
0, 0, 0,
0, 1, 0,
0, 0, 0,
}, 3, 3},
},
{
desc: "diagonal",
kernel: &Kernel{[]float64{
1, 0, 0,
0, 1, 0,
0, 0, 1,
}, 3, 3},
expected: &Kernel{[]float64{
1, 0, 0,
0, 1, 0,
0, 0, 1,
}, 3, 3},
},
{
desc: "3x2",
kernel: &Kernel{[]float64{
1, 1, 1,
0, 1, 0,
}, 3, 2},
expected: &Kernel{[]float64{
1, 0,
1, 1,
1, 0,
}, 2, 3},
},
{
desc: "5x1",
kernel: &Kernel{[]float64{
1, 1, 1, 0, 1,
}, 5, 1},
expected: &Kernel{[]float64{
1,
1,
1,
0,
1,
}, 1, 5},
},
}

for i, c := range cases {
actual := c.kernel.Transposed()
if !kernelEqual(actual.(*Kernel), c.expected) {
t.Errorf("%s case #%d: expected: %#v, actual: %#v", "KernelTransposed "+c.desc, i, c.expected, actual)
}
}
}

func TestKernelString(t *testing.T) {
cases := []struct {
kernel *Kernel
10 changes: 5 additions & 5 deletions effect/effect_test.go
Original file line number Diff line number Diff line change
@@ -816,9 +816,9 @@ func TestUnsharpMask(t *testing.T) {
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x47, 0x25, 0xFF,
0x0, 0xD2, 0x9A, 0xFF, 0xA9, 0x0, 0xFF, 0xFF, 0x36, 0x4D, 0x93, 0xFF,
0x0, 0x0, 0xDA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0xD4, 0xFF,
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x48, 0x26, 0xFF,
0x0, 0xD2, 0x9A, 0xFF, 0xAA, 0x0, 0xFF, 0xFF, 0x36, 0x4D, 0x93, 0xFF,
0x0, 0x0, 0xDA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0xD5, 0xFF,
},
},
},
@@ -838,8 +838,8 @@ func TestUnsharpMask(t *testing.T) {
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x43, 0x32, 0xFF,
0xB, 0xA9, 0x8D, 0xFF, 0xAD, 0x0, 0xFA, 0xFF, 0x5B, 0x46, 0x89, 0xFF,
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x44, 0x33, 0xFF,
0xB, 0xA9, 0x8D, 0xFF, 0xAE, 0x0, 0xFA, 0xFF, 0x5B, 0x46, 0x89, 0xFF,
0x0, 0x0, 0xC2, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0x0, 0xBF, 0xFF,
},
},

0 comments on commit 9db0319

Please sign in to comment.