diff --git a/README.md b/README.md
index 176dde6..b030d5e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,12 @@
+
+
+
+
+
+
-------------------------------------------------------------------------------
@@ -86,7 +92,37 @@
@@ -127,10 +163,14 @@
### ✅ 已实现功能
-- **人脸检测**
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
+- **人脸识别**
+ - 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
+ - 5点人脸关键点定位
+ - 人脸属性检测(性别、年龄、口罩、眼睛状态、脸部姿态)
+ - 人脸活体检测:图片、视频活体检测
- **目标检测**
- 支持通用目标检测,能够识别图像中的多种物体类别,返回物体位置与类别信息
+ - 支持YOLO、resnet50、VGG16等目标检测模型
+ - 支持通用目标检测,能够识别图像中的多种物体类别,返回物体位置与类别信息
### ⌛ 规划中功能
@@ -182,13 +222,16 @@
cn.smartjavaai
smartjavaai-all
- 1.0.11
+ 1.0.12
```
### 3、完整示例代码
[示例代码](https://gitee.com/dengwenjie/SmartJavaAI-Demo)
+### 4、文档地址
+
+[开发文档](http://doc.smartjavaai.cn)
## 🙏 鸣谢
@@ -208,19 +251,18 @@
- **微信**: deng775747758 (请备注:SmartJavaAI)
- **Email**: 775747758@qq.com
-#### 加微信群
-
-
-
-
🚀 **如果这个项目对你有帮助,别忘了点个 Star ⭐!你的支持是我持续优化升级的动力!** ❤️
-
-
## 更新日志
+## [v1.0.12] - 2025-05-09
+- 新增图片与视频活体检测
+- 新增人脸属性识别(性别、年龄、口罩、姿态、眼睛状态)
+- 优化检测返回与包结构
+- 新增 dependencyManagement 统一依赖版本管理
+
## [v1.0.11] - 2025-04-28
- FaceNet 特征提取新增人脸对齐
- 人脸检测新5点人脸关键点定位
diff --git a/pom.xml b/pom.xml
index 3bfd1e4..88955f5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
pom
SmartJavaAI
@@ -15,6 +15,7 @@
smartjavaai-objectdetection
smartjavaai-all
smartjavaai-ocr
+ smartjavaai-bom
@@ -38,13 +39,13 @@
cn.smartjavaai
smartjavaai-common
- 1.0.11
+ 1.0.12
cn.smartjavaai
smartjavaai-face
- 1.0.11
+ 1.0.12
@@ -63,18 +64,18 @@
ai.djl
model-zoo
-
- ai.djl.timeseries
- timeseries
-
-
- ai.djl.huggingface
- tokenizers
-
-
- ai.djl.audio
- audio
-
+
+
+
+
+
+
+
+
+
+
+
+
ai.djl.mxnet
@@ -90,11 +91,7 @@
ai.djl.tensorflow
tensorflow-model-zoo
-
-
- ai.djl.onnxruntime
- onnxruntime-engine
-
+
org.testng
testng
@@ -210,6 +207,12 @@
1.0.3
+
+ org.bytedeco
+ javacv
+ 1.5.10
+
+
diff --git a/smartjavaai-all/pom.xml b/smartjavaai-all/pom.xml
index 80d86b8..53e144a 100644
--- a/smartjavaai-all/pom.xml
+++ b/smartjavaai-all/pom.xml
@@ -6,12 +6,12 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
smartjavaai-all
- 1.0.11
- smartjavaai-all
+ 1.0.12
+ ${project.artifactId}
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
@@ -21,6 +21,15 @@
+
+
+ 11
+ 11
+ UTF-8
+ true
+
+
+
cn.smartjavaai
@@ -33,14 +42,42 @@
smartjavaai-objectdetection
${project.version}
+
+
+
+
+
+
+
+
+
+
+ org.bytedeco
+ javacpp-platform
+ 1.5.10
+
+
+ org.bytedeco
+ ffmpeg-platform
+ 6.1.1-1.5.10
+
+
+
+ org.bytedeco
+ openblas-platform
+ 0.3.26-1.5.10
+
+
+
+ org.bytedeco
+ opencv-platform
+ 4.9.0-1.5.10
+
+
+
-
- 11
- 11
- UTF-8
- true
-
+
diff --git a/smartjavaai-bom/pom.xml b/smartjavaai-bom/pom.xml
new file mode 100644
index 0000000..ad810da
--- /dev/null
+++ b/smartjavaai-bom/pom.xml
@@ -0,0 +1,142 @@
+
+
+ 4.0.0
+
+ cn.smartjavaai
+ smartjavaai-parent
+ 1.0.12
+
+
+ 1.0.12
+ smartjavaai-bom
+ smartjavaai-bom
+ 统一版本管理的 BOM 包,同时支持 import 和全量依赖
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+
+ cn.smartjavaai
+ smartjavaai-face
+ ${project.parent.version}
+
+
+ cn.smartjavaai
+ smartjavaai-objectdetection
+ ${project.parent.version}
+
+
+
+
+
+ https://github.com/geekwenjie/SmartJavaAI
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.4.0
+ true
+
+ dengwenjie
+ true
+ ${project.groupId}:${project.artifactId}:${project.version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.1.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.1.0
+
+ ${java.home}/bin/javadoc
+ none
+
+ -Xdoclint:none
+
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+ scm:git:git://github.com/geekwenjie/SmartJavaAI.git
+ scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git
+ http://github.com/geekwenjie/SmartJavaAI/tree/master
+
+
+
+
+
+ dengwenjie
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+ dengwenjie
+ https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ dengwenjie
+ 775747758@qq.com
+
+ Project Manager
+ Architect
+
+
+
+
+
diff --git a/smartjavaai-common/pom.xml b/smartjavaai-common/pom.xml
index 0307960..572d218 100644
--- a/smartjavaai-common/pom.xml
+++ b/smartjavaai-common/pom.xml
@@ -6,7 +6,7 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
smartjavaai-common
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java
new file mode 100644
index 0000000..17e2d46
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java
@@ -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;
+ }
+
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionRectangle.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionRectangle.java
index 4842691..4b53aee 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionRectangle.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionRectangle.java
@@ -1,98 +1,30 @@
package cn.smartjavaai.common.entity;
+import cn.smartjavaai.common.enums.GenderType;
+import cn.smartjavaai.common.enums.LivenessStatus;
+import lombok.Data;
+
import java.util.List;
/**
* 检测结果-矩形区域
* @author dwj
*/
+@Data
public class DetectionRectangle {
public int x;
public int y;
public int width;
public int height;
- public float score;
- public String className;
-
- /**
- * 人脸关键点
- */
- private List keyPoints;
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.y = y;
this.width = width;
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 getKeyPoints() {
- return keyPoints;
- }
-
- public void setKeyPoints(List keyPoints) {
- this.keyPoints = keyPoints;
}
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
index 7ee6f48..7837fa6 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
@@ -1,5 +1,7 @@
package cn.smartjavaai.common.entity;
+import lombok.Data;
+
import java.util.List;
/**
@@ -7,15 +9,15 @@ import java.util.List;
* @author dwj
* @date 2025/4/12
*/
+@Data
public class DetectionResponse {
- private List rectangleList;
+ private List detectionInfoList;
- public List getRectangleList() {
- return rectangleList;
+ public DetectionResponse() {
}
- public void setRectangleList(List rectangleList) {
- this.rectangleList = rectangleList;
+ public DetectionResponse(List detectionInfoList) {
+ this.detectionInfoList = detectionInfoList;
}
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceAttribute.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceAttribute.java
new file mode 100644
index 0000000..d27a833
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceAttribute.java
@@ -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() {
+ }
+
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java
new file mode 100644
index 0000000..70dc7bf
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java
@@ -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 keyPoints;
+
+ /**
+ * 人脸属性
+ */
+ private FaceAttribute faceAttribute;
+
+ /**
+ * 活体检测结果
+ */
+ private LivenessStatus livenessStatus;
+
+ public FaceInfo() {
+ }
+
+ public FaceInfo(List keyPoints) {
+ this.keyPoints = keyPoints;
+ }
+
+ public FaceInfo(List 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;
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/HeadPose.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/HeadPose.java
new file mode 100644
index 0000000..e873c06
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/HeadPose.java
@@ -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;
+ }
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/ObjectDetInfo.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/ObjectDetInfo.java
new file mode 100644
index 0000000..3a5cec7
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/ObjectDetInfo.java
@@ -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;
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/EyeStatus.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/EyeStatus.java
new file mode 100644
index 0000000..d3f03b4
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/EyeStatus.java
@@ -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; // 默认返回未知
+ }
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/GenderType.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/GenderType.java
new file mode 100644
index 0000000..7265f20
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/GenderType.java
@@ -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; // 默认返回未知
+ }
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/LivenessStatus.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/LivenessStatus.java
new file mode 100644
index 0000000..ff2416e
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/LivenessStatus.java
@@ -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; // 默认返回未知
+ }
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
index 9e99255..162bac5 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
@@ -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();
- }
- }
- }
-
-
-
-
-
-
-
-
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/PoolUtils.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/PoolUtils.java
new file mode 100644
index 0000000..84316f4
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/PoolUtils.java
@@ -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 void returnToPool(ObjectPool pool, T predictor) {
+ if (pool == null || predictor == null) {
+ return;
+ }
+ try {
+ pool.returnObject(predictor);
+ } catch (Exception e) {
+ log.warn("归还Predictor到池失败", e);
+ }
+ }
+
+}
diff --git a/smartjavaai-face/pom.xml b/smartjavaai-face/pom.xml
index 8a9054e..f3f3008 100644
--- a/smartjavaai-face/pom.xml
+++ b/smartjavaai-face/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
smartjavaai-face
- 1.0.11
+ 1.0.12
smartjavaai-face
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
@@ -26,6 +26,8 @@
11
UTF-8
true
+ 1.5.8
+ 5.1.2-1.5.8
@@ -48,6 +50,7 @@
3.36.0.3
+
@@ -117,9 +120,6 @@
-
-
-
scm:git:git://github.com/geekwenjie/SmartJavaAI.git
@@ -151,7 +151,4 @@
-
-
-
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceAttributeConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceAttributeConfig.java
new file mode 100644
index 0000000..a7cc598
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceAttributeConfig.java
@@ -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;
+ }
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceExtractConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
similarity index 94%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceExtractConfig.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
index d5f5d4d..150a99a 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceExtractConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.config;
import lombok.Data;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
similarity index 76%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
index 2186cc9..9e16364 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
@@ -1,10 +1,12 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.config;
import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.face.constant.FaceDetectConstant;
+import cn.smartjavaai.face.enums.FaceModelEnum;
import lombok.Data;
/**
- * 模型配置
+ * 人脸检测识别模型配置
* @author dwj
*/
@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;
/**
* 模型路径
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/LivenessConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/LivenessConfig.java
new file mode 100644
index 0000000..6115fc3
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/LivenessConfig.java
@@ -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;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
similarity index 84%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
index 231f6d5..202bc2f 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
@@ -1,9 +1,10 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.constant;
/**
+ * 人脸检测常量
* @author dwj
*/
-public class FaceConfig {
+public class FaceDetectConstant {
/**
* 置信度阈值
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/LivenessConstant.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/LivenessConstant.java
new file mode 100644
index 0000000..4f8969f
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/LivenessConstant.java
@@ -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;
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/context/PredictorContext.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/context/PredictorContext.java
new file mode 100644
index 0000000..222556f
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/context/PredictorContext.java
@@ -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;
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceAttributeModelEnum.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceAttributeModelEnum.java
new file mode 100644
index 0000000..98d92a7
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceAttributeModelEnum.java
@@ -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);
+ }
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
similarity index 79%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
index 3dc23af..629a722 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
@@ -1,9 +1,4 @@
-package cn.smartjavaai.face;
-
-import cn.smartjavaai.face.model.FeatureExtractionModel;
-import cn.smartjavaai.face.model.RetinaFaceModel;
-import cn.smartjavaai.face.model.SeetaFace6Model;
-import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
+package cn.smartjavaai.face.enums;
/**
* 人脸模型枚举
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/LivenessModelEnum.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/LivenessModelEnum.java
new file mode 100644
index 0000000..0de2af3
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/LivenessModelEnum.java
@@ -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);
+ }
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
new file mode 100644
index 0000000..573558d
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
@@ -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 modelMap = new ConcurrentHashMap<>();
+
+ /**
+ * 算法注册表
+ */
+ private static final Map> 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());
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
similarity index 85%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelFactory.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
index 81d64bf..176de1b 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
@@ -1,11 +1,11 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.factory;
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.model.FeatureExtractionModel;
-import cn.smartjavaai.face.model.RetinaFaceModel;
-import cn.smartjavaai.face.model.SeetaFace6Model;
-import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
+import cn.smartjavaai.face.model.facerec.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@@ -13,7 +13,7 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
- * 人脸算法工厂
+ * 人脸检测识别模型工厂
* @author dwj
*/
@Slf4j
@@ -76,8 +76,8 @@ public class FaceModelFactory {
// 初始化默认配置
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.RETINA_FACE);
- config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
- config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
+ config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
+ config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
return getModel(config);
}
@@ -110,8 +110,8 @@ public class FaceModelFactory {
// 初始化默认配置
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
- config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
- config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
+ config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
+ config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
return getModel(config);
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
new file mode 100644
index 0000000..972854f
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
@@ -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 modelMap = new ConcurrentHashMap<>();
+
+ /**
+ * 算法注册表
+ */
+ private static final Map> 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());
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/FaceAttributeModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/FaceAttributeModel.java
new file mode 100644
index 0000000..b3778a3
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/FaceAttributeModel.java
@@ -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 detect(String imagePath, DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 人脸属性识别(单人脸)
+ * @param imagePath 图片路径
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default FaceAttribute detect(String imagePath, DetectionRectangle faceDetectionRectangle, List keyPoints){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 人脸属性识别(多人脸)
+ * @param imageData 图片数据
+ * @param faceDetectionResponse 人脸检测结果
+ * @return
+ */
+ default List detect(byte[] imageData,DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 人脸属性识别(单人脸)
+ * @param imageData
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default FaceAttribute detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List keyPoints){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 人脸属性识别(多人脸)
+ * @param image BufferedImage
+ * @param faceDetectionResponse 人脸检测结果
+ * @return
+ */
+ default List detect(BufferedImage image,DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 人脸属性识别(单人脸)
+ * @param image BufferedImage
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default FaceAttribute detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List 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("默认不支持该功能");
+ }
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java
new file mode 100644
index 0000000..ad53426
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java
@@ -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 seetaPointFSList = new ArrayList();
+ List faceAttributeList = new ArrayList();
+ 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 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 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 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 faceAttributeList = new ArrayList();
+ 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 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 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 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);
+ }
+ }
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
similarity index 97%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
index c7a7bd6..36f87bc 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
@@ -1,6 +1,8 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.model.facerec;
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 java.awt.image.BufferedImage;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
similarity index 97%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
index 7fcad0a..92e56f0 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
@@ -1,6 +1,8 @@
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.model.facerec;
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 java.awt.image.BufferedImage;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java
similarity index 93%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java
index 35ed281..c90dc72 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.face.model;
+package cn.smartjavaai.face.model.facerec;
import ai.djl.Device;
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.ZooModel;
import ai.djl.training.util.ProgressBar;
+import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
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.factory.FaceModelFactory;
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
import cn.smartjavaai.face.utils.FaceAlignUtils;
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.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
-import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.opencv.core.Mat;
+import org.opencv.face.Face;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.file.Paths;
-import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -223,19 +225,21 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
}
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
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("未检测到人脸");
}
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
NDManager manager = NDManager.newBaseManager();
- for (DetectionRectangle rectangle : detectedResult.getRectangleList()){
+ for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
+ DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
+
float[] features = null;
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.isAlign()){
//获取子图中人脸关键点坐标
- double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
+ double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
@@ -314,18 +318,19 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
if(config.isCropFace()){
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
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("未检测到人脸");
}
//只取第一个人脸
- 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());
//人脸对齐
if(config.isAlign()){
NDManager manager = NDManager.newBaseManager();
//获取子图中人脸关键点坐标
- double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
+ double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
similarity index 97%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
index 7afd83e..9f56317 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.face.model;
+package cn.smartjavaai.face.model.facerec;
import ai.djl.Device;
import ai.djl.MalformedModelException;
@@ -15,7 +15,8 @@ import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
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.translator.FaceDetectionTranslator;
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.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
-import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import javax.imageio.ImageIO;
@@ -33,7 +33,6 @@ import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.time.Duration;
import java.util.Objects;
/**
@@ -73,7 +72,7 @@ public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
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 criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
similarity index 98%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
index 226ebfa..fabb2f7 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
@@ -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.enums.DeviceEnum;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
-import cn.smartjavaai.face.AbstractFaceModel;
-import cn.smartjavaai.face.FaceExtractConfig;
-import cn.smartjavaai.face.FaceModelConfig;
+import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.dao.FaceDao;
import cn.smartjavaai.face.entity.FaceData;
import cn.smartjavaai.face.entity.FaceResult;
import cn.smartjavaai.face.exception.FaceException;
-import cn.smartjavaai.face.utils.FaceAlignUtils;
import cn.smartjavaai.face.utils.FaceUtils;
import com.seeta.pool.*;
import com.seeta.sdk.*;
@@ -63,7 +60,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
}
//加载依赖库
- NativeLoader.loadNativeLibraries(config);
+ NativeLoader.loadNativeLibraries(config.getDevice());
log.info("Loading seetaFace6 library successfully.");
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
@@ -209,7 +206,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
throw new FaceException("无效图片路径", e);
}
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("未识别到人脸");
}
//绘制人脸框
@@ -225,7 +222,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
throw new FaceException("图像无效");
}
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("未识别到人脸");
}
//绘制人脸框
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
similarity index 97%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
index 52f9b9a..c523bc0 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.face.model;
+package cn.smartjavaai.face.model.facerec;
import ai.djl.Device;
import ai.djl.MalformedModelException;
@@ -15,7 +15,8 @@ import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
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.translator.FaceDetectionTranslator;
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.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
-import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Paths;
-import java.time.Duration;
import java.util.Objects;
/**
@@ -72,7 +71,7 @@ public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
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 criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/LivenessDetModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/LivenessDetModel.java
new file mode 100644
index 0000000..755cc8c
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/LivenessDetModel.java
@@ -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 detect(String imagePath, DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 活体检测(单人脸)
+ * @param imagePath 图片路径
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default LivenessStatus detect(String imagePath, DetectionRectangle faceDetectionRectangle, List keyPoints){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 活体检测(多人脸)
+ * @param imageData 图片数据
+ * @param faceDetectionResponse 人脸检测结果
+ * @return
+ */
+ default List detect(byte[] imageData,DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 活体检测(单人脸)
+ * @param imageData
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default LivenessStatus detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List keyPoints){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 活体检测(多人脸)
+ * @param image BufferedImage
+ * @param faceDetectionResponse 人脸检测结果
+ * @return
+ */
+ default List detect(BufferedImage image,DetectionResponse faceDetectionResponse){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 活体检测(单人脸)
+ * @param image BufferedImage
+ * @param faceDetectionRectangle 人脸检测结果-人脸框
+ * @return
+ */
+ default LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List 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 keyPoints){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 视频活体检测(逐帧检测)
+ * @param frameData
+ * @param faceDetectionRectangle
+ * @return
+ */
+ default LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List 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("默认不支持该功能");
+ }
+
+
+
+
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java
new file mode 100644
index 0000000..38cc609
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java
@@ -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 seetaPointFSList = new ArrayList();
+ List livenessStatusList = new ArrayList();
+ 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 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 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 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 livenessStatusList = new ArrayList();
+ 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 detect(BufferedImage image, DetectionResponse faceDetectionResponse) {
+ return detect(image, faceDetectionResponse, true);
+ }
+
+ private LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List 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 keyPoints) {
+ return detect(image, faceDetectionRectangle, keyPoints, true);
+ }
+
+
+
+ @Override
+ public List 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 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 keyPoints) {
+ if(!ImageUtils.isImageValid(frameImage)){
+ throw new FaceException("图像无效");
+ }
+ return detect(frameImage,faceDetectionRectangle, keyPoints,false);
+ }
+
+ @Override
+ public LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List 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;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
index e3b8640..be08045 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
@@ -7,7 +7,7 @@ import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import cn.smartjavaai.common.config.Config;
import cn.smartjavaai.common.enums.DeviceEnum;
-import cn.smartjavaai.face.FaceModelConfig;
+import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.exception.FaceException;
import com.seeta.sdk.util.DllItem;
import com.seeta.sdk.util.LoadNativeCore;
@@ -39,35 +39,48 @@ public class NativeLoader {
*/
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 {
- OsInfo osInfo = SystemUtil.getOsInfo();
- //检查当前系统是否支持
- if(!osInfo.isWindows() && !osInfo.isLinux()){
- throw new FaceException("当前系统不支持:" + osInfo.getName());
- }
- //判断硬件架构是否支持GPU
- if(config.getDevice() != null && config.getDevice().equals(DeviceEnum.GPU)){
- //GPU仅支持amd64
- if(!osInfo.getArch().contains("amd64") && !osInfo.getArch().contains("x86_64")){
- throw new FaceException("seetaface6 GPU模型不支持当前arch:" + osInfo.getArch());
+ if (!isDllLoaded) {
+ synchronized (NativeLoader.class) {
+ if (!isDllLoaded) { // 双重检查
+ OsInfo osInfo = SystemUtil.getOsInfo();
+ //检查当前系统是否支持
+ if(!osInfo.isWindows() && !osInfo.isLinux()){
+ throw new FaceException("当前系统不支持:" + osInfo.getName());
+ }
+ //判断硬件架构是否支持GPU
+ 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 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 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) {
throw new RuntimeException("Native library loading failed", e);
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
index 99a4628..ddb2222 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
@@ -12,6 +12,7 @@ import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
/**
+ * facenet人脸特征提取Translator
* @author dwj
* @date 2025/3/31
*/
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
index 3982512..39fcbd0 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
@@ -5,15 +5,15 @@ import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
-import cn.smartjavaai.common.entity.DetectionResponse;
-import cn.smartjavaai.common.entity.DetectionRectangle;
+import cn.smartjavaai.common.entity.*;
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.face.FaceModelConfig;
+import cn.smartjavaai.face.config.FaceModelConfig;
+import cn.smartjavaai.common.enums.LivenessStatus;
import cn.smartjavaai.face.exception.FaceException;
-import com.seeta.sdk.SeetaImageData;
-import com.seeta.sdk.SeetaPointF;
-import com.seeta.sdk.SeetaRect;
+import com.seeta.sdk.*;
import javax.imageio.ImageIO;
import java.awt.*;
@@ -45,7 +45,7 @@ public class FaceUtils {
}
DetectionResponse detectionResponse = new DetectionResponse();
List detectedObjectList = detection.items();
- List rectangleList = new ArrayList();
+ List detectionInfoList = new ArrayList();
Iterator iterator = detectedObjectList.iterator();
int index = 0;
while(iterator.hasNext()) {
@@ -64,12 +64,13 @@ public class FaceUtils {
if (y < 0) y = 0;
if (x + width > img.getWidth()) width = img.getWidth() - x;
if (y + height > img.getHeight()) height = img.getHeight() - y;
- DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue());
- rectangle.setKeyPoints(keyPoints);
- rectangleList.add(rectangle);
+ DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height);
+ FaceInfo faceInfo = new FaceInfo(keyPoints);
+ DetectionInfo detectionInfo = new DetectionInfo(rectangle, detection.getProbabilities().get(index).floatValue(),faceInfo);
+ detectionInfoList.add(detectionInfo);
index++;
}
- detectionResponse.setRectangleList(rectangleList);
+ detectionResponse.setDetectionInfoList(detectionInfoList);
return detectionResponse;
}
@@ -83,7 +84,7 @@ public class FaceUtils {
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
- List rectangleList = new ArrayList();
+ List detectionInfoList = new ArrayList();
for(int i = 0; i < seetaResult.length; i++){
SeetaRect rect = seetaResult[i];
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
@@ -91,14 +92,15 @@ public class FaceUtils {
/*if(config.getConfidenceThreshold() > 0){
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 keyPoints = Arrays.stream(seetaPointFS)
.map(p -> new Point(p.x, p.y))
.collect(Collectors.toList());
- rectangle.setKeyPoints(keyPoints);
- rectangleList.add(rectangle);
+ FaceInfo faceInfo = new FaceInfo(keyPoints);
+ DetectionInfo detectionInfo = new DetectionInfo(rectangle, 0, faceInfo);
+ detectionInfoList.add(detectionInfo);
}
- detectionResponse.setRectangleList(rectangleList);
+ detectionResponse.setDetectionInfoList(detectionInfoList);
return detectionResponse;
}
@@ -113,7 +115,7 @@ public class FaceUtils {
if(!ImageUtils.isImageValid(sourceImage)){
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("无目标数据");
}
Graphics2D graphics = sourceImage.createGraphics();
@@ -122,13 +124,15 @@ public class FaceUtils {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
int stroke = 2;
- for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
+ 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(rectangle.getKeyPoints() != null){
- drawLandmarks(graphics, rectangle.getKeyPoints());
+ if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
+ !detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
+ drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
}
}
graphics.dispose();
@@ -145,7 +149,7 @@ public class FaceUtils {
if(!ImageUtils.isImageValid(sourceImage)){
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("无目标数据");
}
Graphics2D graphics = sourceImage.createGraphics();
@@ -154,13 +158,15 @@ public class FaceUtils {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
int stroke = 2;
- for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
+ 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(rectangle.getKeyPoints() != null){
- drawLandmarks(graphics, rectangle.getKeyPoints());
+ if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
+ !detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
+ drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
}
}
graphics.dispose();
@@ -215,7 +221,7 @@ public class FaceUtils {
if (width <= 0 || height <= 0) {
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 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 seetaPointFSList, List faceAttributeList){
+ if(Objects.isNull(seetaResult) || seetaResult.length == 0){
+ return null;
+ }
+ List detectionInfoList = new ArrayList();
+ 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 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 seetaPointFSList, List livenessStatusList){
+ if(Objects.isNull(seetaResult) || seetaResult.length == 0){
+ return null;
+ }
+ DetectionResponse detectionResponse = new DetectionResponse();
+ List detectionInfoList = new ArrayList();
+ 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 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 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 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);
+ }
+ }
+
+
+
}
diff --git a/smartjavaai-objectdetection/pom.xml b/smartjavaai-objectdetection/pom.xml
index a88bf39..6d6e156 100644
--- a/smartjavaai-objectdetection/pom.xml
+++ b/smartjavaai-objectdetection/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
smartjavaai-objectdetection
- 1.0.11
+ 1.0.12
smartjavaai-objectdetection
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/utils/DetectorUtils.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/utils/DetectorUtils.java
index 0f06279..c41b220 100644
--- a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/utils/DetectorUtils.java
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/utils/DetectorUtils.java
@@ -3,8 +3,10 @@ package cn.smartjavaai.objectdetection.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
+import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.entity.ObjectDetInfo;
import cn.smartjavaai.common.utils.ImageUtils;
import javax.imageio.ImageIO;
@@ -37,7 +39,7 @@ public class DetectorUtils {
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
- List rectangleList = new ArrayList();
+ List detectionInfoList = new ArrayList();
List detectedObjectList = detection.items();
Iterator iterator = detectedObjectList.iterator();
int index = 0;
@@ -49,11 +51,14 @@ public class DetectorUtils {
int y = (int)(box.getBounds().getY() * (double)img.getHeight());
int width = (int)(box.getBounds().getWidth() * (double)img.getWidth());
int height = (int)(box.getBounds().getHeight() * (double)img.getHeight());
- DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue(),className);
- rectangleList.add(rectangle);
+ DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height);
+ DetectionInfo detectionInfo = new DetectionInfo(rectangle, detection.getProbabilities().get(index).floatValue());
+ ObjectDetInfo objectDetInfo = new ObjectDetInfo(className);
+ detectionInfo.setObjectDetInfo(objectDetInfo);
+ detectionInfoList.add(detectionInfo);
index++;
}
- detectionResponse.setRectangleList(rectangleList);
+ detectionResponse.setDetectionInfoList(detectionInfoList);
return detectionResponse;
}
diff --git a/smartjavaai-ocr/pom.xml b/smartjavaai-ocr/pom.xml
index 5f116f1..3087d92 100644
--- a/smartjavaai-ocr/pom.xml
+++ b/smartjavaai-ocr/pom.xml
@@ -6,7 +6,7 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.11
+ 1.0.12
smartjavaai-ocr
@@ -41,7 +41,7 @@
- 1.0.11
+ 1.0.12
smartjavaai-ocr
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/AbstractOcrModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/AbstractOcrModel.java
deleted file mode 100644
index 13d59c7..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/AbstractOcrModel.java
+++ /dev/null
@@ -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("默认不支持该功能");
- }
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
similarity index 50%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModel.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
index 026d808..5b98d22 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModel.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
@@ -1,35 +1,37 @@
-package cn.smartjavaai.ocr;
+package cn.smartjavaai.ocr.detection;
import cn.smartjavaai.common.entity.DetectionResponse;
-import java.awt.image.BufferedImage;
-import java.io.InputStream;
-
/**
* 人脸识别算法
* @author dwj
*/
-public interface OcrModel {
+public interface OcrDetModel {
/**
* 加载模型
* @param config
*/
- void loadModel(OcrModelConfig config); // 加载模型
-
+ void loadModel(OcrDetModelConfig config); // 加载模型
/**
* 人脸检测
* @param imagePath 图片路径
* @return
*/
- DetectionResponse detect(String imagePath);
+ default DetectionResponse detect(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
* 检测并绘制结果
* @param imagePath 图片输入路径(包含文件名称)
* @param outputPath 图片输出路径(包含文件名称)
*/
- void detectAndDraw(String imagePath, String outputPath);
+ default void detectAndDraw(String imagePath, String outputPath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java
similarity index 72%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelConfig.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java
index 78150ea..42889d2 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelConfig.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.ocr;
+package cn.smartjavaai.ocr.detection;
import cn.smartjavaai.common.enums.DeviceEnum;
import lombok.Data;
@@ -8,12 +8,12 @@ import lombok.Data;
* @date 2025/4/22
*/
@Data
-public class OcrModelConfig {
+public class OcrDetModelConfig {
/**
* 模型名称
*/
- private OcrModelEnum modelEnum;
+ private OcrDetModelEnum modelEnum;
/**
* 设备类型
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java
similarity index 72%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelEnum.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java
index d81815c..ee35871 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelEnum.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java
@@ -1,11 +1,11 @@
-package cn.smartjavaai.ocr;
+package cn.smartjavaai.ocr.detection;
/**
* OCR模型枚举
* @author dwj
* @date 2025/4/4
*/
-public enum OcrModelEnum {
+public enum OcrDetModelEnum {
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("[-_]", "");
- for (OcrModelEnum model : values()) {
+ for (OcrDetModelEnum model : values()) {
if (model.name().replaceAll("_", "").equals(formatted)) {
return model;
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
similarity index 67%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelFactory.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
index d718816..a8413bd 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/OcrModelFactory.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
@@ -1,8 +1,8 @@
-package cn.smartjavaai.ocr;
+package cn.smartjavaai.ocr.detection;
import cn.smartjavaai.common.config.Config;
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 java.util.Map;
@@ -14,25 +14,25 @@ import java.util.concurrent.ConcurrentHashMap;
* @author dwj
*/
@Slf4j
-public class OcrModelFactory {
+public class OcrDetModelFactory {
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
- private static volatile OcrModelFactory instance;
+ private static volatile OcrDetModelFactory instance;
- private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
/**
* 算法注册表
*/
- private static final Map> registry =
+ private static final Map> registry =
new ConcurrentHashMap<>();
- public static OcrModelFactory getInstance() {
+ public static OcrDetModelFactory getInstance() {
if (instance == null) {
- synchronized (OcrModelFactory.class) {
+ synchronized (OcrDetModelFactory.class) {
if (instance == null) {
- instance = new OcrModelFactory();
+ instance = new OcrDetModelFactory();
}
}
}
@@ -46,7 +46,7 @@ public class OcrModelFactory {
* @param name
* @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);
}
@@ -56,7 +56,7 @@ public class OcrModelFactory {
* @param config
* @return
*/
- public OcrModel getModel(OcrModelConfig config) {
+ public OcrDetModel getModel(OcrDetModelConfig config) {
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
throw new OcrException("未配置OCR模型");
}
@@ -70,14 +70,14 @@ public class OcrModelFactory {
* @param config
* @return
*/
- private OcrModel createFaceModel(OcrModelConfig config) {
+ private OcrDetModel createFaceModel(OcrDetModelConfig config) {
Class> clazz = registry.get(config.getModelEnum().name().toLowerCase());
if(clazz == null){
throw new OcrException("Unsupported model");
}
- OcrModel algorithm = null;
+ OcrDetModel algorithm = null;
try {
- algorithm = (OcrModel) clazz.newInstance();
+ algorithm = (OcrDetModel) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new OcrException(e);
}
@@ -88,7 +88,7 @@ public class OcrModelFactory {
// 初始化默认算法
static {
- registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetectModel.class);
+ registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetModel.class);
log.info("缓存目录:{}", Config.getCachePath());
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4Model.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4Model.java
deleted file mode 100644
index 4d43c13..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4Model.java
+++ /dev/null
@@ -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(){
-
- }
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4DetectModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
similarity index 86%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4DetectModel.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
index e796d77..46e4800 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4DetectModel.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
@@ -1,25 +1,22 @@
-package cn.smartjavaai.ocr.model;
+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.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDList;
-import ai.djl.ndarray.NDManager;
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 ai.djl.translate.TranslateException;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
-import cn.smartjavaai.ocr.AbstractOcrModel;
-import cn.smartjavaai.ocr.OcrModelConfig;
+import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
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.OcrUtils;
import lombok.extern.slf4j.Slf4j;
@@ -40,14 +37,14 @@ import java.util.concurrent.ConcurrentHashMap;
* @date 2025/4/21
*/
@Slf4j
-public class PaddleOCRV4DetectModel extends AbstractOcrModel {
+public class PaddleOCRV4DetModel implements OcrDetModel {
private ZooModel detectionModel;
private ObjectPool> predictorPool;
@Override
- public void loadModel(OcrModelConfig config){
+ public void loadModel(OcrDetModelConfig config){
if(StringUtils.isBlank(config.getModelPath())){
throw new OcrException("modelPath is null");
}
@@ -56,7 +53,7 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
.optEngine("OnnxRuntime")
.setTypes(Image.class, NDList.class)
.optModelPath(Paths.get(config.getModelPath()))
- .optTranslator(new PaddleOCRV4DetectionTranslator(new ConcurrentHashMap()))
+ .optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap()))
.optProgress(new ProgressBar())
.build();
try{
@@ -83,11 +80,6 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
return detect(img);
}
- /**
- * 人脸检测
- * @param image
- * @return
- */
private DetectionResponse detect(Image image){
Predictor predictor = null;
try {
@@ -120,7 +112,7 @@ public class PaddleOCRV4DetectModel extends AbstractOcrModel {
try {
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
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("未识别到文字");
}
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java
new file mode 100644
index 0000000..cdd3678
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java
@@ -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> predictorPool;
+
+ @Override
+ public void loadModel(OcrRecModelConfig config){
+ if(StringUtils.isBlank(config.getModelPath())){
+ throw new OcrException("modelPath is null");
+ }
+ Criteria criteria =
+ Criteria.builder()
+ .optEngine("OnnxRuntime")
+ .setTypes(Image.class, NDList.class)
+ .optModelPath(Paths.get(config.getModelPath()))
+ .optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap()))
+ .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 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);
+ }
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4DetectionTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java
similarity index 98%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4DetectionTranslator.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java
index 1de5242..4396d1a 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4DetectionTranslator.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java
@@ -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.util.NDImageUtils;
@@ -27,7 +27,7 @@ import java.util.Map;
* @mail 179209347@qq.com
* @website www.aias.top
*/
-public class PaddleOCRV4DetectionTranslator implements Translator {
+public class PaddleOCRV4DetectTranslator implements Translator {
// det_algorithm == "DB"
private final float thresh = 0.3f;
private final boolean use_dilation = false;
@@ -44,7 +44,7 @@ public class PaddleOCRV4DetectionTranslator implements Translator
private int img_height;
private int img_width;
- public PaddleOCRV4DetectionTranslator(Map arguments) {
+ public PaddleOCRV4DetectTranslator(Map arguments) {
limit_side_len =
arguments.containsKey("limit_side_len")
? Integer.parseInt(arguments.get("limit_side_len").toString())
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4WordRecTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java
similarity index 98%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4WordRecTranslator.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java
index 7642e7c..4cc0a0e 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/translator/PaddleOCRV4WordRecTranslator.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.ocr.translator;
+package cn.smartjavaai.ocr.ppv4.translator;
import ai.djl.Model;
import ai.djl.modality.cv.Image;
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java
new file mode 100644
index 0000000..09da861
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java
@@ -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("默认不支持该功能");
+ }
+
+
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java
new file mode 100644
index 0000000..1a1616f
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java
@@ -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;
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java
new file mode 100644
index 0000000..007c8f6
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java
@@ -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);
+ }
+
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java
new file mode 100644
index 0000000..4083693
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java
@@ -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 modelMap = new ConcurrentHashMap<>();
+
+ /**
+ * 算法注册表
+ */
+ private static final Map> 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());
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
index a05db62..2bc9f86 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
@@ -4,6 +4,7 @@ import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
+import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.utils.OpenCVUtils;
@@ -207,7 +208,8 @@ public class ImageUtils {
*/
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());
// 右下角点
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
index a9c57cf..8fe4ca0 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
@@ -5,6 +5,7 @@ import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
+import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import lombok.extern.slf4j.Slf4j;
@@ -34,7 +35,7 @@ public class OcrUtils {
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
- List rectangleList = new ArrayList();
+ List detectionInfoList = new ArrayList();
for(NDArray box : dt_boxes){
DetectionRectangle rectangle = new DetectionRectangle();
float[] points = box.toFloatArray();
@@ -54,9 +55,9 @@ public class OcrUtils {
rectangle.setY(y);
rectangle.setHeight(height);
rectangle.setWidth(width);
- rectangleList.add(rectangle);
+ detectionInfoList.add(new DetectionInfo(rectangle));
}
- detectionResponse.setRectangleList(rectangleList);
+ detectionResponse.setDetectionInfoList(detectionInfoList);
return detectionResponse;
}
|