From 4290dc3cae4346893940997ddeec2af2cf019cbb Mon Sep 17 00:00:00 2001 From: Mauro M Date: Fri, 8 Dec 2023 07:00:28 +0000 Subject: [PATCH] docs: document filters --- filters.go | 19 ++++++++++++----- main_test.go | 59 +++++++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/filters.go b/filters.go index a7d543e..5173e49 100644 --- a/filters.go +++ b/filters.go @@ -5,7 +5,7 @@ import ( "math" ) -// gaussianFilter applies a Gaussian filter to a matrix representing an image +// gaussianFilter applies a Gaussian filter to a matrix representing an image. func gaussianFilter(matrix [][][4]uint32, kernelSize int, sigma float64) ([][][4]uint32, error) { height := len(matrix) // Get the height of the matrix if height == 0 { @@ -13,14 +13,17 @@ func gaussianFilter(matrix [][][4]uint32, kernelSize int, sigma float64) ([][][4 } width := len(matrix[0]) + // Generate the Gaussian kernel with the given size and standard deviation (sigma). kernel := generateGaussianKernel(kernelSize, sigma) filteredMatrix := Make2D[[4]uint32](height, width) - // Apply the Gaussian kernel to each pixel + // Apply the Gaussian kernel to each pixel. + // kOffset is used to handle border effects by avoiding out-of-bounds indices. kOffset := kernelSize / 2 for y := kOffset; y < height-kOffset; y++ { for x := kOffset; x < width-kOffset; x++ { + // Apply the kernel to the pixel at (x, y) and store the result. filteredMatrix[y][x] = applyKernel(x, y, matrix, kernel, kOffset) } } @@ -28,11 +31,12 @@ func gaussianFilter(matrix [][][4]uint32, kernelSize int, sigma float64) ([][][4 return filteredMatrix, nil } -// applyKernel applies the given Guassian kernel to a single pixel +// applyKernel applies the given Gaussian kernel to a single pixel. func applyKernel(x int, y int, matrix [][][4]uint32, kernel [][]float64, kOffset int) [4]uint32 { var sum [4]float64 for ky := 0; ky < len(kernel); ky++ { for kx := 0; kx < len(kernel); kx++ { + // Multiply each kernel coefficient with the corresponding pixel value. px := matrix[y+kOffset-ky][x+kOffset-kx] for i := range px { sum[i] += float64(px[i]) * kernel[ky][kx] @@ -40,6 +44,7 @@ func applyKernel(x int, y int, matrix [][][4]uint32, kernel [][]float64, kOffset } } + // Convert the summed values back to uint32, ensuring they remain within the valid range [0, 255]. var result [4]uint32 for i := range result { result[i] = uint32(math.Min(math.Max(sum[i], 0), 255)) @@ -47,13 +52,16 @@ func applyKernel(x int, y int, matrix [][][4]uint32, kernel [][]float64, kOffset return result } -// generateGaussianKernel generates a Gaussian kernel +// generateGaussianKernel generates a Gaussian kernel for image blurring. +// The Gaussian kernel is a square matrix used for the blurring effect. func generateGaussianKernel(size int, sigma float64) [][]float64 { kernel := Make2D[float64](size, size) sum := 0.0 offset := size / 2 + // Fill the kernel with values computed using the Gaussian function. + // The kernel values are based on the distance from the center, modulated by the sigma (standard deviation). for y := -offset; y <= offset; y++ { for x := -offset; x <= offset; x++ { val := (1.0 / (2.0 * math.Pi * sigma * sigma)) * math.Exp(-(float64(x*x+y*y) / (2.0 * sigma * sigma))) @@ -62,7 +70,8 @@ func generateGaussianKernel(size int, sigma float64) [][]float64 { } } - // Normalize the kernel + // Normalize the kernel so that the sum of all its values equals 1. + // This ensures that applying the kernel to an image preserves the image's brightness. for y := range kernel { for x := range kernel[y] { kernel[y][x] /= sum diff --git a/main_test.go b/main_test.go index c7f94e2..cf9e551 100644 --- a/main_test.go +++ b/main_test.go @@ -34,9 +34,11 @@ func assertColourEquality(colour1 color.Color, colour2 color.Color) bool { return r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2 } -// generateRandomImage generates a random image of a given width and height. -func generateRandomImage(width, height int) [][][4]uint32 { +// generateRandomImage generates a random valid image of a given width and height. +func generateRandomImage(width int, height int) [][][4]uint32 { img := Make2D[[4]uint32](height, width) + + // Iterate through all elements of the list and assign it to a random array with 4 random values between 0-255 for y := 0; y < height; y++ { for x := 0; x < width; x++ { img[y][x] = [4]uint32{ @@ -50,11 +52,13 @@ func generateRandomImage(width, height int) [][][4]uint32 { return img } +// assertValidMatrix asserts that a given "matrix" is valid, based only on the RGBA values being within the valid interval func assertValidMatrix(matrix [][][4]uint32) error { + // Iterate through all the elements of the matrix for y := range matrix { for x := range matrix[y] { pixel := matrix[y][x] - if pixel[0] > 255 || pixel[1] > 255 || pixel[2] > 255 || pixel[3] > 255 { + if pixel[0] > 255 || pixel[1] > 255 || pixel[2] > 255 || pixel[3] > 255 { // If any of the values are over 255, raise an error return errors.New("invalid matrix: values exceed 255") } } @@ -65,7 +69,7 @@ func assertValidMatrix(matrix [][][4]uint32) error { // TestMatrixContinuity tests that if an image is converted to a matrix, and then back, it remains the same func TestMatrixContinuity(t *testing.T) { - var testImagePath string = ".github/test_images/gnome.png" + var testImagePath = ".github/test_images/gnome.png" matrix, err := readImageToMatrix(testImagePath) if err != nil { @@ -73,7 +77,7 @@ func TestMatrixContinuity(t *testing.T) { return } - temporaryPath := t.TempDir() + "gnome.png" + temporaryPath := t.TempDir() + "gnome.png" // create a temporary path for the output image _ = writeImageFromMatrix(matrix, temporaryPath) @@ -109,34 +113,31 @@ func TestMatrixContinuity(t *testing.T) { // TestMake2D tests the Make2D function, asserting it can correctly make a nxm matrix of type [4]int32 which is our use-case func TestMake2D(t *testing.T) { - const testRuns = 20 - for i := 0; i < testRuns; i++ { - width, height := rand.Intn(100)+1, rand.Intn(100)+1 // As rand.Intn returns from [0, n] we must add one to assert n is never 0 - matrix := Make2D[[4]int32](width, height) - - // Check if the number of rows is n - if len(matrix) != width { - t.Errorf("Expected %d rows, got %d", width, len(matrix)) + width, height := rand.Intn(100)+1, rand.Intn(100)+1 // As rand.Intn returns from [0, n] we must add one to assert n is never 0 + matrix := Make2D[[4]int32](width, height) + + // Check if the number of rows is n + if len(matrix) != width { + t.Errorf("Expected %d rows, got %d", width, len(matrix)) + return + } + + // Check if each row has m elements + for i, row := range matrix { + if len(row) != height { + t.Errorf("Expected %d elements in row %d, got %d", height, i, len(row)) return } + } - // Check if each row has m elements - for i, row := range matrix { - if len(row) != height { - t.Errorf("Expected %d elements in row %d, got %d", height, i, len(row)) + // Check if each element's capacity is 4, which is as requirement + for _, row := range matrix { + for _, elem := range row { + if !(cap(elem) == 4) { + t.Errorf("Expected element to be initialized to an empty slice, got %v", elem) return } } - - // Check if each element's capacity is 4, which is as requirement - for _, row := range matrix { - for _, elem := range row { - if !(cap(elem) == 4) { - t.Errorf("Expected element to be initialized to an empty slice, got %v", elem) - return - } - } - } } } @@ -189,7 +190,7 @@ func TestAssertValidMatrix(t *testing.T) { uint32(rand.Intn(int(maxVal) + 1)), uint32(rand.Intn(int(maxVal) + 1)), uint32(rand.Intn(int(maxVal) + 1)), - } // I miss list comprehensions + } // @NOTE(Mauro): I miss list comprehensions } // Function to generate a random matrix using Make2D @@ -219,8 +220,10 @@ func TestAssertValidMatrix(t *testing.T) { err := assertValidMatrix(matrix) if valid && err != nil { t.Errorf("assertValidMatrix() returned an error for a valid matrix: %v", err) + return } else if !valid && err == nil { t.Errorf("assertValidMatrix() did not return an error for an invalid matrix") + return } } }