Three.js + Tensorflow.js 构建实时人脸点云

本文重点介绍使用 Three.js 和 Tensorflow.js 实现实时人脸网格点云所需的步骤。 它假设你之前了解异步 javascript 和 Three.js 基础知识,因此不会涵盖基础知识。

该项目的源代码可以在此 Git 存储库中找到。 在阅读本文时查看该代码将会很有帮助,因为将跳过一些基本的实现步骤。

本文还将以面向对象的方式实现该项目,其中包含大量抽象,因此对 Typescript 中的类有基本的了解是一个优势。

在这里插入图片描述

推荐:用 NSDT编辑器 快速搭建可编程3D场景

1、获取 Three.js 设置

由于本教程的目标是渲染人脸点云,因此我们需要从设置三个 js 场景开始。

设置场景所需的数据和方法封装在 sceneSetUp.ts 中名为 ThreeSetUp 的工厂类中。 该类负责创建所有必要的场景对象,如渲染器、相机和场景。 它还启动画布元素的调整大小处理程序。 该类具有以下公共方法:

  • getSetUp:此函数返回一个对象,其中包含相机、场景、渲染器和画布的大小信息。
getSetUp(){return {camera: this.camera,scene: this.scene,renderer : this.renderer,sizes: this.sizes,}}
  • apply OrbitControls:此方法将负责在我们的设置中添加轨道控件,并返回我们需要调用以更新轨道控件的函数。
applyOrbitControls(){const controls = new OrbitControls(this.camera, this.renderer.domElement!)controls.enableDamping = truereturn ()=> controls.update();}

我们的主要实现类 FacePointCloud 将启动 ThreeSetUP 类并调用这两个方法来获取设置元素并应用轨道控制。

2、从网络摄像头生成视频数据

为了使我们能够获得面部网格跟踪信息,我们需要一个像素输入来提供给面部网格跟踪器。 在这种情况下,我们将使用设备网络摄像头来生成此类输入。 我们还将使用 HTML 视频元素(不将其添加到 Dom)从网络摄像头读取媒体流,并以我们的代码可以交互的方式加载它。

在此步骤之后,我们将设置一个 HTML canvas 元素(也无需添加到 Dom)并向其渲染视频输出。 这使我们还可以选择从画布生成三个 Js 纹理并将其用作材质(我们不会在本教程中实现这一点)。 我们将使用 canvas 元素作为 FaceMeshTracker 的输入。

为了处理从网络摄像头读取媒体流并将其加载到视频 HTML 元素,我们将创建一个名为 WebcamVideo 的类。 此类将处理创建 HTML 视频元素并调用导航器 api 来加载获取用户权限并从设备的网络摄像头加载信息。

在启动此类时,将调用私有 init 方法,该方法具有以下代码:

private init(){navigator.mediaDevices.getUserMedia(this.videoConstraints).then((mediaStream)=>{this.videoTarget.srcObject = mediaStreamthis.videoTarget.onloadedmetadata = () => this.onLoadMetadata()}).catch(function (err) {alert(err.name + ': ' + err.message)})
}

此方法调用导航器对象的 mediaDevices 属性上的 getUserMedia 方法。 此方法将视频约束(也称为视频设置)作为参数并返回一个承诺。 此承诺解析为包含来自网络摄像头的视频数据的 mediaStream 对象。 在 Promise 的解析回调中,我们将视频元素的源设置为返回的 mediaStream。

在promise解析回调中,我们还在视频元素上添加了一个loadedmetadata事件监听器。 该监听器的回调会触发对象的 onLoadMetaData 方法并设置以下副作用:

  • 自动播放视频
  • 确保视频内嵌播放
  • 调用我们传递给对象的可选回调,以便在事件触发时调用
private onLoadMetadata(){this.videoTarget.setAttribute('autoplay', 'true')this.videoTarget.setAttribute('playsinline', 'true')this.videoTarget.play()this.onReceivingData()
}

此时,我们有一个 WebcamVideo 对象,它负责创建包含实时网络摄像头数据的视频元素。 下一步是将视频输出绘制在画布对象上。

为此,我们将创建一个使用 WebcamVideo 类的特定 WebcamCanvas 类。 此类将创建 WebcamVideo 类的实例,并使用它通过画布上下文方法的drawImage()将视频的输出绘制到画布上。 这将在 updateFromWebcam 方法上实现。

updateFromWebCam(){this.canvasCtx.drawImage(this.webcamVideo.videoTarget,0,0,this.canvas.width,this.canvas.height)
}

我们必须在渲染循环中不断调用此函数,以不断使用视频的当前帧更新画布。

此时,我们已将像素输入准备为显示网络摄像头的画布元素。

3、使用 Tensorflow.js 创建 Face Mesh 检测器

创建人脸网格检测器并生成检测数据是本教程的主要部分。 这将实现 Tensorflow.js 人脸特征点检测模型。

npm add @tensorflow/tfjs-core, @tensorflow/tfjs-converter
npm add @tensorflow/tfjs-backend-webgl
npm add @tensorflow-models/face-detection
npm add @tensorflow-models/face-landmarks-detection

安装所有相关包后,我们将创建一个处理以下内容的类:

  • 加载模型
  • 获取探测器对象
  • 将检测器添加到类中
  • 实现一个公共检测函数以供其他对象使用。

我们创建了一个名为faceLandmark.ts 的文件来实现该类。 文件顶部的导入是:

import '@mediapipe/face_mesh'
import '@tensorflow/tfjs-core'
import '@tensorflow/tfjs-backend-webgl'
import * as faceLandmarksDetection from '@tensorflow-models/face-landmarks-detection'

运行和创建检测器对象将需要这些模块。

我们创建 FaceMeshDetectorClass,如下所示:

export default class FaceMeshDetector {detectorConfig: Config;model: faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;detector: faceLandmarksDetection.FaceLandmarksDetector | null;constructor(){this.model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;this.detectorConfig = {runtime: 'mediapipe',refineLandmarks: true,solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh',}this.detector = null;}private getDetector(){const detector = faceLandmarksDetection.createDetector(this.model, this.detectorConfig as faceLandmarksDetection.MediaPipeFaceMeshMediaPipeModelConfig);return detector;}async loadDetector(){this.detector = await this.getDetector()}async detectFace(source: faceLandmarksDetection.FaceLandmarksDetectorInput){const data = await this.detector!.estimateFaces(source)const keypoints = (data as FaceLandmark[])[0]?.keypointsif(keypoints) return keypoints;return [];}
}

这个类中的主要方法是 getDetector,它调用我们从 Tensorflow.js 导入的 FaceLandMarksDetection 上的 createDetector 方法。 然后 createDetector 采用我们在构造函数中引入的模型:

this.model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;

以及指定检测器参数的检测配置对象:

this.detectorConfig = {runtime: 'mediapipe',refineLandmarks: true,solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh',
}

检测函数将返回一个承诺,该承诺将解析为检测器对象。 然后在 loadDetector 公共异步方法中使用私有 getDetector 函数,该方法将类上的 this. detector 属性设置为检测器。

FaceMeshDetector 类还实现了一个公共的 detectorFace 方法:

async detectFace(source){const data = await this.detector!.estimateFaces(source)const keypoints = (data as FaceLandmark[])[0]?.keypointsif(keypoints) return keypoints;return [];
}

该方法采用一个源参数,即像素输入。 我们将在这里使用上面的画布元素作为跟踪源。 该函数将被如下调用:

faceMeshDetector.detectFace(this.webcamCanvas.canvas)

此方法调用检测器上的estimateFaces方法,如果此方法在网络摄像头输出中检测到人脸,它将返回一个包含检测数据的对象的数组。 该对象有一个称为关键点的属性,它包含模型在面部检测到的 478 个点中每个点的对象数组。 每个对象都有 x、y 和 z 属性,其中包括画布中点的坐标。 例子:

[{box: {xMin: 304.6476503248806,xMax: 502.5079975897382,yMin: 102.16298762367356,yMax: 349.035215984403,width: 197.86034726485758,height: 246.87222836072945},keypoints: [{x: 406.53152857172876, y: 256.8054528661723, z: 10.2, name:"lips"},{x: 406.544237446397, y: 230.06933367750395, z: 8},...],}
]

值得注意的是,这些点作为画布空间中的坐标返回,这意味着参考点、x: 0 和 y: 0 点位于画布的左上角。 稍后当我们必须将坐标转换为 Three.js 场景空间(其参考点位于场景中心)时,这将是相关的。

此时,我们有了像素输入源,以及面部网格检测器,它将为我们提供检测到的点。 现在,我们可以转到 Three.js 部分!

4、创建空点云

为了在 Three.js 中生成面部网格,我们必须从检测器加载面部网格点,然后将它们用作 Three js Points 对象的位置属性。 为了使三个 js 面部网格反映视频中的运动(实时反应),每当我们创建的检测器发生面部检测变化时,我们都必须更新此位置属性。

为了实现这一点,我们将创建另一个名为 PointCloud 的工厂类,它将创建一个空的 Points 对象,以及一个可用于更新此点对象的属性(例如位置属性)的公共方法。 这个类看起来像这样:

export default class PointCloud {bufferGeometry: THREE.BufferGeometry;material: THREE.PointsMaterial;cloud: THREE.Points<THREE.BufferGeometry, THREE.PointsMaterial>;constructor() {this.bufferGeometry = new THREE.BufferGeometry();this.material = new THREE.PointsMaterial({color: 0x888888,size: 0.0151,sizeAttenuation: true,});this.cloud = new THREE.Points(this.bufferGeometry,     this.material);}
updateProperty(attribute: THREE.BufferAttribute, name: string){this.bufferGeometry.setAttribute(name,attribute);this.bufferGeometry.attributes[name].needsUpdate = true;}
}

此类启动空的 BufferGrometry,它是点的材质以及消耗两者的点对象。 将此点对象添加到场景中不会改变任何内容,因为几何体没有任何位置属性,换句话说,没有顶点。

PointCloud 类还公开 updateProperty 方法,该方法接受缓冲区属性和属性名称。 然后,它将调用 bufferGeometry setAttribute 方法并将 needUpdate 属性设置为 true。 这将允许 Three.js 在下一个 requestAnimationFrame 迭代中反映 bufferAttribute 的更改。

我们将使用此 updateProperty 方法根据从 Tensorflow.js 检测器接收到的点来更改点云的形状。

现在,我们还准备好点云来接收新的位置数据。 所以,是时候把所有的东西都绑在一起了!

5、将跟踪信息馈送到点云

为了将所有内容结合在一起,我们将创建一个实现类来调用使所有内容正常工作所需的类、方法和步骤。 这个类称为 FacePointCloud。 在构造函数中,它将实例化以下类:

  • ThreeSetUp 类获取场景设置对象
  • CanvasWebcam 用于获取显示网络摄像头内容的画布对象
  • 用于加载跟踪模型并获取检测器的faceLandMark类
  • PointCloud 类用于设置空点云并稍后使用检测数据更新它
constructor() {this.threeSetUp = new ThreeSetUp()this.setUpElements = this.threeSetUp.getSetUp()this.webcamCanvas = new WebcamCanvas();this.faceMeshDetector = new faceLandMark()this.pointCloud = new PointCloud()
}

这个类还有一个名为bindFaceDataToPointCloud的方法,它执行我们逻辑的主要部分,即获取检测器提供的数据,将其转换为Three.js可以理解的形式,从中创建一个Three.js缓冲区属性并使用 它来更新点云。

async bindFaceDataToPointCloud(){const keypoints = awaitthis.faceMeshDetector.detectFace(this.webcamCanvas.canvas)const flatData = flattenFacialLandMarkArray(keypoints)const facePositions = createBufferAttribute(flatData)this.pointCloud.updateProperty(facePositions, 'position')
}

因此,我们将画布像素源传递给 detectorFace 方法,然后在实用程序函数 flattenFacialLandMarkArray 中对返回的数据执行操作。 这非常重要,因为有两个问题:

正如我们上面提到的,人脸检测模型中的点将以以下形式返回:

keypoints: [{x: 0.542, y: 0.967, z: 0.037},...
]

而 buffer 属性期望数据/数字具有以下形状:

number[] or [0.542, 0.967, 0.037, .....]

数据源(画布)之间的坐标系差异,画布的坐标系如下所示:

在这里插入图片描述

Three.js 场景坐标系如下所示:
在这里插入图片描述

因此,考虑到这两个选项,我们实现了 flattenFacialLandMarkArray 函数来解决这些问题。 该函数的代码如下所示:

function flattenFacialLandMarkArray(data: vector[]){let array: number[] = [];data.forEach((el)=>{el.x = mapRangetoRange(500 / videoAspectRatio, el.x,screenRange.height) - 1el.y = mapRangetoRange(500 / videoAspectRatio, el.y,screenRange.height, true)+1el.z = (el.z / 100 * -1) + 0.5;array = [...array,...Object.values(el),]})return array.filter((el)=> typeof el === 'number');

flattenFacialLandMarkArray 函数获取我们从人脸检测器接收到的关键点输入,并将它们展开到一个数组中,以数字 [] 形式而不是对象 [] 形式。 在将数字传递到新的输出数组之前,它通过 mapRangetoRange 函数将它们从画布坐标系映射到 Three.js 坐标系。 该函数如下所示:

function mapRangetoRange(from: number, point: number, range: range, invert: boolean = false): number{let pointMagnitude: number = point/from;if(invert) pointMagnitude = 1-pointMagnitude;const targetMagnitude = range.to - range.from;const pointInRange = targetMagnitude * pointMagnitude +range.from;return pointInRange
}

我们现在可以创建初始化函数和动画循环。 这是在 FacePointCloud 类的 initWork 方法中实现的,如下所示:

async initWork() {const { camera, scene, renderer } = this.setUpElementscamera.position.z = 3camera.position.y = 1camera.lookAt(0,0,0)const orbitControlsUpdate = this.threeSetUp.applyOrbitControls()const gridHelper = new THREE.GridHelper(10, 10)scene.add(gridHelper)scene.add(this.pointCloud.cloud)await this.faceMeshDetector.loadDetector()const animate = () => {requestAnimationFrame(animate)if (this.webcamCanvas.receivingStreem){this.bindFaceDataToPointCloud()}this.webcamCanvas.updateFromWebCam()orbitControlsUpdate()renderer.render(scene, camera)}animate()
}

我们可以看到这个 init 函数如何将所有内容联系在一起,它获取 Three.js 设置元素并设置相机,向场景和点云添加 gridHelper。

然后,它将检测器加载到faceLandMark 类上,并开始设置我们的动画函数。 在此动画函数中,我们首先检查 WebcamCanvas 元素是否正在接收来自网络摄像头的流,然后调用 bindFaceDataToPointCloud 方法,该方法在内部调用检测面部函数并将数据转换为 bufferAttribute 并更新点云位置属性。

现在,如果运行代码,应该在浏览器中得到以下结果!
在这里插入图片描述


原文链接:Three.js实时构建人脸点云 — BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/113573.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【实战】学习 Electron:构建跨平台桌面应用

文章目录 一、Electron 简介二、Electron 的优势1. 学习曲线平缓2. 丰富的生态系统3. 跨平台支持4. 开源和社区支持 三、Electron 的使用1. 安装 Node.js2. 安装 Electron3. 创建项目4. 初始化项目5. 安装依赖6. 创建主进程文件7. 创建渲染进程文件8. 打包应用程序9. 运行应用程…

在 Ubuntu 22.04安装配置 Ansible

一、按官网指引安装 我使用的ubuntu22.04版本&#xff0c;使用apt安装。官网指引如下&#xff1a; $ sudo apt-get install software-properties-common $ sudo apt-add-repository ppa:ansible/ansible $ sudo apt-get update $ sudo apt-get install ansible 由于内部网络…

36 机器学习(四):异常值检测|线性回归|逻辑回归|聚类算法|集成学习

文章目录 异常值检测箱线图z-score 保存模型 与 使用模型回归的性能评估线性回归正规方程的线性回归梯度下降的线性回归原理介绍L1 和 L2 正则化的介绍api介绍------LinearRegressionapi介绍------SGDRegressor 岭回归 和 Lasso 回归 逻辑回归基本使用原理介绍正向原理介绍损失…

Elasticsearch集群搭建与相关知识点整理

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章参考网上的课程&#xff0c;介绍Elasticsearch集群的搭建&#xff0c;以及Elasticsearch集群相关知识点整理。 如果文章有什么需要改进的地方还请大佬不吝赐教&am…

Git Cherry Pick的使用

cherry-pick命令的基本用法 cherry-pick命令的基本语法如下&#xff1a; git cherry-pick <commit>其中&#xff0c;<commit>是要应用的提交的哈希值或分支名。该命令会将指定的提交应用到当前分支上&#xff0c;并创建一个新的提交。 使用场景 cherry-pick命令…

【JavaEE】JUC 常见的类 -- 多线程篇(8)

JUC 常见的类 1. Callable 接口2. ReentrantLock3. 原子类4. 线程池5. 信号量 Semaphore6. CountDownLatch 1. Callable 接口 Callable Interface 也是一种创建线程的方式 Runnable 能表示一个任务 (run方法) – 返回 voidCallable 也能表示一个任务(call方法) 返回一个具体的…

ArcGIS笔记11_提取栅格中的数据到点要素

本文目录 前言Step 1 准备好点要素和栅格文件Step 2 多值提取到点 前言 很多时候需要将栅格中的数据提取到点要素&#xff0c;让点获取到栅格文件对应坐标所包含的数据&#xff0c;本博文主要介绍这个操作。 Step 1 准备好点要素和栅格文件 如下图所示&#xff1a; Step 2 多…

基于MATLAB的图像条形码识别系统(matlab毕毕业设计2)

摘要 &#xff1a; 本论文旨在介绍一种基于MATLAB的图像条形码识别系统。该系统利用计算机视觉技术和图像处理算法&#xff0c;实现对不同类型的条形码进行准确识别。本文将详细介绍系统学习的流程&#xff0c;并提供详细教案&#xff0c;以帮助读者理解和实施该系统。 引言…

02HTML功能元素

1.功能元素 1.1.列表标签 ​ 列表标签的作用: 给一堆数据添加列表语义, 也就是告诉搜索引擎告诉浏览器这一堆数据是一个整体 - HTML中列表标签的分类 ​ 无序列表(最多)(unordered list) ​ 有序列表(最少)(ordered list) ​ 定义列表(其次)(definition list) 1.1.1.无序列…

notepad++ 批量替换删除指定字符之后 或者 之前的字符,Notepad+批量替换使用大全

notepad 批量替换删除指定字符之后 或者 之前的字符&#xff0c;Notepad批量替换使用大全 资源宝分享&#xff1a;www.httple.net 注意: 不支持多行表达式 (involving \n, \r, etc). 1 基本表达式 符号解释.匹配任意字符&#xff0c;除了新一行(\n)。也就是说 “.”可以匹配 \…

uniapp map地图实现marker聚合点,并点击marker触发事件

1.uniapp官方文档说明 2.关键代码片段 // 仅调用初始化&#xff0c;才会触发 on.("markerClusterCreate", (e) > {})this._mapContext.initMarkerCluster({enableDefaultStyle: false, // 是否使用默认样式zoomOnClick: true, // 点击聚合的点&#xff0c;是否…

经典算法试题(二)

文章目录 一、岁数1、题目2、思路讲解3、代码实现4、结果 二、打碎的鸡蛋1、题目2、思路讲解3、代码实现4、结果 三、分糖1、题目2、思路讲解3、代码实现4、结果 四、兔子产子1、题目2、思路讲解3、代码实现4、结果 五、矩阵问题1、题目2、思路讲解3、代码实现4、结果 六、谁是…

计网----数据包在传输中的变化过程,单播组播和广播,ARP协议,ARP代理,免费ARP,DNS协议,路由数据转发过程

计网----数据包在传输中的变化过程&#xff0c;单播组播和广播&#xff0c;ARP协议&#xff0c;ARP代理&#xff0c;免费ARP&#xff0c;DNS协议&#xff0c;路由数据转发过程 一.数据包在传输中的变化过程&#xff08;在同一个路由器下&#xff09; 1.传输数据时&#xff0c…

Spring IOC之@ComponentScan

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

Redis基本命令和常用数据类型

文章目录 前言一、Redis简介二、基本操作1.赋值2.取值3.切换数据库4.查看数据库所有键&#xff08;key&#xff09;5.查看键值类型6.移动键值到其他数据库7.设置键值生存时间&#xff08;两种&#xff09;8.查看键值生存时间9.查看当前数据库大小10.判断键是否存在11.清空当前数…

洛谷 P1216 [USACO1.5] [IOI1994]数字三角形题解

观察题目我们发现从前往后推会有条件判断&#xff0c;不容易写出来。所以就从后往前推。 也就是说后面的状态已经是推出来了&#xff0c;保证是最大值。 //数字三角形 #include<iostream> using namespace std; const int N 510; int f[N][N], n;int main() {ios::sync…

转行做程序员,多晚都不晚

大家好啊&#xff0c;我是董董灿。 最近有不少小伙伴加我微信咨询一些问题&#xff0c;有同学想了解AI行业的现状&#xff0c;想着转行的&#xff0c;也有在校生想了解毕业后工作方向的&#xff0c;当然也有想学习编程知识的。 诚惶诚恐&#xff0c;没想到之前写的文章&#…

Go开始:Go基本元素介绍

标识符与关键字 在任何编程语言中&#xff0c;标识符和关键字都是核心概念&#xff0c;Go也不例外。标识符用于命名各种类型的代码元素&#xff0c;如变量、常量、函数等。关键字是预留的词汇&#xff0c;用于指示编程语言的特定操作。在本部分中&#xff0c;我们将详细介绍Go语…

如何使用BERT生成单词嵌入?

阿比贾特萨拉里 一、说明 BERT&#xff0c;或来自变形金刚&#xff08;Transformer&#xff09;的双向编码器表示&#xff0c;是由谷歌开发的强大语言模型。它已广泛用于自然语言处理任务&#xff0c;例如情感分析、文本分类和命名实体识别。BERT的主要特征之一是它能够生成单词…

Servlet的生命周期

2023.10.18 WEB容器创建的Servlet对象&#xff0c;这些Servlet对象都会被放到一个集合当中&#xff08;HashMap&#xff09;&#xff0c;这个集合当中存储了Servlet对象和请求路径之间的关系 。只有放到这个HashMap集合中的Servlet才能够被WEB容器管理&#xff0c;自己new的Ser…