
简介这是一套基于face-api.js实现的人脸采集前端项目源码适合Web前端开发者和计算机视觉入门者学习可在浏览器中快速搭建实时人脸检测与采集流程。项目采用JavaScript编写目录结构清晰覆盖从摄像头视频流获取、逐帧人脸检测、特征点定位到图像质量评分与采集存储的完整链路便于二次开发和功能扩展。资源包共28个文件包含JS逻辑代码、JSON工程配置、HTML页面、说明文档以及tiny face detector、ssd mobilenetv1、face landmark、face recognition等预训练模型文件压缩包整体约9.94MB。目前已有257人浏览学习适合作为人脸识别应用开发的起点工程。通过阅读源码可掌握face-api.js在浏览器端的实际调用方式理解模型加载与推理流程并参考其模块化结构快速改造出满足自身需求的人脸采集工具项目同时提醒开发者关注人脸数据采集过程中的隐私授权与安全合规问题。1. 基于 face-api.js 的人脸采集为什么值得自己拆一套在浏览器里做人脸采集很多人第一反应是调后端接口、传视频流再等结果回来。实际上 face-api.js 已经把 TensorFlow.js 的模型压缩到了浏览器端可跑的体量TinyFaceDetector 的模型文件只有 190KB 左右配合 WebGL 加速一帧检测耗时能压到几十毫秒。这意味着摄像头画面可以在本地完成人脸框定、关键点定位、质量评分再决定要传输哪一帧而不是把整段视频流丢到服务器。这套基于 JavaScript 的源码项目正好把这一条链路完整串了起来从摄像头视频流接入到人脸检测与特征点定位再到评分筛选与图像采集最后落到一套 webpack 工程化的前端代码结构里。对于想快速搭起一个前端人脸采集模块的开发者它比从零用底层 TensorFlow.js 写要省事得多同时能通过源码看到前后端分配任务的边界在哪。接下来就按运行链路拆开讲哪些参数是决定效果的关键哪些地方必须自己改。2. face-api.js 检测管线与模型加载TinyFaceDetector 和 FaceLandmark68Net 的选型2.1 先检测人脸框再定位关键点face-api.js 内部的检测逻辑是一套组合管线先用一个目标检测网络找出画面里的人脸边界框再在框内运行关键点回归模型输出 68 个面部特征点。这套流程被封装在detectAllFaces这类方法里你实际调用时只需要三步加载模型、构造检测选项、执行检测。项目中的src/face-detection.js就是这一层的核心封装。它承担了模型加载和统一入口的职责避免业务代码直接散落着 face-api.js 的调用。我习惯把模型加载做成异步初始化避免页面一进来就卡在模型下载上。常见做法是这样import * as faceapi from face-api.js; const MODEL_PATH /models; export async function loadFaceModels() { await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_PATH); await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_PATH); return true; }这里选择tinyFaceDetector是有明确理由的face-api.js 还提供ssdMobilenetv1作为检测器后者精度更高但对移动端和低端笔记本很不友好模型文件更大推理时间也翻倍。在实时视频流里检测器必须在一秒内跑至少 10 帧否则画面会明显迟滞。因此这个项目选 TinyFaceDetector 是正确的方向后面的faceLandmark68Net则负责输出 68 个关键点供后续的眼睛状态判断、人脸角度评估使用。loadFromUri这个函数接收的是模型文件的相对路径它会把整个目录下的权重和 manifest 文件并行加载。路径写错最常见的报错是 404 或者failed to fetch如果你在控制台看到这类 JavaScript 运行时报错先检查模型文件是否真的被打包到静态目录而不是纠结代码逻辑。2.2 检测选项里的 inputSize 与 scoreThreshold有了模型还需要告诉 face-api.js 用什么尺度去扫描人脸。这个项目里通常会在index.js中构造一个TinyFaceDetectorOptions它直接决定检测的召回率和速度。const detectorOptions new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.5 });inputSize是输入网络的图像尺寸。320 的意思是把视频帧缩放到 320x320 再做检测。这个值越大小脸越容易被找到但计算量也线性上升。对于距离摄像头 0.5 米到 1 米左右的采集场景320 是一个安全起点如果你发现人离得远就检测不到先增加到 416而不是先去调阈值。scoreThreshold是置信度阈值默认 0.5 表示只有模型认为概率超过 50% 的框才算人脸。实际项目中我一般设 0.4 到 0.6 之间太低会引入背景噪声太高则容易漏掉侧脸。这两组参数在项目源码里都是常量二次开发时建议放到一个独立配置文件里方便不同采集距离的场景切换。比如自助采集终端用 416 0.4而普通笔记本摄像头用 320 0.5。2.3 模型加载状态与前端工程集成模型加载是异步的如果直接调用检测方法face-api.js 会抛错。因此项目里你会看到一个isModelLoaded这样的状态标志在loadFaceModels完成后才打开摄像头。这一步很重要但很多初学者会忽略。另一个常见坑是模型文件的部署路径。在 webpack 工程里loadFromUri(/models)会从服务器根目录找模型本地开发时需要用 dev server 的静态目录映射生产环境则要在 Nginx 里配置location /models或者用copy-webpack-plugin把models目录直接复制到dist下。项目源码中public或assets目录通常会放这些文件。要注意face-api.js 与 TensorFlow.js 的版本必须一致否则会出现模型层结构不匹配的运行时报错这种情况在升级 JavaScript 依赖时经常遇到。3. 摄像头视频流接入与 requestAnimationFrame 抽帧检测3.1 getUserMedia 的约束与前后置摄像头人脸采集的第一步是拿到摄像头画面。浏览器端的实现统一走navigator.mediaDevices.getUserMedia这个接口在 HTTPS 或 localhost 下才能调用。项目中index.js里会有类似这样的代码const constraints { audio: false, video: { facingMode: user, width: { ideal: 640 }, height: { ideal: 480 } } }; const stream await navigator.mediaDevices.getUserMedia(constraints); video.srcObject stream;facingMode: user指定前置摄像头environment则是后置。这里的宽高用ideal而不是exact是因为摄像头硬件参数是有限的exact会直接导致OverconstrainedError。640x480 是一个折中方案分辨率太低细节不足太高则让检测循环压力变大。在采集人脸用于后续特征提取时我更倾向于 1280x720但检测框的坐标和绘制尺寸也要同比放大。3.2 检测循环用 rAF 而不是 setInterval实时检测的核心是一个持续运行的循环。很多人会用setInterval(fn, 100)来控制帧率但这样有两个问题一是setInterval不保证在每帧的绘制间隙执行可能跟浏览器的渲染任务抢线程二是检测是异步的上一轮还没结束下一轮又开始了容易产生重入。项目的做法是使用requestAnimationFrame配合一个锁标志来避免并发检测let isDetecting false; async function detectFrame() { if (isDetecting) { requestAnimationFrame(detectFrame); return; } isDetecting true; const detection await faceapi .detectAllFaces(video, detectorOptions) .withFaceLandmarks(); renderResults(detection); isDetecting false; requestAnimationFrame(detectFrame); }这段代码有几个关键点。isDetecting标志位是必要的因为detectAllFaces返回 Promise在等待期间 rAF 可能又触发一次如果不加锁模型会同时跑多个推理WebGL 上下文很容易卡死。requestAnimationFrame(detectFrame)放在回调末尾保证上一轮结束才调度下一轮这样实际帧率会受检测耗时影响。如果检测耗时 40ms那么实际就 25fps如果耗时 100ms就只有 10fps。这里不需要刻意去控制检测本身已经是瓶颈。3.3 检测结果的坐标对齐与画布绘制检测结果里的坐标是相对于输入网络尺寸的而不是 video 元素的实际显示尺寸。face-api.js 提供了两组工具函数来对齐faceapi.matchDimensions和faceapi.resizeResults。const displaySize { width: video.width, height: video.height }; faceapi.matchDimensions(canvas, displaySize); const resizedDetections faceapi.resizeResults(detection, displaySize); faceapi.draw.drawDetections(canvas, resizedDetections); faceapi.draw.drawFaceLandmarks(canvas, resizedDetections);matchDimensions会把 canvas 的宽高设置成和 video 一致而resizeResults把检测框和关键点坐标从输入尺寸映射回显示尺寸。如果跳过这一步画出来的人脸框会明显偏移。这个偏移量不是常数和inputSize与视频宽高的比例有关只能通过 resize 解决。在项目源码中face-detection.js会同时返回原始检测结果和绘制结果这样业务层可以拿原始坐标去截取人脸图而 UI 层只用绘制结果。4. 人脸质量评分与图像采集清晰度、尺寸、亮度的筛选逻辑4.1 为什么不能直接截帧保存很多初版实现都是检测到人脸就立刻截取当前帧然后上传。实际效果往往惨不忍睹模糊、闭眼、人脸在画面中占比过小、曝光不足或过曝。这种数据喂给后续的特征提取模型识别率会大幅下降。因此这个项目做了一个非常关键的前置步骤——对人脸区域做质量评分只有达到阈值才采集。评分通常从三个维度综合计算人脸尺寸、图像清晰度和亮度。人脸尺寸可以从前一步的检测框拿到清晰度和亮度则需要对人脸区域的像素做计算。function calculateScore(detection, canvas) { const box detection.box; const sizeScore Math.min(box.width / 200, 1); const faceRegion extractFaceRegion(canvas, box); const sharpnessScore calculateLaplacianVariance(faceRegion); const brightnessScore calculateBrightnessScore(faceRegion); return { sizeScore, sharpnessScore, brightnessScore, total: (sizeScore sharpnessScore brightnessScore) / 3 }; }extractFaceRegion是从画布上把人脸区域裁剪成小图然后转成灰度数据。calculateLaplacianVariance是常见的清晰度衡量方法计算拉普拉斯算子的方差方差越大表示边缘越锐利人眼看起来更清晰。这个值是经验范围不同分辨率下差异很大所以项目里一般会先跑十几个样本定一个合理的下限。calculateBrightnessScore则把人脸区域像素的平均亮度映射到 0 到 1过暗或过亮都会拉低分数。4.2 采集触发条件与数据存储满足质量要求后采集模块会从 video 或离屏 canvas 中截取人脸区域而不是整帧。这样做的好处是减少存储体积也避免背景干扰。async function captureFace(detection) { const { box } detection; const captureCanvas document.createElement(canvas); captureCanvas.width Math.floor(box.width); captureCanvas.height Math.floor(box.height); const ctx captureCanvas.getContext(2d); ctx.drawImage( video, Math.floor(box.x), Math.floor(box.y), Math.floor(box.width), Math.floor(box.height), 0, 0, captureCanvas.width, captureCanvas.height ); const dataUrl captureCanvas.toDataURL(image/jpeg, 0.92); return dataUrl; }这个函数将检测框区域从视频帧中裁剪出来转成 JPEG base64。JPEG 质量 0.92 是一个平衡点过高会明显增大体积过低则可能影响后续人脸特征提取。常见做法是基 64 字符串直接 POST 到后端或者转成 Blob 通过 FormData 上传。下表列出了两种上传方式的差异方式体积开销适合场景注意点base64 字符串比原图增加约 33%接口简单调试方便适合少量采集后端需要处理字符串解码Blob FormData与原图接近批量上传、移动端流量敏感需要显式设置文件类型和文件名我在项目里更推荐直接使用 Blob尤其当采集数量超过几十张时。如果你在 JavaScript 里合并两个对象时不小心把 dataUrl 当成普通字段存储内存占用会很快翻倍。4.3 用关键点做闭眼与遮挡检查质量评分解决的是图像清晰度但闭眼和遮挡是另一个维度。face-api.js 的 68 个关键点里左右眼分别有 6 个点可以通过计算眼睛纵横比EAR判断眼睛是否睁开。常见做法是这样的function getEyeOpenRatio(landmarks) { const leftEye landmarks.getLeftEye(); const rightEye landmarks.getRightEye(); const leftEAR faceapi.utils.eyeOpenRatio(leftEye); const rightEAR faceapi.utils.eyeOpenRatio(rightEye); return (leftEAR rightEAR) / 2; }如果getEyeOpenRatio小于 0.2大概率是闭眼状态此时即使图像再清晰也不能采集。遮挡检查会更复杂比如手遮住半张脸时关键点的置信度会下降但 face-api.js 没有直接给出置信度一个可用技巧是计算左右眼、鼻尖、嘴部关键点之间距离的比例如果明显偏离正常范围说明脸被侧向遮挡或者畸变严重。这个逻辑可以作为评分项的第四个维度代码不复杂但对最终数据质量提升很大。5. webpack 构建配置与二次开发模型托管、报错排查和扩展点5.1 项目结构与 webpack 基础配置这个项目是标准的 webpack 工程入口是src/index.js核心逻辑在src/face-detection.js配置拆分成了webpack.base.js、webpack.dev.js、webpack.prod.js另外还有 Babel、ESLint、PostCSS 这些常规配置。如果你要改造重点看webpack.base.js里的 loader 和插件。// webpack.base.js (简化) module.exports { entry: ./src/index.js, output: { filename: bundle.js, path: path.resolve(__dirname, dist) }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: babel-loader } ] } };这里把 JavaScript 文件交给 Babel 转译是为了让不支持 ES6 的旧浏览器也能运行。注意exclude: /node_modules/必须保留否则 Babel 会尝试转译 TensorFlow.js 这种大型依赖构建时间会变得非常长而且容易出现各种奇怪的报错。如果你在构建时遇到javascript 基础语法相关的解析错误先确认是不是把 node_modules 排除干净了。5.2 模型文件部署与运行时排查在 dev 环境下模型文件通常放在public/models或assets/modelswebpack 会通过copy-webpack-plugin把它复制到 dev server 和构建产物中。如果模型漏配置浏览器控制台会先报 404随后报failed to load module script或could not load model manifest这类错误。一个非常实用的排查方法在loadFaceModels里加一个显式的fetch检查。先请求 manifest 文件如果状态码不是 200就直接抛错并打印可读信息。这样能比 face-api.js 内部的报错更快定位问题。另外TensorFlow.js 在部分移动端浏览器上可能无法获取 WebGL 上下文运行时会回退到 CPU速度骤降。发现这种情况时不要急着换模型先检查navigator.gpu是否可用或者在tf.setBackend(webgl)外层加一个 try/catch捕捉 JavaScript 运行时报错。5.3 二次开发的扩展方向如果你想把这个项目改造成实际产品我建议把检测参数、模型路径、质量阈值、后端接口地址全部集中到一个配置对象里并用 URL query 支持覆盖。这个技巧对调试很有用手机连接同一个局域网打开http://192.168.x.x:8080/?threshold0.4size416就能直接远程调参不用每改一次就重新构建。把配置提出来之后后续接入活体检测、人脸比对接口也只是在采集结果的基础上再加一层异步识别不需要改动视频流和评分模块的代码。本文还有配套的精品资源点击获取