在前面我们实现了PCD的加载器的基础上,这次将加上 pcl.js —— 著名的PCL库的web版本,详情见https://pcl.js.org/,来处理我们加载上去的点云。
具体实现如下:
用户可以通过每个板块的右上角进行处理前 / 后的切换,还可以通过一些参数调控pcl算法(注意:调完参数后需要切换显示模式才能生效)
meanK
和 stddevMulThresh
查看过滤效果。SalientRadius
、NonMaxRadius
、Threshold21
、Threshold32
和 MinNeighbors
查看效果。Radius
、Sigma
、SourceWeight
和 NumberOfNeighbours
查看效果。本项目是基于 Three.js 和 pcl.js 实现的简单一个Web应用程序,用于可视化三维点云数据并且处理三维点云。
使用 VSCode 的 Live Serve 搭建网络编程的环境,采用CDN的方式引入 Three.js (版本:r158) 和 pcl.js(版本:1.16.0)
HTML 代码定义了一个基本网页,用于使用三个不同的JS文件处理点云数据:
PCLFilter.js
PCLKeyPoints.js
PCLCutter.js
结构:
页面包含一个分为三个面板的容器
每个面板都有一个用于选择“原始”和“过滤”点云数据显示的单选按钮组
通过 radio 按钮选择不同的显示模式,可以查看原始点云数据或经过处理后的点云数据。
每个面板还有一个引用特定 .js
文件的脚本标签
使用 flex 布局来布局三个面板,使页面分为左上,左下,右,三个板块
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PCD visulizetitle>
<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/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/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>
该项目中有三个类似的JS文件,每个都大同小异,只是使用了不同的PCL功能罢了,我将主要详细讲解其中一个的全流程
实现点云过滤处理
通过 import 方式引入了 pcl.js 和 three.js 库,以及一些 three.js 相关的模块。
import * as PCL from "https://cdn.jsdelivr.net/npm/[email protected]/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';
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';
fetch
函数异步获取点云数据,将数据转换为 ArrayBuffer
;使用 PCL.init
初始化 pcl.js 库,指定 wasm 文件的路径;再使用 PCL.loadPCDData
函数加载点云数据new PCL.StatisticalOutlierRemoval()
创建统计离群值滤波器;使用 sor.setMeanK
和 sor.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();
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;
}
});
});
}
GUI重置函数
gui.destroy()
方法删除之前的 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]);
}
TextDecoder
将输入的 ArrayBuffer
数据解码为字符串。Blob
构造函数将字符串数据转换为 Blob
对象,设置 MIME 类型为 ‘text/plain’。URL.createObjectURL
创建一个包含 Blob
数据的 URL,用于加载点云模型。load
方法加载点云模型。在加载完成后,调用回调函数,其中 points
包含了点云的几何信息。THREE.PointsMaterial
创建点云的材质,设置颜色、点大小等属性。THREE.Points
创建点云对象,将其添加到场景中。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('点颜色');
});
}
创建和配置 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
:包含两个属性 meanK
和 stddevMulThresh
,分别表示均值的 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);
过滤点云函数
与之前的初始滤波的操作一致
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);
}
function animate() {
requestAnimationFrame(animate);
// 渲染场景
renderer.render(scene, camera);
}
animate();
实现点云关键点提取操作
大致内容与 PCLFilter.js
相似,这里只对关键差异之处进行描述
PCL.computeCloudResolution
计算点云的分辨率。PCL.SearchKdTree
创建 Kd 树,使用 PCL.ISSKeypoint3D
创建 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();
}
在 bindEvent()
函数中设置显示 false
/true
来控制显示关键点
function bindEvent() {
...
switch (mode) {
case "original2":
showPointCloud(false);
break;
case "filtered2":
showPointCloud(true);
break;
...
}
showKeypoints
为真,将关键点的坐标添加到 pos
数组中,并创建关键点的 BufferGeometry
和 PointsMaterial
。BufferGeometry
和 PointsMaterial
。THREE.Group
中。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('关键点大小');
...
}
实现点云最小切割操作
大致内容与 PCLFilter.js
相似,这里只对关键差异之处进行描述
PCL.PointXYZ
实例,并创建一个前景点云 foregroundPoints
,将对象中心添加到其中。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();
}
在 showPointCloud
函数中通过设置是否显示点云的顶点颜色来显示切割部分
let showVertColor = false;
// 当前是切割后的点云就显示点云颜色
if (currentPointCloud != cloudOriginalData) {
showVertColor = true;
}
// 创建点云材质
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.02, vertexColors: showVertColor });
源码见 Github : 图像与动画实验