Skip to content

Commit

Permalink
docs: document filters
Browse files Browse the repository at this point in the history
  • Loading branch information
MM-coder committed Dec 8, 2023
1 parent 9b821df commit 4290dc3
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 33 deletions.
19 changes: 14 additions & 5 deletions filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,63 @@ 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 {
return nil, errors.New("empty matrix")
}
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)
}
}

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]
}
}
}

// 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))
}
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)))
Expand All @@ -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
Expand Down
59 changes: 31 additions & 28 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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")
}
}
Expand All @@ -65,15 +69,15 @@ 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 {
t.Fatalf("Failed to load the test image: %s", err)
return
}

temporaryPath := t.TempDir() + "gnome.png"
temporaryPath := t.TempDir() + "gnome.png" // create a temporary path for the output image

_ = writeImageFromMatrix(matrix, temporaryPath)

Expand Down Expand Up @@ -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
}
}
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}

0 comments on commit 4290dc3

Please sign in to comment.