# Parameters:
#   im: Image to filter, provided as a numpy array
#   sigma:  The variance of the Gaussian (e.g., 1)
#
import numpy as np

def gaussian(im, sigma):
    height, width, _ = im.shape
    img_filtered = np.zeros([height, width, 3])

    # Define filter size.
    # A Gaussian has infinite support, but most of it's mass lies within
    # three standard deviations of the mean. The standard deviation is
    # the square of the variance, sigma.
    n = np.int(np.sqrt(sigma) * 3)

    # Iterate over pixel locations p
    for p_y in range(height):
        for p_x in range(width):
            gp = 0
            w = 0

            # Iterate over kernel locations to define pixel q
            for i in range(-n, n):
                for j in range(-n, n):
                    # Make sure no index goes out of bounds of the image
                    q_y = np.max([0, np.min([height - 1, p_y + i])])
                    q_x = np.max([0, np.min([width - 1, p_x + j])])
                    # Computer Gaussian filter weight at this filter pixel
                    g = np.exp( -((q_x - p_x)**2 + (q_y - p_y)**2) / (2 * sigma**2) )

                    # Accumulate filtered output
                    gp += g * im[p_y, p_x, :]
                    # Accumulate filter weight for later normalization, to maintain image brightness
                    w += g
            img_filtered[p_y, p_x, :] = gp / (w + np.finfo(np.float32).eps)
    return img_filtered