从零玩转人脸识别

语言: CN / TW / HK

前言

在线demo

( 前往享受人脸识别 )

文章作者个人博客

( 前往作者博客 )

本期教程人脸识别第三方平台为虹软科技,本文章讲解的是人脸识别RGB活体追踪技术,免费的功能很多可以自行搭配,希望在你看完本章课程有所收获。

ArcFace 离线SDK,包含人脸检测、性别检测、年龄检测、人脸识别、图像质量检测、RGB活体检测、IR活体检测等能力,初次使用时需联网激活,激活后即可在本地无网络环境下工作,可根据具体的业务需求结合人脸识别SDK灵活地进行应用层开发。

功能介绍

1. 人脸检测

对传入的图像数据进行人脸检测,返回人脸的边框以及朝向信息,可用于后续的人脸识别、特征提取、活体检测等操作;

  • 支持IMAGE模式和VIDEO模式人脸检测。
  • 支持单人脸、多人脸检测,最多支持检测人脸数为50。

2.人脸追踪

对来自于视频流中的图像数据,进行人脸检测,并对检测到的人脸进行持续跟踪。(我们是实时的所以就只能使用第三方操作,先不使用这个)

3.人脸特征提取

提取人脸特征信息,用于人脸的特征比对。

4.人脸属性检测

人脸属性,支持检测年龄、性别以及3D角度。

人脸3D角度:俯仰角(pitch), 横滚角(roll), 偏航角(yaw)。

5.活体检测

离线活体检测,静默式识别,在人脸识别过程中判断操作用户是否为真人,有效防御照片、视频、纸张等不同类型的作弊攻击,提高业务安全性,让人脸识别更安全、更快捷,体验更佳。支持单目RGB活体检测、双目(IR/RGB)活体检测,可满足各类人脸识别终端产品活体检测应用。

开造

访问地址: http://ai.arcsoft.com.cn/technology/faceTracking.html 进入开发者中心进行注册以及认证个人信息

1. 点击我的应用 > 新建应用

2.填写信息立即创建 点击 添加SDK

3.选中免费版人脸识别

4. 填写授权码信息

选择平台先选择windows的根据你的电脑配置来 是64位还是32位的, 语言选择Java

5. 介绍sdk文件

构建项目工程

导入项目依赖

<properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
        <druid-spring-boot-starter.version>1.2.6</druid-spring-boot-starter.version>
        <mybatis-spring-boot.version>2.1.4</mybatis-spring-boot.version>
        <pagehelper.boot.version>1.3.0</pagehelper.boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  人脸识别  -->
        <dependency>
            <groupId>com.arcsoft.face</groupId>
            <artifactId>arcsoft-sdk-face</artifactId>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/arcsoft-sdk-face-3.0.0.0.jar</systemPath>
            <version>3.0.0.0</version>
        </dependency>

        <!-- pool 对象池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0.1-jre</version>
        </dependency>

        <!-- Mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- mybatis-plus 增强CRUD -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!-- pagehelper 分页插件 内置mybatis 依赖-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper.boot.version}</version>
        </dependency>

        <!-- 阿里数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Spring框架基本的核心工具 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <!-- JSON工具类 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.0</version>
        </dependency>
        <!-- SpringWeb模块 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <!-- servlet包 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <!-- aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>top.yangbuyi.YangbuyiFaceDemoApplication</mainClass>
                    <!-- 文件要配置includeSystemScope属性,否则可能会导致arcsoft-sdk-face-3.0.0.0.jar获取不到 -->
                    <includeSystemScope>true</includeSystemScope>
                    <fork>true</fork>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

使用代码生成器生成CURD

版本请对应图片当中的jebat全家桶群里获取3秒破解使用

生成完毕后在根目录创建lib目录将下载下来的人脸识别依赖导入->右击添加到库

application.yml 修改配置文件

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8080
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30

# Spring配置
spring:
  # 同时执行其它配置文件
  profiles:
    active: druid
  mvc: # 把前端的接收到的时间格式 格式化为 yyyy-MM-dd HH:mm:ss
    date-format: yyyy-MM-dd HH:mm:ss
  jackson: # 把后台的时间格式 格式化为 yyyy-MM-dd HH:mm:ss
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  # 服务模块
  devtools:
    restart:
      # 热部署开关
      enabled: true
# MyBatis配置
mybatis-plus:
  # 搜索指定包别名
  typeAliasesPackage: top.yangbuyi.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/*Mapper.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml

# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

# 人脸识别配置

# WIND64
config:
  sdk-lib-path: M:\yangbuyiya-RBAC\libs\WIN64
  app-id: 4QKtmacvsKqaCsoXyyujcs21JTAr79pTczPdZpuaEjhH
  sdk-key: EgBjrmidnqstaL46msfHukeKanYXCujzeHokf2qcC3br
  thread-pool-size: 5

application-druid.yml 数据源配置

# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      url: jdbc:mysql://127.0.0.1:3308/face?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      username: root
      password: 123456
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /yangbuyi/druid/*
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true

resources 下创建mybatis文件夹创建mybatis-config.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <setting name="cacheEnabled" value="true"/>  <!-- 全局映射器启用缓存 -->
        <setting name="useGeneratedKeys" value="true"/>  <!-- 允许 JDBC 支持自动生成主键 -->
        <setting name="defaultExecutorType" value="REUSE"/> <!-- 配置默认的执行器 -->
        <setting name="logImpl" value="SLF4J"/> <!-- 指定 MyBatis 所用日志的具体实现 -->
        <!-- <setting name="mapUnderscoreToCamelCase" value="true"/>  驼峰式命名 -->
    </settings>

</configuration>

项目基础文件配置

创建config文件

创建ApplicationConfig全局配置类

/**
 * 程序注解配置
 *
 * @author yangbuyi
 */
@Configuration
// 表示通过aop框架暴露该代理对象,AopContext能够访问
@EnableAspectJAutoProxy(exposeProxy = true)
// 指定要扫描的Mapper类的包的路径
@MapperScan("top.yangbuyi.mapper")
public class ApplicationConfig {
    /**
     * 时区配置
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization () {
        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
    }

    /**
     * 处理Long类型精度丢失
     *
     * @return
     */
    @Bean("jackson2ObjectMapperBuilderCustomizer")
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer () {
        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance)
                                                     .serializerByType(Long.TYPE, ToStringSerializer.instance);
    }
}

创建全局MybatisPlusConfig配置

/**
 * @program: yangbuyi-rbac
 * @ClassName: MybatisPlusConfig
 * @create: 2022-04-25 15:48
 * @author: yangbuyi.top
 * @since: JDK1.8
 * @MybatisPlusConfig: $
 **/

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor () {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 阻断插件
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
        return interceptor;
    }

    /**
     * 分页插件,自动识别数据库类型 http://baomidou.com/guide/interceptor-pagination.html
     */
    public PaginationInnerInterceptor paginationInnerInterceptor () {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        // 设置数据库类型为mysql
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInnerInterceptor.setMaxLimit(-1L);
        return paginationInnerInterceptor;
    }

    /**
     * 乐观锁插件 http://baomidou.com/guide/interceptor-optimistic-locker.html
     */
    public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor () {
        return new OptimisticLockerInnerInterceptor();
    }

    /**
     * 如果是对全表的删除或更新操作,就会终止该操作 http://baomidou.com/guide/interceptor-block-attack.html
     */
    public BlockAttackInnerInterceptor blockAttackInnerInterceptor () {
        return new BlockAttackInnerInterceptor();
    }
}

创建dto文件夹

创建人脸返回实体 FaceSearchResDto

/**
 * @author yangbuyi.top
 */
@Data
public class FaceSearchResDto {
    /**
     * 唯一人脸Id
     */
    private String faceId;
    /**
     * 人脸名称
     */
    private String name;
    private Integer similarValue;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private String gender;
    /**
     * 图片
     */
    private String image;

}

创建人脸映射 FaceUserInfo

/**
 * @author yangbuyi.top
 */
@Data
public class FaceUserInfo {
    private int id;
    private int groupId;
    private String faceId;
    private String name;
    private Integer similarValue;
    private byte[] faceFeature;
}

创建年龄映射

/**
 * @author yangbuyi.top
 */
public class ProcessInfo {
    private Integer age;
    private Integer gender;

    public Integer getAge () {
        return age;
    }

    public void setAge (Integer age) {
        this.age = age;
    }

    public Integer getGender () {
        return gender;
    }

    public void setGender (Integer gender) {
        this.gender = gender;
    }
}

创建enums文件夹

创建ErrorCodeEnum枚举类

/**
 * @author yangbuyi.top
 */


public enum ErrorCodeEnum {

    MOK(0, "成功"),
    UNKNOWN(1, "未知错误"),
    INVALID_PARAM(2, "无效参数"),
    UNSUPPORTED(3, "引擎不支持"),
    NO_MEMORY(4, "内存不足"),
    BAD_STATE(5, "状态错误"),
    USER_CANCEL(6, "用户取消相关操作"),
    EXPIRED(7, "操作时间过期"),
    USER_PAUSE(8, "用户暂停操作"),
    BUFFER_OVERFLOW(9, "缓冲上溢"),
    BUFFER_UNDERFLOW(10, "缓冲下溢"),
    NO_DISKSPACE(11, "存贮空间不足"),
    COMPONENT_NOT_EXIST(12, "组件不存在"),
    GLOBAL_DATA_NOT_EXIST(13, "全局数据不存在"),
    NO_FACE_DETECTED(14, "未检出到人脸"),
    FACE_DOES_NOT_MATCH(15, "人脸不匹配"),
    INVALID_APP_ID(28673, "无效的AppId"),
    INVALID_SDK_ID(28674, "无效的SdkKey"),
    INVALID_ID_PAIR(28675, "AppId和SdkKey不匹配"),
    MISMATCH_ID_AND_SDK(28676, "SdkKey 和使用的SDK 不匹配"),
    SYSTEM_VERSION_UNSUPPORTED(28677, "系统版本不被当前SDK所支持"),
    LICENCE_EXPIRED(28678, "SDK有效期过期,需要重新下载更新"),
    APS_ENGINE_HANDLE(69633, "引擎句柄非法"),
    APS_MEMMGR_HANDLE(69634, "内存句柄非法"),
    APS_DEVICEID_INVALID(69635, " Device ID 非法"),
    APS_DEVICEID_UNSUPPORTED(69636, "Device ID 不支持"),
    APS_MODEL_HANDLE(69637, "模板数据指针非法"),
    APS_MODEL_SIZE(69638, "模板数据长度非法"),
    APS_IMAGE_HANDLE(69639, "图像结构体指针非法"),
    APS_IMAGE_FORMAT_UNSUPPORTED(69640, "图像格式不支持"),
    APS_IMAGE_PARAM(69641, "图像参数非法"),
    APS_IMAGE_SIZE(69642, "图像尺寸大小超过支持范围"),
    APS_DEVICE_AVX2_UNSUPPORTED(69643, "处理器不支持AVX2指令"),
    FR_INVALID_MEMORY_INFO(73729, "无效的输入内存"),
    FR_INVALID_IMAGE_INFO(73730, "无效的输入图像参数"),
    FR_INVALID_FACE_INFO(73731, "无效的脸部信息"),
    FR_NO_GPU_AVAILABLE(73732, "当前设备无GPU可用"),
    FR_MISMATCHED_FEATURE_LEVEL(73733, "待比较的两个人脸特征的版本不一致"),
    FACEFEATURE_UNKNOWN(81921, "人脸特征检测错误未知"),
    FACEFEATURE_MEMORY(81922, "人脸特征检测内存错误"),
    FACEFEATURE_INVALID_FORMAT(81923, "人脸特征检测格式错误"),
    FACEFEATURE_INVALID_PARAM(81924, "人脸特征检测参数错误"),
    FACEFEATURE_LOW_CONFIDENCE_LEVEL(81925, "人脸特征检测结果置信度低"),
    ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_INIT(86017, "Engine不支持的检测属性"),
    ASF_EX_BASE_FEATURE_UNINITED(86018, "需要检测的属性未初始化"),
    ASF_EX_BASE_FEATURE_UNPROCESSED(86019, "待获取的属性未在process中处理过"),
    ASF_EX_BASE_FEATURE_UNSUPPORTED_ON_PROCESS(86020, "PROCESS不支持的检测属性,例如FR,有自己独立的处理函数"),
    ASF_EX_BASE_INVALID_IMAGE_INFO(86021, "无效的输入图像"),
    ASF_EX_BASE_INVALID_FACE_INFO(86022, "无效的脸部信息"),
    ASF_BASE_ACTIVATION_FAIL(90113, "人脸比对SDK激活失败,请打开读写权限"),
    ASF_BASE_ALREADY_ACTIVATED(90114, "人脸比对SDK已激活"),
    ASF_BASE_NOT_ACTIVATED(90115, "人脸比对SDK未激活"),
    ASF_BASE_SCALE_NOT_SUPPORT(90116, "detectFaceScaleVal 不支持"),
    ASF_BASE_VERION_MISMATCH(90117, "SDK版本不匹配"),
    ASF_BASE_DEVICE_MISMATCH(90118, "设备不匹配"),
    ASF_BASE_UNIQUE_IDENTIFIER_MISMATCH(90119, "唯一标识不匹配"),
    ASF_BASE_PARAM_NULL(90120, "参数为空"),
    ASF_BASE_SDK_EXPIRED(90121, "SDK已过期"),
    ASF_BASE_VERSION_NOT_SUPPORT(90122, "版本不支持"),
    ASF_BASE_SIGN_ERROR(90123, "签名错误"),
    ASF_BASE_DATABASE_ERROR(90124, "数据库插入错误"),
    ASF_BASE_UNIQUE_CHECKOUT_FAIL(90125, "唯一标识符校验失败"),
    ASF_BASE_COLOR_SPACE_NOT_SUPPORT(90126, "输入的颜色空间不支持"),
    ASF_BASE_IMAGE_WIDTH_NOT_SUPPORT(90127, "输入图像的byte数据长度不正确"),
    ASF_NETWORK_BASE_COULDNT_RESOLVE_HOST(94209, "无法解析主机地址"),
    ASF_NETWORK_BASE_COULDNT_CONNECT_SERVER(94210, "无法连接服务器"),
    ASF_NETWORK_BASE_CONNECT_TIMEOUT(94211, "网络连接超时"),
    ASF_NETWORK_BASE_UNKNOWN_ERROR(94212, "未知错误");


    private Integer code;
    private String description;

    ErrorCodeEnum (Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    public Integer getCode () {
        return code;
    }

    public void setCode (Integer code) {
        this.code = code;
    }

    public String getDescription () {
        return description;
    }

    public void setDescription (String description) {
        this.description = description;
    }

    public static ErrorCodeEnum getDescriptionByCode (Integer code) {
        for (ErrorCodeEnum errorCodeEnum : ErrorCodeEnum.values()) {
            if (code.equals(errorCodeEnum.getCode())) {
                return errorCodeEnum;
            }
        }
        return ErrorCodeEnum.UNKNOWN;
    }

}

创建utils文件夹

创建Base64DecodeMultipartFile

package top.yangbuyi.utils;

import org.springframework.web.multipart.MultipartFile;

import java.io.*;

/**
 * @author yangbuyi.top
 * @program: yangbuyi-rbac
 * @ClassName: Base64DecodeMultipartFile
 * @create: 2022-04-25 16:25
 * @since: JDK1.8
 * @Base64DecodeMultipartFile: $
 **/

public class Base64DecodeMultipartFile implements MultipartFile {
    private final byte[] imgContent;
    private final String header;

    public Base64DecodeMultipartFile (byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }

    @Override
    public String getName () {
        return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
    }

    @Override
    public String getOriginalFilename () {
        return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
    }

    @Override
    public String getContentType () {
        return header.split(":")[1];
    }

    @Override
    public boolean isEmpty () {
        return imgContent == null || imgContent.length == 0;
    }

    @Override
    public long getSize () {
        return imgContent.length;
    }

    @Override
    public byte[] getBytes () throws IOException {
        return imgContent;
    }

    @Override
    public InputStream getInputStream () throws IOException {
        return new ByteArrayInputStream(imgContent);
    }

    @Override
    public void transferTo (File dest) throws IOException, IllegalStateException {
        new FileOutputStream(dest).write(imgContent);
    }
}

创建ImageUtils

package top.yangbuyi.utils;

import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;

/**
 * 图片处理工具类
 *
 * @author yangbuyi.top
 */
public class ImageUtils {
    private static final Logger log = LoggerFactory.getLogger(ImageUtils.class);


    public static MultipartFile base64ToMultipartFile (String base64) {
        //base64编码后的图片有头信息所以要分离出来 [0]data:image/png;base64, 图片内容为索引[1]
        String[] baseStrs = base64.split(",");

        //取索引为1的元素进行处理
        byte[] b = Base64.decodeBase64(baseStrs[1]);
        for (int i = 0; i < b.length; ++i) {
            if (b[i] < 0) {
                b[i] += 256;
            }
        }

        //处理过后的数据通过Base64DecodeMultipartFile转换为MultipartFile对象
        return new Base64DecodeMultipartFile(b, baseStrs[0]);
    }

}

项目基本需要的配置与工具已经创建完毕接下来我们开始人脸识别业务编写

实现BasePooledObjectFactory自定义引擎工厂

创建factory文件夹->创建FaceEngineFactory类

import com.arcsoft.face.EngineConfiguration;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.enums.DetectMode;
import com.arcsoft.face.enums.DetectOrient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

/**
 * 引擎工厂
 * @author yangbuyi.top
 */
@Slf4j
public class FaceEngineFactory extends BasePooledObjectFactory<FaceEngine> {

    private final String appId;
    private final String sdkKey;
    private final String sdkLibPath;
    private final EngineConfiguration engineConfiguration;
    private final Integer detectFaceMaxNum = 10;
    private final Integer detectFaceScaleVal = 16;
    private final DetectMode detectMode = DetectMode.ASF_DETECT_MODE_IMAGE;
    private final DetectMode detectVideo = DetectMode.ASF_DETECT_MODE_VIDEO;
    private final DetectOrient detectFaceOrientPriority = DetectOrient.ASF_OP_0_ONLY;


    public FaceEngineFactory (String sdkLibPath, String appId, String sdkKey, EngineConfiguration engineConfiguration) {
        this.sdkLibPath = sdkLibPath;
        this.appId = appId;
        this.sdkKey = sdkKey;
        this.engineConfiguration = engineConfiguration;

    }


    @Override
    public FaceEngine create () throws Exception {
        FaceEngine faceEngine = new FaceEngine(sdkLibPath);
        // 用于在线激活SDK---
        int activeCode = faceEngine.activeOnline(appId, sdkKey);
        log.info("在线激活SDK完毕!,{}", activeCode);
        int initCode = faceEngine.init(engineConfiguration);
        log.info("初始化功能引擎完毕!,{}", initCode);
        return faceEngine;
    }

    @Override
    public PooledObject<FaceEngine> wrap (FaceEngine faceEngine) {
        return new DefaultPooledObject<>(faceEngine);
    }


    @Override
    public void destroyObject (PooledObject<FaceEngine> p) throws Exception {
        FaceEngine faceEngine = p.getObject();
        int unInitCode = faceEngine.unInit();
        super.destroyObject(p);
        log.info("销毁对象完毕! faceEngineUnInitCode: {}", unInitCode);
    }
}

一、编写人脸识别业务接口

在service目录下创建-> FaceEngineService

package top.yangbuyi.service;

import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.toolkit.ImageInfo;
import top.yangbuyi.dto.FaceUserInfo;
import top.yangbuyi.dto.ProcessInfo;

import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * 人脸识别接口
 */
public interface FaceEngineService {

    /**
     * 人脸检测
     * @param imageInfo
     * @return
     */
    List<FaceInfo> detectFaces (ImageInfo imageInfo);

    /**
     * 提取年龄-性别
     * @param imageInfo
     * @return
     */
    List<ProcessInfo> process (ImageInfo imageInfo);

    /**
     * 人脸特征
     * @param imageInfo
     * @return
     */
    byte[] extractFaceFeature (ImageInfo imageInfo) throws InterruptedException;

    /**
     * 人脸比对
     * @param groupId
     * @param faceFeature
     * @return
     */
    List<FaceUserInfo> compareFaceFeature (byte[] faceFeature, Integer groupId) throws InterruptedException, ExecutionException;

}

service.impl 包下创建 FaceEngineServiceImpl 实现类

import cn.hutool.core.collection.CollectionUtil;
import com.arcsoft.face.*;
import com.arcsoft.face.enums.DetectMode;
import com.arcsoft.face.enums.DetectOrient;
import com.arcsoft.face.toolkit.ImageInfo;
import com.google.common.collect.Lists;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import top.yangbuyi.dto.FaceUserInfo;
import top.yangbuyi.dto.ProcessInfo;
import top.yangbuyi.factory.FaceEngineFactory;
import top.yangbuyi.mapper.SysUserFaceInfoMapper;
import top.yangbuyi.service.FaceEngineService;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;


/**
 * @author yangbuyi.top
 */
@Service
public class FaceEngineServiceImpl implements FaceEngineService {

    public final static Logger logger = LoggerFactory.getLogger(FaceEngineServiceImpl.class);

    @Value("${config.sdk-lib-path}")
    public String sdkLibPath;

    @Value("${config.app-id}")
    public String appId;

    @Value("${config.sdk-key}")
    public String sdkKey;

    @Value("${config.thread-pool-size}")
    public Integer threadPoolSize;

    /**
     * 人脸识别引擎
     */
    private static FaceEngine faceEngine;

    /**
     * 相似度
     */
    private final Integer passRate = 95;

    private ExecutorService executorService;

    @Autowired
    private SysUserFaceInfoMapper userFaceInfoMapper;

    /**
     * 线程池
     */
    private GenericObjectPool<FaceEngine> faceEngineObjectPool;


    /**
     * 项目启动时初始化线程池与人脸识别引擎配置
     */
    @PostConstruct
    public void init () {
        executorService = Executors.newFixedThreadPool(threadPoolSize);
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(threadPoolSize);
        poolConfig.setMaxTotal(threadPoolSize);
        poolConfig.setMinIdle(threadPoolSize);
        poolConfig.setLifo(false);

        //引擎配置
        EngineConfiguration engineConfiguration = new EngineConfiguration();
        engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
        engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);

        //功能配置 对应的功能请查看文档
        FunctionConfiguration functionConfiguration = new FunctionConfiguration();
        functionConfiguration.setSupportAge(true);
        functionConfiguration.setSupportFace3dAngle(true);
        functionConfiguration.setSupportFaceDetect(true);
        functionConfiguration.setSupportFaceRecognition(true);
        functionConfiguration.setSupportGender(true);
        functionConfiguration.setSupportLiveness(true);
        functionConfiguration.setSupportIRLiveness(true);
        engineConfiguration.setFunctionConfiguration(functionConfiguration);

        // 底层库算法对象池
        faceEngineObjectPool = new GenericObjectPool(new FaceEngineFactory(sdkLibPath, appId, sdkKey, engineConfiguration), poolConfig);
        try {
            faceEngine = faceEngineObjectPool.borrowObject();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 确保精度
     * @param value
     * @return
     */
    private int plusHundred (Float value) {
        BigDecimal target = new BigDecimal(value);
        BigDecimal hundred = new BigDecimal(100f);
        return target.multiply(hundred).intValue();

    }

}

人脸识别逻辑

1、必须进行人脸特征获取 -> 特征获取成功 -> 进入人脸对比 -> 人脸检测 -> 返回人脸数据

2、必须进行人脸特征获取 -> 特征获取失败 -> 直接跳出返回 未检出到人脸

编写人脸特征获取逻辑

/**
 * 人脸特征
 *
 * @param imageInfo
 * @return
 */
@Override
public byte[]extractFaceFeature(ImageInfo imageInfo)throws InterruptedException{

        FaceEngine faceEngine=null;
        try{
        //获取引擎对象
        faceEngine=faceEngineObjectPool.borrowObject();

        //人脸检测得到人脸列表
        List<FaceInfo> faceInfoList=new ArrayList<FaceInfo>();

        //人脸检测
        int i=faceEngine.detectFaces(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList);

        if(CollectionUtil.isNotEmpty(faceInfoList)){
        FaceFeature faceFeature=new FaceFeature();
        //提取人脸特征
        faceEngine.extractFaceFeature(imageInfo.getImageData(),imageInfo.getWidth(),imageInfo.getHeight(),imageInfo.getImageFormat(),faceInfoList.get(0),faceFeature);

        return faceFeature.getFeatureData();
        }
        }catch(Exception e){
        logger.error("",e);
        }finally{
        if(faceEngine!=null){
        //释放引擎对象
        faceEngineObjectPool.returnObject(faceEngine);
        }

        }

        return null;
        }

编写人脸对比逻辑

/**
 * 人脸比对
 * @param groupId
 * @param faceFeature
 * @return 人脸组
 */
@Override
public List<FaceUserInfo> compareFaceFeature(byte[]faceFeature,Integer groupId)throws InterruptedException,ExecutionException{
        // 识别到的人脸列表
        List<FaceUserInfo> resultFaceInfoList=Lists.newLinkedList();
        // 创建人脸特征对象
        FaceFeature targetFaceFeature=new FaceFeature();
        targetFaceFeature.setFeatureData(faceFeature);
        // 根据分组拿人脸库,从数据库中取出人脸库
        List<FaceUserInfo> faceInfoList=userFaceInfoMapper.getUserFaceInfoByGroupId(groupId);
        // 分成50一组,多线程处理, 数据量大1000组
        List<List<FaceUserInfo>>faceUserInfoPartList=Lists.partition(faceInfoList,50);
        // 多线程
        CompletionService<List<FaceUserInfo>>completionService=new ExecutorCompletionService(executorService);
        for(List<FaceUserInfo> part:faceUserInfoPartList){
        // 开始线程扫描人脸匹配度
        completionService.submit(new CompareFaceTask(part,targetFaceFeature));
        }
        // 获取线程任务参数
        for(List<FaceUserInfo> faceUserInfos:faceUserInfoPartList){
        List<FaceUserInfo> faceUserInfoList=completionService.take().get();
        if(CollectionUtil.isNotEmpty(faceInfoList)){
        resultFaceInfoList.addAll(faceUserInfoList);
        }
        }
        // 从大到小排序
        resultFaceInfoList.sort((h1,h2)->h2.getSimilarValue().compareTo(h1.getSimilarValue()));
        return resultFaceInfoList;
        }


/**
 * 多线程跑人脸对比逻辑
 */
private class CompareFaceTask implements Callable<List<FaceUserInfo>> {

    private final List<FaceUserInfo> faceUserInfoList;
    private final FaceFeature targetFaceFeature;


    public CompareFaceTask (List<FaceUserInfo> faceUserInfoList, FaceFeature targetFaceFeature) {
        this.faceUserInfoList = faceUserInfoList;
        this.targetFaceFeature = targetFaceFeature;
    }

    @Override
    public List<FaceUserInfo> call () throws Exception {
        FaceEngine faceEngine = null;
        //识别到的人脸列表
        List<FaceUserInfo> resultFaceInfoList = Lists.newLinkedList();
        try {
            faceEngine = faceEngineObjectPool.borrowObject();
            for (FaceUserInfo faceUserInfo : faceUserInfoList) {
                FaceFeature sourceFaceFeature = new FaceFeature();
                // 设置人脸特征
                sourceFaceFeature.setFeatureData(faceUserInfo.getFaceFeature());
                FaceSimilar faceSimilar = new FaceSimilar();
                faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar);
                //获取相似值
                Integer similarValue = plusHundred(faceSimilar.getScore());
                //相似值大于配置预期,加入到识别到人脸的列表
                if (similarValue > passRate) {
                    FaceUserInfo info = new FaceUserInfo();
                    info.setName(faceUserInfo.getName());
                    info.setFaceId(faceUserInfo.getFaceId());
                    info.setSimilarValue(similarValue);
                    resultFaceInfoList.add(info);
                }
            }
        } catch (Exception e) {
            logger.error("", e);
        } finally {
            if (faceEngine != null) {
                faceEngineObjectPool.returnObject(faceEngine);
            }
        }
        return resultFaceInfoList;
    }

}

编写根据根据分组ID获取人脸库数据

在mapper下SysUserFaceInfoMapper -> 创建接口 getUserFaceInfoByGroupId

/**
 * 根据分组Id
 * 从数据库中取出人脸库
 *
 * @param groupId
 * @return 人脸库
 */
    List<FaceUserInfo> getUserFaceInfoByGroupId(Integer groupId);

sql实现

<resultMap id="userFace2" type="top.yangbuyi.dto.FaceUserInfo">
<id column="id" property="id" javaType="int"/>
<result column="group_id" property="groupId" javaType="java.lang.Integer"/>
<result column="name" property="name" javaType="java.lang.String"/>
<result column="face_id" property="faceId" javaType="String"/>
<result column="face_feature" property="faceFeature"/>
</resultMap>

<
select id = "getUserFaceInfoByGroupId" resultMap="userFace2" parameterType="java.lang.Integer"
        resultType="top.yangbuyi.dto.FaceUserInfo">
select id, group_id, face_id, name, face_feature
from `sys_user_face_info`
where group_id = #{groupId}
          < /
select>

二、编写人脸添加业务接口

userFaceInfoService 新增人脸业务接口

/**
 * 新增用户人脸识别
 *
 * @param sysUserFaceInfo 用户人脸识别
 * @return 结果
 */
public int insertSysUserFaceInfo(SysUserFaceInfo sysUserFaceInfo);
就是一共简简单单的新增插入数据 这个就不用我教了吧?????????

三、 编写Controller业务控制层

import top.yangbuyi.domain.AjaxResult;

/**
 * (sys_user_face_info)表控制层
 *
 * @author yangbuyi.top
 */
@RestController
@Slf4j
@RequestMapping("face")
@RequiredArgsConstructor
public class SysUserFaceInfoController {
    public final static Logger logger = LoggerFactory.getLogger(SysUserFaceInfoController.class);

    private final FaceEngineService faceEngineService;

    private final SysUserFaceInfoService userFaceInfoService;


    /**
     * 人脸添加
     *
     * @param file    人脸附件
     * @param groupId 分组id
     * @param name    用户登录名称
     */
    @RequestMapping(value = "/faceAdd", method = RequestMethod.POST)
    public AjaxResult faceAdd (@RequestBody Map<String, Object> map) {
        // 业务.... yangbuyi.top 版权所有
        return AjaxResult.success();
    }


    /**
     * 人脸识别
     *
     * @param file    人脸附件
     * @param groupId 分组ID 方便快速识别
     */
    @RequestMapping(value = "/faceSearch", method = RequestMethod.POST)
    public AjaxResult faceSearch (@RequestBody Map<String, Object> map) throws Exception {
        // 业务... yangbuyi.top 版权所有
        return AjaxResult.success();
    }

faceAdd 具体业务编写

  1. 根据前端传递的

    file -> 人脸图片

    groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)

    name -> 当前用户名称(实际开发当中应该是存储id)

String file = String.valueOf(map.get("file"));
        String groupId = String.valueOf(map.get("groupId"));
        String name = String.valueOf(map.get("name"));
        try {
            if (file == null) {
                return AjaxResult.error("file is null");
            }
            if (groupId == null) {
                return AjaxResult.error("file is null");
            }
            if (name == null) {
                return AjaxResult.error("file is null");
            }
            // 转换实体
            byte[] decode = Base64.decode(base64Process(file));
            ImageInfo imageInfo = ImageFactory.getRGBData(decode);

            //人脸特征获取
            byte[] bytes = faceEngineService.extractFaceFeature(imageInfo);
            if (bytes == null) {
                return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription());
            }
            List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo);
            // 开始将人脸识别Base64转换文件流->用于文件上传
            // final MultipartFile multipartFile = ImageUtils.base64ToMultipartFile(file);
            final SysUserFaceInfo one = this.userFaceInfoService.lambdaQuery().eq(SysUserFaceInfo::getName, name).one();
            // 如果存在则更新人脸和特征
            if (null != one) {
                this.userFaceInfoService.lambdaUpdate().set(SysUserFaceInfo::getFaceFeature, bytes)
                        .set(SysUserFaceInfo::getFpath, "存储头像地址")
                        .eq(SysUserFaceInfo::getFaceId, name).update();
            } else {
                // 组装人脸实体
                SysUserFaceInfo userFaceInfo = new SysUserFaceInfo();
                userFaceInfo.setName(name);
                userFaceInfo.setAge(processInfoList.get(0).getAge());
                userFaceInfo.setGender(processInfoList.get(0).getGender().shortValue());
                userFaceInfo.setGroupId(Integer.valueOf(groupId));
                userFaceInfo.setFaceFeature(bytes);
                userFaceInfo.setFpath("存储头像地址");
                // 存储用户ID -> 我这里先使用name代替 -> 假如是唯一性
                userFaceInfo.setFaceId(name);
                //人脸特征插入到数据库
                userFaceInfoService.insertSysUserFaceInfo(userFaceInfo);
            }
            logger.info("faceAdd:" + name);
            return AjaxResult.success("人脸绑定成功!");
        } catch (Exception e) {
            logger.error("", e);
        }
        // 错误返回
        return AjaxResult.error(ErrorCodeEnum.UNKNOWN.getDescription());

faceSearch 具体业务编写

  1. 根据前端传递的

    file -> 人脸图片

    groupId -> 用户分组(用于缩小范围查询该人员的位置有效减少数据量大的问题)

String file = String.valueOf(map.get("file"));
        String groupId = String.valueOf(map.get("groupId"));

        if (groupId == null) {
            return AjaxResult.error("groupId is null");
        }
        byte[] decode = Base64.decode(base64Process(file));
        BufferedImage bufImage = ImageIO.read(new ByteArrayInputStream(decode));
        ImageInfo imageInfo = ImageFactory.bufferedImage2ImageInfo(bufImage);


        //人脸特征获取
        byte[] bytes = faceEngineService.extractFaceFeature(imageInfo);
        // 校验是否显示出人脸
        if (bytes == null) {
            return AjaxResult.error(ErrorCodeEnum.NO_FACE_DETECTED.getDescription());
        }

        //人脸比对,获取比对结果
        List<FaceUserInfo> userFaceInfoList = faceEngineService.compareFaceFeature(bytes, Integer.valueOf(groupId));

        if (CollectionUtil.isNotEmpty(userFaceInfoList)) {
            FaceUserInfo faceUserInfo = userFaceInfoList.get(0);
            FaceSearchResDto faceSearchResDto = new FaceSearchResDto();
            BeanUtil.copyProperties(faceUserInfo, faceSearchResDto);
            List<ProcessInfo> processInfoList = faceEngineService.process(imageInfo);
            if (CollectionUtil.isNotEmpty(processInfoList)) {
                //人脸检测
                List<FaceInfo> faceInfoList = faceEngineService.detectFaces(imageInfo);
                int left = faceInfoList.get(0).getRect().getLeft();
                int top = faceInfoList.get(0).getRect().getTop();
                int width = faceInfoList.get(0).getRect().getRight() - left;
                int height = faceInfoList.get(0).getRect().getBottom() - top;

                Graphics2D graphics2D = bufImage.createGraphics();
                // 红色
                graphics2D.setColor(Color.RED);
                BasicStroke stroke = new BasicStroke(5f);
                graphics2D.setStroke(stroke);
                graphics2D.drawRect(left, top, width, height);
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "jpg", outputStream);
                byte[] bytes1 = outputStream.toByteArray();
                faceSearchResDto.setImage("data:image/jpeg;base64," + Base64Utils.encodeToString(bytes1));
                faceSearchResDto.setAge(processInfoList.get(0).getAge());
                faceSearchResDto.setGender(processInfoList.get(0).getGender().equals(1) ? "女" : "男");
            }
            return AjaxResult.success(faceSearchResDto);
        }
        return AjaxResult.error(ErrorCodeEnum.FACE_DOES_NOT_MATCH.getDescription());

测试接口流程

准备两张图片

百度随便搜索个在线转换 Base64

( 在线图片转Base64 )

1、人脸添加

新增有脸的

新增测试无脸的

2、人脸识别

有脸的

无脸的

结尾在线演示

( 前往享受人脸识别 )