Skip to main content

fricp

Fast and Robust Iterative Closest Point — rigid (optionally similarity) registration between two 3-D point sets, in Rust, with Python bindings.

An implementation of Fast and Robust Iterative Closest Point by Juyong Zhang, Yuxin Yao and Bailin Deng (IEEE TPAMI 2021).

Classical ICP has two well-known weaknesses. It converges linearly, so it takes many iterations. And its squared-distance metric insists that every source point be explained by the target, which quietly wrecks the alignment when the clouds only partly overlap or carry outliers. The paper fixes both:

  • Anderson acceleration. Classical ICP is a majorisation-minimisation algorithm, so it is a fixed-point iteration, so it can be extrapolated from its own history. The extrapolation happens in the Lie algebra se(3), where affine combinations of past iterates are still valid rigid transformations, and an accelerated iterate is kept only if it lowers the target energy — the energy still decreases monotonically.
  • A Welsch robust metric. Replacing the squared distance with 1 - exp(-r² / 2ν²) bounds what any single pair can cost, so outliers stop dragging the fit. Majorising it keeps the alignment step a closed-form weighted SVD, and ν is annealed from coarse to fine so the solve starts near-global and progressively sheds bad pairs.

Install

# Cargo.toml
[dependencies]
fricp = "0.1"
pip install fricp

Use it

use fricp::{register, Config, Method};

let result = register(&source, &target, &Config::new(Method::RobustIcp))?;

println!("{:?}", result.transform.to_matrix4());
println!("overlap ≈ {:.0}%", result.inlier_ratio * 100.0);
let aligned: Vec<[f64; 3]> = source
    .iter()
    .map(|p| result.transform.transform_point(*p))
    .collect();
import fricp

result = fricp.register(source, target, method="robust_icp")

print(result.transform)                      # (4, 4) float64
print(f"overlap ≈ {result.inlier_ratio:.0%}")
aligned = result.apply(source)

Point-to-plane needs normals on the target; estimate them if your data has none:

normals = fricp.estimate_normals(target, k=30)
result = fricp.register(source, target, method="robust_point_to_plane",
                        target_normals=normals)

Which method

Method Normals Use when
Icp / "icp" no the classical baseline, for comparison
FastIcp / "fast_icp" no clean, fully overlapping clouds
RobustIcp / "robust_icp" no default — noise, outliers, partial overlap
PointToPlane / "point_to_plane" yes clean scans of smooth surfaces
RobustPointToPlane / "robust_point_to_plane" yes usually the most accurate on surface scans

All of them refine an alignment. Like every ICP variant they converge to the nearest local minimum, so where they start matters as much as which one you pick. The default start translates the source so the two centroids coincide; Initial::Identity (init="identity") leaves the clouds where they are, and Initial::Pose (init=matrix) takes a known pose from odometry or a global matcher such as Super4PCS, FPFH + RANSAC, or TEASER++.

Estimating scale

The fit is rigid by default. Ask for estimate_scale and the three point-to-point methods solve a 7-DoF similarity p ↦ s R p + t instead, which is what you want when the clouds come from different modalities or carry a unit mismatch:

let config = Config::new(Method::RobustIcp).with_scale_estimation();
let result = register(&source, &target, &config)?;
println!("scale {:.4}", result.transform.scale());
result = fricp.register(source, target, estimate_scale=True)
print(result.scale)
print(result.transform)   # upper-left block is scale * rotation
print(result.rotation)    # the pure rotation, scale divided out

The scale comes from Umeyama's closed form, folded into the same weighted SVD the alignment step already computes — so it is the exact minimiser at each step and robust_icp keeps its monotone energy decrease. Normalisation divides both clouds by the same factor and therefore never affects the result.

One caveat worth knowing. The Welsch energy Σ 1 - exp(-r²/2ν²) is minimised by collapsing the source onto a single target point: drive s → 0 and every residual vanishes. That degenerate optimum is real, and the small-ν stages are where it is most tempting. The scale is therefore clamped into scale_bounds(0.1, 10.0) by default — at every iteration. Narrow it when you know the scale better than two orders of magnitude:

result = fricp.register(source, target, scale_bounds=(0.9, 1.1))

The point-to-plane methods do not support scale yet; asking for it is an error rather than a silent no-op.

What it can take

Registering a bumpy sphere against a clipped copy of itself, 6000 points, plus 5% gross outliers. Numbers are the RMS pose error (paper Eq. 12) at convergence, over a cloud of radius ≈ 1:

True overlap icp robust_icp robust_point_to_plane
94% 0.009 0.000 0.000
87% 0.258 0.000 0.000
74% 0.442 0.000 0.000
56% 0.554 0.333 0.000
33% 0.717 0.545 0.000
~0% 0.911 0.740 0.255

Plain ICP is already lost at 87% overlap. The robust point-to-plane variant holds on down to a third. Below that everything fails, and so does the authors' own C++ implementation on the same data — at that point you need a global method, not a better ICP.

inlier_ratio recovers the true overlap closely (0.87, 0.74, 0.57 for the rows above), which makes it a usable confidence signal.

Correctness

Validated against the authors' reference C++ implementation on its own example data — a 15 446-point target and a 14 806-point source. Largest absolute difference over all 16 entries of the resulting transform:

Method Reference method # max abs difference this crate
Icp 0 5.0e-7 65 iterations / 0.063 s
FastIcp 2 4.4e-5 34 iterations / 0.045 s
RobustIcp 3 7.6e-5 177 iterations / 0.176 s
PointToPlane 4 6.5e-6 11 iterations / 0.022 s
RobustPointToPlane 5 4.7e-6 58 iterations / 0.089 s

(Apple M-series, release build. The residual differences come from the two deliberate deviations below, which change the path taken but not the fixed point reached.)

Reproduce it with the bundled example:

cargo run --release --example register_ply -- target.ply source.ply robust_icp

Deliberate deviations from the reference

Two, both documented in the source:

  • Anderson acceleration runs on the 6-vector twist, not on the 16 entries of the flattened 4×4 logarithm. The two span the same subspace, but the flattened form stores each rotation entry twice and so silently weights the rotation part of the least-squares problem by two. The energy-decrease safeguard means either choice converges to the same fixed point.
  • The point-to-plane Jacobian uses the correct small-angle limits. The reference zeroes ∂t/∂δ when the rotation is near identity; the true limit is ½ (e_j × υ). This crate uses Taylor series for all six exponential-map coefficients, which matters precisely near convergence.

Beyond that: the SE(3) logarithm is computed in closed form via a quaternion (stable all the way to a rotation of π) rather than by real Schur decomposition, and the point-to-plane line search backtracks as Algorithm 2 describes rather than trying a single step.

Details

  • Reproducible. Results are bit-for-bit identical however many threads rayon uses. Parallel work either writes to disjoint slots or reduces over fixed-size chunks combined in order.
  • Parallel by default via rayon (default-features = false for a serial build). The Python bindings release the GIL, so register can be called from a thread pool.
  • Few dependencies: nalgebra and (optionally) rayon. The k-d tree is built in.
  • No unsafe in the core crate (#![forbid(unsafe_code)]).
  • Input is plain [f64; 3] — no nalgebra types needed at the API boundary, though they are available for callers who want them.

Development

cargo test --workspace                      # Rust: unit, integration, doc tests
cargo test -p fricp --no-default-features   # and the serial build

pip install pytest numpy
pip install .                               # or, inside an activated venv:
                                            # maturin develop --release
pytest python/tests

Licence and attribution

GPL-3.0-or-later; see LICENSE.

The algorithm is due to Zhang, Yao and Deng. Their reference implementation at yaoyx689/Fast-Robust-ICP is MIT licensed (© 2020 yaoyuxin) and was used to cross-check this port; its licence permits the relicensing here.

@article{zhang2021fast,
  title   = {Fast and Robust Iterative Closest Point},
  author  = {Zhang, Juyong and Yao, Yuxin and Deng, Bailin},
  journal = {IEEE Transactions on Pattern Analysis and Machine Intelligence},
  year    = {2021}
}

Release files for fricp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fricp 0.1.0
File Size Uploaded
fricp-0.1.0.tar.gz 91.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for fricp 0.1.0
File
fricp-0.1.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details
fricp-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details
fricp-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details
fricp-0.1.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
fricp-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 2.0 MB

Release files / fricp-0.1.0.tar.gz

Download URL fricp-0.1.0.tar.gz
Size 91.6 kB
Tags Source
SHA-256 checksum
How to use checksums
47f32d21f8939ef8192be8312a7a599534db02f9ac44e347b6cb8e5f842cddf3
BLAKE2b-256 checksum
How to use checksums
cfe7e6e9b235a2c995b6c89a7dc9f4aee0e68654fda03fdcea39a0be0ef1da8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / fricp-0.1.0-cp39-abi3-win_amd64.whl

Download URL fricp-0.1.0-cp39-abi3-win_amd64.whl
Size 340.1 kB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
65a3b906636a4fe69cd65092fe1f40d1dbf814b485cbac3e514385c71446f026
BLAKE2b-256 checksum
How to use checksums
0632a5cbe95a3211f502b23ffac7182f32d8b41f24207f20d17113899917dcb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / fricp-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL fricp-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 429.4 kB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
bbb0c5205511c327d43a0fe7647ff67ae2d0718a2aab6b1ba96376872725d569
BLAKE2b-256 checksum
How to use checksums
6a827016f00342c6c7eb3a8ef30fd51cc87166df081e0420113837535bdf565e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / fricp-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL fricp-0.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 407.0 kB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
d7ecf79aa59a406529cd096ff99d3528f73c813f81a21587b219decae5a8b4f4
BLAKE2b-256 checksum
How to use checksums
ce8423b083f80c58b030b9da8b2a0c8673ffc6633df15f349da1b9de72b5f64a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / fricp-0.1.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL fricp-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Size 382.7 kB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
78cfc0be29a2673a1841a1713ba9fe67c09c2218e3d4fe46a6b86154bf74f575
BLAKE2b-256 checksum
How to use checksums
5b8fda8bafdf9d09a15cfcebf750a4ad7fa80bad954927bca8346e2bee2ab85f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / fricp-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL fricp-0.1.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 394.5 kB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
96e149b40922ca281ba14613f678b925e3d08a01f2aef86f560a633f7c9f1475
BLAKE2b-256 checksum
How to use checksums
74bbd3be620f01473e15541995e4b3a47b19bfb1f91fb2128bd949a996a8c30b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page