diff --git a/README.md b/README.md
index 5da7e3f..ec8f975 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,36 @@
-# SmartJavaAI:JAVA深度学习算法工具包
+
+
+
+
+ 🍬JAVA轻量级深度学习算法库,该库致力于构建Java生态与AI模型之间的高效桥梁
+
+## 📚简介
+
+`SmartJavaAI`是专为Java 开发者打造的一个功能丰富、开箱即用的 Java 算法工具包,致力于帮助Java开发者高效集成各类智能算法。SmartJavaAI通过对多种主流算法的统一封装,开发者无需深入了解底层实现,即可轻松在 Java 代码中调用人脸识别、目标检测、OCR 等功能。目前已支持部分人脸识别与目标检测算法,底层实现涵盖了 C++、Python 等语言的深度学习模型。后续将持续扩展更多算法,最终将构建一个面向 Java 开发者的通用智能工具库。
-
+## 🚀 核心亮点
+针对 Java 开发者在集成智能算法时常见的两大痛点:
+- 🐍 主流AI深度学习框架(PyTorch/TensorFlow)的Python生态与Java工程体系割裂
-**SmartJavaAI**是基于 **DJL(Deep Java Library)** 封装的轻量级深度学习算法库,依托DJL的自动模型管理和跨框架特性,**无需安装Python环境**且无需手动下载模型文件(模型由DJL内部自动从云端加载),该库致力于构建Java生态与AI模型之间的高效桥梁。针对Java开发者面临的两大痛点:
-
-- 🐍 主流AI框架(PyTorch/TensorFlow)的Python生态与Java工程体系割裂
-
-- ⚙️ 直接使用DJL需处理模型加载、预处理、后处理等复杂技术细节
+- ⚙️ 现有算法方案分散杂乱,封装不统一,使用门槛高,不易直接服务于 Java 业务开发
我们实现了:
-✅ **开箱即用** - 两行代码完成人脸检测/识别
-✅ **多模型支持** - 集成RetinaFace/Ultra-Light-Fast-Generic-Face-Detector双检测模型(即将支持OCR/目标检测)
-✅ **跨平台兼容** - 完美支持Windows/Linux/macOS系统(x86 & ARM架构)
+
+✅ **开箱即用** - 两行代码即可调用算法
+
+✅ **支持多种深度学习引擎** - Pytorch、Tensorflow、MXNet、ONNX Runtime
+
+✅ **功能丰富** - 当前支持人脸识别与目标检测,未来将陆续支持 OCR、图像分类、NLP 等多个 AI 领域任务,构建全面的智能算法体系。
+
+✅ **跨平台兼容** - 支持Windows/Linux/macOS系统(x86 & ARM架构)
-## 🌟 核心优势
+## 🌟 AI集成方式对比
| 维度 | Python生态 | 原生DJL | 本工具包 |
|------------|---------------------|-----------------|----------------|
| 开发效率 | 需搭建Python环境 | 需实现完整AI Pipeline | 提供即用API |
@@ -27,229 +38,115 @@
| 部署复杂度 | 需维护多语言服务 | 需处理底层资源调度 | 单一Jar包集成 |
| 性能表现 | 原生高性能 | 依赖开发者优化经验 | 内置生产级调优 |
+
+
## 📌 支持功能
-### ✅ 已实现功能
+#### ✅ 已实现功能
- **人脸检测**
- 支持图片/视频流中的多面孔定位与质量评估
+ 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
+- **目标检测**
+ 支持通用目标检测,能够识别图像中的多种物体类别,返回物体位置与类别信息
-- **人脸特征提取**
- 基于深度学习算法生成512维特征向量
-
-- **人脸特征比对** (1:1)
-
-- **人脸查询** (1:N)
- 人脸库注册/人脸库查询/人脸库删除
-
-### ⌛ 规划中功能
+#### ⌛ 规划中功能
- **OCR文字识别**
- 即将支持身份证/银行卡/车牌等关键信息提取,适配复杂背景与模糊文本
-
-- **目标检测**
- 计划集成YOLOv9模型,支持车辆检测/安全帽识别/工业质检等场景
-
+ 即将支持身份证/银行卡/车牌等关键信息提取
- **图像分割**
-
- **语音识别**
基于Transformer的语音转文本引擎,支持中文/英文多语种识别
-## 目前已集成的人脸算法模型
-- **RetinaFace 模型**[[GitHub]](https://github.com/deepinsight/insightface/tree/master/detection/retinaface):一个高效的深度学习人脸检测模型,支持高精度的人脸检测,但目前不支持人脸比对
-- **Ultra-Light-Fast-Generic-Face-Detector-1MB** [[GitHub\]](https://github.com/Linzaer/Ultra-Light-Fast-Generic-Face-Detector-1MB):一个轻量级的人脸检测模型,适用于需要较低延迟和较小模型尺寸的应用场景。
-- **Seetaface6** [[GitHub\]](https://github.com/seetafaceengine/SeetaFace6):是中科视拓最新开放的商业正式级版本,支持人脸检测、关键点定位、人脸识别。同时增加了活体检测、质量评估、年龄性别估计。并且响应时事,开放了口罩检测以及戴口罩的人脸识别模型
-- **[facenet-pytorch](https://github.com/timesler/facenet-pytorch)** [[GitHub\]](https://github.com/seetafaceengine/SeetaFace6):这是 pytorch 中 Inception Resnet (V1) 模型的存储库,在 VGGFace2 和 CASIA-Webface 上进行了预训练。Pytorch 模型权重使用从 David Sandberg 的 [tensorflow Facenet repo](https://github.com/davidsandberg/facenet) 移植的参数进行初始化。该存储库中还包含 MTCNN 的高效 pytorch 实现,用于推理之前的人脸检测。这些模型也是经过预训练的。据我们所知,这是最快的 MTCNN 实现。
+## 🛠️包含组件
-### 人脸模型对比及下载地址
+| 模块 | 介绍 |
+| -------------------|---------------------------------------------------------------------------------- |
+| smartjavaai-common | 基础通用模块,封装了公共功能,供各算法模块共享使用 |
+| smartjavaai-face | 人脸功能模块 |
+| smartjavaai-objectdetection | 目标检测模块 |
+| smartjavaai-seetaface6-lib | seetaface6人脸算法JNI接口封装 |
-| 模型名称 | 下载地址 | 文件大小 | 适用场景 | 兼容系统 |
-| :-----------------------: | :----------------------------------------------------------: | :------: | :---------------: | ------------------- |
-| retinaface | [下载](https://resources.djl.ai/test-models/pytorch/retinaface.zip) | 110MB | 高精度人脸检测 | Windows/Linux/MacOS |
-| ultralightfastgenericface | [下载](https://resources.djl.ai/test-models/pytorch/ultranet.zip) | 1.7MB | 高速人脸检测 | Windows/Linux/MacOS |
-| seetaface6 | [下载](https://pan.baidu.com/s/1hfNacA8ISV2qHrycjOkgqA?pwd=1234) | 288MB | 人脸检测/人脸识别 | Windows |
-| facenet-pytorch | [下载](https://resources.djl.ai/test-models/pytorch/face_feature.zip) | 104MB | 人脸识别 | Windows/Linux/MacOS |
+可以根据需求对每个模块单独引入
-## 环境要求
+-------------------------------------------------------------------------------
+
+
+
+## 📦 安装
+
+#### 1、环境要求
- Java 版本:**JDK 11或更高版本**
- 操作系统:不同模型支持的系统不一样,具体请查看文档
-## 使用步骤
-
->
-> 📌 **运行提示**:
->
-> (1)默认算法(RetinaFace)或轻量算法(Ultra-Light-Fast-Generic-Face-Detector )都为python算法,兼容 Windows、Linux、MacOS,Android 等系统,SmartJavaAI首次启动将自动下载模型到及依赖库到本地(.djl.ai隐藏文件夹),建议保持网络畅通。初始化完成后,后续启动可实现毫秒级响应。在无网络环境下,可指定本地模型路径(需提前下载模型包)。目前,这两种算法不支持人脸识别或人脸比对功能。
->
-> (2)Seetaface6 采用 C++ 编写,兼容 Windows、CentOS、Ubuntu 等系统,虽然Seetaface6 支持linux,但是我们目前仅实现了windows,如果后续对linux需求多,我们将兼容linux。Seetaface6 支持全功能人脸处理(人脸检测、人脸比对 1:1 或 1:N)。SmartJavaAI 通过 JNI 调用 C++ 接口,不支持在线下载模型,需手动下载并存储至本地。使用人脸比对等功能时,需要将项目中db/faces-data.db存放到您本地路径下并在config中指定人脸库路径。
-
-### 1. 安装人脸算法依赖
-
-在 Maven 项目的 `pom.xml` 中添加 SmartJavaAI的人脸算法依赖:
+#### 2、Maven
+在项目的pom.xml的dependencies中加入以下内容(全部功能),也可以根据需求对每个模块单独引入:
```xml
-
-
- ink.numberone
- smartjavaai-face
- 1.0.6
-
-
+
+ ink.numberone
+ smartjavaai-all
+ 1.0.8
+
```
+#### 3、人脸检测运行流程
-### 2. 人脸检测代码示例
+(1)人脸模型下载(使用人脸相关功能)
-```java
-//创建人脸算法
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm();
-//使用图片路径检测
-FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
-```
+如果在有网环境下使用,不需要下载模型(SeetaFace6模型除外)
-### 3. 轻量人脸检测代码示例
+| 模型名称 | 下载地址 | 文件大小 | 适用场景 | 兼容系统 |
+| :-----------------------: | :----------------------------------------------------------: | :------: | :----------------------------------------: | ------------------- |
+| retinaface | [下载](https://resources.djl.ai/test-models/pytorch/retinaface.zip) | 110MB | 高精度人脸检测 | Windows/Linux/MacOS |
+| ultralightfastgenericface | [下载](https://resources.djl.ai/test-models/pytorch/ultranet.zip) | 1.7MB | 高速人脸检测 | Windows/Linux/MacOS |
+| seetaface6 | [下载](https://pan.baidu.com/s/1hfNacA8ISV2qHrycjOkgqA?pwd=1234) | 288MB | 人脸检测、人脸比对、人脸库注册、人脸库查询 | Windows |
+| facenet | [下载](https://resources.djl.ai/test-models/pytorch/face_feature.zip) | 104MB | 人脸特征提取、人脸比对 | Windows/Linux/MacOS |
-```java
-//创建人脸算法
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createLightFaceAlgorithm();
-//使用图片路径检测
-FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
-```
+(2)人脸库下载(使用人脸库相关功能:人脸注册、人脸查询)
-### 4. 使用图片输入流检测
+目前仅SeetaFace6人脸算法支持人脸库注册,查询等功能,所以只有使用SeetaFace6模型时才需要下载`face.db`,`face.db` 是 一个SQLite 数据库,程序启动并使用相关功能时会自动操作该数据库,用于存储人脸特征数据及其对应的唯一标识 Key,支持后续的人脸注册、查询和比对等操作。
-```java
-//支持各种输入流方式检测图片
-File input = new File("src/main/resources/largest_selfie.jpg");
-FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(input));
-```
+下载链接: https://pan.baidu.com/s/1DzE1rDkFnjEXQbIasIdFrA?pwd=1234 提取码: 1234
-### 5. 人脸比对(1:1)
+(3)下载示例代码
->
-> (1)将项目中db/faces-data.db存放到您本地路径下
->
-> (2)下载模型到本地路径,下载地址:https://pan.baidu.com/s/1hfNacA8ISV2qHrycjOkgqA?pwd=1234 提取码:1234
->
-> (3)如果网盘地址请联系本人,文档最后有联系方式
+https://gitee.com/dengwenjie/SmartJavaAI-Demo
-```java
-// 初始化配置
-ModelConfig config = new ModelConfig();
-config.setAlgorithmName("seetaface6");//目前支持人脸比对的算法只有:seetaface6
-//人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
-config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
-//改为模型存放路径
-config.setModelPath("/opt/sf3.0_models");
-//创建人脸算法
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
-//自动裁剪人脸并比对人脸特征
-float similar = currentAlgorithm.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
-log.info("相似度:{}", similar);
-```
+`📁 src/main/java/smartai/examples/face
+└── 📄[RetinaFaceDemo.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/RetinaFaceDemo.java) *(人脸模型:RetinaFace示例代码)*
+└── 📄[LightFaceDemo](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/LightFaceDemo.java) *(人脸模型:UltraLightFastGenericFaceModel示例代码)*
+└── 📄[SeetaFace6Demo.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/SeetaFace6Demo.java) *(人脸模型:SeetaFace6示例代码)*
-### 6. 人脸特征提取及比对(seetaface6)
+└── 📄[FaceNetDemo.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/FaceNetDemo.java) *(人脸模型:FaceNet示例代码)*
-```java
-// 初始化配置
-ModelConfig config = new ModelConfig();
-config.setAlgorithmName("seetaface6");
-//人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
-config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
-//改为模型存放路径
-config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
-//创建人脸算法
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
-//提取图像中最大人脸的特征
-float[] feature1 = currentAlgorithm.featureExtraction("src/main/resources/kana1.jpg");
-float[] feature2 = currentAlgorithm.featureExtraction("src/main/resources/kana2.jpg");
-float similar = currentAlgorithm.calculSimilar(feature1, feature2);
-log.info("相似度:{}", similar);
-```
+(4)离线使用方法
-### 7. 人脸特征提取及比对(facenet-pytorch)
+程序首次运行时,会自动下载所需的底层依赖库到默认的缓存路径。不同操作系统的默认缓存路径如下:
-```java
-//创建脸算法
-FaceAlgorithm featureAlgorithm = FaceAlgorithmFactory.createFaceFeatureAlgorithm();
-//提取身份证人脸特征
-float[] feature1 = featureAlgorithm.featureExtraction("src/main/resources/kana1.jpg");
-float[] feature2 = featureAlgorithm.featureExtraction("src/main/resources/kana2.jpg");
-if (feature1 != null && feature2 != null) {
- //相似度在0.8至0.85及以上时,可判定为同一人,但具体阈值可能因图片而异,存在一定误差。
- float similar = featureAlgorithm.calculSimilar(feature1, feature2);
- log.info("相似度:{}", similar);
-} else {
- log.warn("人脸特征提取失败");
-}
-```
+\{user}需要替换成您当前登录的用户名
-### 8. 注册及搜索人脸(1:N)
+| | 依赖库及缓存目录 |
+| ------- | ------------------------ |
+| windows | C:/Users/\{user}/.djl.ai |
+| linux | /home/{user}/.djl.ai |
+| macos | /Users/{user}/.djl.ai |
-```java
-// 初始化配置
-ModelConfig config = new ModelConfig();
-config.setAlgorithmName("seetaface6");
-//人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
-config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
-//改为模型存放路径
-config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
-//创建人脸算法 自动将人脸库加载到内存中
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
-//等待人脸库加载完毕
-Thread.sleep(1000);
-//注册kana1人脸,参数key建议设置为人名
-boolean isSuccss = currentAlgorithm.register("kana1","src/main/resources/kana1.jpg");
-//注册jsy人脸,参数key建议设置为人名
-isSuccss = currentAlgorithm.register("jsy","src/main/resources/jsy.jpg");
-FaceResult faceResult = currentAlgorithm.search("src/main/resources/kana2.jpg");
-if(faceResult != null){
- log.info("查询到人脸:{}", faceResult.toString());
-}else{
- log.info("未查询到人脸");
-}
-```
+对于需要在离线环境中使用的情况,可以在联网环境中运行程序一次,确保所需的依赖库已下载。然后,将上述缓存目录复制到离线环境中相同的路径下,即可实现离线使用。
-### 9. 人脸检测(离线下载模型)
+请注意,SeetaFace6 默认支持离线使用,无需上述操作即可在离线环境中运行。
-```java
-// 初始化配置
-ModelConfig config = new ModelConfig();
-config.setAlgorithmName("retinaface");//人脸算法模型,目前支持:retinaface/ultralightfastgenericface/seetaface6
-//config.setAlgorithmName("ultralightfastgenericface");//轻量模型
-config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);//置信度阈值
-config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);//每张特征图保留的最大候选框数量
-//nms阈值:控制重叠框的合并程度,取值越低,合并越多重叠框(减少误检但可能漏检);取值越高,保留更多框(增加检出但可能引入冗余)
-config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
-//模型下载地址:
-//retinaface: https://resources.djl.ai/test-models/pytorch/retinaface.zip
-//ultralightfastgenericface: https://resources.djl.ai/test-models/pytorch/ultranet.zip
-//改为模型存放路径
-config.setModelPath("/Users/xxx/Documents/develop/face_model/retinaface.pt");
-//创建人脸算法
-FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
-//使用图片路径检测
-FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
-logger.info("人脸检测结果:{}", JSONObject.toJSONString(result));
-//使用图片流检测
-File input = new File("src/main/resources/largest_selfie.jpg");
-//FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(input));
-//logger.info("人脸检测结果:{}", JSONObject.toJSONString(result));
-BufferedImage image = ImageIO.read(input);
-//创建保存路径
-Path imagePath = Paths.get("output").resolve("retinaface_detected.jpg");
-//绘制人脸框
-ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString());
-```
+#### 4、目标检测运行流程
-## 完整代码
+(1)下载示例代码
-`📁 examples/src/main/java/smartai/examples/face`
-└── 📄[FaceDemo.java](https://github.com/geekwenjie/SmartJavaAI/blob/master/examples/src/main/java/smartai/examples/face/FaceDemo.java) *(基于JDK11构建的完整可执行示例)*
+https://gitee.com/dengwenjie/SmartJavaAI-Demo
+
+`📁 src/main/java/smartai/examples/objectdetection
+└── 📄[ObjectDetection.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/objectdetection/ObjectDetection.java) *(目标检测示例代码)*
@@ -259,7 +156,8 @@ ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString(
本项目在开发过程中借鉴或使用了以下优秀开源项目,特此致谢:
- **[Seetaface6JNI](https://gitee.com/cnsugar/seetaface6JNI)**
----
+- **[Deep Java Library](https://docs.djl.ai)**
+
## 联系方式
@@ -276,6 +174,13 @@ ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString(
## 更新日志
+
+## [v1.0.8] - 2025-04-13
+- 新增目标检测功能
+- 模型调用接口统一封装
+- 修复若干已知问题
+- 支持自定义选择使用 GPU 或 CPU 运算
+- 人脸识别模块新增多种接口,功能更加完善
## [v1.0.6] - 2025-04-01
- 修复人脸识别算法facenet-pytorch实现方式
- 优化Seetaface6算法,兼容jdk高版本
diff --git a/db/faces-data.db b/db/faces-data.db
deleted file mode 100644
index cbbc660..0000000
Binary files a/db/faces-data.db and /dev/null differ
diff --git a/examples/output/retinaface_detected.jpg b/examples/output/retinaface_detected.jpg
deleted file mode 100644
index 50ec51d..0000000
Binary files a/examples/output/retinaface_detected.jpg and /dev/null differ
diff --git a/examples/pom.xml b/examples/pom.xml
deleted file mode 100644
index ea30c65..0000000
--- a/examples/pom.xml
+++ /dev/null
@@ -1,115 +0,0 @@
-
-
- 4.0.0
-
- ink.numberone
- examples
- 1.0.0-SNAPSHOT
-
-
- 11
- 11
- UTF-8
- 1.0.0-SNAPSHOT
- smartai.examples.face.FaceDemo
-
-
-
-
-
- commons-cli
- commons-cli
- 1.9.0
-
-
- commons-io
- commons-io
- 2.17.0
-
-
- org.apache.logging.log4j
- log4j-slf4j2-impl
- 2.24.1
-
-
- org.testng
- testng
- 7.10.2
- test
-
-
-
- ink.numberone
- smartjavaai-face
- 1.0.6
-
-
-
- ch.qos.logback
- logback-classic
- 1.2.3
-
-
- org.slf4j
- slf4j-api
- 1.7.30
-
-
-
- com.alibaba
- fastjson
- 1.2.83
-
-
-
- ai.djl.onnxruntime
- onnxruntime-engine
- 0.20.0
-
-
-
-
- example
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
- 2.3
-
-
- false
-
- jar-with-dependencies
-
-
-
-
- true
-
- lib/
-
- smartai.examples.face.FaceDemo
-
-
-
-
-
- make-assembly
-
- package
-
- assembly
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/src/main/java/smartai/examples/face/FaceDemo.java b/examples/src/main/java/smartai/examples/face/FaceDemo.java
deleted file mode 100644
index 431344d..0000000
--- a/examples/src/main/java/smartai/examples/face/FaceDemo.java
+++ /dev/null
@@ -1,278 +0,0 @@
-package smartai.examples.face;
-
-import cn.smartjavaai.common.entity.Rectangle;
-import cn.smartjavaai.face.*;
-import cn.smartjavaai.face.entity.FaceResult;
-import com.alibaba.fastjson.JSONObject;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.time.StopWatch;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import smartai.examples.utils.ImageUtils;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.awt.image.RasterFormatException;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/**
- * @author dwj
- */
-@Slf4j
-public class FaceDemo {
-
-
- public static void main(String[] args) {
- try {
- featureExtractionAndCompare2();
- //detectFace2();
- //verifyIDCard();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * 人脸检测(服务端模型)
- * 人脸模型:retinaface
- * 特点:识别精度高,高速
- * 应用场景:如监控摄像头、智能安防系统等需要高精度检测的场合
- */
- public static void detectFace(){
- try {
- //创建人脸算法
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm();
- //使用图片路径检测
- FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
- log.info("人脸检测结果:{}", JSONObject.toJSONString(result));
- //使用图片流检测
- File input = new File("src/main/resources/largest_selfie.jpg");
- //FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(input));
- //log.info("人脸检测结果:{}", JSONObject.toJSONString(result));
- BufferedImage image = ImageIO.read(input);
- //创建保存路径
- Path imagePath = Paths.get("output").resolve("retinaface_detected.jpg");
- //绘制人脸框
- ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString());
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- }
-
-
- /**
- * 人脸检测(轻量模型)
- * 人脸模型:Ultra-Light-Fast-Generic-Face-Detector-1MB
- * 特点:高速,准确率略低
- * 应用场景:如监控摄像头、智能安防系统等需要高精度检测的场合
- */
- public static void detectFace2(){
- try {
- //创建轻量人脸算法
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createLightFaceAlgorithm();
- //使用图片路径检测
- FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
- log.info("轻量人脸检测结果:{}", JSONObject.toJSONString(result));
- //使用图片流检测
- //File imageFile = new File("/Users/wenjie/Downloads/djl-master/examples/src/test/resources/largest_selfie.jpg");
- //FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(imageFile));
- File input = new File("src/main/resources/largest_selfie.jpg");
- BufferedImage image = ImageIO.read(input);
- //创建保存路径
- Path imagePath = Paths.get("output").resolve("retinaface_detected.jpg");
- //绘制人脸框
- ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
-
-
- /**
- * 人脸检测(离线模型)
- * 人脸模型:retinaface
- * 特点:识别精度高,高速
- * 应用场景:如监控摄像头、智能安防系统等需要高精度检测的场合
- */
- public static void detectFaceOffine(){
- try {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("retinaface");//人脸算法模型,目前支持:retinaface/ultralightfastgenericface/seetaface6
- //config.setAlgorithmName("ultralightfastgenericface");//轻量模型
- config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);//置信度阈值
- config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);//每张特征图保留的最大候选框数量
- //nms阈值:控制重叠框的合并程度,取值越低,合并越多重叠框(减少误检但可能漏检);取值越高,保留更多框(增加检出但可能引入冗余)
- config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
- //模型下载地址:
- //retinaface: https://resources.djl.ai/test-models/pytorch/retinaface.zip
- //ultralightfastgenericface: https://resources.djl.ai/test-models/pytorch/ultranet.zip
- //改为模型存放路径
- config.setModelPath("/Users/wenjie/Documents/develop/face_model/retinaface.pt");
- //创建人脸算法
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
- //使用图片路径检测
- FaceDetectedResult result = currentAlgorithm.detect("src/main/resources/largest_selfie.jpg");
- log.info("人脸检测结果:{}", JSONObject.toJSONString(result));
- //使用图片流检测
- File input = new File("src/main/resources/largest_selfie.jpg");
- //FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(input));
- //logger.info("人脸检测结果:{}", JSONObject.toJSONString(result));
- BufferedImage image = ImageIO.read(input);
- //创建保存路径
- Path imagePath = Paths.get("output").resolve("retinaface_detected.jpg");
- //绘制人脸框
- ImageUtils.drawBoundingBoxes(image, result, imagePath.toAbsolutePath().toString());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * 人脸比对(1:1)
- * @throws Exception
- */
- public static void featureComparison(){
- try {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("seetaface6");//目前支持人脸比对的算法只有:seetaface6
- //人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
- config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
- //改为模型存放路径
- config.setModelPath("/opt/sf3.0_models");
- //创建人脸算法
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
- //自动裁剪人脸并比对人脸特征
- float similar = currentAlgorithm.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
- log.info("相似度:{}", similar);
- }
- catch (Exception e){
- e.printStackTrace();
- }
- }
-
- /**
- * seetaface6人脸特征提取及比对(可人证核验)
- * 目前仅支持windows 64位系统,如需支持其他操作系统可参考方法:featureExtractionAndCompare2
- */
- public static void featureExtractionAndCompare(){
- try {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("seetaface6");
- //人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
- config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
- //改为模型存放路径
- config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
- //创建人脸算法
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
- //提取图像中最大人脸的特征
- float[] feature1 = currentAlgorithm.featureExtraction("src/main/resources/kana1.jpg");
- float[] feature2 = currentAlgorithm.featureExtraction("src/main/resources/kana2.jpg");
- if(feature1 != null && feature2 != null){
- float similar = currentAlgorithm.calculSimilar(feature1, feature2);
- log.info("相似度:{}", similar);
- }else{
- log.warn("人脸特征提取失败");
- }
- }
- catch (Exception e){
- e.printStackTrace();
- }
- }
-
- /**
- * facenet-pytorch 人脸特征提取及比对(可人证核验)
- * 支持windows,linux,macos
- */
- public static void featureExtractionAndCompare2(){
- try {
- //创建脸算法
- FaceAlgorithm featureAlgorithm = FaceAlgorithmFactory.createFaceFeatureAlgorithm();
- //提取身份证人脸特征
- float[] feature1 = featureAlgorithm.featureExtraction("src/main/resources/kana1.jpg");
- float[] feature2 = featureAlgorithm.featureExtraction("src/main/resources/kana2.jpg");
- if (feature1 != null && feature2 != null) {
- //相似度在0.8至0.85及以上时,可判定为同一人,但具体阈值可能因图片而异,存在一定误差。
- float similar = featureAlgorithm.calculSimilar(feature1, feature2);
- log.info("相似度:{}", similar);
- } else {
- log.warn("人脸特征提取失败");
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * 注册人脸及搜索人脸(1:N)
- */
- public static void registerAndSearchFace(){
- try {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("seetaface6");
- //人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
- config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
- //改为模型存放路径
- config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
- //创建人脸算法 自动将人脸库加载到内存中
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
- //等待人脸库加载完毕
- Thread.sleep(1000);
- //注册kana1人脸,参数key建议设置为人名
- boolean isSuccss = currentAlgorithm.register("kana1","src/main/resources/kana1.jpg");
- //注册jsy人脸,参数key建议设置为人名
- isSuccss = currentAlgorithm.register("jsy","src/main/resources/jsy.jpg");
- FaceResult faceResult = currentAlgorithm.search("src/main/resources/kana2.jpg");
- if(faceResult != null){
- log.info("查询到人脸:{}", faceResult.toString());
- }else{
- log.info("未查询到人脸");
- }
-
- }
- catch (Exception e){
- e.printStackTrace();
- }
- }
-
- /**
- * 删除已注册人脸
- */
- public static void removeRegisterFace(){
- try {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("seetaface6");
- //人脸库路径 如果不指定人脸库,无法使用 1:N人脸搜索
- config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
- //改为模型存放路径
- config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
- //创建人脸算法 自动将人脸库加载到内存中
- FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
- //等待人脸库加载完毕
- Thread.sleep(1000);
- //使用注册人脸时的key值删除,可一次性删除单个
- long num = currentAlgorithm.removeRegister("kana1");
- //删除全部人脸
- //long num = currentAlgorithm.clearFace();
- log.info("删除成功数量:" + num);
- }
- catch (Exception e){
- e.printStackTrace();
- }
- }
-
-}
diff --git a/examples/src/main/java/smartai/examples/utils/ImageUtils.java b/examples/src/main/java/smartai/examples/utils/ImageUtils.java
deleted file mode 100644
index 1914760..0000000
--- a/examples/src/main/java/smartai/examples/utils/ImageUtils.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package smartai.examples.utils;
-
-import cn.smartjavaai.face.FaceDetectedResult;
-
-import javax.imageio.ImageIO;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/**
- * @author dwj
- */
-public class ImageUtils {
-
- /**
- * 绘制人脸框
- * @param sourceImage
- * @param faceDetectedResult
- * @param savePath
- * @throws IOException
- */
- public static void drawBoundingBoxes(BufferedImage sourceImage, FaceDetectedResult faceDetectedResult,String savePath) throws IOException {
- 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(cn.smartjavaai.common.entity.Rectangle rectangle : faceDetectedResult.getRectangles()){
- graphics.setColor(Color.RED);// 边框颜色
- graphics.drawRect(rectangle.getPointList().get(0).getX(),
- rectangle.getPointList().get(0).getY(), rectangle.getWidth(), rectangle.getHeight());
- drawText(graphics, "face", rectangle.getPointList().get(0).getX(), rectangle.getPointList().get(0).getY(), stroke, 4);
- }
- graphics.dispose();
- ImageIO.write(sourceImage, "jpg", new File(savePath));
-
- }
-
- private static void drawText(Graphics2D g, String text, int x, int y, int stroke, int padding) {
- FontMetrics metrics = g.getFontMetrics();
- x += stroke / 2;
- y += stroke / 2;
- int width = metrics.stringWidth(text) + padding * 2 - stroke / 2;
- int height = metrics.getHeight() + metrics.getDescent();
- int ascent = metrics.getAscent();
- Rectangle background = new Rectangle(x, y, width, height);
- g.fill(background);
- g.setPaint(Color.WHITE);
- g.drawString(text, x + padding, y + ascent);
- }
-}
diff --git a/examples/src/main/resources/jsy.jpg b/examples/src/main/resources/jsy.jpg
deleted file mode 100644
index c4640e7..0000000
Binary files a/examples/src/main/resources/jsy.jpg and /dev/null differ
diff --git a/examples/src/main/resources/kana1.jpg b/examples/src/main/resources/kana1.jpg
deleted file mode 100644
index ef364e0..0000000
Binary files a/examples/src/main/resources/kana1.jpg and /dev/null differ
diff --git a/examples/src/main/resources/kana2.jpg b/examples/src/main/resources/kana2.jpg
deleted file mode 100644
index 59c9e52..0000000
Binary files a/examples/src/main/resources/kana2.jpg and /dev/null differ
diff --git a/examples/src/main/resources/largest_selfie.jpg b/examples/src/main/resources/largest_selfie.jpg
deleted file mode 100644
index 605ec97..0000000
Binary files a/examples/src/main/resources/largest_selfie.jpg and /dev/null differ
diff --git a/examples/src/main/resources/logback.xml b/examples/src/main/resources/logback.xml
deleted file mode 100644
index 8f006d9..0000000
--- a/examples/src/main/resources/logback.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
- %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n
-
-
-
-
-
-
-
diff --git a/pom.xml b/pom.xml
index 97803f1..3630892 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,15 @@
ink.numberone
smartjavaai-parent
- 1.0.6
+ 1.0.8
pom
SmartJavaAI
smartjavaai-face
smartjavaai-common
smartjavaai-seetaface6-lib
+ smartjavaai-objectdetection
+ smartjavaai-all
@@ -35,13 +37,13 @@
ink.numberone
smartjavaai-common
- 1.0.6
+ 1.0.8
ink.numberone
smartjavaai-face
- 1.0.6
+ 1.0.8
@@ -51,56 +53,46 @@
ai.djl
api
- ${djl.version}
ai.djl
basicdataset
- ${djl.version}
ai.djl
model-zoo
- ${djl.version}
ai.djl.timeseries
timeseries
- ${djl.version}
ai.djl.huggingface
tokenizers
- ${djl.version}
ai.djl.audio
audio
- ${djl.version}
ai.djl.mxnet
mxnet-model-zoo
- ${djl.version}
ai.djl.pytorch
pytorch-model-zoo
- ${djl.version}
ai.djl.tensorflow
tensorflow-model-zoo
- ${djl.version}
ai.djl.onnxruntime
onnxruntime-engine
- ${djl.version}
org.testng
@@ -121,47 +113,6 @@
3.9
-
- ai.djl.pytorch
- pytorch-engine
- 0.32.0
- runtime
-
-
-
- ai.djl.pytorch
- pytorch-native-cpu
- win-x86_64
- runtime
- 2.5.1
-
-
- ai.djl.pytorch
- pytorch-native-cpu
- linux-x86_64
- runtime
- 2.5.1
-
-
- ai.djl.pytorch
- pytorch-native-cpu-precxx11
- linux-aarch64
- runtime
- 2.5.1
-
-
- ai.djl.pytorch
- pytorch-native-cpu
- osx-aarch64
- 2.5.1
- runtime
-
-
- ai.djl.pytorch
- pytorch-jni
- 2.5.1-0.32.0
- runtime
-
@@ -194,8 +145,52 @@
2.17.0
+
+
+ org.apache.commons
+ commons-pool2
+ 2.12.0
+
+
+
+
+
+ ai.djl.pytorch
+ pytorch-engine
+ runtime
+
+
+ ai.djl.tensorflow
+ tensorflow-engine
+ runtime
+
+
+
+ ai.djl.mxnet
+ mxnet-engine
+ runtime
+
+
+ ai.djl.onnxruntime
+ onnxruntime-engine
+
+
+ ai.djl.ml.xgboost
+ xgboost
+ runtime
+
+
+ ai.djl.tensorrt
+ tensorrt
+ runtime
+
+
+
+
+
+
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-all/pom.xml b/smartjavaai-all/pom.xml
new file mode 100644
index 0000000..b3d4953
--- /dev/null
+++ b/smartjavaai-all/pom.xml
@@ -0,0 +1,139 @@
+
+
+ 4.0.0
+
+ ink.numberone
+ smartjavaai-parent
+ 1.0.8
+
+
+ smartjavaai-all
+ 1.0.8
+ smartjavaai-all
+ SmartJavaAI
+ https://github.com/geekwenjie/SmartJavaAI
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ ink.numberone
+ smartjavaai-face
+ ${project.version}
+
+
+
+ ink.numberone
+ smartjavaai-objectdetection
+ ${project.version}
+
+
+
+
+ 11
+ 11
+ UTF-8
+ true
+
+
+
+
+
+ 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 bffeb9b..b0ef058 100644
--- a/smartjavaai-common/pom.xml
+++ b/smartjavaai-common/pom.xml
@@ -6,7 +6,7 @@
ink.numberone
smartjavaai-parent
- 1.0.6
+ 1.0.8
smartjavaai-common
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
new file mode 100644
index 0000000..3a4b2e2
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionRectangle.java
@@ -0,0 +1,84 @@
+package cn.smartjavaai.common.entity;
+
+/**
+ * 检测结果-矩形区域
+ * @author dwj
+ */
+public class DetectionRectangle {
+
+ public int x;
+ public int y;
+ public int width;
+ public int height;
+ public float score;
+
+ public String className;
+
+ public DetectionRectangle() {
+ }
+
+ public DetectionRectangle(int x, int y, int width, int height, float score) {
+ 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;
+ }
+}
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
new file mode 100644
index 0000000..7ee6f48
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
@@ -0,0 +1,21 @@
+package cn.smartjavaai.common.entity;
+
+import java.util.List;
+
+/**
+ * 检测结果
+ * @author dwj
+ * @date 2025/4/12
+ */
+public class DetectionResponse {
+
+ private List rectangleList;
+
+ public List getRectangleList() {
+ return rectangleList;
+ }
+
+ public void setRectangleList(List rectangleList) {
+ this.rectangleList = rectangleList;
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java
deleted file mode 100644
index dad5e90..0000000
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.smartjavaai.common.entity;
-
-import java.util.List;
-
-/**
- * 矩形区域
- * @author dwj
- */
-public class Rectangle {
-
- /**
- * 矩形区域点集合
- */
- List pointList;
-
- /**
- * 矩形区域宽度
- */
- int width;
-
- /**
- * 矩形区域高度
- */
- int height;
-
- 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 List getPointList() {
- return pointList;
- }
-
- public void setPointList(List pointList) {
- this.pointList = pointList;
- }
-}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/DeviceEnum.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/DeviceEnum.java
new file mode 100644
index 0000000..afbc40f
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/enums/DeviceEnum.java
@@ -0,0 +1,14 @@
+package cn.smartjavaai.common.enums;
+
+/**
+ * 目标检测模型枚举
+ * @author dwj
+ * @date 2025/4/4
+ */
+public enum DeviceEnum {
+
+ // resnet50 系列
+ CPU,
+ GPU;
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/ModelPredictorPoolManager.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/ModelPredictorPoolManager.java
new file mode 100644
index 0000000..27ae467
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/ModelPredictorPoolManager.java
@@ -0,0 +1,71 @@
+package cn.smartjavaai.common.pool;
+
+import ai.djl.inference.Predictor;
+import ai.djl.repository.zoo.ZooModel;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+
+/**
+ * 模型共享池管理器
+ * @author dwj
+ * @date 2025/4/8
+ */
+public class ModelPredictorPoolManager {
+
+ // 每个模型的唯一key -> 对应Predictor池
+ private final Map>> poolMap = new ConcurrentHashMap<>();
+
+ /**
+ * 注册模型池
+ * @param key 模型标识符(自定义,如模型路径、模型名等)
+ * @param model 模型本体
+ * @param config 池配置(可选)
+ */
+ public void registerModel(String key, ZooModel model, GenericObjectPoolConfig> config) {
+ PredictorFactory factory = new PredictorFactory<>(model);
+ GenericObjectPool> pool = new GenericObjectPool<>(factory, config);
+ poolMap.put(key, pool);
+ }
+
+ /**
+ * 借出一个 Predictor
+ */
+ @SuppressWarnings("unchecked")
+ public Predictor borrowPredictor(String key) throws Exception {
+ GenericObjectPool> pool = (GenericObjectPool>) poolMap.get(key);
+ if (pool == null) {
+ throw new IllegalArgumentException("模型未注册: " + key);
+ }
+ return pool.borrowObject();
+ }
+
+ /**
+ * 归还一个 Predictor
+ */
+ @SuppressWarnings("unchecked")
+ public void returnPredictor(String key, Predictor predictor) {
+ GenericObjectPool> pool = (GenericObjectPool>) poolMap.get(key);
+ if (pool != null) {
+ pool.returnObject(predictor);
+ }
+ }
+
+ /**
+ * 销毁全部池
+ */
+ public void closeAll() {
+ for (GenericObjectPool extends Predictor, ?>> pool : poolMap.values()) {
+ pool.close();
+ }
+ poolMap.clear();
+ }
+
+
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/PredictorFactory.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/PredictorFactory.java
new file mode 100644
index 0000000..582ddca
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/pool/PredictorFactory.java
@@ -0,0 +1,35 @@
+package cn.smartjavaai.common.pool;
+
+import ai.djl.inference.Predictor;
+import ai.djl.repository.zoo.ZooModel;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+
+/**
+ * Predictor 工厂类
+ * @author dwj
+ * @date 2025/4/8
+ */
+public class PredictorFactory extends BasePooledObjectFactory> {
+ private final ZooModel model;
+
+ public PredictorFactory(ZooModel model) {
+ this.model = model;
+ }
+
+ @Override
+ public Predictor create() {
+ return model.newPredictor();
+ }
+
+ @Override
+ public PooledObject> wrap(Predictor predictor) {
+ return new DefaultPooledObject<>(predictor);
+ }
+
+ @Override
+ public void destroyObject(PooledObject> p) {
+ p.getObject().close();
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/FileUtils.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/FileUtils.java
new file mode 100644
index 0000000..67afd6f
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/FileUtils.java
@@ -0,0 +1,21 @@
+package cn.smartjavaai.common.utils;
+
+import java.io.File;
+
+/**
+ * 文件操作工具类
+ * @author dwj
+ * @date 2025/4/4
+ */
+public class FileUtils {
+
+ /**
+ * 检查文件是否存在
+ * @param filePath
+ * @return
+ */
+ public static boolean isFileExists(String filePath) {
+ File file = new File(filePath);
+ return file.exists() && !file.isDirectory(); // 确保是文件且存在
+ }
+}
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 6cfd4a1..a9e9d92 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
@@ -1,8 +1,12 @@
package cn.smartjavaai.common.utils;
+import javax.imageio.ImageIO;
+import java.awt.*;
import java.awt.image.BufferedImage;
//import java.awt.image.ColorConvertOp;
import java.awt.image.ComponentSampleModel;
+import java.io.File;
+import java.io.IOException;
import java.util.Arrays;
/**
@@ -74,4 +78,18 @@ public class ImageUtils {
return image;
}
+ /**
+ * 检查图像是否有效
+ * @param image
+ * @return
+ */
+ public static boolean isImageValid(BufferedImage image) {
+ // 检查是否为 null 或尺寸异常(如宽高为0)
+ return image != null && image.getWidth() > 0 && image.getHeight() > 0;
+ }
+
+
+
+
+
}
diff --git a/smartjavaai-face/pom.xml b/smartjavaai-face/pom.xml
index 0f22a62..6bb03a7 100644
--- a/smartjavaai-face/pom.xml
+++ b/smartjavaai-face/pom.xml
@@ -6,11 +6,11 @@
ink.numberone
smartjavaai-parent
- 1.0.6
+ 1.0.8
smartjavaai-face
- 1.0.6
+ 1.0.8
smartjavaai-face
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java
deleted file mode 100644
index b58f914..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package cn.smartjavaai.face;
-
-import cn.smartjavaai.face.entity.FaceResult;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * 人脸识别算法
- * @author dwj
- */
-public abstract class AbstractFaceAlgorithm implements FaceAlgorithm{
- @Override
- public void loadModel(ModelConfig config) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public void loadFaceFeatureModel(ModelConfig config) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public FaceDetectedResult detect(String imagePath) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
- return null;
- }
-
- @Override
- public float[] featureExtraction(String imagePath) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public float[] featureExtraction(InputStream inputStream) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public float featureComparison(String imagePath1, String imagePath2) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public boolean register(String key, String imagePath) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public boolean register(String key, InputStream inputStream) throws Exception {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public FaceResult search(String imagePath) throws Exception{
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public FaceResult search(InputStream inputStream) throws Exception{
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public long removeRegister(String... keys) throws Exception{
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public long clearFace() throws Exception{
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java
new file mode 100644
index 0000000..f4a5a8d
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceModel.java
@@ -0,0 +1,143 @@
+package cn.smartjavaai.face;
+
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.face.entity.FaceResult;
+
+import java.awt.image.BufferedImage;
+import java.io.InputStream;
+
+/**
+ * 人脸识别算法
+ * @author dwj
+ */
+public abstract class AbstractFaceModel implements FaceModel {
+ @Override
+ public void loadModel(FaceModelConfig config) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public DetectionResponse detect(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public DetectionResponse detect(InputStream imageInputStream) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public DetectionResponse detect(BufferedImage image) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public DetectionResponse detect(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public void detectAndDraw(String imagePath, String outputPath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float[] featureExtraction(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float[] featureExtraction(InputStream inputStream) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float calculSimilar(float[] feature1, float[] feature2) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float featureComparison(String imagePath1, String imagePath2) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public boolean register(String key, String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public boolean register(String key, InputStream inputStream) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public boolean register(String key, BufferedImage sourceImage) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public boolean register(String key, byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public FaceResult search(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public FaceResult search(InputStream inputStream) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public long removeRegister(String... keys) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public long clearFace() {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float[] featureExtraction(BufferedImage sourceImage) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float[] featureExtraction(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public float featureComparison(byte[] imageData1, byte[] imageData2) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public FaceResult search(BufferedImage sourceImage) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ @Override
+ public FaceResult search(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java
deleted file mode 100644
index b42dcd8..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package cn.smartjavaai.face;
-
-import ai.djl.MalformedModelException;
-import ai.djl.repository.zoo.ModelNotFoundException;
-import ai.djl.translate.TranslateException;
-import cn.smartjavaai.face.entity.FaceResult;
-
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * 人脸识别算法
- * @author dwj
- */
-public interface FaceAlgorithm {
-
- /**
- * 加载模型
- * @param config
- * @throws Exception
- */
- void loadModel(ModelConfig config) throws Exception; // 加载模型
-
- /**
- * 加载人脸特征提取模型
- * @param config
- * @throws Exception
- */
- void loadFaceFeatureModel(ModelConfig config) throws Exception; // 加载模型
-
- /**
- * 人脸检测
- * @param imagePath 图片路径
- * @return
- * @throws Exception
- */
- FaceDetectedResult detect(String imagePath) throws Exception;
-
- /**
- * 人脸检测
- * @param imageInputStream 图片输入流
- * @return
- * @throws Exception
- */
- FaceDetectedResult detect(InputStream imageInputStream) throws Exception;
-
- /**
- * 特征提取
- * @param imagePath 图片路径
- * @return
- * @throws Exception
- */
- float[] featureExtraction(String imagePath) throws Exception;
-
- /**
- * 特征提取
- * @param inputStream 输入流
- * @return
- * @throws Exception
- */
- float[] featureExtraction(InputStream inputStream) throws Exception;
-
- /**
- * 计算相似度
- * @param feature1 图1特征
- * @param feature2 图2特征
- * @return
- * @throws Exception
- */
- float calculSimilar(float[] feature1, float[] feature2) throws Exception;
-
- /**
- * 特征比较
- * @param imagePath1 图1路径
- * @param imagePath2 图2路径
- * @return
- * @throws Exception
- */
- float featureComparison(String imagePath1, String imagePath2) throws Exception;
-
- /**
- * 特征比较
- * @param inputStream1 图1输入流
- * @param inputStream2 图2输入流
- * @return
- * @throws Exception
- */
- float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception;
-
- /**
- * 注册人脸
- * @param key
- * @param imagePath
- * @return
- */
- boolean register(String key, String imagePath) throws Exception;
-
- /**
- * 注册人脸
- * @param key
- * @param inputStream
- * @return
- */
- boolean register(String key, InputStream inputStream) throws Exception;
-
- /**
- * 查询人脸
- * @param imagePath
- * @return
- */
- FaceResult search(String imagePath) throws Exception;
-
- /**
- * 查询人脸
- * @param inputStream
- * @return
- */
- FaceResult search(InputStream inputStream) throws Exception;
-
- /**
- * 删除已标记人脸
- * @param keys
- * @return
- */
- long removeRegister(String... keys) throws Exception;
-
- /**
- * 清空人脸库数据
- */
- long clearFace() throws Exception;
-
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java
deleted file mode 100644
index 437e316..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package cn.smartjavaai.face;
-
-import cn.smartjavaai.face.algo.FeatureExtractionAlgo;
-import cn.smartjavaai.face.algo.RetinaFace;
-import cn.smartjavaai.face.algo.SeetaFace6Algo;
-import cn.smartjavaai.face.algo.UltraLightFastGenericFace;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * 人脸算法工厂
- * @author dwj
- */
-public class FaceAlgorithmFactory {
-
- /**
- * 算法注册表
- */
- private static final Map> registry =
- new ConcurrentHashMap<>();
-
- /**
- * 注册算法
- * @param name
- * @param clazz
- */
- public static void registerAlgorithm(String name, Class extends FaceAlgorithm> clazz) {
- registry.put(name.toLowerCase(), clazz);
- }
-
- /**
- * 使用ModelConfig创建算法
- * @param config
- * @return
- * @throws Exception
- */
- public static FaceAlgorithm createFaceAlgorithm(ModelConfig config) throws Exception {
- Class> clazz = registry.get(config.getAlgorithmName().toLowerCase());
- if(clazz == null){
- System.out.println("No such algorithm: " + config.getAlgorithmName().toLowerCase());
- throw new IllegalArgumentException("Unsupported algorithm");
- }
- FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
- algorithm.loadModel(config);
- return algorithm;
- }
-
- /**
- * 创建默认算法
- * @return
- * @throws Exception
- */
- public static FaceAlgorithm createFaceAlgorithm() throws Exception {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("retinaface");
- config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
- config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);
- config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
- return createFaceAlgorithm(config);
- }
-
- /**
- * 创建轻量级算法
- * @return
- * @throws Exception
- */
- public static FaceAlgorithm createLightFaceAlgorithm() throws Exception {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("ultralightfastgenericface");
- config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
- config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);
- config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
- Class> clazz = registry.get(config.getAlgorithmName().toLowerCase());
- if(clazz == null){
- throw new IllegalArgumentException("Unsupported algorithm");
- }
- FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
- algorithm.loadModel(config);
- return algorithm;
- }
-
- /**
- * 使用ModelConfig创建人脸特征提取算法
- * @param config
- * @return
- * @throws Exception
- */
- public static FaceAlgorithm createFaceFeatureAlgorithm(ModelConfig config) throws Exception {
- Class> clazz = registry.get(config.getAlgorithmName().toLowerCase());
- if(clazz == null){
- throw new IllegalArgumentException("Unsupported algorithm");
- }
- FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
- algorithm.loadFaceFeatureModel(config);
- return algorithm;
- }
-
- /**
- * 创建人脸特征提取算法
- * @return
- * @throws Exception
- */
- public static FaceAlgorithm createFaceFeatureAlgorithm() throws Exception {
- // 初始化配置
- ModelConfig config = new ModelConfig();
- config.setAlgorithmName("featureExtraction");
- return createFaceFeatureAlgorithm(config);
- }
-
- // 初始化默认算法
- static {
- registerAlgorithm("retinaface", RetinaFace.class);
- registerAlgorithm("ultralightfastgenericface", UltraLightFastGenericFace.class);
- //人脸特征提取
- registerAlgorithm("featureExtraction", FeatureExtractionAlgo.class);
- registerAlgorithm("seetaface6", SeetaFace6Algo.class);
- }
-
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java
deleted file mode 100644
index e04d0c3..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package cn.smartjavaai.face;
-
-
-import cn.smartjavaai.common.entity.Rectangle;
-
-import java.util.List;
-
-/**
- * 人脸检测结果
- * @author dwj
- */
-public class FaceDetectedResult {
-
-
- /**
- * 置信度
- */
- private List probabilities;
-
- /**
- * 人脸框
- */
- private List rectangles;
-
- public List getProbabilities() {
- return probabilities;
- }
-
- public void setProbabilities(List probabilities) {
- this.probabilities = probabilities;
- }
-
- public List getRectangles() {
- return rectangles;
- }
-
- public void setRectangles(List rectangles) {
- this.rectangles = rectangles;
- }
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java
new file mode 100644
index 0000000..bab6128
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModel.java
@@ -0,0 +1,206 @@
+package cn.smartjavaai.face;
+
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.face.entity.FaceResult;
+
+import java.awt.image.BufferedImage;
+import java.io.InputStream;
+
+/**
+ * 人脸识别算法
+ * @author dwj
+ */
+public interface FaceModel {
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ void loadModel(FaceModelConfig config); // 加载模型
+
+
+ /**
+ * 人脸检测
+ * @param imagePath 图片路径
+ * @return
+ */
+ DetectionResponse detect(String imagePath);
+
+ /**
+ * 人脸检测
+ * @param imageInputStream 图片输入流
+ * @return
+ */
+ DetectionResponse detect(InputStream imageInputStream);
+
+ /**
+ * 人脸检测
+ * @param image BufferedImage
+ * @return
+ */
+ DetectionResponse detect(BufferedImage image);
+
+ /**
+ * 人脸检测
+ * @param imageData
+ * @return
+ */
+ DetectionResponse detect(byte[] imageData);
+
+ /**
+ * 检测并绘制人脸
+ * @param imagePath 图片输入路径(包含文件名称)
+ * @param outputPath 图片输出路径(包含文件名称)
+ */
+ void detectAndDraw(String imagePath, String outputPath);
+
+ /**
+ * 检测并绘制人脸
+ * @param sourceImage
+ * @return
+ */
+ BufferedImage detectAndDraw(BufferedImage sourceImage);
+
+ /**
+ * 特征提取
+ * @param imagePath 图片路径
+ * @return
+ */
+ float[] featureExtraction(String imagePath);
+
+ /**
+ * 特征提取
+ * @param inputStream 输入流
+ * @return
+ */
+ float[] featureExtraction(InputStream inputStream);
+
+ /**
+ * 特征提取
+ * @param sourceImage BufferedImage图片数据
+ * @return
+ */
+ float[] featureExtraction(BufferedImage sourceImage);
+
+ /**
+ * 特征提取
+ * @param imageData 图片字节流
+ * @return
+ */
+ float[] featureExtraction(byte[] imageData);
+
+ /**
+ * 计算相似度
+ * @param feature1 图1特征
+ * @param feature2 图2特征
+ * @return
+ */
+ float calculSimilar(float[] feature1, float[] feature2);
+
+ /**
+ * 特征比较
+ * @param imagePath1 图1路径
+ * @param imagePath2 图2路径
+ * @return
+ */
+ float featureComparison(String imagePath1, String imagePath2);
+
+ /**
+ * 特征比较
+ * @param sourceImage1 图1BufferedImage
+ * @param sourceImag2 图2BufferedImage
+ * @return
+ */
+ float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2);
+
+ /**
+ * 特征比较
+ * @param inputStream1 图1输入流
+ * @param inputStream2 图2输入流
+ * @return
+ */
+ float featureComparison(InputStream inputStream1, InputStream inputStream2);
+
+
+ /**
+ * 特征比较
+ * @param imageData1
+ * @param imageData2
+ * @return
+ */
+ float featureComparison(byte[] imageData1, byte[] imageData2);
+
+ /**
+ * 注册人脸
+ * @param key
+ * @param imagePath
+ * @return
+ */
+ boolean register(String key, String imagePath);
+
+ /**
+ * 注册人脸
+ * @param key
+ * @param inputStream
+ * @return
+ */
+ boolean register(String key, InputStream inputStream);
+
+ /**
+ * 注册人脸
+ * @param key
+ * @param sourceImage
+ * @return
+ */
+ boolean register(String key, BufferedImage sourceImage);
+
+
+ /**
+ * 注册人脸
+ * @param key
+ * @param imageData
+ * @return
+ */
+ boolean register(String key, byte[] imageData);
+
+ /**
+ * 查询人脸
+ * @param imagePath
+ * @return
+ */
+ FaceResult search(String imagePath);
+
+ /**
+ * 查询人脸
+ * @param inputStream
+ * @return
+ */
+ FaceResult search(InputStream inputStream);
+
+ /**
+ * 查询人脸
+ * @param sourceImage
+ * @return
+ */
+ FaceResult search(BufferedImage sourceImage);
+
+ /**
+ * 查询人脸
+ * @param imageData
+ * @return
+ */
+ FaceResult search(byte[] imageData);
+
+ /**
+ * 删除已标记人脸
+ * @param keys
+ * @return
+ */
+ long removeRegister(String... keys);
+
+ /**
+ * 清空人脸库数据
+ */
+ long clearFace();
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java
new file mode 100644
index 0000000..a527c95
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelConfig.java
@@ -0,0 +1,45 @@
+package cn.smartjavaai.face;
+
+import cn.smartjavaai.common.enums.DeviceEnum;
+import lombok.Data;
+
+/**
+ * 模型配置
+ * @author dwj
+ */
+@Data
+public class FaceModelConfig {
+
+ /**
+ * 人脸算法名称
+ */
+ private FaceModelEnum modelEnum;
+
+ /**
+ * 置信度阈值
+ */
+ private double confidenceThreshold = FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD;
+
+ /**
+ * 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
+ */
+ private double nmsThresh = FaceConfig.NMS_THRESHOLD;
+
+ /**
+ * 模型路径
+ */
+ private String modelPath;
+
+ /**
+ * 人脸库路径
+ */
+ private String faceDbPath;
+
+ /**
+ * 设备类型
+ */
+ private DeviceEnum device;
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java
new file mode 100644
index 0000000..3dc23af
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelEnum.java
@@ -0,0 +1,44 @@
+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;
+
+/**
+ * 人脸模型枚举
+ * @author dwj
+ * @date 2025/4/10
+ */
+public enum FaceModelEnum {
+
+ RETINA_FACE("RetinaFaceModel"),
+ ULTRA_LIGHT_FAST_GENERIC_FACE("UltraLightFastGenericFaceModel"),
+ FACENET_FEATURE_EXTRACTION("FeatureExtractionModel"),
+ SEETA_FACE6_MODEL("SeetaFace6Model");
+
+ private final String modelClassName;
+
+ FaceModelEnum(String modelClassName) {
+ this.modelClassName = modelClassName;
+ }
+
+ public String getModelClassName() {
+ return modelClassName;
+ }
+
+ /**
+ * 根据名称获取枚举 (忽略大小写和下划线变体)
+ */
+ public static FaceModelEnum fromName(String name) {
+ String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
+ for (FaceModelEnum 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/FaceModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelFactory.java
new file mode 100644
index 0000000..1474dbe
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceModelFactory.java
@@ -0,0 +1,127 @@
+package cn.smartjavaai.face;
+
+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 lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 人脸算法工厂
+ * @author dwj
+ */
+@Slf4j
+public class FaceModelFactory {
+
+ // 使用 volatile 和双重检查锁定来确保线程安全的单例模式
+ private static volatile FaceModelFactory instance;
+
+ private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
+
+ /**
+ * 算法注册表
+ */
+ private static final Map> registry =
+ new ConcurrentHashMap<>();
+
+
+ public static FaceModelFactory getInstance() {
+ if (instance == null) {
+ synchronized (FaceModelFactory.class) {
+ if (instance == null) {
+ instance = new FaceModelFactory();
+ }
+ }
+ }
+ return instance;
+ }
+
+
+
+ /**
+ * 注册算法
+ * @param name
+ * @param clazz
+ */
+ private static void registerAlgorithm(String name, Class extends FaceModel> clazz) {
+ registry.put(name.toLowerCase(), clazz);
+ }
+
+
+ /**
+ * 获取模型(通过配置)
+ * @param config
+ * @return
+ */
+ public FaceModel getModel(FaceModelConfig config) {
+ if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
+ throw new FaceException("未配置人脸模型");
+ }
+ return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
+ return createFaceModel(config);
+ });
+ }
+
+ /**
+ * 获取默认模型
+ * @return
+ */
+ public FaceModel getModel() {
+ // 初始化默认配置
+ FaceModelConfig config = new FaceModelConfig();
+ config.setModelEnum(FaceModelEnum.RETINA_FACE);
+ config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
+ config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
+ return getModel(config);
+ }
+
+ /**
+ * 使用ModelConfig创建算法
+ * @param config
+ * @return
+ */
+ private FaceModel createFaceModel(FaceModelConfig config) {
+ Class> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
+ if(clazz == null){
+ throw new FaceException("Unsupported algorithm");
+ }
+ FaceModel algorithm = null;
+ try {
+ algorithm = (FaceModel) clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new FaceException(e);
+ }
+ algorithm.loadModel(config);
+ return algorithm;
+ }
+
+
+ /**
+ * 获取轻量级人脸模型
+ * @return
+ */
+ public FaceModel getLightFaceModel() {
+ // 初始化默认配置
+ FaceModelConfig config = new FaceModelConfig();
+ config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
+ config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
+ config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
+ return getModel(config);
+ }
+
+
+ // 初始化默认算法
+ static {
+ registerAlgorithm("retinafacemodel", RetinaFaceModel.class);
+ registerAlgorithm("ultralightfastgenericfacemodel", UltraLightFastGenericFaceModel.class);
+ //人脸特征提取
+ registerAlgorithm("featureextractionmodel", FeatureExtractionModel.class);
+ registerAlgorithm("seetaface6model", SeetaFace6Model.class);
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java
deleted file mode 100644
index d8685e9..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package cn.smartjavaai.face;
-
-/**
- * 模型配置
- * @author dwj
- */
-
-public class ModelConfig {
-
- /**
- * 人脸算法名称
- */
- private String algorithmName;
-
- /**
- * 置信度阈值
- */
- private double confidenceThreshold;
-
- /**
- * 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
- */
- private double nmsThresh;
-
- /**
- * 最大检测人脸数量
- */
- private int maxFaceCount;
-
- /**
- * 模型路径
- */
- private String modelPath;
-
- /**
- * 人脸库路径
- */
- private String faceDbPath;
-
-
- public String getAlgorithmName() {
- return algorithmName;
- }
-
- public void setAlgorithmName(String algorithmName) {
- this.algorithmName = algorithmName;
- }
-
- public double getConfidenceThreshold() {
- return confidenceThreshold;
- }
-
- public void setConfidenceThreshold(double confidenceThreshold) {
- this.confidenceThreshold = confidenceThreshold;
- }
-
- public double getNmsThresh() {
- return nmsThresh;
- }
-
- public void setNmsThresh(double nmsThresh) {
- this.nmsThresh = nmsThresh;
- }
-
- public int getMaxFaceCount() {
- return maxFaceCount;
- }
-
- public void setMaxFaceCount(int maxFaceCount) {
- this.maxFaceCount = maxFaceCount;
- }
-
- public String getModelPath() {
- return modelPath;
- }
-
- public void setModelPath(String modelPath) {
- this.modelPath = modelPath;
- }
-
- public String getFaceDbPath() {
- return faceDbPath;
- }
-
- public void setFaceDbPath(String faceDbPath) {
- this.faceDbPath = faceDbPath;
- }
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/FeatureExtractionAlgo.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/FeatureExtractionAlgo.java
deleted file mode 100644
index 734474c..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/FeatureExtractionAlgo.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package cn.smartjavaai.face.algo;
-
-import ai.djl.Device;
-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.modality.cv.translator.ImageFeatureExtractorFactory;
-import ai.djl.repository.zoo.Criteria;
-import ai.djl.repository.zoo.ZooModel;
-import ai.djl.training.util.ProgressBar;
-import cn.smartjavaai.common.entity.Point;
-import cn.smartjavaai.common.entity.Rectangle;
-import cn.smartjavaai.face.AbstractFaceAlgorithm;
-import cn.smartjavaai.face.FaceDetectedResult;
-import cn.smartjavaai.face.FaceDetectionTranslator;
-import cn.smartjavaai.face.ModelConfig;
-import cn.smartjavaai.face.translator.FaceFeatureTranslator;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.InputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * @author dwj
- */
-public class FeatureExtractionAlgo extends AbstractFaceAlgorithm {
-
-
- private Criteria faceFeatureCriteria;
-
- private Predictor predictor;
-
- private ZooModel model;
-
- public static final List mean =
- Arrays.asList(
- 127.5f / 255.0f,
- 127.5f / 255.0f,
- 127.5f / 255.0f,
- 128.0f / 255.0f,
- 128.0f / 255.0f,
- 128.0f / 255.0f);
-
-
- /**
- * 加载人脸特征提取模型
- * @param config
- * @throws Exception
- */
- @Override
- public void loadFaceFeatureModel(ModelConfig config) throws Exception {
- String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
- faceFeatureCriteria =
- Criteria.builder()
- .setTypes(Image.class, float[].class)
- .optModelName("face_feature") // specify model file prefix
- .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
- "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
- .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
- .optTranslator(new FaceFeatureTranslator())
- .optArgument("normalize", normalize)
- .optEngine("PyTorch") // Use PyTorch engine
- .optProgress(new ProgressBar())
- .build();
-
- model = faceFeatureCriteria.loadModel();
- predictor = model.newPredictor();
- }
-
-
- /**
- * 特征提取
- * @param imagePath 图片路径
- * @return
- * @throws Exception
- */
- @Override
- public float[] featureExtraction(String imagePath) throws Exception {
- Path imageFile = Paths.get(imagePath);
- Image img = ImageFactory.getInstance().fromFile(imageFile);
- img.getWrappedImage();
- return predictor.predict(img);
- }
-
- /**
- * 特征提取
- * @param inputStream 输入流
- * @return
- * @throws Exception
- */
- @Override
- public float[] featureExtraction(InputStream inputStream) throws Exception {
- Image img = ImageFactory.getInstance().fromInputStream(inputStream);
- img.getWrappedImage();
- return predictor.predict(img);
- }
-
- /**
- * 计算相似度
- * @param feature1 图1特征
- * @param feature2 图2特征
- * @return
- * @throws Exception
- */
- @Override
- public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
- float ret = 0.0f;
- float mod1 = 0.0f;
- float mod2 = 0.0f;
- int length = feature1.length;
- for (int i = 0; i < length; ++i) {
- ret += feature1[i] * feature2[i];
- mod1 += feature1[i] * feature1[i];
- mod2 += feature2[i] * feature2[i];
- }
- return (float) ((ret / Math.sqrt(mod1) / Math.sqrt(mod2) + 1) / 2.0f);
- }
-
- /**
- * 特征比较
- * @param imagePath1 图1路径
- * @param imagePath2 图2路径
- * @return
- * @throws Exception
- */
- @Override
- public float featureComparison(String imagePath1, String imagePath2) throws Exception {
- float[] feature1 = featureExtraction(imagePath1);
- float[] feature2 = featureExtraction(imagePath2);
- return calculSimilar(feature1, feature2);
- }
-
- /**
- * 特征比较
- * @param inputStream1 图1输入流
- * @param inputStream2 图2输入流
- * @return
- * @throws Exception
- */
- @Override
- public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
- float[] feature1 = featureExtraction(inputStream1);
- float[] feature2 = featureExtraction(inputStream2);
- return calculSimilar(feature1, feature2);
- }
-
- /*@Override
- public float[] recognize(FaceRegion region) {
- return new float[0];
- }*/
-
- /*@Override
- public void loadModel(ModelConfig config) throws Exception {
-
- }*/
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java
deleted file mode 100644
index 98b2512..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package cn.smartjavaai.face.algo;
-
-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.modality.cv.translator.ImageFeatureExtractorFactory;
-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 ai.djl.translate.TranslateException;
-import cn.smartjavaai.common.entity.Point;
-import cn.smartjavaai.common.entity.Rectangle;
-import cn.smartjavaai.face.*;
-import org.apache.commons.lang3.StringUtils;
-
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
-
-/**
- * RetinaFace实现
- * @author dwj
- */
-public class RetinaFace extends AbstractFaceAlgorithm {
-
- private Criteria criteria;
-
- private Criteria faceFeatureCriteria;
-
- private Predictor predictor;
-
- private ZooModel model;
-
- /**
- * 特征图层的基础缩放比例
- */
- public static final int[][] scales = {{16, 32}, {64, 128}, {256, 512}};
- /**
- * 特征图相对于原图的采样步长
- */
- public static final int[] steps = {8, 16, 32};
- /**
- * 缩放系数
- */
- public static final double[] variance = {0.1f, 0.2f};
-
-
- /**
- * 加载模型
- * @param config
- */
- @Override
- public void loadModel(ModelConfig config) throws ModelNotFoundException, MalformedModelException, IOException {
- FaceDetectionTranslator translator =
- new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, config.getMaxFaceCount(), scales, steps);
- criteria =
- Criteria.builder()
- .setTypes(Image.class, DetectedObjects.class)
- .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/retinaface.zip")
- // Load model from local file, e.g:
- .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
- .optModelName(StringUtils.isNotBlank(config.getAlgorithmName()) ? config.getAlgorithmName() : "retinaface") // specify model file prefix
- .optTranslator(translator)
- .optProgress(new ProgressBar())
- .optEngine("PyTorch") // Use PyTorch engine
- .build();
- model = criteria.loadModel();
- predictor = model.newPredictor();
- }
-
-
-
- /**
- * 检测人脸
- * @param imagePath 图片路径
- * @return
- * @throws Exception
- */
- @Override
- public FaceDetectedResult detect(String imagePath) throws Exception{
- Path facePath = Paths.get(imagePath);
- Image img = ImageFactory.getInstance().fromFile(facePath);
- DetectedObjects detection = predictor.predict(img);
- return convertToFaceDetectedResult(detection,img);
- }
-
- /**
- * 检测人脸
- * @param imageInputStream 图片流
- * @return
- * @throws Exception
- */
- @Override
- public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
- Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
- DetectedObjects detection = predictor.predict(img);
- return convertToFaceDetectedResult(detection,img);
- }
-
- /**
- * 转换为FaceDetectedResult
- * @param detection
- * @param img
- * @return
- */
- private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
- FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
- List probabilities = new ArrayList<>(detection.getProbabilities());
- List detectedObjectList = detection.items();
- List RectangleList = detectedObjectList.parallelStream()
- .map(obj -> {
- Rectangle rectangle = new Rectangle();
- List pointList = new ArrayList<>();
- ai.djl.modality.cv.output.Rectangle rectangleDjl = obj.getBoundingBox().getBounds();
- int x = (int)(rectangleDjl.getX() * (double)img.getWidth());
- int y = (int)(rectangleDjl.getY() * (double)img.getHeight());
- int width = (int)(rectangleDjl.getWidth() * (double)img.getWidth());
- int height = (int)(rectangleDjl.getHeight() * (double)img.getHeight());
- pointList.add(new Point(x,y));
- pointList.add(new Point(x + width,y));
- pointList.add(new Point(x,y + height));
- pointList.add(new Point(x + width,y + height));
- rectangle.setPointList(pointList);
- rectangle.setHeight(height);
- rectangle.setWidth(width);
- return rectangle;
- })
- .collect(Collectors.toList());
- faceDetectedResult.setProbabilities(probabilities);
- faceDetectedResult.setRectangles(RectangleList);
- return faceDetectedResult;
- }
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/SeetaFace6Algo.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/SeetaFace6Algo.java
deleted file mode 100644
index e06ba05..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/SeetaFace6Algo.java
+++ /dev/null
@@ -1,319 +0,0 @@
-package cn.smartjavaai.face.algo;
-
-import cn.smartjavaai.common.entity.Point;
-import cn.smartjavaai.common.entity.Rectangle;
-import cn.smartjavaai.common.utils.ImageUtils;
-import cn.smartjavaai.face.AbstractFaceAlgorithm;
-import cn.smartjavaai.face.FaceDetectedResult;
-import cn.smartjavaai.face.ModelConfig;
-import cn.smartjavaai.face.dao.FaceDao;
-import cn.smartjavaai.face.entity.FaceData;
-import cn.smartjavaai.face.entity.FaceResult;
-import com.seetaface.NativeLoader;
-import com.seetaface.SeetaFace6JNI;
-import com.seetaface.model.RecognizeResult;
-import com.seetaface.model.SeetaImageData;
-import com.seetaface.model.SeetaRect;
-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.Path;
-import java.nio.file.Paths;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * SeetaFace6 人脸算法
- * @author dwj
- */
-@Slf4j
-public class SeetaFace6Algo extends AbstractFaceAlgorithm {
-
-
- private ModelConfig config;
-
-
- @Override
- public void loadModel(ModelConfig config) throws Exception {
- this.config = config;
- if (NativeLoader.seetaFace6SDK == null) {
- synchronized (SeetaFace6JNI.class) {
- if(StringUtils.isBlank(config.getModelPath())){
- throw new Exception("modelPath is null");
- }
- //加载依赖库
- NativeLoader.loadNativeLibraries(config.getModelPath());
- log.info("Loading seetaFace6 library successfully.");
- NativeLoader.seetaFace6SDK = new SeetaFace6JNI();
- //加载模型
- boolean isSuccess = NativeLoader.seetaFace6SDK.initModel(config.getModelPath());
- if(!isSuccess){
- throw new Exception("seetaFace6模型初始化失败," + config.getModelPath());
- }
- log.info("Load seetaFace6 model success!");
- new Thread(new Runnable() {
- public void run() {
- try {
- log.info("start load faceDb...");
- loadFaceDb();
- log.info("Load faceDb success!");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
- }
- }
-
- @Override
- public FaceDetectedResult detect(String imagePath) throws Exception {
- // 将图片路径转换为 BufferedImage
- BufferedImage image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
- return convertToFaceDetectedResult(seetaResult);
- }
-
- @Override
- public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
- BufferedImage image = ImageIO.read(imageInputStream);
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
- return convertToFaceDetectedResult(seetaResult);
- }
-
- @Override
- public float[] featureExtraction(String imagePath) throws Exception {
- BufferedImage image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
- }
-
- @Override
- public float[] featureExtraction(InputStream inputStream) throws Exception {
- BufferedImage image = ImageIO.read(inputStream);
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
- }
-
- @Override
- public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
- return NativeLoader.seetaFace6SDK.calculateSimilarity(feature1, feature2);
- }
-
- @Override
- public float featureComparison(String imagePath1, String imagePath2) throws Exception {
- return featureComparison(new FileInputStream(Paths.get(imagePath1).toAbsolutePath().toString()),
- new FileInputStream(Paths.get(imagePath2).toAbsolutePath().toString()));
- }
-
- @Override
- public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
- BufferedImage image1 = ImageIO.read(inputStream1);
- BufferedImage image2 = ImageIO.read(inputStream2);
- SeetaImageData imageData1 = new SeetaImageData(image1.getWidth(), image1.getHeight(), 3);
- imageData1.data = ImageUtils.getMatrixBGR(image1);
-
- SeetaImageData imageData2 = new SeetaImageData(image2.getWidth(), image2.getHeight(), 3);
- imageData2.data = ImageUtils.getMatrixBGR(image2);
- //裁剪
- byte[][] cropImg1 = NativeLoader.seetaFace6SDK.crop(imageData1);
- byte[][] cropImg2 = NativeLoader.seetaFace6SDK.crop(imageData2);
- if(cropImg1 == null || cropImg1.length == 0){
- throw new Exception("未发现人脸");
- }
- if(cropImg2 == null || cropImg2.length == 0){
- throw new Exception("未发现人脸");
- }
-
- BufferedImage cropImage1 = ImageUtils.bgrToBufferedImage(cropImg1[0], 256, 256);
- BufferedImage cropImage2 = ImageUtils.bgrToBufferedImage(cropImg2[0], 256, 256);
-
- SeetaImageData cropImageData1 = new SeetaImageData(cropImage1.getWidth(), cropImage1.getHeight(), 3);
- cropImageData1.data = ImageUtils.getMatrixBGR(cropImage1);
- SeetaImageData cropImageData2 = new SeetaImageData(cropImage2.getWidth(), cropImage2.getHeight(), 3);
- cropImageData2.data = ImageUtils.getMatrixBGR(cropImage2);
- return NativeLoader.seetaFace6SDK.compare(cropImageData1, cropImageData2);
- }
-
-
- /**
- * 转换为FaceDetectedResult
- * @param seetaResult
- * @return
- */
- private FaceDetectedResult convertToFaceDetectedResult(SeetaRect[] seetaResult){
- FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
- List RectangleList = new ArrayList();
- List probabilities = new ArrayList();
- if(seetaResult != null && seetaResult.length > 0){
- for(SeetaRect rect : seetaResult){
- Rectangle rectangle = new Rectangle();
- List pointList = new ArrayList<>();
- pointList.add(new Point(rect.x,rect.y));
- pointList.add(new Point(rect.x + rect.width,rect.y));
- pointList.add(new Point(rect.x,rect.y + rect.height));
- pointList.add(new Point(rect.x + rect.width,rect.y + rect.height));
- rectangle.setPointList(pointList);
- rectangle.setHeight(rect.height);
- rectangle.setWidth(rect.width);
- RectangleList.add(rectangle);
- probabilities.add(new Double(rect.score));
- }
- }
- faceDetectedResult.setProbabilities(probabilities);
- faceDetectedResult.setRectangles(RectangleList);
- return faceDetectedResult;
- }
-
- @Override
- public boolean register(String key, String imagePath) throws Exception {
- return register(key, new FileInputStream(Paths.get(imagePath).toAbsolutePath().toString()));
- }
-
-
- @Override
- public boolean register(String key, InputStream inputStream) throws Exception {
- if(!checkFaceDb()){
- throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
- }
- //裁剪人脸
- BufferedImage image = ImageIO.read(inputStream);
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- byte[][] bytes = NativeLoader.seetaFace6SDK.crop(imageData);
- if (bytes == null || bytes.length == 0) {
- log.info("register face fail: key={}, error=no valid face", key);
- return false;
- }
- long index = NativeLoader.seetaFace6SDK.registerCroppedFace(bytes[0]);
- if (index < 0) {
- log.info("register face fail: key={}, index={}", key, index);
- return false;
- }
- //持久化到sqlite数据库
- FaceData face = new FaceData();
- face.setKey(key);
- face.setIndex(index);
- face.setImgData(bytes[0]);
- new FaceDao(config.getFaceDbPath()).save(face);
- return true;
- }
-
- public boolean register(String key, FaceData faceData) throws Exception {
- long index = NativeLoader.seetaFace6SDK.registerCroppedFace(faceData.getImgData());
- if (index < 0) {
- log.info("register face fail: key={}, index={}", key, index);
- return false;
- }
- int rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
- return rows > 0;
- }
-
-
-
- @Override
- public FaceResult search(String imagePath) throws Exception {
- return search(new FileInputStream(Paths.get(imagePath).toAbsolutePath().toString()));
- }
-
- @Override
- public FaceResult search(InputStream inputStream) throws Exception{
- if(!checkFaceDb()){
- throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
- }
- BufferedImage image = ImageIO.read(inputStream);
- SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
- imageData.data = ImageUtils.getMatrixBGR(image);
- RecognizeResult recognizeResult = NativeLoader.seetaFace6SDK.query(imageData);
- return searchFaceDb(recognizeResult);
-
- }
-
- @Override
- public long removeRegister(String... keys) throws Exception {
- if(!checkFaceDb()){
- throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
- }
- List list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
- if (list == null) {
- return 0;
- }
- long[] array = list.stream().mapToLong(Long::longValue).toArray();
- long rows = NativeLoader.seetaFace6SDK.delete(array);
- new FaceDao(config.getFaceDbPath()).deleteFace(keys);
- return rows;
- }
-
- @Override
- public long clearFace() throws Exception{
- if(!checkFaceDb()){
- throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
- }
- long rows = NativeLoader.seetaFace6SDK.delete(new long[]{-1});
- new FaceDao(config.getFaceDbPath()).deleteAll();
- return rows;
- }
-
- /**
- * 检查是否存在人脸库
- * @return
- */
- private boolean checkFaceDb(){
- if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
- File file = new File(config.getFaceDbPath());
- return file.exists() && file.isFile();
- }
- return false;
- }
-
- private FaceResult searchFaceDb(RecognizeResult recognizeResult) throws SQLException, ClassNotFoundException {
- if(recognizeResult != null && recognizeResult.index >= 0){
- String key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(recognizeResult.index);
- return new FaceResult(key, recognizeResult.similar);
- }
- return null;
- }
-
-
- /**
- * 加载人脸库
- * @throws SQLException
- * @throws ClassNotFoundException
- */
- private void loadFaceDb() throws SQLException, ClassNotFoundException {
- if(!checkFaceDb()){
- log.info("未配置人脸库");
- return;
- }
- //分页查询人脸库
- int pageNo = 0, pageSize = 100;
- while (true) {
- List list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
- if (list == null) {
- break;
- }
- list.forEach(face -> {
- try {
- register(face.getKey(), face);
- } catch (Exception e) {
- e.printStackTrace();
- }
- });
- if (list.size() < pageSize) {
- break;
- }
- pageNo++;
- }
- }
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java
deleted file mode 100644
index 1468654..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package cn.smartjavaai.face.algo;
-
-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.modality.cv.translator.ImageFeatureExtractorFactory;
-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.Point;
-import cn.smartjavaai.common.entity.Rectangle;
-import cn.smartjavaai.face.*;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * @author dwj
- */
-public class UltraLightFastGenericFace extends AbstractFaceAlgorithm {
-
-
- private Criteria criteria;
-
- /**
- * 特征图层的基础缩放比例
- */
- private static final int[][] scales = {{10, 16, 24}, {32, 48}, {64, 96}, {128, 192, 256}};
- /**
- * 特征图相对于原图的采样步长
- */
- private static final int[] steps = {8, 16, 32, 64};
- /**
- * 缩放系数
- */
- private static final double[] variance = {0.1f, 0.2f};
-
- private Predictor predictor;
-
- private ZooModel model;
-
-
-
-
- /**
- * 加载模型
- * @param config
- */
- @Override
- public void loadModel(ModelConfig config) throws ModelNotFoundException, MalformedModelException, IOException {
- FaceDetectionTranslator translator =
- new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, config.getMaxFaceCount(), scales, steps);
- criteria =
- Criteria.builder()
- .setTypes(Image.class, DetectedObjects.class)
- .optModelUrls("https://resources.djl.ai/test-models/pytorch/ultranet.zip")
- .optTranslator(translator)
- .optProgress(new ProgressBar())
- .optEngine("PyTorch") // Use PyTorch engine
- .build();
- model = criteria.loadModel();
- predictor = model.newPredictor();
- }
-
- /**
- * 检测人脸
- * @param imagePath 图片路径
- * @return
- * @throws Exception
- */
- @Override
- public FaceDetectedResult detect(String imagePath) throws Exception{
- Path facePath = Paths.get(imagePath);
- Image img = ImageFactory.getInstance().fromFile(facePath);
- DetectedObjects detection = predictor.predict(img);
- return convertToFaceDetectedResult(detection,img);
- }
-
- /**
- * 检测人脸
- * @param imageInputStream 图片输入流
- * @return
- * @throws Exception
- */
- @Override
- public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
- Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
- DetectedObjects detection = predictor.predict(img);
- return convertToFaceDetectedResult(detection,img);
- }
-
- /**
- * 转换检测结果
- * @param detection
- * @param img
- * @return
- */
- private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
- FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
- List probabilities = new ArrayList<>(detection.getProbabilities());
- List detectedObjectList = detection.items();
- List RectangleList = detectedObjectList.parallelStream()
- .map(obj -> {
- Rectangle rectangle = new Rectangle();
- List pointList = new ArrayList<>();
- ai.djl.modality.cv.output.Rectangle rectangleDjl = obj.getBoundingBox().getBounds();
- int x = (int)(rectangleDjl.getX() * (double)img.getWidth());
- int y = (int)(rectangleDjl.getY() * (double)img.getHeight());
- int width = (int)(rectangleDjl.getWidth() * (double)img.getWidth());
- int height = (int)(rectangleDjl.getHeight() * (double)img.getHeight());
- pointList.add(new Point(x,y));
- pointList.add(new Point(x + width,y));
- pointList.add(new Point(x,y + height));
- pointList.add(new Point(x + width,y + height));
- rectangle.setPointList(pointList);
- rectangle.setHeight(height);
- rectangle.setWidth(width);
- return rectangle;
- })
- .collect(Collectors.toList());
- faceDetectedResult.setProbabilities(probabilities);
- faceDetectedResult.setRectangles(RectangleList);
- return faceDetectedResult;
- }
-
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/exception/FaceException.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/exception/FaceException.java
new file mode 100644
index 0000000..8fcedab
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/exception/FaceException.java
@@ -0,0 +1,30 @@
+package cn.smartjavaai.face.exception;
+
+/**
+ * 人脸检测异常
+ * @author dwj
+ * @date 2025/4/4
+ */
+public class FaceException extends RuntimeException{
+
+ public FaceException() {
+ super();
+ }
+
+ public FaceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ public FaceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public FaceException(String message) {
+ super(message);
+ }
+
+ public FaceException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/SeetaFace6Factory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/SeetaFace6Factory.java
new file mode 100644
index 0000000..5d56f85
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/SeetaFace6Factory.java
@@ -0,0 +1,34 @@
+package cn.smartjavaai.face.factory;
+
+import ai.djl.inference.Predictor;
+import ai.djl.repository.zoo.ZooModel;
+import cn.smartjavaai.face.model.SeetaFace6Model;
+import com.seetaface.SeetaFace6JNI;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+
+/**
+ * Predictor 工厂类
+ * @author dwj
+ * @date 2025/4/8
+ */
+public class SeetaFace6Factory extends BasePooledObjectFactory {
+
+ @Override
+ public SeetaFace6JNI create() {
+ return new SeetaFace6JNI();
+ }
+
+ @Override
+ public PooledObject wrap(SeetaFace6JNI obj) {
+ return new DefaultPooledObject<>(obj);
+ }
+
+ @Override
+ public void destroyObject(PooledObject p) {
+ //p.getObject().dispose(); // 如果需要释放 native 资源
+ SeetaFace6JNI object = p.getObject();
+ object = null;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java
new file mode 100644
index 0000000..af377c1
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/FeatureExtractionModel.java
@@ -0,0 +1,259 @@
+package cn.smartjavaai.face.model;
+
+import ai.djl.Device;
+import ai.djl.MalformedModelException;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+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.enums.DeviceEnum;
+import cn.smartjavaai.common.pool.PredictorFactory;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.face.AbstractFaceModel;
+import cn.smartjavaai.face.FaceModelConfig;
+import cn.smartjavaai.face.exception.FaceException;
+import cn.smartjavaai.face.translator.FaceFeatureTranslator;
+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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author dwj
+ */
+@Slf4j
+public class FeatureExtractionModel extends AbstractFaceModel implements AutoCloseable{
+
+
+ private ObjectPool> predictorPool;
+
+
+ private ZooModel model;
+
+ public static final List mean =
+ Arrays.asList(
+ 127.5f / 255.0f,
+ 127.5f / 255.0f,
+ 127.5f / 255.0f,
+ 128.0f / 255.0f,
+ 128.0f / 255.0f,
+ 128.0f / 255.0f);
+
+
+ /**
+ * 加载人脸特征提取模型
+ * @param config
+ */
+ @Override
+ public void loadModel(FaceModelConfig config) {
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
+ Criteria faceFeatureCriteria =
+ Criteria.builder()
+ .setTypes(Image.class, float[].class)
+ .optModelName("face_feature") // specify model file prefix
+ .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
+ "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
+ .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
+ .optTranslator(new FaceFeatureTranslator())
+ .optArgument("normalize", normalize)
+ .optDevice(device)
+ .optEngine("PyTorch") // Use PyTorch engine
+ .optProgress(new ProgressBar())
+ .build();
+
+ try {
+ model = faceFeatureCriteria.loadModel();
+ // 创建池子:每个线程独享 Predictor
+ this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
+ log.info("当前设备: " + model.getNDManager().getDevice());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new FaceException("模型加载失败", e);
+ }
+ }
+
+ private float[] featureExtraction(Image image){
+ image.getWrappedImage();
+ Predictor predictor = null;
+ try {
+ predictor = predictorPool.borrowObject();
+ return predictor.predict(image);
+ } catch (Exception e) {
+ throw new FaceException("目标检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ predictorPool.returnObject(predictor); //归还
+ log.info("释放资源");
+ } catch (Exception e) {
+ log.warn("归还Predictor失败", e);
+ try {
+ predictor.close(); // 归还失败才销毁
+ } catch (Exception ex) {
+ log.error("关闭Predictor失败", ex);
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 特征提取
+ * @param imagePath 图片路径
+ * @return
+ */
+ @Override
+ public float[] featureExtraction(String imagePath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ Image img = null;
+ try {
+ img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ } catch (IOException e) {
+ throw new FaceException("无效图片", e);
+ }
+ return featureExtraction(img);
+ }
+
+ /**
+ * 特征提取
+ * @param inputStream 输入流
+ * @return
+ */
+ @Override
+ public float[] featureExtraction(InputStream inputStream) {
+ if(Objects.isNull(inputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ Image img = null;
+ try {
+ img = ImageFactory.getInstance().fromInputStream(inputStream);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+ return featureExtraction(img);
+ }
+
+ @Override
+ public float[] featureExtraction(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(sourceImage);
+ return featureExtraction(img);
+ }
+
+ @Override
+ public float[] featureExtraction(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new FaceException("图像无效");
+ }
+ try {
+ return featureExtraction(ImageIO.read(new ByteArrayInputStream(imageData)));
+ } catch (IOException e) {
+ throw new FaceException("无效图片字节流", e);
+ }
+ }
+
+ /**
+ * 计算相似度
+ * @param feature1 图1特征
+ * @param feature2 图2特征
+ * @return
+ */
+ @Override
+ public float calculSimilar(float[] feature1, float[] feature2) {
+ float ret = 0.0f;
+ float mod1 = 0.0f;
+ float mod2 = 0.0f;
+ int length = feature1.length;
+ for (int i = 0; i < length; ++i) {
+ ret += feature1[i] * feature2[i];
+ mod1 += feature1[i] * feature1[i];
+ mod2 += feature2[i] * feature2[i];
+ }
+ return (float) ((ret / Math.sqrt(mod1) / Math.sqrt(mod2) + 1) / 2.0f);
+ }
+
+ /**
+ * 特征比较
+ * @param imagePath1 图1路径
+ * @param imagePath2 图2路径
+ * @return
+ */
+ @Override
+ public float featureComparison(String imagePath1, String imagePath2) {
+ if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
+ throw new FaceException("图像文件不存在");
+ }
+ float[] feature1 = featureExtraction(imagePath1);
+ float[] feature2 = featureExtraction(imagePath2);
+ return calculSimilar(feature1, feature2);
+ }
+
+ /**
+ * 特征比较
+ * @param inputStream1 图1输入流
+ * @param inputStream2 图2输入流
+ * @return
+ */
+ @Override
+ public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
+ if(Objects.isNull(inputStream1) || Objects.isNull(inputStream2)){
+ throw new FaceException("图像输入流无效");
+ }
+ float[] feature1 = featureExtraction(inputStream1);
+ float[] feature2 = featureExtraction(inputStream2);
+ return calculSimilar(feature1, feature2);
+ }
+
+ @Override
+ public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
+ if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
+ throw new FaceException("图像无效");
+ }
+ float[] feature1 = featureExtraction(sourceImage1);
+ float[] feature2 = featureExtraction(sourceImag2);
+ return calculSimilar(feature1, feature2);
+ }
+
+ @Override
+ public float featureComparison(byte[] imageData1, byte[] imageData2) {
+ if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
+ throw new FaceException("图像无效");
+ }
+ float[] feature1 = featureExtraction(imageData1);
+ float[] feature2 = featureExtraction(imageData2);
+ return calculSimilar(feature1, feature2);
+ }
+
+ @Override
+ public void close() {
+ if (predictorPool != null) {
+ predictorPool.close();
+ }
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java
new file mode 100644
index 0000000..f98dfea
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/RetinaFaceModel.java
@@ -0,0 +1,243 @@
+package cn.smartjavaai.face.model;
+
+import ai.djl.Device;
+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.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.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.exception.FaceException;
+import cn.smartjavaai.face.translator.FaceDetectionTranslator;
+import cn.smartjavaai.face.utils.FaceUtils;
+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.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * RetinaFace实现
+ * @author dwj
+ */
+@Slf4j
+public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
+
+
+ private ObjectPool> predictorPool;
+
+ private ZooModel model;
+
+ /**
+ * 特征图层的基础缩放比例
+ */
+ public static final int[][] scales = {{16, 32}, {64, 128}, {256, 512}};
+ /**
+ * 特征图相对于原图的采样步长
+ */
+ public static final int[] steps = {8, 16, 32};
+ /**
+ * 缩放系数
+ */
+ public static final double[] variance = {0.1f, 0.2f};
+
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ @Override
+ public void loadModel(FaceModelConfig config){
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ FaceDetectionTranslator translator =
+ new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, DetectedObjects.class)
+ .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/retinaface.zip")
+ // Load model from local file, e.g:
+ .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
+ .optModelName("retinaface") // specify model file prefix
+ .optTranslator(translator)
+ .optDevice(device)
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+ try {
+ model = criteria.loadModel();
+ // 创建池子:每个线程独享 Predictor
+ this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
+ log.info("当前设备: " + model.getNDManager().getDevice());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new FaceException("模型加载失败", e);
+ }
+ }
+
+
+
+ /**
+ * 检测人脸
+ * @param imagePath 图片路径
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public DetectionResponse detect(String imagePath){
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ Image img = null;
+ try {
+ img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ } catch (IOException e) {
+ throw new FaceException("无效的图片", e);
+ }
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ }
+
+ /**
+ * 检测人脸
+ * @param imageInputStream 图片流
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public DetectionResponse detect(InputStream imageInputStream){
+ if(Objects.isNull(imageInputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ try {
+ Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+
+ }
+
+ @Override
+ public DetectionResponse detect(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new FaceException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(image);
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ }
+
+ @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 void detectAndDraw(String imagePath, String outputPath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ try {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ DetectedObjects detectedObjects = detect(img);
+ if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
+ throw new FaceException("未识别到人脸");
+ }
+ img.drawBoundingBoxes(detectedObjects);
+ Path output = Paths.get(outputPath);
+ log.info("Saving to {}", output.toAbsolutePath().toString());
+ img.save(Files.newOutputStream(output), "png");
+ } catch (IOException e) {
+ throw new FaceException(e);
+ }
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(sourceImage);
+ DetectedObjects detectedObjects = detect(img);
+ if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
+ throw new FaceException("未识别到人脸");
+ }
+ img.drawBoundingBoxes(detectedObjects);
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new FaceException("导出图片失败", e);
+ }
+ }
+
+ /**
+ * 人脸检测
+ * @param image
+ * @return
+ */
+ private DetectedObjects detect(Image image){
+ Predictor predictor = null;
+ try {
+ predictor = predictorPool.borrowObject();
+ return predictor.predict(image);
+ } catch (Exception e) {
+ throw new FaceException("目标检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ predictorPool.returnObject(predictor); //归还
+ log.info("释放资源");
+ } catch (Exception e) {
+ log.warn("归还Predictor失败", e);
+ try {
+ predictor.close(); // 归还失败才销毁
+ } catch (Exception ex) {
+ log.error("关闭Predictor失败", ex);
+ }
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public void close() {
+ if (predictorPool != null) {
+ predictorPool.close();
+ }
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java
new file mode 100644
index 0000000..1030f2a
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/SeetaFace6Model.java
@@ -0,0 +1,585 @@
+package cn.smartjavaai.face.model;
+
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.face.AbstractFaceModel;
+import cn.smartjavaai.face.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.FaceUtils;
+import com.seetaface.NativeLoader;
+import com.seetaface.SeetaFace6JNI;
+import com.seetaface.model.RecognizeResult;
+import com.seetaface.model.SeetaImageData;
+import com.seetaface.model.SeetaRect;
+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.Path;
+import java.nio.file.Paths;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * SeetaFace6 人脸算法
+ * @author dwj
+ */
+@SuppressWarnings("AliMissingOverrideAnnotation")
+@Slf4j
+public class SeetaFace6Model extends AbstractFaceModel {
+
+
+ private FaceModelConfig config;
+
+ private static final Object lock = new Object(); // 全局锁
+
+
+ @Override
+ public void loadModel(FaceModelConfig config) {
+ this.config = config;
+ if (NativeLoader.seetaFace6SDK == null) {
+ synchronized (lock) {
+ if(StringUtils.isBlank(config.getModelPath())){
+ throw new FaceException("modelPath is null");
+ }
+ //加载依赖库
+ NativeLoader.loadNativeLibraries(config.getModelPath());
+ log.info("Loading seetaFace6 library successfully.");
+ NativeLoader.seetaFace6SDK = new SeetaFace6JNI();
+ //加载模型
+ boolean isSuccess = NativeLoader.seetaFace6SDK.initModel(config.getModelPath());
+ if(!isSuccess){
+ throw new FaceException("seetaFace6模型初始化失败," + config.getModelPath());
+ }
+ log.info("Load seetaFace6 model success!");
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ log.info("start load faceDb...");
+ loadFaceDb();
+ log.info("Load faceDb success!");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ }
+ }
+ }
+
+ @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(InputStream imageInputStream) {
+ if(Objects.isNull(imageInputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(imageInputStream);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+ return detect(image);
+ }
+
+ @Override
+ public DetectionResponse detect(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new FaceException("图像无效");
+ }
+ SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
+ imageData.data = ImageUtils.getMatrixBGR(image);
+ synchronized (lock) {
+ SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
+ return FaceUtils.convertToDetectionResponse(seetaResult, config);
+ }
+ }
+
+ @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 void detectAndDraw(String imagePath, String outputPath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ try {
+ //创建保存路径
+ Path imageOutputPath = Paths.get(outputPath);
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
+ } catch (IOException e) {
+ throw new FaceException("无效图片路径", e);
+ }
+ DetectionResponse result = detect(image);
+ if(Objects.isNull(result) || Objects.isNull(result.getRectangleList()) || result.getRectangleList().isEmpty()){
+ throw new FaceException("未识别到人脸");
+ }
+ //绘制人脸框
+ FaceUtils.drawBoundingBoxes(image, result, imageOutputPath.toAbsolutePath().toString());
+ } catch (IOException e) {
+ throw new FaceException(e);
+ }
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ DetectionResponse detectedObjects = detect(sourceImage);
+ if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getRectangleList()) || detectedObjects.getRectangleList().isEmpty()){
+ throw new FaceException("未识别到人脸");
+ }
+ //绘制人脸框
+ try {
+ return FaceUtils.drawBoundingBoxes(sourceImage, detectedObjects);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public float[] featureExtraction(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new FaceException("图像无效");
+ }
+ SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
+ imageData.data = ImageUtils.getMatrixBGR(image);
+ synchronized (lock) {
+ return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
+ }
+
+ }
+
+ @Override
+ public float[] featureExtraction(String imagePath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
+ } catch (IOException e) {
+ throw new FaceException("无效图片路径", e);
+ }
+ return featureExtraction(image);
+ }
+
+ @Override
+ public float[] featureExtraction(InputStream inputStream) {
+ if(Objects.isNull(inputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(inputStream);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+ return featureExtraction(image);
+ }
+
+ @Override
+ public float[] featureExtraction(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new FaceException("图像无效");
+ }
+ try {
+ return featureExtraction(ImageIO.read(new ByteArrayInputStream(imageData)));
+ } catch (IOException e) {
+ throw new FaceException("错误的图像", e);
+ }
+ }
+
+ @Override
+ public float calculSimilar(float[] feature1, float[] feature2) {
+ if(Objects.isNull(feature1) || Objects.isNull(feature2)){
+ throw new FaceException("特征向量无效");
+ }
+ synchronized (lock) {
+ return NativeLoader.seetaFace6SDK.calculateSimilarity(feature1, feature2);
+ }
+ }
+
+ @Override
+ public float featureComparison(String imagePath1, String imagePath2) {
+ if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
+ throw new FaceException("图像文件不存在");
+ }
+ BufferedImage image1 = null;
+ BufferedImage image2 = null;
+ try {
+ image1 = ImageIO.read(new File(Paths.get(imagePath1).toAbsolutePath().toString()));
+ image2 = ImageIO.read(new File(Paths.get(imagePath2).toAbsolutePath().toString()));
+ } catch (IOException e) {
+ throw new FaceException("无效图片路径", e);
+ }
+ return featureComparison(image1, image2);
+ }
+
+ @Override
+ public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
+ if(Objects.isNull(inputStream1) || Objects.isNull(inputStream2)){
+ throw new FaceException("图像输入流无效");
+ }
+ BufferedImage image1 = null;
+ BufferedImage image2 = null;
+ try {
+ image1 = ImageIO.read(inputStream1);
+ image2 = ImageIO.read(inputStream2);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+ return featureComparison(image1, image2);
+ }
+
+
+ @Override
+ public float featureComparison(BufferedImage image1, BufferedImage image2) {
+ if(!ImageUtils.isImageValid(image1) || !ImageUtils.isImageValid(image2)){
+ throw new FaceException("图像无效");
+ }
+ SeetaImageData imageData1 = new SeetaImageData(image1.getWidth(), image1.getHeight(), 3);
+ imageData1.data = ImageUtils.getMatrixBGR(image1);
+
+ SeetaImageData imageData2 = new SeetaImageData(image2.getWidth(), image2.getHeight(), 3);
+ imageData2.data = ImageUtils.getMatrixBGR(image2);
+ synchronized (lock) {
+ //裁剪
+ byte[][] cropImg1 = NativeLoader.seetaFace6SDK.crop(imageData1);
+ byte[][] cropImg2 = NativeLoader.seetaFace6SDK.crop(imageData2);
+ if(cropImg1 == null || cropImg1.length == 0){
+ throw new FaceException("未发现人脸");
+ }
+ if(cropImg2 == null || cropImg2.length == 0){
+ throw new FaceException("未发现人脸");
+ }
+ BufferedImage cropImage1 = ImageUtils.bgrToBufferedImage(cropImg1[0], 256, 256);
+ BufferedImage cropImage2 = ImageUtils.bgrToBufferedImage(cropImg2[0], 256, 256);
+
+ SeetaImageData cropImageData1 = new SeetaImageData(cropImage1.getWidth(), cropImage1.getHeight(), 3);
+ cropImageData1.data = ImageUtils.getMatrixBGR(cropImage1);
+ SeetaImageData cropImageData2 = new SeetaImageData(cropImage2.getWidth(), cropImage2.getHeight(), 3);
+ cropImageData2.data = ImageUtils.getMatrixBGR(cropImage2);
+ return NativeLoader.seetaFace6SDK.compare(cropImageData1, cropImageData2);
+ }
+ }
+
+ @Override
+ public float featureComparison(byte[] imageData1, byte[] imageData2) {
+ if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
+ throw new FaceException("图像无效");
+ }
+ BufferedImage image1 = null;
+ BufferedImage image2 = null;
+ try {
+ image1 = ImageIO.read(new ByteArrayInputStream(imageData1));
+ image2 = ImageIO.read(new ByteArrayInputStream(imageData2));
+ } catch (IOException e) {
+ throw new FaceException("无效图片", e);
+ }
+ return featureComparison(image1, image2);
+ }
+
+ @Override
+ public boolean register(String key, String imagePath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ BufferedImage bufferedImage = null;
+ try {
+ bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
+ } catch (IOException e) {
+ throw new FaceException("无效图片路径", e);
+ }
+ return register(key, bufferedImage);
+ }
+
+
+ @Override
+ public boolean register(String key, BufferedImage image) {
+ if(!checkFaceDb()){
+ throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
+ }
+ if(!ImageUtils.isImageValid(image)){
+ throw new FaceException("图像无效");
+ }
+ SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
+ imageData.data = ImageUtils.getMatrixBGR(image);
+ synchronized (lock) {
+ byte[][] bytes = NativeLoader.seetaFace6SDK.crop(imageData);
+ if (bytes == null || bytes.length == 0) {
+ log.info("register face fail: key={}, error=no valid face", key);
+ return false;
+ }
+ long index = NativeLoader.seetaFace6SDK.registerCroppedFace(bytes[0]);
+ if (index < 0) {
+ log.info("register face fail: key={}, index={}", key, index);
+ return false;
+ }
+ //持久化到sqlite数据库
+ FaceData face = new FaceData();
+ face.setKey(key);
+ face.setIndex(index);
+ face.setImgData(bytes[0]);
+ try {
+ new FaceDao(config.getFaceDbPath()).save(face);
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException("保存人脸库失败", e);
+ }
+ return true;
+ }
+ }
+
+ @Override
+ public boolean register(String key, byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new FaceException("图像无效");
+ }
+ BufferedImage bufferedImage = null;
+ try {
+ bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
+ } catch (IOException e) {
+ throw new FaceException(e);
+ }
+ return register(key, bufferedImage);
+ }
+
+ @Override
+ public boolean register(String key, InputStream inputStream) {
+ if(!checkFaceDb()){
+ throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
+ }
+ if(Objects.isNull(inputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(inputStream);
+ } catch (IOException e) {
+ throw new FaceException("无效的图片输入流", e);
+ }
+ return register(key, image);
+ }
+
+ /**
+ * 注册已裁剪后人脸
+ * @param key
+ * @param faceData
+ * @return
+ */
+ private boolean register(String key, FaceData faceData) {
+ synchronized (lock) {
+ long index = NativeLoader.seetaFace6SDK.registerCroppedFace(faceData.getImgData());
+ if (index < 0) {
+ log.info("register face fail: key={}, index={}", key, index);
+ return false;
+ }
+ int rows = 0;
+ try {
+ rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException(e);
+ }
+ return rows > 0;
+ }
+ }
+
+
+
+ @Override
+ public FaceResult search(String imagePath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ BufferedImage bufferedImage = null;
+ try {
+ bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
+ } catch (IOException e) {
+ throw new FaceException("无效图片路径", e);
+ }
+ return search(bufferedImage);
+ }
+
+ @Override
+ public FaceResult search(InputStream inputStream) {
+ if(Objects.isNull(inputStream)){
+ throw new FaceException("图像输入流无效");
+ }
+ if(!checkFaceDb()){
+ throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
+ }
+ BufferedImage image = null;
+ try {
+ image = ImageIO.read(inputStream);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+ return search(image);
+ }
+
+ @Override
+ public FaceResult search(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new FaceException("图像无效");
+ }
+ SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
+ imageData.data = ImageUtils.getMatrixBGR(image);
+ synchronized (lock) {
+ RecognizeResult recognizeResult = NativeLoader.seetaFace6SDK.query(imageData);
+ return searchFaceDb(recognizeResult);
+ }
+ }
+
+ @Override
+ public FaceResult search(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new FaceException("图像无效");
+ }
+ BufferedImage bufferedImage = null;
+ try {
+ bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
+ } catch (IOException e) {
+ throw new FaceException(e);
+ }
+ return search(bufferedImage);
+ }
+
+ @Override
+ public long removeRegister(String... keys) {
+ if(keys == null || keys.length == 0){
+ throw new FaceException("keys不允许为空");
+ }
+ if(!checkFaceDb()){
+ throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
+ }
+ synchronized (lock) {
+ try {
+ List list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
+ if (list == null) {
+ return 0;
+ }
+ long[] array = list.stream().mapToLong(Long::longValue).toArray();
+ long rows = NativeLoader.seetaFace6SDK.delete(array);
+ new FaceDao(config.getFaceDbPath()).deleteFace(keys);
+ return rows;
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException(e);
+ }
+ }
+
+ }
+
+ @Override
+ public long clearFace(){
+ if(!checkFaceDb()){
+ throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
+ }
+ synchronized (lock) {
+ long rows = NativeLoader.seetaFace6SDK.delete(new long[]{-1});
+ try {
+ new FaceDao(config.getFaceDbPath()).deleteAll();
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException("删除人脸库失败", e);
+ }
+ return rows;
+ }
+
+ }
+
+ /**
+ * 检查是否存在人脸库
+ * @return
+ */
+ private boolean checkFaceDb(){
+ if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
+ File file = new File(config.getFaceDbPath());
+ return file.exists() && file.isFile();
+ }
+ return false;
+ }
+
+ private FaceResult searchFaceDb(RecognizeResult recognizeResult) {
+ if(recognizeResult != null && recognizeResult.index >= 0){
+ String key = null;
+ synchronized (lock) {
+ try {
+ key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(recognizeResult.index);
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException("查询人脸库失败", e);
+ }
+ return new FaceResult(key, recognizeResult.similar);
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * 加载人脸库
+ * @throws SQLException
+ * @throws ClassNotFoundException
+ */
+ private void loadFaceDb() {
+ if(!checkFaceDb()){
+ log.info("未配置人脸库");
+ return;
+ }
+ //分页查询人脸库
+ int pageNo = 0, pageSize = 100;
+ while (true) {
+ List list = null;
+ try {
+ list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
+ } catch (SQLException | ClassNotFoundException e) {
+ throw new FaceException("查询人脸库失败", e);
+ }
+ if (list == null) {
+ break;
+ }
+ list.forEach(face -> {
+ try {
+ register(face.getKey(), face);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ if (list.size() < pageSize) {
+ break;
+ }
+ pageNo++;
+ }
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java
new file mode 100644
index 0000000..4c83a6a
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/UltraLightFastGenericFaceModel.java
@@ -0,0 +1,232 @@
+package cn.smartjavaai.face.model;
+
+import ai.djl.Device;
+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.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.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.exception.FaceException;
+import cn.smartjavaai.face.translator.FaceDetectionTranslator;
+import cn.smartjavaai.face.utils.FaceUtils;
+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;
+
+/**
+ * @author dwj
+ */
+@Slf4j
+public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements AutoCloseable{
+
+
+ private ObjectPool> predictorPool;
+
+ /**
+ * 特征图层的基础缩放比例
+ */
+ private static final int[][] scales = {{10, 16, 24}, {32, 48}, {64, 96}, {128, 192, 256}};
+ /**
+ * 特征图相对于原图的采样步长
+ */
+ private static final int[] steps = {8, 16, 32, 64};
+ /**
+ * 缩放系数
+ */
+ private static final double[] variance = {0.1f, 0.2f};
+
+
+ private ZooModel model;
+
+
+
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ @Override
+ public void loadModel(FaceModelConfig config) {
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ FaceDetectionTranslator translator =
+ new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, DetectedObjects.class)
+ .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/ultranet.zip")
+ .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
+ .optTranslator(translator)
+ .optProgress(new ProgressBar())
+ .optDevice(device)
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+ try {
+ model = criteria.loadModel();
+ // 创建池子:每个线程独享 Predictor
+ this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
+ log.info("当前设备: " + model.getNDManager().getDevice());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new FaceException("模型加载失败", e);
+ }
+ }
+
+ /**
+ * 检测人脸
+ * @param imagePath 图片路径
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public DetectionResponse detect(String imagePath){
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ Image img = null;
+ try {
+ img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ } catch (IOException e) {
+ throw new FaceException("无效的图片", e);
+ }
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ }
+
+ /**
+ * 检测人脸
+ * @param imageInputStream 图片流
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public DetectionResponse detect(InputStream imageInputStream){
+ try {
+ Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ } catch (IOException e) {
+ throw new FaceException("无效图片输入流", e);
+ }
+
+ }
+
+ @Override
+ public DetectionResponse detect(BufferedImage image) {
+ Image img = ImageFactory.getInstance().fromImage(image);
+ DetectedObjects detection = detect(img);
+ return FaceUtils.convertToDetectionResponse(detection,img);
+ }
+
+ @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 void detectAndDraw(String imagePath, String outputPath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new FaceException("图像文件不存在");
+ }
+ try {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ DetectedObjects detectedObjects = detect(img);
+ if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
+ throw new FaceException("未识别到人脸");
+ }
+ img.drawBoundingBoxes(detectedObjects);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(new FileOutputStream(Paths.get(outputPath).toAbsolutePath().toString()), "png");
+ } catch (IOException e) {
+ throw new FaceException(e);
+ }
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(sourceImage);
+ DetectedObjects detectedObjects = detect(img);
+ if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
+ throw new FaceException("未识别到人脸");
+ }
+ img.drawBoundingBoxes(detectedObjects);
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new FaceException("导出图片失败", e);
+ }
+ }
+
+ /**
+ * 人脸检测
+ * @param image
+ * @return
+ */
+ private DetectedObjects detect(Image image){
+ Predictor predictor = null;
+ try {
+ predictor = predictorPool.borrowObject();
+ return predictor.predict(image);
+ } catch (Exception e) {
+ throw new FaceException("目标检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ predictorPool.returnObject(predictor); //归还
+ log.info("释放资源");
+ } catch (Exception e) {
+ log.warn("归还Predictor失败", e);
+ try {
+ predictor.close(); // 归还失败才销毁
+ } catch (Exception ex) {
+ log.error("关闭Predictor失败", ex);
+ }
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public void close() {
+ if (predictorPool != null) {
+ predictorPool.close();
+ }
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceDetectionTranslator.java
similarity index 99%
rename from smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java
rename to smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceDetectionTranslator.java
index f70b567..341524e 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceDetectionTranslator.java
@@ -10,7 +10,7 @@
* OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
-package cn.smartjavaai.face;
+package cn.smartjavaai.face.translator;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.*;
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 ec2d430..99a4628 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,7 +12,7 @@ import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
/**
- * @author 邓文杰
+ * @author dwj
* @date 2025/3/31
*/
public final class FaceFeatureTranslator implements Translator {
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
new file mode 100644
index 0000000..692a2d0
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
@@ -0,0 +1,167 @@
+package cn.smartjavaai.face.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.DetectionResponse;
+import cn.smartjavaai.common.entity.Point;
+import cn.smartjavaai.common.entity.DetectionRectangle;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.face.FaceModelConfig;
+import cn.smartjavaai.face.exception.FaceException;
+import com.seetaface.model.SeetaRect;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 人脸检测相关工具类
+ * @author dwj
+ * @date 2025/4/9
+ */
+public class FaceUtils {
+
+
+ /**
+ * 转换为FaceDetectedResult
+ * @param detection
+ * @param img
+ * @return
+ */
+ public static DetectionResponse convertToDetectionResponse(DetectedObjects detection, Image img){
+ if(Objects.isNull(detection) || Objects.isNull(detection.getProbabilities())
+ || detection.getProbabilities().isEmpty() || Objects.isNull(detection.items()) || detection.items().isEmpty()){
+ return null;
+ }
+ DetectionResponse detectionResponse = new DetectionResponse();
+ List detectedObjectList = detection.items();
+ List rectangleList = new ArrayList();
+ Iterator iterator = detectedObjectList.iterator();
+ int index = 0;
+ while(iterator.hasNext()) {
+ DetectedObjects.DetectedObject result = (DetectedObjects.DetectedObject)iterator.next();
+ BoundingBox box = result.getBoundingBox();
+ int x = (int)(box.getBounds().getX() * (double)img.getWidth());
+ 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());
+ rectangleList.add(rectangle);
+ index++;
+ }
+ detectionResponse.setRectangleList(rectangleList);
+ return detectionResponse;
+ }
+
+ /**
+ * 转换为FaceDetectedResult
+ * @param seetaResult
+ * @return
+ */
+ public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, FaceModelConfig config){
+ if(Objects.isNull(seetaResult) || seetaResult.length == 0){
+ return null;
+ }
+ DetectionResponse detectionResponse = new DetectionResponse();
+ List rectangleList = new ArrayList();
+ for(SeetaRect rect : seetaResult){
+ //过滤置信度
+ if(config.getConfidenceThreshold() > 0 && rect.score < config.getConfidenceThreshold()){
+ continue;
+ }
+ DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height, rect.score);
+ rectangleList.add(rectangle);
+ }
+ detectionResponse.setRectangleList(rectangleList);
+ return detectionResponse;
+ }
+
+ /**
+ * 绘制人脸框
+ * @param sourceImage
+ * @param detectionResponse
+ * @param savePath
+ * @throws IOException
+ */
+ public static void drawBoundingBoxes(BufferedImage sourceImage, DetectionResponse detectionResponse, String savePath) throws IOException {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().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(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
+ graphics.setColor(Color.RED);// 边框颜色
+ graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
+ drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
+ }
+ graphics.dispose();
+ ImageIO.write(sourceImage, "jpg", new File(savePath));
+ }
+
+ /**
+ * 绘制人脸框
+ * @param sourceImage
+ * @param detectionResponse
+ * @throws IOException
+ */
+ public static BufferedImage drawBoundingBoxes(BufferedImage sourceImage, DetectionResponse detectionResponse) throws IOException {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new FaceException("图像无效");
+ }
+ if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().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(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
+ graphics.setColor(Color.RED);// 边框颜色
+ graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
+ drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
+ }
+ graphics.dispose();
+ return sourceImage;
+ }
+
+ /**
+ * 绘制文字
+ * @param g
+ * @param text
+ * @param x
+ * @param y
+ * @param stroke
+ * @param padding
+ */
+ private static void drawText(Graphics2D g, String text, int x, int y, int stroke, int padding) {
+ FontMetrics metrics = g.getFontMetrics();
+ x += stroke / 2;
+ y += stroke / 2;
+ int width = metrics.stringWidth(text) + padding * 2 - stroke / 2;
+ int height = metrics.getHeight() + metrics.getDescent();
+ int ascent = metrics.getAscent();
+ java.awt.Rectangle background = new java.awt.Rectangle(x, y, width, height);
+ g.fill(background);
+ g.setPaint(Color.WHITE);
+ g.drawString(text, x + padding, y + ascent);
+ }
+
+
+}
diff --git a/smartjavaai-face/src/main/java/com/seetaface/SeetaFace6JNI.java b/smartjavaai-face/src/main/java/com/seetaface/SeetaFace6JNI.java
index e3cfd6d..935eb93 100644
--- a/smartjavaai-face/src/main/java/com/seetaface/SeetaFace6JNI.java
+++ b/smartjavaai-face/src/main/java/com/seetaface/SeetaFace6JNI.java
@@ -124,4 +124,12 @@ public class SeetaFace6JNI {
*/
public native int predictImage(SeetaImageData img);
+ public native void dispose();
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ this.dispose();
+ }
+
}
diff --git a/smartjavaai-objectdetection/pom.xml b/smartjavaai-objectdetection/pom.xml
new file mode 100644
index 0000000..bc29850
--- /dev/null
+++ b/smartjavaai-objectdetection/pom.xml
@@ -0,0 +1,137 @@
+
+
+ 4.0.0
+
+ ink.numberone
+ smartjavaai-parent
+ 1.0.8
+
+
+ smartjavaai-objectdetection
+ 1.0.8
+ smartjavaai-objectdetection
+ SmartJavaAI
+ https://github.com/geekwenjie/SmartJavaAI
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ 11
+ 11
+ UTF-8
+ true
+
+
+
+
+ ink.numberone
+ smartjavaai-common
+ ${project.version}
+
+
+
+
+
+
+
+
+
+ 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-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorConfig.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorConfig.java
new file mode 100644
index 0000000..a7502ac
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorConfig.java
@@ -0,0 +1,14 @@
+package cn.smartjavaai.objectdetection;
+
+/**
+ * @author dwj
+ * @date 2025/4/7
+ */
+public class DetectorConfig {
+
+ /**
+ * 置信度阈值
+ */
+ public static final float DEFAULT_THRESHOLD = 0.5F;
+
+}
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelConfig.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelConfig.java
new file mode 100644
index 0000000..c69d8c0
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelConfig.java
@@ -0,0 +1,29 @@
+package cn.smartjavaai.objectdetection;
+
+import cn.smartjavaai.common.enums.DeviceEnum;
+import lombok.Data;
+
+/**
+ * 目标检测模型参数配置
+ *
+ * @author dwj
+ * @date 2025/4/4
+ */
+@Data
+public class DetectorModelConfig {
+
+ /**
+ * 模型名称
+ */
+ private DetectorModelEnum modelEnum;
+
+ /**
+ * 置信度阈值
+ */
+ private float threshold = DetectorConfig.DEFAULT_THRESHOLD;
+
+ /**
+ * 设备类型
+ */
+ private DeviceEnum device;
+}
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelEnum.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelEnum.java
new file mode 100644
index 0000000..8a8d15a
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/DetectorModelEnum.java
@@ -0,0 +1,62 @@
+package cn.smartjavaai.objectdetection;
+
+/**
+ * 目标检测模型枚举
+ * @author dwj
+ * @date 2025/4/4
+ */
+public enum DetectorModelEnum {
+
+ // resnet50 系列
+ SSD_300_RESNET50("ai.djl.pytorch/ssd/0.0.1/ssd_300_resnet50"),
+ SSD_512_RESNET50_V1_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_512_resnet50_v1_voc"),
+
+ // vgg16 系列
+ SSD_512_VGG16_ATROUS_COCO("ai.djl.mxnet/ssd/0.0.1/ssd_512_vgg16_atrous_coco"),
+ SSD_300_VGG16_ATROUS_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_300_vgg16_atrous_voc"),
+
+ // mobilenet 系列
+ SSD_512_MOBILENET1_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_512_mobilenet1.0_voc"),
+
+ // YOLO 系列
+ YOLOV8N("ai.djl.pytorch/yolov8n/0.0.1/yolov8n"),
+ YOLO11N("ai.djl.pytorch/yolo11n/0.0.1/yolo11n"),
+ YOLOV5S("ai.djl.pytorch/yolo5s/0.0.1/yolov5s"),
+ YOLOV5S_ONNXRUNTIME("ai.djl.onnxruntime/yolo5s/0.0.1/yolo5s"),
+ YOLO("ai.djl.mxnet/yolo/0.0.1/yolo"),
+
+ // YOLOv3 变体
+ YOLO3_DARKNET_VOC_416("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_voc_416"),
+ YOLO3_MOBILENET_VOC_320("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_320"),
+ YOLO3_MOBILENET_VOC_416("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_41"),
+ YOLO3_DARKNET_COCO_320("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_320"),
+ YOLO3_DARKNET_COCO_416("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_416"),
+ YOLO3_DARKNET_COCO_608("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_608"),
+ YOLO3_MOBILENET_COCO_320("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_320"),
+ YOLO3_MOBILENET_COCO_416("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_416"),
+ YOLO3_MOBILENET_COCO_608("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_608");
+
+ /**
+ * 根据名称获取枚举 (忽略大小写和下划线变体)
+ */
+ public static DetectorModelEnum fromName(String name) {
+ String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
+ for (DetectorModelEnum model : values()) {
+ if (model.name().replaceAll("_", "").equals(formatted)) {
+ return model;
+ }
+ }
+ throw new IllegalArgumentException("未知模型名称: " + name);
+ }
+
+ private final String modelUri;
+
+ DetectorModelEnum(String modelUri) {
+ this.modelUri = modelUri;
+ }
+
+ public String getModelUri() {
+ return modelUri;
+ }
+
+}
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/exception/DetectionException.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/exception/DetectionException.java
new file mode 100644
index 0000000..de73605
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/exception/DetectionException.java
@@ -0,0 +1,30 @@
+package cn.smartjavaai.objectdetection.exception;
+
+/**
+ * 目标检测异常
+ * @author dwj
+ * @date 2025/4/4
+ */
+public class DetectionException extends RuntimeException{
+
+ public DetectionException() {
+ super();
+ }
+
+ public DetectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ public DetectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public DetectionException(String message) {
+ super(message);
+ }
+
+ public DetectionException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java
new file mode 100644
index 0000000..307aa92
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java
@@ -0,0 +1,216 @@
+package cn.smartjavaai.objectdetection.model;
+
+import ai.djl.Application;
+import ai.djl.Device;
+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.repository.zoo.Criteria;
+import ai.djl.repository.zoo.ModelNotFoundException;
+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.enums.DeviceEnum;
+import cn.smartjavaai.common.pool.ModelPredictorPoolManager;
+import cn.smartjavaai.common.pool.PredictorFactory;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.objectdetection.DetectorConfig;
+import cn.smartjavaai.objectdetection.DetectorModelConfig;
+import cn.smartjavaai.objectdetection.exception.DetectionException;
+import cn.smartjavaai.objectdetection.utils.DetectorUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.Validate;
+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.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * 目标检测模型
+ * @author dwj
+ * @date 2025/4/4
+ */
+@Slf4j
+public class DetectorModel implements AutoCloseable{
+
+ private ZooModel model;
+
+ //private Predictor predictor;
+
+ private static final String DJL_MODEL_PREFIX = "djl://";
+
+ private ObjectPool> predictorPool;
+
+ public void loadModel(DetectorModelConfig config){
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ Criteria criteria = Criteria.builder()
+ .optApplication(Application.CV.OBJECT_DETECTION)
+ .setTypes(Image.class, DetectedObjects.class)
+ .optArgument("threshold", config.getThreshold() > 0 ? config.getThreshold() : DetectorConfig.DEFAULT_THRESHOLD)
+ .optModelUrls(DJL_MODEL_PREFIX + config.getModelEnum().getModelUri())
+ .optDevice(device)
+ .optProgress(new ProgressBar())
+ .build();
+ try {
+ model = criteria.loadModel();
+ // 创建池子:每个线程独享 Predictor
+ this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
+ log.info("当前设备: " + model.getNDManager().getDevice());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new DetectionException("模型加载失败", e);
+ }
+ }
+
+ /**
+ * 目标检测
+ * @param imagePath
+ * @return
+ * @throws Exception
+ */
+ public DetectionResponse detect(String imagePath){
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new DetectionException("图像文件不存在");
+ }
+ Image image = null;
+ try {
+ image = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ } catch (IOException e) {
+ throw new DetectionException("图片转换错误", e);
+ }
+ DetectedObjects detectedObjects = detect(image);
+ return DetectorUtils.convertToDetectionResponse(detectedObjects, image);
+ }
+
+
+ /**
+ * 目标检测-将检测结果绘制到原图
+ * @param imagePath
+ * @return
+ */
+ public void detectAndDraw(String imagePath, String outputPath){
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new DetectionException("图像文件不存在");
+ }
+ try {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ DetectedObjects detectedObjects = detect(img);
+ img.drawBoundingBoxes(detectedObjects);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(new FileOutputStream(Paths.get(outputPath).toAbsolutePath().toString()), "png");
+ } catch (IOException e) {
+ throw new DetectionException(e);
+ }
+ }
+
+ /**
+ * 目标检测
+ * @param imageData
+ * @return
+ */
+ public DetectionResponse detect(byte[] imageData){
+ if(Objects.isNull(imageData)){
+ throw new DetectionException("图像无效");
+ }
+ try {
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
+ return detect(image);
+ } catch (IOException e) {
+ throw new DetectionException("错误的图像", e);
+ }
+
+ }
+
+
+ /**
+ * 目标检测
+ * @param image
+ * @return
+ */
+ public DetectionResponse detect(BufferedImage image){
+ if(!ImageUtils.isImageValid(image)){
+ throw new DetectionException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(image);
+ DetectedObjects detectedObjects = detect(img);
+ return DetectorUtils.convertToDetectionResponse(detectedObjects, img);
+ }
+
+ /**
+ * 目标检测-将检测结果绘制到原图
+ * @param sourceImage
+ * @return
+ */
+ public BufferedImage detectAndDraw(BufferedImage sourceImage){
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new DetectionException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(sourceImage);
+ DetectedObjects detectedObjects = detect(img);
+ img.drawBoundingBoxes(detectedObjects);
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new DetectionException("导出图片失败", e);
+ }
+ }
+
+ /**
+ * 目标检测
+ * @param image
+ * @return
+ */
+ private DetectedObjects detect(Image image){
+ Predictor predictor = null;
+ try {
+ predictor = predictorPool.borrowObject();
+ return predictor.predict(image);
+ } catch (Exception e) {
+ throw new DetectionException("目标检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ predictorPool.returnObject(predictor); //归还
+ log.info("释放资源");
+ } catch (Exception e) {
+ log.warn("归还Predictor失败", e);
+ try {
+ predictor.close(); // 归还失败才销毁
+ } catch (Exception ex) {
+ log.error("关闭Predictor失败", ex);
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 显式释放资源(必须调用!)
+ */
+ @Override
+ public void close() {
+ if (predictorPool != null) {
+ predictorPool.close();
+ }
+ }
+}
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java
new file mode 100644
index 0000000..48c9d23
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java
@@ -0,0 +1,73 @@
+package cn.smartjavaai.objectdetection.model;
+
+import cn.smartjavaai.objectdetection.DetectorModelConfig;
+import cn.smartjavaai.objectdetection.DetectorModelEnum;
+import cn.smartjavaai.objectdetection.exception.DetectionException;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 目标检测 模型工厂
+ * @author dwj
+ */
+@Slf4j
+public class ObjectDetectionModelFactory {
+
+ // 使用 volatile 和双重检查锁定来确保线程安全的单例模式
+ private static volatile ObjectDetectionModelFactory instance;
+
+ private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
+
+ // 私有构造函数,防止外部创建实例
+ private ObjectDetectionModelFactory() {}
+
+ // 双重检查锁定的单例方法
+ public static ObjectDetectionModelFactory getInstance() {
+ if (instance == null) {
+ synchronized (ObjectDetectionModelFactory.class) {
+ if (instance == null) {
+ instance = new ObjectDetectionModelFactory();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * 获取模型(通过配置)
+ * @param config
+ * @return
+ */
+ public DetectorModel getModel(DetectorModelConfig config) {
+ if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
+ throw new DetectionException("未配置模型");
+ }
+ return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
+ DetectorModel model = new DetectorModel();
+ model.loadModel(config);
+ return model;
+ });
+ }
+
+ /**
+ * 获取默认模型
+ * @return
+ */
+ public DetectorModel getModel() {
+ // 初始化默认配置
+ DetectorModelConfig config = new DetectorModelConfig();
+ config.setModelEnum(DetectorModelEnum.YOLO11N);
+ return getModel(config);
+ }
+
+ /**
+ * 关闭所有已加载的模型
+ */
+ public void closeAll() {
+ modelMap.values().forEach(DetectorModel::close);
+ }
+}
+
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
new file mode 100644
index 0000000..0f06279
--- /dev/null
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/utils/DetectorUtils.java
@@ -0,0 +1,62 @@
+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.DetectionRectangle;
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.common.utils.ImageUtils;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 目标检测相关工具类
+ * @author dwj
+ * @date 2025/4/9
+ */
+public class DetectorUtils {
+
+
+ /**
+ * 转换为FaceDetectedResult
+ * @param detection
+ * @param img
+ * @return
+ */
+ public static DetectionResponse convertToDetectionResponse(DetectedObjects detection, Image img){
+ if(Objects.isNull(detection) || Objects.isNull(detection.getProbabilities())
+ || detection.getProbabilities().isEmpty() || Objects.isNull(detection.items()) || detection.items().isEmpty()){
+ return null;
+ }
+ DetectionResponse detectionResponse = new DetectionResponse();
+ List rectangleList = new ArrayList();
+ List detectedObjectList = detection.items();
+ Iterator iterator = detectedObjectList.iterator();
+ int index = 0;
+ while(iterator.hasNext()) {
+ DetectedObjects.DetectedObject result = (DetectedObjects.DetectedObject)iterator.next();
+ String className = result.getClassName();
+ BoundingBox box = result.getBoundingBox();
+ int x = (int)(box.getBounds().getX() * (double)img.getWidth());
+ 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);
+ index++;
+ }
+ detectionResponse.setRectangleList(rectangleList);
+ return detectionResponse;
+ }
+
+
+
+}
diff --git a/smartjavaai-seetaface6-lib/pom.xml b/smartjavaai-seetaface6-lib/pom.xml
index de5d13e..865a063 100644
--- a/smartjavaai-seetaface6-lib/pom.xml
+++ b/smartjavaai-seetaface6-lib/pom.xml
@@ -6,7 +6,7 @@
ink.numberone
smartjavaai-parent
- 1.0.6
+ 1.0.8
smartjavaai-seetaface6-lib