首页 前端知识 Three.js pcl.js 实现Web端的点云处理 显示

Three.js pcl.js 实现Web端的点云处理 显示

2024-02-01 12:02:03 前端知识 前端哥 1197 355 我要收藏

1 功能实现

在前面我们实现了PCD的加载器的基础上,这次将加上 pcl.js —— 著名的PCL库的web版本,详情见https://pcl.js.org/,来处理我们加载上去的点云。

具体实现如下:

brief image-20231209154425964

用户可以通过每个板块的右上角进行处理前 / 后的切换,还可以通过一些参数调控pcl算法(注意:调完参数后需要切换显示模式才能生效

  1. 点云过滤示例1:
    • 主要功能:通过统计离群值移除算法,用户可以调整参数 meanKstddevMulThresh 查看过滤效果。
  2. 点云关键点提取示例2:
    • 主要功能:应用 ISS(Intrinsic Shape Signatures) 算法进行关键点提取,用户可以调整参数 SalientRadiusNonMaxRadiusThreshold21Threshold32MinNeighbors 查看效果。
  3. 最小切割示例3:
    • 主要功能:通过最小切割算法,用户可以调整参数 RadiusSigmaSourceWeightNumberOfNeighbours 查看效果。

2 具体实现

本项目是基于 Three.jspcl.js 实现的简单一个Web应用程序,用于可视化三维点云数据并且处理三维点云。

使用 VSCode 的 Live Serve 搭建网络编程的环境,采用CDN的方式引入 Three.js (版本:r158) 和 pcl.js(版本:1.16.0)

2.1 html

HTML 代码定义了一个基本网页,用于使用三个不同的JS文件处理点云数据:

  • PCLFilter.js
  • PCLKeyPoints.js
  • PCLCutter.js

结构:

  • 页面包含一个分为三个面板的容器

  • 每个面板都有一个用于选择“原始”和“过滤”点云数据显示的单选按钮组

    通过 radio 按钮选择不同的显示模式,可以查看原始点云数据或经过处理后的点云数据。

  • 每个面板还有一个引用特定 .js 文件的脚本标签

  • 使用 flex 布局来布局三个面板,使页面分为左上左下,三个板块

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PCD visulize</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body style="color: rgb(131, 131, 131);">
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/"
}
}
</script>
<div class="container">
<div class="panel panel1" id="Panel1"
style="position: relative;height: calc(50vh - 2px);width: 50vw;border-bottom: #ccc 2px solid;border-right: #ccc 2px solid">
<fieldset style="position: absolute; right: 0; top: 0;">
<legend>选择显示模式</legend>
<div>
<input type="radio" id="original1" name="display1" value="original1" checked />
<label for="original1">处理前</label>
</div>
<div>
<input type="radio" id="filtered1" name="display1" value="filtered1" />
<label for="filtered1">处理后</label>
</div>
</fieldset>
<script type="module" src="js/PCLFilter.js"> </script>
</div>
<div class="panel panel2" id="Panel2"
style="position: absolute;right: 0;top: 0;height: 100vh;width: calc(50vw - 4px);">
<fieldset style="position: absolute; right: 0; top: 0;">
<legend>选择显示模式</legend>
<div>
<input type="radio" id="original2" name="display2" value="original2" checked />
<label for="original2">处理前</label>
</div>
<div>
<input type="radio" id="filtered2" name="display2" value="filtered2" />
<label for="filtered2">处理后</label>
</div>
</fieldset>
<script type="module" src="js/PCLKeyPoints.js"> </script>
</div>
<div class="panel panel3" id="Panel3"
style="position: relative;height: 50vh;width: 50vw;border-right: #ccc 2px solid">
<fieldset style="position: absolute; right: 0; top: 0;">
<legend>选择显示模式</legend>
<div>
<input type="radio" id="original3" name="display3" value="original3" checked />
<label for="original3">处理前</label>
</div>
<div>
<input type="radio" id="filtered3" name="display3" value="filtered3" />
<label for="filtered3">处理后</label>
</div>
</fieldset>
<script type="module" src="js/PCLCutter.js"> </script>
</div>
</div>
<style>
.container {
/* display: grid; */
/* grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr; */
height: 100vh;
}
.panel {
flex: 1 0 50%;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
}
#panel2 {
height: 100vh;
}
</style>
</body>
</html>
复制

2.2 js

该项目中有三个类似的JS文件,每个都大同小异,只是使用了不同的PCL功能罢了,我将主要详细讲解其中一个的全流程

2.2.1 PCLFilter.js

实现点云过滤处理

1. 引入库

通过 import 方式引入了 pcl.js 和 three.js 库,以及一些 three.js 相关的模块。

import * as PCL from "https://cdn.jsdelivr.net/npm/pcl.js@1.16.0/dist/pcl.esm.js";
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
复制
2. three.js 初始化

three.js 的经典三大件,初始化了 OrbitControls 使我们可以用鼠标控制点云,还创建了一个GUI

const container = document.getElementById('Panel1');
// 创建场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(80, container.offsetWidth / container.offsetHeight, 0.01, 10000000);
camera.position.set(0, 0, 1.5);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(container.offsetWidth, container.offsetHeight);
container.appendChild(renderer.domElement);
// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
var gui = new GUI();
gui.title('示例1:点云过滤');
var attributesFolder = gui.addFolder('点云设置');
gui.domElement.style.left = '0.1%';
gui.domElement.style.position = 'absolute';
复制
3. 加载点云数据执行滤波
  • 异步加载点云数据: 通过 fetch 函数异步获取点云数据,将数据转换为 ArrayBuffer;使用 PCL.init 初始化 pcl.js 库,指定 wasm 文件的路径;再使用 PCL.loadPCDData 函数加载点云数据
  • 创建 StatisticalOutlierRemoval 滤波器: 使用 new PCL.StatisticalOutlierRemoval() 创建统计离群值滤波器;使用 sor.setMeanKsor.setStddevMulThresh 设置滤波器的参数;再使用 sor.filter() 对点云进行滤波
  • 保存滤波后和原始点云的数据: 使用 PCL.savePCDDataASCII 将滤波后和原始点云的数据保存为 ASCII 格式
  • 绑定事件: 调用 bindEvent() 函数,用于处理滤波后数据
let cloud; // 存储点云数据
let cloudOriginalData; // 存储原始点云数据
let cloudFilteredData; // 存储滤波后的点云数据
async function main() {
// 异步获取点云数据
const cloudBuffer = await fetch("./images/point_cloud.pcd").then((res) =>
res.arrayBuffer()
);
// 初始化 pcl.js 库
await PCL.init({
url: `https://cdn.jsdelivr.net/npm/pcl.js/dist/pcl-core.wasm`
});
// 加载点云数据
cloud = PCL.loadPCDData(cloudBuffer, PCL.PointXYZ);
// 创建 StatisticalOutlierRemoval 滤波器
const sor = new PCL.StatisticalOutlierRemoval();
sor.setInputCloud(cloud);
sor.setMeanK(40);
sor.setStddevMulThresh(3.0);
// 对点云进行滤波
const cloudFiltered = sor.filter();
// 保存滤波后和原始点云的数据(ASCII格式)
cloudFilteredData = PCL.savePCDDataASCII(cloudFiltered);
cloudOriginalData = PCL.savePCDDataASCII(cloud);
// 绑定事件
bindEvent();
}
// 调用 main 函数
main();
复制
4. 显示切换
  1. bindEvent切换函数

    为页面上的两个单选按钮添加 “change” 事件监听器,实现用户选择显示原始点云或滤波后点云的功能

    function bindEvent() {
    // 初始显示原始点云
    showPointCloud(cloudOriginalData);
    // 获取两个单选按钮元素
    const radioOriginal = document.getElementById("original1");
    const radioFiltered = document.getElementById("filtered1");
    // 为两个单选按钮添加 "change" 事件监听器
    [radioOriginal, radioFiltered].forEach((el) => {
    el.addEventListener("change", (e) => {
    const mode = e.target.id; // 获取选中按钮的 id
    reset(); // 重置 GUI
    // 根据选中的按钮 id,显示相应的点云数据
    switch (mode) {
    case "original1":
    showPointCloud(cloudOriginalData);
    break;
    case "filtered1":
    showPointCloud(cloudFilteredData);
    break;
    }
    });
    });
    }
    复制
  2. GUI重置函数

    • 删除之前的 GUI:使用 gui.destroy() 方法删除之前的 GUI 实例。
    • 创建新的 GUI 实例:创建一个新的 GUI 实例,并进行一些设置,如添加标题、文件夹等。
    • 删除之前的点云:通过 scene.remove(scene.children[0]) 删除之前的点云对象。
    function reset() {
    // 删除之前的 GUI
    gui.destroy();
    // 创建一个新的 GUI 实例
    gui = new GUI();
    // gui.add(isRotation, 'bool').name('旋转');
    gui.title('点云过滤');
    attributesFolder = gui.addFolder('点云设置');
    gui.domElement.style.left = '0.1%';
    gui.domElement.style.position = 'absolute';
    // 删除之前的点云
    scene.remove(scene.children[0]);
    }
    复制
5. 点云显示
  1. 将PCL的点云的数据(ASCII格式),转换为URL以便 three.js 的PCD加载
    • 将 ArrayBuffer 转换为字符串: 使用 TextDecoder 将输入的 ArrayBuffer 数据解码为字符串。
    • 从字符串创建 Blob: 使用 Blob 构造函数将字符串数据转换为 Blob 对象,设置 MIME 类型为 ‘text/plain’。
    • 从 Blob 创建 URL: 使用 URL.createObjectURL 创建一个包含 Blob 数据的 URL,用于加载点云模型。
  2. three.js 的点云加载显示
    • 加载点云模型:使用点云加载器的 load 方法加载点云模型。在加载完成后,调用回调函数,其中 points 包含了点云的几何信息。
    • 几何变换:对点云的几何进行居中和绕 X 轴旋转。
    • 创建点云材质:使用 THREE.PointsMaterial 创建点云的材质,设置颜色、点大小等属性。
    • 根据点云数据设置颜色:根据当前点云是原始数据还是滤波后的数据,设置点云的颜色。
    • 创建点云对象:使用 THREE.Points 创建点云对象,将其添加到场景中。
    • 在 GUI 中添加点云相关设置:使用 attributesFolder.addFolder 创建一个 GUI 文件夹,添加文件名、点数、点大小、点颜色等设置。
function showPointCloud(currentPointCloud) {
// 将 ArrayBuffer 转换为字符串
const decoder = new TextDecoder('utf-8');
const pcdString = decoder.decode(new Uint8Array(currentPointCloud));
// 从字符串创建 Blob
const blob = new Blob([pcdString], { type: 'text/plain' });
// 从 Blob 创建 URL
const url = URL.createObjectURL(blob);
// 创建点云加载器
const loader = new PCDLoader();
// 加载点云模型
loader.load(url, function (points) {
// 将点云几何居中
points.geometry.center();
points.geometry.rotateX(Math.PI);
// 创建点云材质
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.02, vertexColors: false });
// 根据当前点云是原始数据还是滤波后的数据设置点云颜色
if (currentPointCloud == cloudOriginalData) {
material.color.setHex(0xad1010); // 设置为红色
} else {
material.color.setHex(0x1ea10c); // 设置为绿色
}
// 创建点云对象
const pointCloud = new THREE.Points(points.geometry, material);
scene.add(pointCloud);
// 在 GUI 中添加点云相关设置
const folder = attributesFolder.addFolder(`点云 0`);
const text = { pointsNum: points.geometry.attributes.position.count, file: "初始pcd" };
folder.add(text, 'file').name('文件');
folder.add(text, 'pointsNum').name('点数');
folder.add(material, 'size', 0.001, 0.03).name('点大小');
folder.addColor(material, 'color').name('点颜色');
});
}
复制
6. 参数调整更新
  1. 创建和配置 GUI

    • 创建 GUI 实例:使用 GUI 类创建了一个 GUI 实例。
    • 设置 GUI 样式:通过 domElement 对象的样式属性设置 GUI 的位置和样式。
    var plcgui = new GUI();
    plcgui.domElement.style.left = '0.1%';
    plcgui.domElement.style.top = '175px';
    plcgui.domElement.style.position = 'absolute';
    复制
    • 定义参数对象 params:包含两个属性 meanKstddevMulThresh,分别表示均值的 K 值和标准差的倍数阈值。
    • 使用 plcgui.add 添加控件:将参数添加到 GUI 中,并使用 onChange 事件指定在值变化时调用 filterPointCloud 函数
    • 设置控件的范围和名称:对 meanK 设置范围为 1 到 100,对 stddevMulThresh 设置范围为 0.1 到 10,并为每个控件指定名称。
    const params = {
    meanK: 40,
    stddevMulThresh: 3.0
    };
    plcgui.add(params, 'meanK', 1, 100).name('meanK').onChange(filterPointCloud);
    plcgui.add(params, 'stddevMulThresh', 0.1, 10).name('stddevMulThresh').onChange(filterPointCloud);
    复制
  2. 过滤点云函数

    与之前的初始滤波的操作一致

    async function filterPointCloud() {
    const cloudBuffer = await fetch("./images/point_cloud.pcd").then((res) =>
    res.arrayBuffer()
    );
    cloud = PCL.loadPCDData(cloudBuffer, PCL.PointXYZ);
    const sor = new PCL.StatisticalOutlierRemoval();
    sor.setInputCloud(cloud);
    sor.setMeanK(params.meanK);
    sor.setStddevMulThresh(params.stddevMulThresh);
    const cloudFiltered = sor.filter();
    cloudFilteredData = PCL.savePCDDataASCII(cloudFiltered);
    cloudOriginalData = PCL.savePCDDataASCII(cloud);
    }
    复制
7. 渲染循环
function animate() {
requestAnimationFrame(animate);
// 渲染场景
renderer.render(scene, camera);
}
animate();
复制
2.2.2 PCLKeyPoints.js

实现点云关键点提取操作

大致内容与 PCLFilter.js 相似,这里只对关键差异之处进行描述

1. 关键点提取
  • 计算点云分辨率: 使用 PCL.computeCloudResolution 计算点云的分辨率。
  • 创建 Kd 树和 ISS 关键点提取器: 使用 PCL.SearchKdTree 创建 Kd 树,使用 PCL.ISSKeypoint3D 创建 ISS 关键点提取器。
  • 设置参数: 设置 ISS 关键点提取器的各项参数,如搜索半径、非极大值抑制半径、阈值等。
  • 计算关键点: 使用 compute 方法计算关键点,并将结果保存在 keypoints 中。

注意:此处是将结果(关键点)保存到了 keypoints 当中

let cloud;
let keypoints;
async function main() {
const cloudBuffer = await fetch("./images/point_cloud.pcd").then((res) =>
res.arrayBuffer()
);
await PCL.init({
url: `https://cdn.jsdelivr.net/npm/pcl.js/dist/pcl-core.wasm`
});
cloud = PCL.loadPCDData(cloudBuffer, PCL.PointXYZ);
const resolution = PCL.computeCloudResolution(cloud);
const tree = new PCL.SearchKdTree();
const iss = new PCL.ISSKeypoint3D();
keypoints = new PCL.PointCloud();
iss.setSearchMethod(tree);
iss.setSalientRadius(6 * resolution);
iss.setNonMaxRadius(4 * resolution);
iss.setThreshold21(0.975);
iss.setThreshold32(0.975);
iss.setMinNeighbors(5);
iss.setInputCloud(cloud);
iss.compute(keypoints);
cloudOriginalData = PCL.savePCDDataASCII(cloud);
bindEvent();
}
复制
2. 关键点显示

bindEvent() 函数中设置显示 false/true 来控制显示关键点

function bindEvent() {
...
switch (mode) {
case "original2":
showPointCloud(false);
break;
case "filtered2":
showPointCloud(true);
break;
...
}
复制
  • 展示关键点:如果 showKeypoints 为真,将关键点的坐标添加到 pos 数组中,并创建关键点的 BufferGeometryPointsMaterial
  • 创建点云对象:创建点云的 BufferGeometryPointsMaterial
  • 组合点云和关键点:将点云和关键点组合到一个 THREE.Group 中。
  • 调整位置:通过计算包围盒中心,调整组的位置,使其居中。
  • GUI 设置:在 GUI 中添加点云和关键点的相关设置,如文件名、点数、点大小、颜色等。
function showPointCloud(showKeypoints) {
...
const pos = [];
// 如果需要展示关键点
if (showKeypoints) {
for (let i = 0; i < keypoints.points.size; ++i) {
const point = keypoints.points.get(i);
pos.push(point.x, point.y, point.z);
}
}
// 创建关键点 PointsMaterial
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
const keypointsMaterial = new THREE.PointsMaterial({ size: 0.05, color: 0xff0000 });
const keypointsMesh = new THREE.Points(geometry, keypointsMaterial);
// 创建点云的 PointsMaterial
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.02, vertexColors: false });
const pointCloud = new THREE.Points(points.geometry, material);
// 创建一个组,将点云和关键点添加到组中
const group = new THREE.Group();
group.add(pointCloud);
group.add(keypointsMesh);
// 调整组的旋转,使其在显示时朝上
group.rotation.set(Math.PI, 0, 0);
// 计算组的包围盒
const boundingBox = new THREE.Box3();
boundingBox.setFromObject(group);
// 获取包围盒中心
const center = new THREE.Vector3();
boundingBox.getCenter(center);
// 计算平移向量,使组居中
const translation = new THREE.Vector3();
translation.subVectors(new THREE.Vector3(0, 0, 0), center);
group.position.add(translation);
// 将组添加到场景中
scene.add(group);
...
// 在GUI中添加关键点大小的调整
folder.add(keypointsMaterial, 'size', 0.03, 0.1).name('关键点大小');
...
}
复制
2.2.3 PCLCutter.js

实现点云最小切割操作

大致内容与 PCLFilter.js 相似,这里只对关键差异之处进行描述

1. 最小切割
  • 创建对象中心和前景点云: 创建一个表示对象中心的 PCL.PointXYZ 实例,并创建一个前景点云 foregroundPoints,将对象中心添加到其中。
  • 创建 MinCutSegmentation 分割器: 使用 PCL.MinCutSegmentation 创建点云分割器。
  • 设置分割器参数: 设置分割器的参数,包括前景点云、输入点云、半径、标准差、源权重和邻居数量等。
  • 执行分割: 执行分割操作。
  • 获取着色的点云: 使用getColoredCloud方法获取切割部分着色的点云数据,并保存到 cloudFilteredData 中。
async function main() {
const cloudBuffer = await fetch("./images/point_cloud.pcd").then((res) =>
res.arrayBuffer()
);
await PCL.init({
url: `https://cdn.jsdelivr.net/npm/pcl.js/dist/pcl-core.wasm`
});
cloud = PCL.loadPCDData(cloudBuffer, PCL.PointXYZ);
const objectCenter = new PCL.PointXYZ(2, 0, 0);
const foregroundPoints = new PCL.PointCloud();
foregroundPoints.addPoint(objectCenter);
const seg = new PCL.MinCutSegmentation();
seg.setForegroundPoints(foregroundPoints);
seg.setInputCloud(cloud);
seg.setRadius(3.0433856);
seg.setSigma(0.1);
seg.setSourceWeight(0.8);
seg.setNumberOfNeighbours(14);
seg.extract();
const coloredCloud = seg.getColoredCloud();
cloudFilteredData = PCL.savePCDDataASCII(coloredCloud);
cloudOriginalData = PCL.savePCDDataASCII(cloud);
bindEvent();
}
复制
2. 最小切割显示

showPointCloud 函数中通过设置是否显示点云的顶点颜色来显示切割部分

let showVertColor = false;
// 当前是切割后的点云就显示点云颜色
if (currentPointCloud != cloudOriginalData) {
showVertColor = true;
}
// 创建点云材质
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.02, vertexColors: showVertColor });
复制

3 源码

源码见 Github : 图像与动画实验

转载请注明出处或者链接地址:https://www.qianduange.cn//article/998.html
评论
还可以输入200
共0条数据,当前/页
发布的文章
大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!