首先我们可以借助第三方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 而不是图片数据)
我们先要获取到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_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();
                }
            }
        }
    }
}
​
​
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐