Skip to content

Commit

Permalink
feat(LASParser): change lasparser package from loaders.gl to copc
Browse files Browse the repository at this point in the history
  • Loading branch information
Desplandis committed Jan 24, 2024
1 parent f89df8c commit aa9d97e
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 66 deletions.
Binary file added examples/libs/laz-perf/laz-perf.wasm
Binary file not shown.
151 changes: 151 additions & 0 deletions src/Parser/LASLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { LazPerf } from 'laz-perf';
import { Las } from 'copc';

/**
* @typedef {Object} Header - Partial LAS header.
* @property {number} header.pointDataRecordFormat - Type of point data
* records contained by the buffer.
* @property {number} header.pointDataRecordLength - Size (in bytes) of the
* point data records. If the specified size is larger than implied by the
* point data record format (see above) the remaining bytes are user-specfic
* "extra bytes". Those are described by an Extra Bytes VLR.
* @property {number[]} header.scale - Scale factors (an array `[xScale,
* yScale, zScale]`) multiplied to the X, Y, Z point record values.
* @property {number[]} header.offset - Offsets (an array `[xOffset,
* xOffset, zOffset]`) added to the scaled X, Y, Z point record values.
*/

/**
* @classdesc
* Loader for LAS and LAZ (LASZip) point clouds. It uses the copc.js library and
* the laz-perf decoder under the hood.
*
* The laz-perf web assembly module is lazily fetched at runtime when a parsing
* request is initiated. Location of laz-perf wasm defaults to the unpkg
* repository.
*/
class LASLoader {
constructor() {
this._wasmPath = 'https://unpkg.com/[email protected]/lib/';
this._wasmPromise = null;
}

_initDecoder() {
if (this._wasmPromise) {
return this._wasmPromise;
}

this._wasmPromise = LazPerf.create({
locateFile: file => `${this._wasmPath}/${file}`,
});

return this._wasmPromise;
}

_parseView(view, options) {
const colorDepth = options.colorDepth ?? 16;

const getPosition = ['X', 'Y', 'Z'].map(view.getter);
const getIntensity = view.getter('Intensity');
const getReturnNumber = view.getter('ReturnNumber');
const getNumberOfReturns = view.getter('NumberOfReturns');
const getClassification = view.getter('Classification');
const getPointSourceID = view.getter('PointSourceId');
const getColor = view.dimensions.Red ?
['Red', 'Green', 'Blue'].map(view.getter) : undefined;

const positions = new Float32Array(view.pointCount * 3);
const intensities = new Uint16Array(view.pointCount);
const returnNumbers = new Uint8Array(view.pointCount);
const numberOfReturns = new Uint8Array(view.pointCount);
const classifications = new Uint8Array(view.pointCount);
const pointSourceIDs = new Uint16Array(view.pointCount);
const colors = getColor ? new Uint8Array(view.pointCount * 4) : undefined;

for (let i = 0; i < view.pointCount; i++) {
// `getPosition` apply scale and offset transform to the X, Y, Z
// values. See https://github.com/connormanning/copc.js/blob/master/src/las/extractor.ts.
const [x, y, z] = getPosition.map(f => f(i));
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;

intensities[i] = getIntensity(i);
returnNumbers[i] = getReturnNumber(i);
numberOfReturns[i] = getNumberOfReturns(i);

if (getColor) {
// Note that we do not infer color depth as it is expensive
// (i.e. traverse the whole view to check if there exists a red,
// green or blue value > 255).
let [r, g, b] = getColor.map(f => f(i));

if (colorDepth === 16) {
r /= 256;
g /= 256;
b /= 256;
}

colors[i * 4] = r;
colors[i * 4 + 1] = g;
colors[i * 4 + 2] = b;
colors[i * 4 + 3] = 255;
}

classifications[i] = getClassification(i);
pointSourceIDs[i] = getPointSourceID(i);
}

return {
position: positions,
intensity: intensities,
returnNumber: returnNumbers,
numberOfReturns,
classification: classifications,
pointSourceID: pointSourceIDs,
color: colors,
};
}

/**
* Set LazPerf decoder path.
* @param {string} path - path to `laz-perf.wasm` folder.
*/
set lazPerf(path) {
this._wasmPath = path;
this._wasmPromise = null;
}

/**
* Parses a LAS or LAZ (LASZip) file. Note that this function is
* **CPU-bound** and shall be parallelised in a dedicated worker.
* @param {ArrayBuffer} data - Binary data to parse.
* @param {Object} [options] - Parsing options.
* @param {8 | 16} [options.colorDepth] - Color depth encoding (in bits).
* Either 8 or 16 bits. Defaults to 8 bits for LAS 1.2 and 16 bits for later
* versions (as mandatory by the specification)
*/
async parseFile(data, options = {}) {
const bytes = new Uint8Array(data);

const pointData = await Las.PointData.decompressFile(bytes, this._initDecoder());

const header = Las.Header.parse(bytes);
const colorDepth = options.colorDepth ??
((header.majorVersion === 1 && header.minorVersion <= 2) ? 8 : 16);

const getter = async (begin, end) => bytes.slice(begin, end);
const vlrs = await Las.Vlr.walk(getter, header);
const ebVlr = Las.Vlr.find(vlrs, 'LASF_Spec', 4);
const eb = ebVlr && Las.ExtraBytes.parse(await Las.Vlr.fetch(getter, ebVlr));

const view = Las.View.create(pointData, header, eb);
const attributes = this._parseView(view, { colorDepth });
return {
header,
attributes,
};
}
}

export default LASLoader;
76 changes: 43 additions & 33 deletions src/Parser/LASParser.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,76 @@
import * as THREE from 'three';
import { LASLoader } from '@loaders.gl/las';
import LASLoader from 'Parser/LASLoader';

// See this document for LAS format specification
// https://www.asprs.org/wp-content/uploads/2010/12/LAS_1_4_r13.pdf
// http://www.cs.unc.edu/~isenburg/lastools/download/laszip.pdf
const lasLoader = new LASLoader();

/**
* The LASParser module provides a [parse]{@link module:LASParser.parse} method
* that takes a LAS file or a LAZ (LASZip) file in, and gives a
* `THREE.BufferGeometry` containing all the necessary attributes to be
/** The LASParser module provides a [parse]{@link
* module:LASParser.parse} method that takes a LAS or LAZ (LASZip) file in, and
* gives a `THREE.BufferGeometry` containing all the necessary attributes to be
* displayed in iTowns. It uses the
* [LASLoader](https://loaders.gl/modules/las/docs/api-reference/las-loader)
* from `loaders.gl`.
* [copc.js](https://github.com/connormanning/copc.js/) library.
*
* @module LASParser
*/
export default {
/*
* Set the laz-perf decoder path.
* @param {string} path - path to `laz-perf.wasm` folder.
*/
enableLazPerf(path) {
if (!path) {
throw new Error('Path to laz-perf is mandatory');
}
lasLoader.lazPerf = path;
},
/**
* Parses a LAS file or a LAZ (LASZip) file and return the corresponding
* `THREE.BufferGeometry`.
*
* @param {ArrayBuffer} data - The file content to parse.
* @param {Object} [options] - Options to give to the parser.
* @param {number|string} [options.in.colorDepth='auto'] - Does the color
* encoding is known ? Is it `8` or `16` bits ? By default it is to
* `'auto'`, but it will be more performant if a specific value is set.
* @param {number} [options.out.skip=1] - Read one point from every `skip`
* points.
* @param {Object} [options]
* @param {Object} [options.in] - Options to give to the parser.
* @param { 8 | 16 } [options.in.colorDepth] - Color depth (in bits).
* Defaults to 8 bits for LAS 1.2 and 16 bits for later versions
* (as mandatory by the specification)
*
* @return {Promise} A promise resolving with a `THREE.BufferGeometry`. The
* header of the file is contained in `userData`.
*/
parse(data, options = {}) {
options.in = options.in || {};
options.out = options.out || {};
return LASLoader.parse(data, {
las: {
colorDepth: options.in.colorDepth || 'auto',
skip: options.out.skip || 1,
},
if (options.out?.skip) {
console.warn("Warning: options 'skip' not supported anymore");
}
return lasLoader.parseFile(data, {
colorDepth: options.in?.colorDepth,
}).then((parsedData) => {
const geometry = new THREE.BufferGeometry();
geometry.userData = parsedData.loaderData;
geometry.userData.vertexCount = parsedData.header.vertexCount;
geometry.userData.boundingBox = parsedData.header.boundingBox;
const attributes = parsedData.attributes;
geometry.userData = parsedData.header;

const positionBuffer = new THREE.BufferAttribute(parsedData.attributes.POSITION.value, 3);
const positionBuffer = new THREE.BufferAttribute(attributes.position, 3);
geometry.setAttribute('position', positionBuffer);

const intensityBuffer = new THREE.BufferAttribute(parsedData.attributes.intensity.value, 1, true);
const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1, true);
geometry.setAttribute('intensity', intensityBuffer);

const classificationBuffer = new THREE.BufferAttribute(parsedData.attributes.classification.value, 1, true);
geometry.setAttribute('classification', classificationBuffer);
const returnNumber = new THREE.BufferAttribute(attributes.returnNumber, 1);
geometry.setAttribute('returnNumber', returnNumber);

const numberOfReturns = new THREE.BufferAttribute(attributes.numberOfReturns, 1);
geometry.setAttribute('numberOfReturns', numberOfReturns);

if (parsedData.attributes.COLOR_0) {
const colorBuffer = new THREE.BufferAttribute(parsedData.attributes.COLOR_0.value, 4, true);
const classBuffer = new THREE.BufferAttribute(attributes.classification, 1, true);
geometry.setAttribute('classification', classBuffer);

const pointSourceID = new THREE.BufferAttribute(attributes.pointSourceID, 1);
geometry.setAttribute('pointSourceID', pointSourceID);

if (attributes.color) {
const colorBuffer = new THREE.BufferAttribute(attributes.color, 4, true);
geometry.setAttribute('color', colorBuffer);
}

geometry.computeBoundingBox();

return geometry;
});
},
Expand Down
67 changes: 44 additions & 23 deletions test/unit/lasparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,64 @@ import { compareWithEpsilon } from './utils';
describe('LASParser', function () {
let lasData;
let lazData;
let lazDataV1_4;

before(async () => {
const networkOptions = process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {};
const baseurl = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/';
lazData = await Fetcher.arrayBuffer(`${baseurl}data_test.laz`, networkOptions);
lasData = await Fetcher.arrayBuffer(`${baseurl}data_test.las`, networkOptions);
lazDataV1_4 = await Fetcher.arrayBuffer(`${baseurl}ellipsoid-1.4.laz`, networkOptions);
LASParser.enableLazPerf('./examples/libs/laz-perf');
});

it('parses a las file to a THREE.BufferGeometry', async () => {
const bufferGeometry = await LASParser.parse(lasData);
assert.strictEqual(bufferGeometry.userData.vertexCount, 106);
assert.strictEqual(bufferGeometry.attributes.position.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.attributes.intensity.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.attributes.classification.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.userData.pointCount, 106);
assert.strictEqual(bufferGeometry.attributes.position.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.intensity.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.classification.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.color, undefined);

assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x, bufferGeometry.userData.mins[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y, bufferGeometry.userData.mins[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z, bufferGeometry.userData.mins[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x, bufferGeometry.userData.maxs[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y, bufferGeometry.userData.maxs[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z, bufferGeometry.userData.maxs[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x, bufferGeometry.userData.min[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y, bufferGeometry.userData.min[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z, bufferGeometry.userData.min[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x, bufferGeometry.userData.max[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y, bufferGeometry.userData.max[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z, bufferGeometry.userData.max[2], 0.1));
});

it('parses a laz file to a THREE.BufferGeometry', async () => {
const bufferGeometry = await LASParser.parse(lazData);
assert.strictEqual(bufferGeometry.userData.vertexCount, 57084);
assert.strictEqual(bufferGeometry.attributes.position.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.attributes.intensity.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.attributes.classification.count, bufferGeometry.userData.vertexCount);
assert.strictEqual(bufferGeometry.attributes.color, undefined);
describe('parses a laz file to a THREE.BufferGeometry', function () {
it('laz v1.2', async () => {
const bufferGeometry = await LASParser.parse(lazData);
assert.strictEqual(bufferGeometry.userData.pointCount, 57084);
assert.strictEqual(bufferGeometry.attributes.position.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.intensity.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.classification.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.color, undefined);

assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x, bufferGeometry.userData.min[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y, bufferGeometry.userData.min[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z, bufferGeometry.userData.min[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x, bufferGeometry.userData.max[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y, bufferGeometry.userData.max[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z, bufferGeometry.userData.max[2], 0.1));
});

it('laz v1.4', async () => {
const bufferGeometry = await LASParser.parse(lazDataV1_4);
assert.strictEqual(bufferGeometry.userData.pointCount, 100000);
assert.strictEqual(bufferGeometry.attributes.position.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.intensity.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.classification.count, bufferGeometry.userData.pointCount);
assert.strictEqual(bufferGeometry.attributes.color.count, bufferGeometry.userData.pointCount);

assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x, bufferGeometry.userData.mins[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y, bufferGeometry.userData.mins[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z, bufferGeometry.userData.mins[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x, bufferGeometry.userData.maxs[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y, bufferGeometry.userData.maxs[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z, bufferGeometry.userData.maxs[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x, bufferGeometry.userData.min[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y, bufferGeometry.userData.min[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z, bufferGeometry.userData.min[2], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x, bufferGeometry.userData.max[0], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y, bufferGeometry.userData.max[1], 0.1));
assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z, bufferGeometry.userData.max[2], 0.1));
});
});
});
Loading

0 comments on commit aa9d97e

Please sign in to comment.