Last active
November 29, 2022 10:40
-
-
Save ri-sh/45cb32dd5c1485e273ab81468e531f09 to your computer and use it in GitHub Desktop.
Hough Lines from scratch using opencv and numpy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# imports | |
import numpy as np | |
import cv2 | |
import matplotlib.pyplot as plt | |
# The Hough Transform is a popular algorithm for detecting any shape that can | |
# be represented in a parametric mathmatical form in binary images. This | |
# usually means that images need to be thresholded or filtered prior to running | |
# the Hough Transform. | |
# read in shapes image and convert to grayscale | |
shapes = cv2.imread('images/shapes.png') | |
cv2.imshow('Original Image', shapes) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
shapes_grayscale = cv2.cvtColor(shapes, cv2.COLOR_RGB2GRAY) | |
# blur image (this will help clean up noise for Canny Edge Detection) | |
# see Chapter 2.0 for Guassian Blur or check OpenCV documentation | |
shapes_blurred = cv2.GaussianBlur(shapes_grayscale, (5, 5), 1.5) | |
# find Canny Edges and show resulting image | |
canny_edges = cv2.Canny(shapes_blurred, 100, 200) | |
cv2.imshow('Canny Edges', canny_edges) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
########################################### HOUGH LINES FROM SCRATCH USING NUMPY | |
# Step 1: The Hough transform needs a binary edges images. For this particular | |
# python file, I used the openCV built in Class Canny to create this edge image | |
# from the original shapes.png file. | |
# This is the function that will build the Hough Accumulator for the given image | |
def hough_lines_acc(img, rho_resolution=1, theta_resolution=1): | |
''' A function for creating a Hough Accumulator for lines in an image. ''' | |
height, width = img.shape # we need heigth and width to calculate the diag | |
img_diagonal = np.ceil(np.sqrt(height**2 + width**2)) # a**2 + b**2 = c**2 | |
rhos = np.arange(-img_diagonal, img_diagonal + 1, rho_resolution) | |
thetas = np.deg2rad(np.arange(-90, 90, theta_resolution)) | |
# create the empty Hough Accumulator with dimensions equal to the size of | |
# rhos and thetas | |
H = np.zeros((len(rhos), len(thetas)), dtype=np.uint64) | |
y_idxs, x_idxs = np.nonzero(img) # find all edge (nonzero) pixel indexes | |
for i in range(len(x_idxs)): # cycle through edge points | |
x = x_idxs[i] | |
y = y_idxs[i] | |
for j in range(len(thetas)): # cycle through thetas and calc rho | |
rho = int((x * np.cos(thetas[j]) + | |
y * np.sin(thetas[j])) + img_diagonal) | |
H[rho, j] += 1 | |
return H, rhos, thetas | |
# This is a simple peaks function that just finds the indicies of the number | |
# of maximum values equal to num_peaks. You have to be careful here though, if | |
# there's any noise in the image it will like create a 'pocket' of local maxima | |
# values. This function ignores this and in turn has the tendancy to return | |
# multiple lines along an actual line in the image. | |
def hough_simple_peaks(H, num_peaks): | |
''' A function that returns the number of indicies = num_peaks of the | |
accumulator array H that correspond to local maxima. ''' | |
indices = np.argpartition(H.flatten(), -2)[-num_peaks:] | |
return np.vstack(np.unravel_index(indices, H.shape)).T | |
# This more advance Hough peaks funciton has threshold and nhood_size arguments | |
# threshold will threshold the peak values to be above this value if supplied, | |
# where as nhood_size will surpress the surrounding pixels centered around | |
# the local maximum after that value has been assigned as a peak. This will | |
# force the algorithm to look eslwhere after it's already selected a point from | |
# a 'pocket' of local maxima. | |
def hough_peaks(H, num_peaks, threshold=0, nhood_size=3): | |
''' A function that returns the indicies of the accumulator array H that | |
correspond to a local maxima. If threshold is active all values less | |
than this value will be ignored, if neighborhood_size is greater than | |
(1, 1) this number of indicies around the maximum will be surpessed. ''' | |
# loop through number of peaks to identify | |
indicies = [] | |
H1 = np.copy(H) | |
for i in range(num_peaks): | |
idx = np.argmax(H1) # find argmax in flattened array | |
H1_idx = np.unravel_index(idx, H1.shape) # remap to shape of H | |
indicies.append(H1_idx) | |
# surpess indicies in neighborhood | |
idx_y, idx_x = H1_idx # first separate x, y indexes from argmax(H) | |
# if idx_x is too close to the edges choose appropriate values | |
if (idx_x - (nhood_size/2)) < 0: min_x = 0 | |
else: min_x = idx_x - (nhood_size/2) | |
if ((idx_x + (nhood_size/2) + 1) > H.shape[1]): max_x = H.shape[1] | |
else: max_x = idx_x + (nhood_size/2) + 1 | |
# if idx_y is too close to the edges choose appropriate values | |
if (idx_y - (nhood_size/2)) < 0: min_y = 0 | |
else: min_y = idx_y - (nhood_size/2) | |
if ((idx_y + (nhood_size/2) + 1) > H.shape[0]): max_y = H.shape[0] | |
else: max_y = idx_y + (nhood_size/2) + 1 | |
# bound each index by the neighborhood size and set all values to 0 | |
for x in range(min_x, max_x): | |
for y in range(min_y, max_y): | |
# remove neighborhoods in H1 | |
H1[y, x] = 0 | |
# highlight peaks in original H | |
if (x == min_x or x == (max_x - 1)): | |
H[y, x] = 255 | |
if (y == min_y or y == (max_y - 1)): | |
H[y, x] = 255 | |
# return the indicies and the original Hough space with selected points | |
return indicies, H | |
# a simple funciton used to plot a Hough Accumulator | |
def plot_hough_acc(H, plot_title='Hough Accumulator Plot'): | |
''' A function that plot a Hough Space using Matplotlib. ''' | |
fig = plt.figure(figsize=(10, 10)) | |
fig.canvas.set_window_title(plot_title) | |
plt.imshow(H, cmap='jet') | |
plt.xlabel('Theta Direction'), plt.ylabel('Rho Direction') | |
plt.tight_layout() | |
plt.show() | |
# drawing the lines from the Hough Accumulatorlines using OpevCV cv2.line | |
def hough_lines_draw(img, indicies, rhos, thetas): | |
''' A function that takes indicies a rhos table and thetas table and draws | |
lines on the input images that correspond to these values. ''' | |
for i in range(len(indicies)): | |
# reverse engineer lines from rhos and thetas | |
rho = rhos[indicies[i][0]] | |
theta = thetas[indicies[i][1]] | |
a = np.cos(theta) | |
b = np.sin(theta) | |
x0 = a*rho | |
y0 = b*rho | |
# these are then scaled so that the lines go off the edges of the image | |
x1 = int(x0 + 1000*(-b)) | |
y1 = int(y0 + 1000*(a)) | |
x2 = int(x0 - 1000*(-b)) | |
y2 = int(y0 - 1000*(a)) | |
cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2) | |
# run hough_lines_accumulator on the shapes canny_edges image | |
H, rhos, thetas = hough_lines_acc(canny_edges) | |
indicies, H = hough_peaks(H, 3, nhood_size=11) # find peaks | |
plot_hough_acc(H) # plot hough space, brighter spots have higher votes | |
hough_lines_draw(shapes, indicies, rhos, thetas) | |
# Show image with manual Hough Transform Lines | |
cv2.imshow('Major Lines: Manual Hough Transform', shapes) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() |
@rishabhsixfeet Thanks for sharing this.
I needed this to run a bit faster. In case anyone needs Hough Accumulator computed fast, a version made (mostly) with numpy's ufuncs:
def hough_lines_acc(img, rho_resolution=1, theta_resolution=1):
''' A function for creating a Hough Accumulator for lines in an image. '''
height, width = img.shape # we need heigth and width to calculate the diag
img_diagonal = np.ceil(np.sqrt(height ** 2 + width ** 2)) # a**2 + b**2 = c**2
rhos = np.arange(-img_diagonal, img_diagonal + 1, rho_resolution)
thetas = np.deg2rad(np.arange(-90, 90, theta_resolution))
sines = np.sin(thetas)
cosines = np.cos(thetas)
# create the empty Hough Accumulator with dimensions equal to the size of
# rhos and thetas
H = np.zeros((len(rhos), len(thetas)), dtype=np.uint64)
points = np.argwhere(img != 0)
sincos = np.stack((sines, cosines))
rhos_mat = (points @ sincos + img_diagonal).astype(np.int32)
for theta_index in range(len(thetas)):
H[:, theta_index] = np.bincount(rhos_mat[:, theta_index], minlength=H.shape[0])
return H, rhos, thetas
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi, I was working through your code. There is an error in line 53
rho = int((x * np.cos(thetas[j]) + y * np.sin(thetas[j])) + img_diagonal)
should be
rho = int((x * np.cos(thetas[j]) + y * np.sin(thetas[j])) + img_diagonal)//rho_resolution