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>