基于虹软人脸识别在微信小程序中实现人脸登录
一、项目概述
下面开始本篇文章的正式内容。
在当下,人脸登录已经是一个普通到不能再普通的功能,在一些安全校验严格的情况下,必须要保证是本人在登录,要实现这一点,一般会在密码或验证码登录后再进行一次人脸识别来确保。在我们日常使用微信小程序的过程中,也会经常遇到即使输入了正确的账号密码,也要进行人脸识别的情况,这并非是多此一举,而是为我们的数据安全提供了更强的保护。
人脸登录的高层级思路并不复杂,在使用人脸登录前,我们首先要使用其他方式进行人脸注册,比如用户注册时采集人脸,或者后台批量录入人脸等,并根据注册照提取人脸特征,之后将人脸特征和其他信息进行持久化保存(比如存入数据库);当用户进行登录时,我们再获取到采集照,同样进行人脸检测和特征提取,最后将采集照的特征数据和注册照的特征数据进行对比搜索,来判断正在登录的人是否是之前已经录入过信息的人。
这里借用上一篇文章的流程图:
以上流程中的人脸检测和人脸特征提取部分均在服务端完成,本文中的小程序仅实现了流程中获取采集照的部分,而注册的部分依然复用上一篇文章中的前端项目。
老规矩,先看实现效果,由于需要使用到小程序,所以此次用来测试的人只能是我自己了,不过在下实在不帅气,所以会打码处理,但是又本着让大家看文章的时候有个好心情,所以我就使用了抖音方太集成电器旗舰店直播间的年轻漂亮还有点帅气的莓主播来作为对比图片。
这样数据库中就会存有两张截然不同的人脸,登录的时候只会匹配到正确的人脸才会成功,下面是小程序的运行截图:
二、项目设计
1、流程设计
接下来我们就根据这个流程来实现我们的小程序。
2、技术方案
因为涉及到了小程序开发,而微信官方的开发工具又没有Linux版本,所以我在这里使用了两个操作系统,分别是Windows和Linux,因为我的设备本身就是双系统,所以并没有使用虚拟机(下文只列出小程序的主要代码和一部分后端代码,文章结尾会给出项目的GitHub链接,运行前不要忘记阅读本文和上一篇文章的环境搭建部分):
Windows:主要用到了官方的微信开发者工具,下载链接如下:
Linux:我使用的是Fedora 64位桌面版,由于Java是跨平台语言,所以用其他发行版也是没关系的
Java:这里使用了OpenJDK 21,如果你使用了其他版本,修改pom.xml中的对应位置即可
Maven:这是一个Maven项目,具体的依赖信息可以参考项目的pom文件,主要使用到了Spring Boot、Spring Data JPA 和数据库驱动依赖
虹软Linux Pro SDK:在虹软开发者平台获取,此处使用Java版本
数据库:本项目的pom文件使用了PostgreSQL驱动,如果需要使用其他数据库,只需要更改数据库驱动并配置好对应的连接字符串即可
3、环境搭建
Windows系统只需要安装微信开发工具即可,Linux系统的环境请参考文章开头提到的前一篇文章的环境搭建部分。
需要注意的是,项目的配置文件application.properties倒数第二行添加了一项新的配置image-url,用来向前端返回有效的图像地址,项目运行前需要替换为服务端实际的IP地址:
spring.application.name=arcfacelinuxprojavaweb
# DB
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
soring.datasource.driver-class-name=
# JPA
# 出于演示目的,每次启动都重新创建表
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
sprint.jpa.properties.hibernate.format-sql=true
# ArcSoft
# 提前使用
# ./mvnw install:install-file -Dfile=/usr/local/arcsoft_lib/arcsoft-sdk-face-server-1.0.0.0.jar -DgroupId=com.arcsoft -DartifactId=face-sdk -Dversion=1.0.0.0 -Dpackaging=jar
# 将SDK注册进Maven
arcsoft.appId=
arcsoft.sdkKey=
arcsoft.activeKey=
arcsoft.libPath=/usr/local/arcsoft_lib/Linux
# APP
app.image-url=http://192.168.8.136:8080/images/
app.upload-dir=/home/bfss/Program/arcfacelinuxprojavaweb/src/main/resources/static/uploads三、项目实现
接下来列出小程序端的主要代码,如上图所示,小程序主要有三个页面,分别是:
- face:打开前置摄像头获取照片的页面
- login:密码登录页面
- profile:登录成功后跳转的页面
face页面设置了一个定时器,每隔3秒钟捕获一张摄像头拍摄的图片并上传到后端服务器进行比对,一旦后端比对成功,就将后端返回的信息发送给profile页面并进行跳转,以下是face.js的代码:
// pages/face/face.js
Page({
timer: null, // 定时器
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.startFaceCheck(); // 页面显示时开启
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
this.stopFaceCheck(); // 页面隐藏(跳转)时停止,防止内存泄漏
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
this.stopFaceCheck(); // 页面销毁时停止
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
},
startFaceCheck() {
const ctx = wx.createCameraContext();
// 每 3 秒执行一次拍照比对
this.timer = setInterval(() => {
ctx.takePhoto({
quality: 'low', // 低画质节省流量且后端处理快
success: (res) => {
this.uploadAndVerify(res.tempImagePath);
}
});
}, 3000);
},
stopFaceCheck() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
uploadAndVerify(path) {
wx.uploadFile({
url: 'http://192.168.8.136:8080/api/face/search',
filePath: path,
name: 'image',
success: (res) => {
// res.data 转成 JSON
const result = JSON.parse(res.data);
// 返回的状态码是 200
if (res.statusCode === 200 && result.success == true) {
this.stopFaceCheck(); // 停止定时器
wx.showToast({ title: '验证通过', icon: 'success' });
// 将后端返回的人名和图片路径拼接到 URL 中
const name = encodeURIComponent(result.personName);
const avatar = encodeURIComponent(result.imagePath);
// 跳转到个人信息页
setTimeout(() => {
wx.reLaunch({
url: `/pages/profile/profile?name=${name}&avatar=${avatar}`
});
}, 1000);
}
},
fail: (err) => {
console.error("验证失败", err);
}
});
}
})
login页面实现了用户输入账号密码进行验证的功能(伪登录,用户名和密码写在代码文件里),一旦登录成功,就弹出对话框提示用户将要进行人脸识别。以下是login.js的代码:// pages/login/login.js
Page({
/**
* 页面的初始数据
*/
data: {
user: '',
pwd: ''
},
inputUser(e) { this.setData({ user: e.detail.value }) },
inputPwd(e) { this.setData({ pwd: e.detail.value }) },
handleLogin() {
// 伪登录
if (this.data.user === 'admin' && this.data.pwd === '123456') {
// 弹出提示框
wx.showModal({
title: '安全验证',
content: '为了您的账号安全,请进行人脸识别',
showCancel: false,
success: (res) => {
if (res.confirm) {
// 跳转到人脸识别页
wx.navigateTo({ url: '/pages/face/face' });
}
}
});
} else {
wx.showToast({ title: '账号或密码错误', icon: 'none' });
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})profile页面实现了用户信息展示的功能(简易版,因为后端只传输了注册照和图片名),一旦人脸识别通过,就将后端返回的信息展示在页面上。以下是profile.js的代码:
// pages/profile/profile.js
Page({
/**
* 页面的初始数据
*/
data: {
userName: '',
userAvatar: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.setData({
userName: decodeURIComponent(options.name),
userAvatar: decodeURIComponent(options.avatar)
});
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})下面还有两处基于小程序前端而改动的后端代码,分别是:
AppConfig.java,添加了用来提供静态资源的addResourceHandlers方法。
package com.bfss.arcfacelinuxprojavaweb.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer{
@Value("${app.upload-dir}")
private String uploadDir;
// 由于采用前后端分离设计,所以需要配置CORS避免跨域问题
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*");
}
// 添加静态资源映射,实现前端访问图片
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry
.addResourceHandler("images/**")
.addResourceLocations("file:" + uploadDir);
}
}ArcFaceService.java,在searchFaceByImage方法中添加了对比结果为null的判断,并且将返回的图片路径从本机路径改为静态资源路径,便于前端访问图片。
package com.bfss.arcfacelinuxprojavaweb.service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.arcsoft.face.FaceFeature;
import com.arcsoft.face.SearchResult;
import com.arcsoft.face.enums.ExtractType;
import com.bfss.arcfacelinuxprojavaweb.dto.ArcFaceSearchResponse;
import com.bfss.arcfacelinuxprojavaweb.engine.ArcFaceEngine;
import com.bfss.arcfacelinuxprojavaweb.entity.ArcFaceInfoEntity;
import com.bfss.arcfacelinuxprojavaweb.repository.ArcFaceInfoRepository;
import jakarta.transaction.Transactional;
@Service
public class ArcFaceService {
// 人脸注册与搜索服务
private static final Logger logger = LoggerFactory.getLogger(ArcFaceService.class);
private final ArcFaceEngine arcFaceEngine;
private final ArcFaceInfoRepository arcFaceInfoRepository;
@Value("${app.image-url}")
private String imageUrl;
@Value("${app.upload-dir}")
private String uploadDir;
public ArcFaceService(ArcFaceEngine arcFaceEngine, ArcFaceInfoRepository arcFaceInfoRepository){
this.arcFaceEngine = arcFaceEngine;
this.arcFaceInfoRepository = arcFaceInfoRepository;
}
@Transactional
public ArcFaceInfoEntity registerFaceFromImage(MultipartFile file){
// 人脸注册
try{
Path uploadPath = Paths.get(uploadDir);
Files.createDirectories(uploadPath);
Path targetPath = uploadPath.resolve(file.getOriginalFilename());
// 此处为方便演示,使用文件覆盖选项
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
File storedFile = targetPath.toFile();
FaceFeature faceFeature = arcFaceEngine.extractFaceFeatureFromImage(storedFile, ExtractType.REGISTER);
// 这里使用文件名作为personName,在实际项目中可能需要从前端获取
ArcFaceInfoEntity arcFaceInfo = new ArcFaceInfoEntity(file.getOriginalFilename(), faceFeature.getFeatureData(), targetPath.toString());
arcFaceInfo = arcFaceInfoRepository.save(arcFaceInfo);
arcFaceEngine.registerFace(arcFaceInfo.getId(), arcFaceInfo.getFaceFeature());
return arcFaceInfo;
} catch (IOException e){
logger.error("注册函数发生异常", e);
return null;
}
}
public ArcFaceSearchResponse searchFaceByImage(MultipartFile file){
// 人脸搜索
try{
Path tempPath = Paths.get(System.getProperty("java.io.tmpdir"), file.getOriginalFilename());
Files.copy(file.getInputStream(), tempPath);
File tempFile = tempPath.toFile();
FaceFeature faceFeature = arcFaceEngine.extractFaceFeatureFromImage(tempFile, ExtractType.RECOGNIZE);
SearchResult searchResult = arcFaceEngine.searchFace(faceFeature.getFeatureData());
Files.delete(tempPath);
ArcFaceSearchResponse arcFaceSearchResponse;
// 此处设置阈值为0.8,可以根据实际业务场景调整
if(searchResult != null && searchResult.getMaxSimilar() > 0.8){
int faceId = searchResult.getFaceFeatureInfo().getSearchId();
Optional<ArcFaceInfoEntity> faceInfo = arcFaceInfoRepository.findById((long)faceId);
// 在这里将本地图片路径替换为网络路径
arcFaceSearchResponse = new ArcFaceSearchResponse(
true,
faceInfo.get().getPersonName(),
faceInfo.get().getImagePath().replace(uploadDir, imageUrl)
);
}else{
arcFaceSearchResponse = new ArcFaceSearchResponse(false, "", "");
}
return arcFaceSearchResponse;
}catch(IOException e){
logger.error("搜索函数发生异常", e);
return null;
}
}
}以上就是小程序项目的主要代码和一小部分后端代码,整体难度不高,下文会给出这两个项目的Github地址,clone并配置好后即可运行。
评论
发表评论