「智能前端」基于face.js的纯前端人脸识别项目

7,103 阅读6分钟

前言

Github地址:github.com/Caozefu/sma…

最近逛GitHub时发现一位大神基于tensorflow.js开发了一款用于人脸识别和检测的JavaScript API,于是尝试实现了一个自动加载特效的项目。

介绍

Tensorflow

Tensorflow最初是由一群Google的大佬开发出来用于机器学习和深度神经网络研究的。引用社区的一句话,它一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。
具体的专业知识也不太懂,大佬可以移步官方文档 Tensorflow.js

你找不到的表情

face-api

相比较tensorflowface-api是一位大神基于tensorflow开发的方便像我这种凡人使用的API

你找不到的表情

这个API就简单很多了,作者封装了用于人脸检测、人脸点位检测、人脸识别、人脸表情检测、性别检测等。拥有现成的训练完成的模型供我们直接使用,因此我们可以借助这个API来实现一些有趣的功能。

具体文档链接: face-api
提示: 仓库大小约273M,GitHub下载慢的小伙伴可以从gitee导入再clone

1. 起步

1.1 引入face-api

针对浏览器环境或Nodejs运行环境,引入方式会有些不同。

// 浏览器环境
<script src="dist/face-api.min.js"></script>
// npm
npm i face-api.js
// Node环境
// 非必须引入,需要python环境,引入后会提高运行速度
import '@tensorflow/tfjs-node';

// 由于node环境没有canvas和image等dom元素,需要额外引入
import * as canvas from 'canvas';

import * as faceapi from 'face-api.js';
const { Canvas, Image, ImageData } = canvas
faceapi.env.monkeyPatch({ Canvas, Image, ImageData })

本文使用浏览器环境做简单Demo

引入后会有一个全局对象faceapi,其中的faceapi.nets保存了全局神经网络实例对象。

ageGenderNet: new AgeGenderNet()  // 年龄识别
faceExpressionNet: new FaceExpressionNet(),  //  表情识别
faceLandmark68Net: new FaceLandmark68Net(),  // 面部点位识别(68点)
faceLandmark68TinyNet: new FaceLandmark68TinyNet(), // 面部快速点位识别
faceRecognitionNet: new FaceRecognitionNet(),  // 面部识别
mtcnn: new Mtcnn(),   // MTCNN
ssdMobilenetv1: new SsdMobilenetv1(), // mobolenets人脸检测
tinyFaceDetector: new TinyFaceDetector(),  // 人脸快速检测
tinyYolov2: new TinyYolov2(),   // Yolov2人脸检测

1.2 加载模型

在识别前需要先加载对应的训练模型,否则会报错。在源码仓库的weights目录下,有作者提供的模型,当然也可以加载自定义的其他模型。
调用对应实例的load方法,由于加载方式如下

async function loadModel() {
    // await faceapi.nets.ssdMobilenetv1.load('./face-api.js/weights');
    await faceapi.nets.tinyFaceDetector.load('./weights/tiny');
}

由于需要严格按照先加载模型的顺序,因此使用async await保证流程顺序。这里ssdMobilenetv1tinyFaceDetector的区别在于,前者识别准确度和精度都要高一些,不过对应速度会慢很多,适合静态图片或准确度要求较高场景的检测;后者检测速度快,因此适合做实时监测。

1.3 人脸识别

加载完模型后,需要确定一个输入源,可以是imagecanvasvideo

<img id="image" src="img.png" /> 
<video id="video" src="video.mp4" /> 
<canvas id="canvas" />
const input = document.getElementById('image')
// const input = document.getElementById('video')
// const input = document.getElementById('canvas')
// 或者直接传入id
// const input = 'image'

确定输入源后,就可以调用faceapi.detectAllFacesfaceapi.detectSingleFace获取所有面部数据或单个面部数据。

async function getFace() {
    const detections = await faceapi.detectAllFaces(input);
    // const detections = await faceapi.detectAllFaces(input, new faceapi.SsdMobilenetv1Options());
    // const detections = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions());
}

其中detectAllFaces的第二个参数不指定时默认为SsdMobilenetv1Options,如果想要使用TinyFaceDetectorOptions,在加载模型时,应该使用

faceapi.nets.tinyFaceDetector.load();

最终会返回一个检测到人脸的矩形区域数据,数据结构如下,其中box包含了矩形区域的各点位属性等。

1.4 点位识别

上面说到的detectAllFaces方法,会返回一个矩形区域数据,当我们想要更详细的五官和脸部轮廓数据时,只需要在此函数之后链式调用withFaceLandmarks

async function getFace() {
    const detections = await faceapi.detectAllFaces(input).withFaceLandmarks();
}

不过在此之前,需要在模型加载阶段,增加landmark模型引入

async function loadModel() {
    await faceapi.nets.tinyFaceDetector.load('./weights/tiny');
    
    await faceapi.loadFaceLandmarkModel('./weights/face_landmark')
    // await faceapi.loadFaceLandmarkTinyModel();

}

这里同样分为loadFaceLandmarkModelloadFaceLandmarkTinyModel,对应在withFaceLandmarks传入boolean值区分。

const useTinyModel = true;
detectAllFaces(input).withFaceLandmarks(useTinyModel);

最终返回的结果中,会附带一个landmarks,其中的positions中包含了68个点位。至此,我们就可以用这68个点位做点有意思的事。

你找不到的表情

2. 页面搭建

只需要搭建一个简陋的页面,简陋到只有videocanvas标签。

你找不到的表情

// 获取摄像头数据做一下兼容
function getUserMedia(constrains, success, error) {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        //最新标准API
        navigator.mediaDevices.getUserMedia(constrains).then(success).catch(error);
    } else if (navigator.webkitGetUserMedia) {
        //webkit内核浏览器
        navigator.webkitGetUserMedia(constrains).then(success).catch(error);
    } else if (navigator.mozGetUserMedia) {
        //Firefox浏览器
        navagator.mozGetUserMedia(constrains).then(success).catch(error);
    } else if (navigator.getUserMedia) {
        //旧版API
        navigator.getUserMedia(constrains).then(success).catch(error);
    }
}

//成功的回调函数
function success(stream) {
    //将视频流设置为video元素的源
    video.srcObject = stream;
    //播放视频
    video.play();
}

//异常的回调函数
function error(error) {
    console.log("访问用户媒体设备失败:", error.name, error.message);
}

getUserMedia({
    video: {
        width: 500,
        height: 300,
        facingMode: 'user'
    }
}, success, error);

获取到摄像头数据并传入video后,将input设置为此video标签。

3. 实时检测点位

3.1 实时获取数据

由于faceapi本身没有实时获取的方法,因此我们只需要在获取到摄像头数据后实时获取数据即可。

video.onloadedmetadata = function () {
    getFace();
}

video加载数据完成并开始播放后,调用getFace,同时改造一下此方法。

async function getFace() {
    if (video.paused || video.ended) return;
    const input = video;
    const detections = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks(true);

    if (detections && detections.length) {
        // 匹配尺寸
        faceapi.matchDimensions(canvas, input)

        // 绘制外边框
        faceapi.draw.drawDetections(canvas, faceapi.resizeResults(detections, input))
        // 绘制点位
        faceapi.draw.drawFaceLandmarks(canvas, faceapi.resizeResults(detections, input))
    }
    setTimeout(() => getFace())
}

由此形成循环实时获取数据并绘制。

4. 绘制图片

4.1 确定具体点位

由于我们检测到的点位还不知道具体代表什么位置,因此我们增加一个drawPoint用于绘制对应数字到对应点位,这时需要什么位置就使用对应数字的点位。

async function getFace() {
    if (video.paused || video.ended) return;
    const input = video;
    const detections = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks(true);

    if (detections && detections.length) {
        // 匹配尺寸
        faceapi.matchDimensions(canvas, input)

        detections[0].landmarks.positions.forEach((item, index) => {
            drawPoint(item, index);
        }) 
    }
    setTimeout(() => getFace())
}

function drawPoint(point, index) {
    // 绘制文字
    cxt.fillStyle = "#FF0000";
    cxt.font = "10px bold 黑体";
    // 设置水平对齐方式
    cxt.textAlign = "center";
    // 设置垂直对齐方式
    cxt.textBaseline = "middle";
    cxt.fillText(index, point.x, point.y);
}

绘制的结果如下,我们可以看到17,26号点位代表眼睛的左右两侧,我们想要添加对应的效果,只需要计算宽度并加载图片到对应的位置。

4.2 添加图片

首先计算两点位的距离,勾股定理算一下。

const point1 = detections[0].landmarks.positions[17];
const point2 = detections[0].landmarks.positions[26];
const width = Math.sqrt((point2.x - point1.x) ** 2 + (point2.y - point1.y) ** 2) + 30;

canvas添加图片并绘制

function drawImg(point, index, width) {
    // 坐标微调并绘制图片
    cxt.drawImage(img, point.x - 5, point.y - 5, width, width);
}

最后整合一下代码,就完成了简单的人物佩戴眼睛的特效。

总结

虽然只是个简单的Demo,但是可以看到现在纯前端也能够实现复杂的机器学习,后续会对这个项目进行改进,探索增加更多有趣的功能。同时文章中如果有什么不专业写的不对的,欢迎大家及时指正。