From bef574cb916a8c2b76965ef152c08c70587e5363 Mon Sep 17 00:00:00 2001 From: dengwenjie <775747758@qq.com> Date: Fri, 9 May 2025 20:10:04 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8E=E8=A7=86=E9=A2=91=E6=B4=BB=E4=BD=93=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=202=E3=80=81=E6=96=B0=E5=A2=9E=E4=BA=BA=E8=84=B8=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=AF=86=E5=88=AB=EF=BC=88=E6=80=A7=E5=88=AB=E3=80=81?= =?UTF-8?q?=E5=B9=B4=E9=BE=84=E3=80=81=E5=8F=A3=E7=BD=A9=E3=80=81=E5=A7=BF?= =?UTF-8?q?=E6=80=81=E3=80=81=E7=9C=BC=E7=9D=9B=E7=8A=B6=E6=80=81=EF=BC=89?= =?UTF-8?q?=203=E3=80=81=E4=BC=98=E5=8C=96=E6=A3=80=E6=B5=8B=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E4=B8=8E=E5=8C=85=E7=BB=93=E6=9E=84=204=E3=80=81?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20dependencyManagement=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 66 ++- pom.xml | 43 +- smartjavaai-all/pom.xml | 55 +- smartjavaai-bom/pom.xml | 142 +++++ smartjavaai-common/pom.xml | 2 +- .../common/entity/DetectionInfo.java | 53 ++ .../common/entity/DetectionRectangle.java | 80 +-- .../common/entity/DetectionResponse.java | 12 +- .../common/entity/FaceAttribute.java | 50 ++ .../smartjavaai/common/entity/FaceInfo.java | 48 ++ .../smartjavaai/common/entity/HeadPose.java | 32 + .../common/entity/ObjectDetInfo.java | 22 + .../smartjavaai/common/enums/EyeStatus.java | 42 ++ .../smartjavaai/common/enums/GenderType.java | 40 ++ .../common/enums/LivenessStatus.java | 40 ++ .../smartjavaai/common/utils/ImageUtils.java | 35 -- .../smartjavaai/common/utils/PoolUtils.java | 25 + smartjavaai-face/pom.xml | 13 +- .../face/config/FaceAttributeConfig.java | 77 +++ .../face/{ => config}/FaceExtractConfig.java | 2 +- .../face/{ => config}/FaceModelConfig.java | 10 +- .../face/config/LivenessConfig.java | 67 +++ .../FaceDetectConstant.java} | 5 +- .../face/constant/LivenessConstant.java | 27 + .../face/context/PredictorContext.java | 17 + .../face/enums/FaceAttributeModelEnum.java | 36 ++ .../face/{ => enums}/FaceModelEnum.java | 7 +- .../face/enums/LivenessModelEnum.java | 36 ++ .../factory/FaceAttributeModelFactory.java | 97 ++++ .../face/{ => factory}/FaceModelFactory.java | 20 +- .../face/factory/LivenessModelFactory.java | 101 ++++ .../model/attribute/FaceAttributeModel.java | 180 ++++++ .../Seetaface6FaceAttributeModel.java | 473 +++++++++++++++ .../facerec}/AbstractFaceModel.java | 4 +- .../face/{ => model/facerec}/FaceModel.java | 4 +- .../{ => facerec}/FeatureExtractionModel.java | 27 +- .../model/{ => facerec}/RetinaFaceModel.java | 9 +- .../model/{ => facerec}/SeetaFace6Model.java | 13 +- .../UltraLightFastGenericFaceModel.java | 9 +- .../face/model/liveness/LivenessDetModel.java | 206 +++++++ .../liveness/Seetaface6LivenessModel.java | 547 ++++++++++++++++++ .../face/seetaface/NativeLoader.java | 63 +- .../translator/FaceFeatureTranslator.java | 1 + .../cn/smartjavaai/face/utils/FaceUtils.java | 315 +++++++++- smartjavaai-objectdetection/pom.xml | 4 +- .../objectdetection/utils/DetectorUtils.java | 13 +- smartjavaai-ocr/pom.xml | 4 +- .../cn/smartjavaai/ocr/AbstractOcrModel.java | 24 - .../OcrDetModel.java} | 20 +- .../OcrDetModelConfig.java} | 6 +- .../OcrDetModelEnum.java} | 8 +- .../OcrDetModelFactory.java} | 30 +- .../ocr/model/PaddleOCRV4Model.java | 27 - .../model/PaddleOCRV4DetModel.java} | 24 +- .../ocr/ppv4/model/PaddleOCRV4RecModel.java | 129 +++++ .../PaddleOCRV4DetectTranslator.java} | 6 +- .../PaddleOCRV4WordRecTranslator.java | 2 +- .../ocr/recognition/OcrRecModel.java | 37 ++ .../ocr/recognition/OcrRecModelConfig.java | 28 + .../ocr/recognition/OcrRecModelEnum.java | 27 + .../ocr/recognition/OcrRecModelFactory.java | 95 +++ .../cn/smartjavaai/ocr/utils/ImageUtils.java | 4 +- .../cn/smartjavaai/ocr/utils/OcrUtils.java | 7 +- 63 files changed, 3266 insertions(+), 382 deletions(-) create mode 100644 smartjavaai-bom/pom.xml create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceAttribute.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/HeadPose.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/ObjectDetInfo.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/EyeStatus.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/GenderType.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/LivenessStatus.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/PoolUtils.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceAttributeConfig.java rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => config}/FaceExtractConfig.java (94%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => config}/FaceModelConfig.java (76%) create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/config/LivenessConfig.java rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{FaceConfig.java => constant/FaceDetectConstant.java} (84%) create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/LivenessConstant.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/context/PredictorContext.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceAttributeModelEnum.java rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => enums}/FaceModelEnum.java (79%) create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/LivenessModelEnum.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => factory}/FaceModelFactory.java (85%) create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/FaceAttributeModel.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => model/facerec}/AbstractFaceModel.java (97%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/{ => model/facerec}/FaceModel.java (97%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/model/{ => facerec}/FeatureExtractionModel.java (93%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/model/{ => facerec}/RetinaFaceModel.java (97%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/model/{ => facerec}/SeetaFace6Model.java (98%) rename smartjavaai-face/src/main/java/cn/smartjavaai/face/model/{ => facerec}/UltraLightFastGenericFaceModel.java (97%) create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/LivenessDetModel.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/AbstractOcrModel.java rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{OcrModel.java => detection/OcrDetModel.java} (50%) rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{OcrModelConfig.java => detection/OcrDetModelConfig.java} (72%) rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{OcrModelEnum.java => detection/OcrDetModelEnum.java} (72%) rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{OcrModelFactory.java => detection/OcrDetModelFactory.java} (67%) delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/PaddleOCRV4Model.java rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{model/PaddleOCRV4DetectModel.java => ppv4/model/PaddleOCRV4DetModel.java} (86%) create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{translator/PaddleOCRV4DetectionTranslator.java => ppv4/translator/PaddleOCRV4DetectTranslator.java} (98%) rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{ => ppv4}/translator/PaddleOCRV4WordRecTranslator.java (98%) create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java diff --git a/README.md b/README.md index 176dde6..b030d5e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ star + + github star + + + gitcode star +

------------------------------------------------------------------------------- @@ -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 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 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 clazz) { + private static void registerModel(String name, Class 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 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; }