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 +33 -0
- package/README.md +44 -2
- package/package.json +1 -1
- package/src/filter/dedupe.js +50 -15
- package/src/index/grid.js +11 -2
- package/src/index/kdtree.js +176 -0
- package/src/index/knn.js +196 -0
- package/src/index.js +5 -1
- package/src/ops/denoise.js +131 -0
- package/src/ops/normals.js +177 -0
- package/src/ops/outliers.js +50 -0
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
|
|
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.
|
|
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": {
|
package/src/filter/dedupe.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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 =
|
|
64
|
-
while (k
|
|
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)
|
|
74
|
-
var own = key(cx, cy, cz);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
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 =
|
|
91
|
-
while (k
|
|
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
|
-
|
|
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
|
+
}
|
package/src/index/knn.js
ADDED
|
@@ -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.
|
|
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
|
+
}
|