You found the circles — the sample code draws them happily — and now the line wants numbers: where is the part, how wide is it, pass or fail? Everything the detector knows arrives in one flat result array, and almost every Hough-circle bug traces back to reading that array wrong. This guide shows what each element means, how to pull the center and radius out in the legacy C API, modern C++ and Python, how to prove the parsing with a synthetic test circle, and how to turn a radius in pixels into a diameter in millimeters you can hold against a drawing. The worked example runs at 0.024 mm/px, so a 250 px radius decides a 12.0 ± 0.10 mm diameter. It is written for anyone wiring a camera feed to a decision.
What does the Hough circles result array contain?
Each detected circle comes back as one three-element vector of 32-bit floats — (x, y, radius) — where elements 0 and 1 are the center coordinates in pixels and element 2 is the radius in pixels, so the diameter is always 2 × radius.
The container differs between bindings; the layout does not. The legacy C API stacks the triples in a CvSeq you walk element by element with cvGetSeqElem, the C++ call fills a vector of three-float rows (one Vec3f per circle), and Python returns an (N, 1, 3) float32 NumPy array. One row of three floats per circle, in every case.
| Element | Value | Unit | Example row |
|---|---|---|---|
| p[0] | center x | px | 320 |
| p[1] | center y | px | 240 |
| p[2] | radius | px | 50 |
| 2 × p[2] | diameter | px | 100 |
Expect the center to move in accumulator-cell steps and the radius to be a vote-averaged value, so p[2] = 249.5 px is a normal reading, not an error.
How do you read the center and radius out of the result?
Every binding stores three consecutive floats per circle, so read index 0 as center x, index 1 as center y and index 2 as radius in pixels, and keep all three as float until the moment you draw, print or compare them.
- Make the input 8-bit, single channel and smooth it — a 7 × 7 Gaussian is the usual start, because every noise pixel is a potential phantom edge that votes for a center.
- Set minRadius and maxRadius in pixels around the part you expect. Both are filters on the output array, so a tight bracket is free accuracy.
- Call the detector and loop over the results one element at a time.
- Take x = p[0], y = p[1], r = p[2] as float32. Do not round yet.
- Form the diameter as 2 × r only when you print, draw or compare it.
- Log all three floats unrounded, so a tolerance change next month needs no re-shoot of parts.
Legacy C, with the array walk the C API uses:
/* Detector settings below are illustrative for a 640 x 480 frame. */
CvSeq* results = cvHoughCircles(gray, storage, CV_HOUGH_GRADIENT,
1, /* dp: accumulator res, 1 = full */
100, /* minDist: px between centers */
120, /* param1: Canny high threshold */
40, /* param2: accumulator votes */
40, 60); /* minRadius, maxRadius: px */
for (int i = 0; i < results->total; i++) {
float* p = (float*) cvGetSeqElem(results, i);
float cx = p[0]; /* center x, px */
float cy = p[1]; /* center y, px */
float rad = p[2]; /* radius, px */
float dia = 2.0f * rad; /* diameter, px */
}
Modern C++:
std::vector<cv::Vec3f> circles; // one Vec3f per circle: (x, y, radius)
cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT,
1, 100, 120, 40, 40, 60); // dp, minDist, param1, param2, minR, maxR
for (const cv::Vec3f& c : circles) {
float dia = 2.0f * c[2]; // diameter, px
}
Python, with the guard for the empty result:
import cv2
import numpy as np
gray = cv2.GaussianBlur(img_u8, (7, 7), 2) # img_u8: uint8 grayscale frame
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1, minDist=100,
param1=120, param2=40, minRadius=40, maxRadius=60)
rows = np.zeros((0, 3)) if circles is None else circles.reshape(-1, 3)
for p in rows: # p: float32, [x, y, radius]
cx, cy, rad = float(p[0]), float(p[1]), float(p[2])
print(f'center ({cx:.1f}, {cy:.1f}) px, diameter {2 * rad:.1f} px')
How do you prove you read the array correctly?
Draw a disc with a known center and radius, run the same detector call on it and assert that the returned triple matches within two pixels — the test below puts a disc at (320, 240) px with a 50 px radius and demands the array agree on all three numbers.
import cv2
import numpy as np
truth = (320.0, 240.0, 50.0) # px: center x, center y, radius
gray = np.zeros((480, 640), dtype=np.uint8)
cv2.circle(gray, (320, 240), 50, 255, -1) # filled disc: center, radius in px
circles = cv2.HoughCircles(cv2.GaussianBlur(gray, (7, 7), 2),
cv2.HOUGH_GRADIENT, dp=1, minDist=100,
param1=120, param2=40, minRadius=40, maxRadius=60)
rows = np.zeros((0, 3)) if circles is None else circles.reshape(-1, 3)
assert rows.shape[0] == 1, f'expected 1 circle, got {rows.shape[0]}'
x, y, r = (float(v) for v in rows[0])
assert abs(x - truth[0]) <= 2 and abs(y - truth[1]) <= 2 and abs(r - truth[2]) <= 2
The two-pixel allowance: with dp = 1 the center can only land on whole accumulator cells, and the 7 × 7 Gaussian nudges the edge by about a pixel. Run this in the build whenever the OpenCV version changes — it locks the array layout and the call signature, not the detector's skill on your parts.
How do you convert the radius from pixels to millimeters?
Divide one measured field of view by the image width in pixels to get a scale, then multiply — at 96.0 mm across a 4000-pixel-wide frame the scale is 0.024 mm/px, so a 250 px radius reads 6.0 mm and the diameter 12.0 mm.
The scale needs one honest measurement, not a datasheet number: lay a ruler in the part plane or image a gauge of known width and divide. The ruler-in-scene calibration walk-through automates that readout, and finding camera distance from a tape line's pixel width runs the same pixel-to-world division from the other side.
Worked example: hold a 12.0 ± 0.10 mm bushing diameter
- Field of view, measured with the ruler: 96.0 mm across 4000 px.
- Scale: 96.0 mm ÷ 4000 px = 0.024 mm/px.
- Detector row: p = [1980.0, 1512.0, 250.0].
- Radius: 250 px × 0.024 mm/px = 6.0 mm. Diameter: 2 × 6.0 mm = 12.0 mm.
- Nominal diameter in pixels: 12.0 mm ÷ 0.024 mm/px = 500 px. Band: 0.10 mm ÷ 0.024 mm/px = 4.17 px per side, so the diameter must land between 495.83 and 504.17 px.
- Measured 500 px sits at the band center — pass. Center position: fixture taught (2000, 1500) px with a ± 50 px window; Δx = 20 px, Δy = 12 px — both inside, pass.
- Next part, p[2] = 256.0: diameter = 2 × 256 × 0.024 = 12.29 mm, which is 0.19 mm past the 12.10 mm upper limit — reject.
Move the same job to a station imaging 200 mm across the same 4000 px and the scale becomes 0.05 mm/px: the bushing is now a 240 px diameter and the ± 0.10 mm band shrinks to ± 2 px. The pixels and the band always move together — keeping pixel axes and world units mapped is what keeps those logs honest, and this is the same measurement that picks cylinders by diameter with one camera.
How many pixels does a circle need to hold a tolerance?
Give the detector at least a 10 px radius to lock onto and keep the band 2 px wide or wider — with a ± 0.10 mm spec that means 0.05 mm/px or finer, so a 4000 px frame covers at most 200 mm for that tolerance.
Why these two floors: with dp = 1 one accumulator cell is one pixel, so estimates cannot be finer than the ring itself. A 10 px radius puts about 2π × 10 ≈ 63 edge pixels on the voting ring, enough support for a stable peak; a 5 px ring has half of that and the peak smears into neighboring radii. The 2 px band is the same quantization seen from the tolerance side: narrower bands turn pixel noise into false rejects. Both floors are shop practice — tune them against your own parts before you set the spec.
Choose the camera the other way round: feed the field of view and the smallest feature — here the band itself — to the resolution calculator, and its button opens a camera finder already filtered to that pixel count.
What hardware keeps the circle measurement stable?
Three things decide whether yesterday's numbers hold today: a camera whose pixel count meets the scale, a stand that cannot drift, and fixed exposure so the same edges vote for the same center every frame.
The worked example needs 0.024 mm/px over 96.0 mm, which is 4000 px across.
The LS-CMR-0166 delivers 4000 × 3000 pixels over USB3 at 30 fps.
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
An 8-bit mono frame that size carries 4000 × 3000 = 12 MB, so 30 fps means 360 MB/s. USB 3.0 moves 5 Gbit/s — 500 MB/s before 8b/10b coding overhead, roughly 400 MB/s in practice — so the stream fits with headroom.
Rolling shutter smears a disc that moves during the scan, so belt speeds want a global shutter.
The MV-CH120-11UC is a 12 MP USB3 camera with a global shutter.
- Sensor format
- 1.1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 12 MP
Lock exposure and gain manually — the live parameter-control guide shows which knobs — because auto-exposure rescales edge contrast frame by frame and the radius votes wander with it.
A scale you calibrated only survives if the camera cannot move; a knocked tripod re-runs the ruler calibration for you, badly. Bolt the rig down.
The LS-MNT-0040 stands 700 mm tall on a 400 × 450 mm base.
- Type
- Camera stands
Compute cost grows with the accumulator, so crop to the fixture region before the call — zeroing pixels outside an ROI does it without a per-pixel loop. For the capture loop itself, the live-feed circle detection walk-through wires camera, smoothing and detector end to end.
When is the raw circle array not the end of the job?
Stop at the raw triple only when the part sits still and the band is 2 px wide or wider; for sub-pixel centers, several candidate rows or a part you must follow between frames, refine the array before you decide.
Sub-pixel: the center moves in accumulator-cell steps, so a ± 1 px floor is built in. When the drawing wants better, fit the edge in grayscale — the trick behind the sub-pixel grayscale centroid transfers directly to circle edge points.
Several rows: near-duplicate centers usually mean minDist sits below your own peak's jitter. What the minDist parameter really filters covers the mechanics; raise it, then pick the surviving row nearest the taught position.
Tracking: pin a coordinate system to the center once and the rest of the numbers follow the part — setting a coordinate system to a circle center walks that loop.
The decision, per case:
- Stationary part, band ≥ 2 px: read p[0], p[1], p[2]; diameter = 2 × p[2]; compare in pixels inside the band from tol_px = tol_mm / scale.
- Band under 2 px: shrink the field of view or add pixels until tol_mm / scale ≥ 2 px — at 12 MP and ± 0.10 mm that ceiling was 200 mm of field.
- Moving part: global shutter, locked exposure, then the same array math.
- Several rows: bracket with minRadius and maxRadius, raise minDist until duplicates die, pick the row nearest the taught center.
Size the camera on the area scan cameras page or the camera finder, fix it to one of the camera stands, and the system configurator walks the rest of the chain. More image-processing guides sit on the systems topic page.
Questions
Does dp change the units of the returned circles?
No. dp sets only the internal accumulator grid; x, y and radius always come back in pixels of the input image, and minRadius and maxRadius are pixels too. With dp = 1 the center moves in 1 px steps; a dp of 1.5 coarsens that step by half again while buying you speed.
Where is the coordinate origin for the circle center?
At the top-left of the input image: x grows right, y grows down, and the first pixel row is y = 0. Hand the center to a robot or PLC only through one conversion function — image y down versus machine Z up is the classic sign flip — and prove it once by moving a part +10 mm and reading the sign.
How do I pick the right circle when the array holds several?
The array carries no promise about spatial order, so filter before you read: bracket the part with minRadius and maxRadius, then take the row nearest the fixture's taught center, or the largest radius if your part is the biggest disc in view. Log the rejected rows so you can see what the detector actually returned.
Can HoughCircles measure an elliptical part?
Not as an ellipse. The gradient method fits circles only, so an elliptical part returns a radius somewhere between its minor and major radii, and the reported diameter drifts as the part rotates. Fit an ellipse to the outline instead when the aspect ratio departs from round by more than a few percent.
What do param1 and param2 change in my result array?
Neither changes the (x, y, radius) layout. param1 is the high Canny threshold of the gradient pass; param2 is the accumulator vote threshold. Raise param1 when blur or glare floods the frame with weak edges, and raise param2 when rows appear where no part sits — each phantom center then needs more votes to survive.


