System designExplainer

OpenCV Hough circle minDist parameter: what it really filters

What the minDist parameter filters in OpenCV HoughCircles, why CUDA results differ from the CPU, and the radius-aware post-filter that removes duplicates.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01What the minDist parameter actually filters
  2. 02How the CPU function applies it
  3. 03Why the CUDA detector needs your own filter
  4. 04A radius-aware distance filter, with a test
  5. 05Choosing the number from part pitch
  6. 06What minDist cannot do
  7. 07Where the detection runs
  8. 08Checks before you trust the output
  9. 09Questions

A circle detector that returns three circles for one part is not badly tuned; it is showing you what the minDist parameter does and does not filter. minDist is the minimum distance in pixels between two accepted circle centers, nothing more. The CPU implementation in OpenCV applies it as a global merge after voting; the CUDA detector is a separate implementation without that guarantee, so near-duplicates and run-to-run variation survive wherever you set the value. This guide gives the rule as the reference defines it, the arithmetic that sets a starting value from part pitch and pixel scale, and a radius-aware filter that separates true neighbors from duplicates while keeping concentric circles. The same numbers apply on a GPU PC fed by 10GigE cameras and on a smart camera running inspection on the device.

What the minDist parameter actually filters

minDist filters centers, not circles. The OpenCV Hough Circle Transform tutorial states the contract in one line: "Minimum distance between the centers of the detected circles." Every accepted center must sit at least minDist pixels from every other accepted center, measured straight-line in the pixel coordinates the detector returns. No radius enters the test.

d = sqrt((x1 - x2)^2 + (y1 - y2)^2) a candidate center is dropped when d < minDist against any accepted center

Three consequences follow. Distance zero, the case of two circles sharing one center, is below any positive minDist, so a merged result can never contain a concentric pair. minDist says nothing about whether a circle is real; it only decides whether its center counts as new. And because the rule is purely geometric, one number in pixels fits any image size, as long as you recompute it when the pixel scale changes.

How the CPU function applies it

In cv::HoughCircles with HOUGH_GRADIENT, edges come from Canny (param1 is the higher threshold, the lower one is half of it), then centers must pass the accumulator vote threshold (param2). Candidates are accepted strongest-first, and one joins only if it lies at least minDist from every center already accepted. The merge is global and deterministic: the same image and parameters return the same circles in the same order on every run.

Concentric features show the cost. A washer rim of radius 40 px and a bore of radius 15 px share one center, 0 px apart, so the function returns whichever radius collected more votes and drops the other, whatever minRadius and maxRadius allow. Isolate a feature with radius windows, not minDist: minRadius 30 with maxRadius 50 keeps the rim, and a second pass at 10 to 25 px keeps the bore.

Why the CUDA detector needs your own filter

cv::cuda::HoughCirclesDetector is a separate implementation, not a port of the CPU merge. For the same input it can return a different circle set than cv::HoughCircles; users have reported this difference on the OpenCV tracker. The differences that matter on the bench:

Symptom at minDist 20 px CPU cv::HoughCircles CUDA cv::cuda::HoughCirclesDetector
Near-duplicate centers merged can survive, radii 2-5 px apart
Concentric circles, one center one radius returned; use radius windows both radii can be returned; keep both with the filter
Order and count across runs identical can vary

Where the GPU merge falls short inside the implementation is not documented; the behavior is what is reproducible. So stop treating the CUDA minDist as a guarantee. Set it from geometry, as the next section shows, then treat the output as a candidate list and deduplicate it yourself. A dozen lines of filtering behave identically on every run; an experimental sweep of the parameter does not, and a sweet spot found on one frame will not survive the next batch.

A radius-aware distance filter, with a test

Keep a candidate only if no kept circle has both a center within the distance tolerance and a radius within the radius tolerance. The two conditions together remove the near-duplicates the CUDA detector leaks and preserve concentric circles, which the CPU merge cannot return.

import math
import numpy as np

# Illustrative filter, not part of OpenCV. circles is an (N, 3) float32
# array; each row is [x_center, y_center, radius] in pixels.
def filter_circles(circles, min_center_dist, radius_tol):
    keep = []                                   # list of kept row indexes
    for i in range(int(circles.shape[0])):
        duplicate = False
        for j in keep:
            dx = float(circles[i, 0] - circles[j, 0])
            dy = float(circles[i, 1] - circles[j, 1])
            d_center = math.hypot(dx, dy)       # Euclidean center distance
            d_radius = abs(float(circles[i, 2] - circles[j, 2]))
            if d_center < min_center_dist and d_radius < radius_tol:
                duplicate = True
                break
        if not duplicate:
            keep.append(i)
    return circles[keep]

# Test: four raw detections, min_center_dist = 10, radius_tol = 5
dets = np.array([[120, 90, 40],    # accepted
                 [121, 92, 43],    # duplicate of row 0
                 [300, 90, 40],    # different part
                 [120, 90, 15]],   # concentric bore
                dtype=np.float32)
print(filter_circles(dets, 10, 5))              # rows 0, 2, 3 survive

Walk the arithmetic. Row 1 against row 0: dx = 1, dy = 2, so d = sqrt(1 + 4) = 2.24 px, inside the 10 px center tolerance, and the radii differ by 3 px, inside the 5 px tolerance: dropped. Row 2 against row 0: d = 180 px, far outside 10 px: kept. Row 3 against row 0: centers coincide at d = 0 but the radii differ by 25 px, outside the tolerance: the concentric bore survives. Output is three circles, one per physical feature, and running the test prints exactly those rows.

The worst case compares every pair of N candidates, N(N-1)/2 checks: 4,950 at 100 candidates, a few milliseconds even in interpreted Python. Frame time lives in the detector, not here; the guide to per-pixel loops that kill FPS covers that side. The filter slots into a grab loop like the one in the live feed circle detection walkthrough.

Choosing the number from part pitch

Set minDist from geometry, not from sweeps: half the smallest true center-to-center spacing you must keep separate, measured in pixels.

scale = FOV_mm / image_width_px spacing_px = pitch_mm / scale minDist_start = 0.5 * spacing_px

Worked example. A 2448 x 2048 (5 MP) monochrome camera watches a cap-sorting station with a 120 mm wide field of view. Scale: 120 / 2448 = 0.049 mm/px. Caps of 28 mm diameter sit on a 40 mm pitch, so true centers are 40 / 0.049 = 816 px apart and each cap radius is 14 / 0.049 = 286 px. Start at half the spacing: 0.5 x 816 = 408 px, rounded to 400. The margins are wide on both sides. Reported duplicates sit within a few pixels of their originals, so 400 px carries over a hundredfold margin above the roughly 3 px jitter, while true neighbors at 816 px hold a factor-of-two margin against a false merge.

Two bounds finish the choice. The upper bound is the pitch itself: never set minDist at or above the 816 px spacing. The lower bound comes from dp: at dp = 1 the accumulator runs at image resolution and any value above a couple of pixels is meaningful, while at dp = 2 the bins are 2 px wide and sub-bin values do nothing. Sweep the chosen value plus or minus 50% over a labeled frame set before release; if the circle count moves, the pitch or scale input is wrong, not the detector.

What minDist cannot do

minDist merges coincident centers; it never tests whether a circle exists. A false center 500 px from any true center passes every minDist from 1 to 800 px at the station above. The levers that cut false detections are param2, the accumulator vote threshold, where raising it stops weak centers from surviving, and the radius window: minRadius 257 and maxRadius 315, about plus or minus 10% of the 286 px cap radius, reject wrong-size blobs before the merge. Smoothing helps too: a Gaussian blur of about 1 to 2 px sigma stabilizes the gradients Canny votes on.

Genuinely touching objects are a different failure. Their centers are close but the objects are real, so a large minDist destroys true detections; that is a segmentation problem, and the watershed distance-map guide shows the standard fix. The same lesson runs through the whole Hough family: the crack detection guide that moves from Hough lines to contour statistics is what it looks like when a parameter has hit its limit and the method must change.

Where the detection runs

The CUDA path needs a PC with a GPU, so the numbers above ride on the camera link. A 2448 x 2048 8-bit frame is about 5 MB, and 60 fps per camera is roughly 300 MB/s: one stream like that fills a GigE link, and two or three fit under 10GigE with headroom. The MV-GT2104 quad-port copper 10GbE NIC puts four 10GigE camera ports into one processing PC.

Frame grabbersHikrobot MV-GT2104 Quad-port copper 10GbE NICMV-GT2104
Interface
10GigE
Ports
4 ports
$1,429.00View partBuild a system with it

CoaXPress and Camera Link cameras land on frame grabbers instead; the detector code stays the same and only the acquisition layer changes. The accessories finder filters the cards by interface and port count.

If the inspection is one fixed scene at one station, a vision sensor removes the PC pipeline entirely: the MV-SC3050XC-16M-ATN-H is a 5 MP color vision sensor with a global shutter and an M12 mount that inspects on the device. Wherever the circle find lives, on-device or on the GPU PC, the same center-spacing decision appears, and the pitch formula above sets it. Browse smart cameras and vision sensors or the camera finder for the on-device options.

Smart cameras & vision sensorsHikrobot MV-SC3050XC-16M-ATN-H 5 MP color vision sensorMV-SC3050XC-16M-ATN-H
Interface
Fast Ethernet
Resolution
5 MP
Sensor format
1/1.45"
Shutter
Global
$1,809.00View part

Once centers survive the filter they feed the rest of the cell: the circle-center color measurement walkthrough starts from located centers, and the coordinate-system guide turns one center into a tracking frame for moving parts.

Checks before you trust the output

  • Synthetic first: draw 10 circles of radius 286 px spaced 816 px apart on a blank 2448 px-wide image and require exactly 10 centers back, each within 2 px of truth at dp = 1.
  • Labeled set: run 20 to 50 real frames with counted parts and demand zero merges and zero duplicates across the set.
  • GPU stability: run one frame 10 times; after your filter the kept set must be identical every run. If it is not, loosen the radius tolerance from 5 px upward until it is.
  • Concentric case: a washer with radii 40 px and 15 px on one center must return both circles after filtering.
  • Stress: sweep minDist from 200 to 600 px; a changing circle count means the pitch or scale input is wrong.

Set the value from pitch, filter the output, and the CPU-versus-CUDA question stops mattering: both paths return the same physical circles. To check the hardware side against those link numbers, build a system in the configurator or reach us through the contact page. More detection work sits on the systems topic page.

Questions

Does minDist remove false circle detections?

No. It only merges centers that sit close together; a false center hundreds of pixels from any true center passes every minDist value. Cut false detections with the accumulator threshold instead: raising param2 keeps only centers backed by more edge votes, and tightening minRadius and maxRadius to about plus or minus 10 percent of the true radius removes most of the rest.

Why does the CPU function return only one radius for concentric circles?

Concentric centers are 0 px apart, and 0 is below any positive minDist, so the merge treats the second radius as a duplicate of the first and drops it, whatever minRadius and maxRadius allow. To capture both radii, run two passes with non-overlapping radius windows, or run the CUDA detector and keep both with a filter that compares radii before merging.

What dp value should I pair with minDist?

Start at dp = 1: the accumulator then runs at image resolution, centers come back good to a pixel or two, and every minDist value above about 2 px is meaningful. At dp = 2 the accumulator bins are 2 px wide, so centers quantize to the bin grid and minDist values below one bin cannot separate anything. Move to dp = 2 only when the vote image will not fit in memory, and treat sub-bin settings as dead.

How far apart do GPU duplicate centers actually sit?

Close enough that a small tolerance catches them. Reported cases show duplicates at nearly the same position with radii 2-5 px apart, surviving even at minDist values like 20 px. A center tolerance of 10-20 px and a radius tolerance of 5 px removes them, while parts spaced hundreds of pixels apart stay untouched.

How many circles can the pairwise filter handle?

The filter compares every returned center with every kept center, so the cost grows with the square of the candidate count: at most N(N-1)/2 checks. At 100 candidates that is 4,950 distance checks, a few milliseconds in interpreted Python. At 1,000 candidates it is about 499,500 checks, which is where you should cut candidates with param2 before filtering.