# Threshold an image for a given range and Sobel kernel

The class defines functions to threshold an image for a given range and Sobel kernel. Each function has similarities, i.e. they share the same following tasks:

• each function converts an image to grayscale
• rescale to 8 bit
• apply a threshold, and create a binary image result
'''
Define functions to threshold an image for a given range and Sobel kernel
'''

def __init__(self, args):
self.args = args

def gradient_abs_sobel(self, img, orient='x', sobel_kernel=3, thresh=(0, 255)):
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# take the absolute value of the gradient in given orient = 'x' or 'y'
if orient == 'x':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 1, 0))
if orient == 'y':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 0, 1))
# scale to 8-bit (0 - 255) then convert to type = np.uint8
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
# is > thresh_min and < thresh_max
binary_output = np.zeros_like(scaled_sobel)
binary_output[(scaled_sobel >= thresh[0]) & (scaled_sobel <= thresh[1])] = 1
# return the binary image
return binary_output

def gradient_magnitude(self, img, sobel_kernel=3, thresh=(0, 255)):
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# take both Sobel x and y gradients
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
# rescale to 8 bit
# create a binary image of ones where threshold is met, zeros otherwise
# return the binary image
return binary_output

# Define a function to threshold an image for a given range and Sobel kernel
def gradient_direction(self, img, sobel_kernel=3, thresh=(0, np.pi/2)):
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# take both Sobel x and y gradients
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
# take the absolute value of the gradient direction
# apply a threshold, and create a binary image result
# return the binary image
return binary_output
• in gradient_direction I see no scaling. Is this intentional? – Maarten Fabré Apr 17 '18 at 15:08
• @MaartenFabré, yes, it is. – mo2 Apr 21 '18 at 21:12

as things are now, there is little use for the class in this case. the self argument is never used in the functions.

This is easily factored into different functions

def grayscale(img):
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

Whether this deserves a separate function or can be inlined is your choice. If you need to type this a lot, or if the method of grayscaling can change in the future, it will be easier to put it in a separate method.. You could also move the colorspace to the arguments to make this more versatile and also accept HSV or Luv encoded images.

def sobel_xy(gray, sobel_kernel=3, absolute=True):
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
if absolute:
sobel_x, sobel_y = np.absolute(sobel_x), np.absolute(sobel_y)
return sobel_x, sobel_y

This calculation is done in all methods, so refactoring it is simple and effective

return np.hypot(*sobel_xy(gray, sobel_kernel))

return np.arctan2(*sobel_xy(gray, sobel_kernel))

This part is pretty straightforward. All I changed was use np.hypot to calculate the magnitude

dx, dy = (1, 0) if orient == 'x' else (0, 1)
sobel = cv2.Sobel(gray, cv2.CV_64F, dx, dy)
return np.absolute(sobel)

orientation = {'x': 0,'y': 1}[orient]
sobel = sobel_xy(gray)
return sobel[orientation]

Depending on taste and the expense of the sobel calculation you can pick either of the 2

def scale(sobel, MAX=255, dtype=np.uint8):
return dtype(MAX * sobel / np.max(sobel))

return (img >= lower) & (img <= upper)

Are pretty straightforward too. Instead of the np.zeros_like you can directly use the boolean array of the comparison

Now instead of calling 1 function, you just call the 4 elements subsequently. If this is too much work you can still make 1 compound method for the 3 cases.

gray = grayscale(img)
• @Maarten Fabré, thank you for your feedback!! I particularly enjoyed the two following lines and the way you cut the code: lines = { 1 : ' dx, dy = (1, 0) if orient == 'x' else (0, 1) ' ; 2 : ' orientation = {'x': 0,'y': 1}[orient] '} – mo2 Apr 18 '18 at 14:29

Thank to Maarten Fabré's feedbacks, I have rewritten the code as the following:

def grayscale(image):
return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

def sobel_xy(gray, sobel_kernel=9, absolute=True):
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
if absolute:
sobel_x, sobel_y = np.absolute(sobel_x), np.absolute(sobel_y)
return sobel_x, sobel_y

def sobel_abs(gray, orient='x'):
orientation = {'x': 0,'y': 1}[orient]
sobel       = sobel_xy(gray)
return sobel[orientation]

def scale(sobel, MAX=255, dtype=np.uint8):
return dtype(MAX * sobel / np.max(sobel))

return (image >= lower) & (image <= upper)

gray  = grayscale(image)
sobel = sobel_abs(gray, orient=orient)
sobel_scale = scale(sobel, MAX=thresh[1], dtype=np.uint8)

gray    = grayscale(image)

def gradient_direction(image, sobel_kernel=15, thresh=(0, np.pi/2)): # thresh=(0.7, 1.3)
gray       = grayscale(image)

def main():
# parameters and placeholders
args = PARSE_ARGS()
# Choose a Sobel kernel size
ksize = 9
# Apply each of the thresholding functions
mag_binary = gradient_magnitude(image, sobel_kernel=ksize, thresh=(20, 100))
dir_binary = gradient_direction(image, sobel_kernel=15, thresh=(0.7, 1.3))

# combine thresholds
combined1, combined2, combined3 = np.zeros_like(dir_binary), np.zeros_like(dir_binary), np.zeros_like(dir_binary)
combined1[((gradx == 1) & (grady == 1)) | ((mag_binary == 1) & (dir_binary == 1))] = 1
combined3[((mag_binary == 1) & (dir_binary == 1))] = 1

# Plot the result
row, column = [5, 2]
figure, axes = plt.subplots(row, column, figsize=(15, 20))
figure.tight_layout()
list_title_image = [['Original Image',image],
['Expected result', img_solution],
['mag_binary', mag_binary],
['dir_binary', dir_binary],
['combined1', combined1],
['combined2', combined2],
['combined3', combined3],
['Original Image', image] ]

for ax, img in zip(axes.flatten(), list_title_image):
ax.imshow(img[1], cmap='gray')
ax.set_title(img[0], fontsize=15)
ax.axis('off')

if __name__ == '__main__':
main()
• I would replace the thresh[0], thresh[1] by *thresh, or change the arguments of mask from lower, upper to thresh – Maarten Fabré Apr 19 '18 at 19:58
• It's nice that you posted the updated code, but you should accept Maarten's answer, as it helped you. This will give Maarten the rep he deserves. – Cris Luengo Apr 20 '18 at 18:37
• @CrisLuengo, done :) Bear with me, I am quite new on this platform. – mo2 Apr 21 '18 at 21:10