1、新增图片与视频活体检测

2、新增人脸属性识别(性别、年龄、口罩、姿态、眼睛状态)
3、优化检测返回与包结构
4、新增 dependencyManagement 统一依赖版本管理
This commit is contained in:
dengwenjie
2025-05-09 20:10:04 +08:00
parent 42d2943a94
commit bef574cb91
63 changed files with 3266 additions and 382 deletions

View File

@@ -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
View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; // 默认返回未知
}
}

View File

@@ -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; // 默认返回未知
}
}

View File

@@ -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; // 默认返回未知
}
}

View File

@@ -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();
}
}
}
} }

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.face; package cn.smartjavaai.face.config;
import lombok.Data; import lombok.Data;

View File

@@ -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;
/** /**
* 模型路径 * 模型路径

View File

@@ -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;
}
}

View File

@@ -1,9 +1,10 @@
package cn.smartjavaai.face; package cn.smartjavaai.face.constant;
/** /**
* 人脸检测常量
* @author dwj * @author dwj
*/ */
public class FaceConfig { public class FaceDetectConstant {
/** /**
* 置信度阈值 * 置信度阈值

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
/** /**
* 人脸模型枚举 * 人脸模型枚举

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
} }

View File

@@ -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());
}
}

View File

@@ -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("默认不支持该功能");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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点仿射变换

View File

@@ -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)

View File

@@ -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("未识别到人脸");
} }
//绘制人脸框 //绘制人脸框

View File

@@ -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)

View File

@@ -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("默认不支持该功能");
}
}

View File

@@ -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;
}
}

View File

@@ -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);
} }

View File

@@ -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
*/ */

View File

@@ -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);
}
}
} }

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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("默认不支持该功能");
}
}

View File

@@ -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("默认不支持该功能");
}
} }

View File

@@ -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;
/** /**
* 设备类型 * 设备类型

View File

@@ -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;
} }

View File

@@ -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());
} }

View File

@@ -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(){
}
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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())

View File

@@ -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;

View File

@@ -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("默认不支持该功能");
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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());
// 右下角点 // 右下角点

View File

@@ -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;
} }