Skip to content

Create hough_transform.py #12207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions computer_vision/hough_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""
The Hough transform can be used to detect lines, circles or
other parametric curves. It works by transforming
the image edge map (obtained using a Sobel Filter) to Polar coordinates
and then selecting local maxima in the Parametric space as lines based on
majority voting.

References:
https://en.wikipedia.org/wiki/Hough_transform
https://www.cs.cmu.edu/~16385/s17/Slides/5.3_Hough_Transform.pdf
https://www.uio.no/studier/emner/matnat/ifi/INF4300/h09/undervisningsmateriale/hough09.pdf

Requirements (pip):
- matplotlib
- cv2
"""

import cv2
import matplotlib.pyplot as plt
import numpy as np

from digital_image_processing.edge_detection import canny


def generate_accumulator(edges: np.ndarray) -> np.ndarray:
"""
- Generates an accumulator by transforming edge coordinates from Cartesian
to polar coordinates (Hough space).

- The accumulator array can be indexed as `accumulator[p][theta]`

Params:
------
edges (np.ndarray): The edge-detected binary image (single-channel).

Returns:
------
np.ndarray: The accumulator array with votes for line candidates.

Example:
------
>>> img = np.array([[1, 0, 0,], [1, 0, 0,], [1, 0, 0,],])
>>> np.sum(generate_accumulator(img))
np.float64(540.0)
"""
n, m = edges.shape
theta_min, theta_max = 0, 180
p_min, p_max = 0, int(n * np.sqrt(2) + 1)
accumulator = np.zeros((int(theta_max - theta_min), int(p_max - p_min)))
for x in range(n):
for y in range(m):
if edges[x][y]:
for theta in range(theta_min, theta_max):
p = int(
x * np.cos(np.deg2rad(theta)) + y * np.sin(np.deg2rad(theta))
)
accumulator[theta][p] += 1
return accumulator


def hough_transform(
img: np.ndarray, threshold: int = 30, max_num_lines: int = 5
) -> list[tuple[int, int, np.float64]]:
"""
Performs the Hough transform to detect lines in the input image.

Params:
------
img (np.ndarray): Single-channel grayscale image.
threshold (int): Minimum vote count in the accumulator to consider a line.
max_num_lines (int): Maximum number of lines to return.

Returns:
------
list[tuple[int, int, int]]: List of detected lines in (theta, p, votes) format.

Raises:
------
AssertionError: If the image is not square or single-channel.

Example:
------
>>> img = np.vstack([np.zeros((30, 50)),np.ones((1, 50)),np.zeros((19, 50))])
>>> hough_transform(img, 30, 1)
[(0, 28, np.float64(48.0))]
"""
assert img.shape[0] == img.shape[1], "image must have equal dimensions"
assert len(img.shape) == 2, "image should be single-channel"

# Obtain edge map for image
edges = canny.canny(img)

# Transform to Polar Coordinates
n, _ = img.shape
theta_min, theta_max = 0, 180
p_min, p_max = 0, int(n * np.sqrt(2) + 1)
accumulator = generate_accumulator(edges)

# Select maxima in Polar space
res = []
for theta in range(theta_min, theta_max):
for p in range(p_min, p_max):
if accumulator[theta][p] > threshold:
res.append((theta, p, accumulator[theta][p]))

res = sorted(res, key=lambda x: x[2], reverse=True)[:max_num_lines]
return res


def draw_hough_lines(
img: np.ndarray,
lines: list[tuple],
thickness: int = 1,
color: tuple[int, int, int] = (255, 0, 0),
) -> None:
"""
Draws detected Hough lines on the image.

Params:
------
img (np.ndarray): The input image to draw lines on.
lines (list[tuple[int, int, int]]):
List of (theta, p, votes) for detected lines.
thickness (int): Line thickness.
color (tuple[int, int, int]): BGR color of the lines.

Example:
------
>>> draw_hough_lines(create_dummy_img(), [(50, 0),])
"""
for line in lines:
theta, p = line[0], line[1]
a = np.sin(np.deg2rad(theta))
b = np.cos(np.deg2rad(theta))
x0, y0 = a * p, b * p
x1, y1 = int(x0 + 100 * (-b)), int(y0 + 100 * (a))
x2, y2 = int(x0 - 100 * (-b)), int(y0 - 100 * (a))
cv2.line(img, (x1, y1), (x2, y2), color, thickness)


def create_dummy_img(height: int = 50, width: int = 50) -> np.ndarray:
"""
Test function to create dummy 3-channel image of specified width and height
Example:
------
>>> create_dummy_img(100, 120).shape
(100, 120, 3)
"""
img = np.zeros((height, width), dtype=np.uint8)
cv2.line(img, (10, 10), (int(0.6 * height), int(0.8 * width)), 255, 1) # type: ignore[call-overload]
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # type: ignore[assignment]
return img


if __name__ == "__main__":
import doctest

# Run doctests
doctest.testmod()

img = create_dummy_img(60, 80)
# Preprocess Image
img = cv2.resize(img, (64, 64), interpolation=cv2.INTER_AREA)
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.imshow(img)
plt.show()
# Accumulator
accumulator = generate_accumulator(canny.canny(gray_image))
plt.imshow(accumulator)
plt.show()
# Hough Transform
res = hough_transform(gray_image, 30, 1)
draw_hough_lines(img, res)
plt.imshow(img)
plt.show()