las-laz-js 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 (2026-10-22)
4
+
5
+ Denoise: stray points, normals and smoothing, for fuzzy handheld scans.
6
+
7
+ - `selectOutliers(cols, { k = 8, multiplier = 2 })` selects stray points by PDAL's statistical
8
+ outlier rule (a point's mean distance to its k nearest above the cloud's average plus
9
+ `multiplier` standard deviations), for `removePoints`. Checked point for point against PDAL's
10
+ `filters.outlier` `method=statistical`.
11
+ - `denoisePoints(cols, { method, k = 16, maxDistance, sigma, iterations = 1, normals, inPlace })`
12
+ smooths the points against their neighbours (the other points among the k nearest within
13
+ `maxDistance`, which is required): `"bilateral"` (default) moves each point along its normal
14
+ and keeps crests sharp; `"mls"` moves it onto its neighbours' fitted plane. The noise level
15
+ `sigma` is measured unless given. Returns columns as `transformPoints` does; `normals: true`
16
+ adds a `normal` column.
17
+ - `pointNormals(cols, { k = 16, maxDistance })` → `{ normal, residual }`: unit normals (pointing
18
+ up) and each point's RMS distance from its fitted plane; `estimateNoise` is the median residual
19
+ over up to 100,000 points.
20
+ - `nearestPoints(index, point, k, { maxDistance, xy })` → `{ points, d }`: the k nearest on the
21
+ 0.6.0 voxel index, nearest first, ties to the lowest number.
22
+ - The denoise tools search an implicit k-d tree (about 4.6 bytes a point), which suits a scan's
23
+ thousand-fold changes of density. On a 1 M-point handheld scan (Node): `selectOutliers` 2.5 s,
24
+ `pointNormals` 4.6 s, `denoisePoints` 6.3 s, the measured noise going from 0.60 cm to 0.11 cm; on a
25
+ 34 M-point scan, `denoisePoints` takes about 3.7 min. A cluster of exact duplicates costs a search k
26
+ points, not the cluster's size.
27
+ - Demo: a Denoise section on the Edit tab and a Roughness colour mode.
28
+
29
+ ## 0.6.2 (2026-10-22)
30
+
31
+ - `dedupe` and `dedupeSelection` handle more than 16,777,216 occupied cells. They used a
32
+ JavaScript Map, which stops at 2^24 entries, so a 20-30 M-point cloud at a small tolerance threw
33
+ `RangeError: Map maximum size exceeded`. A typed-array hash table (5 bytes a slot) replaces
34
+ it, with the same results and about 1.5 to 2 times the speed (2 M points: 0.9 s in 2D, 2.2 s in 3D).
35
+
3
36
  ## 0.6.1 (2026-10-22)
4
37
 
5
38
  - Demo: a read bigger than the draw cap (20 M) is drawn as an even spread over the whole cloud,
package/README.md CHANGED
@@ -40,7 +40,8 @@ library's Worker (LAZ on every core) behind a progress bar, then shown once in a
40
40
  colour changes recolour the points already read, without reading the file again. The side panel shows the header, VLRs, coordinate
41
41
  system and LAZ compression; reads with class, return, bounds, thinning and dedupe filters;
42
42
  colours by RGB, intensity, elevation or class; lists point statistics; selects points by polygon,
43
- box or class and keeps, deletes, splits, transforms or crops them (the Edit tab); and saves the
43
+ box, class, dust or outliers and keeps, deletes, splits, transforms, smooths or crops them (the
44
+ Edit tab), with a Roughness colour to see the noise; and saves the
44
45
  points back out as LAS or LAZ. To run it locally: `npm run demo`, then open http://localhost:8080/.
45
46
 
46
47
  ## Reading
@@ -235,15 +236,56 @@ cost of thin features. It works on columns in memory (x / y / z, or a k = 3 `pos
235
236
  points take about 2 s in Node. With PDAL's `filters.outlier` (`method=radius`), `min_k` is the same
236
237
  as `minNeighbours`, but PDAL does not count a point exactly `radius` away.
237
238
 
239
+ ### Denoise
240
+
241
+ ```js
242
+ import { selectOutliers, removePoints, estimateNoise, denoisePoints, pointNormals } from "las-laz-js";
243
+
244
+ var strays = selectOutliers(pts); // PDAL's statistical rule: k 8, multiplier 2
245
+ var kept = removePoints(pts, strays);
246
+ var noise = estimateNoise(kept, { maxDistance: 0.3 }); // e.g. 0.006: 6 mm of fuzz
247
+ var smooth = denoisePoints(kept, { maxDistance: 0.3 }); // bilateral; { method: "mls" } for the plane fit
248
+ var { normal, residual } = pointNormals(smooth, { maxDistance: 0.3 }); // unit normals, local roughness
249
+ ```
250
+
251
+ A handheld scan reads a flat bench as a band centimetres thick. Remove the stray points first
252
+ (`selectOutliers`), so they cannot pull the surface, then smooth what is left (`denoisePoints`).
253
+ - **`selectOutliers(cols, { k = 8, multiplier = 2 })`** — each point's mean distance to its k
254
+ nearest other points; a point is selected when that mean is above the cloud's average of those
255
+ means plus `multiplier` standard deviations. It is PDAL's `filters.outlier` `method=statistical`
256
+ and selects the same points.
257
+ - **`denoisePoints(cols, { method, k = 16, maxDistance, sigma, iterations = 1, normals, inPlace })`**
258
+ moves each point against its neighbours — the other points among its k nearest within
259
+ `maxDistance` (required: about 0.2–0.5 m for a handheld scan; a point with fewer than 5 stays
260
+ put). `"bilateral"` (the default) moves a point along its normal by the weighted height of its
261
+ neighbours above its plane, and nearly ignores points far off that plane, so a crest or a toe
262
+ stays sharp. `"mls"` moves it onto the plane fitted to its neighbours: a little smoother, and
263
+ it rounds edges and the top of a cone. `sigma` is the noise level in metres; omitted, it is
264
+ measured (`estimateNoise`). More `iterations`, or a larger `k`, smooth more. The result is
265
+ as `transformPoints` returns: new x, y, z (or `positions`) of the input's types, every other
266
+ column shared; `normals: true` adds a `normal` column.
267
+ - **`pointNormals`** gives each point the unit normal of the plane through it and its neighbours
268
+ (pointing up) and the residual, the RMS distance of those points from the plane: a roughness
269
+ map. Fewer than 3 neighbours, or points all on one line: normal (0, 0, 0), residual NaN.
270
+ **`estimateNoise`** is the median residual over up to 100,000 points spread through the cloud.
271
+
272
+ On a 1 M-point handheld scan (Node, one thread): `selectOutliers` 2.5 s, `estimateNoise` 0.7 s,
273
+ `pointNormals` 4.6 s, `denoisePoints` 6.3 s, taking the measured noise from 0.60 cm to 0.11 cm. On a
274
+ 34 M-point scan: 2.1 min, 14 s, 3.2 min and 3.7 min (about 6.5 µs a point), noise 0.74 cm to 0.58 cm.
275
+ The searches run on a k-d tree (about 4.6 bytes a point); an output's coordinates cost what the
276
+ input's do. The calls are synchronous; for a big scan, run them in a Worker and pass
277
+ `onProgress(done, total)`.
278
+
238
279
  ### Neighbour index
239
280
 
240
281
  ```js
241
- import { buildIndex, nearestPoint, pointsInRadius, pointsNearRay } from "las-laz-js";
282
+ import { buildIndex, nearestPoint, nearestPoints, pointsInRadius, pointsNearRay } from "las-laz-js";
242
283
 
243
284
  var index = buildIndex(pts); // or { cellSize: 0.5 }; omitted: ~16 points a cell
244
285
  var i = nearestPoint(index, [x, y, z]); // point number, or -1
245
286
  var j = nearestPoint(index, [x, y], { xy: true, maxDistance: 0.3 }); // nearest in plan, within 0.3
246
287
  var near = pointsInRadius(index, [x, y, z], 5); // a selection
288
+ var knn = nearestPoints(index, [x, y, z], 8); // { points, d }: the 8 nearest, nearest first
247
289
  var hit = pointsNearRay(index, origin, direction, { radius: 0.2, growth: 0 });
248
290
  // hit = { points, t, d }: every point within radius + growth · t of the ray, t along it, d from its axis
249
291
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "las-laz-js",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader and writer for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { LasError, ERROR_CODES } from "../errors.js";
14
14
  import { columnsOf, gather, sameType } from "../ops/columns.js";
15
+ import { hash32 } from "../index/grid.js";
15
16
 
16
17
  /**
17
18
  * The points dedupe keeps, and where every input point went.
@@ -46,12 +47,40 @@ export function dedupeSelection(cols, tolerance, opts) {
46
47
  var dense = (hi[0] - lo[0] + 3) * SY * SZ <= 9007199254740992;
47
48
  function key(i, j, k) { return dense ? (i * SY + j) * SZ + k : (i * 73856093 + j * 19349663 + k * 83492791) % 9007199254740881; }
48
49
 
49
- // Each cell is a linked list through next[] (Map: key -> most recent point)
50
- var head = new Map();
50
+ function cellKey(p) {
51
+ return key(Math.floor(x[p] / tolerance) - lo[0] + 1, Math.floor(y[p] / tolerance) - lo[1] + 1, use3d ? Math.floor(z[p] / tolerance) - lo[2] + 1 : 1);
52
+ }
53
+
54
+ // Each cell is a linked list of kept points through next[]. Its newest point sits in
55
+ // an open-addressing table (linear probing, power-of-two capacity, at most half
56
+ // full). A slot holds the point number and an 8-bit tag of its key's hash; the key
57
+ // itself is recomputed from the point's cell, only when the tag matches. 5 bytes a
58
+ // slot. (A Map stops at 2^24 cells.)
59
+ var cells = (hi[0] - lo[0] + 1) * (hi[1] - lo[1] + 1) * (hi[2] - lo[2] + 1);
60
+ var cap = 16;
61
+ while (cap < Math.min(n, cells)) cap *= 2;
62
+ var mask = cap - 1, used = 0, table = new Int32Array(cap).fill(-1), tags = new Uint8Array(cap);
63
+ var tag = 0; // the tag of the key find() last looked up
64
+ // The slot of cell key c: its newest kept point, or -1 (the free slot c would take)
65
+ function find(c) {
66
+ var g = hash32(c), h = g & mask;
67
+ tag = Math.imul(g, 0x9E3779B1) >>> 24; // top bits of a multiply mix every bit of g
68
+ while (table[h] >= 0 && (tags[h] !== tag || cellKey(table[h]) !== c)) h = (h + 1) & mask;
69
+ return h;
70
+ }
71
+ function grow() {
72
+ var old = table, oldTags = tags;
73
+ cap *= 2; mask = cap - 1; table = new Int32Array(cap).fill(-1); tags = new Uint8Array(cap);
74
+ for (var s = 0; s < old.length; s++) {
75
+ if (old[s] < 0) continue;
76
+ var h = hash32(cellKey(old[s])) & mask;
77
+ while (table[h] >= 0) h = (h + 1) & mask;
78
+ table[h] = old[s]; tags[h] = oldTags[s];
79
+ }
80
+ }
51
81
  var next = new Int32Array(n);
52
82
  var kept = new Uint32Array(n);
53
83
  var map = new Uint32Array(n);
54
- var dropped = [];
55
84
  var nKept = 0;
56
85
  for (var i = 0; i < n; i++) {
57
86
  var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
@@ -60,8 +89,8 @@ export function dedupeSelection(cols, tolerance, opts) {
60
89
  for (var dx = -1; dx <= 1 && !dup; dx++) {
61
90
  for (var dy = -1; dy <= 1 && !dup; dy++) {
62
91
  for (var dz = -dzRange; dz <= dzRange && !dup; dz++) {
63
- var k = head.get(key(cx + dx, cy + dy, cz + dz));
64
- while (k !== undefined && k >= 0) {
92
+ var k = table[find(key(cx + dx, cy + dy, cz + dz))];
93
+ while (k >= 0) {
65
94
  var ex = x[k] - x[i], ey = y[k] - y[i];
66
95
  var ez = use3d ? z[k] - z[i] : 0;
67
96
  if (ex * ex + ey * ey + ez * ez <= tol2) { dup = true; break; }
@@ -70,25 +99,31 @@ export function dedupeSelection(cols, tolerance, opts) {
70
99
  }
71
100
  }
72
101
  }
73
- if (dup) { dropped.push(i); continue; }
74
- var own = key(cx, cy, cz);
75
- var prev = head.get(own);
76
- next[i] = prev === undefined ? -1 : prev;
77
- head.set(own, i);
102
+ if (dup) continue;
103
+ var own = key(cx, cy, cz), h = find(own);
104
+ if (table[h] < 0) {
105
+ if (2 * (used + 1) > cap) { grow(); h = find(own); }
106
+ used++;
107
+ tags[h] = tag;
108
+ }
109
+ next[i] = table[h];
110
+ table[h] = i;
78
111
  map[i] = nKept;
79
112
  kept[nKept++] = i;
80
113
  }
81
114
 
82
- // Each dropped point maps to its nearest kept point (the cell lists hold kept points only)
83
- dropped.forEach(function (i) {
115
+ // Each dropped point (every point not in kept) maps to its nearest kept point;
116
+ // the cell lists hold kept points only
117
+ for (i = 0, p = 0; i < n; i++) {
118
+ if (p < nKept && kept[p] === i) { p++; continue; }
84
119
  var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
85
120
  var cz = use3d ? Math.floor(z[i] / tolerance) - lo[2] + 1 : 1;
86
121
  var best = -1, bd = Infinity;
87
122
  for (var dx = -1; dx <= 1; dx++) {
88
123
  for (var dy = -1; dy <= 1; dy++) {
89
124
  for (var dz = -dzRange; dz <= dzRange; dz++) {
90
- var k = head.get(key(cx + dx, cy + dy, cz + dz));
91
- while (k !== undefined && k >= 0) {
125
+ var k = table[find(key(cx + dx, cy + dy, cz + dz))];
126
+ while (k >= 0) {
92
127
  var ex = x[k] - x[i], ey = y[k] - y[i];
93
128
  var ez = use3d ? z[k] - z[i] : 0;
94
129
  var d = ex * ex + ey * ey + ez * ez;
@@ -99,7 +134,7 @@ export function dedupeSelection(cols, tolerance, opts) {
99
134
  }
100
135
  }
101
136
  map[i] = map[best];
102
- });
137
+ }
103
138
  return { keep: kept.slice(0, nKept), map: map };
104
139
  }
105
140
 
package/src/index/grid.js CHANGED
@@ -56,6 +56,16 @@ function fmix32(h) {
56
56
  return (h ^ (h >>> 16)) >>> 0;
57
57
  }
58
58
 
59
+ /**
60
+ * A 32-bit hash of a cell key (an integer below 2^53).
61
+ * @param {number} key
62
+ * @returns {number}
63
+ */
64
+ export function hash32(key) {
65
+ var lo = key % 4294967296, hi = (key - lo) / 4294967296;
66
+ return fmix32(lo ^ fmix32(hi));
67
+ }
68
+
59
69
  /**
60
70
  * The home slot of a cell key (an integer below 2^53) in a table of mask + 1 slots.
61
71
  * @param {number} key
@@ -63,8 +73,7 @@ function fmix32(h) {
63
73
  * @returns {number}
64
74
  */
65
75
  export function hashKey(key, mask) {
66
- var lo = key % 4294967296, hi = (key - lo) / 4294967296;
67
- return fmix32(lo ^ fmix32(hi)) & mask;
76
+ return hash32(key) & mask;
68
77
  }
69
78
 
70
79
  function nextPow2(n) {
@@ -0,0 +1,176 @@
1
+ // src/index/kdtree.js
2
+ //==========================================================
3
+ // A k-d tree for k-nearest searches over clouds whose density changes
4
+ // a thousand-fold (a handheld scan: dense beside the walk path,
5
+ // sparse far from it), where one grid cell size cannot suit every
6
+ // point. Implicit and balanced: point numbers are permuted so each
7
+ // node's points are perm[lo, hi), split at mid = floor((lo + hi) / 2)
8
+ // on the axis of the node's largest extent, with every left point at
9
+ // most the split value and every right point at least it. A node
10
+ // stores its axis (a byte) and split value (a Float64), in heap order.
11
+ // Leaves hold up to 16 points. About 4.6 bytes a point. A node whose
12
+ // points all coincide (zero extent) is not split: its point numbers are
13
+ // sorted (axis UNIFORM) and a search offers only the lowest ones, so a
14
+ // cluster of exact duplicates costs k, not its size, per search. Distances are checked in Float64 and pruning compares the
15
+ // same rounded differences, so results are exact; ties go to the
16
+ // lowest point number.
17
+ //==========================================================
18
+
19
+ import { LasError, ERROR_CODES } from "../errors.js";
20
+ import { coordsOf, checkPointCount } from "./grid.js";
21
+
22
+ var LEAF = 16, UNIFORM = 3;
23
+
24
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
25
+
26
+ /**
27
+ * Build a k-d tree over point columns.
28
+ * @param {object} cols - point columns (x, y, z or a k = 3 positions column).
29
+ * @param {string} fn - the calling function's name, for messages.
30
+ * @returns {{count: number, perm: Uint32Array, axis: Uint8Array, split: Float64Array, leafSize: number, coords: object}}
31
+ */
32
+ export function buildKdTree(cols, fn) {
33
+ var coords = coordsOf(cols, fn), n = cols.count;
34
+ checkPointCount(n);
35
+ var A = [coords.x, coords.y, coords.z], st = coords.stride, i, o;
36
+ for (i = 0, o = 0; i < n; i++, o += st) {
37
+ var a = A[0][o], b = A[1][o], c = A[2][o];
38
+ if (a - a !== 0 || b - b !== 0 || c - c !== 0) throw bad("point " + i + " has a non-finite " + (a - a !== 0 ? "x" : b - b !== 0 ? "y" : "z") + " coordinate");
39
+ }
40
+ var perm = new Uint32Array(n);
41
+ for (i = 0; i < n; i++) perm[i] = i;
42
+ // Internal nodes sit at depths 0 .. d - 1, where ceil(n / 2^d) <= LEAF
43
+ var d = 0;
44
+ while (Math.ceil(n / Math.pow(2, d)) > LEAF) d++;
45
+ var nodes = Math.max(1, Math.pow(2, d) - 1), axis = new Uint8Array(nodes), split = new Float64Array(nodes);
46
+
47
+ // The point at position kth of perm[lo, hi) on axis ax, smaller ones before it,
48
+ // larger after (Hoare's selection, median-of-three pivot)
49
+ function select(lo, hi, kth, V) {
50
+ var l = lo, r = hi - 1;
51
+ while (r > l) {
52
+ var m = Math.floor((l + r) / 2), va = V[perm[l] * st], vb = V[perm[m] * st], vc = V[perm[r] * st];
53
+ var pv = va < vb ? (vb < vc ? vb : (va < vc ? vc : va)) : (va < vc ? va : (vb < vc ? vc : vb));
54
+ var x = l, y = r;
55
+ do {
56
+ while (V[perm[x] * st] < pv) x++;
57
+ while (V[perm[y] * st] > pv) y--;
58
+ if (x <= y) { var t = perm[x]; perm[x] = perm[y]; perm[y] = t; x++; y--; }
59
+ } while (x <= y);
60
+ if (kth <= y) r = y;
61
+ else if (kth >= x) l = x;
62
+ else return;
63
+ }
64
+ }
65
+
66
+ var stack = [0, 0, n];
67
+ while (stack.length) {
68
+ var hi = stack.pop(), lo = stack.pop(), node = stack.pop();
69
+ if (hi - lo <= LEAF) continue;
70
+ var best = 0, ext = -1;
71
+ for (var ax = 0; ax < 3; ax++) {
72
+ var V = A[ax], mn = Infinity, mx = -Infinity;
73
+ for (var j = lo; j < hi; j++) { var v = V[perm[j] * st]; if (v < mn) mn = v; if (v > mx) mx = v; }
74
+ if (mx - mn > ext) { ext = mx - mn; best = ax; }
75
+ }
76
+ if (ext === 0) {
77
+ // every point here is the same point: sort by number and stop
78
+ perm.subarray(lo, hi).sort();
79
+ axis[node] = UNIFORM;
80
+ continue;
81
+ }
82
+ axis[node] = best;
83
+ var mid = Math.floor((lo + hi) / 2);
84
+ select(lo, hi, mid, A[best]);
85
+ // the children reorder their ranges, so perm[mid] does not keep this value
86
+ split[node] = A[best][perm[mid] * st];
87
+ stack.push(2 * node + 1, lo, mid, 2 * node + 2, mid, hi);
88
+ }
89
+ return { count: n, perm: perm, axis: axis, split: split, leafSize: LEAF, coords: coords };
90
+ }
91
+
92
+ // The search's state, module-wide: a search is synchronous and does not nest
93
+ var T, X, Y, Z, ST, Q = [0, 0, 0], K, MAXD2, SKIP, P, D, M;
94
+
95
+ function worse(i, j) { return D[i] > D[j] || (D[i] === D[j] && P[i] > P[j]); }
96
+ function swap(i, j) { var tp = P[i]; P[i] = P[j]; P[j] = tp; var td = D[i]; D[i] = D[j]; D[j] = td; }
97
+ function down(i, len) {
98
+ for (;;) {
99
+ var l = 2 * i + 1, r = l + 1, w = i;
100
+ if (l < len && worse(l, w)) w = l;
101
+ if (r < len && worse(r, w)) w = r;
102
+ if (w === i) return;
103
+ swap(i, w); i = w;
104
+ }
105
+ }
106
+ function offer(p, d) {
107
+ if (M < K) {
108
+ P[M] = p; D[M] = d;
109
+ for (var i = M++; i > 0;) { var up = Math.floor((i - 1) / 2); if (!worse(i, up)) break; swap(i, up); i = up; }
110
+ } else if (d < D[0] || (d === D[0] && p < P[0])) {
111
+ P[0] = p; D[0] = d; down(0, M);
112
+ }
113
+ }
114
+ function limit() { return M === K && D[0] < MAXD2 ? D[0] : MAXD2; }
115
+
116
+ function search(node, lo, hi) {
117
+ var perm = T.perm;
118
+ if (hi - lo <= LEAF) {
119
+ for (var j = lo; j < hi; j++) {
120
+ var p = perm[j];
121
+ if (p === SKIP) continue;
122
+ var o = p * ST, dx = X[o] - Q[0], dy = Y[o] - Q[1], dz = Z[o] - Q[2];
123
+ var d = dx * dx + dy * dy + dz * dz;
124
+ if (d <= MAXD2) offer(p, d);
125
+ }
126
+ return;
127
+ }
128
+ var a = T.axis[node];
129
+ if (a === UNIFORM) {
130
+ // one distance for all, numbers ascending: once the heap is full, the rest lose
131
+ var u = perm[lo] * ST, ux = X[u] - Q[0], uy = Y[u] - Q[1], uz = Z[u] - Q[2], du = ux * ux + uy * uy + uz * uz;
132
+ if (du > MAXD2) return;
133
+ for (var i = lo; i < hi; i++) {
134
+ var q = perm[i];
135
+ if (q === SKIP) continue;
136
+ if (M === K && (du > D[0] || (du === D[0] && q > P[0]))) return;
137
+ offer(q, du);
138
+ }
139
+ return;
140
+ }
141
+ var mid = Math.floor((lo + hi) / 2);
142
+ var diff = Q[a] - T.split[node];
143
+ // The far side's points are at least |diff| away on this axis
144
+ if (diff < 0) {
145
+ search(2 * node + 1, lo, mid);
146
+ if (diff * diff <= limit()) search(2 * node + 2, mid, hi);
147
+ } else {
148
+ search(2 * node + 2, mid, hi);
149
+ if (diff * diff <= limit()) search(2 * node + 1, lo, mid);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * The k nearest points to (qx, qy, qz), nearest first by (distance², point number).
155
+ * Fills outP / outD2 (squared distances) and returns how many it found (<= k).
156
+ * @param {object} tree - from buildKdTree.
157
+ * @param {number} qx
158
+ * @param {number} qy
159
+ * @param {number} qz
160
+ * @param {number} k - >= 1; outP and outD2 hold at least k.
161
+ * @param {number} maxD2 - squared distance limit (Infinity: none).
162
+ * @param {number} skip - a point number to leave out (-1: none).
163
+ * @param {Uint32Array} outP
164
+ * @param {Float64Array} outD2
165
+ * @returns {number}
166
+ */
167
+ export function kdNearest(tree, qx, qy, qz, k, maxD2, skip, outP, outD2) {
168
+ if (tree.count === 0) return 0;
169
+ T = tree; X = tree.coords.x; Y = tree.coords.y; Z = tree.coords.z; ST = tree.coords.stride;
170
+ Q[0] = qx; Q[1] = qy; Q[2] = qz; K = k; MAXD2 = maxD2; SKIP = skip; P = outP; D = outD2; M = 0;
171
+ search(0, 0, tree.count);
172
+ for (var end = M - 1; end > 0; end--) { swap(0, end); down(0, end); }
173
+ var m = M;
174
+ T = X = Y = Z = P = D = null;
175
+ return m;
176
+ }
@@ -0,0 +1,196 @@
1
+ // src/index/knn.js
2
+ //==========================================================
3
+ // The k nearest points on a PointIndex (grid.js): Chebyshev rings of
4
+ // cells outward from the query's cell, as nearestPoint searches, with
5
+ // the best k kept in a bounded max-heap ordered by (distance², point
6
+ // number), so ties go to the lowest number. Distances are checked in
7
+ // Float64 against the stored coordinates, so results are exact.
8
+ // Also the argument checks and progress ticker the denoise tools
9
+ // share (their own searches run on a k-d tree, kdtree.js, which suits
10
+ // a scan's thousand-fold changes of density better than one grid).
11
+ //==========================================================
12
+
13
+ import { LasError, ERROR_CODES } from "../errors.js";
14
+ import { checkIndex, lookup } from "./grid.js";
15
+ import { checkPoint, checkPositive, cellDist2, decode } from "./query.js";
16
+
17
+ var EPS = 1e-6; // in cells, as query.js
18
+
19
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
20
+
21
+ /**
22
+ * Refuse a k that is not a whole number >= 1.
23
+ * @param {*} v
24
+ * @param {string} name
25
+ */
26
+ export function checkK(v, name) {
27
+ if (!(Number.isInteger(v) && v >= 1)) throw bad(name + " must be a whole number >= 1, not " + String(v));
28
+ }
29
+
30
+ /**
31
+ * Refuse a missing or non-positive maxDistance.
32
+ * @param {*} v
33
+ */
34
+ export function checkMaxDistance(v) {
35
+ if (v === undefined) throw bad("maxDistance is required: a positive distance in the columns' units (e.g. 0.3 m for a handheld scan)");
36
+ checkPositive(v, "maxDistance");
37
+ }
38
+
39
+ /**
40
+ * A ticker that calls onProgress(done, total) about every 1 % of total, and at total.
41
+ * @param {function|undefined} onProgress
42
+ * @param {number} total
43
+ * @returns {function(number)}
44
+ */
45
+ export function progress(onProgress, total) {
46
+ if (onProgress === undefined) return function () {};
47
+ if (typeof onProgress !== "function") throw bad("onProgress must be a function");
48
+ var step = Math.max(1, Math.floor(total / 100)), last = 0;
49
+ return function (done) {
50
+ if (done === total || done - last >= step) { last = done; onProgress(done, total); }
51
+ };
52
+ }
53
+
54
+ /**
55
+ * The k nearest points to (qx, qy, qz), nearest first by (distance², point number).
56
+ * Fills outP / outD2 (squared distances) and returns how many it found (<= k).
57
+ * @param {object} index - PointIndex.
58
+ * @param {number} qx
59
+ * @param {number} qy
60
+ * @param {number} qz - ignored with xy.
61
+ * @param {number} k - >= 1; outP and outD2 hold at least k.
62
+ * @param {number} maxD2 - squared distance limit (Infinity: none).
63
+ * @param {number} skip - a point number to leave out (-1: none).
64
+ * @param {Uint32Array} outP
65
+ * @param {Float64Array} outD2
66
+ * @param {boolean} [xy] - measure in XY only.
67
+ * @returns {number}
68
+ */
69
+ export function knnSearch(index, qx, qy, qz, k, maxD2, skip, outP, outD2, xy) {
70
+ var m = 0;
71
+ if (index.count === 0) return 0;
72
+ var q = [qx, qy, xy ? 0 : qz];
73
+ var n = [index.nx, index.ny, index.nz], cs = index.cellSize, inv = 1 / cs, mn = index.min, axes = xy ? 2 : 3;
74
+ var c = [0, 0, 0], a;
75
+ for (a = 0; a < axes; a++) c[a] = Math.min(n[a] - 1, Math.max(0, Math.floor((q[a] - mn[a]) * inv)));
76
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
77
+ var P = outP, D = outD2, visited = 0, budget = index.keys.length;
78
+ var out = [0, 0, 0];
79
+ for (a = 0; a < axes; a++) {
80
+ var gl = mn[a], gh = mn[a] + n[a] * cs;
81
+ out[a] = Math.max(0, (q[a] < gl ? gl - q[a] : q[a] > gh ? q[a] - gh : 0) - cs * EPS);
82
+ }
83
+
84
+ // Max-heap on (D, P): the root is the worst of the best m
85
+ function worse(i, j) { return D[i] > D[j] || (D[i] === D[j] && P[i] > P[j]); }
86
+ function swap(i, j) { var tp = P[i]; P[i] = P[j]; P[j] = tp; var td = D[i]; D[i] = D[j]; D[j] = td; }
87
+ function down(i, len) {
88
+ for (;;) {
89
+ var l = 2 * i + 1, r = l + 1, w = i;
90
+ if (l < len && worse(l, w)) w = l;
91
+ if (r < len && worse(r, w)) w = r;
92
+ if (w === i) return;
93
+ swap(i, w); i = w;
94
+ }
95
+ }
96
+ function offer(p, d) {
97
+ if (m < k) {
98
+ P[m] = p; D[m] = d;
99
+ for (var i = m++; i > 0;) { var up = Math.floor((i - 1) / 2); if (!worse(i, up)) break; swap(i, up); i = up; }
100
+ } else if (d < D[0] || (d === D[0] && p < P[0])) {
101
+ P[0] = p; D[0] = d; down(0, m);
102
+ }
103
+ }
104
+ function limit() { return m === k ? Math.min(D[0], maxD2) : maxD2; }
105
+ function scanSlot(s) {
106
+ for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
107
+ var p = order[j];
108
+ if (p === skip) continue;
109
+ var o = p * st;
110
+ var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
111
+ var d = dx * dx + dy * dy + dz * dz;
112
+ if (d <= maxD2) offer(p, d);
113
+ }
114
+ }
115
+ function cell(ix, iy, iz) {
116
+ visited++;
117
+ var s = lookup(index, ix, iy, iz);
118
+ if (s < 0 || cellDist2(index, ix, iy, iz, q, xy) > limit()) return;
119
+ scanSlot(s);
120
+ }
121
+ function ring(r) {
122
+ var x0 = Math.max(0, c[0] - r), x1 = Math.min(n[0] - 1, c[0] + r);
123
+ var y0 = Math.max(0, c[1] - r), y1 = Math.min(n[1] - 1, c[1] + r);
124
+ var z0 = xy ? 0 : Math.max(0, c[2] - r), z1 = xy ? n[2] - 1 : Math.min(n[2] - 1, c[2] + r);
125
+ function column(ix, iy) { for (var iz = z0; iz <= z1; iz++) cell(ix, iy, iz); }
126
+ for (var ix = x0; ix <= x1; ix++) {
127
+ if (ix === c[0] - r || ix === c[0] + r) {
128
+ for (var iy = y0; iy <= y1; iy++) column(ix, iy);
129
+ continue;
130
+ }
131
+ if (c[1] - r >= 0) column(ix, c[1] - r);
132
+ if (c[1] + r < n[1]) column(ix, c[1] + r);
133
+ if (xy) continue;
134
+ for (iy = Math.max(y0, c[1] - r + 1); iy <= Math.min(y1, c[1] + r - 1); iy++) {
135
+ if (c[2] - r >= 0) cell(ix, iy, c[2] - r);
136
+ if (c[2] + r < n[2]) cell(ix, iy, c[2] + r);
137
+ }
138
+ }
139
+ }
140
+ function gap(g) { g = Math.max(0, g - cs * EPS); return g * g; }
141
+ // The occupied cells in order of distance, from scratch, for queries rings would make slow
142
+ function byScan() {
143
+ m = 0;
144
+ var keys = index.keys, cand = [];
145
+ for (var s = 0; s < keys.length; s++) {
146
+ if (keys[s] < 0) continue;
147
+ var cc = decode(index, keys[s]), cd = cellDist2(index, cc[0], cc[1], cc[2], q, xy);
148
+ if (cd <= maxD2) cand.push([cd, s]);
149
+ }
150
+ cand.sort(function (u, v) { return u[0] - v[0]; });
151
+ for (var i = 0; i < cand.length; i++) {
152
+ if (cand[i][0] > limit()) break;
153
+ scanSlot(cand[i][1]);
154
+ }
155
+ }
156
+
157
+ for (var r = 0; ; r++) {
158
+ ring(r);
159
+ if (visited > budget) { byScan(); break; }
160
+ // A lower bound on the squared distance of every point not yet visited
161
+ var bound2 = Infinity;
162
+ for (a = 0; a < axes; a++) {
163
+ var rest = 0;
164
+ for (var b = 0; b < axes; b++) if (b !== a) rest += out[b] * out[b];
165
+ if (c[a] - r > 0) bound2 = Math.min(bound2, gap(q[a] - (mn[a] + (c[a] - r) * cs)) + rest);
166
+ if (c[a] + r < n[a] - 1) bound2 = Math.min(bound2, gap(mn[a] + (c[a] + r + 1) * cs - q[a]) + rest);
167
+ }
168
+ if (bound2 === Infinity) break;
169
+ if (m === k && D[0] < bound2) break;
170
+ if (bound2 > maxD2) break;
171
+ }
172
+ // Heap sort: ascending by (D, P)
173
+ for (var end = m - 1; end > 0; end--) { swap(0, end); down(0, end); }
174
+ return m;
175
+ }
176
+
177
+ /**
178
+ * The k nearest points to a point, nearest first (lowest number on a tie).
179
+ * @param {object} index - PointIndex from buildIndex.
180
+ * @param {number[]} point - [x, y, z]; with xy, [x, y] is enough.
181
+ * @param {number} k - how many, a whole number >= 1.
182
+ * @param {{maxDistance?: number, xy?: boolean}} [opts] - maxDistance omitted: unlimited.
183
+ * @returns {{points: Uint32Array, d: Float64Array}} point numbers and their distances
184
+ */
185
+ export function nearestPoints(index, point, k, opts) {
186
+ checkIndex(index, "nearestPoints");
187
+ var xy = !!(opts && opts.xy);
188
+ var q = checkPoint(point, xy, "point");
189
+ checkK(k, "k");
190
+ var maxD = opts ? opts.maxDistance : undefined;
191
+ checkPositive(maxD, "maxDistance", true);
192
+ var kk = Math.min(k, index.count), P = new Uint32Array(kk), D = new Float64Array(kk);
193
+ var m = kk === 0 ? 0 : knnSearch(index, q[0], q[1], q[2], kk, maxD === undefined ? Infinity : maxD * maxD, -1, P, D, xy);
194
+ for (var i = 0; i < m; i++) D[i] = Math.sqrt(D[i]);
195
+ return { points: m === kk ? P : P.slice(0, m), d: m === kk ? D : D.slice(0, m) };
196
+ }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  //==========================================================
7
7
 
8
8
  /** Library version, matches package.json. */
9
- export var VERSION = "0.6.1";
9
+ export var VERSION = "0.7.0";
10
10
 
11
11
  export { LasError, ERROR_CODES } from "./errors.js";
12
12
  export { openLas, LasReader } from "./LasReader.js";
@@ -19,11 +19,15 @@ export { makeKeep, countKept } from "./filter/filters.js";
19
19
  export { dedupe, dedupeSelection } from "./filter/dedupe.js";
20
20
  export { buildIndex } from "./index/grid.js";
21
21
  export { pointsInRadius, nearestPoint, pointsNearRay } from "./index/query.js";
22
+ export { nearestPoints } from "./index/knn.js";
22
23
  export { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection, unionSelections, intersectSelections, subtractSelections } from "./ops/select.js";
23
24
  export { keepPoints, removePoints, splitPoints, mergePoints } from "./ops/apply.js";
24
25
  export { makeTransform, transformPoints } from "./ops/transform.js";
25
26
  export { interleavePositions } from "./ops/interleave.js";
26
27
  export { selectSparse } from "./ops/sparse.js";
28
+ export { selectOutliers } from "./ops/outliers.js";
29
+ export { pointNormals, estimateNoise } from "./ops/normals.js";
30
+ export { denoisePoints } from "./ops/denoise.js";
27
31
  export { MemorySource } from "./io/MemorySource.js";
28
32
  export { BlobSource } from "./io/BlobSource.js";
29
33
  export { NodeFileSource } from "./io/NodeFileSource.js";
@@ -0,0 +1,131 @@
1
+ // src/ops/denoise.js
2
+ //==========================================================
3
+ // Smooth a fuzzy scan: each point moves against its neighbours (the
4
+ // other points among its k nearest within maxDistance).
5
+ // - "bilateral" (the default; edges stay sharp): the point moves
6
+ // along its normal by the weighted mean height of its neighbours
7
+ // above its plane (Fleishman, Drori and Cohen-Or, 2003, on points).
8
+ // A neighbour's weight falls with its distance (σc, half the
9
+ // farthest neighbour's distance) and with its height (σs, twice
10
+ // the noise level), so a face across a crest counts for almost
11
+ // nothing. The normal is fitted twice: plainly, then with those
12
+ // weights, so a crest point follows the face it is on.
13
+ // - "mls": the point moves onto the plane fitted to its neighbours
14
+ // with distance weights. Smooths more; rounds edges slightly.
15
+ // The noise level (sigma) is measured (estimateNoise) unless given.
16
+ // Every point is computed from the old coordinates, so the result
17
+ // does not depend on point order; a further pass rebuilds the index.
18
+ // A point with fewer than 5 neighbours, or on no plane, stays put.
19
+ // The result is as transformPoints returns it: new coordinates of the
20
+ // input's types, every other column shared.
21
+ //==========================================================
22
+
23
+ import { LasError, ERROR_CODES } from "../errors.js";
24
+ import { coordsOf } from "../index/grid.js";
25
+ import { checkK, checkMaxDistance, progress } from "../index/knn.js";
26
+ import { buildKdTree, kdNearest } from "../index/kdtree.js";
27
+ import { fitPlane, estimateNoise } from "./normals.js";
28
+ import { columnsOf, sameType, checkNoSharedMemory } from "./columns.js";
29
+
30
+ var MIN_NEIGHBOURS = 5;
31
+
32
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
33
+
34
+ /**
35
+ * Smooth point columns against their neighbours.
36
+ * @param {object} cols - point columns (x, y, z or a k = 3 positions column).
37
+ * @param {{method?: string, k?: number, maxDistance: number, sigma?: number, iterations?: number,
38
+ * normals?: boolean, inPlace?: boolean, onProgress?: function(number, number)}} opts -
39
+ * method "bilateral" or "mls" (default "bilateral"), k 16, sigma measured, iterations 1.
40
+ * @returns {object} point columns: new x, y, z (or positions); with normals, a normal column (Float32, k = 3)
41
+ */
42
+ export function denoisePoints(cols, opts) {
43
+ opts = opts || {};
44
+ var method = opts.method === undefined ? "bilateral" : opts.method;
45
+ if (method !== "bilateral" && method !== "mls") throw bad("method must be \"bilateral\" or \"mls\", not " + String(method));
46
+ var k = opts.k === undefined ? 16 : opts.k;
47
+ checkK(k, "k");
48
+ checkMaxDistance(opts.maxDistance);
49
+ var iterations = opts.iterations === undefined ? 1 : opts.iterations;
50
+ if (!(Number.isInteger(iterations) && iterations >= 1)) throw bad("iterations must be a whole number >= 1, not " + String(iterations));
51
+ if (opts.sigma !== undefined && !(typeof opts.sigma === "number" && isFinite(opts.sigma) && opts.sigma > 0)) throw bad("sigma must be a positive finite number, not " + String(opts.sigma));
52
+ var maxDistance = opts.maxDistance, maxD2 = maxDistance * maxDistance;
53
+ var co = coordsOf(cols, "denoisePoints"), n = cols.count, positions = co.stride === 3;
54
+ var tick = progress(opts.onProgress, n * iterations);
55
+ if (opts.inPlace && !positions) checkNoSharedMemory(cols, [{ name: "x" }, { name: "y" }, { name: "z" }]);
56
+ var sigma = opts.sigma !== undefined ? opts.sigma : estimateNoise(cols, { k: k, maxDistance: maxDistance });
57
+ var move = sigma > 0;
58
+
59
+ // Coordinate buffers of the input's types: pass i reads src and writes dst
60
+ function buffers() { return positions ? { count: n, positions: sameType(cols.positions, 3 * n) } : { count: n, x: sameType(cols.x, n), y: sameType(cols.y, n), z: sameType(cols.z, n) }; }
61
+ var src = positions ? { count: n, positions: cols.positions } : { count: n, x: cols.x, y: cols.y, z: cols.z };
62
+ var dst = buffers();
63
+ var normal = opts.normals ? new Float32Array(3 * n) : null;
64
+ var P = new Uint32Array(k), D = new Float64Array(k), W = new Float64Array(k), fit = new Float64Array(7);
65
+ var inv2s = 1 / (2 * (2 * sigma) * (2 * sigma));
66
+
67
+ for (var pass = 0; pass < iterations; pass++) {
68
+ var last = pass === iterations - 1;
69
+ var index = move || (last && normal) ? buildKdTree(src, "denoisePoints") : null;
70
+ var sc = index ? index.coords : coordsOf(src, "denoisePoints");
71
+ var X = sc.x, Y = sc.y, Z = sc.z, st = sc.stride;
72
+ var dc = coordsOf(dst, "denoisePoints"), OX = dc.x, OY = dc.y, OZ = dc.z;
73
+ for (var p = 0, o = 0; p < n; p++, o += st) {
74
+ var px = X[o], py = Y[o], pz = Z[o], nx = 0, ny = 0, nz = 0, delta = 0;
75
+ var m = index ? kdNearest(index, px, py, pz, k, maxD2, p, P, D) : 0;
76
+ if (m >= 3 && fitPlane(X, Y, Z, st, p, P, m, null, fit)) {
77
+ nx = fit[0]; ny = fit[1]; nz = fit[2];
78
+ if (move && m >= MIN_NEIGHBOURS && D[m - 1] > 0) {
79
+ var s = 0.5 * Math.sqrt(D[m - 1]), inv2c = 1 / (2 * s * s), j, q, h;
80
+ if (method === "bilateral") {
81
+ for (j = 0; j < m; j++) {
82
+ q = P[j] * st;
83
+ h = nx * (X[q] - px) + ny * (Y[q] - py) + nz * (Z[q] - pz);
84
+ W[j] = Math.exp(-D[j] * inv2c - h * h * inv2s);
85
+ }
86
+ if (fitPlane(X, Y, Z, st, p, P, m, W, fit)) { nx = fit[0]; ny = fit[1]; nz = fit[2]; }
87
+ var num = 0, den = 0;
88
+ for (j = 0; j < m; j++) {
89
+ q = P[j] * st;
90
+ h = nx * (X[q] - px) + ny * (Y[q] - py) + nz * (Z[q] - pz);
91
+ var w = Math.exp(-D[j] * inv2c - h * h * inv2s);
92
+ num += w * h; den += w;
93
+ }
94
+ if (den > 0) delta = num / den;
95
+ } else {
96
+ for (j = 0; j < m; j++) W[j] = Math.exp(-D[j] * inv2c);
97
+ if (fitPlane(X, Y, Z, st, p, P, m, W, fit)) {
98
+ nx = fit[0]; ny = fit[1]; nz = fit[2];
99
+ delta = nx * fit[4] + ny * fit[5] + nz * fit[6];
100
+ }
101
+ }
102
+ }
103
+ }
104
+ var d = p * dc.stride;
105
+ if (delta !== 0) { OX[d] = px + nx * delta; OY[d] = py + ny * delta; OZ[d] = pz + nz * delta; }
106
+ else { OX[d] = px; OY[d] = py; OZ[d] = pz; }
107
+ if (normal && last) { normal[3 * p] = nx; normal[3 * p + 1] = ny; normal[3 * p + 2] = nz; }
108
+ tick(pass * n + p + 1);
109
+ }
110
+ if (!last) {
111
+ // the input is never written: pass 1 needs a second buffer, later passes swap the two
112
+ var old = src;
113
+ src = dst; dst = pass === 0 ? buffers() : old;
114
+ }
115
+ }
116
+
117
+ var moved = dst;
118
+ if (opts.inPlace) {
119
+ if (positions) cols.positions.set(dst.positions);
120
+ else { cols.x.set(dst.x); cols.y.set(dst.y); cols.z.set(dst.z); }
121
+ moved = cols;
122
+ }
123
+ var names = positions ? ["positions"] : ["x", "y", "z"], out = { count: n };
124
+ columnsOf(cols).forEach(function (c) {
125
+ // defineProperty so a column named "__proto__" stays a column
126
+ var v = names.indexOf(c.name) >= 0 ? moved[c.name] : cols[c.name];
127
+ Object.defineProperty(out, c.name, { value: v, enumerable: true, writable: true, configurable: true });
128
+ });
129
+ if (normal) Object.defineProperty(out, "normal", { value: normal, enumerable: true, writable: true, configurable: true });
130
+ return out;
131
+ }
@@ -0,0 +1,177 @@
1
+ // src/ops/normals.js
2
+ //==========================================================
3
+ // Surface normals by a plane fit: the covariance of a point and its
4
+ // neighbours (the other points among its k nearest within
5
+ // maxDistance), whose smallest eigenvector is the normal, from a
6
+ // cyclic Jacobi solver on the 3 × 3 matrix (steady when eigenvalues
7
+ // are equal, as on a line of points). Sums are taken on offsets from
8
+ // the point, so UTM-sized coordinates lose nothing. Normals point up
9
+ // (z > 0; a horizontal one has x > 0, then y > 0). The residual, the
10
+ // RMS distance of the fitted points from the plane, measures the local
11
+ // noise; estimateNoise is its median over a spread sample. The fit is
12
+ // shared with denoisePoints, which weights it.
13
+ //==========================================================
14
+
15
+ import { checkK, checkMaxDistance, progress } from "../index/knn.js";
16
+ import { buildKdTree, kdNearest } from "../index/kdtree.js";
17
+ import { thinSelection } from "./select.js";
18
+
19
+ var NOISE_SAMPLE = 100000;
20
+
21
+ /**
22
+ * Flip a unit vector so z > 0; if z is 0, x > 0; if x is also 0, y > 0.
23
+ * @param {Float64Array|number[]} v - v[0..2], changed in place.
24
+ */
25
+ export function orientNormal(v) {
26
+ if (v[2] < 0 || (v[2] === 0 && (v[0] < 0 || (v[0] === 0 && v[1] < 0)))) { v[0] = -v[0]; v[1] = -v[1]; v[2] = -v[2]; }
27
+ // no negative zeros
28
+ v[0] += 0; v[1] += 0; v[2] += 0;
29
+ }
30
+
31
+ /**
32
+ * The unit eigenvector of the smallest eigenvalue of a symmetric 3 × 3 matrix, oriented
33
+ * (orientNormal), and that eigenvalue. The lowest axis wins a tie.
34
+ * @param {Float64Array} m - [c00, c01, c02, c11, c12, c22].
35
+ * @param {Float64Array} out - out[0..2] the vector, out[3] the eigenvalue; out[4], when out has
36
+ * room, the middle eigenvalue.
37
+ * @returns {boolean} false (out = 0, 0, 0, NaN) when the trace is 0
38
+ */
39
+ export function smallestEigen(m, out) {
40
+ if (!(m[0] + m[3] + m[5] > 0)) { out[0] = 0; out[1] = 0; out[2] = 0; out[3] = NaN; return false; }
41
+ var A = [[m[0], m[1], m[2]], [m[1], m[3], m[4]], [m[2], m[4], m[5]]];
42
+ var V = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
43
+ var PQ = [[0, 1], [0, 2], [1, 2]];
44
+ for (var sweep = 0; sweep < 50; sweep++) {
45
+ var off = A[0][1] * A[0][1] + A[0][2] * A[0][2] + A[1][2] * A[1][2];
46
+ var diag = A[0][0] * A[0][0] + A[1][1] * A[1][1] + A[2][2] * A[2][2];
47
+ if (off <= 1e-30 * diag) break;
48
+ for (var r = 0; r < 3; r++) {
49
+ var p = PQ[r][0], q = PQ[r][1], apq = A[p][q];
50
+ if (apq === 0) continue;
51
+ // Numerical Recipes' rotation: t = tan of the angle that zeroes A[p][q]
52
+ var theta = (A[q][q] - A[p][p]) / (2 * apq);
53
+ var t = Math.abs(theta) > 1e150 ? 1 / (2 * theta) : (theta >= 0 ? 1 : -1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
54
+ var c = 1 / Math.sqrt(t * t + 1), s = t * c;
55
+ for (var i = 0; i < 3; i++) {
56
+ var aip = A[i][p], aiq = A[i][q];
57
+ A[i][p] = c * aip - s * aiq; A[i][q] = s * aip + c * aiq;
58
+ }
59
+ for (i = 0; i < 3; i++) {
60
+ var api = A[p][i], aqi = A[q][i];
61
+ A[p][i] = c * api - s * aqi; A[q][i] = s * api + c * aqi;
62
+ }
63
+ A[p][q] = 0; A[q][p] = 0;
64
+ for (i = 0; i < 3; i++) {
65
+ var vip = V[i][p], viq = V[i][q];
66
+ V[i][p] = c * vip - s * viq; V[i][q] = s * vip + c * viq;
67
+ }
68
+ }
69
+ }
70
+ var b = 0;
71
+ if (A[1][1] < A[b][b]) b = 1;
72
+ if (A[2][2] < A[b][b]) b = 2;
73
+ out[0] = V[0][b]; out[1] = V[1][b]; out[2] = V[2][b];
74
+ var len = Math.sqrt(out[0] * out[0] + out[1] * out[1] + out[2] * out[2]);
75
+ out[0] /= len; out[1] /= len; out[2] /= len;
76
+ orientNormal(out);
77
+ out[3] = A[b][b];
78
+ if (out.length > 4) out[4] = A[(b + 1) % 3][(b + 1) % 3] < A[(b + 2) % 3][(b + 2) % 3] ? A[(b + 1) % 3][(b + 1) % 3] : A[(b + 2) % 3][(b + 2) % 3];
79
+ return true;
80
+ }
81
+
82
+ var M6 = new Float64Array(6), E4 = new Float64Array(5);
83
+
84
+ /**
85
+ * Fit a plane to point p and its m neighbours (weights w, p's own weight 1; null: all 1),
86
+ * on offsets from p.
87
+ * @param {ArrayLike<number>} X
88
+ * @param {ArrayLike<number>} Y
89
+ * @param {ArrayLike<number>} Z
90
+ * @param {number} stride
91
+ * @param {number} p - point number.
92
+ * @param {Uint32Array} nbrs - neighbour point numbers (the first m).
93
+ * @param {number} m
94
+ * @param {Float64Array|null} w - w[j] the weight of nbrs[j].
95
+ * @param {Float64Array} out - [nx, ny, nz, residual, cx, cy, cz]: the unit normal, the RMS distance
96
+ * from the plane, and the weighted centroid's offset from p.
97
+ * @returns {boolean} false (normal 0, residual NaN) when the points do not span a plane: all
98
+ * coincident, or on one line (the middle eigenvalue at most 1e-12 of the trace)
99
+ */
100
+ export function fitPlane(X, Y, Z, stride, p, nbrs, m, w, out) {
101
+ var o = p * stride, px = X[o], py = Y[o], pz = Z[o];
102
+ var W = 1, sx = 0, sy = 0, sz = 0, sxx = 0, sxy = 0, sxz = 0, syy = 0, syz = 0, szz = 0;
103
+ for (var j = 0; j < m; j++) {
104
+ var q = nbrs[j] * stride, wj = w ? w[j] : 1;
105
+ var dx = X[q] - px, dy = Y[q] - py, dz = Z[q] - pz;
106
+ W += wj; sx += wj * dx; sy += wj * dy; sz += wj * dz;
107
+ sxx += wj * dx * dx; sxy += wj * dx * dy; sxz += wj * dx * dz;
108
+ syy += wj * dy * dy; syz += wj * dy * dz; szz += wj * dz * dz;
109
+ }
110
+ var cx = sx / W, cy = sy / W, cz = sz / W;
111
+ M6[0] = sxx - sx * cx; M6[1] = sxy - sx * cy; M6[2] = sxz - sx * cz;
112
+ M6[3] = syy - sy * cy; M6[4] = syz - sy * cz; M6[5] = szz - sz * cz;
113
+ out[4] = cx; out[5] = cy; out[6] = cz;
114
+ if (!smallestEigen(M6, E4) || E4[4] <= 1e-12 * (M6[0] + M6[3] + M6[5])) { out[0] = 0; out[1] = 0; out[2] = 0; out[3] = NaN; return false; }
115
+ out[0] = E4[0]; out[1] = E4[1]; out[2] = E4[2];
116
+ out[3] = Math.sqrt(Math.max(0, E4[3]) / W);
117
+ return true;
118
+ }
119
+
120
+ function options(opts, fn) {
121
+ opts = opts || {};
122
+ var k = opts.k === undefined ? 16 : opts.k;
123
+ checkK(k, "k");
124
+ checkMaxDistance(opts.maxDistance);
125
+ return { k: k, maxD2: opts.maxDistance * opts.maxDistance, maxDistance: opts.maxDistance, onProgress: opts.onProgress, fn: fn };
126
+ }
127
+
128
+ // Fit point p's plane into fit; false with fewer than 3 neighbours or no plane
129
+ function fitAt(index, p, o, P, D, fit) {
130
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, i = p * st;
131
+ var m = kdNearest(index, X[i], Y[i], Z[i], o.k, o.maxD2, p, P, D);
132
+ if (m < 3) { fit[0] = 0; fit[1] = 0; fit[2] = 0; fit[3] = NaN; return false; }
133
+ return fitPlane(X, Y, Z, st, p, P, m, null, fit);
134
+ }
135
+
136
+ /**
137
+ * Each point's surface normal and residual, from a plane fitted to it and its neighbours.
138
+ * @param {object} cols - point columns (x, y, z or a k = 3 positions column).
139
+ * @param {{k?: number, maxDistance: number, onProgress?: function(number, number)}} opts - k 16.
140
+ * @returns {{normal: Float32Array, residual: Float32Array}} normal 3 values a point (0, 0, 0 with
141
+ * fewer than 3 neighbours); residual NaN there
142
+ */
143
+ export function pointNormals(cols, opts) {
144
+ var o = options(opts, "pointNormals");
145
+ var tick = progress(o.onProgress, cols.count);
146
+ var index = buildKdTree(cols, "pointNormals"), n = index.count;
147
+ var normal = new Float32Array(3 * n), residual = new Float32Array(n);
148
+ var P = new Uint32Array(o.k), D = new Float64Array(o.k), fit = new Float64Array(7);
149
+ for (var p = 0; p < n; p++) {
150
+ fitAt(index, p, o, P, D, fit);
151
+ normal[3 * p] = fit[0]; normal[3 * p + 1] = fit[1]; normal[3 * p + 2] = fit[2];
152
+ residual[p] = fit[3];
153
+ tick(p + 1);
154
+ }
155
+ return { normal: normal, residual: residual };
156
+ }
157
+
158
+ /**
159
+ * The cloud's noise level: the median residual (lower median, finite ones) over up to
160
+ * 100,000 points spread evenly through it.
161
+ * @param {object} cols - point columns (x, y, z or a k = 3 positions column).
162
+ * @param {{k?: number, maxDistance: number}} opts - k 16.
163
+ * @returns {number} NaN when no sampled point has a plane
164
+ */
165
+ export function estimateNoise(cols, opts) {
166
+ var o = options(opts, "estimateNoise");
167
+ var index = buildKdTree(cols, "estimateNoise");
168
+ var sample = thinSelection(index.count, NOISE_SAMPLE), res = new Float64Array(sample.length), m = 0;
169
+ var P = new Uint32Array(o.k), D = new Float64Array(o.k), fit = new Float64Array(7);
170
+ for (var i = 0; i < sample.length; i++) {
171
+ // Float32, as pointNormals reports it
172
+ if (fitAt(index, sample[i], o, P, D, fit)) res[m++] = Math.fround(fit[3]);
173
+ }
174
+ if (m === 0) return NaN;
175
+ var s = res.subarray(0, m).sort();
176
+ return s[Math.floor((m - 1) / 2)];
177
+ }
@@ -0,0 +1,50 @@
1
+ // src/ops/outliers.js
2
+ //==========================================================
3
+ // Stray points by PDAL's statistical outlier rule (filters.outlier,
4
+ // method=statistical): each point's mean distance to its k nearest
5
+ // other points; the mean and sample standard deviation (n - 1,
6
+ // accumulated as PDAL does) of those means over the cloud; a point
7
+ // whose mean is above mean + multiplier · sd is an outlier. Returned
8
+ // as a selection for removePoints, as selectSparse.
9
+ //==========================================================
10
+
11
+ import { LasError, ERROR_CODES } from "../errors.js";
12
+ import { checkK, progress } from "../index/knn.js";
13
+ import { buildKdTree, kdNearest } from "../index/kdtree.js";
14
+
15
+ function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
16
+
17
+ /**
18
+ * The points whose mean distance to their k nearest other points is above the cloud's
19
+ * mean of those means plus multiplier standard deviations.
20
+ * @param {object} cols - point columns (x, y, z or a k = 3 positions column).
21
+ * @param {{k?: number, multiplier?: number, onProgress?: function(number, number)}} [opts] - k 8, multiplier 2 (PDAL's defaults).
22
+ * @returns {Uint32Array} ascending point numbers
23
+ */
24
+ export function selectOutliers(cols, opts) {
25
+ opts = opts || {};
26
+ var k = opts.k === undefined ? 8 : opts.k, mult = opts.multiplier === undefined ? 2 : opts.multiplier;
27
+ checkK(k, "k");
28
+ if (!(typeof mult === "number" && isFinite(mult))) throw bad("multiplier must be a finite number, not " + String(mult));
29
+ var tick = progress(opts.onProgress, cols.count);
30
+ var index = buildKdTree(cols, "selectOutliers"), n = index.count;
31
+ if (n <= k) { tick(n); return new Uint32Array(0); }
32
+ var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride;
33
+ var P = new Uint32Array(k), D = new Float64Array(k), means = new Float64Array(n);
34
+ var M1 = 0, M2 = 0;
35
+ for (var i = 0, o = 0; i < n; i++, o += st) {
36
+ kdNearest(index, X[o], Y[o], Z[o], k, Infinity, i, P, D);
37
+ var s = 0;
38
+ for (var j = 0; j < k; j++) s += Math.sqrt(D[j]);
39
+ var mean = s / k;
40
+ means[i] = mean;
41
+ var delta = mean - M1, dn = delta / (i + 1);
42
+ M1 += dn; M2 += delta * dn * i;
43
+ tick(i + 1);
44
+ }
45
+ var threshold = M1 + mult * Math.sqrt(M2 / (n - 1)), hits = 0;
46
+ for (i = 0; i < n; i++) if (means[i] > threshold) hits++;
47
+ var out = new Uint32Array(hits);
48
+ for (i = 0, j = 0; i < n; i++) if (means[i] > threshold) out[j++] = i;
49
+ return out;
50
+ }