Voxel downsampling with geometry preservation
How far can bigpointcloud_001.ply be reduced before its geometry starts to go?
Measured, not eyeballed.
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.
- safe — largest voxel whose 95th-percentile error stays under one NN spacing
- aggressive — largest voxel whose mean error stays under one NN spacing
Voxel-size sweep
| voxel | × NN | points | kept | mean err | RMS | p95 | Hausdorff | normal dev | bbox shrink | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0.070 | 0.5× | 11,320 | 99.7% | 0.0002 | 0.0022 | 0.0000 | 0.0409 | 0.1° | 0.00% | |
| 0.105 | 0.7× | 11,138 | 98.1% | 0.0015 | 0.0080 | 0.0000 | 0.0788 | 0.4° | 0.00% | |
| 0.140 | 1.0× | 10,705 | 94.3% | 0.0058 | 0.0179 | 0.0540 | 0.0952 | 1.4° | 0.18% | |
| 0.210 | 1.5× | 8,832 | 77.8% | 0.0315 | 0.0507 | 0.0990 | 0.1598 | 5.7° | 0.28% | safe |
| 0.279 | 2.0× | 5,592 | 49.2% | 0.0829 | 0.0960 | 0.1536 | 0.2145 | 13.3° | 0.35% | aggressive |
| 0.419 | 3.0× | 2,620 | 23.1% | 0.1467 | 0.1567 | 0.2357 | 0.3530 | 23.0° | 1.15% | geometry degrading |
| 0.559 | 4.0× | 1,511 | 13.3% | 0.1999 | 0.2139 | 0.3175 | 0.4801 | 28.3° | 1.50% | geometry degrading |
| 0.838 | 6.0× | 655 | 5.8% | 0.3102 | 0.3330 | 0.5034 | 0.7154 | 33.4° | 4.69% | geometry degrading |
| 1.117 | 8.0× | 385 | 3.4% | 0.4017 | 0.4271 | 0.6343 | 0.9313 | 39.4° | 11.05% | geometry degrading |
| 1.676 | 12.0× | 163 | 1.4% | 0.5899 | 0.6305 | 0.9446 | 1.2392 | 45.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.
Visual comparison
Open the interactive 3-D viewer → (rotate/zoom, original vs downsampled side by side)
Why voxel, not random
| method (5,592 pts) | mean | RMS | p95 | Hausdorff | normal dev |
|---|---|---|---|---|---|
| voxel 0.279 | 0.0829 | 0.0960 | 0.1536 | 0.2145 | 13.3° |
| random subsample | 0.0860 | 0.1261 | 0.2310 | 0.6478 | 12.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.
Files
- bigpointcloud_001.ply — original, 11,357 pts (0.58 MB)
- bigpointcloud_001_voxel0.210_safe.ply — 8,832 pts (0.45 MB)
- bigpointcloud_001_voxel0.279_aggressive.ply — 5,592 pts (0.29 MB)
- sweep.csv — full metric table
- voxel_downsample.py — the script that produced all of this
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.