JeelizFaceFilter 源码解读:纯 WebGL 跑神经网络的实时人脸追踪库
AR 人脸滤镜——给脸戴上虚拟眼镜、面具、动态妆容——背后都需要一个能力:实时知道脸在画面里的哪个位置、朝向如何。传统做法要么把视频流传到服务端跑模型(有延迟、有隐私顾虑、有带宽成本),要么依赖体积庞大的 WASM/原生 SDK。
JeelizFaceFilter 给出了另一种答案:把神经网络推理直接编译进 WebGL 着色器,跑在用户的 GPU 上。整个库~112KB、零第三方依赖、100% 客户端推理,视频帧从摄像头到检测结果全程不出浏览器。
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 脚手架示例 |
整个系统可以分成五层:底层是 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 对象。它把"脸在哪、多大、怎么转、表情如何"压缩成一组归一化数值:
| 字段 | 含义 |
|---|---|
detected | 检测置信度,0~1,常用 > 0.8 作为"检测到"的阈值 |
x / y | 人脸中心在视口中的归一化坐标 |
s | 检测窗口的相对尺寸(越大表示脸越近) |
rx / ry / rz | 头部绕三轴的旋转角(弧度) |
expressions | 表情系数数组,如 expressions[0] 为张嘴程度(0 闭合 → 1 全开) |
有一个容易踩坑的设计细节,README 专门强调过:这个对象的引用在每帧之间不变,是出于内存优化考虑而复用的。如果你想把某一帧的值留到回调之外使用,必须拷贝属性值,而不能持有对象引用,否则下一帧的数据会覆盖它。
多脸模式下,detectState 变成一个数组,长度等于最大检测人脸数,每个元素都是上面这个结构。
库只输出 2D 视口里的归一化坐标(x/y/s)和旋转角,那么如何把一个 3D 物体(比如一副眼镜)准确"贴"到脸上?答案在 JeelizThreeHelper.js 的 render() 里——它用相机的视场角(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.js 的 render()(已精简注释)
这段代码是"引擎适配层"的精华:库负责检测,helper 负责把检测结果翻译成具体引擎(这里是 THREE.js)能理解的 position / rotation。换一个引擎,只需重写这层翻译,库本身一行不动。
库的招牌技术是 Jeeliz 自研的 WebGL Deep Learning:神经网络不是用 WASM 或 CPU 跑,而是把权重和前向计算编译成 WebGL 着色器,借助 GPU 的并行能力推理。这也是它对 WebGL 能力有硬性要求的原因:
- 有
WebGL2时直接用,无需额外扩展; - 只有
WebGL1时,必须支持OES_TEXTURE_FLOAT或OES_TEXTURE_HALF_FLOAT扩展(浮点纹理用于承载中间计算); - 两者都不满足,则判定设备不兼容,回调返回
GL_INCOMPATIBLE。
神经网络的原始输出是有噪声的——直接用会导致 3D 内容抖动。库内置了一套稳定化机制:先算一个稳定因子 k(0~1),k 接近 0 表示检测质量差或用户在快速运动,此时偏向响应速度;k 接近 1 表示检测稳定、用户基本不动,此时偏向平滑。两次检测之间按 k 推出的混合系数 alpha 做浮动平均(默认 alphaRange = [0.05, 1.0]),在"跟手"和"不抖"之间动态权衡。
优点
- 极致轻量: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