简述
最近需要用到人脸识别功能,于是就上网找了下人脸识别的 API,最后找到了 face++
于是就想着用 face++ 的 api 来做一个刷脸登陆的 Demo
实现思路
注册时,前台通过浏览器调用摄像头,配合 viedo 和 canvas 标签截取人脸,转成 base64 传到后台并保存到数据库中;登陆时将登陆时的人脸和注册实时保存的人脸作比较
效果
先看下做出来的效果
功能主要是:
- 注册录脸
- 登陆刷脸
注册录脸
在注册界面 http://localhost:8080/faceDemo/register.html ,输入用户名,密码,并且在点击提交时录入人脸
如果录入时检测不到会提示(左边是 video 展示,右边是 canvas 截图)
或者录入的人脸质量不足以用来对比都会提示失败(我这里用手遮住了下脸????)
如果录入正常的话提示录入成功
登陆刷脸
注册完之后,回到登陆页面 http://localhost:8080/faceDemo/
有两种登陆方式,通过点击下面的链接切换:
- 密码
- 刷脸
密码登陆就不多说了,刷脸登陆的话,填写好用户名,然后点击登陆
后台会把当前人脸和注册时的人脸作对比
如果对比成功的话,就会提示登陆成功
测试环境
- 开发工具:IDEA2018
- 容器:Tomcat8
- jdk:1.8
项目架构
前端:Vue
后端:SSH
数据库:Mysql
实践
注册 face++
要调用 face++ 的人脸识别接口,需要到他们官网去注册,然后再应用管理里添加 API Key ,拿到调用接口的 app_key 和 app_secret
核心代码
项目中最核心的代码都写在了 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.*;
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 = "XXXXXXXXXXXXXXXXXXXXXXXXXX";
// 你的 SECRET
private static final String API_SECRET = "XXXXXXXXXXXXXXXXXXXXXX";
private final static int CONNECT_TIME_OUT = 30000;
private final static int READ_OUT_TIME = 50000;
private static String boundaryString = getBoundary();
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");
if (validateFaceQuality(fq)) {
dataResp.setCode(DataResp.Code.SUCCESS);
dataResp.setMessage("录入成功");
} 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;
}
}
其中 faceDetect 方法就是人脸识别的方法,faceCompare 就是人脸对比的方法
API_KEY 和 API_SECRET 替换一下你的 key 和 secret 就可以了
Demo源码
项目用了 mysql 做数据库,sql 文件放在了 resources/sql 目录下
目录结构:
将 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"
修改后
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" maxPostSize="0" maxHttpHeaderSize ="300000"/>
demo 项目的源码放到了码云上了,有需要的小伙伴可以拉来玩玩