UNPKG

@loaders.gl/pcd

Version:

Framework-independent loader for the PCD format

331 lines (330 loc) 12.7 kB
// PCD Loader, adapted from THREE.js (MIT license) // Description: A loader for PCD ascii and binary files. // Limitations: Compressed binary files are not supported. // // Attributions per original THREE.js source file: // @author Filipe Caixeta / http://filipecaixeta.com.br // @author Mugen87 / https://github.com/Mugen87 import { getMeshBoundingBox } from '@loaders.gl/schema'; import { decompressLZF } from "./decompress-lzf.js"; import { getPCDSchema } from "./get-pcd-schema.js"; const LITTLE_ENDIAN = true; /** * * @param data * @returns */ export default function parsePCD(data) { // parse header (always ascii format) const textData = new TextDecoder().decode(data); const pcdHeader = parsePCDHeader(textData); let attributes = {}; // parse data switch (pcdHeader.data) { case 'ascii': attributes = parsePCDASCII(pcdHeader, textData); break; case 'binary': attributes = parsePCDBinary(pcdHeader, data); break; case 'binary_compressed': attributes = parsePCDBinaryCompressed(pcdHeader, data); break; default: throw new Error(`PCD: ${pcdHeader.data} files are not supported`); } attributes = getMeshAttributes(attributes); const header = getMeshHeader(pcdHeader, attributes); const metadata = Object.fromEntries([ ['mode', '0'], ['boundingBox', JSON.stringify(header.boundingBox)] ]); const schema = getPCDSchema(pcdHeader, metadata); return { loader: 'pcd', loaderData: pcdHeader, header, schema, mode: 0, // POINTS topology: 'point-list', attributes }; } // Create a header that contains common data for PointCloud category loaders function getMeshHeader(pcdHeader, attributes) { if (typeof pcdHeader.width === 'number' && typeof pcdHeader.height === 'number') { const pointCount = pcdHeader.width * pcdHeader.height; // Supports "organized" point sets return { vertexCount: pointCount, boundingBox: getMeshBoundingBox(attributes) }; } return { vertexCount: pcdHeader.vertexCount, boundingBox: pcdHeader.boundingBox }; } /** * @param attributes * @returns Normalized attributes */ function getMeshAttributes(attributes) { const normalizedAttributes = { POSITION: { // Binary PCD is only 32 bit value: new Float32Array(attributes.position), size: 3 } }; if (attributes.normal && attributes.normal.length > 0) { normalizedAttributes.NORMAL = { value: new Float32Array(attributes.normal), size: 3 }; } if (attributes.color && attributes.color.length > 0) { // TODO - RGBA normalizedAttributes.COLOR_0 = { value: new Uint8Array(attributes.color), size: 3 }; } if (attributes.intensity && attributes.intensity.length > 0) { // TODO - RGBA normalizedAttributes.COLOR_0 = { value: new Uint8Array(attributes.color), size: 3 }; } if (attributes.label && attributes.label.length > 0) { // TODO - RGBA normalizedAttributes.COLOR_0 = { value: new Uint8Array(attributes.label), size: 3 }; } return normalizedAttributes; } /** * Incoming data parsing * @param data * @returns Header */ /* eslint-disable complexity, max-statements */ function parsePCDHeader(data) { const result1 = data.search(/[\r\n]DATA\s(\S*)\s/i); const result2 = /[\r\n]DATA\s(\S*)\s/i.exec(data.substr(result1 - 1)); const pcdHeader = {}; pcdHeader.data = result2 && result2[1]; if (result2 !== null) { pcdHeader.headerLen = (result2 && result2[0].length) + result1; } pcdHeader.str = data.substr(0, pcdHeader.headerLen); // remove comments pcdHeader.str = pcdHeader.str.replace(/\#.*/gi, ''); // parse pcdHeader.version = /VERSION (.*)/i.exec(pcdHeader.str); pcdHeader.fields = /FIELDS (.*)/i.exec(pcdHeader.str); pcdHeader.size = /SIZE (.*)/i.exec(pcdHeader.str); pcdHeader.type = /TYPE (.*)/i.exec(pcdHeader.str); pcdHeader.count = /COUNT (.*)/i.exec(pcdHeader.str); pcdHeader.width = /WIDTH (.*)/i.exec(pcdHeader.str); pcdHeader.height = /HEIGHT (.*)/i.exec(pcdHeader.str); pcdHeader.viewpoint = /VIEWPOINT (.*)/i.exec(pcdHeader.str); pcdHeader.points = /POINTS (.*)/i.exec(pcdHeader.str); // evaluate if (pcdHeader.version !== null) { pcdHeader.version = parseFloat(pcdHeader.version[1]); } if (pcdHeader.fields !== null) { pcdHeader.fields = pcdHeader.fields[1].split(' '); } if (pcdHeader.type !== null) { pcdHeader.type = pcdHeader.type[1].split(' '); } if (pcdHeader.width !== null) { pcdHeader.width = parseInt(pcdHeader.width[1], 10); } if (pcdHeader.height !== null) { pcdHeader.height = parseInt(pcdHeader.height[1], 10); } if (pcdHeader.viewpoint !== null) { pcdHeader.viewpoint = pcdHeader.viewpoint[1]; } if (pcdHeader.points !== null) { pcdHeader.points = parseInt(pcdHeader.points[1], 10); } if (pcdHeader.points === null && typeof pcdHeader.width === 'number' && typeof pcdHeader.height === 'number') { pcdHeader.points = pcdHeader.width * pcdHeader.height; } if (pcdHeader.size !== null) { pcdHeader.size = pcdHeader.size[1].split(' ').map((x) => parseInt(x, 10)); } if (pcdHeader.count !== null) { pcdHeader.count = pcdHeader.count[1].split(' ').map((x) => parseInt(x, 10)); } else { pcdHeader.count = []; if (pcdHeader.fields !== null) { for (let i = 0; i < pcdHeader.fields.length; i++) { pcdHeader.count.push(1); } } } pcdHeader.offset = {}; let sizeSum = 0; if (pcdHeader.fields !== null && pcdHeader.size !== null) { for (let i = 0; i < pcdHeader.fields.length; i++) { if (pcdHeader.data === 'ascii') { pcdHeader.offset[pcdHeader.fields[i]] = i; } else { pcdHeader.offset[pcdHeader.fields[i]] = sizeSum; sizeSum += pcdHeader.size[i]; } } } // for binary only pcdHeader.rowSize = sizeSum; return pcdHeader; } /** * @param pcdHeader * @param textData * @returns [attributes] */ // eslint-enable-next-line complexity, max-statements function parsePCDASCII(pcdHeader, textData) { const position = []; const normal = []; const color = []; const intensity = []; const label = []; const offset = pcdHeader.offset; const pcdData = textData.substr(pcdHeader.headerLen); const lines = pcdData.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i] !== '') { const line = lines[i].split(' '); if (offset.x !== undefined) { position.push(parseFloat(line[offset.x])); position.push(parseFloat(line[offset.y])); position.push(parseFloat(line[offset.z])); } if (offset.rgb !== undefined) { const floatValue = parseFloat(line[offset.rgb]); const binaryColor = new Float32Array([floatValue]); const dataview = new DataView(binaryColor.buffer, 0); color.push(dataview.getUint8(0)); color.push(dataview.getUint8(1)); color.push(dataview.getUint8(2)); // TODO - handle alpha channel / RGBA? } if (offset.normal_x !== undefined) { normal.push(parseFloat(line[offset.normal_x])); normal.push(parseFloat(line[offset.normal_y])); normal.push(parseFloat(line[offset.normal_z])); } if (offset.intensity !== undefined) { intensity.push(parseFloat(line[offset.intensity])); } if (offset.label !== undefined) { label.push(parseInt(line[offset.label])); } } } return { position, normal, color }; } /** * @param pcdHeader * @param data * @returns [attributes] */ function parsePCDBinary(pcdHeader, data) { const position = []; const normal = []; const color = []; const intensity = []; const label = []; const dataview = new DataView(data, pcdHeader.headerLen); const offset = pcdHeader.offset; for (let i = 0, row = 0; i < pcdHeader.points; i++, row += pcdHeader.rowSize) { if (offset.x !== undefined) { position.push(dataview.getFloat32(row + offset.x, LITTLE_ENDIAN)); position.push(dataview.getFloat32(row + offset.y, LITTLE_ENDIAN)); position.push(dataview.getFloat32(row + offset.z, LITTLE_ENDIAN)); } if (offset.rgb !== undefined) { color.push(dataview.getUint8(row + offset.rgb + 0)); color.push(dataview.getUint8(row + offset.rgb + 1)); color.push(dataview.getUint8(row + offset.rgb + 2)); } if (offset.normal_x !== undefined) { normal.push(dataview.getFloat32(row + offset.normal_x, LITTLE_ENDIAN)); normal.push(dataview.getFloat32(row + offset.normal_y, LITTLE_ENDIAN)); normal.push(dataview.getFloat32(row + offset.normal_z, LITTLE_ENDIAN)); } if (offset.intensity !== undefined) { intensity.push(dataview.getFloat32(row + offset.intensity, LITTLE_ENDIAN)); } if (offset.label !== undefined) { label.push(dataview.getInt32(row + offset.label, LITTLE_ENDIAN)); } } return { position, normal, color, intensity, label }; } /** Parse compressed PCD data in in binary_compressed form ( https://pointclouds.org/documentation/tutorials/pcd_file_format.html) * from https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/PCDLoader.js * @license MIT (http://opensource.org/licenses/MIT) * @param pcdHeader * @param data * @returns [attributes] */ // eslint-enable-next-line complexity, max-statements function parsePCDBinaryCompressed(pcdHeader, data) { const position = []; const normal = []; const color = []; const intensity = []; const label = []; const sizes = new Uint32Array(data.slice(pcdHeader.headerLen, pcdHeader.headerLen + 8)); const compressedSize = sizes[0]; const decompressedSize = sizes[1]; const decompressed = decompressLZF(new Uint8Array(data, pcdHeader.headerLen + 8, compressedSize), decompressedSize); const dataview = new DataView(decompressed.buffer); const offset = pcdHeader.offset; for (let i = 0; i < pcdHeader.points; i++) { if (offset.x !== undefined) { position.push(dataview.getFloat32(pcdHeader.points * offset.x + pcdHeader.size[0] * i, LITTLE_ENDIAN)); position.push(dataview.getFloat32(pcdHeader.points * offset.y + pcdHeader.size[1] * i, LITTLE_ENDIAN)); position.push(dataview.getFloat32(pcdHeader.points * offset.z + pcdHeader.size[2] * i, LITTLE_ENDIAN)); } if (offset.rgb !== undefined) { color.push(dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 0) / 255.0); color.push(dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 1) / 255.0); color.push(dataview.getUint8(pcdHeader.points * offset.rgb + pcdHeader.size[3] * i + 2) / 255.0); } if (offset.normal_x !== undefined) { normal.push(dataview.getFloat32(pcdHeader.points * offset.normal_x + pcdHeader.size[4] * i, LITTLE_ENDIAN)); normal.push(dataview.getFloat32(pcdHeader.points * offset.normal_y + pcdHeader.size[5] * i, LITTLE_ENDIAN)); normal.push(dataview.getFloat32(pcdHeader.points * offset.normal_z + pcdHeader.size[6] * i, LITTLE_ENDIAN)); } if (offset.intensity !== undefined) { const intensityIndex = pcdHeader.fields.indexOf('intensity'); intensity.push(dataview.getFloat32(pcdHeader.points * offset.intensity + pcdHeader.size[intensityIndex] * i, LITTLE_ENDIAN)); } if (offset.label !== undefined) { const labelIndex = pcdHeader.fields.indexOf('label'); label.push(dataview.getInt32(pcdHeader.points * offset.label + pcdHeader.size[labelIndex] * i, LITTLE_ENDIAN)); } } return { position, normal, color, intensity, label }; }