人脸识别实现登录
首先我们可以借助第三方API 本例展示Face++ 来实现人脸识别登录Face⁺⁺-旷视Face⁺⁺人脸识别,OCR文字识别,人工智能开放平台
先进行注册

登录控制台

创建API Key(试用就行)
可以通过接口测试工具来调用API接口(APiFOX)
人脸检测
选择的图片为:

选择的参数为:
api_key
api_secret
image_file
返回值说明:
| 字段 | 解释 |
|---|---|
| request_id | 用于区分每一次请求的唯一的字符串。 |
| faces | 被检测出的人脸数组 |
| image_id | 被检测的图片在系统中的标识。 |
| time_used | 整个请求所花费的时间,单位为毫秒 |
| error_message | 当请求失败时才会返回此字符串,具体返回内容见后续错误信息章节。否则此字段不存在 |
| face_num | 检测出的人脸个数 |
faces数组的说明
| face_token | 人脸的标识 |
|---|---|
| face_rectangle | 人脸矩形框的位置,包括以下属性。每个属性的值都是整数:1.top:矩形框左上角像素点的纵坐标 2.left:矩形框左上角像素点的横坐标 3.width:矩形框的宽度 4.height:矩形框的高度 |
| landmark | 人脸的关键点坐标数组。当传入的 landmark 参数值为 1 时,返回 83 个关键点坐标数组。当传入的 landmark 参数值为 2 时,返回 106 个关键点坐标数组 |
人脸比对
选择下面两张图片
选择的参数为: api_key api_secret image_file1 image_file2
用接口测试工具

{
"request_id": "1761146330,7de564eb-568c-4ba7-b12f-eaca729aa292",
"time_used": 499,
"confidence": 61.102,
"thresholds": {
"1e-3": 62.327,
"1e-4": 69.101,
"1e-5": 73.975
},
"faces1": [
{
"face_token": "f4af428016d481ec542fba05fd534911",
"face_rectangle": {
"top": 296,
"left": 330,
"width": 133,
"height": 133
}
}
],
"faces2": [
{
"face_token": "e1418fc8c95cd139853e1b2ed0d124cf",
"face_rectangle": {
"top": 537,
"left": 219,
"width": 718,
"height": 718
}
}
],
"image_id1": "sBrsvA6a/Z4pSg+u/sZFxA==",
"image_id2": "UvMyyo82pv95xt3unvvqzg=="
}
通过返回来的数据: 此时的confidence小于le-5 判断不是同一个人
下面来比较下面两张图片:


返回来的数据:
{
"request_id": "1761146805,f4f2862d-51e3-415e-9688-2e2a6b0fa258",
"time_used": 496,
"confidence": 89.104,
"thresholds": {
"1e-3": 62.327,
"1e-4": 69.101,
"1e-5": 73.975
},
"faces1": [
{
"face_token": "7f365c01865327abc860f0358bcc4fc0",
"face_rectangle": {
"top": 537,
"left": 219,
"width": 718,
"height": 718
}
}
],
"faces2": [
{
"face_token": "39917d5de711ca2a786d0160b95a7e18",
"face_rectangle": {
"top": 166,
"left": 448,
"width": 346,
"height": 346
}
}
],
"image_id1": "UvMyyo82pv95xt3unvvqzg==",
"image_id2": "EyWJtMpVgtwMnfmwozyV3g=="
}
此时的confidence大于le-5 判断是同一个人人脸搜索
创建人脸库
-
描述: 创建一个人脸的集合 FaceSet,用于存储人脸标识 face_token。一个 FaceSet 能够存储10000个 face_token。
-
试用API Key可以创建1000个FaceSet,正式API Key可以创建10000个FaceSet。
-
调用URL https://api-cn.faceplusplus.com/facepp/v3/faceset/create
-
调用方法 POST
选择的参数为: api_key api_secret outer_id

添加人脸(明确一下 人脸库存的是face_token 而不是图片数据)
-
描述 为一个已经创建的 FaceSet 添加人脸标识 face_token。一个 FaceSet 最多存储1,000个 face_token。
-
调用 URL https://api-cn.faceplusplus.com/facepp/v3/faceset/addface
-
调用方法 POST
我们先要获取到face_token

这里获取到"face_token": "b83193dfc55674b73e047232b2a33ef5",
然后将获取到的face_token存储到faceset库中

获取人脸信息: 描述 获取一个 FaceSet 的所有信息,包括此 FaceSet 的 faceset_token, outer_id, display_name 的信息,以及此 FaceSet 中存放的 face_token 数量与列表。
调用URL https://api-cn.faceplusplus.com/facepp/v3/faceset/getdetail
删除人脸
-
描述 移除一个FaceSet中的某些或者全部face_token
-
调用URL https://api-cn.faceplusplus.com/facepp/v3/faceset/removeface

再次查看faceset库

可以看到face_tokens 中的数组 比之前少一个 说明删除成功
具体实现登录功能
在POM中添加依赖
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<version>1.0.3</version>
</dependency>
yaml配置
face: config: api-key: jXoaOVgRt4KfdJk2vw86vTHkhO-8nj7- api-secret: DxGZ5cWoq0oFd2ArIU86lJPOiOmChN5A outer-id: link_faceset
配置启动类:
@SpringBootApplication
public class MallUserApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(MallUserApplication.class, args);
}
}
FaceDTO:
package com.qf.user.dao;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.nio.file.Paths;
import java.util.List;
/**
* 用于访问face++人脸系统
*/
@ConfigurationProperties("face.config")
@Component
@Getter
@Setter
public class FaceDao {
@Resource
private RestTemplate restTemplate;
private String apiKey;
private String apiSecret;
private String outerId;
/**
* 人脸检测
*/
public DetectResponseEntity detect(String filePath) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA); // 多部件表单体
MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
// ----------------- 表单 part
multipartBodyBuilder.part("api_key", apiKey);
multipartBodyBuilder.part("api_secret", apiSecret);
// ----------------- 文件 part
// 从磁盘读取文件
multipartBodyBuilder.part("image_file", new FileSystemResource(Paths.get(filePath)), MediaType.IMAGE_PNG);
// build完整的消息体
MultiValueMap<String, HttpEntity<?>> multipartBody = multipartBodyBuilder.build();
ResponseEntity<DetectResponseEntity> responseEntity = restTemplate.postForEntity("https://api-cn.faceplusplus.com/facepp/v3/detect", multipartBody, DetectResponseEntity.class);
return responseEntity.getBody();
}
/**
* 创建faceset
*/
public void faceSetCreate() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add("api_key", apiKey);
map.add("api_secret", apiSecret);
map.add("outer_id", outerId);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>
(map, headers);
restTemplate.postForEntity("https://api-cn.faceplusplus.com/facepp/v3/faceset/create", request, String.class);
}
/**
* 得到outer_id=trave_faceset的信息
*/
public FaceSetResponseEntity getFaceSetDetail() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add("api_key", apiKey);
map.add("api_secret", apiSecret);
map.add("outer_id", outerId);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<FaceSetResponseEntity> responseEntity = restTemplate.postForEntity("https://api-cn.faceplusplus.com/facepp/v3/faceset/getdetail", request, FaceSetResponseEntity.class);
return responseEntity.getBody();
}
/**
* 添加faceToken到FaceSet
* 人脸标识 faceTokens 组成的字符串,可以是一个或者多个,用逗号分隔。最多不超过5个
face_token
* @param faceTokens
*/
public void addFaceToFaceSet(String faceTokens) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add("api_key", apiKey);
map.add("api_secret", apiSecret);
map.add("outer_id", outerId);
map.add("face_tokens", faceTokens);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>
(map, headers);
restTemplate.postForEntity("https://api-cn.faceplusplus.com/facepp/v3/faceset/addface", request, String.class);
}
/**
* 人脸比对
* @param faceToken1
* @param faceToken2
* @return
*/
public boolean compareFace(String faceToken1, String faceToken2) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA); // 多部件表单体
MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
// ----------------- 表单 part
multipartBodyBuilder.part("api_key", apiKey);
multipartBodyBuilder.part("api_secret", apiSecret);
multipartBodyBuilder.part("face_token1", faceToken1);
multipartBodyBuilder.part("face_token2", faceToken2);
// ----------------- 文件 part
// 从磁盘读取文件
// multipartBodyBuilder.part("image_file", new FileSystemResource(Paths.get(filePath)), MediaType.IMAGE_PNG);
// build完整的消息体
MultiValueMap<String, HttpEntity<?>> multipartBody = multipartBodyBuilder.build();
ResponseEntity<CompareResponseEntity> responseEntity = restTemplate.postForEntity("https://api-cn.faceplusplus.com/facepp/v3/compare", multipartBody, CompareResponseEntity.class);
System.out.println(responseEntity);
CompareResponseEntity e = responseEntity.getBody();
if (e.getConfidence() >= e.getThresholds().e5) {
return true;
} else {
return false;
}
}
/**
* 人脸比对返回实体类
*/
@Data
public static class CompareResponseEntity {
private Double confidence;
private ThresholdsResponseEntity thresholds;
}
/**
* 人脸对比置信度阈值返回实体类
*/
@Data
public static class ThresholdsResponseEntity {
@JsonProperty("1e-5")
private Double e5;
}
/**
* FaceSet返回实体类
*/
@Data
public static class FaceSetResponseEntity{
private String faceset_token;
private String outer_id;
private Integer face_count;
private List<String> face_tokens;
}
@Data
/**
* 人脸检测返回数据实体类
*/
public static class DetectResponseEntity {
private String request_id;
private Integer face_num;
private List<FaceEntity> faces;
}
@Data
/**
* 人脸实体类
*/
public static class FaceEntity {
private String face_token;
}
}
FaceServie:
package com.qf.user.service;
public interface FaceService {
/**
* 添加人脸到人脸库
* @param filePath 人脸图片路径
*/
void addFace(String filePath);
/**
* 通过人脸识别登录
* @param filePath 人脸图片路径
* @return token 登录成功返回token,失败返回null
*/
String loginByFace(String filePath);
}
-
FaceServiceImpl:
package com.qf.user.service.impl; import com.qf.user.dao.FaceDao; import com.qf.user.service.FaceService; import com.qf.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.File; @Service public class FaceServiceImpl implements FaceService { @Autowired private FaceDao faceDao; @Resource private UserService userService; @Override public void addFace(String filePath) { try { System.out.println("开始录入人脸,文件路径: " + filePath); // 检查文件是否存在 File file = new File(filePath); if (!file.exists()) { throw new RuntimeException("文件不存在: " + filePath); } System.out.println("文件存在,大小: " + file.length() + " 字节"); // 获取或创建 FaceSet FaceDao.FaceSetResponseEntity fs = null; try { fs = faceDao.getFaceSetDetail(); System.out.println("获取到FaceSet: " + (fs != null ? fs.getOuter_id() : "null")); } catch (Exception e) { System.out.println("FaceSet不存在,准备创建: " + e.getMessage()); } if (fs == null) { //faceset不存在 System.out.println("创建新的FaceSet"); faceDao.faceSetCreate(); } // 检测人脸 System.out.println("调用Face++检测人脸"); FaceDao.DetectResponseEntity dr = faceDao.detect(filePath); if (dr == null) { throw new RuntimeException("人脸检测返回null"); } System.out.println("检测到 " + dr.getFace_num() + " 张人脸"); if (dr.getFace_num() == 0) { throw new RuntimeException("未检测到人脸,请确保照片中有清晰的人脸"); } // 添加人脸到FaceSet for (FaceDao.FaceEntity f : dr.getFaces()) { System.out.println("添加人脸token到FaceSet: " + f.getFace_token()); faceDao.addFaceToFaceSet(f.getFace_token()); } System.out.println("人脸录入成功"); } catch (Exception e) { System.err.println("人脸录入失败: " + e.getMessage()); e.printStackTrace(); throw new RuntimeException("人脸录入失败: " + e.getMessage(), e); } } @Override public String loginByFace(String filePath) { try { FaceDao.FaceSetResponseEntity fs = null; try { fs = faceDao.getFaceSetDetail(); } catch (Exception e) { System.out.println("获取FaceSet失败: " + e.getMessage()); } if (fs == null) { //faceset不存在 System.out.println("FaceSet不存在,创建新的FaceSet"); faceDao.faceSetCreate(); fs = faceDao.getFaceSetDetail(); } // 检测上传的人脸 FaceDao.DetectResponseEntity dr = faceDao.detect(filePath); if (dr == null || dr.getFace_num() < 1) { System.out.println("未检测到人脸"); new File(filePath).delete(); return null; } String ft1 = dr.getFaces().get(0).getFace_token(); System.out.println("检测到人脸token: " + ft1); // 如果人脸库为空,返回失败 if (fs.getFace_tokens() == null || fs.getFace_tokens().isEmpty()) { System.out.println("人脸库为空,请先录入人脸"); new File(filePath).delete(); return null; } // 与人脸库中的人脸进行比对 System.out.println("开始比对,人脸库中有 " + fs.getFace_tokens().size() + " 个人脸"); for (String ft2: fs.getFace_tokens()) { System.out.println("正在比对人脸: " + ft2); boolean matched = faceDao.compareFace(ft1, ft2); System.out.println("比对结果: " + matched); if (matched) { System.out.println("!!! 人脸匹配成功 !!!"); new File(filePath).delete(); // TODO: 需要建立人脸与用户的关联关系 // 临时方案:使用ID=1的用户(从日志看,用户ID为1已存在) String token = userService.generateToken("1"); System.out.println("!!! 生成的token: " + token); return token; } } System.out.println("未找到匹配的人脸"); new File(filePath).delete(); return null; } catch (Exception e) { e.printStackTrace(); new File(filePath).delete(); return null; } } }
-
FaceController
package com.qf.user.controller; import com.qf.common.core.annotation.NoLogin; import com.qf.common.core.common.R; import com.qf.user.service.FaceService; import com.qf.user.utils.ImageUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.util.UUID; @Controller @RequestMapping("/api/user") public class FaceController { @Resource private FaceService faceService; /** * 调到录入人脸页面 * @return */ @RequestMapping("/toinput") public String toInput() { return "/face/input"; } @RequestMapping("/tologin") public String toLogin() { return "/face/login"; } /** * 录入人脸 * @param request * @return * @throws IOException */ @ResponseBody @PostMapping("/upload") public R doAdd(@RequestBody java.util.Map<String, String> params, HttpServletRequest request) throws IOException { String imgData = params.get("imgData"); String fullPath = null; try { // 获取保存路径,优先使用项目临时目录 String savePath = request.getServletContext().getRealPath("img/face/"); if (savePath == null) { // 如果getRealPath返回null,使用系统临时目录 savePath = System.getProperty("java.io.tmpdir") + "face" + File.separator; } String fileName = UUID.randomUUID().toString().replaceAll("-", "") + ".png"; System.out.println("保存路径: " + savePath); System.out.println("文件名: " + fileName); // 处理Base64数据:如果包含前缀则去掉,否则直接使用 String base64Data = imgData; if (imgData.contains(",")) { base64Data = imgData.substring(imgData.indexOf(",") + 1); } // 生成图片 boolean success = ImageUtils.generateImage(base64Data, savePath, fileName); if (!success) { return R.fail("图片保存失败"); } // 构建完整文件路径 fullPath = savePath + fileName; System.out.println("完整文件路径: " + fullPath); // 录入人脸 faceService.addFace(fullPath); return R.success("人脸录入成功"); } catch (Exception e) { e.printStackTrace(); System.err.println("人脸录入失败: " + e.getMessage()); return R.fail("人脸录入失败: " + e.getMessage()); } finally { // 清理临时文件(可选,根据业务需求决定) // if (fullPath != null) { // new File(fullPath).delete(); // } } } /** * 人脸识别登录 * @param request * @return * @throws IOException */ @NoLogin @ResponseBody @PostMapping("/face/login") public R login(@RequestBody java.util.Map<String, String> params, HttpServletRequest request) throws IOException { String imgData = params.get("imgData"); String fullPath = null; try { System.out.println("========== 开始人脸识别登录 =========="); // 获取保存路径 String savePath = request.getServletContext().getRealPath("img/face/login/"); if (savePath == null) { savePath = System.getProperty("java.io.tmpdir") + "face" + File.separator + "login" + File.separator; } String fileName = UUID.randomUUID().toString().replaceAll("-", "") + ".png"; System.out.println("保存路径: " + savePath); // 处理Base64数据 String base64Data = imgData; if (imgData.contains(",")) { base64Data = imgData.substring(imgData.indexOf(",") + 1); } // 生成图片 boolean success = ImageUtils.generateImage(base64Data, savePath, fileName); if (!success) { return R.fail("图片保存失败"); } fullPath = savePath + fileName; System.out.println("完整路径: " + fullPath); // 进行人脸识别登录 String token = faceService.loginByFace(fullPath); if (token != null && !token.isEmpty()) { System.out.println("人脸识别登录成功,token: " + token); return R.success(token); } else { System.out.println("人脸识别登录失败:未找到匹配的人脸"); return R.fail("未识别到匹配的人脸,请重试"); } } catch (Exception e) { e.printStackTrace(); System.err.println("人脸识别失败: " + e.getMessage()); return R.fail("人脸识别失败: " + e.getMessage()); } finally { // 清理临时文件 if (fullPath != null) { new File(fullPath).delete(); } } } } -
ImageUtils
package com.qf.user.utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
/**
* 图片处理工具类
*/
public class ImageUtils {
/**
* 将Base64编码的图片数据生成图片文件
* @param imgStr Base64编码的图片数据
* @param savePath 保存路径
* @param fileName 文件名
* @return 是否成功
*/
public static boolean generateImage(String imgStr, String savePath, String fileName) {
if (imgStr == null || imgStr.isEmpty()) {
System.err.println("Base64数据为空");
return false;
}
FileOutputStream out = null;
try {
// Base64解码
byte[] b = Base64.getDecoder().decode(imgStr);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {
// 调整异常数据
b[i] += 256;
}
}
// 确保目录存在
File dir = new File(savePath);
if (!dir.exists()) {
boolean created = dir.mkdirs();
System.out.println("创建目录: " + savePath + ", 结果: " + created);
}
// 生成图片 - 确保路径分隔符正确
String imgFilePath;
if (savePath.endsWith(File.separator)) {
imgFilePath = savePath + fileName;
} else {
imgFilePath = savePath + File.separator + fileName;
}
System.out.println("生成图片文件: " + imgFilePath);
// 写入文件
out = new FileOutputStream(imgFilePath);
out.write(b);
out.flush();
System.out.println("图片保存成功,大小: " + b.length + " 字节");
return true;
} catch (IllegalArgumentException e) {
System.err.println("Base64解码失败: " + e.getMessage());
e.printStackTrace();
return false;
} catch (IOException e) {
System.err.println("文件写入失败: " + e.getMessage());
e.printStackTrace();
return false;
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)