一、简介
点云数据是一种用于表示三维空间中物体表面的离散点集合。每个点通常包含其在三维空间中的位置(x、y、z坐标)以及可能的其他属性(如颜色、强度、法线等)。点云数据广泛应用于各个领域,包括计算机视觉、机器人学、建筑、考古、地理信息系统(GIS)和自动驾驶等。
1.1、点云的格式
-
LAS(LASer File Format)
-
描述:LAS 是一种广泛用于 LiDAR 数据的格式,包含了点的空间坐标、强度、分类、时间戳、颜色等信息。LAS 文件是二进制格式,体积较小,读取速度快。
-
应用:地理信息系统(GIS)、环境建模、城市规划。
-
-
LAZ
-
描述:LAZ 是 LAS 格式的压缩版本,使用 LZMA 压缩算法,显著减少了文件大小。适用于需要存储和传输大量 LiDAR 数据的场景。
-
应用:与 LAS 相同,但更加适合数据存储和传输。
-
-
PLY
-
描述:与 LAS 相同,但更加适合数据存储和传输。
-
应用:计算机图形学、3D 打印、3D 扫描。
-
-
PCD
-
描述:PCD 是 Point Cloud Library(PCL) 的专用格式,支持多种点云数据类型,包括有序和无序点云。PCD 文件支持存储多种数据属性,如位置、颜色、法线等。
-
应用:机器人视觉、3D 计算机视觉、自动驾驶。
-
-
E57
-
描述:E57 是用于存储三维测量数据的格式,包括点云、图像和其他元数据。支持高精度和大规模数据存储,通常用于工业和建筑领域。
-
应用:建筑信息模型(BIM)、工业检测、文化遗产保护。
-
-
XYZ
-
描述:XYZ 是一种简单的文本格式,通常每行包含一个点的空间坐标(X、Y、Z)。有时也会包含其他属性,如强度或颜色。
-
应用:数据交换、简单的点云数据存储和处理。
-
-
OBJ
-
描述:OBJ 文件格式主要用于表示三维几何图形,包括顶点、法线、纹理坐标和面信息。虽然主要用于存储三维模型,但也可以存储点云数据。
-
应用:3D 模型交换、计算机图形学、3D 打印。
-
-
PTS
-
描述:PTS 是一种常见的点云文本格式,每行通常包含一个点的坐标和其他属性(如颜色、强度)。PTS 文件易于阅读和编辑,但体积较大。
-
应用:数据交换、点云数据存储和处理。
-
-
PTX
-
描述:PTX 是一种标准的点云交换格式,支持点的坐标和强度信息。通常用于激光扫描仪导出数据,包含扫描位置和其他元数据。
-
应用:地形建模、建筑信息模型(BIM)、工业检测。
-
-
ASPRS LAS
-
描述:类似于标准的 LAS 格式,但由美国摄影测量与遥感学会(ASPRS)制定,具有更严格的标准和更多的元数据支持。
-
应用:专业测绘、GIS 应用、环境监测
-
二、点云的展示思路
最常见的就是点云转成3Dtiles格式再进行加载,所以优先介绍下3dtiles格式。
3D Tiles 是一种用于流式传输大型三维地理空间数据集的开放规范,主要由 Cesium 创造。这种数据格式的创建背后有几个关键原因:
-
高效的数据管理和流式传输:传统的3D数据格式并不适合大规模地理空间数据集的高效流式传输和渲染。3D Tiles 旨在解决这个问题,通过分层级和按需加载的方式,使得大型数据集可以在各种设备上高效地渲染和导航,无论数据集有多大。
-
多样化的数据类型支持:3D Tiles 支持多种不同的数据类型,包括点云、3D建模、影像等,这使其成为一种多功能的数据格式,适用于各种不同的地理空间数据应用场景。
-
与现有技术的兼容性:3D Tiles 旨在与现有的Web技术和标准(如 WebGL)兼容,这样可以使得基于Web的地理空间应用更容易实现,并可以利用现有的技术生态系统。
三、展示
1、ceisum展示
在Cesium的模型类型的数据的展示,大多都会转成3Dtiles的数据格式进行加载,所以针对于点云数据的加载就是如何将点云数据转成3DTiles的格式,在此推荐Cesiumlab软件,该软件可以将大多数的模型数据转成我们所需要的3Dtiles数据。
let url='http://data1.mars3d.cn/3dtiles/pnts-ganta/tileset.json'
const loadModel = async () => {
const tileset = await Cesium.Cesium3DTileset.fromUrl(
url, {
dynamicScreenSpaceError: true,
dynamicScreenSpaceErrorDensity: 2.0e-4,
dynamicScreenSpaceErrorFactor: 24.0,
dynamicScreenSpaceErrorHeightFalloff: 0.25
});
let ts = viewer?.scene.primitives.add(tileset);
viewer?.zoomTo(ts);
}
优势:cesium针对于3Dtiles数据的加载做了相当多的优化,无论是加载方式、显示、参数控制都有很多方法,能让我们适配不同的加载场景。并且我们还可以通过各种性能优化,实现大数据的加载,例如(100G),这相对于Threejs,性能更好,加载更快。
缺点:需要先转换成3Dtiles格式,且不支持点云文件的直接加载,这是相对于Threejs的劣势的地方。并且市面上的直接加载原始点云文件的文献相对较少,需要实现需要深入研究。
由于后续,我们正针对于二进制的点云数据进行了解析,现在又了坐标和颜色,也可以直接加载。只是过程相对繁琐而已。
2、Threejs展示
Threejs提供了官方的PCDLoader和PLYLoader的加载器,使得我们能够直接加载.pcd后缀的点云模型。但是对去其他类型的点云模型是无法加载的,经过探索,有以下的实现策略
-
自己封装对应类型的点云Loader,
-
点云的归根到底就是点的展现,我们需要实现的就是读取点云数据,获取里面的点的position和material,我们获取后就可以将这些在Threejs中单独的实例出来。当然这也是最复杂也是最消耗性能的一种方法,不是很推荐。
-
在此推荐下las的借鉴策略,las读取借鉴
-
-
将除去.pcd和.ply格式下的所有点云文件转成.pcd格式或者.ply再进行加载。
-
potree--参考地址
-
实现原理是是先使用 potree 的八叉树索引构建工具将 las 数据转化为 octree 数据格式,然后使用网络 potree-core 库(potree 简化版),并实在 three 中通过八叉树索引加载点云动态加载。
-
Potree在Web上展示的点云文件,支持binary,las和laz三种数据格式。所以,对其它的三维模型文件格式(例如:ply),需通过PotreeConverter工具进行转换。
-
此方法是较为推荐的一种方法,但是过程相对复杂,但是有一个相对完整的流程,并且可以实现全自动的处理流程,
-
思路:在存放三维模型文件的服务器,存在一个后台运行的程序:定时读取新的三维模型文件、调用PotreeConverter控制台程序自动转换模型文件到Potree支持的目录、关闭PotreeConverter控制台程序、将生成Potree目录信息拼接成http网址(例如:http://localhost/potree/test.html,其中localhost将被真实的域名或IP取代),最后更新到关联数据库中。此后台程序将包装成Service API的形式(例如:RESTful方式)以供调用。当服务层每上传一个模型文件成功后,调用该后台Service API接口来获到生成的网址,将该网址随原客户端信息一起更新到数据库中。这种由服务层直接拉的方式,可避免定时转换问题、动态监测模型文件、以及远程更新数据库的问题。
-
potree介绍:Potree 是一个开源库,用于可视化和处理点云数据,尤其是在 Web 环境中。
-
PotreeConverter:要使得Potree可以正确解析以及展示数据,必须经过PotreeConvert工具将点云数据进行转化成二进制数据。
流程:参考地址
-
-
git clone https://github.com/potree/PotreeConverter.git
cd PotreeConverter
mkdir build
cd build
cmake ..
make
这时会编译成功PotreeConverter可执行文件,在同级目录下面,将点云数据las文件复制到此处,在同级目录下执行:
./PotreeConverter xxx.las -o output
转换后的文件:
-
3dtils加载策略
-
通过上述我们已经可以知晓,大多点云文件都可以通过cesiumlab转成3dtiles格式,且大多的引擎都是支持3dtiles格式的,所以在Threejs中也可以通过直接加载3dtiles文件的方式来加载点云文件。
-
实现:
-
原理:借助于NASA-AMMOS/3DTilesRendererJS
-
步骤:
-
-
npm install 3d-tiles-renderer --save
import { TilesRenderer } from '3d-tiles-renderer';
...
const url = '/data/pointCloud/tileset.json'
const tilesRenderer = new TilesRenderer( url );
tilesRenderer.setCamera( camera );
tilesRenderer.setResolutionFromRenderer( camera, gl );
scene.add( tilesRenderer.group );
....
加载效果
优点:
- 支持部分点云文件的直接加载,效果相对更加精致。
- 加载方式多种,可实现的策略较多。
缺点:
- 虽然支持部分点云文件的直接加载,但是支持的文件并不完全。需要我们自行实现,相对于Cesium,参考文献相对多一点,实现步骤相对容易。
- 相对于Cesium,Threejs对大量数据的支持并不是很好,所以我们在正对去大数据的加载时,需要自行优化。
- 不能直接支持地理信息,需要中间转换。
四、如何直接解析二进制文件并展示--LAS为例
在思考这个问题前,我们应该理解一个问题,点云肯定是有很多点点构成的。既然是二进制数据,我们想要加载就需要依次解决以下结果问题。
1、如何获取到二进制文件
2、如何解析二进制文件,
1、如何加载二进制文件
就是读取笨蛋文件获取接口请求。
async function fetchLasFile(url: string | URL | Request) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
}
2、如何解析二进制文件--DataView
DataView :是 JavaScript 中的一个内置对象,用于以不同的字节序(Endianess)从底层的 ArrayBuffer 对象读取和写入数据。它提供了一种机制来直接操作二进制数据,可以有效地处理复杂的数据格式,例如网络协议、文件格式等。
例如:
const dataView = new DataView(arrayBuffer);
...
dataView.getUint16(offset,true)//获取2个字节的数据2*8
...
3、las数据
既然已经知道了如何解析二进制的数据,我们接下来需要知道的就是需要知道数据的格式,然后去分别解析出二进制数据中的值。
LAS格式文件包含公共报头块、任意数量(可选)可变长度记录(VLRs)、点数据记录(Point data Records)和任意数量(可选)扩展可变长度记录(EVLRs)
公共报头块包含泛型数据,如点编号和点数据边界。可变长度记录包含可变类型的数据,包括投影信息、元数据、波形包信息和用户应用程序数据。扩展可变长度记录(EVLRs)允许比可变长度记录(VLRs)更高的负载,并且它们具有可被追加到LASfile末尾的优点。
总结:.las的二进制文件主要是由Header+Body+其他组成,在Header中存储了很多相关的信息,其中告诉了二进制点云文件的存储格式、所以我们最先需要的是解析出点云文件中的Header信息,
以.las 版本为例,介绍下.las文件的组成:1.2版本介绍
可以发现LAS的格式定义为:
从链接文件中我们可以发现HEADER中的存储的信息
其中需要注意的有以下几个字节:
Version Major:版本的大版本==1.2版本解析出来就是1
Version Minor:版本的小版本==1.2版本解析出来的就是2
Point Data Format ID (0-99 for spec) :点位数据的存储的Format类型,对应1.2版本中的1、2、3
Number of point records :总点数的数目。
Point Data Record Length :每一个点数据的字节的长度,
Header Size :头部文件的字节长度,也就是告诉我们第一个点的字节位置
X scale factor :X坐标的缩放因子
Y scale factor :Y坐标的缩放因子
Z scale factor :Z坐标的缩放因子
X offset : X坐标的偏移量
Y offset : Y坐标的偏移量
Z offset : Z坐标的偏移量
思路:我们先确实该二进制文件的版本类型(通过Version Major和Version Minor)然后就可以找到对应的版本对应的Header的存储数据的格式(不同版本不一样)然后我们在通过解析二进制然后找出其他对应的值,最后再去解析点位的坐标以及颜色的值,值得注意的是由于点位是16进制,所以需要转位8进制然后在归一化(后续代码有介绍),接下来让我们通过代码来实现吧。
接下来就是需要解析出文件中的Header里面存储的相关信息, 我们通过版本(1.2)找到了Header中每个数据对应的字节长度,然后我们通过其字节长度解析书所有字段对应的值:
function parseLasHeader(arrayBuffer) {
const dataView = new DataView(arrayBuffer);
let offset = 0;
const header = {
fileSignature: '',
fileSourceId: 0,
globalEncoding: 0,
projectId: {
data1: 0,
data2: 0,
data3: 0,
data4: ''
},
versionMajor: 0,
versionMinor: 0,
systemIdentifier: '',
generatingSoftware: '',
fileCreationDayOfYear: 0,
fileCreationYear: 0,
headerSize: 0,
offsetToPointData: 0,
numberOfVariableLengthRecords: 0,
pointDataFormatId: 0,
pointDataRecordLength: 0,
numberOfPointRecords: 0,
numberOfPointsByReturn: [],
scaleFactors: { x: 0, y: 0, z: 0 },
offsets: { x: 0, y: 0, z: 0 },
bounds: {
maxX: 0, minX: 0,
maxY: 0, minY: 0,
maxZ: 0, minZ: 0
}
};
// File Signature (4 bytes)
for (let i = 0; i < 4; i++) {
header.fileSignature += String.fromCharCode(dataView.getUint8(offset + i));
}
offset += 4;
// File Source ID (2 bytes)
header.fileSourceId = dataView.getUint16(offset, true);
offset += 2;
// Global Encoding (2 bytes)
header.globalEncoding = dataView.getUint16(offset, true);
offset += 2;
// Project ID - GUID data
header.projectId.data1 = dataView.getUint32(offset, true);
offset += 4;
header.projectId.data2 = dataView.getUint16(offset, true);
offset += 2;
header.projectId.data3 = dataView.getUint16(offset, true);
offset += 2;
header.projectId.data4 = '';
for (let i = 0; i < 8; i++) {
header.projectId.data4 += dataView.getUint8(offset + i).toString(16).padStart(2, '0');
}
offset += 8;
// Version Major (1 byte)
header.versionMajor = dataView.getUint8(offset);
offset += 1;
// Version Minor (1 byte)
header.versionMinor = dataView.getUint8(offset);
offset += 1;
// System Identifier (32 bytes)
for (let i = 0; i < 32; i++) {
header.systemIdentifier += String.fromCharCode(dataView.getUint8(offset + i));
}
offset += 32;
// Generating Software (32 bytes)
for (let i = 0; i < 32; i++) {
header.generatingSoftware += String.fromCharCode(dataView.getUint8(offset + i));
}
offset += 32;
// File Creation Day of Year (2 bytes)
header.fileCreationDayOfYear = dataView.getUint16(offset, true);
offset += 2;
// File Creation Year (2 bytes)
header.fileCreationYear = dataView.getUint16(offset, true);
offset += 2;
// Header Size (2 bytes)
header.headerSize = dataView.getUint16(offset, true);
offset += 2;
// Offset to point data (4 bytes)
header.offsetToPointData = dataView.getUint32(offset, true);
offset += 4;
// Number of Variable Length Records (4 bytes)
header.numberOfVariableLengthRecords = dataView.getUint32(offset, true);
offset += 4;
// Point Data Format ID (1 byte)
header.pointDataFormatId = dataView.getUint8(offset);
offset += 1;
// Point Data Record Length (2 bytes)
header.pointDataRecordLength = dataView.getUint16(offset, true);
offset += 2;
// Number of point records (4 bytes)
header.numberOfPointRecords = dataView.getUint32(offset, true);
offset += 4;
// Number of points by return (20 bytes, unsigned long[5])
for (let i = 0; i < 5; i++) {
header.numberOfPointsByReturn.push(dataView.getUint32(offset + i * 4, true));
}
offset += 20;
// Scale factors and offsets (double, 8 bytes each)
header.scaleFactors.x = dataView.getFloat64(offset, true);
offset += 8;
header.scaleFactors.y = dataView.getFloat64(offset, true);
offset += 8;
header.scaleFactors.z = dataView.getFloat64(offset, true);
offset += 8;
header.offsets.x = dataView.getFloat64(offset, true);
offset += 8;
header.offsets.y = dataView.getFloat64(offset, true);
offset += 8;
header.offsets.z = dataView.getFloat64(offset, true);
offset += 8;
// Bounds (double, 8 bytes each)
header.bounds.maxX = dataView.getFloat64(offset, true);
offset += 8;
header.bounds.minX = dataView.getFloat64(offset, true);
offset += 8;
header.bounds.maxY = dataView.getFloat64(offset, true);
offset += 8;
header.bounds.minY = dataView.getFloat64(offset, true);
offset += 8;
header.bounds.maxZ = dataView.getFloat64(offset, true);
offset += 8;
header.bounds.minZ = dataView.getFloat64(offset, true);
offset += 8;
return header;
}
解析出的结果:
解析出头部信息后,就可以根据相关信息解析出点位的坐标信息和颜色信息
值得注意的坐标信息为:X实际坐标=X的坐标*X的缩放因子+X的偏移量
颜色的信息为16进制>>8进制-->归一化
从解析的Header文件中获取到
pointDataFormatId的值为3,也就说明该二进制文件的数据格式为3,也就可以根据字节长度以及排列求出坐标从第0个自己开始,长度为4。颜色是从第28个字节长度开始。所以+28。
function parsePointData(arrayBuffer, header) {
const dataView = new DataView(arrayBuffer);
const points = new Float32Array(header.numberOfPointRecords - header.numberOfPointRecords % 3);//坐标应该为3的倍数--目前找到的决绝办法-不然会报错
const colors = new Float32Array(header.numberOfPointRecords - header.numberOfPointRecords % 3);
let offset = header.offsetToPointData;//点位的偏移量
for (let i = 0; i < (header.numberOfPointRecords); i++) {
// Read the X, Y, and Z coordinates as signed 32-bit integers
const x = dataView.getInt32(offset, true);
const y = dataView.getInt32(offset + 4, true);
const z = dataView.getInt32(offset + 4 + 4, true);
// Apply scale factors and offsets
points[i * 3] = x * header.scaleFactors.x + header.offsets.x;
points[i * 3 + 1] = y * header.scaleFactors.y + header.offsets.y;
points[i * 3 + 2] = z * header.scaleFactors.z + header.offsets.z;
//为何从加28开始计算颜色,因为通过稳定查处Format3格式的数据的r是从点位开始的第28个字节开始的。
colors[i * 3] = (dataView.getUint16(offset + 28, true) >> 8) / 255;
colors[i * 3 + 1] = (dataView.getUint16(offset + 30, true) >> 8) / 255;
colors[i * 3 + 2] = (dataView.getUint16(offset + 32, true) >> 8) / 255;
// Move to the next point data record
offset += header.pointDataRecordLength;
}
return {
positions: points,
colors: colors
};
}
-
绘制出点云数据
-
通过上述的代码成功解析出了点的坐标和坐标和点的颜色信息,接下来只需要绘制出点云数据就ok
-
利用Threejs来展示点云数据:
const pointGeometry = new THREE.BufferGeometry();
pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
pointGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.2, // 点的大小
vertexColors: true, // 使用顶点颜色
sizeAttenuation: true, // 点的大小是否随距离变化
})
const points = new THREE.Points(pointGeometry, material);
scene.add(points);
//最后设置一下相机的视角:
const vista = new THREE.Box3().setFromObject(points);
const center = vista.getCenter(new THREE.Vector3());
const size = vista.getSize(new THREE.Vector3());
const maxDimension = Math.max(size.x, size.y, size.z);
const distance = maxDimension / (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) / 2));
camera.position.copy(center);
camera.position.z += distance;
camera.lookAt(center);
-
展示效果:
总结:文章中只有1.2版本的las加载,我们需要根据解析出来的版本号去找到对应的header以及data的数据存储格式,再去解析出相关的点云数据。在这有问题可以申请加群QQ:825493528