Seanny123
Seanny123

Reputation: 9336

Laplacian of Gaussian Edge Detector Being Affected by Change of Mask Size

For a class, I've written a Laplacian of Gaussian edge detector that works in the following way.

  1. Make a Laplacian of Gaussian mask given the variance of the Gaussian the size of the mask
  2. Convolve it with the image
  3. Find the zero crossings in a really shoddy manner, these are the edges of the image

If you so desire, the code for this program can be viewed here, but the most important part is where I create my Gaussian mask which depends on two functions that I've reproduced here for your convenience:

# Function for calculating the laplacian of the gaussian at a given point and with a given variance
def l_o_g(x, y, sigma):
    # Formatted this way for readability
    nom = ( (y**2)+(x**2)-2*(sigma**2) )
    denom = ( (2*math.pi*(sigma**6) ))
    expo = math.exp( -((x**2)+(y**2))/(2*(sigma**2)) )
    return nom*expo/denom


# Create the laplacian of the gaussian, given a sigma
# Note the recommended size is 7 according to this website http://homepages.inf.ed.ac.uk/rbf/HIPR2/log.htm 
# Experimentally, I've found 6 to be much more reliable for images with clear edges and 4 to be better for images with a lot of little edges
def create_log(sigma, size = 7):
    w = math.ceil(float(size)*float(sigma))

    # If the dimension is an even number, make it uneven
    if(w%2 == 0):
        print "even number detected, incrementing"
        w = w + 1

    # Now make the mask
    l_o_g_mask = []

    w_range = int(math.floor(w/2))
    print "Going from " + str(-w_range) + " to " + str(w_range)
    for i in range_inc(-w_range, w_range):
        for j in range_inc(-w_range, w_range):
            l_o_g_mask.append(l_o_g(i,j,sigma))
    l_o_g_mask = np.array(l_o_g_mask)
    l_o_g_mask = l_o_g_mask.reshape(w,w)
    return l_o_g_mask

All in all, it works relatively well, even if it is extremely slow because I don't know how to leverage Numpy. However, whenever I change the size of the Gaussian mask, the thickness of the edges I detect change drastically.

Here is the image run with a size of mask equivalent to 4 times the given variance of the Gaussian: 4 times the variance

Here is the same image run with a size of mask equivalent to 6 times the variance: 6 times the variance

I'm kind of baffled, because the only thing the size parameter should change is the accuracy of the approximation of the Laplacian of Gaussian mask before I begin to convolve it with the image. So I ran a test where I wanted to vizualize how my mask looked given different size parameters.

Here it is with a size of 4: 4_l_o_g

Here it is with a size of 6: 6_l_o_g

The shape of the function seems to be the same as far as I can tell from the zero crossings (they happen to be spaced around four pixels apart) and their peaks. Is there a better way to check?

Any suggestions as to why this issue might be occurring or how to investigate further are appreciated.

Upvotes: 1

Views: 2457

Answers (1)

Seanny123
Seanny123

Reputation: 9336

It turns out your concept about the effect of increasing the mask size is wrong. Increasing the size doesn't actually improve the quality of approximation or the resolution of the function. To explain, instead of using a complicated 2D function like the Laplacian of the Gaussian, let's take things back down to the one dimension and pretend we are approximating the function f(x) = x^2.

Now you code for calculating the function would look like this:

def derp(theta, size):
    w = math.ceil(float(size)*float(sigma))

    # If the dimension is an even number, make it uneven
    if(w%2 == 0):
        print "even number detected, incrementing"
        w = w + 1

    # Now make the mask
    x_mask = []

    w_range = int(math.floor(w/2))
    print "Going from " + str(-w_range) + " to " + str(w_range)
    for i in range_inc(-w_range, w_range):
        x_mask = a*i^2

If you were to increase the "size" of this function, you wouldn't be increasing the resolution, you're actually increasing the range of values of x that you're grabbing from. For example, for a size of 3 you're evaluating -1, 0, 1, for a size of 5 you're evaluating -2, -1, 0, 1, 2. Notice this doesn't increase the spacing between the pixels. This is what you're actually seeing when you talk about the zero crossing occurring the same number of pixels apart.

Consequently, when convoluting with this really silly mask, you would get really different results. But what if we went back to the Laplacian of the Gaussian?

Well, the nice property the Laplacian of the Gaussian has is that the farther you go with it, the more zero values you get. So unlike our silly x^2 function, you should be getting the same results after some time.

Now, I think the reason you didn't see this with your test cases is because they were too limited in size, because your program is too slow for you to really see the difference between size=15 and size=20, but if were to actually run those cases I think you would see that the image doesn't change that much.

This still doesn't answer what you should be doing, for that, we're going to have to look to the professionals. Namely, the implementation of the gaussian_filter in Scipy (source here).

When you look at their source code, the first thing you'll notice is that when creating their mask they're basically doing the same thing as you. They are always using an integer step size and they are scaling the size of the mask by it's standard deviation.

As to why they are doing it that way, I can't answer, since I don't have that much of an in depth knowledge of image processing or Scipy. However, this may make for a good new question to ask on SO.

Upvotes: 1

Related Questions