【Three.js 与 AI】用 TensorFlow.js 给模型“开天眼”

0 阅读5分钟

前言

摄像头对着桌子,AI 说:这是手机,这是杯子。然后 Three.js 就在原地生成了它们的 3D 模型。

去年我做了一个互动展厅项目,客户提了个需求:参观者用手在摄像头前比划,就能操控大屏上的 3D 模型旋转、缩放。

我第一反应:这不得上 Kinect?或者用体感摄像头?成本直接起飞。

后来同事说:“试试 TensorFlow.js 呗,浏览器里就能跑手势识别。”

我半信半疑地打开官网,跑了个手部姿态检测的 demo——摄像头里我的手变成了一堆关键点,实时追踪,延迟居然只有几十毫秒。

那一刻我突然意识到:浏览器已经能“看见”世界了

从那以后,我就迷上了把 TensorFlow.js 和 Three.js 结合起来。今天就用一个真实项目里的例子,聊聊怎么让 Three.js 模型拥有“眼睛”。


一、项目场景:手势控制机械臂

需求很简单:摄像头识别手的位置和手势,控制一个 3D 机械臂模型。手指张开机械臂松开,握拳就抓紧,手移动机械臂跟着移动。

效果就像钢铁侠操控全息投影那样。


二、技术选型

  • TensorFlow.js:加载预训练的 HandPose 模型(手部 21 个关键点检测)
  • Three.js:渲染机械臂模型,并实时更新它的位置和关节旋转
  • WebRTC:获取摄像头视频流

HandPose 模型输出每个关键点的 (x, y, z) 坐标,z 是深度(距离摄像头的远近)。有了这些点,就能算出手掌中心位置、手指弯曲程度。


三、一步步实现

1. 初始化摄像头和 TensorFlow.js 模型

<video id="video" style="display: none;" width="640" height="480" autoplay playsinline></video>
<canvas id="output" style="display: none;"></canvas>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/hand-pose-detection"></script>
const video = document.getElementById('video');
const camera = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = camera;
await video.play();

// 加载手部检测模型(使用 MediaPipe 的模型,精度较高)
const model = handPoseDetection.SupportedModels.MediaPipeHands;
const detector = await handPoseDetection.createDetector(model, {
  runtime: 'tfjs',
  maxHands: 1
});

2. 在 Three.js 中创建一个简易机械臂

为了演示,我用几个立方体拼一个简单的机械臂:基座、大臂、小臂、夹爪。

const scene = new THREE.Scene();
const camera3D = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera3D.position.set(2, 2, 5);
camera3D.lookAt(0, 1, 0);

// 基座
const base = new THREE.Mesh(new THREE.BoxGeometry(1, 0.3, 1), new THREE.MeshStandardMaterial({ color: 0x44aa88 }));
base.position.y = 0.15;
scene.add(base);

// 大臂
const arm1 = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.5, 0.4), new THREE.MeshStandardMaterial({ color: 0xff8844 }));
arm1.position.set(0, 1.0, 0);
base.add(arm1); // 让大臂作为基座的子元素,方便整体移动

// 小臂
const arm2 = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.2, 0.3), new THREE.MeshStandardMaterial({ color: 0x44aaff }));
arm2.position.set(0, 1.4, 0);
arm1.add(arm2);

// 夹爪(两个小方块)
const claw1 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.6), new THREE.MeshStandardMaterial({ color: 0xdddddd }));
claw1.position.set(0.3, 0.2, 0);
arm2.add(claw1);
const claw2 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.6), new THREE.MeshStandardMaterial({ color: 0xdddddd }));
claw2.position.set(-0.3, 0.2, 0);
arm2.add(claw2);

// 添加光照
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(2, 5, 3);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404060));

3. 检测循环:将手部数据映射到模型

每帧从 TensorFlow.js 获取手部关键点,然后计算手掌中心位置和手指开合度。

async function detectHand() {
  const hands = await detector.estimateHands(video);
  if (hands.length > 0) {
    const hand = hands[0];
    const keypoints = hand.keypoints; // 21 个关键点

    // 计算手掌中心(取手腕和手掌根部的中部)
    const wrist = keypoints[0];
    const palmBase = keypoints[9];
    const palmX = (wrist.x + palmBase.x) / 2;
    const palmY = (wrist.y + palmBase.y) / 2;
    const palmZ = (wrist.z + palmBase.z) / 2;

    // 将屏幕坐标映射到 Three.js 世界坐标(需要适当缩放和平移)
    // 这里简单映射:x: -2~2, y: 0~2, z: 0~2
    const worldX = (palmX / video.videoWidth - 0.5) * 4;
    const worldY = 2 - (palmY / video.videoHeight) * 2; // 翻转 Y
    const worldZ = palmZ * 2; // 深度直接映射

    // 移动机械臂基座
    base.position.set(worldX, worldY, worldZ);

    // 计算手指开合度:用拇指尖和食指尖距离
    const thumbTip = keypoints[4];
    const indexTip = keypoints[8];
    const dist = Math.hypot(thumbTip.x - indexTip.x, thumbTip.y - indexTip.y, thumbTip.z - indexTip.z);
    
    // 开合度映射到夹爪张开角度(这里简单控制夹爪间距)
    const openRatio = Math.min(dist / 100, 1); // 假设最大距离100px对应完全张开
    claw1.position.x = 0.3 + openRatio * 0.2;
    claw2.position.x = -0.3 - openRatio * 0.2;
  }

  requestAnimationFrame(detectHand);
}
detectHand();

4. 渲染循环

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera3D);
}
animate();

四、效果与优化

跑起来后,摄像头里的手怎么动,屏幕上的机械臂就怎么动。张开手指夹爪张开,握拳夹爪闭合。虽然只用了几十行代码,但效果非常科幻。

当然这只是个 demo,实际项目里需要处理更多细节:

  • 平滑滤波:手部检测会有抖动,可以用移动平均让模型运动更顺滑。
  • 坐标映射:需要根据摄像头视野和场景大小精确换算,避免手跑出画面模型就飞了。
  • 多手势识别:可以训练简单的分类器,识别更多手势(比如比心、点赞),触发不同动画。

五、更多玩法:物体识别 + 3D 生成

手势控制只是开胃菜。另一个我特别喜欢的组合是:用 TensorFlow.js 的 COCO-SSD 模型识别摄像头里的物体,然后在 Three.js 场景里对应位置生成该物体的 3D 模型。

比如摄像头扫到一本书,场景里就出现一本书的模型;扫到手机,就出现手机模型。这简直就是AR 的浏览器实现

核心代码片段:

import * as cocoSsd from '@tensorflow-models/coco-ssd';

const model = await cocoSsd.load();
const predictions = await model.detect(video);

predictions.forEach(pred => {
  const [x, y, w, h] = pred.bbox;
  // 在 Three.js 中创建一个方块,位置对应 bounding box
  const box = new THREE.Mesh(new THREE.BoxGeometry(w/100, h/100, 0.1), new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }));
  box.position.set((x - video.videoWidth/2) / 200, -(y - video.videoHeight/2) / 200, 0);
  scene.add(box);
});

虽然简单,但已经能看出“开天眼”的雏形了。


六、踩坑记录

  1. 模型加载慢:TensorFlow.js 模型文件很大(几 MB 到几十 MB),首次加载有延迟。可以用 IndexedDB 缓存,或者用更轻量的模型(如 MoveNet)。
  2. 性能:同时跑摄像头、TensorFlow.js 推理和 Three.js 渲染,对手机压力较大。需要限制推理帧率(比如每秒 10 次),或者用 Web Worker 跑推理。
  3. 坐标系转换:摄像头图像坐标和 3D 世界坐标的对应关系需要反复调试。可以用辅助线在场景里画出摄像头视野范围,方便校准。

七、总结

TensorFlow.js + Three.js = 浏览器里的“天眼”。

以前需要专业硬件和本地应用才能实现的手势控制、物体识别,现在一个网页就能搞定。虽然精度和实时性还不能和专业设备比,但对于互动展示、创意编程、教学演示来说,已经足够惊艳。

如果你也想让你的 3D 模型“看见”世界,不妨从今天的手势控制开始试试。


互动

你用 TensorFlow.js 和 Three.js 结合做过什么好玩的东西?评论区晒出来,让我抄抄作业 😏

下篇预告:【Three.js 项目复盘】一个智慧工厂监控大屏的踩坑实录