Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 48bba17

Browse files
committedOct 20, 2024·
add hough transform
1 parent 03a4251 commit 48bba17

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed
 

‎computer_vision/hough_transform.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
The Hough transform can be used to detect lines, circles or
3+
other parametric curves. It works by transforming
4+
the image edge map (obtained using a Sobel Filter) to Polar coordinates
5+
and then selecting local maxima in the Parametric space as lines based on
6+
majority voting.
7+
8+
References:
9+
https://en.wikipedia.org/wiki/Hough_transform
10+
https://www.cs.cmu.edu/~16385/s17/Slides/5.3_Hough_Transform.pdf
11+
https://www.uio.no/studier/emner/matnat/ifi/INF4300/h09/undervisningsmateriale/hough09.pdf
12+
13+
Requirements (pip):
14+
- matplotlib
15+
- cv2
16+
"""
17+
18+
import cv2
19+
import matplotlib.pyplot as plt
20+
import numpy as np
21+
22+
from digital_image_processing.edge_detection import canny
23+
24+
25+
def generate_accumulator(edges: np.ndarray) -> np.ndarray:
26+
"""
27+
- Generates an accumulator by transforming edge coordinates from Cartesian
28+
to polar coordinates (Hough space).
29+
30+
- The accumulator array can be indexed as `accumulator[p][theta]`
31+
32+
Params:
33+
------
34+
edges (np.ndarray): The edge-detected binary image (single-channel).
35+
36+
Returns:
37+
------
38+
np.ndarray: The accumulator array with votes for line candidates.
39+
40+
Example:
41+
------
42+
>>> img = np.array([[1, 0, 0,], [1, 0, 0,], [1, 0, 0,],])
43+
>>> np.sum(generate_accumulator(img))
44+
np.float64(540.0)
45+
"""
46+
n, m = edges.shape
47+
theta_min, theta_max = 0, 180
48+
p_min, p_max = 0, int(n * np.sqrt(2) + 1)
49+
accumulator = np.zeros((int(theta_max - theta_min), int(p_max - p_min)))
50+
for x in range(n):
51+
for y in range(m):
52+
if edges[x][y]:
53+
for theta in range(theta_min, theta_max):
54+
p = int(
55+
x * np.cos(np.deg2rad(theta)) + y * np.sin(np.deg2rad(theta))
56+
)
57+
accumulator[theta][p] += 1
58+
return accumulator
59+
60+
61+
def hough_transform(
62+
img: np.ndarray, threshold: int = 30, max_num_lines: int = 5
63+
) -> list[tuple[int, int, np.float64]]:
64+
"""
65+
Performs the Hough transform to detect lines in the input image.
66+
67+
Params:
68+
------
69+
img (np.ndarray): Single-channel grayscale image.
70+
threshold (int): Minimum vote count in the accumulator to consider a line.
71+
max_num_lines (int): Maximum number of lines to return.
72+
73+
Returns:
74+
------
75+
list[tuple[int, int, int]]: List of detected lines in (theta, p, votes) format.
76+
77+
Raises:
78+
------
79+
AssertionError: If the image is not square or single-channel.
80+
81+
Example:
82+
------
83+
>>> img = np.vstack([np.zeros((30, 50)),np.ones((1, 50)),np.zeros((19, 50))])
84+
>>> hough_transform(img, 30, 1)
85+
[(0, 28, np.float64(48.0))]
86+
"""
87+
assert img.shape[0] == img.shape[1], "image must have equal dimensions"
88+
assert len(img.shape) == 2, "image should be single-channel"
89+
90+
# Obtain edge map for image
91+
edges = canny.canny(img)
92+
93+
# Transform to Polar Coordinates
94+
n, _ = img.shape
95+
theta_min, theta_max = 0, 180
96+
p_min, p_max = 0, int(n * np.sqrt(2) + 1)
97+
accumulator = generate_accumulator(edges)
98+
99+
# Select maxima in Polar space
100+
res = []
101+
for theta in range(theta_min, theta_max):
102+
for p in range(p_min, p_max):
103+
if accumulator[theta][p] > threshold:
104+
res.append((theta, p, accumulator[theta][p]))
105+
106+
res = sorted(res, key=lambda x: x[2], reverse=True)[:max_num_lines]
107+
return res
108+
109+
110+
def draw_hough_lines(
111+
img: np.ndarray,
112+
lines: list[tuple],
113+
thickness: int = 1,
114+
color: tuple[int, int, int] = (255, 0, 0),
115+
) -> None:
116+
"""
117+
Draws detected Hough lines on the image.
118+
119+
Params:
120+
------
121+
img (np.ndarray): The input image to draw lines on.
122+
lines (list[tuple[int, int, int]]):
123+
List of (theta, p, votes) for detected lines.
124+
thickness (int): Line thickness.
125+
color (tuple[int, int, int]): BGR color of the lines.
126+
127+
Example:
128+
------
129+
>>> draw_hough_lines(create_dummy_img(), [(50, 0),])
130+
"""
131+
for line in lines:
132+
theta, p = line[0], line[1]
133+
a = np.sin(np.deg2rad(theta))
134+
b = np.cos(np.deg2rad(theta))
135+
x0, y0 = a * p, b * p
136+
x1, y1 = int(x0 + 100 * (-b)), int(y0 + 100 * (a))
137+
x2, y2 = int(x0 - 100 * (-b)), int(y0 - 100 * (a))
138+
cv2.line(img, (x1, y1), (x2, y2), color, thickness)
139+
140+
141+
def create_dummy_img(height: int = 50, width: int = 50) -> np.ndarray:
142+
"""
143+
Test function to create dummy 3-channel image of specified width and height
144+
Example:
145+
------
146+
>>> create_dummy_img(100, 120).shape
147+
(100, 120, 3)
148+
"""
149+
img = np.zeros((height, width), dtype=np.uint8)
150+
cv2.line(img, (10, 10), (int(0.6 * height), int(0.8 * width)), 255, 1) # type: ignore[call-overload]
151+
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # type: ignore[assignment]
152+
return img
153+
154+
155+
if __name__ == "__main__":
156+
import doctest
157+
158+
# Run doctests
159+
doctest.testmod()
160+
161+
img = create_dummy_img(60, 80)
162+
# Preprocess Image
163+
img = cv2.resize(img, (64, 64), interpolation=cv2.INTER_AREA)
164+
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
165+
plt.imshow(img)
166+
plt.show()
167+
# Accumulator
168+
accumulator = generate_accumulator(canny.canny(gray_image))
169+
plt.imshow(accumulator)
170+
plt.show()
171+
# Hough Transform
172+
res = hough_transform(gray_image, 30, 1)
173+
draw_hough_lines(img, res)
174+
plt.imshow(img)
175+
plt.show()

0 commit comments

Comments
 (0)
Please sign in to comment.