web端实现基于face-api.js + facenet的人脸识别


前言

项目要求实现web端的人脸比对,即对比两张图片中的人脸是否为同一个人。此问题可以通过人脸识别相关模型来实现,比如经典的FaceNet。然而,图片中通常不止包含人脸,为了更好的提取人脸嵌入向量,就需要先借助人脸检测算法(如RetinaFaceMTCNN)抠出人脸部分;其次,实践发现人脸倾斜对最终的效果有较大的影响,因此还需要关键点检测算法对人脸进行对齐操作。简言之,整体流程为:1)人脸检测,2)关键点检测,对人脸进行矫正;3)人脸识别,提取嵌入向量,通过向量相似度对比实现人脸比对。
RetinaFace+FaceNet搭建人脸识别平台可以借鉴大佬的文章。但了解过模型后发现,RetinaFace需要较多的后处理操作(NMS以及与输出与anchor之间的计算),并不是放在前端的最佳选择。而face-api.js是一个很好用的包,内置了轻量级的人脸检测等算法,用完之后就想说真香。
因此,本次最终选用了face-api.js + FaceNet + onnxruntime-web的混搭方案(对强迫症不太友好);而整体的代码结构可以参考上一篇yolov7

repo:https://github.com/DaiHaoguang3151/face-recognition-web_


一、人脸检测

face-api.js是一个优秀的人脸检测库,底层基于@tensorflow/tfjs-core。原本只想使用onnxruntime-web跑自己的模型,但是相比于face-api.js + tfjs有如下劣势:1)onnx中并没有找到tensor相关ops,不方便进行基于tensor的后处理操作;2)face-api.js直接封装好了模型,不需要考虑后处理,使用起来显然比onnx方便。
face-api.js及其依赖
face-api包含了人脸检测、人脸关键点检测、表情识别和年龄检测等功能。加载模型代码如下:

// 方式1
const loadModels = async () => {
  const url = process.env.PUBLIC_URL + '/models'
  await faceapi.loadTinyFaceDetectorModel(url)
  await faceapi.loadFaceLandmarkTinyModel(url)
  await faceapi.loadFaceExpressionModel(url)
  await faceapi.loadFaceRecognitionModel(url)
  console.log('Models loaded')
}

// 方式2
const loadModels = async () => {
  await faceapi.nets.tinyFaceDetector.loadFromUri('/models')
  await faceapi.nets.faceLandmark68TinyNet.loadFromUri('/models')
  await faceapi.nets.faceExpressionNet('/models')
  await faceapi.nets.faceRecognitionNet('/models')
  console.log('Models loaded')
}

相应的需要再models文件夹下放置好相关的模型,模型才能被正确加载,模型文件可以在这里找到:
模型文件
基于实验效果,这边选择了如下方案(模型偏大,但是为了效果可以接受):

// 模型加载
useEffect(() => {
    const loadModels = async () => {
        await faceapi.nets.ssdMobilenetv1.loadFromUri("/model");
        await faceapi.nets.faceLandmark68Net.loadFromUri("/model")
    };
    loadModels();
})

// 检测image
const detections = await faceapi.detectAllFaces(image, new faceapi.SsdMobilenetv1Options()).withFaceLandmarks(false);

还可以选择直接使用face-api.js原生接口进行人脸识别(模型小,实现便捷,但是人脸区分度差一些),代码如下:

import * as faceapi from "face-api.js"

// 模型加载
useEffect(() => {
    const loadModels = async () => {
        await faceapi.nets.ssdMobilenetv1.loadFromUri("/model");
        await faceapi.nets.faceLandmark68Net.loadFromUri("/model");
        await faceapi.nets.faceRecognizeNet.loadFromUri("/model");  // 人脸识别模型
    };
    loadModels();
})

// 检测image,desciptor就是嵌入向量
const detections = await faceapi.detectAllFaces(image, new faceapi.SsdMobilenetv1Options()).withFaceLandmarks(false).withFaceDescriptors();

// 计算距离
const distance = (faceapi.euclideanDistance(embedding1.data, embedding2.data)) ** 2;

// 人脸识别,从图片或者数据库中找出最佳匹配结果
const faceMatcher = new faceapi.FaceMatcher(det1.detections[0].descriptor);
const match = faceMatcher.findBestMatch(det2.detections[0].descriptor);
console.log("match res=====> ", match.toString());  // person1 (0.48)

二、人脸对齐

人脸对齐是人脸识别前的重要步骤,因为如果人脸有明显倾斜,就会严重影响人脸识别结果。究其原因,可能是因为模型训练时的人脸大多是比较正的,并且没有做超过45°以上的旋转变换。此处不会探究数据增强和模型训练,而是讨论人脸对齐以规避人脸识别出错的风险。
人脸对齐方式有多种,一种方式是计算左右眼与水平方向的角度,然后旋转对齐。此处会遇到两个问题:1)旋转后的人脸宽高是多少?2)如何在前端旋转图片并截取?
人脸对齐
关于问题1),我们可以选择按照先验知识,人眼间距和人脸比例计算出宽高;也可以直接粗暴的保留原始检测框宽高,实际上是可行的。对于问题2),在前端不能直接旋转图片,而应该采用旋转和平移画布的方式实现,具体可以参考戴圣诞帽这篇文章。

// 不能直接变换图片,只能对canvas进行仿射变换
const ctx = canvas.getContext("2d");
// 根据左右眼中心坐标计算偏转角度
const angle = Math.atan2(leftEyeCenterY - rightEyeCenterY, rightEyeCenterX - leftEyeCenterX);

// 平移画布 -> 方便后面绕画布中心点旋转
ctx.translate(canvas.width / 2, canvas.height / 2);
// 旋转画布
ctx.rotate(angle);
// 绘制人脸
ctx.drawImage(image, -faceCenterX, -faceCenterY);
// (faceCenterX, faceCenterY)是检测框中心

三、人脸识别

经过人脸对齐,就可以进行人脸识别操作了。尽管face-api.js中内置了recognize网络,但是此处还是选择使用自己的模型。
将训练好的FaceNet的模型转换成facenet.onnx,借助推理框架onnxruntime-web,对两张人脸做embed操作,并计算两者之间的距离,距离大于阈值则说明是两个人,否则是同一个人。

// 人脸识别/比对
const embedding1 = await embed(face1, session, inputShape);
const embedding2 = await embed(face2, session, inputShape);
const distance = calculateDistance(embedding1, embedding2);
console.log("Distance between embeddings: ", distance);
if (distance > 0.85) {   // 阈值可以自己试一下
  console.log("Two persons");
} else {
  console.log("Same person");
}

四、其他

在计算两张人脸的嵌入向量距离时,可以选择使用tfjs-core中的算子,进行矩阵运算;也可以使用for循环。我对比了两者所消耗的时间,使用for循环竟然比使用tfjs-core快很多,所以最后还是使用for循环。

// import * as tf from '@tensorflow/tfjs-core'
// import * as faceapi from "face-api.js"

/**
 * 计算两个人脸特征向量之间的距离
 * @param {ort.Tensor} embedding1  第一张图片的特征向量
 * @param {ort.Tensor} embedding2  第二张图片的特征向量
 */
export const calculateDistance = (embedding1, embedding2) => {
    // onnx找不到tensor矩阵运算的算子,要么就是使用for循环,要么安装tfjs
    let distance = 0;
    for (let i = 0; i < embedding1.size; i++) {
        distance += Math.pow(embedding1.data[i] - embedding2.data[i], 2);
    };

    // 使用tfjs的tensor算子进行操作,但是速度反而慢了。
    // const embTensor1 = tf.tensor(embedding1.data);
    // const embTensor2 = tf.tensor(embedding2.data);
    // const diff = embTensor1.sub(embTensor2);
    // const distance = diff.square().sum(-1).arraySync();

    // 使用face-api.js,也比for循环慢一些
    // const distance = (faceapi.euclideanDistance(embedding1.data, embedding2.data)) ** 2;

    return distance;
}

总结

本文使用了face-api.js + FaceNet + onnxruntime-web的混搭方案实现了web端人脸检测、人脸对齐以及人脸识别,供大家参考。

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值