I've been using morph opening in OpenCV to reduce noise outside of my ROI in images via opencv
, and until now, whenever I need a higher degree of noise reduction I just randomly increase kernel size or increase the number of iterations until I'm happy. Is there a significant difference in results depending on which you increase / how would you decide which to change in a given situation? I'm trying to come up with a better approach to which parameter I change (by how much) other than guess-and-check.
It depends on the kernel type. For dilating or eroding with an odd-square kernel, there is no difference whether you increase the size or increase the iterations (assuming values which would make them equal are used). For example:
>>> M = np.zeros((7,7), dtype=np.uint8)
>>> M[3,3] = 1
>>> k1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
>>> M1 = cv2.dilate(M, k1, iterations=2)
>>> k2 = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
>>> M2 = cv2.dilate(M, k2, iterations=1)
>>> M1
[[0 0 0 0 0 0 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 0 0 0 0 0 0]]
>>> M2
[[0 0 0 0 0 0 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 1 1 1 1 1 0]
[0 0 0 0 0 0 0]]
And this is fairly intuitive. A 3x3
rectangular kernel for dilating will find any white pixel, and turn the neighboring pixels white. So it's easy to see that doing this twice will make any single white pixel turn into a 5x5 block of white pixels. Here we're assuming the center pixel is the one that is compared---the anchor---but this could be changed, which could affect the result. For example, suppose you were comparing two iterations of a (2, 2)
kernel with a single iteration of a (3, 3)
kernel:
>>> M = np.zeros((5, 5), dtype=np.uint8)
>>> M[2,2] = 1
>>> k1 = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
>>> M1 = cv2.dilate(M, k1, iterations=2)
>>> k2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
>>> M2 = cv2.dilate(M, k2, iterations=1)
>>> M1
[[0 0 0 0 0]
[0 0 0 0 0]
[0 0 1 1 1]
[0 0 1 1 1]
[0 0 1 1 1]]
>>> M2
[[0 0 0 0 0]
[0 1 1 1 0]
[0 1 1 1 0]
[0 1 1 1 0]
[0 0 0 0 0]]
You can see that while it creates the shape (intuitive), they're not in the same place (non-intuitive). And that's because the anchor of a (2, 2)
kernel cannot be in the center of the kernel---in this case we see that with a centered pixel, the neighbors that dilate are only to the bottom-right, since it has to choose a direction because it can only expand the single pixel to fill up a (2, 2)
square.
Things become even more tricky with non-rectangular shaped kernels. For example:
>>> M = np.zeros((5, 5), dtype=np.uint8)
>>> M[2,2] = 1
>>> k1 = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
>>> M1 = cv2.dilate(M, k1, iterations=2)
>>> k2 = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
>>> M2 = cv2.dilate(M, k2, iterations=1)
>>> M1
[[0 0 1 0 0]
[0 1 1 1 0]
[1 1 1 1 1]
[0 1 1 1 0]
[0 0 1 0 0]]
>>> M2
[[0 0 1 0 0]
[0 0 1 0 0]
[1 1 1 1 1]
[0 0 1 0 0]
[0 0 1 0 0]]
The first pass of M1 creates a small cross 3 pixels high, 3 pixels wide. But then each one of those pixels creates a cross at their location, which actually creates a diamond pattern.
So to sum up for basic morphological operations, with rectangular kernels, at least odd-dimensioned ones, the result is the same---but for other kernels, the result is different. You can apply the other morphological operations to simple examples like this to get a hang of how they behave and which you should be using and how to increase their effects.