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.rhkb.cn/news/167029.html

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

相关文章

从零实现FFmpeg6.0+ SDL2播放器

FFmpeg6.0开发环境搭建播放器代码框架分析解复用模块开发实现包队列和帧队列设计音视频解码线程实现SDL2音频声音输出SDL2视频画面渲染-YUV显示音视频同步-基于音频 地址: https://xxetb.xet.tech/s/3NWJGf

软件工程与计算总结(二十)软件交付

软件交付是软件项目的结束阶段 &#xff0c;标志着软件开发任务的完成——其作为一个分水岭&#xff0c;区分了软件开发与软件维护两个既连续又不同的软件产品生存状态~ 在经历连续的辛苦工作之后&#xff0c;开发人员在胜利曙光之前难免会忽视软件交付阶段的一些工作——在准…

[每周一更]-(第68期):Excel常用函数及常用操作

日常工作&#xff0c;偶尔也会存在excel表格入库的情况&#xff0c;针对复杂的入库情况&#xff0c;一般都是代码编号&#xff0c;读文件-写db形式&#xff1b;但是有些简单就直接操作&#xff0c;但是 这些简单的入库不仅仅是直接入库&#xff0c;而是内容中有部分需要进行映射…

Egg.js项目EJS模块引擎

1.介绍 灵活的视图渲染&#xff1a;使用 egg-view-ejs 插件&#xff0c;你可以轻松地在 Egg.js 项目中使用 EJS 模板引擎进行视图渲染。EJS 是一种简洁、灵活的模板语言&#xff0c;可以帮助你构建动态的 HTML 页面。 内置模板缓存&#xff1a;egg-view-ejs 插件内置了模板缓存…

【Java】ArrayList集合使用

ArrayList集合常见方法 方法名称说明public boolean add(E e)将元素插入到指定位置的arraylist中&#xff0c;返回值&#xff1a;返回boolean类型public E remove(int index)删除 arraylist里的单个元素&#xff0c;返回值&#xff1a;返回删除之前的元素public E set(int inde…

LeetCode:2316. 统计无向图中无法互相到达点对数(C++)

目录 2316. 统计无向图中无法互相到达点对数 题目描述&#xff1a; 实现代码与解析&#xff1a; 并查集 原理思路&#xff1a; 2316. 统计无向图中无法互相到达点对数 题目描述&#xff1a; 给你一个整数 n &#xff0c;表示一张 无向图 中有 n 个节点&#xff0c;编号为…

【已解决】Unity 使用NPOI 写word文档报错:System.TypeLoadException:……0.86.0.518

报错显示 System.TypeLoadException: Could not resolve type with token 01000080 from typeref (expected class ICSharpCode.SharpZipLib.Zip.UseZip64 in assembly ICSharpCode.SharpZipLib, Version0.86.0.518, Cultureneutral, PublicKeyToken1b03e6acf1164f73) at NPOI.…

三种字符串格式化方法(%、format、f-string)

一、使用 % name 第一帅 print(我是宇宙无敌天下%s % name) age 18 print(我是宇宙无敌天下%s&#xff0c;我今年%d岁%(name,age)) price 5.99print(白心火龙果单价是%.1f元一斤%price)二、使用 format 在字符串中&#xff0c;使用{ }进行占位&#xff0c;然后在字符串后…

【C语言】用函数实现模块化程序设计

前言&#xff1a;如果把所有的程序代码都写在一个主函数(main函数)中&#xff0c;就会使主函数变得庞杂、头绪不清&#xff0c;使阅读和维护程序变得困难。此外&#xff0c;有时程序中要多次实现某一功能&#xff0c;如果重新编写实现此功能就会使得程序冗长、不精炼。 &#x…

pensieve运行的经验

1运行run_videopy时出现如下问题&#xff1a; cmd: Union[List[str], str], ^ SyntaxError: invalid syntax原因是EasyProcess版本与python版本不对应&#xff0c;解决办法可见之前这篇博客&#xff1a;SyntaxError: invalid syntax。 2解决完上述问题后&#xff0c;输…

FreeSWITCH 1.10.10 简单图形化界面12 - 注册IMS

FreeSWITCH 1.10.10 简单图形化界面12 - 注册IMS 0、 界面预览1、IMS注册-SIP中继基本设置界面2、IMS注册-SIP中继呼叫设置3、IMS中继-代理设置界面4、IMS注册-SIP中继状态界面5、IMS注册-SIP中继详细状态界面6、IMS注册-SIP中继代拨号码优先界面 FreeSWITCH界面安装参考&#…

系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第五部分:支付系统

本心、输入输出、结果 文章目录 系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第五部分&#xff1a;支付系统前言如何学习支付系统信用卡为什么被称为“银行最赚钱的产品”&#xff1f;VISA/万事达卡如何赚钱&#xff1f;步骤说明为什么开证行应该得到补偿 当我们在商家…

万宾科技智能井盖传感器特点介绍

当谈论城市基础设施的管理和安全时&#xff0c;井盖通常不是第一项引人注目的话题。然而&#xff0c;传统井盖和智能井盖传感器之间的差异已经引起了城市规划者和工程师的广泛关注。这两种技术在功能、管理、安全和成本等多个方面存在着显著的差异。 WITBEE万宾智能井盖传感器E…

并发编程-线程池ThreadPoolExecutor底层原理分析(一)

问题&#xff1a; 线程池的核心线程数、最大线程数该如何设置&#xff1f; 线程池执行任务的具体流程是怎样的&#xff1f; 线程池的五种状态是如何流转的&#xff1f; 线程池中的线程是如何关闭的&#xff1f; 线程池为什么一定得是阻塞队列&#xff1f; 线程发生异常&…

优维低代码实践:片段

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

【vue】使用less报错:显示this.getOptions is not a function

在vue-cli中使用 lang“less” 时报错&#xff1a; Module build failed: TypeError: this.getOptions is not a function at Object.lessLoader 原因&#xff1a;版本过高所致&#xff0c;所用版本为 解决&#xff1a;降低版本&#xff1a;npm install less-loader4.1.0 --s…

STM32+摁键与定时器实现Led灯控制(中断)

中断作为单片机开发必须掌握的内容&#xff0c;它能够在不搭载操作系统的情况下让我们体验多任务处理的快感&#xff0c;保证了高优先级任务的实时性&#xff0c;同时系统中断也能够提供给用户在核心发生错误之后进行处理的机会。STM32F103系列单片机中断非常强大&#xff0c;每…

Linux中常见的权限问题

目录 前言1. 目录权限2. umask3. 粘滞位结语 前言 在了解完上一篇文章 Linux权限的理解与操作 之后&#xff0c;还有一些比较常见的权限问题需要我们去了解。其中包括目录的权限&#xff0c;umask 以及 粘滞位的使用。 1. 目录权限 问题一&#xff1a;进入一个目录&#xff0…

QT QGLWidge

QGLWidget 学习 前言1.四边形 QGLWidget 2*32. 正方体 1*2前言 1.四边形 QGLWidget 2*3 坐标 效果 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存glLoadIdentity(); //重置当前的模型观察矩阵glTranslate…

2001-2022年全国290+个地级市高铁开通数据

2001-2022年全国290个地级市高铁开通数据 1、时间&#xff1a;2001-2022年 2、范围&#xff1a;298地级市&#xff08;293地级市数&#xff08;其中莱芜市2019年撤市设区&#xff09;4直辖市数 &#xff09; 3、来源&#xff1a;国家铁路局、铁路客货运输专刊及相关统计 国…