Skip to content

Commit

Permalink
feat(LasParser): add parsing of chunks of LAS files
Browse files Browse the repository at this point in the history
  • Loading branch information
Desplandis committed Apr 30, 2024
1 parent a4d20f9 commit eec3197
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 54 deletions.
39 changes: 37 additions & 2 deletions src/Parser/LASLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down
97 changes: 67 additions & 30 deletions src/Parser/LASParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<THREE.BufferGeometry>} 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`.
Expand All @@ -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;
});
},
Expand Down
48 changes: 26 additions & 22 deletions test/unit/lasparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
});

0 comments on commit eec3197

Please sign in to comment.