1,需要腾讯人脸第三方
腾讯人脸第三方申请步骤
2,相关表结构设计
create database faceDB;
use faceDB;
-- 人脸表
create table face(
fid int primary key auto_increment COMMENT '主键',
face_base longtext COMMENT '图片数据 base_64编码',
create_time datetime COMMENT '插入时间',
vef_num int COMMENT '验证次数',
face_name varchar(100) COMMENT '人脸名称',
remark varchar(200) COMMENT '人脸备注',
face_status int COMMENT '人脸是否可用,(0==可用,1,不可用)',
update_extend1 varchar(300) COMMENT '扩展字段1',
update_extend2 varchar(300) COMMENT '扩展字段2',
update_extend3 varchar(300) COMMENT '扩展字段3'
);
-- 验证日志表
create table face_vef_log(
lid int primary key auto_increment COMMENT '主键',
vef_time datetime COMMENT '验证时间',
vef_code int COMMENT '返回code',
vef_msg varchar(200) COMMENT '返回的消息',
login_name varchar(100) COMMENT '验证人'
);
3,相关配置
tencentcloudapi:
# 你的 secretId
secretId: 你腾讯云的secretId
# 你的 secretKey
secretKey: 你腾讯云的secretKey
# 请求官方地址 不变
endpoint: iai.tencentcloudapi.com
#地域 可不变
region: ap-shanghai
package com.face.config;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.face.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @author tanyongpeng
* <p>des</p>
**/
@Component
@Slf4j
public class FaceConfig implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String face_token = request.getHeader("face_token");
Map<String,Object> infoMap = new HashMap<>();
try{
//token验证
DecodedJWT tokenInfo = JwtUtils.getTokenInfo(face_token);
//添加返回信息
infoMap.put("msg","验证成功");
return true;
}catch (SignatureVerificationException e){
e.printStackTrace();
infoMap.put("msg","无效签名");
}catch (TokenExpiredException e){
infoMap.put("msg","token已过期");
}catch (AlgorithmMismatchException e){
infoMap.put("msg","算法不一致");
}catch (Exception e){
infoMap.put("msg","无效签名");
}
ObjectMapper objectMapper = new ObjectMapper();
String mapInfoJson = objectMapper.writeValueAsString(infoMap);
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write(mapInfoJson);
return false;
}
}
package com.face.server;
import com.face.bean.result.FaceResult;
import com.face.utils.TimeUtils;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.iai.v20180301.IaiClient;
import com.tencentcloudapi.iai.v20180301.models.CompareFaceRequest;
import com.tencentcloudapi.iai.v20180301.models.CompareFaceResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author tanyongpeng
* <p>调用腾讯接口</p>
**/
@Component
@Data
@Slf4j
public class FaceContrastServer {
@Value("${tencentcloudapi.secretId}")
private String secretId;
@Value("${tencentcloudapi.secretKey}")
private String secretKey;
@Value("${tencentcloudapi.endpoint}")
private String endpoint;
@Value("${tencentcloudapi.region}")
private String region;
//第一张是数据库图片,第二张是登录时验证图片
public FaceResult faceContrast(String imageA, String imageB){
FaceResult faceResult = new FaceResult();
try{
Credential cred = new Credential(secretId, secretKey);
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(endpoint);
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
IaiClient client = new IaiClient(cred, region, clientProfile);
CompareFaceRequest req = new CompareFaceRequest();
req.setImageA(imageA);
req.setImageB(imageB);
CompareFaceResponse resp = client.CompareFace(req);
faceResult.setScore(resp.getScore());
faceResult.setCode(FaceResult.SUCCESS_CODE);
} catch (TencentCloudSDKException e) {
faceResult.setCode(FaceResult.FACE_ERROR);
faceResult.setMsg(e.getMessage());
}
return faceResult;
}
}
4,人脸注册,登录后端代码
package com.face.service.impl;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.face.bean.Face;
import com.face.bean.result.FaceResult;
import com.face.mapper.FaceMapper;
import com.face.server.FaceContrastServer;
import com.face.service.FaceService;
import com.face.utils.JwtUtils;
import com.face.utils.TimeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author typsusan
* @description 针对表【face】的数据库操作Service实现
* @createDate 2022-07-17 03:33:50
*/
@Service
public class FaceServiceImpl extends ServiceImpl<FaceMapper, Face>
implements FaceService {
@Autowired
FaceContrastServer faceContrastServer;
@Override
public FaceResult vef(String imageBase) {
imageBase = JSONUtil.parseObj(imageBase).getStr("imageBase");
List<Face> faceList = lambdaQuery().orderByDesc(Face::getVefNum).list();
FaceResult faceState = null;
// 如果人脸库为空,则第一次登录为录入人脸
if (faceList.size() == 0){
return initFace(imageBase);
}else {
int faceLength = faceList.size();
for (Face face : faceList) {
FaceResult faceResult = faceContrastServer.faceContrast(face.getFaceBase(), imageBase);
// 是否比对成功
if (faceResult.getCode() == FaceResult.SUCCESS_CODE ){
// 相似度是否大于80
if (faceResult.getScore() > FaceResult.SATISFY_SCORE){
if (face.getFaceStatus() == 0){
// 成功
lambdaUpdate().set(Face::getVefNum,face.getVefNum()+1).eq(Face::getFid,face.getFid()).update();
faceResult.setMsg(TimeUtils.timeQuantum()+"好,"+face.getFaceName());
faceResult.setName(face.getFaceName());
Map<String,String> map = new HashMap<>();
map.put("score",String.valueOf(faceResult.getScore()));
map.put("faceName",faceResult.getName());
faceResult.setToken(JwtUtils.genereteToken(map));
return faceResult;
}else {
// 失败 人脸被禁用
lambdaUpdate().set(Face::getVefNum,face.getVefNum()+1).eq(Face::getFid,face.getFid()).update();
faceResult.setMsg(face.getFaceName()+",当前人脸被禁用");
faceResult.setName(face.getFaceName());
faceResult.setCode(FaceResult.FORBIDDEN_FACE);
faceState = faceResult;
// 就算上一张人脸被禁用还得往下执行
// 可能当前用户存在多张人脸,
if (faceLength == 1){
return faceResult;
}
faceLength --;
}
}else {
// 人脸库没有检测到人脸
if (faceLength == 1){
// 判断当前人脸是否被禁用,如被禁用,提示被禁用
// 禁用优先级大于 没有检测到人脸
return faceState != null?faceState:FaceResult.error(FaceResult.NOT_FOUND_FACE,"人脸库不存在该人脸",faceResult.getScore());
}
faceLength --;
}
}else {
// 接口返回异常
return faceResult;
}
}
}
// 空异常
return FaceResult.error(FaceResult.NULL_ERROR,"空指针异常");
}
public FaceResult initFace(String imageBase){
FaceResult faceResult = new FaceResult();
Face face = new Face();
face.setFaceBase(imageBase);
face.setCreateTime(new Date());
face.setVefNum(0);
face.setFaceName("admin");
face.setFaceStatus(0);
boolean save = save(face);
faceResult.setCode(FaceResult.INIT_FACE);
faceResult.setMsg("人脸初始化"+(save?"成功":"失败")+","+(save?"请验证登录":"请稍后再试"));
faceResult.setName(face.getFaceName());
return faceResult;
}
}
5,前端代码
/**
* 获取 浏览器 拍照的权限
*/
/**
* 获取浏览器权限
* @param option
*/
function getCamera(option) {
option.thisCancas = document.getElementById(option.canvasId);
option.thisContext = option.thisCancas.getContext("2d");
option.thisVideo = document.getElementById(option.videoId);
option.thisVideo.style.display = "block";
// 获取媒体属性,旧版本浏览器可能不支持mediaDevices,我们首先设置一个空对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// 一些浏览器实现了部分mediaDevices,我们不能只分配一个对象
// 使用getUserMedia,因为它会覆盖现有的属性。
// 这里,如果缺少getUserMedia属性,就添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) {
// 首先获取现存的getUserMedia(如果存在)
var getUserMedia =
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.getUserMedia;
// 有些浏览器不支持,会返回错误信息
// 保持接口一致
if (!getUserMedia) {
//不存在则报错
return Promise.reject(
new Error("getUserMedia is not implemented in this browser")
);
}
// 否则,使用Promise将调用包装到旧的navigator.getUserMedia
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
var constraints = {
audio: false,
video: {
width: option.videoWidth,
height: option.videoHeight,
transform: "scaleX(-1)",
},
};
navigator.mediaDevices
.getUserMedia(constraints)
.then(function (stream) {
// 旧的浏览器可能没有srcObject
if ("srcObject" in option.thisVideo) {
option.thisVideo.srcObject = stream;
} else {
// 避免在新的浏览器中使用它,因为它正在被弃用。
option.thisVideo.src = window.URL.createObjectURL(stream);
}
option.thisVideo.onloadedmetadata = function (e) {
option.thisVideo.play();
};
})
.catch((err) => {
console.log(err);
});
return option
}
/**
* 绘制图片
* 返回格式为base64
* @param option
*/
function draw(option){
option.thisContext.drawImage(option.thisVideo,0,0,option.videoWidth,option.videoHeight);
return option.thisCancas.toDataURL("image/png");
}
export default {
getCamera,
draw
}
<template>
<div>
<div class="login">
</div>
<!--登录中间块-->
<div class="login-mid">
<div class="login-mid-top">
<div class="shadow-top-left"></div>
<div class="shadow-top-right"></div>
</div>
<div class="login-mid-mid">
<!--捕获人脸区域-->
<div class="videoCamera-canvasCamera">
<video
id="videoCamera"
:width="videoWidth"
:height="videoHeight"
autoplay
></video>
<canvas
style="display: none"
id="canvasCamera"
:width="videoWidth"
:height="videoHeight"
></canvas>
<!--人脸特效区域-->
<div v-if="faceImgState" class="face-special-effects-2"></div>
<div v-else class="face-special-effects"></div>
</div>
<!--按钮区域-->
<div class="face-btn">
<button @click="faceVef()">{{faceImgState?'正在识别中...':'开始识别'}}</button>
</div>
<!--消息区域-->
<div class="msg">
<div class="server-msg">{{msg}}</div>
<div class="welcome">Welcome to face recognition</div>
</div>
</div>
<div class="login-mid-bot">
<div class="shadow-bot-left"></div>
<div class="shadow-bot-right"></div>
</div>
</div>
</div>
</template>
<script>
import $camera from '../../camera/index.js'
export default {
data() {
return {
videoWidth: 200,
videoHeight: 200,
msg:'',
faceImgState:false,
faceOption:{}
};
},
mounted() {
//调用摄像头
this.faceOption = $camera.getCamera({
videoWidth: 200,
videoHeight: 200,
thisCancas: null,
thisContext: null,
thisVideo: null,
canvasId:'canvasCamera',
videoId:'videoCamera'
});
//this.getCompetence();
},
methods: {
// 调用后台接口
faceVef(){
// 开始绘制图片
let imageBase = $camera.draw(this.faceOption)
if (this.faceImgState){
return
}
this.faceImgState = true
if (imageBase === '' || imageBase === null || imageBase === undefined){
this.$message.error("图片数据为空")
}else {
this.$http.post("/face/vef",{imageBase}).then(res =>{
console.log(res)
this.faceImgState = false
// 跳转首页
if (res.data.code === 200){
// 关闭摄像头
this.faceOption.thisVideo.srcObject.getTracks()[0].stop();
localStorage.setItem("face_token",res.data.token);
localStorage.setItem("username",res.data.name);
this.$message.success(res.data.msg)
this.$router.push("/home")
}
if (res.data.code === 201){
this.$message.success(res.data.msg)
}
},onerror =>{
this.faceImgState = false
})
}
}
},
};
</script>
<style>
@import "./index.css";
</style>
6,实现实例
参考:项目后端地址face-easy
参考:项目前端地址face-ui