Updated July 31, 2026: The original tutorial, examples, media, narrative, and teaching order are preserved below. The labelled 2026 correction area adds only the verified API, compatibility, validation, and failure-handling changes needed for current OpenCV; where a historical snippet uses an obsolete API, the correction area is the runnable path.
In this post, we will learn how to detect lines and circles in an image, with the help of a technique called Hough transform.
What is Hough transform?
Hough transform is a feature extraction method for detecting simple shapes such as circles, lines etc in an image.
A “simple” shape is one that can be represented by only a few parameters. For example, a line can be represented by two parameters (slope, intercept) and a circle has three parameters — the coordinates of the center and the radius (x, y, r). Hough transform does an excellent job in finding such shapes in an image.
The main advantage of using the Hough transform is that it is insensitive to occlusion.
Let’s see how Hough transform works by way of an example.
Hough transform to detect lines in an image

Equation of a line in polar coordinates
From high school math class we know the polar form of a line is represented as:
(1) ![]()
Here
represents the perpendicular distance of the line from the origin in pixels, and
is the angle measured in radians, which the line makes with the origin as shown in the figure above.
You may be tempted to ask why we did not use the familiar equation of the line given below
![]()
The reason is that the slope, m, can take values between –
to +
. For the Hough transform, the parameters need to be bounded.
You may also have a follow-up question. In the
form,
is bounded, but can’t
take a value between 0 to +
? That may be true in theory, but in practice,
is also bounded because the image itself is finite.
Accumulator
When we say that a line in 2D space is parameterized by
and
, it means that if we any pick a
, it corresponds to a line.
Imagine a 2D array where the x-axis has all possible
values and the y-axis has all possible
values. Any bin in this 2D array corresponds to one line.

This 2D array is called an accumulator because we will use the bins of this array to collect evidence about which lines exist in the image. The top left cell corresponds to a (-R, 0) and the bottom right corresponds to (R,
).
We will see in a moment that the value inside the bin (
,
) will increase as more evidence is gathered about the presence of a line with parameters
and
.
The following steps are performed to detect lines in an image.
Step 1 : Initialize Accumulator
First, we need to create an accumulator array. The number of cells you choose to have is a design decision. Let’s say you chose a 10×10 accumulator. It means that
can take only 10 distinct values and the
can take 10 distinct values, and therefore you will be able to detect 100 different kinds of lines. The size of the accumulator will also depend on the resolution of the image. But if you are just starting, don’t worry about getting it perfectly right. Pick a number like 20×20 and see what results you get.
Step 2: Detect Edges
Now that we have set up the accumulator, we want to collect evidence for every cell of the accumulator because every cell of the accumulator corresponds to one line.
How do we collect evidence?
The idea is that if there is a visible line in the image, an edge detector should fire at the boundaries of the line. These edge pixels provide evidence for the presence of a line.
The output of edge detection is an array of edge pixels ![]()
Step 3: Voting by Edge Pixels
For every edge pixel (x, y) in the above array, we vary the values of
from 0 to
and plug it in equation 1 to obtain a value for
.
In the Figure below we vary the
for three pixels ( represented by the three colored curves ), and obtain the values for
using equation 1.
As you can see, these curves intersect at a point indicating that a line with parameters
and
is passing through them.

Typically, we have hundreds of edge pixels and the accumulator is used to find the intersection of all the curves generated by the edge pixels.
Let’s see how this is done.
Let’s say our accumulator is 20×20 in size. So, there are 20 distinct values of
and so for every edge pixel (x, y), we can calculate 20 (
,
) pairs by using equation 1. The bin of the accumulator corresponding to these 20 values of
is incremented.
We do this for every edge pixel and now we have an accumulator that has all the evidence about all possible lines in the image.
We can simply select the bins in the accumulator above a certain threshold to find the lines in the image. If the threshold is higher, you will find fewer strong lines, and if it is lower, you will find a large number of lines including some weak ones.
HoughLine: How to Detect Lines using OpenCV
In OpenCV, line detection using Hough Transform is implemented in the function HoughLines and HoughLinesP [Probabilistic Hough Transform]. This function takes the following arguments:
- edges: Output of the edge detector.
- lines: A vector to store the coordinates of the start and end of the line.
- rho: The resolution parameter
in pixels. - theta: The resolution of the parameter
in radians. - threshold: The minimum number of intersecting points to detect a line.
Python:
# Read image
img = cv2.imread('lanes.jpg', cv2.IMREAD_COLOR) # road.png is the filename
# Convert the image to gray-scale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Find the edges in the image using canny detector
edges = cv2.Canny(gray, 50, 200)
# Detect points that form a line
lines = cv2.HoughLinesP(edges, 1, np.pi/180, max_slider, minLineLength=10, maxLineGap=250)
# Draw lines on the image
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(img, (x1, y1), (x2, y2), (255, 0, 0), 3)
# Show result
cv2.imshow("Result Image", img)
C++:
// Read the image as gray-scale
Mat img = imread('lanes.jpg', IMREAD_COLOR);
// Convert to gray-scale
Mat gray = cvtColor(img, COLOR_BGR2GRAY);
// Store the edges
Mat edges;
// Find the edges in the image using canny detector
Canny(gray, edges, 50, 200);
// Create a vector to store lines of the image
vector<Vec4i> lines;
// Apply Hough Transform
HoughLinesP(edges, lines, 1, CV_PI/180, thresh, 10, 250);
// Draw lines on the image
for (size_t i=0; i<lines.size(); i++) {
Vec4i l = lines[i];
line(src, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(255, 0, 0), 3, LINE_AA);
}
// Show result image
imshow("Result Image", img);
Line Detection Result
Below we show a result of using hough transform for line detection. Bear in mind the quality of detected lines depends heavily on the quality of the edge map. Therefore, in the real world Hough transform is used when you can control the environment and therefore obtain consistent edge maps or when you can train an edge detector for the specific kind of edges you are looking for.

HoughCircles : Detect circles in an image with OpenCV
In the case of line Hough transform, we required two parameters, (
,
) but to detect circles, we require three parameters
coordinates of the center of the circle.- radius.
As you can imagine, a circle detector will require a 3D accumulator — one for each parameter.
The equation of a circle is given by
(2) ![]()
The following steps are followed to detect circles in an image: –
- Find the edges in the given image with the help of edge detectors (Canny).
- For detecting circles in an image, we set a threshold for the maximum and minimum value of the radius.
- Evidence is collected in a 3D accumulator array for the presence of circles with different centers and radii.
The function HoughCircles is used in OpenCV to detect the circles in an image. It takes the following parameters:
- image: The input image.
- method: Detection method.
- dp: the Inverse ratio of accumulator resolution and image resolution.
- mindst: minimum distance between centers od detected circles.
- param_1 and param_2: These are method specific parameters.
- min_Radius: minimum radius of the circle to be detected.
- max_Radius: maximum radius to be detected.
Python:
# Read image as gray-scale
img = cv2.imread('circles.png', cv2.IMREAD_COLOR)
# Convert to gray-scale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Blur the image to reduce noise
img_blur = cv2.medianBlur(gray, 5)
# Apply hough transform on the image
circles = cv2.HoughCircles(img_blur, cv2.HOUGH_GRADIENT, 1, img.shape[0]/64, param1=200, param2=10, minRadius=5, maxRadius=30)
# Draw detected circles
if circles is not None:
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
# Draw outer circle
cv2.circle(img, (i[0], i[1]), i[2], (0, 255, 0), 2)
# Draw inner circle
cv2.circle(img, (i[0], i[1]), 2, (0, 0, 255), 3)
C++:
// Read the image as gray-scale
img = imread("circles.png", IMREAD_COLOR);
// Convert to gray-scale
gray = cvtColor(img, COLOR_BGR2GRAY);
// Blur the image to reduce noise
Mat img_blur;
medianBlur(gray, img_blur, 5);
// Create a vector for detected circles
vector<Vec3f> circles;
// Apply Hough Transform
HoughCircles(img_blur, circles, HOUGH_GRADIENT, 1, img.rows/64, 200, 10, 5, 30);
// Draw detected circles
for(size_t i=0; i<circles.size(); i++) {
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
circle(img, center, radius, Scalar(255, 255, 255), 2, 8, 0);
}
HoughCircles function has inbuilt canny detection, therefore it is not required to detect edges explicitly in it.
Circle Detection Result
The result of circle detection using Hough transform is shown below. The quality of result depends heavily on the quality of edges you can find, and also on how much prior knowledge you have about the size of the circle you want to detect.


Verified 2026 Implementation and Corrections
Scope of this correction: Clarify standard versus probabilistic output and add deterministic tests. The material below is retained from the tested modernization only where it addresses that scope; it does not replace the original explanation or examples above.
Detect Line Segments with HoughLinesP
The refreshed line pipeline follows five explicit steps:
- Validate a nonempty 8-bit BGR image.
- Convert BGR to grayscale.
- Apply a
5×5Gaussian blur. - Run Canny with low and high thresholds.
- Run
HoughLinesPand normalize the returned endpoints.
def detect_lines(
image_bgr,
*,
canny_low=50,
canny_high=150,
hough_threshold=50,
min_line_length=40,
max_line_gap=25,
):
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, canny_low, canny_high)
raw_lines = cv2.HoughLinesP(
edges,
1.0,
np.pi / 180.0,
hough_threshold,
minLineLength=min_line_length,
maxLineGap=max_line_gap,
)
return edges, normalize_lines(raw_lines)
The rho resolution is one pixel and the angular resolution is one degree. Those values define the accumulator discretization; they do not promise one-pixel or one-degree accuracy in the final scene geometry.
The tested road image produced 7,395 nonzero edge pixels:

HoughLinesP.The Hough stage returned 48 segments. Notice that several segments may describe different parts or sides of one painted lane marking. Hough output is a set of local hypotheses, not a semantic lane model.
Detect Circles with HoughCircles
The circle pipeline converts to grayscale and uses a median blur before HoughCircles. Median filtering is effective at suppressing isolated intensity noise while retaining strong boundaries:
def detect_circles(
image_bgr,
*,
dp=1.2,
min_distance=20.0,
param1=120.0,
param2=30.0,
min_radius=20,
max_radius=60,
):
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
blurred = cv2.medianBlur(gray, 5)
raw_circles = cv2.HoughCircles(
blurred,
cv2.HOUGH_GRADIENT,
dp,
min_distance,
param1=param1,
param2=param2,
minRadius=min_radius,
maxRadius=max_radius,
)
return blurred, normalize_circles(raw_circles)
For the bundled eye image, the command-line default derives min_distance from one quarter of the image height and constrains radii to 25..55 pixels.

The tested parameters returned four circles:

The false positives are instructive. Hough voting sees geometry, not object identity. Radius bounds, a region of interest, contrast checks, symmetry, or a learned eye detector can reject hypotheses that do not belong to the intended class.
Make Detection Results Deterministic
OpenCV may return no detections as None in Python. Application code is simpler when the result always has a stable shape:
def normalize_circles(circles):
if circles is None:
return np.empty((0, 3), dtype=np.float32)
normalized = np.asarray(circles, dtype=np.float32).reshape(-1, 3)
order = np.lexsort(
(normalized[:, 2], normalized[:, 1], normalized[:, 0])
)
return normalized[order]
Line segments receive similar treatment. Each endpoint pair is reordered so the lexicographically smaller point comes first, and then the N×4 rows are sorted. This does not change the geometry; it makes logs, tests, serialization, and Python/C++ comparisons reproducible.
def normalize_lines(lines):
if lines is None:
return np.empty((0, 4), dtype=np.int32)
normalized = np.asarray(lines, dtype=np.int32).reshape(-1, 4).copy()
for row in normalized:
if (int(row[2]), int(row[3])) < (int(row[0]), int(row[1])):
row[:] = (row[2], row[3], row[0], row[1])
return normalized[np.lexsort(
(normalized[:, 3], normalized[:, 2],
normalized[:, 1], normalized[:, 0])
)]
The C++ implementation uses the same normalization and sorting rules with std::vector<cv::Vec4i> and std::vector<cv::Vec3f>.
Run the Python and C++ Examples
Install the Python dependencies:
python3 -m pip install -r requirements.txt
Detect line segments, save the edge map and annotated result, and avoid GUI windows:
python3 hough_lines.py \
lanes.jpg \
--output-dir outputs \
--no-display
Detect circles:
python3 hough_circles.py \
brown-eyes.jpg \
--min-radius 25 \
--max-radius 55 \
--output-dir outputs \
--no-display
Build and test C++:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build --output-on-failure
Run the headless C++ programs:
./build/hough_lines lanes.jpg --output-dir outputs --no-display
./build/hough_circles brown-eyes.jpg --output-dir outputs --no-display
Every CLI honors an explicit positional input path. When no path is supplied, the bundled default is resolved relative to the project, so launching from another working directory remains reliable.
Tested Results
The refreshed code was tested with Python 3.14.3 and OpenCV-Python 4.13.0, AppleClang 21 with native OpenCV 4.12.0, and the official OpenCV 5.0.0 source tag for both Python and C++.
| Validation path | Result |
|---|---|
| Python with OpenCV 4.13.0 | 7 of 7 tests passed |
| Native C++ with OpenCV 4.12.0 | 2 of 2 CTest cases passed |
| Python with exact OpenCV 5.0.0 | 7 of 7 tests passed |
| C++ with exact OpenCV 5.0.0 | 2 of 2 CTest cases passed |
| Bundled line image | 48 segments from 7,395 edge pixels |
| Synthetic line image | 6 segments; required horizontal and diagonal geometry recovered |
| Bundled circle image | 4 circles |
| Synthetic circle image | 1 circle with center and radius inside the required tolerance |
| Empty-result behavior | Stable 0×4 line and 0×3 circle arrays |
| Headless output writes | Four nonempty PNG files produced |
The semantic tests deliberately avoid requiring an exact raw ordering from OpenCV. They normalize outputs, then verify properties that matter: a long horizontal line, a long diagonal line, and a circle near the known synthetic center and radius.
From idea to working model to real-time deployment
Big Vision takes computer vision projects through the full journey, not just the easy parts.

Limitations and Production Guidance
- Hough detection is not recognition. Circular eyebrow texture can vote like an iris; road barriers can vote like lane boundaries.
- Preprocessing controls the evidence. Blur and Canny settings can matter as much as Hough parameters.
- Perspective changes apparent geometry. Parallel world lines converge in the image, and a tilted circle projects to an ellipse.
- Duplicate hypotheses are normal. Merge collinear segments or nearby circles according to application geometry.
- Scale priors matter. Radius and line-length bounds should adapt when camera resolution or object distance changes.
- Counts can vary across images and parameters. The exact metrics reported here belong to the bundled fixtures and tested settings.
- Restrict the search region. A mask or region of interest often provides a larger precision gain than threshold tuning alone.
- Use temporal evidence for video. Tracking and multi-frame confirmation reduce flicker and isolated false positives.
- Measure geometry, not screenshots. Validate endpoints, orientation, center error, radius error, and downstream task performance.
For lanes, a production pipeline may additionally filter by orientation, road-region geometry, vanishing point, and temporal continuity. For eyes, use face landmarks or a detector to define a plausible region before circle voting.
