首页
/ OpenCV.js 浏览器端 DNN 实战:YuNet 人脸检测 + SFace 人脸识别管线

OpenCV.js 浏览器端 DNN 实战:YuNet 人脸检测 + SFace 人脸识别管线

2026-09-06 17:47:51作者:廉皓灿Ida

本文基于 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.readNetcv.blobFromImageNet.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.3topK=5000。该接口在 OpenCV.js 的绑定字典中被显式导出,包含 setInputSizedetect 等成员函数(见 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;
};

几个值得注意的实现细节:

  1. 输入尺寸自适应netDet.setInputSize(new cv.Size(img.cols, img.rows)) 让检测网络的输入 Blob 与当前帧同尺寸。教程特别提醒:可以调整输入 Blob 尺寸来平衡检测质量与效率——输入 Blob 越大,能检测到的目标人脸可以越小,代价是推理开销上升。
  2. 输出结构detect() 的结果是一个一维 Float32Array,每张人脸占 15 个 float:前 4 个为边界框(左上角 x、y 和宽高),中间 10 个为 5 个五官关键点(左眼、右眼、鼻尖、左嘴角、右嘴角的 x/y),最后 1 个为置信度。循环步长 i += 15 即由此而来。
  3. 越界防护:边界框坐标被裁剪到 [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);
  }
};

要点:

  1. 帧格式cv.VideoCapture 读取的是 CV_8UC4(RGBA)帧,先 cv.cvtColor 转为 BGR 的 frameBGR 供检测/识别使用,绘图则画在原始 frame 上并 cv.imshow<canvas id="output">
  2. 限速逻辑:目标 30 FPS,每帧结束后计算 delay = 1000/FPS - 已耗时,用 setTimeout 调度下一帧——这是浏览器单线程环境下模拟固定帧率的典型做法;
  3. 启动时机:主循环只在 OpenCV.js 运行时初始化且两个模型都下载完成后才启动。Start 按钮的回调里先判断 netDet/netRecogn 是否为 undefined,是则调用 loadModels(run) 异步下载模型再进入 run()js_face_recognition.html 第 189-209 行)。

六、部署与运行前提

复现该示例需要满足以下条件:

  1. 构建:一个包含 objdetect 模块 FaceDetectorYN 的 OpenCV.js 产物(示例会主动检测 cv.FaceDetectorYN 是否存在)。OpenCV.js 的构建体系在仓库的 modules/js 目录中,JS 绑定生成器为 embindgen.py,各模块导出的 JS API 由其 misc/js/gen_dict.json 声明;
  2. 页面结构<body onload="cv['onRuntimeInitialized']=()=>{ main() }"> 确保在 WASM 运行时就绪后才执行 main()opencv.jsutils.js(提供 createFileFromUrl 下载封装)通过 <script> 引入;
  3. 模型获取:YuNet 与 SFace 两个 ONNX 文件在首次点击 Start 时由 utils.createFileFromUrl 下载到本地,后续启动可直接复用;
  4. 权限navigator.mediaDevices.getUserMedia({video: true, audio: false}) 需要用户授权摄像头,因此示例应在 https://localhost 等安全上下文中运行;
  5. 版本对应:教程兼容声明为 OpenCV >= 3.3.1(OpenCV.js 支持 dnn 的起点),而当前示例中的模型文件名(2023mar2021dec)与接口形态对应的是较新的 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

登录后查看全文
热门项目推荐
相关项目推荐