Voxel downsampling with geometry preservation

How far can bigpointcloud_001.ply be reduced before its geometry starts to go? Measured, not eyeballed.

Input
11,357
points, with normals + RGB
Native resolution
0.140
mean nearest-neighbour spacing
Safe voxel
0.210
8,832 pts — 78% kept
Aggressive voxel
0.279
5,592 pts — 49% kept

Method

Voxel size is chosen against an error budget rather than by guesswork. Every candidate is scored by how far the surface actually moved — the distance from each original point to the nearest kept point — plus how much the normals rotated and whether the bounding box shrank. The natural yardstick is the cloud's own nearest-neighbour spacing (0.140): a downsample that perturbs the surface by less than that has not degraded it beyond the noise the original sampling already carries.

Voxel-size sweep

voxel× NNpointskeptmean errRMS p95Hausdorffnormal devbbox shrink
0.0700.5×11,32099.7%0.00020.00220.00000.04090.1°0.00%
0.1050.7×11,13898.1%0.00150.00800.00000.07880.4°0.00%
0.1401.0×10,70594.3%0.00580.01790.05400.09521.4°0.18%
0.2101.5×8,83277.8%0.03150.05070.09900.15985.7°0.28%safe
0.2792.0×5,59249.2%0.08290.09600.15360.214513.3°0.35%aggressive
0.4193.0×2,62023.1%0.14670.15670.23570.353023.0°1.15%geometry degrading
0.5594.0×1,51113.3%0.19990.21390.31750.480128.3°1.50%geometry degrading
0.8386.0×6555.8%0.31020.33300.50340.715433.4°4.69%geometry degrading
1.1178.0×3853.4%0.40170.42710.63430.931339.4°11.05%geometry degrading
1.67612.0×1631.4%0.58990.63050.94461.239245.3°15.01%geometry degrading

Geometry holds to about 0.28 — half the points, surface displaced 0.083 units on average (0.4% of the 20.7-unit bbox diagonal), normals within 13°. Past 0.42 it degrades quickly: normals swing 23°+, and by voxel 1.1 the bounding box itself has lost 11%, meaning thin structure is being erased outright rather than merely thinned.

Points kept, surface error and normal deviation versus voxel size
Point count falls off a cliff well before the error curves rise steeply — the region left of the green line buys a large reduction almost for free.

Visual comparison

Original, safe voxel, aggressive voxel and random baseline, in two views
Original · voxel 0.210 · voxel 0.279 · random subsample at the same budget. Colours are the cloud's own RGB; point size is identical across panels, so density differences are real.

Open the interactive 3-D viewer → (rotate/zoom, original vs downsampled side by side)

Why voxel, not random

method (5,592 pts)meanRMSp95Hausdorffnormal dev
voxel 0.2790.08290.09600.15360.214513.3°
random subsample0.08600.12610.23100.647812.9°

Mean error is nearly the same; the tails are not. Random subsampling is 50% worse at p95 and 3× worse at the Hausdorff bound — it thins uniformly-dense regions and leaves holes wherever the dice fall badly. Voxel downsampling caps the worst-case gap by construction, and averages positions, colours and normals within each cell instead of picking a survivor.

Per-point error heat maps for the two voxel sizes and the random baseline
Original points coloured by distance to the kept surface, shared scale. Voxel error is dim and uniform; the random baseline is speckled with bright hotspots — the same average, a much worse worst case.

Files

Normals and colours survive the downsample (averaged per voxel), so the outputs drop straight into a PointNet++ pipeline that expects them.

Reproduce

python voxel_downsample.py --input bigpointcloud_001.ply — auto-selects both operating points. Override with --voxel 0.28 or --target-points 3000. Needs open3d, numpy, matplotlib, and optionally plotly for the interactive viewer.