Have you ever wondered which edge detection methods are best for given images? Yeah? So am I, so let's compare them.
We will be implementing some of the most commonly used methods and also using methods from OpenCV and PIL
We will be comparing the following methods:
The sobel is one of the most commonly used edge detectors. It is based on convolving the image with a small, separable, and integer valued filter in horizontal and vertical direction and is therefore relatively inexpensive in terms of computations. The Sobel edge enhancement filter has the advantage of providing differencing (which gives the edge response) and smoothing (which reduces noise) concurrently.
Here is a python implementation of Sobel operator
import numpy as np | |
from PIL import Image | |
import matplotlib.pyplot as plt | |
# Open the image | |
img = np.array(Image.open('dancing-spider.jpg')).astype(np.uint8) | |
# Apply gray scale | |
gray_img = np.round(0.299 * img[:, :, 0] + | |
0.587 * img[:, :, 1] + | |
0.114 * img[:, :, 2]).astype(np.uint8) | |
# Sobel Operator | |
h, w = gray_img.shape | |
# define filters | |
horizontal = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # s2 | |
vertical = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) # s1 | |
# define images with 0s | |
newhorizontalImage = np.zeros((h, w)) | |
newverticalImage = np.zeros((h, w)) | |
newgradientImage = np.zeros((h, w)) | |
# offset by 1 | |
for i in range(1, h - 1): | |
for j in range(1, w - 1): | |
horizontalGrad = (horizontal[0, 0] * gray_img[i - 1, j - 1]) + \ | |
(horizontal[0, 1] * gray_img[i - 1, j]) + \ | |
(horizontal[0, 2] * gray_img[i - 1, j + 1]) + \ | |
(horizontal[1, 0] * gray_img[i, j - 1]) + \ | |
(horizontal[1, 1] * gray_img[i, j]) + \ | |
(horizontal[1, 2] * gray_img[i, j + 1]) + \ | |
(horizontal[2, 0] * gray_img[i + 1, j - 1]) + \ | |
(horizontal[2, 1] * gray_img[i + 1, j]) + \ | |
(horizontal[2, 2] * gray_img[i + 1, j + 1]) | |
newhorizontalImage[i - 1, j - 1] = abs(horizontalGrad) | |
verticalGrad = (vertical[0, 0] * gray_img[i - 1, j - 1]) + \ | |
(vertical[0, 1] * gray_img[i - 1, j]) + \ | |
(vertical[0, 2] * gray_img[i - 1, j + 1]) + \ | |
(vertical[1, 0] * gray_img[i, j - 1]) + \ | |
(vertical[1, 1] * gray_img[i, j]) + \ | |
(vertical[1, 2] * gray_img[i, j + 1]) + \ | |
(vertical[2, 0] * gray_img[i + 1, j - 1]) + \ | |
(vertical[2, 1] * gray_img[i + 1, j]) + \ | |
(vertical[2, 2] * gray_img[i + 1, j + 1]) | |
newverticalImage[i - 1, j - 1] = abs(verticalGrad) | |
# Edge Magnitude | |
mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0)) | |
newgradientImage[i - 1, j - 1] = mag | |
plt.figure() | |
plt.title('dancing-spider-sobel.png') | |
plt.imsave('dancing-spider-sobel.png', newgradientImage, cmap='gray', format='png') | |
plt.imshow(newgradientImage, cmap='gray') | |
plt.show() |
Results:
In this example we apply the mask on the gray-scale image, however we can produce a better result by applying the mask on each RGB channel.
import numpy as np | |
from PIL import Image | |
import matplotlib.pyplot as plt | |
# Open the image | |
img = np.array(Image.open('dancing-spider.jpg')).astype(np.uint8) | |
# Sobel Operator | |
h, w, d = img.shape | |
# define filters | |
horizontal = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # s2 | |
vertical = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) # s1 | |
# define images with 0s | |
newgradientImage = np.zeros((h, w, d)) | |
# offset by 1 | |
for channel in range(d): | |
for i in range(1, h - 1): | |
for j in range(1, w - 1): | |
horizontalGrad = (horizontal[0, 0] * img[i - 1, j - 1, channel]) + \ | |
(horizontal[0, 1] * img[i - 1, j, channel]) + \ | |
(horizontal[0, 2] * img[i - 1, j + 1, channel]) + \ | |
(horizontal[1, 0] * img[i, j - 1, channel]) + \ | |
(horizontal[1, 1] * img[i, j, channel]) + \ | |
(horizontal[1, 2] * img[i, j + 1, channel]) + \ | |
(horizontal[2, 0] * img[i + 1, j - 1, channel]) + \ | |
(horizontal[2, 1] * img[i + 1, j, channel]) + \ | |
(horizontal[2, 2] * img[i + 1, j + 1, channel]) | |
verticalGrad = (vertical[0, 0] * img[i - 1, j - 1, channel]) + \ | |
(vertical[0, 1] * img[i - 1, j, channel]) + \ | |
(vertical[0, 2] * img[i - 1, j + 1, channel]) + \ | |
(vertical[1, 0] * img[i, j - 1, channel]) + \ | |
(vertical[1, 1] * img[i, j, channel]) + \ | |
(vertical[1, 2] * img[i, j + 1, channel]) + \ | |
(vertical[2, 0] * img[i + 1, j - 1, channel]) + \ | |
(vertical[2, 1] * img[i + 1, j, channel]) + \ | |
(vertical[2, 2] * img[i + 1, j + 1, channel]) | |
# Edge Magnitude | |
mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0)) | |
# Avoid underflow: clip result | |
newgradientImage[i - 1, j - 1, channel] = mag | |
# now add the images r g and b | |
rgb_edge = newgradientImage[:,:,0] + newgradientImage[:,:,1] + newgradientImage[:,:,2] | |
plt.figure() | |
plt.title('dancing-spider-sobel-rgb.png') | |
plt.imsave('dancing-spider-sobel-rgb.png', rgb_edge, cmap='gray', format='png') | |
plt.imshow(rgb_edge, cmap='gray') | |
plt.show() |
Results: One channel(left), RGB channel(right).
You can see a slight improvement but note that it's computationally more costly.
Prewitt operator is similar to the Sobel operator and is used for detecting vertical and horizontal edges in images. However, unlike the Sobel, this operator does not place any emphasis on the pixels that are closer to the center of the mask.
The only differance is the mask
import numpy as np | |
from PIL import Image | |
import matplotlib.pyplot as plt | |
# Open the image | |
img = np.array(Image.open('dancing-spider.jpg')).astype(np.uint8) | |
# Apply gray scale | |
gray_img = np.round(0.299 * img[:, :, 0] + | |
0.587 * img[:, :, 1] + | |
0.114 * img[:, :, 2]).astype(np.uint8) | |
# Prewitt Operator | |
h, w = gray_img.shape | |
# define filters | |
horizontal = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]) # s2 | |
vertical = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]) # s1 | |
# define images with 0s | |
newgradientImage = np.zeros((h, w)) | |
# offset by 1 | |
for i in range(1, h - 1): | |
for j in range(1, w - 1): | |
horizontalGrad = (horizontal[0, 0] * gray_img[i - 1, j - 1]) + \ | |
(horizontal[0, 1] * gray_img[i - 1, j]) + \ | |
(horizontal[0, 2] * gray_img[i - 1, j + 1]) + \ | |
(horizontal[1, 0] * gray_img[i, j - 1]) + \ | |
(horizontal[1, 1] * gray_img[i, j]) + \ | |
(horizontal[1, 2] * gray_img[i, j + 1]) + \ | |
(horizontal[2, 0] * gray_img[i + 1, j - 1]) + \ | |
(horizontal[2, 1] * gray_img[i + 1, j]) + \ | |
(horizontal[2, 2] * gray_img[i + 1, j + 1]) | |
verticalGrad = (vertical[0, 0] * gray_img[i - 1, j - 1]) + \ | |
(vertical[0, 1] * gray_img[i - 1, j]) + \ | |
(vertical[0, 2] * gray_img[i - 1, j + 1]) + \ | |
(vertical[1, 0] * gray_img[i, j - 1]) + \ | |
(vertical[1, 1] * gray_img[i, j]) + \ | |
(vertical[1, 2] * gray_img[i, j + 1]) + \ | |
(vertical[2, 0] * gray_img[i + 1, j - 1]) + \ | |
(vertical[2, 1] * gray_img[i + 1, j]) + \ | |
(vertical[2, 2] * gray_img[i + 1, j + 1]) | |
# Edge Magnitude | |
mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0)) | |
newgradientImage[i - 1, j - 1] = mag | |
plt.figure() | |
plt.title('dancing-spider-prewitt.png') | |
plt.imsave('dancing-spider-prewitt.png', newgradientImage, cmap='gray', format='png') | |
plt.imshow(newgradientImage, cmap='gray') | |
plt.show() |
Result:
Laplacian is somewhat different from the methods we have discussed so far. Unlike the Sobel and Prewitt’s edge detectors, the Laplacian edge detector uses only one kernel. It calculates second order derivatives in a single pass. Two commonly used small kernels are:
Because these masks are approximating a second derivative measurement on the image, they are very sensitive to noise. To correct this, the image is often Gaussian smoothed before applying the Laplacian filter.
We can also convolve gaussian mask with the Laplacian mask and apply to the image in one pass. I will explain how to convolve one kernel with another in a separate tutorial. However, we will be applying them separately in the following example. To make things easier we will be using OpenCV.
import cv2 | |
import matplotlib.pyplot as plt | |
# Open the image | |
img = cv2.imread('shapes.jpg') | |
# Apply gray scale | |
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
# Apply gaussian blur | |
blur_img = cv2.GaussianBlur(gray_img, (3, 3), 0) | |
# Positive Laplacian Operator | |
laplacian = cv2.Laplacian(blur_img, cv2.CV_64F) | |
plt.figure() | |
plt.title('Shapes') | |
plt.imsave('shapes-lap.png', laplacian, cmap='gray', format='png') | |
plt.imshow(laplacian, cmap='gray') | |
plt.show() |
Results:
Canny edge detector is probably the most commonly used and most effective method, it can have it's own tutorial, because it's much more complex edge detecting method then the ones described above. However, I will try to make it short and easy to understand.
First two steps are very straight forward, note that in the second step we are also computing the orientation of gradients "theta = arctan(Gy / Gx)" Gy and Gx are gradient x direction and y direction respectively.
Now let's talk about Non-maximum suppression and what it does. In this step we are trying to relate the edge direction to a direction that can be traced along the edges based on the previously calculated gradient strengths and edge directions. At each pixel location we have four possible directions. We check all directions if the gradient is maximum at this point. Perpendicular pixel values are compared with the value in the edge direction. If their value is lower than the pixel on the edge then they are suppressed. After this step we will get broken thin edges that needs to be fixed, so let's move on to the next step.
Hysteresis is a way of linking the broken lines produced in the previous step. This is done by iterating over the pixels and checking if the current pixel is an edge. If it's an edge then check surrounding area for edges. If they have the same direction then we mark them as an edge pixel. We also use 2 thresholds, a high and low. If the pixels is greater than lower threshold it is marked as an edge. Then pixels that are greater than the lower threshold and also are greater than high threshold, are also selected as strong edge pixels. When there are no more changes to the image we stop.
Now let's looked the code:
import cv2 | |
import matplotlib.pyplot as plt | |
# Open the image | |
img = cv2.imread('dancing-spider.jpg') | |
# Apply Canny | |
edges = cv2.Canny(img, 100, 200, 3, L2gradient=True) | |
plt.figure() | |
plt.title('Spider') | |
plt.imsave('dancing-spider-canny.png', edges, cmap='gray', format='png') | |
plt.imshow(edges, cmap='gray') | |
plt.show() |
Results:
If you have any questions or I messed up somewhere, please emails me at nikatsanka@gmail.com