API reference

Module

EdgesGauging.EdgesGaugingModule
EdgesGauging

Provides 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)
source

Enumerations

EdgesGauging.EdgePolarityType
EdgePolarity

Specifies which gradient direction constitutes a detectable edge.

ValueMeaning
POLARITY_POSITIVEDark-to-bright (rising) transitions only.
POLARITY_NEGATIVEBright-to-dark (falling) transitions only.
POLARITY_ANYBoth rising and falling transitions.

Examples

julia> POLARITY_POSITIVE isa EdgePolarity
true

julia> length(instances(EdgePolarity))
3
source
EdgesGauging.EdgeSelectorType
EdgeSelector

Controls how many edges are returned from a single 1-D profile scan.

ValueMeaning
SELECT_FIRSTReturn only the first edge (leftmost / topmost).
SELECT_LASTReturn only the last edge (rightmost / bottommost).
SELECT_BESTReturn only the strongest edge (highest gradient).
SELECT_ALLReturn all edges above the strength threshold.

Examples

julia> SELECT_ALL isa EdgeSelector
true

julia> length(instances(EdgeSelector))
4
source
EdgesGauging.ScanOrientationType
ScanOrientation

Primary scan direction for 2-D image edge detection.

ValueProfiles run along…Scan moves…
LEFT_TO_RIGHTRows (horizontal)Left to right
RIGHT_TO_LEFTRows (horizontal)Right to left
TOP_TO_BOTTOMColumns (vertical)Top to bottom
BOTTOM_TO_TOPColumns (vertical)Bottom to top

Examples

julia> LEFT_TO_RIGHT isa ScanOrientation
true

julia> length(instances(ScanOrientation))
4
source
EdgesGauging.InterpolationModeType
InterpolationMode

Selects how an image is sampled at sub-pixel positions when extracting profiles along arbitrary geometric paths.

ValueMethodSub-pixel quality
INTERP_NEARESTPiecewise constantReturns the nearest pixel value (no blending).
INTERP_BILINEAR2×2 linear blendC0 continuous; small bias at pixel boundaries.
INTERP_BICUBIC4×4 cubic B-splineC2 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))
3
source

Result types

EdgesGauging.EdgeResultType
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.0
source
EdgesGauging.ProfileEdgesResultType
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) of smoothed.
source
EdgesGauging.ImageEdgeType
ImageEdge{T<:AbstractFloat}

A single edge detected in a 2-D image, returned by gauge_edges_info 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), or nothing for scans that do not have a meaningful strip index (e.g. radial scans produced by gauge_circular_edge_points_info and gauge_ring_edge_points_info).

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)
true
source
EdgesGauging.LineFitType
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.02
source
EdgesGauging.CircleFitType
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)
true
source

Constraint types

EdgesGauging.LineConstraintsType
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.

FieldDefaultMeaning
min_angle-π/2Minimum allowed line orientation angle (radians).
max_angleπ/2Maximum allowed line orientation angle (radians).
min_inlier_ratio0.1Minimum fraction of points that must be inliers.
min_inlier_count2Absolute 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
2
source
EdgesGauging.LineSegmentConstraintsType
LineSegmentConstraints{T<:AbstractFloat}

Constraints for LineSegmentModel RANSAC fitting. Extends LineConstraints with segment-length limits.

FieldDefaultMeaning
min_angle-π/2Minimum allowed orientation angle (radians).
max_angleπ/2Maximum allowed orientation angle (radians).
min_inlier_ratio0.1Minimum inlier fraction.
min_inlier_count2Minimum number of inliers.
min_length0.0Minimum projected span of inlier points (pixels).
max_lengthInfMaximum projected span (pixels).
source
EdgesGauging.CircleConstraintsType
CircleConstraints{T<:AbstractFloat}

Geometric constraints applied during RANSAC circle fitting (used by gauge_circle and ransac2).

FieldDefaultMeaning
min_radius0.0Minimum allowed circle radius (pixels).
max_radiusInfMaximum allowed circle radius (pixels).
min_completeness0.1Minimum arc completeness fraction (0–1).
min_inlier_count3Absolute 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.1
source

Errors

EdgesGauging.GaugingErrorType
GaugingError(reason, msg) <: Exception

Raised 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 via data_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
true
source

Profile extraction

EdgesGauging.extract_line_profileFunction
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 to Float64.
  • p0_rc, p1_rc: segment endpoints as (row, col) tuples (1-based, pixel-centre convention; see InterpolationMode). May lie outside the image — out-of-bounds samples become NaN.
  • width::Int = 1: number of samples taken perpendicular to the segment at each centreline position. 1 extracts a slim profile; values > 1 average width parallel samples spaced 1 px apart and centred on the segment.
  • n_samples::Int = 0: number of samples along the segment. 0 means auto — picks max(2, ⌈hypot(Δrow, Δcol)⌉ + 1) so the centreline samples are spaced ≈ 1 px apart.
  • interp::InterpolationMode = INTERP_BICUBIC: sampling method — INTERP_NEAREST, INTERP_BILINEAR, or INTERP_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]
true
julia> 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)
true
julia> 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])
true
source
EdgesGauging.extract_arc_profileFunction
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 from start_angle to end_angle; pass end_angle < start_angle for 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, so width > 1 widens 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. 0 means auto — picks max(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)
true
julia> 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)
true
source

Edge detection

EdgesGauging.gauge_edges_in_profileFunction
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). Pass 0 or 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), or POLARITY_ANY (both).
  • selector: how many edges to return per profile — SELECT_FIRST, SELECT_LAST, SELECT_BEST (strongest), or SELECT_ALL.

Returns

A ProfileEdgesResult containing:

  • edges: detected edges, each with a sub-pixel position (1-based) and strength.
  • 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
true
julia> r = gauge_edges_in_profile(fill(128.0, 50), 2.0, 1.0);

julia> isempty(r.edges)   # flat profile → no edges
true
julia> r = gauge_edges_in_profile(Float64[], 1.0, 1.0);

julia> isempty(r.edges)   # empty profile → no crash
true
source
EdgesGauging.gauge_edges_infoFunction
gauge_edges_info(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 to Float64.
  • roi: (row_start, col_start, row_end, col_end), 1-based inclusive. Values are clamped to image bounds, and row_start > row_end is swapped.
  • orientation: scan direction — one of LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP.
  • sigma, threshold, polarity, selector: forwarded to gauge_edges_in_profile for each extracted profile.
  • threaded: if true, scans are processed in parallel with Threads.@threads. Requires Threads.nthreads() > 1 to 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_info(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
1
julia> img = fill(128.0, 20, 20);

julia> isempty(gauge_edges_info(img, (1,1,20,20), LEFT_TO_RIGHT, 1.0, 1.0))
true
source
EdgesGauging.gauge_edge_points_infoFunction
gauge_edge_points_info(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_info.

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 to gauge_edges_in_profile for 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_info(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)
true
source
EdgesGauging.gauge_circular_edge_points_infoFunction
gauge_circular_edge_points_info(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 _rc suffix is a reminder that this is (row, col), not (x, y) — detected edges in the returned ImageEdge are 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). Use for a full 360° scan.
  • spacing_radians: angular step between consecutive rays. Returns an empty vector when spacing_radians == 0.
  • profile_length: number of pixels sampled along each ray.
  • sigma, threshold, polarity, selector: forwarded to gauge_edges_in_profile.
  • interp: pixel interpolation method — INTERP_NEAREST, INTERP_BILINEAR, or INTERP_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_info(img, (10.0,10.0),
                   0.0, 2π, deg2rad(30.0), 15, 0.5, 5.0,
                   POLARITY_NEGATIVE, SELECT_FIRST);

julia> length(edges) >= 8
true
julia> img = ones(20, 20);

julia> isempty(gauge_circular_edge_points_info(img, (10.0,10.0), 0.0, 2π, 0.0, 10, 1.0, 1.0))
true
source
EdgesGauging.gauge_ring_edge_points_infoFunction
gauge_ring_edge_points_info(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_info 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_info(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
true
julia> img = ones(20, 20);

julia> gauge_ring_edge_points_info(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
[...]
source

Low-level fitting

EdgesGauging.fit_line_tlsFunction
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)
true
julia> fit_line_tls([(1.0, 1.0)])   # too few points
ERROR: ArgumentError: fit_line_tls requires ≥ 2 points
[...]
source
EdgesGauging.fit_circle_kasaFunction
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.0
julia> fit_circle_kasa([(0.0,0.0),(1.0,0.0)])   # too few points
ERROR: ArgumentError: fit_circle_kasa requires ≥ 3 points
[...]
source
EdgesGauging.fit_circle_taubinFunction
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.0
julia> fit_circle_taubin([(0.0,0.0),(1.0,0.0)])   # too few points
ERROR: ArgumentError: fit_circle_taubin requires ≥ 3 points
[...]
source
EdgesGauging.fit_circle_lmFunction
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.0
source
EdgesGauging.fit_parabolaFunction
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.0
julia> fit_parabola([1.0, 2.0], [1.0, 4.0])   # too few points
ERROR: ArgumentError: fit_parabola requires ≥ 3 points
[...]
source

High-level gauging

EdgesGauging.gauge_lineFunction
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_inforansac2LineFit.

Arguments

  • roi, orientation, spacing, thickness, sigma, threshold, polarity, selector: passed to gauge_edge_points_info.
  • constraints: LineConstraints to 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
true
source
EdgesGauging.gauge_circleFunction
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_inforansac2 (which rejects arc-incomplete candidates via data_constraints_met) → CircleFit.

Arguments

  • center_rc: (row, col) of the approximate circle centre (1-based). The _rc suffix is a reminder that this is (row, col), not (x, y).
  • start_angle, angular_span, spacing_radians, profile_length: passed to gauge_circular_edge_points_info.
  • sigma, threshold, polarity, selector: edge detection parameters.
  • constraints: CircleConstraints to enforce radius limits and arc completeness.
  • confidence, inlier_threshold, max_iter: RANSAC parameters.
  • refine: when true, 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; default false.

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 via data_constraints_met rather 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
true
source

RANSAC engine and model interface

EdgesGauging.ransacFunction
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 as pt[1], pt[2].
  • ModelType: concrete model type, e.g. LineModel or CircleModel.
  • inlier_threshold: maximum point_distance to 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 explicit MersenneTwister for 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)
10
julia> model, inl, outl = ransac(Tuple{Float64,Float64}[], LineModel, 0.5);

julia> isnothing(model)
true

julia> isempty(inl)
true
source
EdgesGauging.ransac2Function
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, analogous to the C++ ransac2().

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 to ModelType — passed verbatim to constraints_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)
10
julia> 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
true
source
EdgesGauging.sample_sizeFunction
sample_size(::Type{M}) -> Int

Return the minimum number of points required to fit model type M uniquely.

Model typeMinimum points
LineModel2
LineSegmentModel2
CircleModel3

Examples

julia> sample_size(LineModel)
2

julia> sample_size(CircleModel)
3
source
EdgesGauging.fit_modelFunction
fit_model(::Type{M}, pts) -> M

Fit model type M to the point collection pts and return the fitted model.

Each concrete model delegates to its dedicated low-level fitting function:

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)
true
source
EdgesGauging.point_distanceFunction
point_distance(m, pt) -> Float64

Return 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 as LineModel.
  • 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.0
source
EdgesGauging.constraints_metFunction
constraints_met(m, c) -> Bool

Return true when the model m satisfies all constraints in c.

Model / Constraint pairChecks
LineModel + LineConstraintsOrientation angle within range
LineSegmentModel + LineSegmentConstraintsSame + segment length
CircleModel + CircleConstraintsRadius 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))
false
source
EdgesGauging.data_constraints_metFunction
data_constraints_met(m, c, inlier_pts) -> Bool

Per-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.

source

Override for circles: require arc_completeness ≥ c.min_completeness.

source

Override for line segments: require min_length ≤ segment_length ≤ max_length.

source
EdgesGauging.arc_completenessFunction
arc_completeness(m::CircleModel, inlier_pts; sector_deg=30.0) -> Float64

Compute 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.0
source
EdgesGauging.rms_errorFunction
rms_error(m, pts) -> Float64

Return 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.0
source
EdgesGauging.segment_lengthFunction
segment_length(m::LineSegmentModel, inlier_pts) -> Float64

Return 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.0
source
EdgesGauging.LineModelType
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}())
true
source
EdgesGauging.LineSegmentModelType
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.0
source
EdgesGauging.CircleModelType
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
source