使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)

简述

最近需要用到人脸识别功能,于是就上网找了下人脸识别的 API,最后找到了 face++
于是就想着用 face++ 的 api 来做一个刷脸登陆的 Demo

实现思路

注册时,前台通过浏览器调用摄像头,配合 viedo 和 canvas 标签截取人脸,转成 base64 传到后台并保存到数据库中;登陆时将登陆时的人脸和注册实时保存的人脸作比较


2019年1月11日 更新

今天又花了点时间在原来的基础上加了一个类似支付宝那样的“张张嘴”和“眨眨眼”的活体检测功能,做这个是之前这个做完后,觉得可能还是不太完善,要是有人拍了一张图片拿来验证估计也能通过,所以就加了“张张嘴”和“眨眨眼”,保证在摄像头前的人是“活”人


效果

先看下做出来的效果
功能主要是:

  • 注册录脸
  • 登陆刷脸
  • 活体检测(张张嘴,眨眨眼)

注册录脸

在注册界面 http://localhost:8080/faceDemo/register.html ,输入用户名,密码,并且在点击提交时录入人脸
如果录入时检测不到会提示【比如歪脖子躲到了一边】(左边是 video 展示,右边是 canvas 截图)
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第1张图片

使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第2张图片

或者录入的人脸质量不足以用来对比都会提示失败(我这里用手遮住了下脸)
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第3张图片

如果录入正常的话提示录入成功
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第4张图片

登陆刷脸

注册完之后,回到登陆页面 http://localhost:8080/faceDemo/
有两种登陆方式,通过点击下面的链接切换:

  • 密码
  • 使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第5张图片
  • 刷脸
    使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第6张图片

密码登陆就不多说了,刷脸登陆的话,填写好用户名,然后点击登陆
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第7张图片

后台会把当前人脸和注册时的人脸作对比
如果对比成功的话,就会提示登陆成功
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第8张图片

活体检测(张张嘴,眨眨眼)

在登陆界面,填写好用户名,再选中“进行活体检测”
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第9张图片

点击登陆之后,会先进行和正常刷脸登陆流程一样,先对比刷脸,如果对比成功会提示“对比成功”

使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第10张图片

然后会先提示“张张嘴”,进行活体检测
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第11张图片

我们嘴巴张合张合一下(张的时候张大点效果更佳),如果检测成功就会做出提示

使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第12张图片

“张张嘴”检测成功后,会再提示进行“眨眨眼”检测
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第13张图片

我们眼睛 blink blink 地眨几下,不出意外就会提示成功
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第14张图片

并提示活体检测登陆成功

使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第15张图片

如果检测过程中,比如“张张嘴”检测过程中保持嘴巴不动一段时间,就会提示检测对象没有按提示做出相应活动,“眨眨眼”同理

使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第16张图片

测试环境

  • 开发工具:IDEA2018
  • 容器:Tomcat8
  • jdk:1.8

项目架构

前端:Vue
后端:SSM
数据库:Mysql

实践

注册 face++

要调用 face++ 的人脸识别接口,需要到他们官网去注册,然后再应用管理里添加 API Key ,拿到调用接口的 app_key 和 app_secret
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第17张图片

核心代码

后台核心代码

项目中最核心的代码都写在了 FaceHelper.java 中

package com.faceDemo.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.faceDemo.model.DataResp;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.util.StringUtils;

import javax.net.ssl.SSLException;
import javax.xml.crypto.Data;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;

/**
 * 人脸识别工具类
 * @author ma_yi
 * 个人博客 https://blog.csdn.net/qq_23412263
 */
public class FaceHelper {
    // 调用 API
    private static final String FACE_URL ="https://api-cn.faceplusplus.com/facepp/v3/";

    public static final String FACE_API_DETECT = "detect";
    public static final String FACE_API_COMPARE = "compare";

    // 你的 key
    public static final String API_KEY = "XXXXXXXXXXXXXXXXXXX";
    // 你的 SECRET
    private static final String API_SECRET = "XXXXXXXXXXXXXXXXXXXX";

    private final static int CONNECT_TIME_OUT = 30000;
    private final static int READ_OUT_TIME = 50000;
    private static String boundaryString = getBoundary();

    // 活体检测类型
    public static final String CHECK_TYPE_MOUTH = "mouth";
    public static final String CHECK_TYPE_EYE = "eye";

    // 嘴巴或眼睛状态
    private static final String STATUS_OPEN = "open";
    private static final String STATUS_CLOSE = "close";

    // 活体检测失败次数阈值,超过这个次数就认为检测失败,不再检测
    private static final int CHECK_FAILED_THRESHOLD = 10;

    // 记录检测失败次数的 key,和用户 id 一起组成唯一的 key
    private static final String KEY_FAILED_COUNT = "failedCount";

    // 存放张张嘴脸部对比数据的 map
    private static Map<String, Object> mouthCheckMap = new HashMap<>();
    // 存放眨眨眼脸部对比数据的 map
    private static Map<String, Object> eyeCheckMap = new HashMap<>();

    public static byte[] post(String api, HashMap<String, String> map, HashMap<String, byte[]> fileMap) throws Exception {
        HttpURLConnection conne;
        URL url1 = new URL(FACE_URL+api);
        conne = (HttpURLConnection) url1.openConnection();
        conne.setDoOutput(true);
        conne.setUseCaches(false);
        conne.setRequestMethod("POST");
        conne.setConnectTimeout(CONNECT_TIME_OUT);
        conne.setReadTimeout(READ_OUT_TIME);
        conne.setRequestProperty("accept", "*/*");
        conne.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundaryString);
        conne.setRequestProperty("connection", "Keep-Alive");
        conne.setRequestProperty("user-agent", "Mozilla/4.0 (compatible;MSIE 6.0;Windows NT 5.1;SV1)");
        DataOutputStream obos = new DataOutputStream(conne.getOutputStream());
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, String> entry = (Map.Entry) iter.next();
            String key = entry.getKey();
            String value = entry.getValue();
            obos.writeBytes("--" + boundaryString + "\r\n");
            obos.writeBytes("Content-Disposition: form-data; name=\"" + key
                    + "\"\r\n");
            obos.writeBytes("\r\n");
            obos.writeBytes(value + "\r\n");
        }
        if (fileMap != null && fileMap.size() > 0) {
            Iterator fileIter = fileMap.entrySet().iterator();
            while (fileIter.hasNext()) {
                Map.Entry<String, byte[]> fileEntry = (Map.Entry<String, byte[]>) fileIter.next();
                obos.writeBytes("--" + boundaryString + "\r\n");
                obos.writeBytes("Content-Disposition: form-data; name=\"" + fileEntry.getKey()
                        + "\"; filename=\"" + encode(" ") + "\"\r\n");
                obos.writeBytes("\r\n");
                obos.write(fileEntry.getValue());
                obos.writeBytes("\r\n");
            }
        }
        obos.writeBytes("--" + boundaryString + "--" + "\r\n");
        obos.writeBytes("\r\n");
        obos.flush();
        obos.close();
        InputStream ins = null;
        int code = conne.getResponseCode();
        try {
            if (code == 200) {
                ins = conne.getInputStream();
            } else {
                ins = conne.getErrorStream();
            }
        } catch (SSLException e) {
            e.printStackTrace();
            return new byte[0];
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buff = new byte[4096];
        int len;
        while ((len = ins.read(buff)) != -1) {
            baos.write(buff, 0, len);
        }
        byte[] bytes = baos.toByteArray();
        ins.close();
        return bytes;
    }

    private static String getBoundary() {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 32; ++i) {
            sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-".charAt(random.nextInt("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_".length())));
        }
        return sb.toString();
    }

    private static String encode(String value) throws Exception {
        return URLEncoder.encode(value, "UTF-8");
    }

    /**
     * 人脸识别
     * @param imgBase64
     */
    public static DataResp faceDetect(String imgBase64) {
        HashMap<String, String> map = new HashMap<>();
        map.put("api_key", API_KEY); // 调用此API的API Key
        map.put("api_secret", API_SECRET); // 调用此API的API Secret
        map.put("return_landmark", "1"); // 是否检测并返回人脸关键点,1 表示返回 83 个人脸关键点
        map.put("return_attributes", "gender,age,smiling,headpose,facequality,blur,eyestatus,emotion,ethnicity,beauty,mouthstatus,eyegaze,skinstatus"); // 是否检测并返回根据人脸特征判断出的年龄、性别、情绪等属性
        map.put("image_base64", imgBase64);

        DataResp dataResp = new DataResp();

        String respString = "";
        try {
            byte[] respByte = post(FACE_API_DETECT, map, null);

            respString = new String(respByte);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (!StringUtils.isEmpty(respString)) {
            System.out.println("脸部识别响应:" + respString);
            JSONObject json = JSON.parseObject(respString);
            // 被检测出的人脸数组
            JSONArray faces = json.getJSONArray("faces");
            if (faces.size() > 0) {
                // 默认取识别出的第一张人脸
                JSONObject face = (JSONObject) faces.get(0);

                System.out.println(face.toString());
                // 获取 facequality 字段,用于判断图片质量是否可以用于后续的人脸对比
                JSONObject fq = face.getJSONObject("attributes").getJSONObject("facequality");

                //-- 打印测试下嘴巴状态
                JSONObject mouthStatus = face.getJSONObject("attributes").getJSONObject("mouthstatus");
                System.out.println("嘴巴状态:" + mouthStatus.toString());

                // 测试下眼睛状态
                JSONObject eyeStatus = face.getJSONObject("attributes").getJSONObject("eyestatus");
                System.out.println("眼睛状态:" + eyeStatus);
                //--

                if (validateFaceQuality(fq)) {
                    dataResp.setCode(DataResp.Code.SUCCESS);
                    dataResp.setMessage("录入成功");
                    // 返回识别的脸部 json 数据,用于后续操作
                    dataResp.setData(face);
                } else {
                    dataResp.setCode(DataResp.Code.ERROR);
                    dataResp.setMessage("请端正姿势");
                    System.out.println("请端正姿势");
                }

            } else {
                dataResp.setCode(DataResp.Code.ERROR);
                dataResp.setMessage("识别不到人脸");
                System.out.println("识别不到人脸");
            }
        }

        return dataResp;
    }

    /**
     * 人脸对比
     * @return
     */
    public static DataResp faceCompare(String imgBase64No1, String imgBase64No2) {

        HashMap<String, String> map = new HashMap<>();
        map.put("api_key", API_KEY);
        map.put("api_secret", API_SECRET);
        map.put("image_base64_1", imgBase64No1); // 用于对比的第一张 base64 编码图片
        map.put("image_base64_2", imgBase64No2); // 用于对比的第二张 base64 编码图片
        DataResp dataResp = new DataResp();

        String respString = "";
        try {
            byte[] respByte = post(FACE_API_COMPARE, map, null);

            respString = new String(respByte);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (!StringUtils.isEmpty(respString)) {
            System.out.println("脸部对比响应:" + respString);

            JSONObject json = JSON.parseObject(respString);
            if (validateFaceConfidence(json)) {
                dataResp.setCode(DataResp.Code.SUCCESS);
                dataResp.setMessage("刷脸对比成功");
                dataResp.setData(json);
            } else {
                dataResp.setCode(DataResp.Code.ERROR);
                dataResp.setMessage("刷脸失败,不是同一个人");
                dataResp.setData(json);
            }
        }

        return dataResp;
    }

    /**
     * 校验人脸质量
     * @return
     */
    public static boolean validateFaceQuality(JSONObject fq) {
        if (fq != null) {
            // value 人脸的质量判断的分数,是一个浮点数
            double value = fq.getDouble("value");
            // threshold 表示人脸质量基本合格的一个阈值,超过该阈值的人脸适合用于人脸比对
            double threshold = fq.getDouble("threshold");

            return value > threshold;
        }

        return false;
    }

    /**
     * 校验置信度,也就是判断是不是同一个人
     * @return
     */
    public static boolean validateFaceConfidence(JSONObject json) {
        if (json != null) {
            // 获取比对结果置信值
            double confidence = json.getDouble("confidence");
            // 获取误识率为十万分之一的置信度阈值
            double threshold1E5 = json.getJSONObject("thresholds").getDouble("1e-5");

            // 如果置信值超过“十万分之一”阈值,则是同一个人的几率非常高
            return confidence > threshold1E5;
        }
        return false;
    }

    /**
     * 张张嘴或眨眨眼检测
     * @param imgBase64
     * @param checkType
     * @param userId
     * @return // 返回 0/1/-1,1 代表成功;0 代表继续检测;-1 代表检测失败,认为不是活体
     */
    public static synchronized DataResp aliveCheck(String imgBase64, String checkType, String userId) {

        System.out.println("正在检测====>>>>>>>>>" + (checkType.equals(CHECK_TYPE_MOUTH) ? "张张嘴" : "眨眨眼"));

        DataResp dataResp = faceDetect(imgBase64);

        // 当前识别到的脸,是一个 JSONObject
        JSONObject currentFace = (JSONObject) dataResp.getData();

        if (dataResp.getCode() == DataResp.Code.SUCCESS) {

            Map<String, Object> tmpMap = null;

            // 根据检测类型,引用对应的 map
            if (checkType.equals(CHECK_TYPE_MOUTH)) {
                tmpMap = mouthCheckMap;
            } else {
                tmpMap = eyeCheckMap;
            }

            JSONObject sourceFace = (JSONObject) tmpMap.get(userId);
            if (sourceFace == null) {
                // 如果是第一次请求,就把第一次的脸部特征当作后续比较的参考对象
                tmpMap.put(userId, currentFace);
                // 并初始化检测失败次数为 0
                tmpMap.put(userId + KEY_FAILED_COUNT, 0);
                dataResp.setData(0);
                dataResp.setMessage("需继续检测");
                dataResp.setCode(DataResp.Code.SUCCESS);
            } else {
                // 否则,就把当前识别的脸和第一次的脸做比较
                // 把当前的 face 的 mouth 特征和第一次的 face 的 mouth 特征取出,并进行比较
                String currentFaceStatus = getStatus(currentFace, checkType);
                String sourceFaceStatus = getStatus(sourceFace, checkType);
                // 判断两次的状态是否一致
                if (!currentFaceStatus.equals(sourceFaceStatus)) {
                    // 如果不一致,则证明摄像头前的人是活动着的,检测通过
                    dataResp.setData(1);
                    dataResp.setCode(DataResp.Code.SUCCESS);
                    dataResp.setMessage("检测通过");
                    // 检测通过需要把该用户对用的 map 里的值清掉,否则会影响下次的检测
                    clearMapKey(tmpMap, userId);
                } else {
                    // 否则,继续检测,记录失败次数,超过一定失败次数则检测不通过,则认为摄像头前的人是不动的
                    int failedCount = (Integer) tmpMap.get(userId + KEY_FAILED_COUNT);
                    System.out.println("检测失败次数:"+failedCount);
                    if (failedCount > CHECK_FAILED_THRESHOLD) {
                        dataResp.setData(-1);
                        dataResp.setCode(DataResp.Code.ERROR);
                        dataResp.setMessage("检测失败,检测对象没有按提示活动");
                        // 检测失败了,也要清掉 map 对应的数据
                        clearMapKey(tmpMap, userId);
                    } else {
                        failedCount++;
                        tmpMap.put(userId + KEY_FAILED_COUNT, failedCount);
                        dataResp.setData(0);
                        dataResp.setCode(DataResp.Code.SUCCESS);
                        dataResp.setMessage("需继续检测");
                    }
                }
            }
        }

        return dataResp;
    }

    /**
     * 根据分数判断是张开还是闭合
     * @param value
     */
    public static String validateStatus(double open, double close) {
        // 如果 open 的分值比 close 的分值大,则认为是打开的,否则相反
        return open > close ? STATUS_OPEN : STATUS_CLOSE;
    }

    /**
     * 清除 map 中的值
     */
    private static void clearMapKey(Map<String, Object> map, String key) {
        map.remove(key);
        map.remove(key + KEY_FAILED_COUNT);
    }

    /**
     * 获取嘴巴或眼睛张合状态
     * @param face
     * @param checkType
     * @return
     */
    private static String getStatus(JSONObject face, String checkType) {
        String status = "";
        if (checkType.equals(CHECK_TYPE_MOUTH)) {
            // mouthstatus 字段记录了嘴巴的状态
            JSONObject mouthStatus = face.getJSONObject("attributes").getJSONObject("mouthstatus");
            // 获取 mouthstatus 字段的子字段 open 和 close,这两个是浮点数的分值,并确定状态
            status = validateStatus(mouthStatus.getDouble("open"), mouthStatus.getDouble("close"));
        } else {
            // eyestatus 字段记录眼睛状态数据,他的子字段 left_eye_status 和 right_eye_status 又分别记录了左右眼的状态
            JSONObject eyeStatus = face.getJSONObject("attributes").getJSONObject("eyestatus");
            // 分别获取左右眼的状态
            JSONObject leftEyeStatus = eyeStatus.getJSONObject("left_eye_status");
            JSONObject rightEyeStatus = eyeStatus.getJSONObject("right_eye_status");

            System.out.println("leftEyeStatus:"+leftEyeStatus);

            // 左右眼状态中的子字段 no_glass_eye_open 和 no_glass_eye_close 分别记录了不戴眼镜睁开眼的置信度和不戴眼镜闭眼的置信度,都是浮点数来的(这里只考虑不戴眼镜的)
            String ls = validateStatus(leftEyeStatus.getDouble("no_glass_eye_open"), leftEyeStatus.getDouble("no_glass_eye_close"));
            String rs = validateStatus(rightEyeStatus.getDouble("no_glass_eye_open"), rightEyeStatus.getDouble("no_glass_eye_close"));

            // 这里如果左右眼的状态一致的话,返回其中一个就行了
            if (ls.equals(rs)) {
                status = ls;
            } else {
                // 如果左右眼不一样,那应该是在放电了 
                // 不过放在本例也算合理,也算眨了眼睛了,也取其中一个吧
                System.out.println("请不要对我放电~");
                status = ls;
            }
        }
        return status;
    }
}


其中 faceDetect 方法就是人脸识别的方法, faceCompare 就是人脸对比的方法
API_KEY 和 API_SECRET 替换一下你的 key 和 secret 就可以了

aliveCheck 方法就是用来活体检测的主要代码,具体做了什么还请看代码,基本每一步都做了注释了

活体检测实现思路

顺便说下“张张嘴”和“眨眨眼”的实现思路(不合理的地方还望提出纠正,谢谢,我测试发现这样做出来效果还挺不错)

“眨眨眼”其实和“张张嘴”是一样的,这里以“张张嘴”为例

  1. 首先前台在提示 “张张嘴”的时候,前台会进行截图,然后将这个图片发到后台
  2. 后台接收到之后,判断是不是第一次检测请求,如果是第一次请求的话,就把这个图片识别后的脸部信息按用户 id 为 key 保存在一个静态公共的 map 中,作为以后比较的参考数据,如果不是第一次请求,那么就把 map 中的第一次保存的脸部数据拿出来和当前识别后的脸部数据做比较
  3. 比如第一次存到的脸部数据的嘴巴状态是 “close” 的状态,那么只要后面的检测请求中的脸部识别数据拿到的状态如果是 “open” 的话,就可以认为检测对象有在活动,检测就通过了

前台核心代码

前台登陆界面 index.html 代码


<html>
<head>
    <meta charset="UTF-8">
    <title>登陆title>
    <style>
        .wrapper{
            text-align: center;
        }
        .face canvas{
            display: none;
        }
        .tip {
            color: red;
        }
    style>
head>

<body>
    <div id="app">
        <div class="wrapper">
            <div class="username">
                <label>用户名label>
                <input v-model="username">
            div>
            <div v-show="passwordLogin">
                <label>密码label>
                <input type="password" v-model="password">
            div>
            <div v-show="!passwordLogin">
                <label>进行活体检测:label><input type="checkbox" v-model="aliveCheck">
            div>
            <div class="btn-wrapper">
                <button type="button" @click="login">登陆button>

                <a href="javascript:void(0)" @click="toggleLoginType">{{passwordLogin ? '刷脸': '密码'}}登陆a>
                <a href="register.html">注册a>
            div>
            <div class="tip">
                {{tip}}
            div>
            <div class="face" v-show="!passwordLogin">
                <video ref="video" width="300" height="300">video>
                <canvas ref="canvas" width="300" height="300">canvas>
                
            div>
        div>
    div>

    <script src="jquery-3.3.1.min.js">script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js">script>
    <script>

        const CODE_SUCCESS = 0
        const CODE_ERROR = 1

        // 活体检测类型
        const [CHECK_TYPE_MOUTH, CHECK_TYPE_EYE] = ['mouth', 'eye']

        // 活体检测结果 1:通过 0:需要继续检测 -1:失败,摄像头前的对象没有活动
        const [CHECK_SUCCESS, CHECK_NORMAL, CHECK_FAILED] = [1, 0, -1]

        const vm = new Vue({
            el: '#app',
            data: {
                username: '',
                password: '',
                passwordLogin: true, // 密码登陆方式
                tip: '',
                aliveCheck: false   // 登陆时是否进行活体检测,默认 false
            },
            mounted() {
                this._initMedia()
            },
            methods: {
                // 初始化摄像头
                _initMedia() {
                    let constraints = {
                        audio: false,
                        video: {width: 300, height: 300}
                    }

                    let _this = this
                    // 调用浏览器摄像头
                    navigator.mediaDevices.getUserMedia(constraints)
                        .then((mediaStream) => {
                            _this.video = this.$refs.video
                            // 将结果分配给 video 标签
                            _this.video.srcObject = mediaStream
                            _this.video.onloadedmetadata = function (e) {
                                // 元数据加载后,播放
                                _this.video.play()
                            }
                        })
                        .catch((err) => {
                            console.log(err.name + ":" + err.message)
                        })
                },
                // canvas 截取图片
                _captureImg() {
                    // 取到 canvas
                    this.canvas = this.$refs.canvas;
                    // 获取 canvas 上下文
                    let ctx = this.canvas.getContext('2d')
                    // 截图
                    ctx.drawImage(this.video,0, 0, 300, 300)
                    // 将截图转换成 base64
                    this.image = this.canvas.toDataURL('image/png')
                    // 只保留 base64 部分
                    let base64Str = this.image.split('base64,')[1]

                    // console.log(base64Str)

                    return base64Str
                },
                login() {

                    if (this.username === '') {
                        this.tip = '用户名不能为空!'
                        return
                    }

                    this.tip = ''

                    let _this = this

                    let base64Str = ''

                    // 判断登陆方式
                    if (!this.passwordLogin) {
                        // 刷脸登陆
                        base64Str = this._captureImg()
                    }

                    this.tip = '正在登陆...'

                    // 登陆请求
                    $.ajax({
                        url: 'user/login.do',
                        data: {
                            username: _this.username,
                            password: _this.password,
                            imgBase64: base64Str,
                            passwordLogin: _this.passwordLogin
                        },
                        success(resp) {
                            console.log(resp)
                            if (resp) {
                                if (resp.code === CODE_SUCCESS) {
                                    // 判断是否还要进行活体检测,即检测 张张嘴和眨眨眼
                                    if (_this.aliveCheck) {
                                        _this.tip = '对比成功'
                                        // resp.data 为 后台返回的 user 对象
                                        _this._handleAliveCheck(resp.data)
                                    } else {
                                        alert("登陆成功!")
                                    }
                                } else {
                                    alert(resp.message)
                                }
                            }
                            // _this.tip = ''
                        },
                        error(error) {
                            console.log(error)
                        }
                    })

                },
                // 切换登陆方式
                toggleLoginType() {
                    this.passwordLogin = !this.passwordLogin
                },
                // 处理活体检测
                _handleAliveCheck(user) {

                    // 延时一下执行
                    setTimeout(() => {
                        this._postAliveCheck(CHECK_TYPE_MOUTH, user.id)
                    }, 2000)
                },
                // 活体检测请求,这里会递归调用,其实也可以用定时,这里
                // 用递归主要是希望每一次检测请求都在检测完成之后才继续调下一次
                _postAliveCheck(checkType, userId) {
                    console.log(checkType)
                    this.tip = `请${checkType===CHECK_TYPE_MOUTH ? '张张嘴' : '眨眨眼'}`
                    let imgBase64 = this._captureImg()
                    $.ajax({
                        url: 'user/aliveCheck.do',
                        // type: 'POST',
                        data: {
                            imgBase64: imgBase64,
                            checkType: checkType,
                            userId: userId
                        },
                        success(resp) {
                            if (resp) {
                                console.log(`code:${resp.code},data:${resp.data},`)
                                if (resp.code === CODE_SUCCESS) {
                                    if (resp.data === CHECK_SUCCESS) {
                                        alert(`${checkType===CHECK_TYPE_MOUTH ? '张张嘴' : '眨眨眼'}检测成功`)
                                        vm.tip = ''
                                        if (checkType === CHECK_TYPE_EYE) {
                                            // 如果眨眨眼也检测完成之后,这样流程就走完了
                                            alert("活体检测登陆成功")
                                        } else {
                                            // 如果张张嘴检测完之后,接着检测眨眨眼
                                            vm._postAliveCheck(CHECK_TYPE_EYE, userId);
                                        }

                                    } else if (resp.data === CHECK_NORMAL) {
                                        console.log('继续检测...')
                                        vm._postAliveCheck(checkType, userId)
                                    }
                                } else if (resp.code === CODE_ERROR && resp.data === CHECK_FAILED) {
                                    alert("检测失败,检测对象没有活动");
                                    vm.tip = ''
                                } else {
                                    // 脸部识别不到提示的信息,继续检测
                                    alert(resp.message)
                                    vm._postAliveCheck(checkType, userId)
                                }
                            }
                        },
                        error(error) {
                            console.log(error)
                        }
                    })
                }
            }
        })
    script>
body>
html>

Demo源码

项目用了 mysql 做数据库,sql 文件放在了 resources/sql 目录下
目录结构:
使用 face++ API 实现人脸识别,刷脸登陆和活体检测(张张嘴,眨眨眼)_第18张图片

将 facedemo.sql 导入 mysql 的 新建的名为 facedemo 数据库
修改项目中的 jdbc.properties 文件,修改为自己的数据库链接即可

小问题

还有一个地方需要注意的就是,如果在测试录脸时候发现后台报

java.lang.IllegalArgumentException: Request header is too large

原因是本来post请求是没有参数大小限制,但是服务器有自己的默认大小,修改 tomcat 配置即可

解决

打开 tomcat 的 conf/server.xml 文件,找到自己 tomcat 端口节点,加上两个属性

maxPostSize="0" maxHttpHeaderSize ="300000"

修改后

demo 项目的源码放到了码云上了,有需要的小伙伴可以拉来玩玩

源码传送门

你可能感兴趣的:(人脸识别)