1
0
Эх сурвалжийг харах

add convolution functions

disintegration 8 жил өмнө
parent
commit
90156c85f3
2 өөрчлөгдсөн 428 нэмэгдсэн , 0 устгасан
  1. 148 0
      convolution.go
  2. 280 0
      convolution_test.go

+ 148 - 0
convolution.go

@@ -0,0 +1,148 @@
+package imaging
+
+import (
+	"image"
+)
+
+// ConvolveOptions are convolution parameters.
+type ConvolveOptions struct {
+	// If Normalize is true the kernel is normalized before convolution.
+	Normalize bool
+
+	// If Abs is true the absolute value of each color channel is taken after convolution.
+	Abs bool
+
+	// Bias is added to each color channel value after convolution.
+	Bias int
+}
+
+// Convolve3x3 convolves the image with the specified 3x3 convolution kernel.
+// Default parameters are used if a nil *ConvolveOptions is passed.
+func Convolve3x3(img image.Image, kernel [9]float64, options *ConvolveOptions) *image.NRGBA {
+	return convolve(img, kernel[:], options)
+}
+
+// Convolve5x5 convolves the image with the specified 5x5 convolution kernel.
+// Default parameters are used if a nil *ConvolveOptions is passed.
+func Convolve5x5(img image.Image, kernel [25]float64, options *ConvolveOptions) *image.NRGBA {
+	return convolve(img, kernel[:], options)
+}
+
+func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *image.NRGBA {
+	src := toNRGBA(img)
+	w := src.Bounds().Max.X
+	h := src.Bounds().Max.Y
+	dst := image.NewNRGBA(image.Rect(0, 0, w, h))
+
+	if w < 1 || h < 1 {
+		return dst
+	}
+
+	if options == nil {
+		options = &ConvolveOptions{}
+	}
+
+	if options.Normalize {
+		normalizeKernel(kernel)
+	}
+
+	type coef struct {
+		x, y int
+		k    float64
+	}
+	var coefs []coef
+	var m int
+
+	switch len(kernel) {
+	case 9:
+		m = 1
+	case 25:
+		m = 2
+	default:
+		return dst
+	}
+
+	i := 0
+	for y := -m; y <= m; y++ {
+		for x := -m; x <= m; x++ {
+			if kernel[i] != 0 {
+				coefs = append(coefs, coef{x: x, y: y, k: kernel[i]})
+			}
+			i++
+		}
+	}
+
+	parallel(h, func(partStart, partEnd int) {
+		for y := partStart; y < partEnd; y++ {
+			for x := 0; x < w; x++ {
+				var r, g, b float64
+				for _, c := range coefs {
+					ix := x + c.x
+					if ix < 0 {
+						ix = 0
+					} else if ix >= w {
+						ix = w - 1
+					}
+
+					iy := y + c.y
+					if iy < 0 {
+						iy = 0
+					} else if iy >= h {
+						iy = h - 1
+					}
+
+					off := iy*src.Stride + ix*4
+					r += float64(src.Pix[off+0]) * c.k
+					g += float64(src.Pix[off+1]) * c.k
+					b += float64(src.Pix[off+2]) * c.k
+				}
+
+				if options.Abs {
+					if r < 0 {
+						r = -r
+					}
+					if g < 0 {
+						g = -g
+					}
+					if b < 0 {
+						b = -b
+					}
+				}
+
+				if options.Bias != 0 {
+					r += float64(options.Bias)
+					g += float64(options.Bias)
+					b += float64(options.Bias)
+				}
+
+				srcOff := y*src.Stride + x*4
+				dstOff := y*dst.Stride + x*4
+				dst.Pix[dstOff+0] = clamp(r)
+				dst.Pix[dstOff+1] = clamp(g)
+				dst.Pix[dstOff+2] = clamp(b)
+				dst.Pix[dstOff+3] = src.Pix[srcOff+3]
+			}
+		}
+	})
+
+	return dst
+}
+
+func normalizeKernel(kernel []float64) {
+	var sum, sumpos float64
+	for i := range kernel {
+		sum += kernel[i]
+		if kernel[i] > 0 {
+			sumpos += kernel[i]
+		}
+	}
+	if sum != 0 {
+		for i := range kernel {
+			kernel[i] /= sum
+		}
+	} else if sumpos != 0 {
+		for i := range kernel {
+			kernel[i] /= sumpos
+		}
+	}
+}

+ 280 - 0
convolution_test.go

@@ -0,0 +1,280 @@
+package imaging
+
+import (
+	"image"
+	"image/color"
+	"testing"
+)
+
+func TestConvolve3x3(t *testing.T) {
+	testCases := []struct {
+		desc    string
+		src     image.Image
+		kernel  [9]float64
+		options *ConvolveOptions
+		want    *image.NRGBA
+	}{
+		{
+			"Convolve3x3 0x0",
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 0, 0),
+				Stride: 0,
+				Pix:    []uint8{},
+			},
+			[9]float64{
+				0, 0, 0,
+				0, 1, 0,
+				0, 0, 0,
+			},
+			nil,
+			&image.NRGBA{Rect: image.Rect(0, 0, 0, 0)},
+		},
+		{
+			"Convolve3x3 4x4 identity",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+			[9]float64{
+				0, 0, 0,
+				0, 1, 0,
+				0, 0, 0,
+			},
+			nil,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+		},
+		{
+			"Convolve3x3 4x4 abs",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+			[9]float64{
+				0, 0, 0,
+				0, -1, 0,
+				0, 0, 0,
+			},
+			&ConvolveOptions{Abs: true},
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+		},
+		{
+			"Convolve3x3 4x4 bias",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+			[9]float64{
+				0, 0, 0,
+				0, 1, 0,
+				0, 0, 0,
+			},
+			&ConvolveOptions{Bias: 0x10},
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x03, 0x14, 0x15, 0x16, 0x07, 0x18, 0x19, 0x1a, 0x0b, 0x1c, 0x1d, 0x1e, 0x0f,
+					0x20, 0x21, 0x22, 0x13, 0x24, 0x25, 0x26, 0x17, 0x28, 0x29, 0x2a, 0x1b, 0x2c, 0x2d, 0x2e, 0x1f,
+					0x30, 0x31, 0x32, 0x23, 0x34, 0x35, 0x36, 0x27, 0x38, 0x39, 0x3a, 0x2b, 0x3c, 0x3d, 0x3e, 0x2f,
+					0x40, 0x41, 0x42, 0x33, 0x44, 0x45, 0x46, 0x37, 0x48, 0x49, 0x4a, 0x3b, 0x4c, 0x4d, 0x4e, 0x3f,
+				},
+			},
+		},
+		{
+			"Convolve3x3 4x4 norm",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+			[9]float64{
+				1, 1, 1,
+				1, 1, 1,
+				1, 1, 1,
+			},
+			&ConvolveOptions{Normalize: true},
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x07, 0x08, 0x09, 0x03, 0x09, 0x0a, 0x0b, 0x07, 0x0d, 0x0e, 0x0f, 0x0b, 0x10, 0x11, 0x12, 0x0f,
+					0x11, 0x12, 0x13, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1b, 0x1c, 0x1d, 0x1f,
+					0x21, 0x22, 0x23, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2b, 0x2c, 0x2d, 0x2f,
+					0x2c, 0x2d, 0x2e, 0x33, 0x2f, 0x30, 0x31, 0x37, 0x33, 0x34, 0x35, 0x3b, 0x35, 0x36, 0x37, 0x3f,
+				},
+			},
+		},
+		{
+			"Convolve3x3 3x3 laplacian",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x01, 0xff, 0x00, 0x01, 0x02, 0xff, 0x00, 0x01, 0x03, 0xff,
+					0x00, 0x01, 0x04, 0xff, 0x10, 0x10, 0x10, 0xff, 0x00, 0x01, 0x05, 0xff,
+					0x00, 0x01, 0x06, 0xff, 0x00, 0x01, 0x07, 0xff, 0x00, 0x01, 0x08, 0xff,
+				},
+			},
+			[9]float64{
+				-1, -1, -1,
+				-1, 8, -1,
+				-1, -1, -1,
+			},
+			nil,
+			&image.NRGBA{
+				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, 0x80, 0x78, 0x5c, 0xff, 0x00, 0x00, 0x00, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		got := Convolve3x3(tc.src, tc.kernel, tc.options)
+		want := tc.want
+		if !compareNRGBA(got, want, 0) {
+			t.Errorf("test [%s] failed: want %#v got %#v", tc.desc, want, got)
+		}
+	}
+}
+
+func TestConvolve5x5(t *testing.T) {
+	testCases := []struct {
+		desc    string
+		src     image.Image
+		kernel  [25]float64
+		options *ConvolveOptions
+		want    *image.NRGBA
+	}{
+		{
+			"Convolve5x5 4x4 translate",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+			[25]float64{
+				0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0,
+				0, 0, 0, 0, 0,
+				0, 0, 0, 0, 1,
+			},
+			nil,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0x28, 0x29, 0x2a, 0x03, 0x2c, 0x2d, 0x2e, 0x07, 0x2c, 0x2d, 0x2e, 0x0b, 0x2c, 0x2d, 0x2e, 0x0f,
+					0x38, 0x39, 0x3a, 0x13, 0x3c, 0x3d, 0x3e, 0x17, 0x3c, 0x3d, 0x3e, 0x1b, 0x3c, 0x3d, 0x3e, 0x1f,
+					0x38, 0x39, 0x3a, 0x23, 0x3c, 0x3d, 0x3e, 0x27, 0x3c, 0x3d, 0x3e, 0x2b, 0x3c, 0x3d, 0x3e, 0x2f,
+					0x38, 0x39, 0x3a, 0x33, 0x3c, 0x3d, 0x3e, 0x37, 0x3c, 0x3d, 0x3e, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		got := Convolve5x5(tc.src, tc.kernel, tc.options)
+		want := tc.want
+		if !compareNRGBA(got, want, 0) {
+			t.Errorf("test [%s] failed: want %#v got %#v", tc.desc, want, got)
+		}
+	}
+}
+
+func BenchmarkConvolve3x3(b *testing.B) {
+	r := image.Rect(0, 0, 500, 500)
+	img := image.NewRGBA(r)
+	for x := r.Min.X; x < r.Max.X; x++ {
+		for y := r.Min.Y; y < r.Max.Y; y++ {
+			img.SetRGBA(x, y, color.RGBA{uint8(x % 255), uint8(y % 255), uint8((x + y) % 255), 255})
+		}
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		Convolve3x3(
+			img,
+			[9]float64{
+				-1, -1, 0,
+				-1, 0, 1,
+				0, 1, 1,
+			},
+			nil,
+		)
+	}
+}
+
+func BenchmarkConvolve5x5(b *testing.B) {
+	r := image.Rect(0, 0, 500, 500)
+	img := image.NewRGBA(r)
+	for x := r.Min.X; x < r.Max.X; x++ {
+		for y := r.Min.Y; y < r.Max.Y; y++ {
+			img.SetRGBA(x, y, color.RGBA{uint8(x % 255), uint8(y % 255), uint8((x + y) % 255), 255})
+		}
+	}
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		Convolve5x5(
+			img,
+			[25]float64{
+				-1, -1, -1, -1, 0,
+				-1, -1, -1, 0, 1,
+				-1, -1, 0, 1, 1,
+				-1, 0, 1, 1, 1,
+				0, 1, 1, 1, 1,
+			},
+			nil,
+		)
+	}
+}