Heavy outliers, noisy correspondences, and a compact registration benchmark in one self-contained tutorial.
This repository compares Least Squares, RANSAC, GNC-GM, and GNC-TLS on classic Stanford 3D scan data.
This repository is a small but complete tutorial on outlier-robust point cloud registration.
The main script, simple_gnc_pcr.py, takes a .ply point cloud, applies a random rigid transform, injects inlier noise and extreme outliers, and then compares several pose estimators under the same synthetic correspondence set.
The goal is simple:
- show how quickly naive least squares breaks under heavy outlier corruption
- compare minimal solvers such as RANSAC against non-minimal robust estimators
- visualize correspondences and registration results in a way that is easy to explain in a talk, class, or GitHub project page
- keep the code path short enough that the full experiment can be read in one sitting
- Single-file experiment pipeline in
simple_gnc_pcr.py - Random SE(3) perturbation plus Monte Carlo evaluation over 30 runs
- Truncated Gaussian inlier noise and uniformly sampled spherical outliers
- Side-by-side comparison of
LS,RANSAC1K,RANSAC10K,GNC-GM, andGNC-TLS - Ready-to-use Stanford Bunny, Armadillo, Dragon, and Happy Buddha assets included in the repository
If you want the theory behind the code, I also wrote a short blog series on GNC and robust estimation:
- Graduated Non-Convexity (1): motivation for robust estimation, why outliers break plain least squares, and why non-convex robust costs create local minimum issues
- Graduated Non-Convexity (2): a walkthrough of Black-Rangarajan duality and the weighted least-squares view of robust kernels
- Graduated Non-Convexity (3): the actual GNC algorithm,
mucontinuation, and how GNC-GM is turned into a practical solver
The posts are a more theory-first companion to this repository, while the code here is meant to stay compact and experiment-focused.
| GT | LS | RANSAC-1K | RANSAC-10K | GNC-TLS |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
| Corr | RANSAC | GNC-GM | GNC-TLS |
|---|---|---|---|
Armadillo![]() |
![]() |
![]() |
![]() |
Bunny![]() |
![]() |
![]() |
![]() |
Dragon![]() |
![]() |
![]() |
![]() |
The misc/ folder also contains the animated GNC-TLS trajectory and additional presentation-style figures.
simple_gnc_pcr.py follows the pipeline below:
- Load a
.plypoint cloud and normalize it to a consistent scale. - Sample a random rotation and translation to create a target point cloud.
- Generate synthetic correspondences from ground-truth pairs.
- Add truncated Gaussian noise to inliers.
- Replace a large fraction of target correspondences with uniformly sampled outliers inside a sphere.
- Estimate the rigid transform with multiple solvers.
- Measure translation and rotation error over repeated Monte Carlo trials.
- Save box-style RMSE summaries as
translation_rmse.pngandrotation_rmse.png.
LS: vanilla least-squares alignment using Horn's methodRANSAC1K: minimal-set RANSAC with 1,000 iterationsRANSAC10K: minimal-set RANSAC with 10,000 iterationsGNC-GM: Graduated Non-Convexity with the Geman-McClure penaltyGNC-TLS: Graduated Non-Convexity with a truncated least-squares penalty
Install the Python dependencies:
pip install numpy open3d pandas seaborn matplotlib tqdmRun the tutorial on one of the bundled point clouds:
python simple_gnc_pcr.py \
--ply bunny/reconstruction/bun_zipper_res4.ply \
--noise_std 0.01 \
--outlier_ratio 0.99 \
--voxel 0.03You can also try other included assets:
python simple_gnc_pcr.py --ply Armadillo_scans/Armadillo.ply --noise_std 0.01 --outlier_ratio 0.95 --voxel 0.03 --visualize
python simple_gnc_pcr.py --ply dragon_recon/dragon_vrip_res4.ply --noise_std 0.01 --outlier_ratio 0.99 --voxel 0.03
python simple_gnc_pcr.py --ply happy_recon/happy_vrip_res4.ply --noise_std 0.01 --outlier_ratio 0.99 --voxel 0.03| Option | Description | Default |
|---|---|---|
--ply |
Path to the input .ply point cloud |
required |
--noise_std |
Standard deviation of Gaussian inlier noise | 0.01 |
--outlier_ratio |
Fraction of correspondences replaced by outliers | 0.99 |
--voxel |
Voxel size used for downsampling | 0.03 |
--visualize |
Open Open3D windows for correspondences and registration results | off |

















