From eec3197d69c435eef25d27c5eadc3129fabae2dc Mon Sep 17 00:00:00 2001 From: Bouillaguet Quentin Date: Mon, 12 Feb 2024 13:56:42 +0100 Subject: [PATCH] feat(LasParser): add parsing of chunks of LAS files --- src/Parser/LASLoader.js | 39 ++++++++++++++++- src/Parser/LASParser.js | 97 ++++++++++++++++++++++++++++------------- test/unit/lasparser.js | 48 ++++++++++---------- 3 files changed, 130 insertions(+), 54 deletions(-) diff --git a/src/Parser/LASLoader.js b/src/Parser/LASLoader.js index 44bf8ec92d..ce0ca491df 100644 --- a/src/Parser/LASLoader.js +++ b/src/Parser/LASLoader.js @@ -15,6 +15,10 @@ import { Las } from 'copc'; * xOffset, zOffset]`) added to the scaled X, Y, Z point record values. */ +function defaultColorEncoding(header) { + return (header.majorVersion === 1 && header.minorVersion <= 2) ? 8 : 16; +} + /** * @classdesc * Loader for LAS and LAZ (LASZip) point clouds. It uses the copc.js library and @@ -131,6 +135,38 @@ class LASLoader { this._wasmPromise = null; } + /** + * Parses a LAS or LAZ (LASZip) chunk. Note that this function is + * **CPU-bound** and shall be parallelised in a dedicated worker. + * @param {Uint8Array} data - File chunk data. + * @param {Object} options - Parsing options. + * @param {Header} options.header - Partial LAS header. + * @param {number} options.pointCount - Number of points encoded in this + * data chunk. + * @param {Las.ExtraBytes[]} [options.eb] - Extra bytes LAS VLRs + * headers. + * @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 parseChunk(data, options) { + const { header, eb, pointCount } = options; + const { pointDataRecordFormat, pointDataRecordLength } = header; + + const colorDepth = options.colorDepth ?? defaultColorEncoding(header); + + const bytes = new Uint8Array(data); + const pointData = await Las.PointData.decompressChunk(bytes, { + pointCount, + pointDataRecordFormat, + pointDataRecordLength, + }, this._initDecoder()); + + const view = Las.View.create(pointData, header, eb); + const attributes = this._parseView(view, { colorDepth }); + return { attributes }; + } + /** * Parses a LAS or LAZ (LASZip) file. Note that this function is * **CPU-bound** and shall be parallelised in a dedicated worker. @@ -146,8 +182,7 @@ class LASLoader { 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 colorDepth = options.colorDepth ?? defaultColorEncoding(header); const getter = async (begin, end) => bytes.slice(begin, end); const vlrs = await Las.Vlr.walk(getter, header); diff --git a/src/Parser/LASParser.js b/src/Parser/LASParser.js index 12a6510e86..9d1f7f630b 100644 --- a/src/Parser/LASParser.js +++ b/src/Parser/LASParser.js @@ -3,6 +3,39 @@ import LASLoader from 'Parser/LASLoader'; const lasLoader = new LASLoader(); +function buildBufferGeometry(attributes) { + const geometry = new THREE.BufferGeometry(); + + const positionBuffer = new THREE.BufferAttribute(attributes.position, 3); + geometry.setAttribute('position', positionBuffer); + + const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1); + geometry.setAttribute('intensity', intensityBuffer); + + const returnNumber = new THREE.BufferAttribute(attributes.returnNumber, 1); + geometry.setAttribute('returnNumber', returnNumber); + + const numberOfReturns = new THREE.BufferAttribute(attributes.numberOfReturns, 1); + geometry.setAttribute('numberOfReturns', numberOfReturns); + + const classBuffer = new THREE.BufferAttribute(attributes.classification, 1); + 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); + } + const scanAngle = new THREE.BufferAttribute(attributes.scanAngle, 1); + geometry.setAttribute('scanAngle', scanAngle); + + geometry.userData.origin = new THREE.Vector3().fromArray(attributes.origin); + + return geometry; +} + /** 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 @@ -22,6 +55,38 @@ export default { } lasLoader.lazPerf = path; }, + + + /** + * Parses a chunk of a LAS or LAZ (LASZip) and returns the corresponding + * `THREE.BufferGeometry`. + * + * @param {ArrayBuffer} data - The file content to parse. + * @param {Object} options + * @param {Object} options.in - Options to give to the parser. + * @param {number} options.in.pointCount - Number of points encoded in this + * data chunk. + * @param {Object} options.in.header - Partial LAS file header. + * @param {number} options.in.header.pointDataRecordFormat - Type of Point + * Data Record contained in the LAS file. + * @param {number} options.in.header.pointDataRecordLength - Size (in bytes) + * of the Point Data Record. + * @param {Object} [options.eb] - Extra bytes LAS VLRs headers. + * @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`. + */ + parseChunk(data, options = {}) { + return lasLoader.parseChunk(data, options.in).then((parsedData) => { + const geometry = buildBufferGeometry(parsedData.attributes); + geometry.computeBoundingBox(); + return geometry; + }); + }, + /** * Parses a LAS file or a LAZ (LASZip) file and return the corresponding * `THREE.BufferGeometry`. @@ -43,37 +108,9 @@ export default { return lasLoader.parseFile(data, { colorDepth: options.in?.colorDepth, }).then((parsedData) => { - const geometry = new THREE.BufferGeometry(); - const attributes = parsedData.attributes; - geometry.userData = parsedData.header; - - const positionBuffer = new THREE.BufferAttribute(attributes.position, 3); - geometry.setAttribute('position', positionBuffer); - - const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1); - geometry.setAttribute('intensity', intensityBuffer); - - const returnNumber = new THREE.BufferAttribute(attributes.returnNumber, 1); - geometry.setAttribute('returnNumber', returnNumber); - - const numberOfReturns = new THREE.BufferAttribute(attributes.numberOfReturns, 1); - geometry.setAttribute('numberOfReturns', numberOfReturns); - - const classBuffer = new THREE.BufferAttribute(attributes.classification, 1); - 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); - } - const scanAngle = new THREE.BufferAttribute(attributes.scanAngle, 1); - geometry.setAttribute('scanAngle', scanAngle); - + const geometry = buildBufferGeometry(parsedData.attributes); + geometry.userData.header = parsedData.header; geometry.computeBoundingBox(); - geometry.userData.origin = new THREE.Vector3().fromArray(attributes.origin); return geometry; }); }, diff --git a/test/unit/lasparser.js b/test/unit/lasparser.js index 0f5d5687a5..3cb111f42a 100644 --- a/test/unit/lasparser.js +++ b/test/unit/lasparser.js @@ -26,35 +26,39 @@ describe('LASParser', function () { it('parses a las file to a THREE.BufferGeometry', async function () { if (!lasData) { this.skip(); } const bufferGeometry = await LASParser.parse(lasData); - 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); + const header = bufferGeometry.userData.header; + const origin = bufferGeometry.userData.origin; + assert.strictEqual(header.pointCount, 106); + assert.strictEqual(bufferGeometry.attributes.position.count, header.pointCount); + assert.strictEqual(bufferGeometry.attributes.intensity.count, header.pointCount); + assert.strictEqual(bufferGeometry.attributes.classification.count, header.pointCount); assert.strictEqual(bufferGeometry.attributes.color, undefined); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x + bufferGeometry.userData.origin.x, bufferGeometry.userData.min[0], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + bufferGeometry.userData.origin.y, bufferGeometry.userData.min[1], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + bufferGeometry.userData.origin.z, bufferGeometry.userData.min[2], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + bufferGeometry.userData.origin.x, bufferGeometry.userData.max[0], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + bufferGeometry.userData.origin.y, bufferGeometry.userData.max[1], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + bufferGeometry.userData.origin.z, bufferGeometry.userData.max[2], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x + origin.x, header.min[0], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + origin.y, header.min[1], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + origin.z, header.min[2], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + origin.x, header.max[0], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + origin.y, header.max[1], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + origin.z, header.max[2], epsilon)); }); it('parses a laz file to a THREE.BufferGeometry', async function () { if (!lazV14Data) { this.skip(); } const bufferGeometry = await LASParser.parse(lazV14Data); - 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.origin.x, bufferGeometry.userData.min[0], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + bufferGeometry.userData.origin.y, bufferGeometry.userData.min[1], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + bufferGeometry.userData.origin.z, bufferGeometry.userData.min[2], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + bufferGeometry.userData.origin.x, bufferGeometry.userData.max[0], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + bufferGeometry.userData.origin.y, bufferGeometry.userData.max[1], epsilon)); - assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + bufferGeometry.userData.origin.z, bufferGeometry.userData.max[2], epsilon)); + const header = bufferGeometry.userData.header; + const origin = bufferGeometry.userData.origin; + assert.strictEqual(header.pointCount, 100000); + assert.strictEqual(bufferGeometry.attributes.position.count, header.pointCount); + assert.strictEqual(bufferGeometry.attributes.intensity.count, header.pointCount); + assert.strictEqual(bufferGeometry.attributes.classification.count, header.pointCount); + assert.strictEqual(bufferGeometry.attributes.color.count, header.pointCount); + + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x + origin.x, header.min[0], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + origin.y, header.min[1], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + origin.z, header.min[2], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.x + origin.x, header.max[0], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.y + origin.y, header.max[1], epsilon)); + assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.max.z + origin.z, header.max[2], epsilon)); }); }); });