Skip to content

rsasaki0109/SpatialRust

Repository files navigation

SpatialRust

SpatialRust hero: public PCL table_scene_lms400 scan, voxel downsample, plane RANSAC, and Euclidean cluster labels from a real MVP pipeline run

Rust-native spatial computing
Point clouds · wgpu · COPC · RANSAC · ICP — native Rust, no C++ binding layer.

CI Docs Changelog License Rust 1.75+ wgpu

The hero GIF above is real MVP pipeline output (not a mockup): it uses the public PCL table_scene_lms400.pcd sample, voxel-downsamples it, RANSAC peels off the dominant plane, and Euclidean clustering lights up objects in color — every frame rendered straight from a live pipeline run.

SpatialRust MVP pipeline preview: RANSAC plane inliers, Euclidean cluster labels, and the pipeline stages

⚡ GPU-accelerated 🗂️ COPC-native 🦀 Pure Rust 🧩 Composable
wgpu voxel filter, ~3.9× at 2M points, automatic CPU fallback bounds + LOD partial reads straight off disk — no full-tile load no C++ / FFI binding layer to fight one MVP crate: IO → filter → segment → register

A multi-object point cloud rotating, each object colored by its DBSCAN cluster label The same scene voxelized into a rotating 3D occupancy grid of cyan blocks

DBSCAN clustering and voxel occupancy grids, generated by examples/make_gifs.py through the Python bindings.

Why SpatialRust?

Typical C++ stack (PCL / Open3D bindings) SpatialRust
Core language C++ + FFI glue Native Rust
GPU path varies by wrapper wgpu voxel filter with CPU fallback
COPC bolt-on scripts bounds + LOD queries in library & CLI
Pipeline glue code composable MVP crate

One command from LAS/COPC to labeled clusters:

cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- scan.las labeled.las

Partial COPC read + pipeline — stream only the region of interest straight off disk, no full-tile load:

cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- \
  --bounds 0,0,-1,100,100,1 --resolution 0.5 scan.copc.laz roi.copc.laz

COPC partial read: a bounds box selects a region of interest from the full tile, then the recentered subset is read out to roi.copc.laz

Performance

The voxel downsampler runs on CPU or GPU (wgpu). ExecutionPolicy::Auto keeps small clouds on the CPU — where it's fastest — and switches to the GPU as point counts grow, so you get the best of both without tuning.

Voxel downsample latency: CPU vs GPU across point counts, showing the GPU crossover above ~200k points and ~3.9x speedup at 2M

End-to-end centroid filter latency (leaf=4.0), measured via cargo bench -p spatialrust-filtering:

Points CPU GPU Winner
100k ~7 ms ~17 ms CPU
200k ~24 ms ~26 ms ~even
500k ~94 ms ~51 ms GPU
1M ~155 ms ~56 ms GPU (~2.8x)
2M ~389 ms ~101 ms GPU (~3.9x)

Reproduce: cargo bench -p spatialrust-filtering --features filter-voxel-gpu --bench voxel_downsample.

Normal estimation has an optional wgpu path (GpuNormalEstimator, feature-normal-gpu). In radius mode the neighbor search runs entirely on the GPU via a uniform grid (covariance + Jacobi eigensolver included), which is up to ~50× faster than the CPU KD-tree estimator:

Points CPU (KD-tree) GPU grid Speedup
100k ~220 ms ~8.6 ms ~26×
200k ~442 ms ~15 ms ~29×
500k ~1.47 s ~29 ms ~50×

(A k-nearest mode that keeps neighbor search on the CPU is also available but only ~1.1× — see notes.) Reproduce: cargo bench -p spatialrust-features --features feature-normal-gpu --bench normals.

vs PCL

A reproducible, apples-to-apples comparison against PCL 1.15.1 — both libraries process the same public PCL table_scene_lms400.pcd scan (460,400 points) with matching parameters (harness). Values below are from a local Windows release run using MSYS2 g++ 16.1.0 and vcpkg; rerun the harness before publishing fresh cross-machine numbers.

powershell -ExecutionPolicy Bypass -File bench\pcl_comparison\run.ps1
Operation SpatialRust PCL
Radius Outlier Removal 0.0899 s 1.8784 s 20.89× faster
Statistical Outlier Removal 0.1664 s 2.0933 s 12.58× faster
Normal estimation (k=10) 0.1461 s 1.9750 s 13.52× faster
Voxel downsample 0.0104 s 0.0181 s 1.74× faster

SpatialRust wins 4 of 4 against this PCL run; voxel downsampling now uses a specialized XYZ centroid path with compact u32 voxel keys for the common min-origin case.

vs Open3D

An Open3D comparison harness is available at bench/open3d_comparison. It runs the same public PCL table_scene_lms400.pcd scan through SpatialRust and Open3D with matching voxel, normal, statistical outlier, and radius outlier parameters:

python bench/open3d_comparison/run.py

Indicative local result on one Windows machine (Open3D 0.19.0, Python 3.12, 460,400-point public PCL sample):

Operation SpatialRust Open3D
Voxel downsample 0.0132 s 0.0234 s 1.77× faster
Normal estimation 0.1997 s 0.4946 s 2.48× faster
Statistical Outlier Removal 0.2105 s 0.6565 s 3.12× faster
Radius Outlier Removal 0.1049 s 66.4701 s 633.65× faster

Record CPU, Open3D version, Python version, and thread settings before publishing new numbers.

Registration methods

Four registration backends, compared on a synthetic box corner (7500 points, small misalignment):

Method Recovery error Time Notes
ICP (point-to-point) 0.0196 m ~147 ms slow to converge on planar surfaces
Point-to-plane ICP 0.0007 m ~6.5 ms best speed/accuracy balance
GICP 0.0006 m ~26 ms most accurate; per-point covariance (optional GPU covariance ~1.7×, register-gicp-gpu)
NDT 0.0008 m ~8.7 ms voxel distributions + Levenberg–Marquardt

See notes. Reproduce: cargo bench -p spatialrust-registration --features register-icp,register-icp-point-to-plane,register-gicp,register-ndt --bench registration.

Status

MVP pipeline is implemented end-to-end: PCD/PLY/LAS/COPC IO, voxel downsampling (CPU + optional wgpu), normals, RANSAC plane segmentation, Euclidean clustering, region growing, and registration (ICP point-to-point/point-to-plane, GICP, NDT). See docs/ARCHITECTURE.md for the master design.

Workspace crates

One dataflow, eleven crates — each pipeline stage maps to the crate that implements it, all sitting on a small math/core/search foundation:

SpatialRust architecture: Load → Voxel → Normals → Plane → Cluster → Register → Save dataflow with implementing crates, wgpu voxel acceleration, and the core/math/search foundation

Crate Role
spatialrust Meta crate / stable re-exports
spatialrust-core Point schema, metadata, execution traits
spatialrust-math Vec/Mat/Pose math primitives
spatialrust-io Point cloud readers/writers (PCD, PLY, LAS, COPC)
spatialrust-search KD-tree search, k-NN / radius graphs
spatialrust-filtering Voxel / FPS downsample, outlier removal, crop, MLS
spatialrust-features Normals (CPU + wgpu), ISS keypoints, FPFH, boundary, normal orientation
spatialrust-segmentation RANSAC plane / sphere / cylinder, Euclidean, DBSCAN, region growing, ground
spatialrust-registration ICP (point-to-point, point-to-plane), GICP, NDT, FPFH global
spatialrust-transform Affine transforms, recenter / normalize, merge, AABB / OBB
spatialrust-voxelize Voxel occupancy grids and LiDAR range images
spatialrust-metrics Chamfer / Hausdorff cloud distances
spatialrust-pipeline Composable MVP pipelines
spatialrust-gpu wgpu runtime and voxel kernels

Python

The whole pipeline is callable from Python with NumPy interop — no C++ binding layer:

import numpy as np
import spatialrust as sr

cloud = sr.PointCloud.from_xyz(points)            # (N, 3) float32 -> native cloud
result = sr.run_pipeline(cloud, leaf_size=0.1, cluster_tolerance=0.3)

print(result.plane_normal)                        # dominant plane normal (nx, ny, nz)
labels = result.labels()                          # (N,) int32 cluster ids
sr.write("labeled.las", result.output)            # LAS/PCD/PLY/COPC by extension

Top-down view of clusters segmented from the public PCL table_scene_lms400 point cloud via a single Python run_pipeline() call

Registration is callable too — align two scans with ICP / point-to-plane / GICP / NDT:

result = sr.register_gicp(source, target)   # also: register_icp / _point_to_plane / _ndt
T = result.transform()                       # 4x4 matrix mapping source -> target

Before/after of two scans aligned by SpatialRust: a misaligned orange source scan snaps onto the blue target after registration

And it's a preprocessing front-end for learned models — turn a scan into model-ready tensors in a few calls (clean → unit-sphere normalize → FPS → voxel grid / range image / k-NN edge_index):

sampled = sr.farthest_point_sampling(sr.normalize_unit_sphere(cloud), 2048)
occ, origin, vsize = sr.voxelize(sampled, voxel_size=0.06)   # (nz, ny, nx) occupancy
edge_index = sr.knn_graph(sampled, k=16)                     # (2, E) PyG-style graph
rimg = sr.range_image(sampled, width=256, height=64)         # (H, W) LiDAR depth

Four panels: FPS-sampled points, a voxel occupancy grid, a LiDAR range image, and a k-NN graph — the model-ready tensors SpatialRust produces from one scan

Generated by examples/ml_preprocess.py — see the Python README.

Build the extension with maturin and reproduce the Python previews from the same public sample:

pip install maturin numpy matplotlib
cd crates/spatialrust-py && maturin develop --release
mkdir -p ../../target/readme-data
curl -L --fail -o ../../target/readme-data/table_scene_lms400.pcd \
  https://raw.githubusercontent.com/PointCloudLibrary/data/master/tutorials/table_scene_lms400.pcd
PUBLIC=../../target/readme-data/table_scene_lms400.pcd
python examples/segment_room.py \
  --input "$PUBLIC" \
  --leaf-size 0.03 --plane-distance 0.025 \
  --cluster-tolerance 0.06 --min-cluster-size 8 \
  --png ../../docs/assets/python_segmentation.png
python examples/register_scans.py \
  --input "$PUBLIC" --leaf 0.05 \
  --png ../../docs/assets/python_registration.png
python examples/ml_preprocess.py \
  --input "$PUBLIC" \
  --png ../../docs/assets/ml_preprocess.png

Prebuilt abi3 wheels (CPython 3.8+) are produced by CI and published to PyPI on tagged releases (pip install spatialrust). See crates/spatialrust-py/README.md for the full Python API.

Quick start

cargo test --workspace
cargo test -p spatialrust --features mvp
cargo doc --workspace --open

CLI (MVP pipeline)

cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- input.las output.las
cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- \
  --leaf-size 0.2 --voxel-policy auto scan.copc.laz out.copc.laz
cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- \
  --bounds 0,0,-1,100,100,1 scan.copc.laz roi.copc.laz
cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- \
  --bounds 0,0,-1,100,100,1 --resolution 0.5 scan.copc.laz roi.copc.laz
cargo run -p spatialrust --features mvp --bin spatialrust-mvp -- \
  --resolution 0.5 scan.copc.laz coarse.copc.laz

Library

Load or save by file extension:

use spatialrust::{read_point_cloud_file, write_point_cloud_file};

let cloud = read_point_cloud_file("scan.las")?;
write_point_cloud_file("output.ply", &cloud)?;

COPC partial read:

use spatialrust::{read_copc_file_with_query, CopcBounds, CopcQuery};

let bounds = CopcBounds::from_ranges((0.0, 100.0), (0.0, 100.0), (-1.0, 1.0));
let cloud = read_copc_file_with_query("scan.copc.laz", &CopcQuery::bounds(bounds))?;

MVP target pipeline

PCD/PLY/LAS/COPC -> voxel downsample -> normals -> plane RANSAC -> clustering -> ICP -> save

Top-down 2D view of the MVP pipeline stages on the public PCL table_scene_lms400 point cloud: input scan, voxel grid, plane RANSAC, and Euclidean clusters

GPU voxel downsampling (wgpu) is available behind features. ExecutionPolicy::Auto keeps CPU for clouds below ~500k points (centroid mode).

cargo test -p spatialrust-gpu --features gpu-wgpu
cargo test -p spatialrust --features filter-voxel-gpu
cargo test -p spatialrust --features mvp mvp_copc_pipeline_roundtrip
cargo test -p spatialrust --features mvp mvp_copc_query_pipeline

README visuals

The main README pipeline visuals use the public PCL table_scene_lms400.pcd sample, cached under target/readme-data/ at generation time rather than committed to the repository. Regenerate them with:

cargo run -p spatialrust --features mvp --example readme_mvp_preview

Outputs: readme_hero.gif (header), readme_mvp_preview.svg (pipeline panel), copc_query.gif (COPC partial read), benchmark_voxel.svg (Performance chart), architecture.svg (crates diagram), readme_mvp_pipeline.gif (compact 2D view), and social_preview.svg.

Use SPATIALRUST_README_CLOUD=/path/to/cloud.pcd to render the same assets from another local public dataset.

The rotating clusters_rotating.gif and voxelize_rotating.gif are generated through the Python bindings from the same public sample: python crates/spatialrust-py/examples/make_gifs.py --input target/readme-data/table_scene_lms400.pcd (needs maturin develop + Matplotlib/Pillow).

Social preview

Upload docs/assets/social_preview.svg (or export to PNG) as the GitHub repository social image under Settings → General → Social preview.

License

Licensed under MIT OR Apache-2.0 at your option.

About

Rust-native spatial computing: point clouds, LiDAR/COPC, geometry, wgpu, RANSAC, ICP, and robotics-ready spatial data

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors