点云文件在web端的展示思路

一、简介

        点云数据是一种用于表示三维空间中物体表面的离散点集合。每个点通常包含其在三维空间中的位置(x、y、z坐标)以及可能的其他属性(如颜色、强度、法线等)。点云数据广泛应用于各个领域,包括计算机视觉、机器人学、建筑、考古、地理信息系统(GIS)和自动驾驶等。

       https://cloud.pix4d.com/demo

  1.1、点云的格式

  1.   LAS(LASer File Format)

    • 描述:LAS 是一种广泛用于 LiDAR 数据的格式,包含了点的空间坐标、强度、分类、时间戳、颜色等信息。LAS 文件是二进制格式,体积较小,读取速度快。

    • 应用:地理信息系统(GIS)、环境建模、城市规划。

  1.   LAZ

    • 描述:LAZ 是 LAS 格式的压缩版本,使用 LZMA 压缩算法,显著减少了文件大小。适用于需要存储和传输大量 LiDAR 数据的场景。

    • 应用:与 LAS 相同,但更加适合数据存储和传输。

  1.   PLY

    • 描述:与 LAS 相同,但更加适合数据存储和传输。

    • 应用:计算机图形学、3D 打印、3D 扫描。

  1.   PCD

    • 描述:PCD 是 Point Cloud Library(PCL) 的专用格式,支持多种点云数据类型,包括有序和无序点云。PCD 文件支持存储多种数据属性,如位置、颜色、法线等。

    • 应用:机器人视觉、3D 计算机视觉、自动驾驶。

  1.   E57

    • 描述:E57 是用于存储三维测量数据的格式,包括点云、图像和其他元数据。支持高精度和大规模数据存储,通常用于工业和建筑领域。

    • 应用:建筑信息模型(BIM)、工业检测、文化遗产保护。

  1.   XYZ

    • 描述:XYZ 是一种简单的文本格式,通常每行包含一个点的空间坐标(X、Y、Z)。有时也会包含其他属性,如强度或颜色。

    • 应用:数据交换、简单的点云数据存储和处理。

  1.   OBJ

    • 描述:OBJ 文件格式主要用于表示三维几何图形,包括顶点、法线、纹理坐标和面信息。虽然主要用于存储三维模型,但也可以存储点云数据。

    • 应用:3D 模型交换、计算机图形学、3D 打印。

  1.   PTS

    • 描述:PTS 是一种常见的点云文本格式,每行通常包含一个点的坐标和其他属性(如颜色、强度)。PTS 文件易于阅读和编辑,但体积较大。

    • 应用:数据交换、点云数据存储和处理。

  1.   PTX

    • 描述:PTX 是一种标准的点云交换格式,支持点的坐标和强度信息。通常用于激光扫描仪导出数据,包含扫描位置和其他元数据。

    • 应用:地形建模、建筑信息模型(BIM)、工业检测。

  1.   ASPRS LAS

    • 描述:类似于标准的 LAS 格式,但由美国摄影测量与遥感学会(ASPRS)制定,具有更严格的标准和更多的元数据支持。

    • 应用:专业测绘、GIS 应用、环境监测

二、点云的展示思路

        最常见的就是点云转成3Dtiles格式再进行加载,所以优先介绍下3dtiles格式。

        3D Tiles 是一种用于流式传输大型三维地理空间数据集的开放规范,主要由 Cesium 创造。这种数据格式的创建背后有几个关键原因:

  1. 高效的数据管理和流式传输:传统的3D数据格式并不适合大规模地理空间数据集的高效流式传输和渲染。3D Tiles 旨在解决这个问题,通过分层级和按需加载的方式,使得大型数据集可以在各种设备上高效地渲染和导航,无论数据集有多大。

  2. 多样化的数据类型支持:3D Tiles 支持多种不同的数据类型,包括点云、3D建模、影像等,这使其成为一种多功能的数据格式,适用于各种不同的地理空间数据应用场景。

  3. 与现有技术的兼容性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后缀的点云模型。但是对去其他类型的点云模型是无法加载的,经过探索,有以下的实现策略

  1. 自己封装对应类型的点云Loader,
    1. 点云的归根到底就是点的展现,我们需要实现的就是读取点云数据,获取里面的点的position和material,我们获取后就可以将这些在Threejs中单独的实例出来。当然这也是最复杂也是最消耗性能的一种方法,不是很推荐。

    2. 在此推荐下las的借鉴策略,las读取借鉴

  2. 将除去.pcd和.ply格式下的所有点云文件转成.pcd格式或者.ply再进行加载。
    1. 在线转换工具

  3. potree--参考地址
    1. 实现原理是是先使用 potree 的八叉树索引构建工具将 las 数据转化为 octree 数据格式,然后使用网络 potree-core 库(potree 简化版),并实在 three 中通过八叉树索引加载点云动态加载。

    2. Potree在Web上展示的点云文件,支持binary,las和laz三种数据格式。所以,对其它的三维模型文件格式(例如:ply),需通过PotreeConverter工具进行转换。

    3. 此方法是较为推荐的一种方法,但是过程相对复杂,但是有一个相对完整的流程,并且可以实现全自动的处理流程,

      1. 思路:在存放三维模型文件的服务器,存在一个后台运行的程序:定时读取新的三维模型文件、调用PotreeConverter控制台程序自动转换模型文件到Potree支持的目录、关闭PotreeConverter控制台程序、将生成Potree目录信息拼接成http网址(例如:http://localhost/potree/test.html,其中localhost将被真实的域名或IP取代),最后更新到关联数据库中。此后台程序将包装成Service API的形式(例如:RESTful方式)以供调用。当服务层每上传一个模型文件成功后,调用该后台Service API接口来获到生成的网址,将该网址随原客户端信息一起更新到数据库中。这种由服务层直接拉的方式,可避免定时转换问题、动态监测模型文件、以及远程更新数据库的问题。

      2. potree介绍:Potree 是一个开源库,用于可视化和处理点云数据,尤其是在 Web 环境中。

      3. 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

  转换后的文件:

  1. 3dtils加载策略

    1. 通过上述我们已经可以知晓,大多点云文件都可以通过cesiumlab转成3dtiles格式,且大多的引擎都是支持3dtiles格式的,所以在Threejs中也可以通过直接加载3dtiles文件的方式来加载点云文件。

    2. 实现:

      1. 原理:借助于NASA-AMMOS/3DTilesRendererJS

      2. 步骤:

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

  • 23
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于Web端的三维点云框架,一个常用的选择是Three.js。Three.js是一个用于在Web上创建和显示3D图形的JavaScript库,可以在浏览器中实现三维点云的渲染和交互。 使用Three.js,您可以加载和显示点云数据,并应用各种效果和操作。以下是使用Three.js创建和展示三维点云的一般步骤: 1. 在HTML页面中引入Three.js库: ```html <script src="https://threejs.org/build/three.js"></script> ``` 2. 创建一个场景(Scene)、相机(Camera)和渲染器(Renderer): ```javascript var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); ``` 3. 加载点云数据并创建点云对象: ```javascript // 假设您已经有一个点云数据数组 points var geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(points, 3)); var material = new THREE.PointsMaterial({ color: 0x00ff00 }); var pointCloud = new THREE.Points(geometry, material); scene.add(pointCloud); ``` 4. 渲染场景: ```javascript function animate() { requestAnimationFrame(animate); // 对场景中的点云进行更新或交互操作 renderer.render(scene, camera); } animate(); ``` 这只是一个简单的示例,您可以根据自己的需求进行扩展和定制。Three.js还提供了丰富的功能和效果,例如相机控制、光照、阴影等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值