TomRoyls/TurboPoissonRecon

Header-friendly C++23 screened Poisson surface reconstruction library with Open3D-compatible parameters

★ 0Forks 0C++GitHub ↗Compare

README

TurboPoissonRecon

A from-scratch, header-friendly C++23 screened Poisson surface reconstruction library in the spirit of Kazhdan & Hoppe (2013), with Open3D-compatible parameters.

Single-threaded by design, dependency-light (Eigen, nanoflann for the example tools), and easy to integrate: point clouds are consumed through a traits template in the style of small_gicp, so raw SoA/AoS buffers from existing projects plug in without conversion.

Features

  • Adaptive octree over a normalized cube (Morton-ordered, 2:1 balanced)
  • Degree-1 hat FEM basis on octree nodes; degree-2 B-spline density estimation
  • Screened Poisson system assembled exactly (Gauss quadrature product integrals)
  • Multigrid-preconditioned conjugate gradient solve over the full node system (prefix-subspace V-cycle, exact parent-cell block Gauss-Seidel smoothing, cascadic initial guess)
  • Marching cubes extraction with gradient-based quadratic edge fitting, active-cell banding, connectivity filtering, and optional corner smoothing
  • Templated point cloud input via CloudTraits (works with SoA structs, AoS vectors, or the built-in std::vector<Eigen::Vector3d> overloads)
  • Binary/ASCII PLY readers and writers for clouds and meshes
  • Catch2 test suite (65 tests) covering the math kernels, octree, IO, traits, end-to-end reconstruction topology (depths 4–7), PLY robustness (malformed counts, count-vs-file-size plausibility for binary and ASCII payloads, face index bounds, non-finite values, polygon fan triangulation), and a smoothing-denoises-instead-of-degrading regression test

Quick start

# Option A: subdirectory / FetchContent
add_subdirectory(TurboPoissonRecon)
target_link_libraries(your_app PRIVATE TurboPoissonRecon::TurboPoissonRecon)
# Option B: install + find_package
find_package(TurboPoissonRecon REQUIRED)

Dependencies (Eigen >= 3.3, optionally nanoflann and Catch2 for tests/examples) are located locally when available and fetched via CMake FetchContent otherwise.

Simple reconstruction

#include <TurboPoissonRecon/PoissonReconstruction.hpp>

std::vector<Eigen::Vector3d> points  = /* ... */;
std::vector<Eigen::Vector3d> normals = /* ... */;

turbopoissonrecon::PoissonParams params;
params.depth      = 8;   // octree depth (Open3D semantics)
params.scale      = 1.1; // cube scale around the bounding box
params.linearFit  = false;

const turbopoissonrecon::TriangleMesh mesh =
    turbopoissonrecon::PoissonReconstruct(points, normals, params);
// mesh.vertices, mesh.triangles, mesh.densities (float per vertex)

Custom point cloud types

Provide a CloudTraits specialization and pass any container; no copies:

struct MyCloud { const float* xyz; const float* nrm; size_t count; };

template <>
struct turbopoissonrecon::traits::Traits<MyCloud>
{
	[[nodiscard]] static std::size_t size(const MyCloud& c) { return c.count; }

	[[nodiscard]] static bool has_normals(const MyCloud&) { return true; }

	[[nodiscard]] static Eigen::Vector3f point(const MyCloud& c, std::size_t i)
	{
		return { c.xyz[3*i], c.xyz[3*i+1], c.xyz[3*i+2] };
	}

	[[nodiscard]] static Eigen::Vector3f normal(const MyCloud& c, std::size_t i)
	{
		return { c.nrm[3*i], c.nrm[3*i+1], c.nrm[3*i+2] };
	}
};

const auto mesh = turbopoissonrecon::PoissonReconstruct(myCloud, params);

Parameters

Parameter Default Meaning
depth 8 Maximum octree depth; values outside 2–15 are clamped
fullDepth 5 Depth of full (non-adaptive) refinement; clamped to depth
scale 1.1 Cube scale factor around the sample bounding box; must be ≥ 1
width 0 If > 0, target voxel size (overrides depth)
pointWeight 3.0 Screening (interpolation) weight
samplesPerNode 1.5 Minimum samples for finest-level splats
linearFit false Linear instead of quadratic edge placement
smoothingPasses 20 Laplacian passes over extracted corner values (capped at a quarter of the corner lattice width)
smoothingWeight 0.4 Per-pass neighbor blend factor

Note: pointWeight defaults to 3.0, a deliberate deviation from Open3D's internal 2.0, chosen for stronger denoising on noisy inputs.

Invalid parameters (non-finite or non-positive scale/samplesPerNode, non-finite or negative pointWeight/width, smoothingWeight outside [0, 1], negative smoothingPasses/fullDepth) and degenerate inputs (empty clouds, zero-extent bounding boxes, non-finite points) throw std::invalid_argument. depth and fullDepth are clamped instead of rejected. Samples whose normal is zero-length or non-finite are silently dropped before reconstruction; fewer than 16 usable samples throws. Inputs whose reconstruction cube exceeds the float range of the output mesh vertices are likewise rejected, as are pointWeight values too large for the depth-scaled screening terms.

Validation

Topology guarantees on synthetic clouds (Fibonacci-latticed, 6k–30k samples, with and without Gaussian jitter):

Input Result
Sphere, depth 4–7, noiseless closed genus-0 mesh (Euler = 2), 0 boundary / 0 non-manifold edges
Sphere, depth 5–6, noise σ = 0.02–0.03 closed genus-0 mesh, radial deviation ≲ 0.1
Torus, depth 5–6 closed genus-1 mesh (Euler = 0), 0 boundary / 0 non-manifold edges

Depth-7 watertightness at the 6k-sample minimum rests on two extraction defaults: the ±2.5-cell active-sample band (the surface can pass up to ~1.7 cells from the nearest sample) and the resolution-capped corner smoothing, which heals the between-sample troughs of the degree-1 hat basis. Tightening either reopens boundary-edge holes on sparser inputs; a dedicated Catch2 test pins the closed topology at depth 7.

Quantitative comparison

Noisy unit sphere (30,000 samples, uniform jitter ±0.02 per axis, seed 42), depth 7, single-threaded. Accuracy measured with examples/CompareMeshes against a 40,962-vertex analytic icosphere: symmetric vertex-to-vertex nearest-neighbor distances (nanoflann ANN); the Chamfer-RMS column is the pooled RMS over both directions.

Reconstruction Vertices Hausdorff Chamfer (mean) Chamfer (RMS)
TurboPoissonRecon 60,762 0.040 0.0127 0.0157
Kazhdan PoissonRecon (V8.0) 64,165 0.018 0.0076 0.0081
CGAL 6.x (poisson_surface_reconstruction_delaunay) 436 0.173 0.0386 0.0751

An earlier revision of this table also listed Open3D 0.19 (Hausdorff 0.048, Chamfer mean 0.0173, measured with the previous average-of-RMS definition); it could not be re-measured under the current metric in this environment and is retained here only as a historical reference point.

Reproduce with:

examples/GeneratePointCloud sphere 30000 noisy.ply 0.02 42
examples/ReconTool noisy.ply tpr.ply 7
comparisons/CgalRecon noisy.ply cgal.ply 0.5        # -DTURBOPOISSONRECON_BUILD_COMPARISON=ON
examples/CompareMeshes icosphere.ply tpr.ply

Notes:

  • Mean Chamfer accuracy is within ~65% of the Kazhdan reference; the Hausdorff gap comes from isolated residual bumps where the reference implementations' degree-2 FEM basis regularizes more aggressively. The screening weight and extraction smoothing parameters trade between fidelity to noisy inputs and denoising. (This row was re-measured twice: correcting the extraction's quadratic edge fit moved Hausdorff from 0.132 to 0.123, and the watertightness rework below traded Chamfer for it again.)
  • CGAL's delaunay-based method is listed as a qualitatively different, smoothing-heavy baseline; it is not the screened-Poisson pipeline. Its vertex-to-vertex distances reflect the coarse tessellation (436 vertices).
  • Runtime (depth 7, 30k samples, one thread): TurboPoissonRecon ≈ 45 s on the current development machine (≈ 15 s exact system assembly, ≈ 22 s solve, ≈ 8 s extraction including the wider active-cell band and stronger default corner smoothing; down from ≈ 4 min before the traversal, kernel and solver rework on the same machine — the phase split was ≈ 183 s assembly, ≈ 27 s solve, ≈ 28 s extraction back then), Kazhdan PoissonRecon ≈ 25 s (multigrid solver, degree-2 basis). The solver is a symmetric multigrid V-cycle inside preconditioned conjugate gradients: levels are the per-depth node prefixes (whose principal submatrices make the zero-extension embedding an exact Galerkin transfer), smoothing is an exact block Gauss-Seidel over the 2×2×2 sibling groups sharing a parent cell, and a cascadic climb provides the initial guess. The outer iteration still solves the assembled system exactly — in roughly a hundred iterations at depth 7 where the former Jacobi-preconditioned conjugate gradients needed about eight hundred.

Repository layout

include/TurboPoissonRecon/     public API (PoissonReconstruction, CloudTraits, PLY)
src/TurboPoissonRecon/         implementation (Bspline, Octree, PoissonSystem, IsoSurface)
examples/                      GeneratePointCloud, ReconTool, CompareMeshes
comparisons/                   CGAL and Kazhdan PoissonRecon comparison drivers
scripts/compare_open3d.py      Open3D reference runner
tests/PoissonTests/            Catch2 suite

Building

The library builds as a static library by design (no export macros or shared-library configuration); link it through the namespaced CMake target.

cmake -B build -DCMAKE_BUILD_TYPE=Release \
      -DTURBOPOISSONRECON_BUILD_TESTS=ON \
      -DTURBOPOISSONRECON_BUILD_EXAMPLES=ON \
      -DTURBOPOISSONRECON_BUILD_COMPARISON=ON
cmake --build build
ctest --test-dir build

All third-party dependencies are consumed locally when present (Eigen, CGAL, nanoflann) and fetched otherwise (Catch2, Kazhdan PoissonRecon for the comparison executable).

License

MIT — see the header of every source file.

Contributors

TomRoyls

Issues