ESC
输入关键词搜索文章
目录

JeelizFaceFilter 源码解读:纯 WebGL 跑神经网络的实时人脸追踪库

~112KB核心库体积
0第三方依赖
8最多同时追踪人脸
100%客户端推理
背景与动机
为什么需要一个"纯前端"的人脸追踪库

AR 人脸滤镜——给脸戴上虚拟眼镜、面具、动态妆容——背后都需要一个能力:实时知道脸在画面里的哪个位置、朝向如何。传统做法要么把视频流传到服务端跑模型(有延迟、有隐私顾虑、有带宽成本),要么依赖体积庞大的 WASM/原生 SDK。

JeelizFaceFilter 给出了另一种答案:把神经网络推理直接编译进 WebGL 着色器,跑在用户的 GPU 上。整个库 ~112KB零第三方依赖100% 客户端推理,视频帧从摄像头到检测结果全程不出浏览器。
JeelizFaceFilter 实时人脸追踪演示
官方演示:库实时输出人脸的位置、缩放与三轴旋转,3D 内容据此叠加到脸上(图片来源:项目 README)
项目概览
一句话定位与核心卖点

JeelizFaceFilter 是一个 JavaScript/WebGL 的轻量实时人脸检测与追踪库,专为增强现实人脸滤镜设计。它从 WebRTC 摄像头视频流中实时检测人脸,输出可直接驱动 3D 引擎的姿态参数。

它的核心特性可以归纳为:

  • WebGL Deep Learning:神经网络推理跑在 GPU 上,精度自适应——硬件越好,每秒处理的检测越多。
  • 多脸追踪:最多同时检测并追踪 8 张人脸。
  • 完整姿态输出:位置、缩放、三轴旋转(rx/ry/rz),外加表情系数(如张嘴程度)。
  • 引擎无关:通过 helper 适配层对接 THREE.js、BABYLON.js、A-FRAME,也支持 Canvas2D、CSS3D 这类更轻量的渲染方式。
  • 鲁棒性:对各种光照条件稳定,支持 HD 视频,移动端友好。

仓库地图

仓库结构清晰,按"核心库 / 适配层 / 模型 / 演示"四块组织:

目录作用
/dist/核心库脚本,jeelizFaceFilter.js(112KB 压缩版)及其 module 变体
/neuralNets/训练好的神经网络模型,NN_DEFAULT.json 为默认,另有多个表情/场景特化模型
/helpers/对接各 3D 引擎的适配器,如 JeelizThreeHelper.js
/demos/按 2D/3D 引擎分类的示例代码
/libs/演示用的第三方 3D 引擎(库本身不依赖)
/reactThreeFiberDemo/React/Vite/Three-Fiber 脚手架示例
架构与设计
从摄像头到 3D 内容的分层结构

整个系统可以分成五层:底层是 GPU 上的神经网络推理,往上是统一的数据契约 detectState,再往上是各引擎的适配层,最终落到具体的 3D 渲染。库本身只负责前两层,适配与渲染交给 helper 和宿主应用。

graph TD
    Cam["摄像头 (WebRTC)"] --> Video["videoTexture
WebGL 视频纹理"] Video --> NN["神经网络推理
(WebGL Shader, GPU)"] NN --> State["detectState
detected / x / y / s / rx / ry / rz / expressions"] State --> Helper["引擎适配层
JeelizThreeHelper / Babylon / Canvas2D"] Helper --> Engine["3D 引擎
THREE.Object3D 等"] Engine --> Scene["AR 场景渲染"] style NN fill:#e3f2fd style State fill:#fff3e0 style Helper fill:#e8f5e9

主调用链:一次检测如何流动

使用方只需调用 JEELIZFACEFILTER.init() 并传入回调。初始化完成后,库进入渲染循环,每一帧都把最新的 detectState 通过 callbackTrack 交还给应用:

sequenceDiagram
    participant App as 应用代码
    participant FF as JEELIZFACEFILTER
    participant GPU as WebGL / GPU
    participant Helper as ThreeHelper

    App->>FF: init({canvasId, NNCPath,
callbackReady, callbackTrack}) FF->>GPU: 加载 NN_DEFAULT.json
编译着色器 FF-->>App: callbackReady(errCode, spec) loop 每一渲染帧 GPU->>GPU: 视频纹理 → 神经网络推理 GPU-->>FF: 原始检测结果 FF->>FF: 稳定化 (floating average) FF-->>App: callbackTrack(detectState) App->>Helper: detect(detectState) Helper->>Helper: 映射到 Object3D
position/rotation/scale end

最简上手:30 行跑通人脸框

官方 Canvas2D demo 是理解 API 的最佳起点——它不引入任何 3D 引擎,只在脸周围画一个黄色边框:

JEELIZFACEFILTER.init({
  canvasId: 'jeeFaceFilterCanvas',
  NNCPath: '../../../neuralNets/',  // NN_DEFAULT.json 所在目录
  callbackReady: function(errCode, spec){
    if (errCode){ console.log('ERROR =', errCode); return; }
    CVD = JeelizCanvas2DHelper(spec);
    CVD.ctx.strokeStyle = 'yellow';
  },
  // 每一渲染帧都会被调用:
  callbackTrack: function(detectState){
    if (detectState.detected > 0.8){          // 置信度阈值
      const faceCoo = CVD.getCoordinates(detectState);
      CVD.ctx.clearRect(0, 0, CVD.canvas.width, CVD.canvas.height);
      CVD.ctx.strokeRect(faceCoo.x, faceCoo.y, faceCoo.w, faceCoo.h);
      CVD.update_canvasTexture();
    }
    CVD.draw();
  }
});

来源:demos/canvas2D/faceTrack/main.js

关键实现 · 设计点一
detectState:稳定的数据契约

整个库与外界沟通的唯一接口,就是回调里那个 detectState 对象。它把"脸在哪、多大、怎么转、表情如何"压缩成一组归一化数值:

字段含义
detected检测置信度,0~1,常用 > 0.8 作为"检测到"的阈值
x / y人脸中心在视口中的归一化坐标
s检测窗口的相对尺寸(越大表示脸越近)
rx / ry / rz头部绕三轴的旋转角(弧度)
expressions表情系数数组,如 expressions[0] 为张嘴程度(0 闭合 → 1 全开)

有一个容易踩坑的设计细节,README 专门强调过:这个对象的引用在每帧之间不变,是出于内存优化考虑而复用的。如果你想把某一帧的值留到回调之外使用,必须拷贝属性值,而不能持有对象引用,否则下一帧的数据会覆盖它。

多脸模式下,detectState 变成一个数组,长度等于最大检测人脸数,每个元素都是上面这个结构。

关键实现 · 设计点二
从 2D 检测窗口反算 3D 位姿

库只输出 2D 视口里的归一化坐标(x/y/s)和旋转角,那么如何把一个 3D 物体(比如一副眼镜)准确"贴"到脸上?答案在 JeelizThreeHelper.jsrender() 里——它用相机的视场角(FoV)做反投影,把 2D 检测窗口换算成视图坐标系下的 3D 位置。

核心思路是:检测窗口的尺寸 s 反映了脸到相机的距离——窗口越大、脸越近、深度越小。结合相机 FoV 就能解出深度 D,再用 x/y 投影出横纵坐标:

// tan(水平 FoV / 2)
const halfTanFOVX = Math.tan(camera.aspect * camera.fov * Math.PI/360);

// 检测窗口相对宽度
const W = detectState.s * _scaleW;
// 立方体前表面到相机的距离(窗口越大 → 距离越近)
const DFront = 1 / (2 * W * halfTanFOVX);
const D = DFront + 0.5;  // 单位立方体中心到相机的距离

// 由归一化 x/y 反投影出视图坐标系下的 3D 坐标
const z = -D;  // 视图坐标系 Z 朝后,故取负
const x = detectState.x * _scaleW * D * halfTanFOVX;
const y = detectState.y * _scaleW * D * halfTanFOVX / _canvasAspectRatio;

// 三轴旋转:注意欧拉顺序为 "ZYX"
threeCompositeObject.rotation.set(
  detectState.rx + _settings.rotationOffsetX,
  detectState.ry,
  detectState.rz, "ZYX");
threeCompositeObject.position.applyEuler(threeCompositeObject.rotation);
_threeTranslation.set(x, y + pivotOffsetY, z + pivotOffsetZ);
threeCompositeObject.position.add(_threeTranslation);

来源:helpers/JeelizThreeHelper.jsrender()(已精简注释)

这段代码是"引擎适配层"的精华:库负责检测,helper 负责把检测结果翻译成具体引擎(这里是 THREE.js)能理解的 position / rotation。换一个引擎,只需重写这层翻译,库本身一行不动。

关键实现 · 设计点三
WebGL Deep Learning 与抗抖动稳定化

库的招牌技术是 Jeeliz 自研的 WebGL Deep Learning:神经网络不是用 WASM 或 CPU 跑,而是把权重和前向计算编译成 WebGL 着色器,借助 GPU 的并行能力推理。这也是它对 WebGL 能力有硬性要求的原因:

  • WebGL2 时直接用,无需额外扩展;
  • 只有 WebGL1 时,必须支持 OES_TEXTURE_FLOATOES_TEXTURE_HALF_FLOAT 扩展(浮点纹理用于承载中间计算);
  • 两者都不满足,则判定设备不兼容,回调返回 GL_INCOMPATIBLE

神经网络的原始输出是有噪声的——直接用会导致 3D 内容抖动。库内置了一套稳定化机制:先算一个稳定因子 k(0~1),k 接近 0 表示检测质量差或用户在快速运动,此时偏向响应速度k 接近 1 表示检测稳定、用户基本不动,此时偏向平滑。两次检测之间按 k 推出的混合系数 alpha 做浮动平均(默认 alphaRange = [0.05, 1.0]),在"跟手"和"不抖"之间动态权衡。

设计取舍:把"响应 vs 稳定"做成一个随检测质量自动滑动的连续参数,而不是固定平滑系数,是这类实时追踪库的关键工程经验——用户快速转头时不能拖影,静止时又不能抖动。
工程评价
优点、局限与可迁移经验

优点

  • 极致轻量:112KB、零依赖,对加载性能和隐私都友好,视频帧不出浏览器。
  • 清晰的分层:核心检测库与引擎适配层彻底解耦,detectState 作为稳定契约,新增引擎成本极低。
  • API 极简:一个 init() 加两个回调即可跑通,上手门槛低。

局限与复现风险

  • 依赖 WebGL 浮点纹理:老旧设备或受限的 WebGL1 环境可能直接不兼容。
  • 模型黑盒:神经网络以 JSON 权重形式提供,库聚焦推理,训练/精度细节不开放。
  • 对象引用复用detectState 引用不变这一优化,对不熟悉的使用者是隐蔽的陷阱。

可迁移经验

  • 用一个稳定的数据契约解耦上下游detectState 让"检测"和"渲染"互不感知,是适配多引擎的关键。
  • 把"响应 vs 稳定"参数化:随质量自适应的浮动平均,适用于任何实时信号驱动 UI 的场景。
  • 把重计算下沉到 GPU:当目标平台是浏览器、又要避免服务端往返时,WebGL 着色器是值得考虑的推理载体。

参考来源

  • JeelizFaceFilter GitHub 仓库:github.com/jeeliz/jeelizFaceFilter
  • 项目 README:Features / Architecture / Specifications / Under the hood
  • 引擎适配器源码:helpers/JeelizThreeHelper.js
  • 最简示例:demos/canvas2D/faceTrack/main.js