API reference
Module
EdgesGauging.EdgesGauging — Module
EdgesGaugingProvides subpixel-accurate edge detection in images and robust geometric fitting (line, circle) using RANSAC with outlier rejection.
Quick start
using EdgesGauging
# 1D edge detection in a pixel profile
result = gauge_edges_in_profile(profile, 2.0, 0.1, POLARITY_POSITIVE, SELECT_BEST)
# Fit a circle to an image region
fit = gauge_circle(image, (row_c, col_c), 0.0, 2π, deg2rad(3.0), 80, 2.0, 0.1;
constraints = CircleConstraints{Float64}(min_radius=10.0, max_radius=200.0))
println("Centre: (", fit.cx, ", ", fit.cy, ") radius: ", fit.r)Enumerations
EdgesGauging.EdgePolarity — Type
EdgePolaritySpecifies which gradient direction constitutes a detectable edge.
| Value | Meaning |
|---|---|
POLARITY_POSITIVE | Dark-to-bright (rising) transitions only. |
POLARITY_NEGATIVE | Bright-to-dark (falling) transitions only. |
POLARITY_ANY | Both rising and falling transitions. |
Examples
julia> POLARITY_POSITIVE isa EdgePolarity
true
julia> length(instances(EdgePolarity))
3EdgesGauging.EdgeSelector — Type
EdgeSelectorControls how many edges are returned from a single 1-D profile scan.
| Value | Meaning |
|---|---|
SELECT_FIRST | Return only the first edge (leftmost / topmost). |
SELECT_LAST | Return only the last edge (rightmost / bottommost). |
SELECT_BEST | Return only the strongest edge (highest gradient). |
SELECT_ALL | Return all edges above the strength threshold. |
Examples
julia> SELECT_ALL isa EdgeSelector
true
julia> length(instances(EdgeSelector))
4EdgesGauging.ScanOrientation — Type
ScanOrientationPrimary scan direction for 2-D image edge detection.
| Value | Profiles run along… | Scan moves… |
|---|---|---|
LEFT_TO_RIGHT | Rows (horizontal) | Left to right |
RIGHT_TO_LEFT | Rows (horizontal) | Right to left |
TOP_TO_BOTTOM | Columns (vertical) | Top to bottom |
BOTTOM_TO_TOP | Columns (vertical) | Bottom to top |
Examples
julia> LEFT_TO_RIGHT isa ScanOrientation
true
julia> length(instances(ScanOrientation))
4EdgesGauging.InterpolationMode — Type
InterpolationModeSelects how an image is sampled at sub-pixel positions when extracting profiles along arbitrary geometric paths.
| Value | Method | Sub-pixel quality |
|---|---|---|
INTERP_NEAREST | Piecewise constant | Returns the nearest pixel value (no blending). |
INTERP_BILINEAR | 2×2 linear blend | C0 continuous; small bias at pixel boundaries. |
INTERP_BICUBIC | 4×4 cubic B-spline | C2 continuous; best sub-pixel edge stability. |
INTERP_BICUBIC is the recommended default for sub-pixel edge detection because the gradient profile remains smooth across pixel boundaries, which keeps the parabolic sub-pixel fit unbiased.
Coordinate convention
Integer indices lie at pixel centres: image[i, j] is the value at (row, col) = (i, j), and the pixel itself spatially covers (i-0.5, j-0.5) … (i+0.5, j+0.5). The full image extends from (0.5, 0.5) to (nrows+0.5, ncols+0.5). Sample positions outside that range yield NaN.
Examples
julia> INTERP_BICUBIC isa InterpolationMode
true
julia> length(instances(InterpolationMode))
3Result types
EdgesGauging.EdgeResult — Type
EdgeResult{T<:AbstractFloat}A single edge detected in a 1-D intensity profile.
Fields
position::T: Sub-pixel position along the profile (1-based index units). Determined by fitting a parabola to the local gradient extremum.strength::T: Absolute gradient value at the extremum — proportional to the contrast of the edge.
Examples
julia> e = EdgeResult{Float64}(12.3, 95.0);
julia> e.position
12.3
julia> e.strength
95.0EdgesGauging.ProfileEdgesResult — Type
ProfileEdgesResult{T<:AbstractFloat}Full return value of gauge_edges_in_profile.
Fields
edges::Vector{EdgeResult{T}}: Detected edges, ordered by position.smoothed::Vector{T}: Gaussian-smoothed copy of the input profile.gradient::Vector{T}: Discrete gradient ([-1,0,1] kernel) ofsmoothed.
EdgesGauging.ImageEdge — Type
ImageEdge{T<:AbstractFloat}A single edge detected in a 2-D image, returned by gauge_edges and related scanning functions.
Fields
x::T: Column coordinate (horizontal, 1-based).y::T: Row coordinate (vertical, 1-based).strength::T: Absolute gradient value at the edge — indicates contrast.scan_index::Union{Int,Nothing}: Index of the row or column profile that produced this edge (useful for tracing which strip produced each point), ornothingfor scans that do not have a meaningful strip index (e.g. radial scans produced bygauge_circular_edge_pointsandgauge_ring_edge_points).
Examples
julia> e = ImageEdge{Float64}(40.5, 12.0, 88.0, 3);
julia> e.x
40.5
julia> e.scan_index
3
julia> radial = ImageEdge{Float64}(40.5, 12.0, 88.0, nothing);
julia> isnothing(radial.scan_index)
trueEdgesGauging.LineFit — Type
LineFit{T<:AbstractFloat}Result of gauge_line: a RANSAC-fitted infinite line.
The line is represented in normalised implicit form Ax + By + C = 0 (A² + B² = 1), so the perpendicular distance from any point (x, y) to the line equals |Ax + By + C| directly.
Fields
A,B,C: Normalised line coefficients (A² + B² = 1).inliers::Vector{Int}: Indices into the edge-point array of inlier points.outliers::Vector{Int}: Indices of outlier points.rms::T: Root-mean-square perpendicular distance of inliers from the line.
Examples
julia> f = LineFit{Float64}(0.0, 1.0, -5.0, [1,2,3], [4], 0.02);
julia> f.A^2 + f.B^2 # normalised
1.0
julia> f.rms
0.02EdgesGauging.CircleFit — Type
CircleFit{T<:AbstractFloat}Result of gauge_circle: a RANSAC-fitted circle.
Fields
cx,cy: Centre coordinates (column, row; 1-based).r: Radius in pixels.inliers::Vector{Int}: Indices of inlier edge points.outliers::Vector{Int}: Indices of outlier edge points.rms::T: Root-mean-square radial distance of inliers from the circle.
Examples
julia> f = CircleFit{Float64}(100.0, 100.0, 45.0, collect(1:60), Int[], 0.3);
julia> f.r
45.0
julia> isempty(f.outliers)
trueConstraint types
EdgesGauging.LineConstraints — Type
LineConstraints{T<:AbstractFloat}Geometric constraints applied during RANSAC line fitting (used by gauge_line and ransac2).
All fields have keyword-argument constructors with the defaults shown.
| Field | Default | Meaning |
|---|---|---|
min_angle | -π/2 | Minimum allowed line orientation angle (radians). |
max_angle | π/2 | Maximum allowed line orientation angle (radians). |
min_inlier_ratio | 0.1 | Minimum fraction of points that must be inliers. |
min_inlier_count | 2 | Absolute minimum number of inlier points required. |
The angle is the orientation of the line, measured from the positive x-axis and normalised to [-π/2, π/2]. A horizontal line has angle 0; a vertical line has angle ±π/2.
Examples
julia> c = LineConstraints{Float64}(min_angle=0.0, max_angle=Float64(π/4));
julia> c.min_angle
0.0
julia> c.min_inlier_count # default
2EdgesGauging.LineSegmentConstraints — Type
LineSegmentConstraints{T<:AbstractFloat}Constraints for LineSegmentModel RANSAC fitting. Extends LineConstraints with segment-length limits.
| Field | Default | Meaning |
|---|---|---|
min_angle | -π/2 | Minimum allowed orientation angle (radians). |
max_angle | π/2 | Maximum allowed orientation angle (radians). |
min_inlier_ratio | 0.1 | Minimum inlier fraction. |
min_inlier_count | 2 | Minimum number of inliers. |
min_length | 0.0 | Minimum projected span of inlier points (pixels). |
max_length | Inf | Maximum projected span (pixels). |
EdgesGauging.CircleConstraints — Type
CircleConstraints{T<:AbstractFloat}Geometric constraints applied during RANSAC circle fitting (used by gauge_circle and ransac2).
| Field | Default | Meaning |
|---|---|---|
min_radius | 0.0 | Minimum allowed circle radius (pixels). |
max_radius | Inf | Maximum allowed circle radius (pixels). |
min_completeness | 0.1 | Minimum arc completeness fraction (0–1). |
min_inlier_count | 3 | Absolute minimum number of inlier points. |
Arc completeness measures what fraction of the full 360° is covered by inlier points, computed in 30° sectors by arc_completeness.
Examples
julia> c = CircleConstraints{Float64}(min_radius=10.0, max_radius=50.0);
julia> c.min_radius
10.0
julia> c.min_completeness # default
0.1Errors
EdgesGauging.GaugingError — Type
GaugingError(reason, msg) <: ExceptionRaised by gauge_line and gauge_circle when a gauging pipeline cannot produce a valid fit. The reason field is a Symbol so callers can branch on the failure mode programmatically.
Fields
reason::Symbol— one of::too_few_points— edge detection returned fewer points than the fitter requires.:ransac_failed— RANSAC exhausted its iteration budget without finding a model that satisfies the constraints. This includes arc-completeness failure for circles (enforced inside RANSAC viadata_constraints_met).
msg::String— human-readable detail (safe to show to users).
Examples
julia> e = GaugingError(:too_few_points, "only 1 point detected");
julia> e.reason
:too_few_points
julia> e isa Exception
trueProfile extraction
EdgesGauging.extract_line_profile — Function
extract_line_profile(image, p0_rc, p1_rc;
width=1, n_samples=0, interp=INTERP_BICUBIC)
-> Vector{Float64}Sample the image along the straight line segment from p0_rc to p1_rc and return the resulting 1-D intensity profile.
Arguments
image: 2-D array (matrix). Any element type convertible toFloat64.p0_rc,p1_rc: segment endpoints as(row, col)tuples (1-based, pixel-centre convention; seeInterpolationMode). May lie outside the image — out-of-bounds samples becomeNaN.width::Int = 1: number of samples taken perpendicular to the segment at each centreline position.1extracts a slim profile; values > 1 averagewidthparallel samples spaced 1 px apart and centred on the segment.n_samples::Int = 0: number of samples along the segment.0means auto — picksmax(2, ⌈hypot(Δrow, Δcol)⌉ + 1)so the centreline samples are spaced ≈ 1 px apart.interp::InterpolationMode = INTERP_BICUBIC: sampling method —INTERP_NEAREST,INTERP_BILINEAR, orINTERP_BICUBIC. Bicubic is recommended for sub-pixel edge detection because the resulting gradient profile is smooth across pixel boundaries.
Strip aggregation
For width > 1, samples at the same centreline position but different perpendicular offsets are averaged. Out-of-bounds perpendicular samples are excluded from the mean — the position only becomes NaN if all perpendicular samples are outside the image. This keeps strips that graze the image border usable.
Returns
A Vector{Float64} of length n_samples (or the auto-derived count). Entries where every sample at that centreline position fell outside the image are NaN.
Examples
julia> img = Float64[10*r + c for r in 1:5, c in 1:5];
julia> p = extract_line_profile(img, (3.0, 1.0), (3.0, 5.0); width=1, interp=INTERP_NEAREST);
julia> p == [31.0, 32.0, 33.0, 34.0, 35.0]
truejulia> img = fill(7.0, 5, 5);
julia> p = extract_line_profile(img, (3.0, 1.0), (3.0, 5.0); width=3, interp=INTERP_NEAREST);
julia> all(==(7.0), p)
truejulia> img = Float64[10*r + c for r in 1:5, c in 1:5];
julia> p = extract_line_profile(img, (3.0, 4.0), (3.0, 7.0); n_samples=4, interp=INTERP_NEAREST);
julia> p[1:2] == [34.0, 35.0]
true
julia> isnan(p[3]) && isnan(p[4])
trueEdgesGauging.extract_arc_profile — Function
extract_arc_profile(image, center_rc, radius, start_angle, end_angle;
width=1, n_samples=0, interp=INTERP_BICUBIC)
-> Vector{Float64}Sample the image along a circular arc and return the resulting 1-D intensity profile.
Arguments
image: 2-D array (matrix).center_rc: arc centre as a(row, col)tuple (1-based, pixel-centre convention).radius: arc radius in pixels.start_angle,end_angle: angles in radians, measured counter-clockwise from the +column axis (0= pointing right). The arc traverses fromstart_angletoend_angle; passend_angle < start_anglefor a clockwise arc.width::Int = 1: number of samples taken perpendicular to the arc at each centreline position. The perpendicular to an arc is the radial direction, sowidth > 1widens the band radially (inside ↔ outside the nominal radius). Samples are 1 px apart and centred on the arc.n_samples::Int = 0: number of samples along the arc.0means auto — picksmax(2, ⌈|end_angle - start_angle| · radius⌉ + 1), giving an arc-length-uniform spacing of ≈ 1 px regardless of radius.interp::InterpolationMode = INTERP_BICUBIC: sampling method.
Returns
A Vector{Float64}. Entries become NaN where every perpendicular sample at that arc position lies outside the image.
Examples
julia> img = [hypot(r - 10.0, c - 10.0) < 5.0 ? 100.0 : 0.0 for r in 1:20, c in 1:20];
julia> p = extract_arc_profile(img, (10.0, 10.0), 3.0, 0.0, 2π; n_samples=24, interp=INTERP_NEAREST);
julia> all(==(100.0), p)
truejulia> img = ones(20, 20);
julia> p1 = extract_arc_profile(img, (10.0, 10.0), 3.0, 0.0, π); # auto-density
julia> p2 = extract_arc_profile(img, (10.0, 10.0), 6.0, 0.0, π);
julia> isapprox(length(p2), 2 * length(p1); atol=2)
trueEdge detection
EdgesGauging.gauge_edges_in_profile — Function
gauge_edges_in_profile(profile, sigma, threshold,
polarity=POLARITY_ANY, selector=SELECT_ALL)
-> ProfileEdgesResult{Float64}Detect edges in a 1-D intensity profile using Gaussian smoothing, a symmetric gradient kernel, and parabolic sub-pixel interpolation.
Arguments
profile: 1-D array of pixel intensities (any numeric element type).sigma: Gaussian smoothing standard deviation (pixels). Pass0or a non-positive value to skip smoothing entirely.threshold: minimum edge strength (|gradient|at the extremum) to report. Edges weaker than this are discarded.polarity: which gradient directions to detect —POLARITY_POSITIVE(dark→bright),POLARITY_NEGATIVE(bright→dark), orPOLARITY_ANY(both).selector: how many edges to return per profile —SELECT_FIRST,SELECT_LAST,SELECT_BEST(strongest), orSELECT_ALL.
Returns
A ProfileEdgesResult containing:
edges: detected edges, each with a sub-pixelposition(1-based) andstrength.smoothed: Gaussian-smoothed copy of the input profile.gradient: discrete[-1, 0, 1]derivative of the smoothed profile.
Profiles shorter than 3 elements return an empty edge list without error.
Sub-pixel interpolation
The extremum position is refined by fitting a parabola through the three samples around the gradient peak:
offset = 0.5 * (g[i-1] - g[i+1]) / (g[i-1] - 2·g[i] + g[i+1])clamped to ±0.5 so the result stays within the local sample interval.
Examples
julia> p = [0.0, 0.0, 0.0, 100.0, 100.0, 100.0];
julia> r = gauge_edges_in_profile(p, 0.0, 5.0, POLARITY_POSITIVE, SELECT_FIRST);
julia> length(r.edges)
1
julia> r.edges[1].position # step sits between indices 3 and 4 → 3.5
3.5
julia> r.edges[1].strength > 0
truejulia> r = gauge_edges_in_profile(fill(128.0, 50), 2.0, 1.0);
julia> isempty(r.edges) # flat profile → no edges
truejulia> r = gauge_edges_in_profile(Float64[], 1.0, 1.0);
julia> isempty(r.edges) # empty profile → no crash
trueEdgesGauging.gauge_edges — Function
gauge_edges(image, roi, orientation, sigma, threshold,
polarity=POLARITY_ANY, selector=SELECT_ALL;
threaded=false)
-> Vector{ImageEdge{Float64}}Detect edges across all row or column profiles within a rectangular ROI.
For each 1-D profile extracted from the image (one per row for LEFT_TO_RIGHT/RIGHT_TO_LEFT, one per column for TOP_TO_BOTTOM/ BOTTOM_TO_TOP), gauge_edges_in_profile is called and the resulting sub-pixel positions are converted to 2-D image coordinates.
Arguments
image: 2-D array (matrix). Any element type convertible toFloat64.roi:(row_start, col_start, row_end, col_end), 1-based inclusive. Values are clamped to image bounds, androw_start > row_endis swapped.orientation: scan direction — one ofLEFT_TO_RIGHT,RIGHT_TO_LEFT,TOP_TO_BOTTOM,BOTTOM_TO_TOP.sigma,threshold,polarity,selector: forwarded togauge_edges_in_profilefor each extracted profile.threaded: iftrue, scans are processed in parallel withThreads.@threads. RequiresThreads.nthreads() > 1to actually speed up. Output order is preserved (edges are collected per-scan into buckets and merged in scan-index order at the end).
Returns
A Vector{ImageEdge{Float64}} with x = column, y = row (1-based), in scan order (row or column index increases monotonically).
Examples
julia> img = [col < 5 ? 0.0 : 100.0 for _ in 1:4, col in 1:8];
julia> edges = gauge_edges(img, (1,1,4,8), LEFT_TO_RIGHT, 0.0, 5.0,
POLARITY_POSITIVE, SELECT_FIRST);
julia> length(edges) # one edge per row
4
julia> all(e.x == 4.5 for e in edges) # step between col 4 and 5
true
julia> edges[1].scan_index
1julia> img = fill(128.0, 20, 20);
julia> isempty(gauge_edges(img, (1,1,20,20), LEFT_TO_RIGHT, 1.0, 1.0))
trueEdgesGauging.gauge_edge_points — Function
gauge_edge_points(image, roi, orientation, spacing, thickness,
sigma, threshold,
polarity=POLARITY_ANY, selector=SELECT_FIRST)
-> Vector{Vector{ImageEdge{Float64}}}Scan a rectangular ROI with multiple parallel measurement strips and return the detected edge points per strip.
The ROI is divided into strips whose centres are spaced spacing pixels apart along the scan-perpendicular direction. Each strip is thickness pixels wide and is scanned with gauge_edges.
This two-level structure lets callers inspect per-strip results (e.g. to detect broken or missing edges) before passing all points to RANSAC.
Arguments
roi:(row_start, col_start, row_end, col_end), 1-based inclusive.orientation: scan direction for each strip profile.spacing: distance between adjacent strip centres (pixels).thickness: full width of each strip (pixels).sigma,threshold,polarity,selector: forwarded togauge_edges_in_profilefor each row or column profile.
Returns
One inner Vector{ImageEdge{Float64}} per strip, in scan order along the perpendicular axis.
Examples
julia> img = [col < 5 ? 0.0 : 100.0 for _ in 1:9, col in 1:8];
julia> strips = gauge_edge_points(img, (1,1,9,8), LEFT_TO_RIGHT,
3.0, 1, 0.0, 5.0,
POLARITY_POSITIVE, SELECT_FIRST);
julia> length(strips) >= 3
true
julia> all(!isempty(s) for s in strips)
true
julia> all(all(e.x == 4.5 for e in s) for s in strips)
trueEdgesGauging.gauge_circular_edge_points — Function
gauge_circular_edge_points(image, center, start_angle, angular_span,
spacing_radians, profile_length,
sigma, threshold,
polarity=POLARITY_ANY, selector=SELECT_FIRST;
threaded=false, interp=INTERP_BICUBIC)
-> Vector{ImageEdge{Float64}}Detect edge points by casting radial profiles from center_rc at evenly-spaced angles and calling gauge_edges_in_profile on each profile.
Profiles are sampled using the chosen InterpolationMode (bicubic by default for best sub-pixel stability); the interpolant is built once and reused across all rays. Samples that fall outside the image yield NaN, which gauge_edges_in_profile handles via NaN-aware smoothing.
Arguments
center_rc:(row, col)of the scan centre (1-based). The_rcsuffix is a reminder that this is (row, col), not(x, y)— detected edges in the returnedImageEdgeare exposed as(x=col, y=row)for Cartesian use.start_angle: angle of the first ray in radians (0= rightward / +col direction).angular_span: total angular range to cover (radians). Use2πfor a full 360° scan.spacing_radians: angular step between consecutive rays. Returns an empty vector whenspacing_radians == 0.profile_length: number of pixels sampled along each ray.sigma,threshold,polarity,selector: forwarded togauge_edges_in_profile.interp: pixel interpolation method —INTERP_NEAREST,INTERP_BILINEAR, orINTERP_BICUBIC(default).
Returns
All detected edge points across all rays, collected into a single flat vector in angular order.
Examples
julia> img = [sqrt((r-10.0)^2+(c-10.0)^2) < 5.0 ? 100.0 : 0.0 for r in 1:20, c in 1:20];
julia> edges = gauge_circular_edge_points(img, (10.0,10.0),
0.0, 2π, deg2rad(30.0), 15, 0.5, 5.0,
POLARITY_NEGATIVE, SELECT_FIRST);
julia> length(edges) >= 8
truejulia> img = ones(20, 20);
julia> isempty(gauge_circular_edge_points(img, (10.0,10.0), 0.0, 2π, 0.0, 10, 1.0, 1.0))
trueEdgesGauging.gauge_ring_edge_points — Function
gauge_ring_edge_points(image, center, inner_radius, outer_radius,
start_angle, angular_span, spacing_radians,
sigma, threshold,
polarity=POLARITY_ANY, selector=SELECT_FIRST;
threaded=false, interp=INTERP_BICUBIC)
-> Vector{ImageEdge{Float64}}Like gauge_circular_edge_points but limits each radial profile to the annular region between inner_radius and outer_radius pixels from center_rc.
This is useful when the feature of interest lies within a known distance range from a reference point, and avoids false detections from structures inside the inner boundary.
Pass interp to choose the pixel interpolation method (INTERP_NEAREST, INTERP_BILINEAR, or INTERP_BICUBIC — default). Samples that fall outside the image yield NaN and are handled by the NaN-aware edge-detection pipeline.
Throws ArgumentError if inner_radius >= outer_radius.
Examples
julia> img = [sqrt((r-10.0)^2+(c-10.0)^2) < 5.0 ? 100.0 : 0.0 for r in 1:20, c in 1:20];
julia> edges = gauge_ring_edge_points(img, (10.0,10.0), 3.0, 8.0,
0.0, 2π, deg2rad(30.0), 0.5, 5.0,
POLARITY_NEGATIVE, SELECT_FIRST);
julia> length(edges) >= 8
truejulia> img = ones(20, 20);
julia> gauge_ring_edge_points(img, (10.0,10.0), 5.0, 3.0,
0.0, 2π, deg2rad(30.0), 1.0, 1.0)
ERROR: ArgumentError: inner_radius must be less than outer_radius
[...]Low-level fitting
EdgesGauging.fit_line_tls — Function
fit_line_tls(points) -> (A, B, C)Fit the line Ax + By + C = 0 (with A² + B² = 1) to points using Total Least Squares (equivalent to PCA / SVD of the centred point matrix).
points is a vector of (x, y) tuples or any length-2 indexable collection. Returns (A, B, C) as Float64.
Sign convention: A is preferably positive; if A == 0 then B > 0.
Throws ArgumentError if fewer than 2 points are provided.
Examples
julia> pts = [(0.0, 5.0), (1.0, 5.0), (2.0, 5.0)]; # horizontal line y = 5
julia> A, B, C = fit_line_tls(pts);
julia> round(A^2 + B^2, digits=10) # unit normal
1.0
julia> all(abs(A*p[1] + B*p[2] + C) < 1e-10 for p in pts)
truejulia> fit_line_tls([(1.0, 1.0)]) # too few points
ERROR: ArgumentError: fit_line_tls requires ≥ 2 points
[...]EdgesGauging.fit_circle_kasa — Function
fit_circle_kasa(points) -> (cx, cy, r)Fit a circle to points using the Kåsa (1976) algebraic method: minimise ∑(x² + y² − 2·cx·x − 2·cy·y + cx² + cy² − r²)² by solving a linear system in the centre coordinates.
Kåsa is fast and numerically straightforward, but biased toward smaller radii when samples cover only a short arc — the regime typical of edge-gauging. Use fit_circle_taubin for substantially lower bias at essentially the same cost, or fit_circle_lm for a geometric maximum-likelihood refinement.
Returns (cx, cy, r) as Float64.
Throws ArgumentError if fewer than 3 points are provided.
Examples
julia> angles = range(0, 2π, length=13)[1:end-1];
julia> pts = [(3.0 + 5.0*cos(θ), 4.0 + 5.0*sin(θ)) for θ in angles];
julia> cx, cy, r = fit_circle_kasa(pts);
julia> round(cx, digits=6)
3.0
julia> round(cy, digits=6)
4.0
julia> round(r, digits=6)
5.0julia> fit_circle_kasa([(0.0,0.0),(1.0,0.0)]) # too few points
ERROR: ArgumentError: fit_circle_kasa requires ≥ 3 points
[...]EdgesGauging.fit_circle_taubin — Function
fit_circle_taubin(points) -> (cx, cy, r)Fit a circle to points using Taubin's algebraic method (Taubin 1991; Chernov & Lesort 2005), implemented as a 3-column SVD on centred and Z-scaled coordinates.
Taubin minimises the same algebraic residual as Kåsa but normalises it by the squared gradient ‖∇F‖², which removes the strong radius bias that Kåsa exhibits on short arcs. On a full circle the two methods agree; on a partial arc Taubin's centre and radius are markedly closer to the geometric optimum at essentially the same cost.
Returns (cx, cy, r) as Float64.
Throws ArgumentError if fewer than 3 points are given, all points coincide, or the point set is collinear.
Examples
julia> angles = range(0, 2π, length=13)[1:end-1];
julia> pts = [(3.0 + 5.0*cos(θ), 4.0 + 5.0*sin(θ)) for θ in angles];
julia> cx, cy, r = fit_circle_taubin(pts);
julia> round(cx, digits=6)
3.0
julia> round(cy, digits=6)
4.0
julia> round(r, digits=6)
5.0julia> fit_circle_taubin([(0.0,0.0),(1.0,0.0)]) # too few points
ERROR: ArgumentError: fit_circle_taubin requires ≥ 3 points
[...]EdgesGauging.fit_circle_lm — Function
fit_circle_lm(points; cx0, cy0, r0, max_iter=50, tol=1e-10) -> (cx, cy, r)Refine a circle estimate by minimising the sum of squared orthogonal distances Σ (√((xᵢ-cx)² + (yᵢ-cy)²) − r)² with a damped Gauss-Newton (Levenberg-Marquardt) iteration. This is the maximum-likelihood estimator under isotropic Gaussian noise on the points, in contrast to the algebraic methods (Kåsa, Taubin) which minimise a different, biased cost.
Requires a starting estimate (cx0, cy0, r0) — typically the output of fit_circle_taubin. Convergence is quadratic from a good start and usually completes in fewer than ten iterations.
Returns (cx, cy, r) as Float64.
Throws ArgumentError if fewer than 3 points are given. If the iteration diverges (damping grows beyond a safe bound), the last accepted estimate is returned rather than throwing.
Examples
julia> angles = range(0, 2π, length=20)[1:end-1];
julia> pts = [(3.0 + 5.0*cos(θ), 4.0 + 5.0*sin(θ)) for θ in angles];
julia> cx, cy, r = fit_circle_lm(pts; cx0=2.5, cy0=4.5, r0=4.5);
julia> round(cx, digits=8)
3.0
julia> round(cy, digits=8)
4.0
julia> round(r, digits=8)
5.0EdgesGauging.fit_parabola — Function
fit_parabola(xs, ys) -> (a, b, c)Fit the parabola y = a·x² + b·x + c to the data vectors xs, ys using QR least-squares decomposition.
Returns (a, b, c) as Float64.
Throws ArgumentError if xs and ys differ in length or if fewer than 3 points are provided.
Examples
julia> a, b, c = fit_parabola([0.0, 1.0, 2.0], [1.0, 0.0, 3.0]);
julia> round(a, digits=10)
2.0
julia> round(b, digits=10)
-3.0
julia> round(c, digits=10)
1.0julia> fit_parabola([1.0, 2.0], [1.0, 4.0]) # too few points
ERROR: ArgumentError: fit_parabola requires ≥ 3 points
[...]High-level gauging
EdgesGauging.gauge_line — Function
gauge_line(image, roi, orientation, spacing, thickness, sigma, threshold;
polarity=POLARITY_ANY, selector=SELECT_FIRST,
constraints=LineConstraints{Float64}(),
confidence=0.99, inlier_threshold=1.0, max_iter=10_000)
-> LineFit{Float64}Detect edge points along parallel strips within roi, then fit a line with RANSAC outlier rejection.
The function chains gauge_edge_points → ransac2 → LineFit.
Arguments
roi,orientation,spacing,thickness,sigma,threshold,polarity,selector: passed togauge_edge_points.constraints:LineConstraintsto enforce angle and inlier limits.confidence: RANSAC confidence level (default 0.99).inlier_threshold: maximum perpendicular distance (pixels) to count a point as an inlier (default 1.0).max_iter: hard cap on RANSAC iterations (default 10 000).
Returns
A LineFit with normalised coefficients (A, B, C), inlier/outlier index vectors, and RMS residual.
Throws GaugingError with reason = :too_few_points when edge detection yields fewer than 2 points, or reason = :ransac_failed when RANSAC cannot find a line that satisfies constraints.
Examples
julia> img = [col < 30 ? 0.0 : 200.0 for _ in 1:50, col in 1:60];
julia> fit = gauge_line(img, (5,5,45,55), LEFT_TO_RIGHT, 5.0, 3, 1.5, 20.0);
julia> fit isa LineFit{Float64}
true
julia> fit.rms < 1.0
true
julia> abs(-fit.C / fit.A - 30.0) < 2.0 # line passes near col 30
trueEdgesGauging.gauge_circle — Function
gauge_circle(image, center, start_angle, angular_span, spacing_radians,
profile_length, sigma, threshold;
polarity=POLARITY_ANY, selector=SELECT_FIRST,
constraints=CircleConstraints{Float64}(),
confidence=0.99, inlier_threshold=1.0, max_iter=10_000,
refine=false)
-> CircleFit{Float64}Detect edge points along radial profiles from center_rc and fit a circle with RANSAC outlier rejection.
The function chains gauge_circular_edge_points → ransac2 (which rejects arc-incomplete candidates via data_constraints_met) → CircleFit.
Arguments
center_rc:(row, col)of the approximate circle centre (1-based). The_rcsuffix is a reminder that this is (row, col), not(x, y).start_angle,angular_span,spacing_radians,profile_length: passed togauge_circular_edge_points.sigma,threshold,polarity,selector: edge detection parameters.constraints:CircleConstraintsto enforce radius limits and arc completeness.confidence,inlier_threshold,max_iter: RANSAC parameters.refine: whentrue, run a geometric Levenberg-Marquardt refit (fit_circle_lm) on the final inlier set after RANSAC, starting from the algebraic Taubin estimate. This minimises orthogonal distances rather than the (biased) algebraic residual and gives the maximum-likelihood centre and radius under isotropic noise. Adds a few extra iterations of cost; defaultfalse.
Returns
A CircleFit with (cx, cy, r), inlier/outlier indices, and RMS.
Throws GaugingError with reason:
:too_few_points— fewer than 3 edge points detected.:ransac_failed— no circle satisfies the constraints (including the arc completeness check, which is now enforced during RANSAC viadata_constraints_metrather than post-hoc).
Examples
julia> img = [sqrt((r-25.0)^2+(c-25.0)^2) < 15.0 ? 200.0 : 0.0 for r in 1:50, c in 1:50];
julia> cc = CircleConstraints{Float64}(min_radius=10.0, max_radius=20.0, min_completeness=0.5);
julia> fit = gauge_circle(img, (25.0,25.0), 0.0, 2π, deg2rad(5.0), 25,
1.5, 20.0; polarity=POLARITY_NEGATIVE, constraints=cc);
julia> fit isa CircleFit{Float64}
true
julia> abs(fit.r - 15.0) < 2.0
true
julia> fit.rms < 2.0
trueRANSAC engine and model interface
EdgesGauging.ransac — Function
ransac(points, ModelType, inlier_threshold;
confidence=0.99, max_iter=10_000, rng=default_rng())
-> (model, inlier_indices, outlier_indices)Run RANSAC (Fischler & Bolles 1981) to robustly fit ModelType to points.
The iteration count is determined adaptively using the formula
N = ⌈log(1 − α) / log(1 − (1 − ε)^k)⌉where α is confidence, ε is the estimated outlier fraction (updated after each new best consensus set), and k = sample_size(ModelType).
After the best consensus set is found, the model is refit on all inliers for improved parameter accuracy.
Arguments
points: vector of points, each indexable aspt[1],pt[2].ModelType: concrete model type, e.g.LineModelorCircleModel.inlier_threshold: maximumpoint_distanceto count as an inlier.confidence: desired probability of finding the correct model (default 0.99).max_iter: hard upper limit on iterations (default 10 000).rng: random-number generator; pass an explicitMersenneTwisterfor reproducibility.
Returns
(best_model, inlier_indices, outlier_indices) where the indices refer into the original points vector.
Returns (nothing, Int[], collect(1:n)) when no model could be fitted (e.g. fewer points than the minimum sample size, or all samples degenerate).
Examples
julia> pts = [(x, 2.0*x + 1.0) for x in 1.0:10.0]; # exact line y = 2x+1
julia> model, inl, outl = ransac(pts, LineModel, 0.1; rng=MersenneTwister(0));
julia> length(outl) # no outliers on an exact line
0
julia> length(inl)
10julia> model, inl, outl = ransac(Tuple{Float64,Float64}[], LineModel, 0.5);
julia> isnothing(model)
true
julia> isempty(inl)
trueEdgesGauging.ransac2 — Function
ransac2(points, ModelType, inlier_threshold, constraints;
min_inliers=sample_size(ModelType),
confidence=0.99, max_iter=10_000,
initial_outlier_ratio=0.5, rng=default_rng())
-> (model, inlier_indices, outlier_indices)Extended RANSAC that validates model constraints after each candidate fit.
After each successful random fit, constraints_met is called to reject geometrically inadmissible candidates (e.g. circles with wrong radius). The best surviving consensus set is immediately refit on all its inliers, and the adaptive iteration budget is updated from the new inlier count.
Arguments
constraints: a constraint struct matched toModelType— passed verbatim toconstraints_met.min_inliers: minimum number of inliers required to accept a model (default:sample_size(ModelType)).initial_outlier_ratio: assumed outlier fraction used to seed the iteration budget before any good model is found (default 0.5).
All other arguments are as for ransac.
Examples
julia> pts = [(x, 2.0*x + 1.0) for x in 1.0:10.0];
julia> c = LineConstraints{Float64}(min_angle=-Float64(π/2), max_angle=Float64(π/2));
julia> model, inl, _ = ransac2(pts, LineModel, 0.1, c; rng=MersenneTwister(0));
julia> length(inl)
10julia> pts = [(x, x) for x in 1.0:10.0]; # 45° line
julia> c_tight = LineConstraints{Float64}(min_angle=Float64(π/3), max_angle=Float64(π/2));
julia> model, inl, _ = ransac2(pts, LineModel, 0.1, c_tight;
rng=MersenneTwister(0), max_iter=200);
julia> isnothing(model) # 45° line is below min_angle = 60°, so rejected
trueEdgesGauging.sample_size — Function
sample_size(::Type{M}) -> IntReturn the minimum number of points required to fit model type M uniquely.
| Model type | Minimum points |
|---|---|
LineModel | 2 |
LineSegmentModel | 2 |
CircleModel | 3 |
Examples
julia> sample_size(LineModel)
2
julia> sample_size(CircleModel)
3EdgesGauging.fit_model — Function
fit_model(::Type{M}, pts) -> MFit model type M to the point collection pts and return the fitted model.
Each concrete model delegates to its dedicated low-level fitting function:
LineModel/LineSegmentModel→fit_line_tlsCircleModel→fit_circle_taubin
Throws if pts contains fewer points than sample_size(M) requires, or if the point configuration is degenerate.
Examples
julia> pts = [(x, 2.0*x) for x in 1.0:5.0];
julia> m = fit_model(LineModel, pts);
julia> m isa LineModel
true
julia> all(point_distance(m, p) < 1e-10 for p in pts)
trueEdgesGauging.point_distance — Function
point_distance(m, pt) -> Float64Return the geometric distance from point pt (a length-2 indexable) to model m.
LineModel: perpendicular distance|Ax + By + C|(denominator is 1 because the normal is normalised).LineSegmentModel: same perpendicular distance asLineModel.CircleModel:|√((x−cx)²+(y−cy)²) − r|, i.e. radial distance from the circle boundary.
Examples
julia> m = CircleModel(0.0, 0.0, 10.0);
julia> point_distance(m, (10.0, 0.0)) # exactly on the circle
0.0
julia> point_distance(m, (7.0, 0.0)) # 3 pixels inside
3.0
julia> point_distance(m, (13.0, 0.0)) # 3 pixels outside
3.0EdgesGauging.constraints_met — Function
constraints_met(m, c) -> BoolReturn true when the model m satisfies all constraints in c.
| Model / Constraint pair | Checks |
|---|---|
LineModel + LineConstraints | Orientation angle within range |
LineSegmentModel + LineSegmentConstraints | Same + segment length |
CircleModel + CircleConstraints | Radius within [min, max] |
Examples
julia> m = CircleModel(0.0, 0.0, 25.0);
julia> constraints_met(m, CircleConstraints{Float64}(min_radius=10.0, max_radius=50.0))
true
julia> constraints_met(m, CircleConstraints{Float64}(min_radius=30.0, max_radius=50.0))
falseEdgesGauging.data_constraints_met — Function
data_constraints_met(m, c, inlier_pts) -> BoolPer-candidate constraint check evaluated on a model m's inlier set during ransac2. Unlike constraints_met, this has access to the inlier points themselves so constraints that depend on the data (e.g. arc completeness) can be enforced while RANSAC is still searching, letting it reject an inlier-majority-but-invalid candidate in favour of a smaller valid one.
Default implementation returns true. Override for specific (Model, Constraint) pairs as needed.
Override for circles: require arc_completeness ≥ c.min_completeness.
Override for line segments: require min_length ≤ segment_length ≤ max_length.
EdgesGauging.arc_completeness — Function
arc_completeness(m::CircleModel, inlier_pts; sector_deg=30.0) -> Float64Compute what fraction of the full 360° circle boundary is covered by inlier_pts, measured in angular sectors of sector_deg degrees.
Returns a value in [0, 1] — 1.0 means all sectors contain at least one inlier; 0.0 means the point set is empty.
Used by gauge_circle to reject partial-arc detections that do not meet the CircleConstraints min_completeness requirement.
Examples
julia> m = CircleModel(0.0, 0.0, 10.0);
julia> pts_full = [(10.0*cos(deg2rad(d)), 10.0*sin(deg2rad(d))) for d in 15:30:345];
julia> arc_completeness(m, pts_full) ≈ 1.0
true
julia> pts_half = [(10.0*cos(deg2rad(d)), 10.0*sin(deg2rad(d))) for d in 15:30:175];
julia> comp = arc_completeness(m, pts_half);
julia> 0.4 <= comp <= 0.6
true
julia> arc_completeness(m, [])
0.0EdgesGauging.rms_error — Function
rms_error(m, pts) -> Float64Return the root-mean-square distance of all points in pts from model m, using point_distance.
Returns 0.0 for an empty point set.
Examples
julia> m = CircleModel(0.0, 0.0, 5.0);
julia> pts = [(5.0*cos(θ), 5.0*sin(θ)) for θ in range(0, 2π, length=9)[1:end-1]];
julia> rms_error(m, pts) < 1e-10
true
julia> rms_error(m, [])
0.0EdgesGauging.segment_length — Function
segment_length(m::LineSegmentModel, inlier_pts) -> Float64Return the projected span of inlier_pts along the line direction of m.
This is the length of the smallest segment of the fitted line that contains all inlier projections — i.e. max(proj) − min(proj) where the projection is taken along the line's tangent direction (-B, A).
Returns 0.0 for an empty inlier set.
Examples
julia> m = LineSegmentModel(0.0, 1.0, -3.0); # horizontal line y = 3
julia> pts = [(1.0, 3.0), (5.0, 3.0), (9.0, 3.0)];
julia> segment_length(m, pts)
8.0
julia> segment_length(m, [])
0.0EdgesGauging.LineModel — Type
LineModel(A, B, C)Fitted infinite line Ax + By + C = 0 (A² + B² = 1), as used inside RANSAC.
The normalisation A² + B² = 1 means point_distance is the true perpendicular distance from the point to the line. Use LineConstraints to restrict allowed orientations when calling ransac2.
See also fit_line_tls, gauge_line.
Examples
julia> m = LineModel(0.0, 1.0, -5.0); # y = 5
julia> point_distance(m, (3.0, 5.0)) # on the line
0.0
julia> point_distance(m, (0.0, 3.0)) # 2 pixels below
2.0
julia> constraints_met(m, LineConstraints{Float64}())
trueEdgesGauging.LineSegmentModel — Type
LineSegmentModel(A, B, C)Fitted line segment — identical algebraic representation to LineModel but paired with LineSegmentConstraints to also enforce limits on the projected span of inlier points via segment_length.
Examples
julia> pts = [(Float64(x), 0.0) for x in 1:10];
julia> A, B, C = fit_line_tls(pts);
julia> m = LineSegmentModel(A, B, C);
julia> round(segment_length(m, pts), digits=10)
9.0EdgesGauging.CircleModel — Type
CircleModel(cx, cy, r)Fitted circle with centre (cx, cy) and radius r.
Use CircleConstraints to enforce radius and arc-completeness limits when calling ransac2.
See also fit_circle_taubin, gauge_circle, arc_completeness.
Examples
julia> m = CircleModel(0.0, 0.0, 10.0);
julia> point_distance(m, (10.0, 0.0))
0.0
julia> constraints_met(m, CircleConstraints{Float64}(min_radius=5.0, max_radius=15.0))
true
julia> constraints_met(m, CircleConstraints{Float64}(min_radius=11.0, max_radius=20.0))
false