Sub-Pixel Circle Fitting: Breaking the 0.1 px Barrier
A deep dive into how RANSAC, sector-centroid sampling, and Devernay sub-pixel edge detection combine to achieve circle-centre accuracy below 0.1 pixels.
Pixel-level circle fitting is easy. Sub-pixel fitting β stable, robust, and accurate to a tenth of a pixel β requires three ideas working together. This post unpacks each one.
Why Pixel-Level Fitting Falls Short
Standard Hough-circle detection and even basic least-squares fits operate on integer-coordinate edge pixels. The quantisation error alone introduces ~0.5 px bias, which is unacceptable for precision measurement (wafer holes, bearing races, lens apertures).
To reach < 0.1 px, you need three upgrades:
- Sub-pixel edge localisation β move edge coordinates off the integer grid
- Robust fitting β reject outlier edges caused by noise, scratches, or partial occlusion
- Uniform angular sampling β prevent dense local edges from biasing the fit
Step 1 β Devernay Sub-Pixel Edge Localisation
Canny produces edges snapped to integer pixels. Devernay's algorithm instead fits a 1-D quadratic to the gradient profile perpendicular to the edge:
The sub-pixel position is the quadratic's maximum:
Applied to each Sobel-filtered scan line, this moves the detected edge point to a floating-point coordinate with ~0.1 px localisation accuracy on clean industrial images.
Step 2 β Annular ROI + Sector Centroids
Raw edge images contain hundreds of irrelevant edges (text, PCB traces, shadows). The algorithm restricts processing to a ring-shaped Region of Interest centred on the expected circle:
inner_radius < dist(p, roi_centre) < outer_radius
Inside the ring, points are bucketed into 24 angular sectors (15Β° each). Each sector's centroid becomes one representative sample point. This achieves two things:
- Robustness to occlusion β only sectors with insufficient points are skipped; the rest still vote
- Bias elimination β a scratch producing 500 local edge points gets the same vote as a clean 10-point sector
Step 3 β RANSAC + LevenbergβMarquardt
Three non-collinear points uniquely determine a circle. Given points the centre satisfies the linear system obtained by expanding:
RANSAC samples 3 sector centroids per iteration, fits a candidate circle, counts how many of the remaining centroids are within inlier_dist pixels of that circle, and keeps the best hypothesis after 1000 iterations.
The RANSAC winner is then passed to LevenbergβMarquardt with all inliers:
This final refinement eliminates the remaining discretisation bias and pushes accuracy below 0.1 px.
Accuracy Breakdown
| Stage | Typical Centre Error |
|---|---|
| Hough (integer pixels) | ~0.5β1.0 px |
| RANSAC on Canny edges | ~0.3β0.5 px |
| + Sector centroids | ~0.2β0.3 px |
| + Devernay sub-pixel edges | ~0.08β0.12 px |
| + LM refinement | < 0.1 px |
Practical Code
In VisionLab, the full pipeline is one API call:
CircleFitParams p;
p.roi_center = cv::Point2f(320, 240);
p.roi_inner_radius = 80;
p.roi_outer_radius = 120;
p.ransac_iter = 1000;
p.inlier_dist = 2.0f;
p.use_devernay = true;
cv::Point2f centre;
float radius;
fitCircleRobustAdvanced(gray, p, centre, radius);
Or via the Plugin SDK with a JSON parameter string β no OpenCV in the host process required.