Jacky Baltes
National Taiwan Normal University
Taipei, Taiwan
jacky.baltes@ntnu.edu.tw
import matplotlib.pyplot as plt
import numpy as np
import math
import random
import cv2
from skimage import io
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/86/Red-tailed_Tropicbird3.jpg/200px-Red-tailed_Tropicbird3.jpg" #@param {type:"string"}
img = io.imread( url )
img = cv2.resize(img, (320, 240 ) )
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
grad_x = cv2.Sobel( gray, cv2.CV_16S, 1, 0, 3 )
grad_y = cv2.Sobel( gray, cv2.CV_16S, 0, 1, 3 )
abs_grad_x = cv2.convertScaleAbs( grad_x )
#print( grad_x[0:5,0:5])
#print( abs_grad_x[0:5,0:5])
abs_grad_y = cv2.convertScaleAbs( grad_y )
fig = plt.figure( figsize=(10,10) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Sobel X')
ax1.imshow(abs_grad_x, interpolation = 'bicubic', cmap = "gray")
ax1.set_xticks([])
ax1.set_yticks([])
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('Sobel Y')
ax2.imshow(abs_grad_y, interpolation = 'bicubic', cmap = "gray")
ax2.set_xticks([])
ax2.set_yticks([])
bird21 = addJBFigure("bird21", 0, 0, fig )
plt.close()
Sobel edge detector uses first derivative to find edges (i.e., large changes in brightness to the neighboring pixels)
grad = cv2.addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0 )
ret, threshold = cv2.threshold( grad, 100, 255, cv2.THRESH_BINARY )
#print( threshold[0:5, 0:5] )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Threshold')
ax1.imshow(threshold, interpolation = 'bicubic', cmap = "gray")
ax1.set_xticks([])
ax1.set_yticks([])
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('Threshold (Zoom)')
ax2.imshow(threshold[100:140,150:190], interpolation = 'bicubic', cmap = "gray")
ax2.set_xticks([])
ax2.set_yticks([])
b23 = addJBFigure("b23", 0, 0, fig )
plt.close()
grad = cv2.addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0 )
ret, threshold = cv2.threshold( grad, 100, 255, cv2.THRESH_BINARY )
#print( threshold[0:5, 0:5] )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Threshold')
ax1.imshow(threshold, interpolation = 'bicubic', cmap = "gray")
ax1.set_xticks([])
ax1.set_yticks([])
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('Threshold (Zoom)')
ax2.imshow(threshold[100:140,150:190], interpolation = 'bicubic', cmap = "gray")
ax2.set_xticks([])
ax2.set_yticks([])
b23 = addJBFigure("b23", 0, 0, fig )
plt.close()
Threshold the absolute gradient image to classify pixels as edge/non-edge pixels
In the zoomed image, we can see that there are multiple pixels per edge. Should be just a single pixel
Some spurious edge pixels in the background and the bird
Sobel edge detection uses the first derivative to detect an edge (first order operator)
Two adjacent edges may have a high first derivative, hence we get multiple edge pixels for one edge
The second derivative allows us to distinguish between a maximum, minimum, or saddle point
These are called second order operators
Check exact point where 2nd derivative is 0. May not lie on an exact pixel
Look for zero crossing of the 2nd derivative
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/86/Red-tailed_Tropicbird3.jpg/200px-Red-tailed_Tropicbird3.jpg" #@param {type:"string"}
img = io.imread( url )
img = cv2.resize(img, (320, 240 ) )
fig = plt.figure( figsize=(10,10) )
ax1 = fig.add_subplot(1,1,1)
ax1.set_title('Original Image')
ax1.imshow(img, interpolation = 'bicubic')
ax1.set_xticks([])
ax1.set_yticks([])
plt.show()
\[ I(x,y) = \left[ \begin{array}{ccccc} p_{-2,-2}, p_{-1,-2}, p_{0,-2}, p_{1,-2}, p_{2,-2} \\ p_{-2,-1}, p_{-1,-1}, p_{0,-1}, p_{1,-1}, p_{2,-1} \\ p_{-2,0}, p_{-1,0}, \underline{p_{0,0}}, p_{1,0}, p_{2,0} \\ p_{-2,1}, p_{-1,1}, p_{0,1}, p_{1,1}, p_{2,1} \\ p_{-2,2}, p_{-1,2}, p_{0,2}, p_{1,2}, p_{2,2} \\ \end{array} \right] \]
First derivative is the change in value of the image function \(I(x,y)\)
Since we are dealing with samples measured along the x and y direction
Abbreviated as \( I_{x}(x,y), I_{y}(x,y) \)
\[ I_x(x,y) = \frac{p_{1,0} - p_{-1,0}}{2}\\ I_y(x,y) = \frac{p_{0,1} - p_{0,1}}{2} \]
First derivative is the change in the first derivative, I need to calculate two first derivatives
\[ I^{+}_{x}(x,y) = p_{1,0} - p_{0,0} \\ I^{-}_{x}(x,y) = p_{0,0} - p_{-1,0} \\ \]
\[ \begin{array}{c} I^{+}_{x}(x,y) - I^{-}_{x}(x,y)\\ ( p_{1,0} - p_{0,0} ) - ( p_{0,0} - p_{-1,0} ) \\ p_{1,0} - 2 p_{0,0} + p_{-1,0} \\ \end{array} \]
Convolution mask for x and y direction simulataneously \[ \begin{array}{ccc} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{array} \]
Usually, also check for diagonal edges
\[ \begin{array}{ccc} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \\ \end{array} \]
kernel = 7 #@param ["3", "5", "7", "9", "11"] {type:"raw"}
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax1.set_title('Grayscale Image')
gray = cv2.cvtColor( img, cv2.COLOR_RGB2GRAY );
ax1.imshow(gray, interpolation = 'bicubic', cmap='gray')
ax1.set_xticks([])
ax1.set_yticks([])
bird24 = addJBFigure( "bird24", 0, 0, fig )
plt.close()
%matplotlib notebook
laplacian = cv2.Laplacian(gray,cv2.CV_64F)
sobelX = cv2.Sobel(gray,cv2.CV_64F,1,0,ksize=3) # x
sobelY = cv2.Sobel(gray,cv2.CV_64F,0,1,ksize=3) # y
fig = plt.figure(figsize=(15,15))
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Laplacian')
#ax1.set_xticks([])
#ax1.set_yticks([])
ax1.imshow( laplacian, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f1 = addJBFigure( "f1", 0, 0, fig )
%matplotlib notebook
roi = gray[100:200,125:225]
laplacian = cv2.Laplacian(roi,cv2.CV_64F)
sobelX = cv2.Sobel(roi,cv2.CV_64F,1,0,ksize=5) # x
sobelY = cv2.Sobel(roi,cv2.CV_64F,0,1,ksize=5) # y
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Laplacian')
ax1.set_xticks([])
ax1.set_yticks([])
ax1.imshow( laplacian, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f2 = addJBFigure( "f2", 0, 0, fig )
gray2 = gray + 3 * np.random.normal( 0.0, 5, (gray.shape) )
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Original Image')
ax1.imshow(gray, interpolation = 'bicubic', cmap='gray')
ax1.set_xticks([])
ax1.set_yticks([])
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('Image distorted with noise')
ax2.imshow(gray2, interpolation = 'bicubic', cmap='gray')
ax2.set_xticks([])
ax2.set_yticks([])
f10 = addJBFigure( "f10", 0, 0, fig )
roi = gray[100:200,125:225]
roi2 = gray2[100:200, 125:225]
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Original Image')
ax1.imshow(roi, interpolation = 'bicubic', cmap='gray')
ax1.set_xticks([])
ax1.set_yticks([])
ax2 = fig.add_subplot(1,2,2)
ax2.set_title('Image distorted with noise')
ax2.imshow(roi2, interpolation = 'bicubic', cmap='gray')
ax2.set_xticks([])
ax2.set_yticks([])
f11 = addJBFigure("f11", 0, 0, fig )
laplacian = cv2.Laplacian(roi2,cv2.CV_64F)
sobelX = cv2.Sobel(roi2,cv2.CV_64F,1,0,ksize=3) # x
sobelY = cv2.Sobel(roi2,cv2.CV_64F,0,1,ksize=3) # y
ret,thresh1 = cv2.threshold(laplacian,200,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(laplacian,60,255,cv2.THRESH_BINARY_INV)
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Laplacian')
ax1.set_xticks([])
ax1.set_yticks([])
ax1.imshow( thresh1 + thresh2, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f12 = addJBFigure("f12", 0, 0, fig )
Laplacian operator is very sensitive to noise
kernel = 9 #@param ["3", "5", "7", "9"] {type:"raw"}
roi2 = gray2[100:200, 125:225]
k = cv2.getGaussianKernel( kernel,0)
blur = cv2.GaussianBlur(roi2, (kernel,kernel), 0)
laplacian = cv2.Laplacian(blur,cv2.CV_64F)
sobelX = cv2.Sobel(blur,cv2.CV_64F,1,0,ksize=5) # x
sobelY = cv2.Sobel(blur,cv2.CV_64F,0,1,ksize=5) # y
ret,thresh1 = cv2.threshold(laplacian,8,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(laplacian,0,255,cv2.THRESH_BINARY_INV)
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('Laplacian')
ax1.set_xticks([])
ax1.set_yticks([])
ax1.imshow( thresh1, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f13 = addJBFigure( "f13", 0, 0, fig )
blur = cv2.GaussianBlur(gray2, (kernel,kernel), 0)
laplacian = cv2.Laplacian(blur,cv2.CV_64F)
sobelX = cv2.Sobel(blur,cv2.CV_64F,1,0,ksize=5) # x
sobelY = cv2.Sobel(blur,cv2.CV_64F,0,1,ksize=5) # y
ret,thresh1 = cv2.threshold(laplacian,8,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(laplacian,0,255,cv2.THRESH_BINARY_INV)
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('LoG')
ax1.set_xticks([])
ax1.set_yticks([])
ax1.imshow( thresh1, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f14 = addJBFigure("f14", 0, 0, fig )
blur = cv2.GaussianBlur(gray2, (kernel,kernel), 0)
laplacian = cv2.Laplacian(blur,cv2.CV_64F)
sobelX = cv2.Sobel(blur,cv2.CV_64F,1,0,ksize=5) # x
sobelY = cv2.Sobel(blur,cv2.CV_64F,0,1,ksize=5) # y
ret,thresh1 = cv2.threshold(laplacian,8,255,cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(laplacian,0,255,cv2.THRESH_BINARY_INV)
fig = plt.figure( figsize=(15,15) )
ax1 = fig.add_subplot(1,2,1)
ax1.set_title('LoG')
ax1.set_xticks([])
ax1.set_yticks([])
ax1.imshow( thresh1, cmap = 'gray')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('SobelXY')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.imshow( sobelX + sobelY, cmap = 'gray' )
f14 = addJBFigure("f14", 0, 0, fig )
Best choice of edge detector depends on your application
Sobel (First order operators): + robust to noise, + complete outlines, - multiple pixels per edge, - extra edge pixels
Laplacian (Second order operators): + single pixel edges, - sensitive to noise (Gaussian blur), - holes in the outline
Best choice of edge detector depends on your application
Sobel (First order operators): + robust to noise, + complete outlines, - multiple pixels per edge, - extra edge pixels
Laplacian (Second order operators): + single pixel edges, - sensitive to noise (Gaussian blur), - holes in the outline
Two error classes: False positives, False negatives
In practical applications, the
Machine vision system to detect errors in a painting job, fit circles to rivet holes. False positives were a bigger problem, since they would need to check the part.
System to detect bad potato chips on an assembly line, then false negatives were a bigger problem. If you put a moist potato chip in the bag, then the customer will never buy your potato chips again
Assume that you want to buil a vision system to detect if someone is carrying a gun in carry-on luggage
Two types of errors, but false negatives may cause people to die in a terrorist attack
Even more, the priors are very different - millions of normal travellers, luckily very small number of terrorists. This causes a problem, since any algorithm that always says no gun will be 99.99..% accurate
Search everybody, but then that would be a huge invasion in privacy and costs a lot of time and money