OpenCV.js 浏览器端 DNN 实战:YuNet 人脸检测 + SFace 人脸识别管线
本文基于 OpenCV 官方教程《How to run deep networks in browser》(教程原文),讲解如何完全在浏览器中使用 OpenCV.js 运行深度学习模型,搭建一条"人脸检测 + 人脸识别"的端到端管线:通过 cv.FaceDetectorYN(YuNet)实时定位人脸与五官关键点,再通过 cv.readNet 加载 SFace 识别模型提取 128 维特征向量,并用余弦相似度完成人脸匹配。读完后,你将能够独立编写一个纯前端、无需后端的人脸识别 Demo,并理解检测参数、输入 Blob 尺寸与匹配阈值之间的权衡关系。
一、为什么能在浏览器里跑 DNN
OpenCV.js 是把 OpenCV 编译为 WebAssembly 的产物,在浏览器中暴露 cv 全局命名空间,其中同时包含 cv.dnn 相关接口(cv.readNet、cv.blobFromImage、Net.forward)以及 cv.FaceDetectorYN 等专用检测器。教程标注该能力要求 OpenCV >= 3.3.1,即从 3.3.1 起 dnn 模块便已随 OpenCV.js 一同提供。
本教程以仓库中的官方示例 js_face_recognition.html 为参照实现。整个示例只有一个 HTML 页面:
loadModels()负责下载 ONNX 模型文件(检测模型 YuNet、识别模型 SFace);main()在 OpenCV.js 运行时初始化完成后(cv.onRuntimeInitialized)启动相机、加载模型并进入主循环;- 点击
Start按钮开始演示,点击Add a person为被识别为 "unknown" 的人脸命名并注册。
示例中加载的两个模型(见 js_face_recognition.html 第 85-93 行):
var detectModel = 'https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx';
var recognModel = 'https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx';
// ...下载完成后:
netDet = new cv.FaceDetectorYN("face_detection_yunet_2023mar.onnx", "", new cv.Size(320, 320), 0.9, 0.3, 5000);
netRecogn = cv.readNet('face_recognition_sface_2021dec.onnx');
其中 cv.FaceDetectorYN 的构造参数依次为:模型路径、配置路径(可为空串)、默认输入尺寸 320x320、置信度阈值 0.9、NMS 阈值 0.3、topK=5000。该接口在 OpenCV.js 的绑定字典中被显式导出,包含 setInputSize、detect 等成员函数(见 gen_dict.json),其 C++ 端实现位于 face_detect.cpp,接口声明见 face.hpp。
注意:示例在 main() 开头做了兼容检查——当前页面要求使用包含 FaceDetectorYN 的 OpenCV.js 构建,否则会提示重新构建或使用最新版 OpenCV.js(js_face_recognition.html 第 100-103 行)。
二、人脸检测:解析 FaceDetectorYN 的输出
人脸检测网络接收 BGR 图像,输出可能包含人脸的一组边界框;实际使用只需筛选置信度足够高的框。OpenCV.js 中通过 detectFaces() 完成这一步(js_face_recognition.html 第 14-51 行):
function detectFaces(img) {
netDet.setInputSize(new cv.Size(img.cols, img.rows));
var out = new cv.Mat();
netDet.detect(img, out);
var faces = [];
for (var i = 0, n = out.data32F.length; i < n; i += 15) {
var left = out.data32F[i];
var top = out.data32F[i + 1];
var right = (out.data32F[i] + out.data32F[i + 2]);
var bottom = (out.data32F[i + 1] + out.data32F[i + 3]);
left = Math.min(Math.max(0, left), img.cols - 1);
top = Math.min(Math.max(0, top), img.rows - 1);
right = Math.min(Math.max(0, right), img.cols - 1);
bottom = Math.min(Math.max(0, bottom), img.rows - 1);
if (left < right && top < bottom) {
faces.push({
x: left, y: top,
width: right - left, height: bottom - top,
x1: /* 左眼, 越界则置 -1 */ ...,
/* x2~y5: 右眼、鼻尖、左嘴角、右嘴角 */
confidence: out.data32F[i + 14]
})
}
}
out.delete();
return faces;
};
几个值得注意的实现细节:
- 输入尺寸自适应:
netDet.setInputSize(new cv.Size(img.cols, img.rows))让检测网络的输入 Blob 与当前帧同尺寸。教程特别提醒:可以调整输入 Blob 尺寸来平衡检测质量与效率——输入 Blob 越大,能检测到的目标人脸可以越小,代价是推理开销上升。 - 输出结构:
detect()的结果是一个一维Float32Array,每张人脸占 15 个 float:前 4 个为边界框(左上角 x、y 和宽高),中间 10 个为 5 个五官关键点(左眼、右眼、鼻尖、左嘴角、右嘴角的 x/y),最后 1 个为置信度。循环步长i += 15即由此而来。 - 越界防护:边界框坐标被裁剪到
[0, cols-1]/[0, rows-1];关键点坐标若落在图像外则置为-1,主循环绘制圆点前会检查rect.x1 > 0等条件,避免画到画面之外。
三、人脸识别:从人脸到 128 维特征向量
教程中介绍的经典 OpenFace 方案(https://github.com/cmusatyalab/openface 项目)是:识别模型接收 96x96 的 RGB 人脸图像,输出一个 128 维单位向量,把每张脸表示为单位多维球面上的一点,于是两张脸的差异就转化为两个输出向量的夹角。
当前仓库示例实际采用的识别模型是 SFace(face_recognition_sface_2021dec.onnx),输入尺寸为 112x112。提取特征向量的 face2vec() 如下(js_face_recognition.html 第 55-61 行):
function face2vec(face) {
var blob = cv.blobFromImage(face, 1.0, {width: 112, height: 112}, [0, 0, 0, 0], true, false)
netRecogn.setInput(blob);
var vec = netRecogn.forward();
blob.delete();
return vec;
};
cv.blobFromImage(face, 1.0, {width: 112, height: 112}, [0,0,0,0], true, false)把人脸 ROI 缩放到模型输入尺寸并打包成输入 Blob:缩放比例为 1.0(即直接 resize 到 112x112)、减均值和缩放系数均为 0、swapRB=true(模型要求 RGB 输入,而输入是 BGR)、crop=false;netRecogn.forward()无参调用,返回模型的第一个输出张量,即 128 维特征向量(一个cv.Mat)。
四、匹配逻辑:点积相似度 + 阈值
识别阶段把新特征向量与已注册的人脸向量逐一比对,返回最相似者的名字(js_face_recognition.html 第 65-80 行):
function recognize(face) {
var vec = face2vec(face);
var bestMatchName = 'unknown';
var bestMatchScore = 30; // Threshold for face recognition.
for (name in persons) {
var personVec = persons[name];
var score = vec.dot(personVec);
if (score > bestMatchScore) {
bestMatchScore = score;
bestMatchName = name;
}
}
vec.delete();
return bestMatchName;
};
persons是一个普通 JS 对象,persons[name]保存该人的 128 维特征向量;- 用
vec.dot(personVec)(点积)作为相似度分数,超过阈值即认为是同一人; - 源码注释明确
30只是默认匹配阈值(Threshold for face recognition.),实际项目应根据所用模型输出的向量尺度自行标定这个阈值;无匹配时返回'unknown'。
注册新人脸(Add a person 按钮)的流程为:对当前帧跑一次 detectFaces,取第一个检测框做 frameBGR.roi(rects[0]),弹出输入框让用户输入名字,随后 persons[name] = face2vec(face).clone() 存入特征向量,并把 112x112 的可视化小图(经 cv.resize + cv.cvtColor + cv.imshow)插入页面表格(js_face_recognition.html 第 126-149 行)。
五、主循环:相机取帧、检测、识别、绘制
主循环函数 captureFrame() 负责整帧管线(js_face_recognition.html 第 153-186 行):
var isRunning = false;
const FPS = 30; // Target number of frames processed per second.
function captureFrame() {
var begin = Date.now();
cap.read(frame); // Read a frame from camera
cv.cvtColor(frame, frameBGR, cv.COLOR_RGBA2BGR);
var faces = detectFaces(frameBGR);
faces.forEach(function(rect) {
cv.rectangle(frame, {x: rect.x, y: rect.y}, {x: rect.x + rect.width, y: rect.y + rect.height}, [0, 255, 0, 255]);
if(rect.x1>0 && rect.y1>0)
cv.circle(frame, {x: rect.x1, y: rect.y1}, 2, [255, 0, 0, 255], 2)
// ...x2~x5 四个关键点分别用红/绿/紫/青色圆点绘制
var face = frameBGR.roi(rect);
var name = recognize(face);
cv.putText(frame, name, {x: rect.x, y: rect.y}, cv.FONT_HERSHEY_SIMPLEX, 1.0, [0, 255, 0, 255]);
});
cv.imshow(output, frame);
// Loop this function.
if (isRunning) {
var delay = 1000 / FPS - (Date.now() - begin);
setTimeout(captureFrame, delay);
}
};
要点:
- 帧格式:
cv.VideoCapture读取的是CV_8UC4(RGBA)帧,先cv.cvtColor转为 BGR 的frameBGR供检测/识别使用,绘图则画在原始frame上并cv.imshow到<canvas id="output">; - 限速逻辑:目标 30 FPS,每帧结束后计算
delay = 1000/FPS - 已耗时,用setTimeout调度下一帧——这是浏览器单线程环境下模拟固定帧率的典型做法; - 启动时机:主循环只在 OpenCV.js 运行时初始化且两个模型都下载完成后才启动。
Start按钮的回调里先判断netDet/netRecogn是否为undefined,是则调用loadModels(run)异步下载模型再进入run()(js_face_recognition.html 第 189-209 行)。
六、部署与运行前提
复现该示例需要满足以下条件:
- 构建:一个包含
objdetect模块FaceDetectorYN的 OpenCV.js 产物(示例会主动检测cv.FaceDetectorYN是否存在)。OpenCV.js 的构建体系在仓库的 modules/js 目录中,JS 绑定生成器为 embindgen.py,各模块导出的 JS API 由其misc/js/gen_dict.json声明; - 页面结构:
<body onload="cv['onRuntimeInitialized']=()=>{ main() }">确保在 WASM 运行时就绪后才执行main();opencv.js与utils.js(提供createFileFromUrl下载封装)通过<script>引入; - 模型获取:YuNet 与 SFace 两个 ONNX 文件在首次点击
Start时由utils.createFileFromUrl下载到本地,后续启动可直接复用; - 权限:
navigator.mediaDevices.getUserMedia({video: true, audio: false})需要用户授权摄像头,因此示例应在https://或localhost等安全上下文中运行; - 版本对应:教程兼容声明为 OpenCV >= 3.3.1(OpenCV.js 支持 dnn 的起点),而当前示例中的模型文件名(
2023mar、2021dec)与接口形态对应的是较新的 OpenCV 发布,使用旧版 OpenCV.js 时需以仓库内当前示例代码为准核对接口。
小结
这条管线展示了浏览器端 DNN 应用的标准范式:WASM 加载(onRuntimeInitialized)→ 模型下载(createFileFromUrl)→ 检测器实例化(FaceDetectorYN)/通用网络加载(readNet)→ 逐帧循环(取帧、检测、特征提取、相似度匹配、Canvas 绘制)。调整 setInputSize 的输入尺寸可以控制可检测人脸的最小尺寸与推理开销的权衡;FaceDetectorYN 的置信度阈值(0.9)、NMS 阈值(0.3)控制误检与漏检;识别阶段的 bestMatchScore 阈值则决定"认作同一人"的严格程度。仓库内的完整可运行实现见 js_face_recognition.html,教程原文见 dnn_javascript.markdown。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00