File size: 10,252 Bytes
79e4bdf ef7cc6a 79e4bdf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Voxel downsampling — geometry preservation study</title>
<style>
:root {
--bg: #ffffff; --fg: #16181d; --muted: #5d6470; --line: #e3e6ea;
--accent: #2c7fb8; --safe: #e8f4fb; --aggr: #fdf1e6; --code: #f5f6f8;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14161a; --fg: #e8eaed; --muted: #9aa3af; --line: #2a2e35;
--accent: #6ab7e6; --safe: #17303f; --aggr: #3a2b1c; --code: #1c1f25;
}
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 2.5rem 1.25rem 5rem;
background: var(--bg); color: var(--fg);
font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
main { max-width: 1080px; margin: 0 auto; }
h1 { font-size: 1.9rem; line-height: 1.25; margin: 0 0 .4rem; letter-spacing: -.02em; }
h2 { font-size: 1.2rem; margin: 3rem 0 .6rem; letter-spacing: -.01em; }
.sub { color: var(--muted); margin: 0 0 2rem; }
p { margin: 0 0 1rem; }
code { background: var(--code); padding: .12em .4em; border-radius: 4px;
font: .875em/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; }
a { color: var(--accent); }
.cards { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(165px, 1fr)); margin: 1.5rem 0 0; }
.card { border: 1px solid var(--line); border-radius: 10px; padding: .85rem 1rem; }
.card .k { font-size: .74rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.card .v { font-size: 1.45rem; font-weight: 600; letter-spacing: -.02em; margin-top: .15rem; }
.card .n { font-size: .8rem; color: var(--muted); }
.scroll { overflow-x: auto; margin: 1rem 0; }
table { border-collapse: collapse; width: 100%; font-size: .875rem; white-space: nowrap; }
th, td { padding: .45rem .6rem; text-align: right; border-bottom: 1px solid var(--line); }
th { font-weight: 600; color: var(--muted); font-size: .78rem; text-transform: uppercase;
letter-spacing: .04em; text-align: right; }
th:first-child, td:first-child { text-align: left; }
td.note { text-align: left; color: var(--muted); font-size: .8rem; }
tr.safe { background: var(--safe); }
tr.aggr { background: var(--aggr); }
tbody tr.safe td, tbody tr.aggr td { font-weight: 600; }
figure { margin: 1rem 0 0; }
figure img { width: 100%; max-width: 100%; border: 1px solid var(--line); border-radius: 8px; background: #fff; }
figcaption { color: var(--muted); font-size: .85rem; margin-top: .5rem; }
ul { margin: 0 0 1rem; padding-left: 1.2rem; }
li { margin-bottom: .35rem; }
.files a { text-decoration: none; }
.files li { font-size: .92rem; }
.size { color: var(--muted); font-size: .82rem; }
hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0 0; }
footer { color: var(--muted); font-size: .85rem; margin-top: 1rem; }
</style>
</head>
<body>
<main>
<h1>Voxel downsampling with geometry preservation</h1>
<p class="sub">How far can <code>bigpointcloud_001.ply</code> be reduced before its geometry starts to go?
Measured, not eyeballed.</p>
<div class="cards">
<div class="card"><div class="k">Input</div><div class="v">11,357</div><div class="n">points, with normals + RGB</div></div>
<div class="card"><div class="k">Native resolution</div><div class="v">0.140</div><div class="n">mean nearest-neighbour spacing</div></div>
<div class="card"><div class="k">Safe voxel</div><div class="v">0.210</div><div class="n">8,832 pts — 78% kept</div></div>
<div class="card"><div class="k">Aggressive voxel</div><div class="v">0.279</div><div class="n">5,592 pts — 49% kept</div></div>
</div>
<h2>Method</h2>
<p>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.</p>
<ul>
<li><strong>safe</strong> — largest voxel whose <em>95th-percentile</em> error stays under one NN spacing</li>
<li><strong>aggressive</strong> — largest voxel whose <em>mean</em> error stays under one NN spacing</li>
</ul>
<h2>Voxel-size sweep</h2>
<div class="scroll">
<table>
<thead><tr>
<th>voxel</th><th>× NN</th><th>points</th><th>kept</th><th>mean err</th><th>RMS</th>
<th>p95</th><th>Hausdorff</th><th>normal dev</th><th>bbox shrink</th><th></th>
</tr></thead>
<tbody>
<tr><td>0.070</td><td>0.5×</td><td>11,320</td><td>99.7%</td><td>0.0002</td><td>0.0022</td><td>0.0000</td><td>0.0409</td><td>0.1°</td><td>0.00%</td><td class=note></td></tr>
<tr><td>0.105</td><td>0.7×</td><td>11,138</td><td>98.1%</td><td>0.0015</td><td>0.0080</td><td>0.0000</td><td>0.0788</td><td>0.4°</td><td>0.00%</td><td class=note></td></tr>
<tr><td>0.140</td><td>1.0×</td><td>10,705</td><td>94.3%</td><td>0.0058</td><td>0.0179</td><td>0.0540</td><td>0.0952</td><td>1.4°</td><td>0.18%</td><td class=note></td></tr>
<tr class="safe"><td>0.210</td><td>1.5×</td><td>8,832</td><td>77.8%</td><td>0.0315</td><td>0.0507</td><td>0.0990</td><td>0.1598</td><td>5.7°</td><td>0.28%</td><td class=note>safe</td></tr>
<tr class="aggr"><td>0.279</td><td>2.0×</td><td>5,592</td><td>49.2%</td><td>0.0829</td><td>0.0960</td><td>0.1536</td><td>0.2145</td><td>13.3°</td><td>0.35%</td><td class=note>aggressive</td></tr>
<tr><td>0.419</td><td>3.0×</td><td>2,620</td><td>23.1%</td><td>0.1467</td><td>0.1567</td><td>0.2357</td><td>0.3530</td><td>23.0°</td><td>1.15%</td><td class=note>geometry degrading</td></tr>
<tr><td>0.559</td><td>4.0×</td><td>1,511</td><td>13.3%</td><td>0.1999</td><td>0.2139</td><td>0.3175</td><td>0.4801</td><td>28.3°</td><td>1.50%</td><td class=note>geometry degrading</td></tr>
<tr><td>0.838</td><td>6.0×</td><td>655</td><td>5.8%</td><td>0.3102</td><td>0.3330</td><td>0.5034</td><td>0.7154</td><td>33.4°</td><td>4.69%</td><td class=note>geometry degrading</td></tr>
<tr><td>1.117</td><td>8.0×</td><td>385</td><td>3.4%</td><td>0.4017</td><td>0.4271</td><td>0.6343</td><td>0.9313</td><td>39.4°</td><td>11.05%</td><td class=note>geometry degrading</td></tr>
<tr><td>1.676</td><td>12.0×</td><td>163</td><td>1.4%</td><td>0.5899</td><td>0.6305</td><td>0.9446</td><td>1.2392</td><td>45.3°</td><td>15.01%</td><td class=note>geometry degrading</td></tr>
</tbody>
</table>
</div>
<p>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.</p>
<figure>
<img src="sweep.png" alt="Points kept, surface error and normal deviation versus voxel size">
<figcaption>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.</figcaption>
</figure>
<h2>Visual comparison</h2>
<figure>
<img src="comparison.png" alt="Original, safe voxel, aggressive voxel and random baseline, in two views">
<figcaption>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.</figcaption>
</figure>
<p><a href="comparison.html">Open the interactive 3-D viewer →</a> (rotate/zoom, original vs downsampled side by side)</p>
<h2>Why voxel, not random</h2>
<div class="scroll">
<table>
<thead><tr><th>method (5,592 pts)</th><th>mean</th><th>RMS</th><th>p95</th><th>Hausdorff</th><th>normal dev</th></tr></thead>
<tbody>
<tr class="aggr"><td>voxel 0.279</td><td>0.0829</td><td>0.0960</td><td>0.1536</td><td>0.2145</td><td>13.3°</td></tr>
<tr><td>random subsample</td><td>0.0860</td><td>0.1261</td><td>0.2310</td><td>0.6478</td><td>12.9°</td></tr>
</tbody>
</table>
</div>
<p>Mean error is nearly the same; the tails are not. Random subsampling is 50% worse at p95 and
<strong>3× worse at the Hausdorff bound</strong> — 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.</p>
<figure>
<img src="error_map.png" alt="Per-point error heat maps for the two voxel sizes and the random baseline">
<figcaption>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.</figcaption>
</figure>
<h2>Files</h2>
<ul class="files">
<li><a href="bigpointcloud_001.ply">bigpointcloud_001.ply</a> — original, 11,357 pts <span class="size">(0.58 MB)</span></li>
<li><a href="bigpointcloud_001_voxel0.210_safe.ply">bigpointcloud_001_voxel0.210_safe.ply</a> — 8,832 pts <span class="size">(0.45 MB)</span></li>
<li><a href="bigpointcloud_001_voxel0.279_aggressive.ply">bigpointcloud_001_voxel0.279_aggressive.ply</a> — 5,592 pts <span class="size">(0.29 MB)</span></li>
<li><a href="sweep.csv">sweep.csv</a> — full metric table</li>
<li><a href="voxel_downsample.py">voxel_downsample.py</a> — the script that produced all of this</li>
</ul>
<p>Normals and colours survive the downsample (averaged per voxel), so the outputs drop straight into
a PointNet++ pipeline that expects them.</p>
<h2>Reproduce</h2>
<p><code>python voxel_downsample.py --input bigpointcloud_001.ply</code> — auto-selects both operating
points. Override with <code>--voxel 0.28</code> or <code>--target-points 3000</code>. Needs
<code>open3d</code>, <code>numpy</code>, <code>matplotlib</code>, and optionally <code>plotly</code>
for the interactive viewer.</p>
<hr>
<footer>Generated with Open3D 0.18 · figures rendered headless with Matplotlib.</footer>
</main>
</body>
</html>
|