mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-11 20:28:56 +00:00
1、新增图片与视频活体检测
2、新增人脸属性识别(性别、年龄、口罩、姿态、眼睛状态) 3、优化检测返回与包结构 4、新增 dependencyManagement 统一依赖版本管理
This commit is contained in:
66
README.md
66
README.md
@@ -17,6 +17,12 @@
|
|||||||
<a target="_blank" href='https://gitee.com/dengwenjie/SmartJavaAI/stargazers'>
|
<a target="_blank" href='https://gitee.com/dengwenjie/SmartJavaAI/stargazers'>
|
||||||
<img src='https://gitee.com/dengwenjie/SmartJavaAI/badge/star.svg?theme=gvp' alt='star'/>
|
<img src='https://gitee.com/dengwenjie/SmartJavaAI/badge/star.svg?theme=gvp' alt='star'/>
|
||||||
</a>
|
</a>
|
||||||
|
<a target="_blank" href='https://github.com/geekwenjie/SmartJavaAI'>
|
||||||
|
<img src="https://img.shields.io/github/stars/geekwenjie/SmartJavaAI.svg?style=social" alt="github star"/>
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href='https://gitcode.com/geekwenjie/SmartJavaAI'>
|
||||||
|
<img src="https://gitcode.com/geekwenjie/SmartJavaAI/star/badge.svg" alt="gitcode star"/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
@@ -86,7 +92,37 @@
|
|||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-n.png" width = "500px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-n.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div align="left">
|
||||||
|
<p>人脸属性检测</p>
|
||||||
|
- 性别检测 <br>
|
||||||
|
- 年龄检测 <br>
|
||||||
|
- 口罩检测<br>
|
||||||
|
- 眼睛状态检测<br>
|
||||||
|
- 脸部姿态检测<br>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face_attribute.jpg" width = "500px"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div align="left">
|
||||||
|
<p>活体检测</p>
|
||||||
|
- 图片和视频活体检测 <br>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/liveness.jpg" width = "500px"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div align="left">
|
<div align="left">
|
||||||
@@ -127,10 +163,14 @@
|
|||||||
|
|
||||||
### ✅ 已实现功能
|
### ✅ 已实现功能
|
||||||
|
|
||||||
- **人脸检测**
|
- **人脸识别**
|
||||||
人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
|
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
|
||||||
|
- 5点人脸关键点定位
|
||||||
|
- 人脸属性检测(性别、年龄、口罩、眼睛状态、脸部姿态)
|
||||||
|
- 人脸活体检测:图片、视频活体检测
|
||||||
- **目标检测**
|
- **目标检测**
|
||||||
支持通用目标检测,能够识别图像中的多种物体类别,返回物体位置与类别信息
|
- 支持YOLO、resnet50、VGG16等目标检测模型
|
||||||
|
- 支持通用目标检测,能够识别图像中的多种物体类别,返回物体位置与类别信息
|
||||||
|
|
||||||
### ⌛ 规划中功能
|
### ⌛ 规划中功能
|
||||||
|
|
||||||
@@ -182,13 +222,16 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-all</artifactId>
|
<artifactId>smartjavaai-all</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
### 3、完整示例代码
|
### 3、完整示例代码
|
||||||
|
|
||||||
[示例代码](https://gitee.com/dengwenjie/SmartJavaAI-Demo)
|
[示例代码](https://gitee.com/dengwenjie/SmartJavaAI-Demo)
|
||||||
|
|
||||||
|
### 4、文档地址
|
||||||
|
|
||||||
|
[开发文档](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
|
|
||||||
## 🙏 鸣谢
|
## 🙏 鸣谢
|
||||||
@@ -208,19 +251,18 @@
|
|||||||
- **微信**: deng775747758 (请备注:SmartJavaAI)
|
- **微信**: deng775747758 (请备注:SmartJavaAI)
|
||||||
- **Email**: 775747758@qq.com
|
- **Email**: 775747758@qq.com
|
||||||
|
|
||||||
#### 加微信群
|
|
||||||
|
|
||||||
<p align="left">
|
|
||||||
<a href="https://gitee.com/dengwenjie/SmartJavaAI"><img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/wechat_group.jpg" width="45%"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
🚀 **如果这个项目对你有帮助,别忘了点个 Star ⭐!你的支持是我持续优化升级的动力!** ❤️
|
🚀 **如果这个项目对你有帮助,别忘了点个 Star ⭐!你的支持是我持续优化升级的动力!** ❤️
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
## [v1.0.12] - 2025-05-09
|
||||||
|
- 新增图片与视频活体检测
|
||||||
|
- 新增人脸属性识别(性别、年龄、口罩、姿态、眼睛状态)
|
||||||
|
- 优化检测返回与包结构
|
||||||
|
- 新增 dependencyManagement 统一依赖版本管理
|
||||||
|
|
||||||
## [v1.0.11] - 2025-04-28
|
## [v1.0.11] - 2025-04-28
|
||||||
- FaceNet 特征提取新增人脸对齐
|
- FaceNet 特征提取新增人脸对齐
|
||||||
- 人脸检测新5点人脸关键点定位
|
- 人脸检测新5点人脸关键点定位
|
||||||
|
|||||||
43
pom.xml
43
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<modules>
|
<modules>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<module>smartjavaai-objectdetection</module>
|
<module>smartjavaai-objectdetection</module>
|
||||||
<module>smartjavaai-all</module>
|
<module>smartjavaai-all</module>
|
||||||
<module>smartjavaai-ocr</module>
|
<module>smartjavaai-ocr</module>
|
||||||
|
<module>smartjavaai-bom</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -38,13 +39,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-common</artifactId>
|
<artifactId>smartjavaai-common</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-face</artifactId>
|
<artifactId>smartjavaai-face</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
@@ -63,18 +64,18 @@
|
|||||||
<groupId>ai.djl</groupId>
|
<groupId>ai.djl</groupId>
|
||||||
<artifactId>model-zoo</artifactId>
|
<artifactId>model-zoo</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<!-- <dependency>-->
|
||||||
<groupId>ai.djl.timeseries</groupId>
|
<!-- <groupId>ai.djl.timeseries</groupId>-->
|
||||||
<artifactId>timeseries</artifactId>
|
<!-- <artifactId>timeseries</artifactId>-->
|
||||||
</dependency>
|
<!-- </dependency>-->
|
||||||
<dependency>
|
<!-- <dependency>-->
|
||||||
<groupId>ai.djl.huggingface</groupId>
|
<!-- <groupId>ai.djl.huggingface</groupId>-->
|
||||||
<artifactId>tokenizers</artifactId>
|
<!-- <artifactId>tokenizers</artifactId>-->
|
||||||
</dependency>
|
<!-- </dependency>-->
|
||||||
<dependency>
|
<!-- <dependency>-->
|
||||||
<groupId>ai.djl.audio</groupId>
|
<!-- <groupId>ai.djl.audio</groupId>-->
|
||||||
<artifactId>audio</artifactId>
|
<!-- <artifactId>audio</artifactId>-->
|
||||||
</dependency>
|
<!-- </dependency>-->
|
||||||
<!-- MXNet -->
|
<!-- MXNet -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ai.djl.mxnet</groupId>
|
<groupId>ai.djl.mxnet</groupId>
|
||||||
@@ -90,11 +91,7 @@
|
|||||||
<groupId>ai.djl.tensorflow</groupId>
|
<groupId>ai.djl.tensorflow</groupId>
|
||||||
<artifactId>tensorflow-model-zoo</artifactId>
|
<artifactId>tensorflow-model-zoo</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- ONNXRuntime -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>ai.djl.onnxruntime</groupId>
|
|
||||||
<artifactId>onnxruntime-engine</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.testng</groupId>
|
<groupId>org.testng</groupId>
|
||||||
<artifactId>testng</artifactId>
|
<artifactId>testng</artifactId>
|
||||||
@@ -210,6 +207,12 @@
|
|||||||
<version>1.0.3</version>
|
<version>1.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacv</artifactId>
|
||||||
|
<version>1.5.10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>smartjavaai-all</artifactId>
|
<artifactId>smartjavaai-all</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
<name>smartjavaai-all</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
<licenses>
|
<licenses>
|
||||||
@@ -21,6 +21,15 @@
|
|||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
|
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.test.skip>true</maven.test.skip>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
@@ -33,14 +42,42 @@
|
|||||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 引用所有平台包 -->
|
||||||
|
<!-- <dependency>-->
|
||||||
|
<!-- <groupId>org.bytedeco</groupId>-->
|
||||||
|
<!-- <artifactId>javacv-platform</artifactId>-->
|
||||||
|
<!-- <version>1.5.10</version>-->
|
||||||
|
<!-- </dependency>-->
|
||||||
|
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacpp-platform</artifactId>
|
||||||
|
<version>1.5.10</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg-platform</artifactId>
|
||||||
|
<version>6.1.1-1.5.10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas-platform</artifactId>
|
||||||
|
<version>0.3.26-1.5.10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv-platform</artifactId>
|
||||||
|
<version>4.9.0-1.5.10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<properties>
|
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<maven.test.skip>true</maven.test.skip>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
142
smartjavaai-bom/pom.xml
Normal file
142
smartjavaai-bom/pom.xml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>cn.smartjavaai</groupId>
|
||||||
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
|
<version>1.0.12</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<version>1.0.12</version>
|
||||||
|
<artifactId>smartjavaai-bom</artifactId>
|
||||||
|
<name>smartjavaai-bom</name>
|
||||||
|
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<!-- 1. dependencyManagement(用于 import 模式) -->
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.smartjavaai</groupId>
|
||||||
|
<artifactId>smartjavaai-face</artifactId>
|
||||||
|
<version>${project.parent.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.smartjavaai</groupId>
|
||||||
|
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||||
|
<version>${project.parent.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
||||||
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>MIT License</name>
|
||||||
|
<url>https://opensource.org/licenses/MIT</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.sonatype.central</groupId>
|
||||||
|
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||||
|
<version>0.4.0</version>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<configuration>
|
||||||
|
<publishingServerId>dengwenjie</publishingServerId>
|
||||||
|
<tokenAuth>true</tokenAuth>
|
||||||
|
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-source-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-sources</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar-no-fork</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<configuration>
|
||||||
|
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
|
||||||
|
<doclint>none</doclint>
|
||||||
|
<additionalJOptions>
|
||||||
|
<additionalJOption>-Xdoclint:none</additionalJOption>
|
||||||
|
</additionalJOptions>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-javadocs</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-gpg-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>sign-artifacts</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>sign</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<!-- 必须添加:SCM信息 -->
|
||||||
|
<scm>
|
||||||
|
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
||||||
|
<developerConnection>scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git</developerConnection>
|
||||||
|
<url>http://github.com/geekwenjie/SmartJavaAI/tree/master</url>
|
||||||
|
</scm>
|
||||||
|
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<snapshotRepository>
|
||||||
|
<id>dengwenjie</id>
|
||||||
|
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||||
|
</snapshotRepository>
|
||||||
|
<repository>
|
||||||
|
<id>dengwenjie</id>
|
||||||
|
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<name>dengwenjie</name>
|
||||||
|
<email>775747758@qq.com</email>
|
||||||
|
<roles>
|
||||||
|
<role>Project Manager</role>
|
||||||
|
<role>Architect</role>
|
||||||
|
</roles>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>smartjavaai-common</artifactId>
|
<artifactId>smartjavaai-common</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测结果信息
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DetectionInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测位置信息
|
||||||
|
*/
|
||||||
|
private DetectionRectangle detectionRectangle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测得分
|
||||||
|
*/
|
||||||
|
private float score;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
private FaceInfo faceInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标检测信息
|
||||||
|
*/
|
||||||
|
private ObjectDetInfo objectDetInfo;
|
||||||
|
|
||||||
|
public DetectionInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionInfo(DetectionRectangle detectionRectangle) {
|
||||||
|
this.detectionRectangle = detectionRectangle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionInfo(DetectionRectangle detectionRectangle, float score) {
|
||||||
|
this.detectionRectangle = detectionRectangle;
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionInfo(DetectionRectangle detectionRectangle, float score, FaceInfo faceInfo) {
|
||||||
|
this.detectionRectangle = detectionRectangle;
|
||||||
|
this.score = score;
|
||||||
|
this.faceInfo = faceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,98 +1,30 @@
|
|||||||
package cn.smartjavaai.common.entity;
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.GenderType;
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测结果-矩形区域
|
* 检测结果-矩形区域
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
public class DetectionRectangle {
|
public class DetectionRectangle {
|
||||||
|
|
||||||
public int x;
|
public int x;
|
||||||
public int y;
|
public int y;
|
||||||
public int width;
|
public int width;
|
||||||
public int height;
|
public int height;
|
||||||
public float score;
|
|
||||||
public String className;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 人脸关键点
|
|
||||||
*/
|
|
||||||
private List<Point> keyPoints;
|
|
||||||
|
|
||||||
public DetectionRectangle() {
|
public DetectionRectangle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionRectangle(int x, int y, int width, int height, float score) {
|
public DetectionRectangle(int x, int y, int width, int height) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.score = score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DetectionRectangle(int x, int y, int width, int height, float score, String className) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
this.score = score;
|
|
||||||
this.className = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getX() {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setX(int x) {
|
|
||||||
this.x = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getY() {
|
|
||||||
return y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setY(int y) {
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWidth() {
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWidth(int width) {
|
|
||||||
this.width = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeight(int height) {
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getScore() {
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScore(float score) {
|
|
||||||
this.score = score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClassName() {
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClassName(String className) {
|
|
||||||
this.className = className;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Point> getKeyPoints() {
|
|
||||||
return keyPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeyPoints(List<Point> keyPoints) {
|
|
||||||
this.keyPoints = keyPoints;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package cn.smartjavaai.common.entity;
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,15 +9,15 @@ import java.util.List;
|
|||||||
* @author dwj
|
* @author dwj
|
||||||
* @date 2025/4/12
|
* @date 2025/4/12
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
public class DetectionResponse {
|
public class DetectionResponse {
|
||||||
|
|
||||||
private List<DetectionRectangle> rectangleList;
|
private List<DetectionInfo> detectionInfoList;
|
||||||
|
|
||||||
public List<DetectionRectangle> getRectangleList() {
|
public DetectionResponse() {
|
||||||
return rectangleList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRectangleList(List<DetectionRectangle> rectangleList) {
|
public DetectionResponse(List<DetectionInfo> detectionInfoList) {
|
||||||
this.rectangleList = rectangleList;
|
this.detectionInfoList = detectionInfoList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.EyeStatus;
|
||||||
|
import cn.smartjavaai.common.enums.GenderType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class FaceAttribute {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别
|
||||||
|
*/
|
||||||
|
private GenderType genderType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年龄
|
||||||
|
*/
|
||||||
|
private Integer age;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左眼状态
|
||||||
|
*/
|
||||||
|
private EyeStatus leftEyeStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右眼状态
|
||||||
|
*/
|
||||||
|
private EyeStatus rightEyeStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否带口罩
|
||||||
|
*/
|
||||||
|
private Boolean wearingMask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 姿态
|
||||||
|
*/
|
||||||
|
private HeadPose headPose;
|
||||||
|
|
||||||
|
|
||||||
|
public FaceAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸信息
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class FaceInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸关键点
|
||||||
|
*/
|
||||||
|
private List<Point> keyPoints;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性
|
||||||
|
*/
|
||||||
|
private FaceAttribute faceAttribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测结果
|
||||||
|
*/
|
||||||
|
private LivenessStatus livenessStatus;
|
||||||
|
|
||||||
|
public FaceInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceInfo(List<Point> keyPoints) {
|
||||||
|
this.keyPoints = keyPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceInfo(List<Point> keyPoints, FaceAttribute faceAttribute, LivenessStatus livenessStatus) {
|
||||||
|
this.keyPoints = keyPoints;
|
||||||
|
this.faceAttribute = faceAttribute;
|
||||||
|
this.livenessStatus = livenessStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceInfo(FaceAttribute faceAttribute, LivenessStatus livenessStatus) {
|
||||||
|
this.faceAttribute = faceAttribute;
|
||||||
|
this.livenessStatus = livenessStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 姿态检测结果(单位:度)
|
||||||
|
* pitch:上下(俯仰角),正值抬头,负值低头
|
||||||
|
* yaw:左右(偏航角),正值右偏,负值左偏
|
||||||
|
* roll:倾斜(翻滚角),正值右倾,负值左倾
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class HeadPose {
|
||||||
|
|
||||||
|
/** 俯仰角:头上下抬(-90°~+90°) */
|
||||||
|
private Float pitch;
|
||||||
|
|
||||||
|
/** 偏航角:头左右转(-90°~+90°) */
|
||||||
|
private Float yaw;
|
||||||
|
|
||||||
|
/** 翻滚角:头部倾斜(-90°~+90°) */
|
||||||
|
private Float roll;
|
||||||
|
|
||||||
|
public HeadPose() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeadPose(Float pitch, Float yaw, Float roll) {
|
||||||
|
this.pitch = pitch;
|
||||||
|
this.yaw = yaw;
|
||||||
|
this.roll = roll;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cn.smartjavaai.common.entity;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标检测信息
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ObjectDetInfo {
|
||||||
|
|
||||||
|
private String className;
|
||||||
|
|
||||||
|
public ObjectDetInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectDetInfo(String className) {
|
||||||
|
this.className = className;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package cn.smartjavaai.common.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 眼睛状态
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
public enum EyeStatus {
|
||||||
|
|
||||||
|
OPEN(0, "睁眼"),
|
||||||
|
|
||||||
|
CLOSED(1, "闭眼"),
|
||||||
|
|
||||||
|
NON_EYE_REGION(2, "非眼部区域"),
|
||||||
|
UNKNOWN(3, "未知状态");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
EyeStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EyeStatus fromCode(int code) {
|
||||||
|
for (EyeStatus status : EyeStatus.values()) {
|
||||||
|
if (status.getCode() == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cn.smartjavaai.common.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性别枚举
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/6
|
||||||
|
*/
|
||||||
|
public enum GenderType {
|
||||||
|
|
||||||
|
MALE(0, "男"),
|
||||||
|
FEMALE(1, "女"),
|
||||||
|
|
||||||
|
UNKNOWN(2, "未知");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
GenderType(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GenderType fromCode(int code) {
|
||||||
|
for (GenderType status : GenderType.values()) {
|
||||||
|
if (status.getCode() == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cn.smartjavaai.common.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测结果
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/29
|
||||||
|
*/
|
||||||
|
public enum LivenessStatus {
|
||||||
|
|
||||||
|
LIVE(0, "活体"),
|
||||||
|
NON_LIVE(1, "非活体"),
|
||||||
|
UNKNOWN(2, "未知"),
|
||||||
|
DETECTING(3, "正在检测");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
LivenessStatus(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LivenessStatus fromCode(int code) {
|
||||||
|
for (LivenessStatus status : LivenessStatus.values()) {
|
||||||
|
if (status.getCode() == code) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -145,41 +145,6 @@ public class ImageUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 画检测框
|
|
||||||
*
|
|
||||||
* @param image
|
|
||||||
* @param x
|
|
||||||
* @param y
|
|
||||||
* @param width
|
|
||||||
* @param height
|
|
||||||
*/
|
|
||||||
public static void drawImageRect(Image image, DetectionResponse detectionResponse) {
|
|
||||||
if(Objects.nonNull(detectionResponse) && Objects.nonNull(detectionResponse.getRectangleList()) && !detectionResponse.getRectangleList().isEmpty()){
|
|
||||||
// 将绘制图像转换为Graphics2D'
|
|
||||||
BufferedImage bufferedImage = (BufferedImage)image.getWrappedImage();
|
|
||||||
Graphics2D g = (Graphics2D) bufferedImage.getGraphics();
|
|
||||||
try {
|
|
||||||
g.setColor(new Color(0, 255, 0));
|
|
||||||
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
|
|
||||||
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
|
||||||
g.setStroke(bStroke);
|
|
||||||
for(DetectionRectangle detectionRectangle : detectionResponse.getRectangleList()){
|
|
||||||
g.drawRect(detectionRectangle.getX(), detectionRectangle.getY(), detectionRectangle.getWidth(), detectionRectangle.getHeight());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
g.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package cn.smartjavaai.common.utils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.pool2.ObjectPool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/7
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PoolUtils {
|
||||||
|
|
||||||
|
// 泛型方法,支持任意类型的 Predictor 和对象池
|
||||||
|
public static <T> void returnToPool(ObjectPool<T> pool, T predictor) {
|
||||||
|
if (pool == null || predictor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pool.returnObject(predictor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor到池失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>smartjavaai-face</artifactId>
|
<artifactId>smartjavaai-face</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
<name>smartjavaai-face</name>
|
<name>smartjavaai-face</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.test.skip>true</maven.test.skip>
|
<maven.test.skip>true</maven.test.skip>
|
||||||
|
<javacv.version>1.5.8</javacv.version>
|
||||||
|
<javacv.ffmpeg.version>5.1.2-1.5.8</javacv.ffmpeg.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
<version>3.36.0.3</version>
|
<version>3.36.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -117,9 +120,6 @@
|
|||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 必须添加:SCM信息 -->
|
<!-- 必须添加:SCM信息 -->
|
||||||
<scm>
|
<scm>
|
||||||
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
||||||
@@ -151,7 +151,4 @@
|
|||||||
</developers>
|
</developers>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package cn.smartjavaai.face.config;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.face.enums.FaceAttributeModelEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别模型配置
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class FaceAttributeConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别模型枚举
|
||||||
|
*/
|
||||||
|
private FaceAttributeModelEnum modelEnum = FaceAttributeModelEnum.SEETA_FACE6_MODEL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型路径
|
||||||
|
*/
|
||||||
|
private String modelPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备类型
|
||||||
|
*/
|
||||||
|
private DeviceEnum device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gpu设备ID 当device为GPU时生效
|
||||||
|
*/
|
||||||
|
private int gpuId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用年龄检测
|
||||||
|
*/
|
||||||
|
private boolean enableAge = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用性别检测
|
||||||
|
*/
|
||||||
|
private boolean enableGender = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用人脸姿态检测
|
||||||
|
*/
|
||||||
|
private boolean enableHeadPose = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用眼睛状态检测
|
||||||
|
*/
|
||||||
|
private boolean enableEyeStatus = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用口罩检测
|
||||||
|
*/
|
||||||
|
private boolean enableMask = true;
|
||||||
|
|
||||||
|
|
||||||
|
public FaceAttributeConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceAttributeConfig(FaceAttributeModelEnum modelEnum) {
|
||||||
|
this.modelEnum = modelEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceAttributeConfig(FaceAttributeModelEnum modelEnum, String modelPath) {
|
||||||
|
this.modelEnum = modelEnum;
|
||||||
|
this.modelPath = modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FaceAttributeConfig(String modelPath) {
|
||||||
|
this.modelPath = modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.config;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.config;
|
||||||
|
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
|
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模型配置
|
* 人脸检测识别模型配置
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@@ -18,7 +20,7 @@ public class FaceModelConfig {
|
|||||||
/**
|
/**
|
||||||
* 置信度阈值
|
* 置信度阈值
|
||||||
*/
|
*/
|
||||||
private double confidenceThreshold = FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD;
|
private double confidenceThreshold = FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 相似度阈值 作用:判断是否为同一人脸
|
* 相似度阈值 作用:判断是否为同一人脸
|
||||||
@@ -28,7 +30,7 @@ public class FaceModelConfig {
|
|||||||
/**
|
/**
|
||||||
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
|
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
|
||||||
*/
|
*/
|
||||||
private double nmsThresh = FaceConfig.NMS_THRESHOLD;
|
private double nmsThresh = FaceDetectConstant.NMS_THRESHOLD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模型路径
|
* 模型路径
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package cn.smartjavaai.face.config;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
|
import cn.smartjavaai.face.constant.LivenessConstant;
|
||||||
|
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||||
|
import cn.smartjavaai.face.enums.LivenessModelEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测模型配置
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LivenessConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测模型枚举
|
||||||
|
*/
|
||||||
|
private LivenessModelEnum modelEnum = LivenessModelEnum.SEETA_FACE6_MODEL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型路径
|
||||||
|
*/
|
||||||
|
private String modelPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备类型
|
||||||
|
*/
|
||||||
|
private DeviceEnum device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gpu设备ID 当device为GPU时生效
|
||||||
|
*/
|
||||||
|
private int gpuId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸清晰度阈值
|
||||||
|
*/
|
||||||
|
private float faceClarityThreshold = LivenessConstant.DEFAULT_FACE_CLARITY_THRESHOLD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体阈值
|
||||||
|
*/
|
||||||
|
private float realityThreshold = LivenessConstant.DEFAULT_REALITY_THRESHOLD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频检测帧数
|
||||||
|
*/
|
||||||
|
private int frameCount = LivenessConstant.DEFAULT_FRAME_COUNT;
|
||||||
|
|
||||||
|
public LivenessConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivenessConfig(LivenessModelEnum modelEnum) {
|
||||||
|
this.modelEnum = modelEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivenessConfig(LivenessModelEnum modelEnum, String modelPath) {
|
||||||
|
this.modelEnum = modelEnum;
|
||||||
|
this.modelPath = modelPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivenessConfig(String modelPath) {
|
||||||
|
this.modelPath = modelPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.constant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 人脸检测常量
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
public class FaceConfig {
|
public class FaceDetectConstant {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 置信度阈值
|
* 置信度阈值
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package cn.smartjavaai.face.constant;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.face.enums.LivenessModelEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测常量
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
public class LivenessConstant {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认人脸清晰度阈值
|
||||||
|
*/
|
||||||
|
public static final float DEFAULT_FACE_CLARITY_THRESHOLD = 0.3F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认活体阈值
|
||||||
|
*/
|
||||||
|
public static final float DEFAULT_REALITY_THRESHOLD = 0.8F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频默认检测帧数
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_FRAME_COUNT = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package cn.smartjavaai.face.context;
|
||||||
|
|
||||||
|
import com.seeta.sdk.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/5/8
|
||||||
|
*/
|
||||||
|
public class PredictorContext {
|
||||||
|
|
||||||
|
public GenderPredictor genderPredictor;
|
||||||
|
public AgePredictor agePredictor;
|
||||||
|
public EyeStateDetector eyeStateDetector;
|
||||||
|
public MaskDetector maskDetector;
|
||||||
|
public PoseEstimator poseEstimator;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cn.smartjavaai.face.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别模型枚举
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/10
|
||||||
|
*/
|
||||||
|
public enum FaceAttributeModelEnum {
|
||||||
|
|
||||||
|
SEETA_FACE6_MODEL("SeetaFace6Model");
|
||||||
|
|
||||||
|
private final String modelClassName;
|
||||||
|
|
||||||
|
FaceAttributeModelEnum(String modelClassName) {
|
||||||
|
this.modelClassName = modelClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelClassName() {
|
||||||
|
return modelClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||||
|
*/
|
||||||
|
public static FaceAttributeModelEnum fromName(String name) {
|
||||||
|
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||||
|
for (FaceAttributeModelEnum model : values()) {
|
||||||
|
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.enums;
|
||||||
|
|
||||||
import cn.smartjavaai.face.model.FeatureExtractionModel;
|
|
||||||
import cn.smartjavaai.face.model.RetinaFaceModel;
|
|
||||||
import cn.smartjavaai.face.model.SeetaFace6Model;
|
|
||||||
import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸模型枚举
|
* 人脸模型枚举
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cn.smartjavaai.face.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测模型枚举
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/10
|
||||||
|
*/
|
||||||
|
public enum LivenessModelEnum {
|
||||||
|
|
||||||
|
SEETA_FACE6_MODEL("SeetaFace6Model");
|
||||||
|
|
||||||
|
private final String modelClassName;
|
||||||
|
|
||||||
|
LivenessModelEnum(String modelClassName) {
|
||||||
|
this.modelClassName = modelClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelClassName() {
|
||||||
|
return modelClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||||
|
*/
|
||||||
|
public static LivenessModelEnum fromName(String name) {
|
||||||
|
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||||
|
for (LivenessModelEnum model : values()) {
|
||||||
|
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package cn.smartjavaai.face.factory;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.config.Config;
|
||||||
|
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||||
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
|
import cn.smartjavaai.face.model.attribute.Seetaface6FaceAttributeModel;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import cn.smartjavaai.face.model.attribute.FaceAttributeModel;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性检测模型工厂
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class FaceAttributeModelFactory {
|
||||||
|
|
||||||
|
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||||
|
private static volatile FaceAttributeModelFactory instance;
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, FaceAttributeModel> modelMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 算法注册表
|
||||||
|
*/
|
||||||
|
private static final Map<String, Class<? extends FaceAttributeModel>> registry =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public static FaceAttributeModelFactory getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (FaceAttributeModelFactory.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new FaceAttributeModelFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册模型
|
||||||
|
* @param name
|
||||||
|
* @param clazz
|
||||||
|
*/
|
||||||
|
private static void registerModel(String name, Class<? extends FaceAttributeModel> clazz) {
|
||||||
|
registry.put(name.toLowerCase(), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型(通过配置)
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public FaceAttributeModel getModel(FaceAttributeConfig config) {
|
||||||
|
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||||
|
throw new FaceException("未配置活体检测模型");
|
||||||
|
}
|
||||||
|
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||||
|
return createFaceModel(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用ModelConfig创建算法
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private FaceAttributeModel createFaceModel(FaceAttributeConfig config) {
|
||||||
|
Class<?> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
|
||||||
|
if(clazz == null){
|
||||||
|
throw new FaceException("Unsupported algorithm");
|
||||||
|
}
|
||||||
|
FaceAttributeModel model = null;
|
||||||
|
try {
|
||||||
|
model = (FaceAttributeModel) clazz.newInstance();
|
||||||
|
} catch (InstantiationException | IllegalAccessException e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
}
|
||||||
|
model.loadModel(config);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化默认算法
|
||||||
|
static {
|
||||||
|
registerModel("seetaface6model", Seetaface6FaceAttributeModel.class);
|
||||||
|
log.info("缓存目录:{}", Config.getCachePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.factory;
|
||||||
|
|
||||||
import cn.smartjavaai.common.config.Config;
|
import cn.smartjavaai.common.config.Config;
|
||||||
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
|
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import cn.smartjavaai.face.model.FeatureExtractionModel;
|
import cn.smartjavaai.face.model.facerec.*;
|
||||||
import cn.smartjavaai.face.model.RetinaFaceModel;
|
|
||||||
import cn.smartjavaai.face.model.SeetaFace6Model;
|
|
||||||
import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -13,7 +13,7 @@ import java.util.Objects;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸算法工厂
|
* 人脸检测识别模型工厂
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -76,8 +76,8 @@ public class FaceModelFactory {
|
|||||||
// 初始化默认配置
|
// 初始化默认配置
|
||||||
FaceModelConfig config = new FaceModelConfig();
|
FaceModelConfig config = new FaceModelConfig();
|
||||||
config.setModelEnum(FaceModelEnum.RETINA_FACE);
|
config.setModelEnum(FaceModelEnum.RETINA_FACE);
|
||||||
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
|
config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||||
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
|
config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
|
||||||
return getModel(config);
|
return getModel(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +110,8 @@ public class FaceModelFactory {
|
|||||||
// 初始化默认配置
|
// 初始化默认配置
|
||||||
FaceModelConfig config = new FaceModelConfig();
|
FaceModelConfig config = new FaceModelConfig();
|
||||||
config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||||
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
|
config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||||
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
|
config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
|
||||||
return getModel(config);
|
return getModel(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package cn.smartjavaai.face.factory;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.config.Config;
|
||||||
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.face.config.LivenessConfig;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
|
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||||
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
|
import cn.smartjavaai.face.model.facerec.*;
|
||||||
|
import cn.smartjavaai.face.model.liveness.LivenessDetModel;
|
||||||
|
import cn.smartjavaai.face.model.liveness.Seetaface6LivenessModel;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测模型工厂
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class LivenessModelFactory {
|
||||||
|
|
||||||
|
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||||
|
private static volatile LivenessModelFactory instance;
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, LivenessDetModel> modelMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 算法注册表
|
||||||
|
*/
|
||||||
|
private static final Map<String, Class<? extends LivenessDetModel>> registry =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public static LivenessModelFactory getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (LivenessModelFactory.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new LivenessModelFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册模型
|
||||||
|
* @param name
|
||||||
|
* @param clazz
|
||||||
|
*/
|
||||||
|
private static void registerModel(String name, Class<? extends LivenessDetModel> clazz) {
|
||||||
|
registry.put(name.toLowerCase(), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型(通过配置)
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LivenessDetModel getModel(LivenessConfig config) {
|
||||||
|
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||||
|
throw new FaceException("未配置活体检测模型");
|
||||||
|
}
|
||||||
|
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||||
|
return createFaceModel(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用ModelConfig创建算法
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private LivenessDetModel createFaceModel(LivenessConfig config) {
|
||||||
|
Class<?> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
|
||||||
|
if(clazz == null){
|
||||||
|
throw new FaceException("Unsupported algorithm");
|
||||||
|
}
|
||||||
|
LivenessDetModel model = null;
|
||||||
|
try {
|
||||||
|
model = (LivenessDetModel) clazz.newInstance();
|
||||||
|
} catch (InstantiationException | IllegalAccessException e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
}
|
||||||
|
model.loadModel(config);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化默认算法
|
||||||
|
static {
|
||||||
|
registerModel("seetaface6model", Seetaface6LivenessModel.class);
|
||||||
|
log.info("缓存目录:{}", Config.getCachePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package cn.smartjavaai.face.model.attribute;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.common.entity.FaceAttribute;
|
||||||
|
import cn.smartjavaai.common.entity.Point;
|
||||||
|
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||||
|
import cn.smartjavaai.common.enums.GenderType;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别模型
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
public interface FaceAttributeModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载模型
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
void loadModel(FaceAttributeConfig config); // 加载模型
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(String imagePath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(BufferedImage image){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param imageData 图片字节流
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(byte[] imageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<FaceAttribute> detect(String imagePath, DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(单人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param imageData 图片数据
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<FaceAttribute> detect(byte[] imageData,DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(单人脸)
|
||||||
|
* @param imageData
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(多人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<FaceAttribute> detect(BufferedImage image,DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(单人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(分数最高人脸)
|
||||||
|
* @param image
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectTopFace(BufferedImage image){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(分数最高人脸)
|
||||||
|
* @param imagePath
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectTopFace(String imagePath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(分数最高人脸)
|
||||||
|
* @param imageData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectTopFace(byte[] imageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(裁剪后的人脸)
|
||||||
|
* @param image
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectCropedFace(BufferedImage image){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(裁剪后的人脸)
|
||||||
|
* @param imagePath
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectCropedFace(String imagePath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸属性识别(裁剪后的人脸)
|
||||||
|
* @param imageData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default FaceAttribute detectCropedFace(byte[] imageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
package cn.smartjavaai.face.model.attribute;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.entity.*;
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.common.enums.EyeStatus;
|
||||||
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
|
import cn.smartjavaai.common.utils.PoolUtils;
|
||||||
|
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||||
|
import cn.smartjavaai.common.enums.GenderType;
|
||||||
|
import cn.smartjavaai.face.context.PredictorContext;
|
||||||
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
|
import cn.smartjavaai.face.seetaface.NativeLoader;
|
||||||
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
|
import com.seeta.pool.*;
|
||||||
|
import com.seeta.sdk.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* seetaface6 人脸属性识别模型
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/30
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class Seetaface6FaceAttributeModel implements FaceAttributeModel {
|
||||||
|
|
||||||
|
|
||||||
|
private FaceDetectorPool faceDetectorPool;
|
||||||
|
private GenderPredictorPool genderPredictorPool;
|
||||||
|
private FaceLandmarkerPool faceLandmarkerPool;
|
||||||
|
private AgePredictorPool agePredictorPool;
|
||||||
|
private EyeStateDetectorPool eyeStateDetectorPool;
|
||||||
|
private MaskDetectorPool maskDetectorPool;
|
||||||
|
private PoseEstimatorPool poseEstimatorPool;
|
||||||
|
|
||||||
|
private FaceAttributeConfig config;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadModel(FaceAttributeConfig config) {
|
||||||
|
if(StringUtils.isBlank(config.getModelPath())){
|
||||||
|
throw new FaceException("modelPath is null");
|
||||||
|
}
|
||||||
|
this.config = config;
|
||||||
|
//加载依赖库
|
||||||
|
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||||
|
log.info("Loading seetaFace6 library successfully.");
|
||||||
|
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||||
|
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
|
||||||
|
String[] genderPredictorModelPath = {config.getModelPath() + File.separator + "gender_predictor.csta"};
|
||||||
|
String[] agePredictorModelPath = {config.getModelPath() + File.separator + "age_predictor.csta"};
|
||||||
|
String[] eyeStateDetectorModelPath = {config.getModelPath() + File.separator + "eye_state.csta"};
|
||||||
|
String[] maskDetectorModelPath = {config.getModelPath() + File.separator + "mask_detector.csta"};
|
||||||
|
String[] poseEstimatorModelPath = {config.getModelPath() + File.separator + "pose_estimation.csta"};
|
||||||
|
SeetaDevice device = SeetaDevice.SEETA_DEVICE_AUTO;
|
||||||
|
int gpuId = 0;
|
||||||
|
if(Objects.nonNull(config.getDevice())){
|
||||||
|
device = config.getDevice() == DeviceEnum.CPU ? SeetaDevice.SEETA_DEVICE_CPU : SeetaDevice.SEETA_DEVICE_GPU;
|
||||||
|
if(config.getGpuId() >= 0 && device == SeetaDevice.SEETA_DEVICE_GPU){
|
||||||
|
gpuId = config.getGpuId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SeetaModelSetting faceDetectorPoolSetting = new SeetaModelSetting(gpuId, faceDetectorModelPath, device);
|
||||||
|
SeetaConfSetting faceDetectorPoolConfSetting = new SeetaConfSetting(faceDetectorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting faceLandmarkerPoolSetting = new SeetaModelSetting(gpuId, faceLandmarkerModelPath, device);
|
||||||
|
SeetaConfSetting faceLandmarkerPoolConfSetting = new SeetaConfSetting(faceLandmarkerPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting genderPredictorPoolSetting = new SeetaModelSetting(gpuId, genderPredictorModelPath, device);
|
||||||
|
SeetaConfSetting genderPredictorPoolConfSetting = new SeetaConfSetting(genderPredictorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting agePredictorPoolSetting = new SeetaModelSetting(gpuId, agePredictorModelPath, device);
|
||||||
|
SeetaConfSetting agePredictorPoolConfSetting = new SeetaConfSetting(agePredictorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting eyeStateDetectorPoolSetting = new SeetaModelSetting(gpuId, eyeStateDetectorModelPath, device);
|
||||||
|
SeetaConfSetting eyeStateDetectorPoolConfSetting = new SeetaConfSetting(eyeStateDetectorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting maskDetectorPoolSetting = new SeetaModelSetting(gpuId, maskDetectorModelPath, device);
|
||||||
|
SeetaConfSetting maskDetectorPoolConfSetting = new SeetaConfSetting(maskDetectorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting poseEstimatorPoolSetting = new SeetaModelSetting(gpuId, poseEstimatorModelPath, device);
|
||||||
|
SeetaConfSetting poseEstimatorPoolConfSetting = new SeetaConfSetting(poseEstimatorPoolSetting);
|
||||||
|
|
||||||
|
this.faceDetectorPool = new FaceDetectorPool(faceDetectorPoolConfSetting);
|
||||||
|
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
|
||||||
|
this.genderPredictorPool = new GenderPredictorPool(genderPredictorPoolConfSetting);
|
||||||
|
this.agePredictorPool = new AgePredictorPool(agePredictorPoolConfSetting);
|
||||||
|
this.eyeStateDetectorPool = new EyeStateDetectorPool(eyeStateDetectorPoolConfSetting);
|
||||||
|
this.maskDetectorPool = new MaskDetectorPool(maskDetectorPoolConfSetting);
|
||||||
|
this.poseEstimatorPool = new PoseEstimatorPool(poseEstimatorPoolConfSetting);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(String imagePath) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(byte[] imageData) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(BufferedImage image) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
//创建推力器上下文
|
||||||
|
PredictorContext predictorContext = new PredictorContext();
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
FaceDetector detectPredictor = null;
|
||||||
|
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
|
||||||
|
List<FaceAttribute> faceAttributeList = new ArrayList<FaceAttribute>();
|
||||||
|
try {
|
||||||
|
detectPredictor = faceDetectorPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
//检测人脸
|
||||||
|
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||||
|
if(Objects.isNull(seetaResult)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
for(SeetaRect seetaRect : seetaResult){
|
||||||
|
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||||
|
seetaPointFSList.add(landmarks);
|
||||||
|
//人脸属性检测
|
||||||
|
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||||
|
faceAttributeList.add(faceAttribute);
|
||||||
|
}
|
||||||
|
return FaceUtils.convertToFaceAttributeResponse(seetaResult, seetaPointFSList, faceAttributeList);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("人脸属性检测错误", e);
|
||||||
|
} finally {
|
||||||
|
// 统一归还所有 Predictor 到池
|
||||||
|
PoolUtils.returnToPool(faceDetectorPool, detectPredictor);
|
||||||
|
PoolUtils.returnToPool(faceLandmarkerPool, faceLandmarker);
|
||||||
|
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||||
|
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||||
|
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||||
|
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||||
|
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单人脸属性检测
|
||||||
|
* @param imageData
|
||||||
|
* @param seetaRect
|
||||||
|
* @param landmarks
|
||||||
|
* @param predictorContext
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private FaceAttribute detect(SeetaImageData imageData, SeetaRect seetaRect, SeetaPointF[] landmarks, PredictorContext predictorContext){
|
||||||
|
FaceAttribute faceAttribute = new FaceAttribute();
|
||||||
|
//性别检测
|
||||||
|
GenderType genderType = null;
|
||||||
|
if (config.isEnableGender()){
|
||||||
|
GenderPredictor.GENDER[] gender = new GenderPredictor.GENDER[1];
|
||||||
|
boolean isSuccess = predictorContext.genderPredictor.PredictGenderWithCrop(imageData, landmarks, gender);
|
||||||
|
genderType = isSuccess ? FaceUtils.convertToGenderType(gender[0]) : GenderType.UNKNOWN;
|
||||||
|
}
|
||||||
|
//眼睛状态检测
|
||||||
|
EyeStatus leftEyeStatus = null;
|
||||||
|
EyeStatus rightEyeStatus = null;
|
||||||
|
if (config.isEnableEyeStatus()){
|
||||||
|
EyeStateDetector.EYE_STATE[] eyeState = predictorContext.eyeStateDetector.detect(imageData, landmarks);
|
||||||
|
leftEyeStatus = FaceUtils.convertToEyeStatus(eyeState[0]);
|
||||||
|
rightEyeStatus = FaceUtils.convertToEyeStatus(eyeState[1]);
|
||||||
|
}
|
||||||
|
//年龄检测
|
||||||
|
Integer age = 0;
|
||||||
|
if (config.isEnableAge()){
|
||||||
|
age = predictorContext.agePredictor.predictAgeWithCrop(imageData, landmarks);
|
||||||
|
}
|
||||||
|
//口罩检测
|
||||||
|
Boolean wearingMask = null;
|
||||||
|
if (config.isEnableMask()){
|
||||||
|
float[] score = new float[1];
|
||||||
|
wearingMask = predictorContext.maskDetector.detect(imageData, seetaRect, score);
|
||||||
|
}
|
||||||
|
//姿态检测
|
||||||
|
if (config.isEnableHeadPose()){
|
||||||
|
float[] yaw = new float[1];//左右转头(水平旋转)
|
||||||
|
float[] pitch = new float[1]; //上下抬头/低头(垂直旋转)
|
||||||
|
float[] roll = new float[1]; //头部左右倾斜(平面旋转)
|
||||||
|
predictorContext.poseEstimator.Estimate(imageData, seetaRect, yaw, pitch, roll);
|
||||||
|
faceAttribute.setHeadPose(new HeadPose(yaw[0], pitch[0], roll[0]));
|
||||||
|
}
|
||||||
|
faceAttribute.setGenderType(genderType);
|
||||||
|
faceAttribute.setAge(age);
|
||||||
|
faceAttribute.setLeftEyeStatus(leftEyeStatus);
|
||||||
|
faceAttribute.setRightEyeStatus(rightEyeStatus);
|
||||||
|
faceAttribute.setWearingMask(wearingMask);
|
||||||
|
return faceAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FaceAttribute> detect(String imagePath, DetectionResponse faceDetectionResponse) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image, faceDetectionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FaceAttribute> detect(byte[] imageData, DetectionResponse faceDetectionResponse) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FaceAttribute> detect(BufferedImage image, DetectionResponse faceDetectionResponse) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
if(Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
//创建推力器上下文
|
||||||
|
PredictorContext predictorContext = new PredictorContext();
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
List<FaceAttribute> faceAttributeList = new ArrayList<FaceAttribute>();
|
||||||
|
try {
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||||
|
for(DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()){
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(detectionInfo.getDetectionRectangle());
|
||||||
|
SeetaPointF[] landmarks = null;
|
||||||
|
FaceInfo faceInfo = detectionInfo.getFaceInfo();
|
||||||
|
//如果没有人脸标识,则提取人脸标识
|
||||||
|
if(faceInfo == null || faceInfo.getKeyPoints() == null || faceInfo.getKeyPoints().isEmpty()){
|
||||||
|
//提取人脸的5点人脸标识
|
||||||
|
landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||||
|
}else{
|
||||||
|
landmarks = FaceUtils.convertToSeetaPointF(faceInfo.getKeyPoints());
|
||||||
|
}
|
||||||
|
//人脸属性检测
|
||||||
|
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||||
|
faceAttributeList.add(faceAttribute);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||||
|
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||||
|
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||||
|
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||||
|
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||||
|
}
|
||||||
|
return faceAttributeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image, faceDetectionRectangle, keyPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle, keyPoints);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
if(Objects.isNull(faceDetectionRectangle)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
//创建推力器上下文
|
||||||
|
PredictorContext predictorContext = new PredictorContext();
|
||||||
|
try {
|
||||||
|
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(faceDetectionRectangle);
|
||||||
|
SeetaPointF[] landmarks = null;
|
||||||
|
if(keyPoints == null || keyPoints.isEmpty()){
|
||||||
|
throw new FaceException("人脸关键点keyPoints为空");
|
||||||
|
}
|
||||||
|
landmarks = FaceUtils.convertToSeetaPointF(keyPoints);
|
||||||
|
//人脸属性检测
|
||||||
|
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||||
|
return faceAttribute;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||||
|
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||||
|
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||||
|
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||||
|
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detectTopFace(String imagePath) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detectTopFace(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detectTopFace(byte[] imageData) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detectTopFace(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FaceAttribute detectTopFace(BufferedImage image) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
FaceDetector detectPredictor = null;
|
||||||
|
//创建推力器上下文
|
||||||
|
PredictorContext predictorContext = new PredictorContext();
|
||||||
|
try {
|
||||||
|
detectPredictor = faceDetectorPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||||
|
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||||
|
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
//检测人脸
|
||||||
|
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||||
|
if(Objects.isNull(seetaResult)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaResult[0], landmarks);
|
||||||
|
//人脸属性检测
|
||||||
|
FaceAttribute faceAttribute = detect(imageData, seetaResult[0], landmarks, predictorContext);
|
||||||
|
return faceAttribute;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (detectPredictor != null) {
|
||||||
|
try {
|
||||||
|
faceDetectorPool.returnObject(detectPredictor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||||
|
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||||
|
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||||
|
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||||
|
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||||
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
import cn.smartjavaai.face.entity.FaceResult;
|
import cn.smartjavaai.face.entity.FaceResult;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package cn.smartjavaai.face;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||||
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
import cn.smartjavaai.face.entity.FaceResult;
|
import cn.smartjavaai.face.entity.FaceResult;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.face.model;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import ai.djl.Device;
|
import ai.djl.Device;
|
||||||
import ai.djl.MalformedModelException;
|
import ai.djl.MalformedModelException;
|
||||||
@@ -12,14 +12,18 @@ import ai.djl.repository.zoo.Criteria;
|
|||||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||||
import ai.djl.repository.zoo.ZooModel;
|
import ai.djl.repository.zoo.ZooModel;
|
||||||
import ai.djl.training.util.ProgressBar;
|
import ai.djl.training.util.ProgressBar;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||||
import cn.smartjavaai.common.utils.FileUtils;
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
import cn.smartjavaai.face.*;
|
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||||
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
|
import cn.smartjavaai.face.factory.FaceModelFactory;
|
||||||
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
|
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
|
||||||
import cn.smartjavaai.face.utils.FaceAlignUtils;
|
import cn.smartjavaai.face.utils.FaceAlignUtils;
|
||||||
import cn.smartjavaai.face.utils.FaceUtils;
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
@@ -28,17 +32,15 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.pool2.ObjectPool;
|
import org.apache.commons.pool2.ObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
import org.opencv.core.Mat;
|
import org.opencv.core.Mat;
|
||||||
|
import org.opencv.face.Face;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -223,19 +225,21 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
|
|||||||
}
|
}
|
||||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
||||||
DetectionResponse detectedResult = faceModel.detect(image);
|
DetectionResponse detectedResult = faceModel.detect(image);
|
||||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getRectangleList()) || detectedResult.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("未检测到人脸");
|
throw new FaceException("未检测到人脸");
|
||||||
}
|
}
|
||||||
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||||
NDManager manager = NDManager.newBaseManager();
|
NDManager manager = NDManager.newBaseManager();
|
||||||
for (DetectionRectangle rectangle : detectedResult.getRectangleList()){
|
for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
|
||||||
|
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||||
|
|
||||||
float[] features = null;
|
float[] features = null;
|
||||||
//裁剪人脸
|
//裁剪人脸
|
||||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||||
//人脸对齐
|
//人脸对齐
|
||||||
if(config.isAlign()){
|
if(config.isAlign()){
|
||||||
//获取子图中人脸关键点坐标
|
//获取子图中人脸关键点坐标
|
||||||
double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
|
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||||
NDArray srcPoints = manager.create(pointsArray);
|
NDArray srcPoints = manager.create(pointsArray);
|
||||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||||
// 5点仿射变换
|
// 5点仿射变换
|
||||||
@@ -314,18 +318,19 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
|
|||||||
if(config.isCropFace()){
|
if(config.isCropFace()){
|
||||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
||||||
DetectionResponse detectedResult = faceModel.detect(image);
|
DetectionResponse detectedResult = faceModel.detect(image);
|
||||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getRectangleList()) || detectedResult.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("未检测到人脸");
|
throw new FaceException("未检测到人脸");
|
||||||
}
|
}
|
||||||
//只取第一个人脸
|
//只取第一个人脸
|
||||||
DetectionRectangle rectangle = detectedResult.getRectangleList().get(0);
|
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
|
||||||
|
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||||
//裁剪人脸
|
//裁剪人脸
|
||||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||||
//人脸对齐
|
//人脸对齐
|
||||||
if(config.isAlign()){
|
if(config.isAlign()){
|
||||||
NDManager manager = NDManager.newBaseManager();
|
NDManager manager = NDManager.newBaseManager();
|
||||||
//获取子图中人脸关键点坐标
|
//获取子图中人脸关键点坐标
|
||||||
double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
|
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||||
NDArray srcPoints = manager.create(pointsArray);
|
NDArray srcPoints = manager.create(pointsArray);
|
||||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||||
// 5点仿射变换
|
// 5点仿射变换
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.face.model;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import ai.djl.Device;
|
import ai.djl.Device;
|
||||||
import ai.djl.MalformedModelException;
|
import ai.djl.MalformedModelException;
|
||||||
@@ -15,7 +15,8 @@ import cn.smartjavaai.common.enums.DeviceEnum;
|
|||||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||||
import cn.smartjavaai.common.utils.FileUtils;
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
import cn.smartjavaai.face.*;
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
||||||
import cn.smartjavaai.face.utils.FaceUtils;
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
@@ -24,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.pool2.ObjectPool;
|
import org.apache.commons.pool2.ObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -33,7 +33,6 @@ import java.io.*;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +72,7 @@ public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
|
|||||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||||
}
|
}
|
||||||
FaceDetectionTranslator translator =
|
FaceDetectionTranslator translator =
|
||||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
|
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceDetectConstant.MAX_FACE_LIMIT, scales, steps);
|
||||||
Criteria<Image, DetectedObjects> criteria =
|
Criteria<Image, DetectedObjects> criteria =
|
||||||
Criteria.builder()
|
Criteria.builder()
|
||||||
.setTypes(Image.class, DetectedObjects.class)
|
.setTypes(Image.class, DetectedObjects.class)
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
package cn.smartjavaai.face.model;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
import cn.smartjavaai.common.utils.FileUtils;
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
import cn.smartjavaai.face.AbstractFaceModel;
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
import cn.smartjavaai.face.FaceExtractConfig;
|
|
||||||
import cn.smartjavaai.face.FaceModelConfig;
|
|
||||||
import cn.smartjavaai.face.dao.FaceDao;
|
import cn.smartjavaai.face.dao.FaceDao;
|
||||||
import cn.smartjavaai.face.entity.FaceData;
|
import cn.smartjavaai.face.entity.FaceData;
|
||||||
import cn.smartjavaai.face.entity.FaceResult;
|
import cn.smartjavaai.face.entity.FaceResult;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import cn.smartjavaai.face.utils.FaceAlignUtils;
|
|
||||||
import cn.smartjavaai.face.utils.FaceUtils;
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
import com.seeta.pool.*;
|
import com.seeta.pool.*;
|
||||||
import com.seeta.sdk.*;
|
import com.seeta.sdk.*;
|
||||||
@@ -63,7 +60,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
|||||||
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
|
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
|
||||||
}
|
}
|
||||||
//加载依赖库
|
//加载依赖库
|
||||||
NativeLoader.loadNativeLibraries(config);
|
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||||
log.info("Loading seetaFace6 library successfully.");
|
log.info("Loading seetaFace6 library successfully.");
|
||||||
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||||
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
|
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
|
||||||
@@ -209,7 +206,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
|||||||
throw new FaceException("无效图片路径", e);
|
throw new FaceException("无效图片路径", e);
|
||||||
}
|
}
|
||||||
DetectionResponse result = detect(image);
|
DetectionResponse result = detect(image);
|
||||||
if(Objects.isNull(result) || Objects.isNull(result.getRectangleList()) || result.getRectangleList().isEmpty()){
|
if(Objects.isNull(result) || Objects.isNull(result.getDetectionInfoList()) || result.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("未识别到人脸");
|
throw new FaceException("未识别到人脸");
|
||||||
}
|
}
|
||||||
//绘制人脸框
|
//绘制人脸框
|
||||||
@@ -225,7 +222,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
|||||||
throw new FaceException("图像无效");
|
throw new FaceException("图像无效");
|
||||||
}
|
}
|
||||||
DetectionResponse detectedObjects = detect(sourceImage);
|
DetectionResponse detectedObjects = detect(sourceImage);
|
||||||
if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getRectangleList()) || detectedObjects.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getDetectionInfoList()) || detectedObjects.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("未识别到人脸");
|
throw new FaceException("未识别到人脸");
|
||||||
}
|
}
|
||||||
//绘制人脸框
|
//绘制人脸框
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.face.model;
|
package cn.smartjavaai.face.model.facerec;
|
||||||
|
|
||||||
import ai.djl.Device;
|
import ai.djl.Device;
|
||||||
import ai.djl.MalformedModelException;
|
import ai.djl.MalformedModelException;
|
||||||
@@ -15,7 +15,8 @@ import cn.smartjavaai.common.enums.DeviceEnum;
|
|||||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||||
import cn.smartjavaai.common.utils.FileUtils;
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
import cn.smartjavaai.face.*;
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
||||||
import cn.smartjavaai.face.utils.FaceUtils;
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
@@ -24,13 +25,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.pool2.ObjectPool;
|
import org.apache.commons.pool2.ObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +71,7 @@ public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements
|
|||||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||||
}
|
}
|
||||||
FaceDetectionTranslator translator =
|
FaceDetectionTranslator translator =
|
||||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
|
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceDetectConstant.MAX_FACE_LIMIT, scales, steps);
|
||||||
Criteria<Image, DetectedObjects> criteria =
|
Criteria<Image, DetectedObjects> criteria =
|
||||||
Criteria.builder()
|
Criteria.builder()
|
||||||
.setTypes(Image.class, DetectedObjects.class)
|
.setTypes(Image.class, DetectedObjects.class)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package cn.smartjavaai.face.model.liveness;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.common.entity.Point;
|
||||||
|
import cn.smartjavaai.face.config.LivenessConfig;
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测模型
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
public interface LivenessDetModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载模型
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
void loadModel(LivenessConfig config); // 加载模型
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(String imagePath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(BufferedImage image){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param imageData 图片字节流
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(byte[] imageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<LivenessStatus> detect(String imagePath, DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(单人脸)
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param imageData 图片数据
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<LivenessStatus> detect(byte[] imageData,DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(单人脸)
|
||||||
|
* @param imageData
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(多人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @param faceDetectionResponse 人脸检测结果
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default List<LivenessStatus> detect(BufferedImage image,DetectionResponse faceDetectionResponse){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(单人脸)
|
||||||
|
* @param image BufferedImage
|
||||||
|
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(分数最高人脸)
|
||||||
|
* @param image
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectTopFace(BufferedImage image){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(分数最高人脸)
|
||||||
|
* @param imagePath
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectTopFace(String imagePath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活体检测(分数最高人脸)
|
||||||
|
* @param imageData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectTopFace(byte[] imageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测(逐帧检测)
|
||||||
|
* @param frameImage
|
||||||
|
* @param faceDetectionRectangle
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideoByFrame(BufferedImage frameImage, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测(逐帧检测)
|
||||||
|
* @param frameData
|
||||||
|
* @param faceDetectionRectangle
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测(逐帧检测)
|
||||||
|
* @param frameImageData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideoByFrame(byte[] frameImageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测(逐帧检测)
|
||||||
|
* @param frameImageData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideoByFrame(BufferedImage frameImageData){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测
|
||||||
|
* @param videoInputStream
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideo(InputStream videoInputStream){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频活体检测
|
||||||
|
* @param videoPath
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default LivenessStatus detectVideo(String videoPath){
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
package cn.smartjavaai.face.model.liveness;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.entity.*;
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
|
import cn.smartjavaai.face.config.LivenessConfig;
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
|
import cn.smartjavaai.face.seetaface.NativeLoader;
|
||||||
|
import cn.smartjavaai.face.utils.FaceUtils;
|
||||||
|
import com.seeta.pool.*;
|
||||||
|
import com.seeta.sdk.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||||
|
import org.bytedeco.javacv.Frame;
|
||||||
|
import org.bytedeco.javacv.Java2DFrameUtils;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* seetaface6 活体检测模型
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/30
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class Seetaface6LivenessModel implements LivenessDetModel{
|
||||||
|
|
||||||
|
|
||||||
|
private FaceDetectorPool faceDetectorPool;
|
||||||
|
private FaceAntiSpoofingPool faceAntiSpoofingPool;
|
||||||
|
private FaceLandmarkerPool faceLandmarkerPool;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadModel(LivenessConfig config) {
|
||||||
|
if(StringUtils.isBlank(config.getModelPath())){
|
||||||
|
throw new FaceException("modelPath is null");
|
||||||
|
}
|
||||||
|
//加载依赖库
|
||||||
|
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||||
|
log.info("Loading seetaFace6 library successfully.");
|
||||||
|
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||||
|
String[] faceAntiSpoofingModelPath = {config.getModelPath() + File.separator + "fas_first.csta",config.getModelPath() + File.separator + "fas_second.csta"};
|
||||||
|
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
|
||||||
|
SeetaDevice device = SeetaDevice.SEETA_DEVICE_AUTO;
|
||||||
|
int gpuId = 0;
|
||||||
|
if(Objects.nonNull(config.getDevice())){
|
||||||
|
device = config.getDevice() == DeviceEnum.CPU ? SeetaDevice.SEETA_DEVICE_CPU : SeetaDevice.SEETA_DEVICE_GPU;
|
||||||
|
if(config.getGpuId() >= 0 && device == SeetaDevice.SEETA_DEVICE_GPU){
|
||||||
|
gpuId = config.getGpuId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SeetaModelSetting faceDetectorPoolSetting = new SeetaModelSetting(gpuId, faceDetectorModelPath, device);
|
||||||
|
SeetaConfSetting faceDetectorPoolConfSetting = new SeetaConfSetting(faceDetectorPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting faceLandmarkerPoolSetting = new SeetaModelSetting(gpuId, faceLandmarkerModelPath, device);
|
||||||
|
SeetaConfSetting faceLandmarkerPoolConfSetting = new SeetaConfSetting(faceLandmarkerPoolSetting);
|
||||||
|
|
||||||
|
SeetaModelSetting faceAntiSpoofingSetting = new SeetaModelSetting(gpuId, faceAntiSpoofingModelPath, device);
|
||||||
|
SeetaConfSetting faceAntiSpoofingPoolConfSetting = new SeetaConfSetting(faceAntiSpoofingSetting);
|
||||||
|
|
||||||
|
this.faceDetectorPool = new FaceDetectorPool(faceDetectorPoolConfSetting);
|
||||||
|
this.faceAntiSpoofingPool = new FaceAntiSpoofingPool(faceAntiSpoofingPoolConfSetting);
|
||||||
|
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
//设置参数
|
||||||
|
try {
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
if(config.getFaceClarityThreshold() > 0 && config.getRealityThreshold() > 0){
|
||||||
|
faceAntiSpoofing.SetThreshold(config.getFaceClarityThreshold(), config.getRealityThreshold());
|
||||||
|
}
|
||||||
|
if(config.getFrameCount() > 0){
|
||||||
|
faceAntiSpoofing.SetVideoFrameCount(config.getFrameCount());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
} finally {
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(String imagePath) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(byte[] imageData) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(BufferedImage image) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
FaceAntiSpoofing.Status status = null;
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
FaceDetector detectPredictor = null;
|
||||||
|
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
|
||||||
|
List<LivenessStatus> livenessStatusList = new ArrayList<LivenessStatus>();
|
||||||
|
try {
|
||||||
|
detectPredictor = faceDetectorPool.borrowObject();
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
//检测人脸
|
||||||
|
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||||
|
if(Objects.isNull(seetaResult)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
for(SeetaRect seetaRect : seetaResult){
|
||||||
|
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||||
|
seetaPointFSList.add(landmarks);
|
||||||
|
//检测图片
|
||||||
|
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||||
|
livenessStatusList.add(FaceUtils.convertToLivenessStatus(status));
|
||||||
|
}
|
||||||
|
return FaceUtils.convertToDetectionResponse(seetaResult, seetaPointFSList, livenessStatusList);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (detectPredictor != null) {
|
||||||
|
try {
|
||||||
|
faceDetectorPool.returnObject(detectPredictor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LivenessStatus> detect(String imagePath, DetectionResponse faceDetectionResponse) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image, faceDetectionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detect(image, faceDetectionRectangle, keyPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private List<LivenessStatus> detect(BufferedImage image, DetectionResponse faceDetectionResponse,boolean isImage) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
if(Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
FaceAntiSpoofing.Status status = null;
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
List<LivenessStatus> livenessStatusList = new ArrayList<LivenessStatus>();
|
||||||
|
try {
|
||||||
|
for(DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()){
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(detectionInfo.getDetectionRectangle());
|
||||||
|
SeetaPointF[] landmarks = null;
|
||||||
|
FaceInfo faceInfo = detectionInfo.getFaceInfo();
|
||||||
|
//如果没有人脸标识,则提取人脸标识
|
||||||
|
if(faceInfo == null || faceInfo.getKeyPoints() == null || faceInfo.getKeyPoints().isEmpty()){
|
||||||
|
//提取人脸的5点人脸标识
|
||||||
|
landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||||
|
}else{
|
||||||
|
landmarks = FaceUtils.convertToSeetaPointF(faceInfo.getKeyPoints());
|
||||||
|
}
|
||||||
|
//检测图片
|
||||||
|
if(isImage){
|
||||||
|
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||||
|
}else{
|
||||||
|
//检测视频
|
||||||
|
status = faceAntiSpoofing.PredictVideo(imageData, seetaRect, landmarks);
|
||||||
|
}
|
||||||
|
livenessStatusList.add(FaceUtils.convertToLivenessStatus(status));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return livenessStatusList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LivenessStatus> detect(BufferedImage image, DetectionResponse faceDetectionResponse) {
|
||||||
|
return detect(image, faceDetectionResponse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints, boolean isImage) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
if(Objects.isNull(faceDetectionRectangle)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
FaceAntiSpoofing.Status status = null;
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
try {
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(faceDetectionRectangle);
|
||||||
|
SeetaPointF[] landmarks = null;
|
||||||
|
if(keyPoints == null || keyPoints.isEmpty()){
|
||||||
|
throw new FaceException("人脸关键点keyPoints为空");
|
||||||
|
}
|
||||||
|
landmarks = FaceUtils.convertToSeetaPointF(keyPoints);
|
||||||
|
//检测图片
|
||||||
|
if(isImage){
|
||||||
|
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||||
|
}else{
|
||||||
|
//检测视频
|
||||||
|
status = faceAntiSpoofing.PredictVideo(imageData, seetaRect, landmarks);
|
||||||
|
}
|
||||||
|
return FaceUtils.convertToLivenessStatus(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
return detect(image, faceDetectionRectangle, keyPoints, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LivenessStatus> detect(byte[] imageData, DetectionResponse faceDetectionResponse) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle, keyPoints);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectTopFace(String imagePath) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new FaceException("图像文件不存在");
|
||||||
|
}
|
||||||
|
// 将图片路径转换为 BufferedImage
|
||||||
|
BufferedImage image = null;
|
||||||
|
try {
|
||||||
|
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("无效图片路径", e);
|
||||||
|
}
|
||||||
|
return detectTopFace(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private LivenessStatus detectTopFace(BufferedImage image, boolean isImage) {
|
||||||
|
if(!ImageUtils.isImageValid(image)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
FaceAntiSpoofing.Status status = null;
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
FaceLandmarker faceLandmarker = null;
|
||||||
|
FaceDetector detectPredictor = null;
|
||||||
|
try {
|
||||||
|
detectPredictor = faceDetectorPool.borrowObject();
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||||
|
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||||
|
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||||
|
//检测人脸
|
||||||
|
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||||
|
if(Objects.isNull(seetaResult)){
|
||||||
|
throw new FaceException("无人脸数据");
|
||||||
|
}
|
||||||
|
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||||
|
faceLandmarker.mark(imageData, seetaResult[0], landmarks);
|
||||||
|
//检测图片
|
||||||
|
if(isImage){
|
||||||
|
status = faceAntiSpoofing.Predict(imageData, seetaResult[0], landmarks);
|
||||||
|
}else{
|
||||||
|
status = faceAntiSpoofing.PredictVideo(imageData, seetaResult[0], landmarks);
|
||||||
|
}
|
||||||
|
return FaceUtils.convertToLivenessStatus(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException("活体检测错误", e);
|
||||||
|
} finally {
|
||||||
|
if (detectPredictor != null) {
|
||||||
|
try {
|
||||||
|
faceDetectorPool.returnObject(detectPredictor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (faceLandmarker != null) {
|
||||||
|
try {
|
||||||
|
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectTopFace(BufferedImage image) {
|
||||||
|
return detectTopFace(image, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectTopFace(byte[] imageData) {
|
||||||
|
if(Objects.isNull(imageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detectTopFace(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideoByFrame(BufferedImage frameImage, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(!ImageUtils.isImageValid(frameImage)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
return detect(frameImage,faceDetectionRectangle, keyPoints,false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||||
|
if(Objects.isNull(frameData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detect(ImageIO.read(new ByteArrayInputStream(frameData)), faceDetectionRectangle, keyPoints, false);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideoByFrame(byte[] frameImageData) {
|
||||||
|
if(Objects.isNull(frameImageData)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return detectVideoByFrame(ImageIO.read(new ByteArrayInputStream(frameImageData)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FaceException("错误的图像", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideoByFrame(BufferedImage frameImageData) {
|
||||||
|
return detectTopFace(frameImageData, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideo(InputStream videoInputStream) {
|
||||||
|
if(Objects.isNull(videoInputStream)){
|
||||||
|
throw new FaceException("视频无效");
|
||||||
|
}
|
||||||
|
return detectVideo(new FFmpegFrameGrabber(videoInputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LivenessStatus detectVideo(String videoPath) {
|
||||||
|
if(!FileUtils.isFileExists(videoPath)){
|
||||||
|
throw new FaceException("视频文件不存在");
|
||||||
|
}
|
||||||
|
return detectVideo(new FFmpegFrameGrabber(videoPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LivenessStatus detectVideo(FFmpegFrameGrabber grabber) {
|
||||||
|
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||||
|
try {
|
||||||
|
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||||
|
grabber.start();
|
||||||
|
// 获取视频总帧数
|
||||||
|
int totalFrames = grabber.getLengthInFrames();
|
||||||
|
int videoFrameCountConfig = faceAntiSpoofing.GetVideoFrameCount();
|
||||||
|
log.info("视频总帧数:{},检测帧数:{}", totalFrames, videoFrameCountConfig);
|
||||||
|
if(totalFrames < videoFrameCountConfig){
|
||||||
|
throw new FaceException("视频帧数低于检测帧数");
|
||||||
|
}
|
||||||
|
// 逐帧处理视频
|
||||||
|
for (int frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
||||||
|
// 获取当前帧
|
||||||
|
Frame frame = grabber.grabImage();
|
||||||
|
if (frame != null) {
|
||||||
|
BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame);
|
||||||
|
LivenessStatus livenessStatus = detectVideoByFrame(bufferedImage);
|
||||||
|
//满足检测帧数之后停止检测
|
||||||
|
if(livenessStatus != LivenessStatus.DETECTING){
|
||||||
|
return livenessStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grabber.stop();
|
||||||
|
} catch (FFmpegFrameGrabber.Exception e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FaceException(e);
|
||||||
|
} finally {
|
||||||
|
if (faceAntiSpoofing != null) {
|
||||||
|
try {
|
||||||
|
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LivenessStatus.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import cn.hutool.system.OsInfo;
|
|||||||
import cn.hutool.system.SystemUtil;
|
import cn.hutool.system.SystemUtil;
|
||||||
import cn.smartjavaai.common.config.Config;
|
import cn.smartjavaai.common.config.Config;
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
import cn.smartjavaai.face.FaceModelConfig;
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import com.seeta.sdk.util.DllItem;
|
import com.seeta.sdk.util.DllItem;
|
||||||
import com.seeta.sdk.util.LoadNativeCore;
|
import com.seeta.sdk.util.LoadNativeCore;
|
||||||
@@ -39,35 +39,48 @@ public class NativeLoader {
|
|||||||
*/
|
*/
|
||||||
private static final String PROPERTIES_FILE_NAME = "dll.properties";
|
private static final String PROPERTIES_FILE_NAME = "dll.properties";
|
||||||
|
|
||||||
|
// 使用 volatile 保证内存可见性
|
||||||
|
private static volatile boolean isDllLoaded = false;
|
||||||
|
|
||||||
|
|
||||||
public static void loadNativeLibraries(FaceModelConfig config) {
|
|
||||||
|
public static void loadNativeLibraries(DeviceEnum device) {
|
||||||
try {
|
try {
|
||||||
OsInfo osInfo = SystemUtil.getOsInfo();
|
if (!isDllLoaded) {
|
||||||
//检查当前系统是否支持
|
synchronized (NativeLoader.class) {
|
||||||
if(!osInfo.isWindows() && !osInfo.isLinux()){
|
if (!isDllLoaded) { // 双重检查
|
||||||
throw new FaceException("当前系统不支持:" + osInfo.getName());
|
OsInfo osInfo = SystemUtil.getOsInfo();
|
||||||
}
|
//检查当前系统是否支持
|
||||||
//判断硬件架构是否支持GPU
|
if(!osInfo.isWindows() && !osInfo.isLinux()){
|
||||||
if(config.getDevice() != null && config.getDevice().equals(DeviceEnum.GPU)){
|
throw new FaceException("当前系统不支持:" + osInfo.getName());
|
||||||
//GPU仅支持amd64
|
}
|
||||||
if(!osInfo.getArch().contains("amd64") && !osInfo.getArch().contains("x86_64")){
|
//判断硬件架构是否支持GPU
|
||||||
throw new FaceException("seetaface6 GPU模型不支持当前arch:" + osInfo.getArch());
|
if(device != null && device.equals(DeviceEnum.GPU)){
|
||||||
|
//GPU仅支持amd64
|
||||||
|
if(!osInfo.getArch().contains("amd64") && !osInfo.getArch().contains("x86_64")){
|
||||||
|
throw new FaceException("seetaface6 GPU模型不支持当前arch:" + osInfo.getArch());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
|
||||||
|
//创建目录
|
||||||
|
FileUtil.mkdir(seetaface6NativePath);
|
||||||
|
log.info("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
|
||||||
|
//拷贝依赖库到缓存目录
|
||||||
|
List<File> fileList = getLibFiles(osInfo, device);
|
||||||
|
if(fileList != null && !fileList.isEmpty()){
|
||||||
|
// 加载依赖库文件
|
||||||
|
fileList.forEach(file -> {
|
||||||
|
System.load(file.getAbsolutePath());
|
||||||
|
log.info(String.format("load %s finish", file.getAbsolutePath()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isDllLoaded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.info("SeetaFace DLL is already loaded.");
|
||||||
}
|
}
|
||||||
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
|
|
||||||
//创建目录
|
|
||||||
FileUtil.mkdir(seetaface6NativePath);
|
|
||||||
log.info("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
|
|
||||||
//拷贝依赖库到缓存目录
|
|
||||||
List<File> fileList = getLibFiles(osInfo, config.getDevice());
|
|
||||||
if(fileList != null && !fileList.isEmpty()){
|
|
||||||
// 加载依赖库文件
|
|
||||||
fileList.forEach(file -> {
|
|
||||||
System.load(file.getAbsolutePath());
|
|
||||||
log.info(String.format("load %s finish", file.getAbsolutePath()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Native library loading failed", e);
|
throw new RuntimeException("Native library loading failed", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ai.djl.translate.Translator;
|
|||||||
import ai.djl.translate.TranslatorContext;
|
import ai.djl.translate.TranslatorContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* facenet人脸特征提取Translator
|
||||||
* @author dwj
|
* @author dwj
|
||||||
* @date 2025/3/31
|
* @date 2025/3/31
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import ai.djl.modality.cv.output.BoundingBox;
|
|||||||
import ai.djl.modality.cv.output.DetectedObjects;
|
import ai.djl.modality.cv.output.DetectedObjects;
|
||||||
import ai.djl.ndarray.NDArray;
|
import ai.djl.ndarray.NDArray;
|
||||||
import ai.djl.ndarray.NDManager;
|
import ai.djl.ndarray.NDManager;
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.*;
|
||||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
|
||||||
import cn.smartjavaai.common.entity.Point;
|
import cn.smartjavaai.common.entity.Point;
|
||||||
|
import cn.smartjavaai.common.enums.EyeStatus;
|
||||||
|
import cn.smartjavaai.common.enums.GenderType;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
import cn.smartjavaai.face.FaceModelConfig;
|
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||||
|
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||||
import cn.smartjavaai.face.exception.FaceException;
|
import cn.smartjavaai.face.exception.FaceException;
|
||||||
import com.seeta.sdk.SeetaImageData;
|
import com.seeta.sdk.*;
|
||||||
import com.seeta.sdk.SeetaPointF;
|
|
||||||
import com.seeta.sdk.SeetaRect;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -45,7 +45,7 @@ public class FaceUtils {
|
|||||||
}
|
}
|
||||||
DetectionResponse detectionResponse = new DetectionResponse();
|
DetectionResponse detectionResponse = new DetectionResponse();
|
||||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
Iterator iterator = detectedObjectList.iterator();
|
Iterator iterator = detectedObjectList.iterator();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while(iterator.hasNext()) {
|
while(iterator.hasNext()) {
|
||||||
@@ -64,12 +64,13 @@ public class FaceUtils {
|
|||||||
if (y < 0) y = 0;
|
if (y < 0) y = 0;
|
||||||
if (x + width > img.getWidth()) width = img.getWidth() - x;
|
if (x + width > img.getWidth()) width = img.getWidth() - x;
|
||||||
if (y + height > img.getHeight()) height = img.getHeight() - y;
|
if (y + height > img.getHeight()) height = img.getHeight() - y;
|
||||||
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue());
|
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height);
|
||||||
rectangle.setKeyPoints(keyPoints);
|
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||||
rectangleList.add(rectangle);
|
DetectionInfo detectionInfo = new DetectionInfo(rectangle, detection.getProbabilities().get(index).floatValue(),faceInfo);
|
||||||
|
detectionInfoList.add(detectionInfo);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
detectionResponse.setRectangleList(rectangleList);
|
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||||
return detectionResponse;
|
return detectionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ public class FaceUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
DetectionResponse detectionResponse = new DetectionResponse();
|
DetectionResponse detectionResponse = new DetectionResponse();
|
||||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
for(int i = 0; i < seetaResult.length; i++){
|
for(int i = 0; i < seetaResult.length; i++){
|
||||||
SeetaRect rect = seetaResult[i];
|
SeetaRect rect = seetaResult[i];
|
||||||
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||||
@@ -91,14 +92,15 @@ public class FaceUtils {
|
|||||||
/*if(config.getConfidenceThreshold() > 0){
|
/*if(config.getConfidenceThreshold() > 0){
|
||||||
continue;
|
continue;
|
||||||
}*/
|
}*/
|
||||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height, 0);
|
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||||
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||||
.map(p -> new Point(p.x, p.y))
|
.map(p -> new Point(p.x, p.y))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
rectangle.setKeyPoints(keyPoints);
|
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||||
rectangleList.add(rectangle);
|
DetectionInfo detectionInfo = new DetectionInfo(rectangle, 0, faceInfo);
|
||||||
|
detectionInfoList.add(detectionInfo);
|
||||||
}
|
}
|
||||||
detectionResponse.setRectangleList(rectangleList);
|
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||||
return detectionResponse;
|
return detectionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ public class FaceUtils {
|
|||||||
if(!ImageUtils.isImageValid(sourceImage)){
|
if(!ImageUtils.isImageValid(sourceImage)){
|
||||||
throw new FaceException("图像无效");
|
throw new FaceException("图像无效");
|
||||||
}
|
}
|
||||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("无目标数据");
|
throw new FaceException("无目标数据");
|
||||||
}
|
}
|
||||||
Graphics2D graphics = sourceImage.createGraphics();
|
Graphics2D graphics = sourceImage.createGraphics();
|
||||||
@@ -122,13 +124,15 @@ public class FaceUtils {
|
|||||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||||
int stroke = 2;
|
int stroke = 2;
|
||||||
for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
|
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||||
|
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||||
graphics.setColor(Color.RED);// 边框颜色
|
graphics.setColor(Color.RED);// 边框颜色
|
||||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||||
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||||
//绘制人脸关键点
|
//绘制人脸关键点
|
||||||
if(rectangle.getKeyPoints() != null){
|
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||||
drawLandmarks(graphics, rectangle.getKeyPoints());
|
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||||
|
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
graphics.dispose();
|
graphics.dispose();
|
||||||
@@ -145,7 +149,7 @@ public class FaceUtils {
|
|||||||
if(!ImageUtils.isImageValid(sourceImage)){
|
if(!ImageUtils.isImageValid(sourceImage)){
|
||||||
throw new FaceException("图像无效");
|
throw new FaceException("图像无效");
|
||||||
}
|
}
|
||||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
throw new FaceException("无目标数据");
|
throw new FaceException("无目标数据");
|
||||||
}
|
}
|
||||||
Graphics2D graphics = sourceImage.createGraphics();
|
Graphics2D graphics = sourceImage.createGraphics();
|
||||||
@@ -154,13 +158,15 @@ public class FaceUtils {
|
|||||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||||
int stroke = 2;
|
int stroke = 2;
|
||||||
for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
|
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||||
|
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||||
graphics.setColor(Color.RED);// 边框颜色
|
graphics.setColor(Color.RED);// 边框颜色
|
||||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||||
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||||
//绘制人脸关键点
|
//绘制人脸关键点
|
||||||
if(rectangle.getKeyPoints() != null){
|
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||||
drawLandmarks(graphics, rectangle.getKeyPoints());
|
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||||
|
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
graphics.dispose();
|
graphics.dispose();
|
||||||
@@ -215,7 +221,7 @@ public class FaceUtils {
|
|||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return null; // 无效区域
|
return null; // 无效区域
|
||||||
}
|
}
|
||||||
return new DetectionRectangle(x, y, width, height, rectangle.score);
|
return new DetectionRectangle(x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,4 +315,263 @@ public class FaceUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将DetectionRectangle转换为SeetaRect
|
||||||
|
* @param detectionRectangle
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static SeetaRect convertToSeetaRect(DetectionRectangle detectionRectangle){
|
||||||
|
SeetaRect seetaRect = new SeetaRect();
|
||||||
|
seetaRect.x = detectionRectangle.getX();
|
||||||
|
seetaRect.y = detectionRectangle.getY();
|
||||||
|
seetaRect.width = detectionRectangle.getWidth();
|
||||||
|
seetaRect.height = detectionRectangle.getHeight();
|
||||||
|
return seetaRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将PointList转换为SeetaPointF[]
|
||||||
|
* @param pointList
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static SeetaPointF[] convertToSeetaPointF(List<Point> pointList){
|
||||||
|
return pointList.stream()
|
||||||
|
.map(p -> {
|
||||||
|
SeetaPointF sp = new SeetaPointF();
|
||||||
|
sp.x = p.getX();
|
||||||
|
sp.y = p.getY();
|
||||||
|
return sp;
|
||||||
|
})
|
||||||
|
.toArray(SeetaPointF[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将SeetaAntiSpoofing.Status转换为LivenessStatus
|
||||||
|
* @param status
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static LivenessStatus convertToLivenessStatus(FaceAntiSpoofing.Status status){
|
||||||
|
if(status == null){
|
||||||
|
return LivenessStatus.UNKNOWN;
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case REAL:
|
||||||
|
return LivenessStatus.LIVE;
|
||||||
|
case SPOOF:
|
||||||
|
return LivenessStatus.NON_LIVE;
|
||||||
|
case FUZZY:
|
||||||
|
return LivenessStatus.UNKNOWN;
|
||||||
|
case DETECTING:
|
||||||
|
return LivenessStatus.DETECTING;
|
||||||
|
default:
|
||||||
|
return LivenessStatus.UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转为GenderType
|
||||||
|
* @param gender
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static GenderType convertToGenderType(GenderPredictor.GENDER gender){
|
||||||
|
if(gender == null){
|
||||||
|
return GenderType.UNKNOWN;
|
||||||
|
}
|
||||||
|
switch (gender) {
|
||||||
|
case MALE:
|
||||||
|
return GenderType.MALE;
|
||||||
|
case FEMALE:
|
||||||
|
return GenderType.FEMALE;
|
||||||
|
default:
|
||||||
|
return GenderType.UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转为EyeStatus
|
||||||
|
* @param eyeState
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static EyeStatus convertToEyeStatus(EyeStateDetector.EYE_STATE eyeState){
|
||||||
|
if(eyeState == null){
|
||||||
|
return EyeStatus.UNKNOWN;
|
||||||
|
}
|
||||||
|
switch (eyeState) {
|
||||||
|
case EYE_OPEN:
|
||||||
|
return EyeStatus.OPEN;
|
||||||
|
case EYE_CLOSE:
|
||||||
|
return EyeStatus.CLOSED;
|
||||||
|
case EYE_RANDOM:
|
||||||
|
return EyeStatus.NON_EYE_REGION;
|
||||||
|
default:
|
||||||
|
return EyeStatus.UNKNOWN; // 默认返回未知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DetectionResponse convertToFaceAttributeResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<FaceAttribute> faceAttributeList){
|
||||||
|
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
|
for(int i = 0; i < seetaResult.length; i++){
|
||||||
|
SeetaRect rect = seetaResult[i];
|
||||||
|
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
FaceInfo faceInfo = new FaceInfo();
|
||||||
|
if(seetaPointFSList != null && seetaPointFSList.size() > 0){
|
||||||
|
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||||
|
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||||
|
.map(p -> new Point(p.x, p.y))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
faceInfo.setKeyPoints(keyPoints);
|
||||||
|
}
|
||||||
|
if(faceAttributeList != null && faceAttributeList.size() > 0){
|
||||||
|
faceInfo.setFaceAttribute(faceAttributeList.get(i));
|
||||||
|
}
|
||||||
|
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
|
||||||
|
}
|
||||||
|
return new DetectionResponse(detectionInfoList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为FaceDetectedResult
|
||||||
|
* @param seetaResult
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<LivenessStatus> livenessStatusList){
|
||||||
|
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
DetectionResponse detectionResponse = new DetectionResponse();
|
||||||
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
|
for(int i = 0; i < seetaResult.length; i++){
|
||||||
|
SeetaRect rect = seetaResult[i];
|
||||||
|
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||||
|
//过滤置信度
|
||||||
|
/*if(config.getConfidenceThreshold() > 0){
|
||||||
|
continue;
|
||||||
|
}*/
|
||||||
|
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||||
|
.map(p -> new Point(p.x, p.y))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||||
|
faceInfo.setLivenessStatus(livenessStatusList.get(i));
|
||||||
|
DetectionInfo detectionInfo = new DetectionInfo(rectangle, 0, faceInfo);
|
||||||
|
detectionInfoList.add(detectionInfo);
|
||||||
|
}
|
||||||
|
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||||
|
return detectionResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制人脸属性
|
||||||
|
* @param sourceImage
|
||||||
|
* @param detectionResponse
|
||||||
|
* @param savePath
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static void drawBoxesWithFaceAttribute(BufferedImage sourceImage, DetectionResponse detectionResponse, String savePath) throws IOException {
|
||||||
|
if(!ImageUtils.isImageValid(sourceImage)){
|
||||||
|
throw new FaceException("图像无效");
|
||||||
|
}
|
||||||
|
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
|
throw new FaceException("无目标数据");
|
||||||
|
}
|
||||||
|
Graphics2D graphics = sourceImage.createGraphics();
|
||||||
|
graphics.setColor(Color.RED);// 边框颜色
|
||||||
|
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
|
||||||
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||||
|
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||||
|
int stroke = 2;
|
||||||
|
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||||
|
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||||
|
graphics.setColor(Color.RED);// 边框颜色
|
||||||
|
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||||
|
//drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||||
|
//绘制人脸关键点
|
||||||
|
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||||
|
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||||
|
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||||
|
}
|
||||||
|
// 判断人脸框是否足够大
|
||||||
|
if (rectangle.getHeight() > 60 && detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getFaceAttribute() != null) {
|
||||||
|
StringBuilder attrText = new StringBuilder();
|
||||||
|
FaceAttribute faceAttribute = detectionInfo.getFaceInfo().getFaceAttribute();
|
||||||
|
if (faceAttribute.getGenderType() != null) {
|
||||||
|
attrText.append(faceAttribute.getGenderType().name()).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceAttribute.getAge() != null) {
|
||||||
|
attrText.append(faceAttribute.getAge()).append("岁").append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceAttribute.getWearingMask() != null) {
|
||||||
|
attrText.append(faceAttribute.getWearingMask() ? "戴口罩" : "未戴口罩").append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||||
|
attrText.append("眼睛:")
|
||||||
|
.append(faceAttribute.getLeftEyeStatus().name())
|
||||||
|
.append("/")
|
||||||
|
.append(faceAttribute.getRightEyeStatus().name())
|
||||||
|
.append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
if (faceAttribute.getGenderType() != null) {
|
||||||
|
lines.add("性别: " + faceAttribute.getGenderType().name());
|
||||||
|
}
|
||||||
|
if (faceAttribute.getAge() != null) {
|
||||||
|
lines.add("年龄: " + faceAttribute.getAge());
|
||||||
|
}
|
||||||
|
if (faceAttribute.getWearingMask() != null) {
|
||||||
|
lines.add("口罩: " + (faceAttribute.getWearingMask() ? "是" : "否"));
|
||||||
|
}
|
||||||
|
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||||
|
lines.add("眼睛: " + faceAttribute.getLeftEyeStatus().name() + "/" + faceAttribute.getRightEyeStatus().name());
|
||||||
|
}
|
||||||
|
if (faceAttribute.getHeadPose() != null) {
|
||||||
|
//attrText.append("姿态:").append(faceAttribute.getHeadPose().toString());
|
||||||
|
HeadPose pose = faceAttribute.getHeadPose();
|
||||||
|
String pitch = pose.getPitch() != null ? String.valueOf(pose.getPitch().intValue()) : "-";
|
||||||
|
String yaw = pose.getYaw() != null ? String.valueOf(pose.getYaw().intValue()) : "-";
|
||||||
|
String roll = pose.getRoll() != null ? String.valueOf(pose.getRoll().intValue()) : "-";
|
||||||
|
lines.add("姿态: P=" + pitch + " Y=" + yaw + " R=" + roll);
|
||||||
|
}
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
drawMultilineTextWithBackground(graphics, lines, rectangle.getX(), rectangle.getY()); // 适当偏移
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graphics.dispose();
|
||||||
|
ImageIO.write(sourceImage, "jpg", new File(savePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawMultilineTextWithBackground(Graphics2D g, List<String> lines, int x, int y) {
|
||||||
|
Font font = new Font("SansSerif", Font.PLAIN, 14);
|
||||||
|
g.setFont(font);
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int lineHeight = fm.getHeight();
|
||||||
|
int maxWidth = lines.stream().mapToInt(fm::stringWidth).max().orElse(0);
|
||||||
|
|
||||||
|
int padding = 4;
|
||||||
|
int boxWidth = maxWidth + padding * 2;
|
||||||
|
int boxHeight = lineHeight * lines.size() + padding * 2;
|
||||||
|
|
||||||
|
// 背景矩形
|
||||||
|
g.setColor(new Color(0, 0, 0, 128));
|
||||||
|
g.fillRoundRect(x, y, boxWidth, boxHeight, 8, 8);
|
||||||
|
|
||||||
|
// 绘制每一行文字
|
||||||
|
g.setColor(Color.WHITE);
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
g.drawString(lines.get(i), x + padding, y + padding + (i + 1) * lineHeight - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
<name>smartjavaai-objectdetection</name>
|
<name>smartjavaai-objectdetection</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package cn.smartjavaai.objectdetection.utils;
|
|||||||
import ai.djl.modality.cv.Image;
|
import ai.djl.modality.cv.Image;
|
||||||
import ai.djl.modality.cv.output.BoundingBox;
|
import ai.djl.modality.cv.output.BoundingBox;
|
||||||
import ai.djl.modality.cv.output.DetectedObjects;
|
import ai.djl.modality.cv.output.DetectedObjects;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.common.entity.ObjectDetInfo;
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
import cn.smartjavaai.common.utils.ImageUtils;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -37,7 +39,7 @@ public class DetectorUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
DetectionResponse detectionResponse = new DetectionResponse();
|
DetectionResponse detectionResponse = new DetectionResponse();
|
||||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||||
Iterator iterator = detectedObjectList.iterator();
|
Iterator iterator = detectedObjectList.iterator();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
@@ -49,11 +51,14 @@ public class DetectorUtils {
|
|||||||
int y = (int)(box.getBounds().getY() * (double)img.getHeight());
|
int y = (int)(box.getBounds().getY() * (double)img.getHeight());
|
||||||
int width = (int)(box.getBounds().getWidth() * (double)img.getWidth());
|
int width = (int)(box.getBounds().getWidth() * (double)img.getWidth());
|
||||||
int height = (int)(box.getBounds().getHeight() * (double)img.getHeight());
|
int height = (int)(box.getBounds().getHeight() * (double)img.getHeight());
|
||||||
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue(),className);
|
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height);
|
||||||
rectangleList.add(rectangle);
|
DetectionInfo detectionInfo = new DetectionInfo(rectangle, detection.getProbabilities().get(index).floatValue());
|
||||||
|
ObjectDetInfo objectDetInfo = new ObjectDetInfo(className);
|
||||||
|
detectionInfo.setObjectDetInfo(objectDetInfo);
|
||||||
|
detectionInfoList.add(detectionInfo);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
detectionResponse.setRectangleList(rectangleList);
|
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||||
return detectionResponse;
|
return detectionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>smartjavaai-ocr</artifactId>
|
<artifactId>smartjavaai-ocr</artifactId>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<version>1.0.11</version>
|
<version>1.0.12</version>
|
||||||
<name>smartjavaai-ocr</name>
|
<name>smartjavaai-ocr</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr;
|
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 人脸识别算法
|
|
||||||
* @author dwj
|
|
||||||
*/
|
|
||||||
public abstract class AbstractOcrModel implements OcrModel {
|
|
||||||
@Override
|
|
||||||
public void loadModel(OcrModelConfig config) {
|
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetectionResponse detect(String imagePath) {
|
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void detectAndDraw(String imagePath, String outputPath) {
|
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
package cn.smartjavaai.ocr;
|
package cn.smartjavaai.ocr.detection;
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸识别算法
|
* 人脸识别算法
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
public interface OcrModel {
|
public interface OcrDetModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载模型
|
* 加载模型
|
||||||
* @param config
|
* @param config
|
||||||
*/
|
*/
|
||||||
void loadModel(OcrModelConfig config); // 加载模型
|
void loadModel(OcrDetModelConfig config); // 加载模型
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 人脸检测
|
* 人脸检测
|
||||||
* @param imagePath 图片路径
|
* @param imagePath 图片路径
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
DetectionResponse detect(String imagePath);
|
default DetectionResponse detect(String imagePath) {
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测并绘制结果
|
* 检测并绘制结果
|
||||||
* @param imagePath 图片输入路径(包含文件名称)
|
* @param imagePath 图片输入路径(包含文件名称)
|
||||||
* @param outputPath 图片输出路径(包含文件名称)
|
* @param outputPath 图片输出路径(包含文件名称)
|
||||||
*/
|
*/
|
||||||
void detectAndDraw(String imagePath, String outputPath);
|
default void detectAndDraw(String imagePath, String outputPath) {
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.ocr;
|
package cn.smartjavaai.ocr.detection;
|
||||||
|
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -8,12 +8,12 @@ import lombok.Data;
|
|||||||
* @date 2025/4/22
|
* @date 2025/4/22
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class OcrModelConfig {
|
public class OcrDetModelConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模型名称
|
* 模型名称
|
||||||
*/
|
*/
|
||||||
private OcrModelEnum modelEnum;
|
private OcrDetModelEnum modelEnum;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备类型
|
* 设备类型
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package cn.smartjavaai.ocr;
|
package cn.smartjavaai.ocr.detection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OCR模型枚举
|
* OCR模型枚举
|
||||||
* @author dwj
|
* @author dwj
|
||||||
* @date 2025/4/4
|
* @date 2025/4/4
|
||||||
*/
|
*/
|
||||||
public enum OcrModelEnum {
|
public enum OcrDetModelEnum {
|
||||||
|
|
||||||
PADDLEOCR_V4_DET_MODEL;
|
PADDLEOCR_V4_DET_MODEL;
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@ public enum OcrModelEnum {
|
|||||||
/**
|
/**
|
||||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||||
*/
|
*/
|
||||||
public static OcrModelEnum fromName(String name) {
|
public static OcrDetModelEnum fromName(String name) {
|
||||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||||
for (OcrModelEnum model : values()) {
|
for (OcrDetModelEnum model : values()) {
|
||||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package cn.smartjavaai.ocr;
|
package cn.smartjavaai.ocr.detection;
|
||||||
|
|
||||||
import cn.smartjavaai.common.config.Config;
|
import cn.smartjavaai.common.config.Config;
|
||||||
import cn.smartjavaai.ocr.exception.OcrException;
|
import cn.smartjavaai.ocr.exception.OcrException;
|
||||||
import cn.smartjavaai.ocr.model.PaddleOCRV4DetectModel;
|
import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -14,25 +14,25 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OcrModelFactory {
|
public class OcrDetModelFactory {
|
||||||
|
|
||||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||||
private static volatile OcrModelFactory instance;
|
private static volatile OcrDetModelFactory instance;
|
||||||
|
|
||||||
private static final ConcurrentHashMap<String, OcrModel> modelMap = new ConcurrentHashMap<>();
|
private static final ConcurrentHashMap<String, OcrDetModel> modelMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 算法注册表
|
* 算法注册表
|
||||||
*/
|
*/
|
||||||
private static final Map<String, Class<? extends OcrModel>> registry =
|
private static final Map<String, Class<? extends OcrDetModel>> registry =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
public static OcrModelFactory getInstance() {
|
public static OcrDetModelFactory getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (OcrModelFactory.class) {
|
synchronized (OcrDetModelFactory.class) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new OcrModelFactory();
|
instance = new OcrDetModelFactory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ public class OcrModelFactory {
|
|||||||
* @param name
|
* @param name
|
||||||
* @param clazz
|
* @param clazz
|
||||||
*/
|
*/
|
||||||
private static void registerModel(String name, Class<? extends OcrModel> clazz) {
|
private static void registerModel(String name, Class<? extends OcrDetModel> clazz) {
|
||||||
registry.put(name.toLowerCase(), clazz);
|
registry.put(name.toLowerCase(), clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ public class OcrModelFactory {
|
|||||||
* @param config
|
* @param config
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public OcrModel getModel(OcrModelConfig config) {
|
public OcrDetModel getModel(OcrDetModelConfig config) {
|
||||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||||
throw new OcrException("未配置OCR模型");
|
throw new OcrException("未配置OCR模型");
|
||||||
}
|
}
|
||||||
@@ -70,14 +70,14 @@ public class OcrModelFactory {
|
|||||||
* @param config
|
* @param config
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private OcrModel createFaceModel(OcrModelConfig config) {
|
private OcrDetModel createFaceModel(OcrDetModelConfig config) {
|
||||||
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||||
if(clazz == null){
|
if(clazz == null){
|
||||||
throw new OcrException("Unsupported model");
|
throw new OcrException("Unsupported model");
|
||||||
}
|
}
|
||||||
OcrModel algorithm = null;
|
OcrDetModel algorithm = null;
|
||||||
try {
|
try {
|
||||||
algorithm = (OcrModel) clazz.newInstance();
|
algorithm = (OcrDetModel) clazz.newInstance();
|
||||||
} catch (InstantiationException | IllegalAccessException e) {
|
} catch (InstantiationException | IllegalAccessException e) {
|
||||||
throw new OcrException(e);
|
throw new OcrException(e);
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ public class OcrModelFactory {
|
|||||||
|
|
||||||
// 初始化默认算法
|
// 初始化默认算法
|
||||||
static {
|
static {
|
||||||
registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetectModel.class);
|
registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetModel.class);
|
||||||
log.info("缓存目录:{}", Config.getCachePath());
|
log.info("缓存目录:{}", Config.getCachePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.model;
|
|
||||||
|
|
||||||
import ai.djl.inference.Predictor;
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import ai.djl.ndarray.NDList;
|
|
||||||
import ai.djl.ndarray.NDManager;
|
|
||||||
import ai.djl.repository.zoo.Criteria;
|
|
||||||
import ai.djl.repository.zoo.ModelZoo;
|
|
||||||
import ai.djl.repository.zoo.ZooModel;
|
|
||||||
import ai.djl.training.util.ProgressBar;
|
|
||||||
import cn.smartjavaai.ocr.translator.PaddleOCRV4DetectionTranslator;
|
|
||||||
import org.opencv.core.Mat;
|
|
||||||
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author dwj
|
|
||||||
* @date 2025/4/21
|
|
||||||
*/
|
|
||||||
public class PaddleOCRV4Model {
|
|
||||||
|
|
||||||
|
|
||||||
public void loadModel(){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
package cn.smartjavaai.ocr.model;
|
package cn.smartjavaai.ocr.ppv4.model;
|
||||||
|
|
||||||
import ai.djl.MalformedModelException;
|
import ai.djl.MalformedModelException;
|
||||||
import ai.djl.inference.Predictor;
|
import ai.djl.inference.Predictor;
|
||||||
import ai.djl.modality.cv.Image;
|
import ai.djl.modality.cv.Image;
|
||||||
import ai.djl.modality.cv.ImageFactory;
|
import ai.djl.modality.cv.ImageFactory;
|
||||||
import ai.djl.modality.cv.output.DetectedObjects;
|
|
||||||
import ai.djl.ndarray.NDList;
|
import ai.djl.ndarray.NDList;
|
||||||
import ai.djl.ndarray.NDManager;
|
|
||||||
import ai.djl.repository.zoo.Criteria;
|
import ai.djl.repository.zoo.Criteria;
|
||||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||||
import ai.djl.repository.zoo.ModelZoo;
|
import ai.djl.repository.zoo.ModelZoo;
|
||||||
import ai.djl.repository.zoo.ZooModel;
|
import ai.djl.repository.zoo.ZooModel;
|
||||||
import ai.djl.training.util.ProgressBar;
|
import ai.djl.training.util.ProgressBar;
|
||||||
import ai.djl.translate.TranslateException;
|
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||||
import cn.smartjavaai.common.utils.FileUtils;
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
import cn.smartjavaai.ocr.AbstractOcrModel;
|
import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
|
||||||
import cn.smartjavaai.ocr.OcrModelConfig;
|
|
||||||
import cn.smartjavaai.ocr.exception.OcrException;
|
import cn.smartjavaai.ocr.exception.OcrException;
|
||||||
import cn.smartjavaai.ocr.translator.PaddleOCRV4DetectionTranslator;
|
import cn.smartjavaai.ocr.detection.OcrDetModel;
|
||||||
|
import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
|
||||||
import cn.smartjavaai.ocr.utils.ImageUtils;
|
import cn.smartjavaai.ocr.utils.ImageUtils;
|
||||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -40,14 +37,14 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* @date 2025/4/21
|
* @date 2025/4/21
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PaddleOCRV4DetectModel extends AbstractOcrModel {
|
public class PaddleOCRV4DetModel implements OcrDetModel {
|
||||||
|
|
||||||
private ZooModel detectionModel;
|
private ZooModel detectionModel;
|
||||||
|
|
||||||
private ObjectPool<Predictor<Image, NDList>> predictorPool;
|
private ObjectPool<Predictor<Image, NDList>> predictorPool;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadModel(OcrModelConfig config){
|
public void loadModel(OcrDetModelConfig config){
|
||||||
if(StringUtils.isBlank(config.getModelPath())){
|
if(StringUtils.isBlank(config.getModelPath())){
|
||||||
throw new OcrException("modelPath is null");
|
throw new OcrException("modelPath is null");
|
||||||
}
|
}
|
||||||
@@ -56,7 +53,7 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
|
|||||||
.optEngine("OnnxRuntime")
|
.optEngine("OnnxRuntime")
|
||||||
.setTypes(Image.class, NDList.class)
|
.setTypes(Image.class, NDList.class)
|
||||||
.optModelPath(Paths.get(config.getModelPath()))
|
.optModelPath(Paths.get(config.getModelPath()))
|
||||||
.optTranslator(new PaddleOCRV4DetectionTranslator(new ConcurrentHashMap<String, String>()))
|
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||||
.optProgress(new ProgressBar())
|
.optProgress(new ProgressBar())
|
||||||
.build();
|
.build();
|
||||||
try{
|
try{
|
||||||
@@ -83,11 +80,6 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
|
|||||||
return detect(img);
|
return detect(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 人脸检测
|
|
||||||
* @param image
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private DetectionResponse detect(Image image){
|
private DetectionResponse detect(Image image){
|
||||||
Predictor<Image, NDList> predictor = null;
|
Predictor<Image, NDList> predictor = null;
|
||||||
try {
|
try {
|
||||||
@@ -120,7 +112,7 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
|
|||||||
try {
|
try {
|
||||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||||
DetectionResponse detectionResponse = detect(img);
|
DetectionResponse detectionResponse = detect(img);
|
||||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
|
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
throw new OcrException("未识别到文字");
|
throw new OcrException("未识别到文字");
|
||||||
}
|
}
|
||||||
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
|
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package cn.smartjavaai.ocr.ppv4.model;
|
||||||
|
|
||||||
|
import ai.djl.MalformedModelException;
|
||||||
|
import ai.djl.inference.Predictor;
|
||||||
|
import ai.djl.modality.cv.Image;
|
||||||
|
import ai.djl.modality.cv.ImageFactory;
|
||||||
|
import ai.djl.ndarray.NDList;
|
||||||
|
import ai.djl.repository.zoo.Criteria;
|
||||||
|
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||||
|
import ai.djl.repository.zoo.ModelZoo;
|
||||||
|
import ai.djl.repository.zoo.ZooModel;
|
||||||
|
import ai.djl.training.util.ProgressBar;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||||
|
import cn.smartjavaai.common.utils.FileUtils;
|
||||||
|
import cn.smartjavaai.ocr.detection.OcrDetModel;
|
||||||
|
import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
|
||||||
|
import cn.smartjavaai.ocr.exception.OcrException;
|
||||||
|
import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
|
||||||
|
import cn.smartjavaai.ocr.recognition.OcrRecModel;
|
||||||
|
import cn.smartjavaai.ocr.recognition.OcrRecModelConfig;
|
||||||
|
import cn.smartjavaai.ocr.utils.ImageUtils;
|
||||||
|
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.pool2.ObjectPool;
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||||
|
import org.opencv.core.Mat;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PaddleOCRV4 识别模型实现
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/21
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class PaddleOCRV4RecModel implements OcrRecModel {
|
||||||
|
|
||||||
|
private ZooModel detectionModel;
|
||||||
|
|
||||||
|
private ObjectPool<Predictor<Image, NDList>> predictorPool;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadModel(OcrRecModelConfig config){
|
||||||
|
if(StringUtils.isBlank(config.getModelPath())){
|
||||||
|
throw new OcrException("modelPath is null");
|
||||||
|
}
|
||||||
|
Criteria<Image, NDList> criteria =
|
||||||
|
Criteria.builder()
|
||||||
|
.optEngine("OnnxRuntime")
|
||||||
|
.setTypes(Image.class, NDList.class)
|
||||||
|
.optModelPath(Paths.get(config.getModelPath()))
|
||||||
|
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||||
|
.optProgress(new ProgressBar())
|
||||||
|
.build();
|
||||||
|
try{
|
||||||
|
detectionModel = ModelZoo.loadModel(criteria);
|
||||||
|
// 创建池子:每个线程独享 Predictor
|
||||||
|
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||||
|
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||||
|
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||||
|
throw new OcrException("模型加载失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DetectionResponse detect(String imagePath){
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new OcrException("图像文件不存在");
|
||||||
|
}
|
||||||
|
Image img = null;
|
||||||
|
try {
|
||||||
|
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OcrException("无效的图片", e);
|
||||||
|
}
|
||||||
|
return detect(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DetectionResponse detect(Image image){
|
||||||
|
Predictor<Image, NDList> predictor = null;
|
||||||
|
try {
|
||||||
|
predictor = predictorPool.borrowObject();
|
||||||
|
NDList result = predictor.predict(image);
|
||||||
|
return OcrUtils.convertToDetectionResponse(result, image);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OcrException("OCR检测错误", e);
|
||||||
|
}finally {
|
||||||
|
if (predictor != null) {
|
||||||
|
try {
|
||||||
|
predictorPool.returnObject(predictor); //归还
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
try {
|
||||||
|
predictor.close(); // 归还失败才销毁
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("关闭Predictor失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void detectAndDraw(String imagePath, String outputPath) {
|
||||||
|
if(!FileUtils.isFileExists(imagePath)){
|
||||||
|
throw new OcrException("图像文件不存在");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||||
|
DetectionResponse detectionResponse = detect(img);
|
||||||
|
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||||
|
throw new OcrException("未识别到文字");
|
||||||
|
}
|
||||||
|
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
|
||||||
|
Path output = Paths.get(outputPath);
|
||||||
|
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||||
|
img.save(Files.newOutputStream(output), "png");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OcrException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.ocr.translator;
|
package cn.smartjavaai.ocr.ppv4.translator;
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
import ai.djl.modality.cv.Image;
|
||||||
import ai.djl.modality.cv.util.NDImageUtils;
|
import ai.djl.modality.cv.util.NDImageUtils;
|
||||||
@@ -27,7 +27,7 @@ import java.util.Map;
|
|||||||
* @mail 179209347@qq.com
|
* @mail 179209347@qq.com
|
||||||
* @website www.aias.top
|
* @website www.aias.top
|
||||||
*/
|
*/
|
||||||
public class PaddleOCRV4DetectionTranslator implements Translator<Image, NDList> {
|
public class PaddleOCRV4DetectTranslator implements Translator<Image, NDList> {
|
||||||
// det_algorithm == "DB"
|
// det_algorithm == "DB"
|
||||||
private final float thresh = 0.3f;
|
private final float thresh = 0.3f;
|
||||||
private final boolean use_dilation = false;
|
private final boolean use_dilation = false;
|
||||||
@@ -44,7 +44,7 @@ public class PaddleOCRV4DetectionTranslator implements Translator<Image, NDList>
|
|||||||
private int img_height;
|
private int img_height;
|
||||||
private int img_width;
|
private int img_width;
|
||||||
|
|
||||||
public PaddleOCRV4DetectionTranslator(Map<String, ?> arguments) {
|
public PaddleOCRV4DetectTranslator(Map<String, ?> arguments) {
|
||||||
limit_side_len =
|
limit_side_len =
|
||||||
arguments.containsKey("limit_side_len")
|
arguments.containsKey("limit_side_len")
|
||||||
? Integer.parseInt(arguments.get("limit_side_len").toString())
|
? Integer.parseInt(arguments.get("limit_side_len").toString())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cn.smartjavaai.ocr.translator;
|
package cn.smartjavaai.ocr.ppv4.translator;
|
||||||
|
|
||||||
import ai.djl.Model;
|
import ai.djl.Model;
|
||||||
import ai.djl.modality.cv.Image;
|
import ai.djl.modality.cv.Image;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package cn.smartjavaai.ocr.recognition;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OCR模型
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
public interface OcrRecModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载模型
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
void loadModel(OcrRecModelConfig config); // 加载模型
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人脸检测
|
||||||
|
* @param imagePath 图片路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default DetectionResponse detect(String imagePath) {
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测并绘制结果
|
||||||
|
* @param imagePath 图片输入路径(包含文件名称)
|
||||||
|
* @param outputPath 图片输出路径(包含文件名称)
|
||||||
|
*/
|
||||||
|
default void detectAndDraw(String imagePath, String outputPath) {
|
||||||
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cn.smartjavaai.ocr.recognition;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/22
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OcrRecModelConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型名称
|
||||||
|
*/
|
||||||
|
private OcrRecModelEnum modelEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备类型
|
||||||
|
*/
|
||||||
|
private DeviceEnum device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型路径
|
||||||
|
*/
|
||||||
|
private String modelPath;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package cn.smartjavaai.ocr.recognition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OCR识别模型枚举
|
||||||
|
* @author dwj
|
||||||
|
* @date 2025/4/4
|
||||||
|
*/
|
||||||
|
public enum OcrRecModelEnum {
|
||||||
|
|
||||||
|
PADDLEOCR_V4_REC_MODEL;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||||
|
*/
|
||||||
|
public static OcrRecModelEnum fromName(String name) {
|
||||||
|
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||||
|
for (OcrRecModelEnum model : values()) {
|
||||||
|
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package cn.smartjavaai.ocr.recognition;
|
||||||
|
|
||||||
|
import cn.smartjavaai.common.config.Config;
|
||||||
|
import cn.smartjavaai.ocr.exception.OcrException;
|
||||||
|
import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OCR模型工厂
|
||||||
|
* @author dwj
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OcrRecModelFactory {
|
||||||
|
|
||||||
|
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||||
|
private static volatile OcrRecModelFactory instance;
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, OcrRecModel> modelMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 算法注册表
|
||||||
|
*/
|
||||||
|
private static final Map<String, Class<? extends OcrRecModel>> registry =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public static OcrRecModelFactory getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (OcrRecModelFactory.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new OcrRecModelFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册算法
|
||||||
|
* @param name
|
||||||
|
* @param clazz
|
||||||
|
*/
|
||||||
|
private static void registerModel(String name, Class<? extends OcrRecModel> clazz) {
|
||||||
|
registry.put(name.toLowerCase(), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型(通过配置)
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public OcrRecModel getModel(OcrRecModelConfig config) {
|
||||||
|
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||||
|
throw new OcrException("未配置OCR模型");
|
||||||
|
}
|
||||||
|
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||||
|
return createFaceModel(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用ModelConfig创建算法
|
||||||
|
* @param config
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private OcrRecModel createFaceModel(OcrRecModelConfig config) {
|
||||||
|
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||||
|
if(clazz == null){
|
||||||
|
throw new OcrException("Unsupported model");
|
||||||
|
}
|
||||||
|
OcrRecModel algorithm = null;
|
||||||
|
try {
|
||||||
|
algorithm = (OcrRecModel) clazz.newInstance();
|
||||||
|
} catch (InstantiationException | IllegalAccessException e) {
|
||||||
|
throw new OcrException(e);
|
||||||
|
}
|
||||||
|
algorithm.loadModel(config);
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化默认算法
|
||||||
|
static {
|
||||||
|
//registerModel("PADDLEOCR_V4_REC_MODEL", PaddleOCRV4DetModel.class);
|
||||||
|
log.info("缓存目录:{}", Config.getCachePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import ai.djl.modality.cv.Image;
|
|||||||
import ai.djl.modality.cv.ImageFactory;
|
import ai.djl.modality.cv.ImageFactory;
|
||||||
import ai.djl.modality.cv.output.DetectedObjects;
|
import ai.djl.modality.cv.output.DetectedObjects;
|
||||||
import ai.djl.ndarray.NDArray;
|
import ai.djl.ndarray.NDArray;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||||
@@ -207,7 +208,8 @@ public class ImageUtils {
|
|||||||
*/
|
*/
|
||||||
public static void drawRect(Mat mat, DetectionResponse detectionResponse) {
|
public static void drawRect(Mat mat, DetectionResponse detectionResponse) {
|
||||||
|
|
||||||
for(DetectionRectangle detectionRectangle : detectionResponse.getRectangleList()){
|
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||||
|
DetectionRectangle detectionRectangle = detectionInfo.getDetectionRectangle();
|
||||||
// 左上角点
|
// 左上角点
|
||||||
Point topLeft = new Point(detectionRectangle.getX(), detectionRectangle.getY());
|
Point topLeft = new Point(detectionRectangle.getX(), detectionRectangle.getY());
|
||||||
// 右下角点
|
// 右下角点
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ai.djl.modality.cv.output.BoundingBox;
|
|||||||
import ai.djl.modality.cv.output.DetectedObjects;
|
import ai.djl.modality.cv.output.DetectedObjects;
|
||||||
import ai.djl.ndarray.NDArray;
|
import ai.djl.ndarray.NDArray;
|
||||||
import ai.djl.ndarray.NDList;
|
import ai.djl.ndarray.NDList;
|
||||||
|
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -34,7 +35,7 @@ public class OcrUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
DetectionResponse detectionResponse = new DetectionResponse();
|
DetectionResponse detectionResponse = new DetectionResponse();
|
||||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||||
for(NDArray box : dt_boxes){
|
for(NDArray box : dt_boxes){
|
||||||
DetectionRectangle rectangle = new DetectionRectangle();
|
DetectionRectangle rectangle = new DetectionRectangle();
|
||||||
float[] points = box.toFloatArray();
|
float[] points = box.toFloatArray();
|
||||||
@@ -54,9 +55,9 @@ public class OcrUtils {
|
|||||||
rectangle.setY(y);
|
rectangle.setY(y);
|
||||||
rectangle.setHeight(height);
|
rectangle.setHeight(height);
|
||||||
rectangle.setWidth(width);
|
rectangle.setWidth(width);
|
||||||
rectangleList.add(rectangle);
|
detectionInfoList.add(new DetectionInfo(rectangle));
|
||||||
}
|
}
|
||||||
detectionResponse.setRectangleList(rectangleList);
|
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||||
return detectionResponse;
|
return detectionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user