Fast, robust 3-D cylinder fitting — MAGSAC+PROSAC RANSAC, analytic LM, ellipse, cone, curved cylinder.
Project description
cylfit
Fast, robust 3-D cylinder fitting for Python.
Eight fitting scenarios — clean cylinder, 30% outliers, 180° partial arc, elliptical cross-section, cone, 2-D ellipse projection, residual distributions, and radius MAE vs noise.
Highlights
| Feature | Detail |
|---|---|
| Estimation | MAGSAC soft scoring + PROSAC quality-ranked sampling |
| Refinement | Levenberg–Marquardt with analytic closed-form Jacobian |
| Parallelism | Multi-threaded RANSAC via ThreadPoolExecutor (n_jobs) |
| Shapes | Circular cylinder · Elliptical cylinder · Cone · Curved cylinder · Pipe network |
| I/O | PLY · PCD · LAS/LAZ · XYZ · CSV · Open3D adapter |
| Testing | 168 tests — property-based (Hypothesis), golden-value regression, statistical bias, cross-implementation |
| CI | Ubuntu · macOS · Windows × Python 3.9 – 3.12 |
| Coverage | ≥ 80 % branch coverage enforced |
Install
pip install cylfit
With optional extras:
pip install "cylfit[visualize]" # matplotlib plots
pip install "cylfit[io]" # LAS/LAZ support (laspy)
pip install "cylfit[dev]" # pytest + hypothesis + pytest-cov
Quick start
import numpy as np
from cylfit import fit_cylinder, generate_noisy_cylinder
# Generate synthetic data (or load your own N×3 array)
syn = generate_noisy_cylinder(radius=1.5, noise=0.02, outlier_fraction=0.25, random_state=42)
model = fit_cylinder(syn.points, threshold=0.06, ransac_trials=128, random_state=42)
print(f"radius : {model.radius:.4f}")
print(f"axis : {model.axis_direction}")
print(f"RMSE : {model.rmse:.4f}")
print(f"inliers : {model.inlier_mask.mean():.0%}")
print(f"converged : {model.converged}")
Load a real point cloud
from cylfit import load_points, fit_cylinder
pts = load_points("scan.ply") # PLY, PCD, LAS, XYZ, CSV
model = fit_cylinder(pts, threshold=0.05)
print(model.to_json())
API overview
Fitting functions
from cylfit import (
fit_cylinder, # general robust fitter
fit_cylinder_known_radius, # radius pinned
fit_cylinder_with_normals, # normal-seeded axis init
fit_cylinder_fixed_axis, # axis fixed
fit_cylinder_constrained_axis, # axis within a cone
)
from cylfit.elliptical import fit_elliptical_cylinder
from cylfit.cone import fit_cone
from cylfit.curved import fit_curved_cylinder
from cylfit.network import find_cylinder_joints, build_pipe_network
Key parameters (fit_cylinder)
| Parameter | Default | Description |
|---|---|---|
threshold |
auto | Inlier distance (same units as points). Auto-estimated when omitted. |
ransac_trials |
128 | RANSAC iterations. More → more robust, slower. |
n_jobs |
1 | Worker threads for parallel RANSAC. |
random_state |
None | Integer seed for full reproducibility. |
known_radius |
None | Fix radius and solve only for axis + position. |
initial_axis |
None | Warm-start axis direction to skip RANSAC. |
CylinderModel attributes
model.axis_point # np.ndarray (3,) — point on axis near cloud centroid
model.axis_direction # np.ndarray (3,) — unit vector
model.radius # float
model.height # float (height_max − height_min)
model.inlier_mask # np.ndarray (N,) bool
model.residuals # np.ndarray (N,) signed radial distances
model.rmse # float — inlier RMSE
model.converged # bool
model.iterations # int
model.start_point # np.ndarray (3,)
model.end_point # np.ndarray (3,)
model.to_dict() # → dict
model.to_json() # → str
Fitting scenarios
Robust fit with outliers
model = fit_cylinder(pts, threshold=0.08, ransac_trials=256, random_state=0)
# MAGSAC scoring down-weights outliers; PROSAC prefers near-surface samples
Known radius (e.g. standard pipe)
model = fit_cylinder_known_radius(pts, radius=0.0508) # 2-inch nominal
print(model.radius) # exactly 0.0508
Elliptical cross-section
from cylfit.elliptical import fit_elliptical_cylinder
model = fit_elliptical_cylinder(pts, threshold=0.05)
print(model.semi_major, model.semi_minor, model.aspect_ratio)
Cone
from cylfit.cone import fit_cone
model = fit_cone(pts, ransac_trials=64, random_state=0)
print(f"half-angle: {model.half_angle_deg:.2f}°")
print(f"apex: {model.apex}")
Curved cylinder (bent pipe)
from cylfit.curved import fit_curved_cylinder
model = fit_curved_cylinder(pts, n_segments=10)
print(f"spine length: {model.total_length:.3f}")
print(f"mean curvature: {model.curvature_mean:.4f}")
Pipe network junction detection
from cylfit.network import find_cylinder_joints
cylinders = [fit_cylinder(seg) for seg in segments]
joints = find_cylinder_joints(cylinders, threshold=0.05)
for j in joints:
print(f"cylinders {j.cylinder_a_idx}↔{j.cylinder_b_idx} "
f"gap={j.gap:.3f} angle={j.angle_deg:.1f}°")
Residuals and inlier contract
The inlier mask always satisfies:
inlier_mask[i] ⟺ |residuals[i]| ≤ threshold
Residuals are signed radial distances: positive = outside, negative = inside the cylinder surface.
Reproducibility
m1 = fit_cylinder(pts, random_state=42)
m2 = fit_cylinder(pts, random_state=42)
assert (m1.axis_direction == m2.axis_direction).all() # bit-identical
File I/O
from cylfit import load_points
pts = load_points("scan.ply") # ASCII or binary PLY
pts = load_points("scan.pcd") # PCL PCD (ASCII or binary)
pts = load_points("scan.las") # LAS / LAZ (requires laspy extra)
pts = load_points("scan.xyz") # whitespace or comma-delimited
pts = load_points("scan.csv") # auto-detects comma delimiter
Open3D adapter
import open3d as o3d
from cylfit import from_open3d
cloud = o3d.io.read_point_cloud("scan.ply")
pts = from_open3d(cloud)
model = fit_cylinder(pts)
Algorithm
Input: N×3 point cloud
│
├─ PROSAC RANSAC (quality-ranked sampling)
│ └─ MAGSAC scoring (Gaussian soft inlier count)
│ └─ PCA fallback on < 8 candidates
│
├─ Levenberg–Marquardt refinement
│ ├─ Analytic closed-form Jacobian ∂r/∂(p₀, d̂, R)
│ └─ Huber-weighted iterative reweighting
│
└─ CylinderModel (axis_point, axis_direction, radius, residuals, …)
MAGSAC scoring replaces hard inlier counts with a Gaussian soft count
Σ exp(−rᵢ²/2σ²) where σ = threshold/3. This produces a smoother
landscape for the best-hypothesis selection and is more robust at the
inlier/outlier boundary.
PROSAC ranks candidate points by their proximity to the current best hypothesis and biases early trials toward high-quality points, reducing the number of trials needed to find a good seed.
Analytic Jacobian — for a cylinder with axis point p₀ and unit direction d̂,
the residual for point pᵢ is rᵢ = ‖qᵢ‖ − R where qᵢ = (pᵢ − p₀) − [(pᵢ − p₀)·d̂]d̂.
The 6-column Jacobian has a closed form that avoids finite-difference overhead
and is more numerically stable near the axis.
Testing
# Fast smoke test
python -m pytest tests/ -q
# With coverage report
python -m pytest tests/ --cov=cylfit --cov-report=html
# Property-based tests only
python -m pytest tests/test_properties.py -v
# Golden-value regression pins
python -m pytest tests/test_regression.py -v
The test suite includes:
- Unit tests (
test_core.py,test_shapes.py,test_io.py) — API contract - Property tests (
test_properties.py) — Hypothesis-driven geometric invariants - Regression tests (
test_regression.py) — golden-value pins for radius/RMSE/inliers - Bias tests (
test_bias.py) — estimator unbiasedness and consistency over 20 seeds - Edge case tests (
test_edge_cases.py) — NaN/Inf, partial arc, degenerate geometry - Cross-implementation (
test_cross_impl.py) — comparison vspyransac3d/cylinder_fittingwhen installed
Benchmark
pip install "cylfit[competitors]"
python examples/run_benchmark.py
Generates examples/benchmark_report.md comparing cylfit against
pyransac3d and cylinder_fitting across clean, noisy, partial-arc, and
short-wide cylinder scenarios.
Contributing
See CONTRIBUTING.md. In short:
git clone https://github.com/weiykong/cylfit
pip install -e ".[dev]"
python -m pytest tests/ -q
ruff check src/ tests/
PRs are welcome. Please update CHANGELOG.md and (if algorithm behaviour changed) the golden pins in tests/test_regression.py.
License
MIT © 2026 cylfit Contributors
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cylfit-0.1.0.tar.gz.
File metadata
- Download URL: cylfit-0.1.0.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
379e515f610db27da12b1c1ab3419c0ef215f08211d45f1f8f1995fb8e65560a
|
|
| MD5 |
5fa50b7c4e0678f05ef9c495f8822e41
|
|
| BLAKE2b-256 |
d17815525dadf6219c295d7e5d1cf258e318c850c44c906b049314844aa960bf
|
Provenance
The following attestation bundles were made for cylfit-0.1.0.tar.gz:
Publisher:
publish.yml on weiykong/cylfit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cylfit-0.1.0.tar.gz -
Subject digest:
379e515f610db27da12b1c1ab3419c0ef215f08211d45f1f8f1995fb8e65560a - Sigstore transparency entry: 1435917706
- Sigstore integration time:
-
Permalink:
weiykong/cylfit@3dd6f5bb38f44e9f454c2b026413b8b2e4166e2d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/weiykong
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3dd6f5bb38f44e9f454c2b026413b8b2e4166e2d -
Trigger Event:
push
-
Statement type:
File details
Details for the file cylfit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: cylfit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 46.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11cf8c5c8e6c9822c314f2511cb8578119fba5ee5fa038fe5867b24ba3a56b0d
|
|
| MD5 |
7ad5de93449e3905bb7714032028b8f1
|
|
| BLAKE2b-256 |
199007d513fcdc7ab13cb1353f3067bcd8f6bc4ac2a364a5a312d06178c04bdf
|
Provenance
The following attestation bundles were made for cylfit-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on weiykong/cylfit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cylfit-0.1.0-py3-none-any.whl -
Subject digest:
11cf8c5c8e6c9822c314f2511cb8578119fba5ee5fa038fe5867b24ba3a56b0d - Sigstore transparency entry: 1435917710
- Sigstore integration time:
-
Permalink:
weiykong/cylfit@3dd6f5bb38f44e9f454c2b026413b8b2e4166e2d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/weiykong
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3dd6f5bb38f44e9f454c2b026413b8b2e4166e2d -
Trigger Event:
push
-
Statement type: