Skip to main content

rcpd

Coherent point drift in Rust, with Python bindings: fit a transform between two point clouds when you do not know — and do not need to know — which point matches which.

The clouds may have different numbers of points. The fit is by EM over a Gaussian mixture (Myronenko & Song 2010), and what comes back is a closed-form transform, so it applies exactly to points that took no part in fitting it.

import rcpd

tr = rcpd.register_rigid(source, target)     # rotation, translation, scale
tr = rcpd.register_deform(source, target)    # a smooth non-rigid warp
moved = tr.apply(source)
use rcpd::{rigid_register, RigidOpts};

let fit = rigid_register(source.view(), target.view(), RigidOpts::default(), None)?;
let moved = fit.transform.apply(source.view())?;

What is here

  • Rigid / similarity registration — rotation, translation and optionally one uniform scale, which can be confined to a range you consider plausible.
  • Deformable registration — a smooth warp, regularised by the motion-coherence prior and solved through the low-rank formulation from the original paper.

Either as a single pair or a whole grid of pairs at once, one registration per core. Both return a function of position rather than a set of moved points, so either applies exactly to points that took no part in the fit — which is what makes fitting on a subsample free of approximation, and what saves a grid of registrations from being hundreds of gigabytes of displaced clouds.

Install

pip install rcpd

or, for the Rust crate,

cargo add rcpd

Rigid, then deformable

The two fits are not alternatives. Running both is the usual workflow: the rigid fit takes out the pose, and the deformable fit is asked only for what is left.

rigid = rcpd.register_rigid(source, target)
moved = rigid.apply(source)

deform = rcpd.register_deform(moved, target)   # note: fitted on the *moved* points
final = deform.apply(moved)

rigid.nrms, deform.nrms                        # the two are on the same scale

Points that took no part in either fit go through both, in the same order — deform.apply(rigid.apply(pts)).

Fitting the deformation on moved rather than on source is the load-bearing part. The motion-coherence prior penalises displacement, so any pose offset left for the deformation to undo is paid for out of the same budget as the warp — and past a large enough offset it is not undone at all: the fit settles into a wrong local optimum whose residual reads like two unrelated clouds. On a 400-point cloud rotated 70 degrees, shifted, and warped by something no rigid transform can express, that is exactly what happens:

nrms
rigid alone 0.053
deformable alone 0.25 — no better than two unrelated clouds
rigid, then deformable 1.5e-4 — at the numerical floor

Where the offset stops being recoverable is data-dependent; on that cloud it is somewhere between 45 and 70 degrees.

Judging a fit

Nearly every fit converges, and that is not the same as being right — coherent point drift will settle a cloud onto an unrelated one perfectly happily. So the transform carries the residual it left behind:

tr.rms      # residual distance, in the units that went in
tr.nrms     # the same, as a fraction of the target cloud's own radius

nrms is the one to compare between fits. It is calibrated rather than merely ordered — displace every target point by 1% of the cloud's radius and it reads 0.01 — so a cutoff can be picked by hand. Measured on five neuron skeletons:

nrms
2e-5 one cloud against a rotated copy of itself
0.008 one neuron against its own other half
0.06 – 0.11 two different neurons of the same type
~0.2 two unrelated point clouds

A rigid fit's and a deformable fit's nrms are on the same scale, which is the point: run both and the pair of numbers says whether the extra freedom bought anything. On two example skeletons, rigid gives 0.087 and deformable 0.058.

Below ~1e-3 there is nothing left to measure: the E-step runs in f32, which resolves the variance to ~1e-7 relative and hence the residual — its square root — to ~3e-4.

Why not pycpd?

pycpd is the reference Python implementation and has been unmaintained since 2021. It also writes the E-step as

P = np.sum((X[None, :, :] - TY[:, None, :]) ** 2, axis=2)

which materialises an M x N x 3 array, squares it into a second, then builds three more M x N temporaries — every iteration, of which there are typically 50-100. Two ~4,500-point clouds:

time peak RSS
pycpd 10.8 s +1,759 MB
rcpd, 1 core 2.48 s +0.4 MB
rcpd, 14 cores 0.29 s +25 MB

The memory is the more interesting column. The M-step only ever reads four reductions of the correspondence matrix, and its normaliser is a reduction over rows — so a block of columns is self-contained, and the whole E-step fits in one exp pass over a few hundred KB. Peak memory stops depending on M x N at all, which is what makes the batch entry point possible: at a gigabyte apiece you cannot hold fourteen registrations at once.

Across a grid the gap widens, because pycpd has no parallelism to give: a 12 x 12 pairwise alignment of the same clouds takes 30 s here against ~26 min.

Deformable is where pycpd stops being usable at all, and for an algorithmic reason rather than an implementation one: its M-step solves a dense M x M system once per iteration, which is cubic in the point count in time and quadratic in memory. On the same two ~4,500-point clouds, 50 iterations:

time peak RSS
pycpd 36.6 s +3,191 MB
rcpd 0.80 s +95 MB

The fix is the one in the original paper — approximate the kernel by its leading eigenpairs and apply the Woodbury identity, so each iteration is a K x K solve. Those eigenpairs come from a randomised subspace iteration that never forms the kernel, rather than the dense np.linalg.eigh(G) that cycpd uses; see core/src/lowrank.rs for why, and for why there is no fast Gauss transform here.

Held at the same iteration count — taking the deliberate convergence difference out of the comparison — the two implementations move the points to within 3e-6 of the cloud's radius at five iterations, and 3e-4 by sixty.

Deliberate differences from pycpd

Four, all fixes.

Convergence is relative. pycpd stops when its objective moves by less than tolerance in absolute terms — on a quantity that scales with the data, so the same clouds converge differently in nanometres and in microns. Here the test is on the relative change in the fitted variance, so it is dimensionless.

A deformable fit gets a second stopping rule, because it has enough freedom to drive the residual below what the f32 kernel can resolve — and there the variance stops descending and rattles by a few percent an iteration around a value it has already reached, so tolerance is never met however long it runs. Over a window of iterations the fit compares how far the variance moved with how far it travelled, and stops when it is travelling without arriving. On a well-matched pair that is 48 iterations rather than 200, for a residual 2% different; a fit that is merely slow is untouched, since a descending variance has the two distances equal.

scale is honoured. pycpd.RigidRegistration takes no scale argument in any released version, including master, so a caller that passes one has it land in **kwargs and a scale fitted regardless. scale=False here holds it at exactly 1, and scale_bounds holds it inside a range:

tr = rcpd.register_rigid(source, target, scale_bounds=(0.8, 1.25))

The limits are imposed at every EM step, so what comes back is the best alignment within them — rotation and translation re-fitted against the limited scale — rather than a free fit squashed into range afterwards. A pair that wants a scale outside the range comes back sitting exactly on the nearer limit, which is how you tell that the constraint bound. This is worth having on a grid of pairs: two clouds of the same shape at very different sizes will otherwise be talked into a flattering nrms by a scale you know to be impossible.

w is scale-free. The outlier term weighs a Gaussian density against a uniform one over some volume V; pycpd drops V, leaving a uniform density of 1, which is only right for data spanning about one unit. On a cloud spanning 1e4 units every point is declared an outlier and the fit collapses — at w=0.1 pycpd returns a scale of 0.19 against a true 1.0, and at w=0.01 it does not converge in 500 iterations. Here V is carried explicitly, as the bounding volume of the fixed cloud.

At w=0 — the default — the two agree to the precision of the f32 kernel: on real skeletons, transformed points match to 4e-8 of the cloud's extent.

beta is a fraction of the cloud, not a length. The motion-coherence kernel's width in data units silently means something different for the same object in nanometres and in microns, and the failure is quiet rather than loud: too wide a kernel makes the kernel matrix nearly all-ones, the deformation collapses to a global translation, and what comes back looks like a fit rather than like an error. Here beta is a fraction of the cloud's radius, so it means the same thing whatever the units. Note it is coupled to num_modes — the kernel's rank grows roughly as (1/beta)**3.

There is also no LinAlgError to work around. The 3x3 SVD here is a Jacobi eigensolve that cannot fail to converge, and a rank-deficient system (collinear points) completes the rotation basis rather than raising.

Development

cargo test --workspace                 # Rust core and bindings
cd py && maturin develop --release     # build the extension in place
cd py && pytest tests/                 # Python suite (checks against pycpd where installed)

Releasing

The version lives in the workspace Cargo.toml and nowhere else — the crate, the extension and the Python package all inherit it. Bump it, land the changelog entry, then tag:

git tag v0.1.0 && git push origin v0.1.0

.github/workflows/release.yml builds wheels for macOS, Linux (glibc and musl, x86-64 and aarch64) and Windows, plus an sdist that it builds back into a wheel to check it is not broken, publishes those to PyPI, then publishes the crate to crates.io. It refuses to start if the tag and the manifest disagree, since neither registry lets a version be replaced. Running it from the Actions tab instead builds everything and publishes nothing, which is how to test a change to it.

Licence

GPL-3.0-or-later.

Release files for rcpd 0.1.1

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

Source distribution (sdist)

Source distribution for rcpd 0.1.1
File Size Uploaded
rcpd-0.1.1.tar.gz 107.8 kB Details

Built distributions (wheels)

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

Total release size: 4.2 MB

Release files / rcpd-0.1.1.tar.gz

Download URL rcpd-0.1.1.tar.gz
Size 107.8 kB
Tags Source
SHA-256 checksum
How to use checksums
0e9930c9f993e97c69d8a946a2c2da7bb09f5ee7ed28b74959a8fa284269769f
BLAKE2b-256 checksum
How to use checksums
7537abea981b520ed2e23553bc2bec45bde5d4869348f101b00160429297cc66
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-win_amd64.whl

Download URL rcpd-0.1.1-cp39-abi3-win_amd64.whl
Size 440.7 kB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
b3ddf0001a0b5432b664f35e8ca6bd0a7b584486a3f5ef9b8ef91c1b1bfaf3ea
BLAKE2b-256 checksum
How to use checksums
a11f27ec26d03461e8d84bfa9a44a7d68f37b0701416fa2910b46e44961d703c
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl

Download URL rcpd-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl
Size 779.2 kB
Tags CPython 3.9 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
2a3e8abf44d678da041e6afb18a001efaec72fdc15e2d75ac79b6a0a3feb2599
BLAKE2b-256 checksum
How to use checksums
5911198ef5b2f27d299b660dc72c02aa158863d986268e43084a1030ced22019
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl

Download URL rcpd-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl
Size 731.0 kB
Tags CPython 3.9 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
f6d3619e07f20124c0bbb772a47a787e805df5b51675ed92f587f373c3b4c9d0
BLAKE2b-256 checksum
How to use checksums
ceead344ae56ee2c70adc1e7e7dac54425cc5ba8477a6ba8f2e290f1bab755c0
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL rcpd-0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 569.2 kB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
0c4dee9ff14f7ae01c2c60d89313595e6e752a8ee82c9ae3dde256973a79a303
BLAKE2b-256 checksum
How to use checksums
d0112fc285214cbf91defe3a7ef9e3f5c36a3efffab26e1bc1cdd0c9c22ec52f
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL rcpd-0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 552.1 kB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
a716f612367af1792250973b3f3587d37654c0dedf799f0bb8c7d2bae31ecd4b
BLAKE2b-256 checksum
How to use checksums
b13af3837326faae6e387cf5cbff3996573506ac2116d056a46335b564ce0d50
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-macosx_11_0_arm64.whl

Download URL rcpd-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
Size 519.5 kB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b1d2d4b297861ae58b62ae7e90112ef97346c6b4bfe4a2a0d564ebe35f5f6917
BLAKE2b-256 checksum
How to use checksums
56bc09bb2cb1fdcc64572ad518a3b4e36d3b115cb2b3c02ef2b17fcaf2a62908
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 11, 2026.

Transparency log

Release files / rcpd-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl

Download URL rcpd-0.1.1-cp39-abi3-macosx_10_12_x86_64.whl
Size 537.2 kB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
3910054d066b81fd70374c683d3eab49d6e037ea4ccb677991259b27f3475e77
BLAKE2b-256 checksum
How to use checksums
9b4dc47816a91ab6ee33f25192254c002ca3e9811e602e12da6765bfa539f232
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 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

8 release files

0.1.0

8 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