package imaging import ( "image" "testing" ) func TestConvolve3x3(t *testing.T) { testCases := []struct { name 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 { t.Run(tc.name, func(t *testing.T) { got := Convolve3x3(tc.src, tc.kernel, tc.options) if !compareNRGBA(got, tc.want, 0) { t.Fatalf("got result %#v want %#v", got, tc.want) } }) } } func TestConvolve5x5(t *testing.T) { testCases := []struct { name 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 { t.Run(tc.name, func(t *testing.T) { got := Convolve5x5(tc.src, tc.kernel, tc.options) if !compareNRGBA(got, tc.want, 0) { t.Fatalf("got result %#v want %#v", got, tc.want) } }) } } func TestNormalizeKernel(t *testing.T) { testCases := []struct { name string kernel []float64 want []float64 }{ { name: "positive sum", kernel: []float64{ 2, 0, 2, 0, 2, 0, 2, 0, 2, }, want: []float64{ 0.2, 0, 0.2, 0, 0.2, 0, 0.2, 0, 0.2, }, }, { name: "negative sum", kernel: []float64{ -2, 0, -2, 2, 2, 2, -2, 0, -2, }, want: []float64{ 1, 0, 1, -1, -1, -1, 1, 0, 1, }, }, { name: "zero sum", kernel: []float64{ 0, 2, 0, 2, 0, -2, 0, -2, 0, }, want: []float64{ 0, 0.5, 0, 0.5, 0, -0.5, 0, -0.5, 0, }, }, { name: "all zero", kernel: []float64{ 0, 0, 0, 0, 0, 0, 0, 0, 0, }, want: []float64{ 0, 0, 0, 0, 0, 0, 0, 0, 0, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { normalizeKernel(tc.kernel) for i := range tc.kernel { if tc.kernel[i] != tc.want[i] { t.Fatalf("got kernel %v want %v", tc.kernel, tc.want) } } }) } } func BenchmarkConvolve3x3(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { Convolve3x3( testdataBranchesJPG, [9]float64{ -1, -1, 0, -1, 0, 1, 0, 1, 1, }, nil, ) } } func BenchmarkConvolve5x5(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { Convolve5x5( testdataBranchesJPG, [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, ) } }