mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-10-02 10:17:01 +00:00
新增目标检测功能
This commit is contained in:
287
README.md
287
README.md
@@ -1,25 +1,36 @@
|
||||
# SmartJavaAI:JAVA深度学习算法工具包
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/dengwenjie/SmartJavaAI"><img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/logo.png?v=2025-04-13T07:48:42.197Z" width="45%"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<strong>🍬JAVA轻量级深度学习算法库,该库致力于构建Java生态与AI模型之间的高效桥梁</strong>
|
||||
</p>
|
||||
## 📚简介
|
||||
|
||||
`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
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.8</version>
|
||||
</dependency>
|
||||
```
|
||||
#### 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) <sub>*(人脸模型:RetinaFace示例代码)*</sub>
|
||||
|
||||
└── 📄[LightFaceDemo](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/LightFaceDemo.java) <sub>*(人脸模型:UltraLightFastGenericFaceModel示例代码)*</sub>
|
||||
|
||||
└── 📄[SeetaFace6Demo.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/SeetaFace6Demo.java) <sub>*(人脸模型:SeetaFace6示例代码)*</sub>
|
||||
|
||||
### 6. 人脸特征提取及比对(seetaface6)
|
||||
└── 📄[FaceNetDemo.java](https://gitee.com/dengwenjie/SmartJavaAI-Demo/blob/master/src/main/java/smartai/examples/face/FaceNetDemo.java) <sub>*(人脸模型:FaceNet示例代码)*</sub>
|
||||
|
||||
```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) <sub>*(基于JDK11构建的完整可执行示例)*</sub>
|
||||
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) <sub>*(目标检测示例代码)*</sub>
|
||||
|
||||
|
||||
|
||||
@@ -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高版本
|
||||
|
||||
BIN
db/faces-data.db
BIN
db/faces-data.db
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 357 KiB |
115
examples/pom.xml
115
examples/pom.xml
@@ -1,115 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>examples</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<smartjavaai.version>1.0.0-SNAPSHOT</smartjavaai.version>
|
||||
<exec.mainClass>smartai.examples.face.FaceDemo</exec.mainClass>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
<version>1.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
<version>2.24.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
<version>7.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.6</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.30</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.83</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.onnxruntime</groupId>
|
||||
<artifactId>onnxruntime-engine</artifactId>
|
||||
<version>0.20.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>example</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.3</version>
|
||||
<configuration>
|
||||
<!--如果不想在打包的后缀加上assembly.xml中设置的id,可以加上下面的配置-->
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<archive>
|
||||
<manifest>
|
||||
<!-- 是否绑定依赖,将外部jar包依赖加入到classPath中 -->
|
||||
<addClasspath>true</addClasspath>
|
||||
<!-- 依赖前缀,与之前设置的文件夹路径要匹配 -->
|
||||
<classpathPrefix>lib/</classpathPrefix>
|
||||
<!-- 执行的主程序入口 -->
|
||||
<mainClass>smartai.examples.face.FaceDemo</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<!--绑定的maven操作-->
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>assembly</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 463 KiB |
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 步骤2: 配置文件 (src/main/resources/logback.xml) -->
|
||||
<configuration scan="true" scanPeriod="30 seconds">
|
||||
<!-- 控制台日志输出 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
103
pom.xml
103
pom.xml
@@ -6,13 +6,15 @@
|
||||
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
<packaging>pom</packaging>
|
||||
<description>SmartJavaAI</description>
|
||||
<modules>
|
||||
<module>smartjavaai-face</module>
|
||||
<module>smartjavaai-common</module>
|
||||
<module>smartjavaai-seetaface6-lib</module>
|
||||
<module>smartjavaai-objectdetection</module>
|
||||
<module>smartjavaai-all</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
@@ -35,13 +37,13 @@
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -51,56 +53,46 @@
|
||||
<dependency>
|
||||
<groupId>ai.djl</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl</groupId>
|
||||
<artifactId>basicdataset</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl</groupId>
|
||||
<artifactId>model-zoo</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.timeseries</groupId>
|
||||
<artifactId>timeseries</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.huggingface</groupId>
|
||||
<artifactId>tokenizers</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.audio</groupId>
|
||||
<artifactId>audio</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<!-- MXNet -->
|
||||
<dependency>
|
||||
<groupId>ai.djl.mxnet</groupId>
|
||||
<artifactId>mxnet-model-zoo</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<!-- Pytorch -->
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-model-zoo</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<!-- TensorFlow -->
|
||||
<dependency>
|
||||
<groupId>ai.djl.tensorflow</groupId>
|
||||
<artifactId>tensorflow-model-zoo</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<!-- ONNXRuntime -->
|
||||
<dependency>
|
||||
<groupId>ai.djl.onnxruntime</groupId>
|
||||
<artifactId>onnxruntime-engine</artifactId>
|
||||
<version>${djl.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
@@ -121,47 +113,6 @@
|
||||
<version>3.9</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-engine</artifactId>
|
||||
<version>0.32.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-native-cpu</artifactId>
|
||||
<classifier>win-x86_64</classifier>
|
||||
<scope>runtime</scope>
|
||||
<version>2.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-native-cpu</artifactId>
|
||||
<classifier>linux-x86_64</classifier>
|
||||
<scope>runtime</scope>
|
||||
<version>2.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-native-cpu-precxx11</artifactId>
|
||||
<classifier>linux-aarch64</classifier>
|
||||
<scope>runtime</scope>
|
||||
<version>2.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-native-cpu</artifactId>
|
||||
<classifier>osx-aarch64</classifier>
|
||||
<version>2.5.1</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-jni</artifactId>
|
||||
<version>2.5.1-0.32.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
@@ -194,8 +145,52 @@
|
||||
<version>2.17.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Pool2 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
<version>2.12.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
<artifactId>pytorch-engine</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.tensorflow</groupId>
|
||||
<artifactId>tensorflow-engine</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.mxnet</groupId>
|
||||
<artifactId>mxnet-engine</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.onnxruntime</groupId>
|
||||
<artifactId>onnxruntime-engine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.ml.xgboost</groupId>
|
||||
<artifactId>xgboost</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.tensorrt</groupId>
|
||||
<artifactId>tensorrt</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
<licenses>
|
||||
<license>
|
||||
|
||||
139
smartjavaai-all/pom.xml
Normal file
139
smartjavaai-all/pom.xml
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.8</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.8</version>
|
||||
<name>smartjavaai-all</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.test.skip>true</maven.test.skip>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>dengwenjie</publishingServerId>
|
||||
<tokenAuth>true</tokenAuth>
|
||||
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
|
||||
<doclint>none</doclint>
|
||||
<additionalJOptions>
|
||||
<additionalJOption>-Xdoclint:none</additionalJOption>
|
||||
</additionalJOptions>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<!-- 必须添加:SCM信息 -->
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
||||
<developerConnection>scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git</developerConnection>
|
||||
<url>http://github.com/geekwenjie/SmartJavaAI/tree/master</url>
|
||||
</scm>
|
||||
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>dengwenjie</name>
|
||||
<email>775747758@qq.com</email>
|
||||
<roles>
|
||||
<role>Project Manager</role>
|
||||
<role>Architect</role>
|
||||
</roles>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
</project>
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检测结果
|
||||
* @author dwj
|
||||
* @date 2025/4/12
|
||||
*/
|
||||
public class DetectionResponse {
|
||||
|
||||
private List<DetectionRectangle> rectangleList;
|
||||
|
||||
public List<DetectionRectangle> getRectangleList() {
|
||||
return rectangleList;
|
||||
}
|
||||
|
||||
public void setRectangleList(List<DetectionRectangle> rectangleList) {
|
||||
this.rectangleList = rectangleList;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 矩形区域
|
||||
* @author dwj
|
||||
*/
|
||||
public class Rectangle {
|
||||
|
||||
/**
|
||||
* 矩形区域点集合
|
||||
*/
|
||||
List<Point> 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<Point> getPointList() {
|
||||
return pointList;
|
||||
}
|
||||
|
||||
public void setPointList(List<Point> pointList) {
|
||||
this.pointList = pointList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
|
||||
/**
|
||||
* 目标检测模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum DeviceEnum {
|
||||
|
||||
// resnet50 系列
|
||||
CPU,
|
||||
GPU;
|
||||
|
||||
}
|
||||
@@ -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<String, GenericObjectPool<? extends Predictor<?, ?>>> poolMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册模型池
|
||||
* @param key 模型标识符(自定义,如模型路径、模型名等)
|
||||
* @param model 模型本体
|
||||
* @param config 池配置(可选)
|
||||
*/
|
||||
public <I, O> void registerModel(String key, ZooModel<I, O> model, GenericObjectPoolConfig<Predictor<I, O>> config) {
|
||||
PredictorFactory<I, O> factory = new PredictorFactory<>(model);
|
||||
GenericObjectPool<Predictor<I, O>> pool = new GenericObjectPool<>(factory, config);
|
||||
poolMap.put(key, pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 借出一个 Predictor
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> Predictor<I, O> borrowPredictor(String key) throws Exception {
|
||||
GenericObjectPool<Predictor<I, O>> pool = (GenericObjectPool<Predictor<I, O>>) poolMap.get(key);
|
||||
if (pool == null) {
|
||||
throw new IllegalArgumentException("模型未注册: " + key);
|
||||
}
|
||||
return pool.borrowObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* 归还一个 Predictor
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> void returnPredictor(String key, Predictor<I, O> predictor) {
|
||||
GenericObjectPool<Predictor<I, O>> pool = (GenericObjectPool<Predictor<I, O>>) poolMap.get(key);
|
||||
if (pool != null) {
|
||||
pool.returnObject(predictor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁全部池
|
||||
*/
|
||||
public void closeAll() {
|
||||
for (GenericObjectPool<? extends Predictor<?, ?>> pool : poolMap.values()) {
|
||||
pool.close();
|
||||
}
|
||||
poolMap.clear();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<I, O> extends BasePooledObjectFactory<Predictor<I, O>> {
|
||||
private final ZooModel<I, O> model;
|
||||
|
||||
public PredictorFactory(ZooModel<I, O> model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predictor<I, O> create() {
|
||||
return model.newPredictor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PooledObject<Predictor<I, O>> wrap(Predictor<I, O> predictor) {
|
||||
return new DefaultPooledObject<>(predictor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PooledObject<Predictor<I, O>> p) {
|
||||
p.getObject().close();
|
||||
}
|
||||
}
|
||||
@@ -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(); // 确保是文件且存在
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
<name>smartjavaai-face</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -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("默认不支持该功能");
|
||||
}
|
||||
}
|
||||
@@ -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("默认不支持该功能");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<String, Class<? extends FaceAlgorithm>> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Double> probabilities;
|
||||
|
||||
/**
|
||||
* 人脸框
|
||||
*/
|
||||
private List<Rectangle> rectangles;
|
||||
|
||||
public List<Double> getProbabilities() {
|
||||
return probabilities;
|
||||
}
|
||||
|
||||
public void setProbabilities(List<Double> probabilities) {
|
||||
this.probabilities = probabilities;
|
||||
}
|
||||
|
||||
public List<Rectangle> getRectangles() {
|
||||
return rectangles;
|
||||
}
|
||||
|
||||
public void setRectangles(List<Rectangle> rectangles) {
|
||||
this.rectangles = rectangles;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String, FaceModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends FaceModel>> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Image, float[]> faceFeatureCriteria;
|
||||
|
||||
private Predictor<Image, float[]> predictor;
|
||||
|
||||
private ZooModel<Image, float[]> model;
|
||||
|
||||
public static final List<Float> 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 {
|
||||
|
||||
}*/
|
||||
}
|
||||
@@ -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<Image, DetectedObjects> criteria;
|
||||
|
||||
private Criteria<Image, float[]> faceFeatureCriteria;
|
||||
|
||||
private Predictor<Image, DetectedObjects> predictor;
|
||||
|
||||
private ZooModel<Image, DetectedObjects> 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<Double> probabilities = new ArrayList<>(detection.getProbabilities());
|
||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||
List<Rectangle> RectangleList = detectedObjectList.parallelStream()
|
||||
.map(obj -> {
|
||||
Rectangle rectangle = new Rectangle();
|
||||
List<Point> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Rectangle> RectangleList = new ArrayList<Rectangle>();
|
||||
List<Double> probabilities = new ArrayList<Double>();
|
||||
if(seetaResult != null && seetaResult.length > 0){
|
||||
for(SeetaRect rect : seetaResult){
|
||||
Rectangle rectangle = new Rectangle();
|
||||
List<Point> 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<Long> 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<FaceData> 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Image, DetectedObjects> 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<Image, DetectedObjects> predictor;
|
||||
|
||||
private ZooModel<Image, DetectedObjects> 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<Double> probabilities = new ArrayList<>(detection.getProbabilities());
|
||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||
List<Rectangle> RectangleList = detectedObjectList.parallelStream()
|
||||
.map(obj -> {
|
||||
Rectangle rectangle = new Rectangle();
|
||||
List<Point> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SeetaFace6JNI> {
|
||||
|
||||
@Override
|
||||
public SeetaFace6JNI create() {
|
||||
return new SeetaFace6JNI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PooledObject<SeetaFace6JNI> wrap(SeetaFace6JNI obj) {
|
||||
return new DefaultPooledObject<>(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PooledObject<SeetaFace6JNI> p) {
|
||||
//p.getObject().dispose(); // 如果需要释放 native 资源
|
||||
SeetaFace6JNI object = p.getObject();
|
||||
object = null;
|
||||
}
|
||||
}
|
||||
@@ -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<Predictor<Image, float[]>> predictorPool;
|
||||
|
||||
|
||||
private ZooModel<Image, float[]> model;
|
||||
|
||||
public static final List<Float> 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<Image, float[]> 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<Image, float[]> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
|
||||
private ZooModel<Image, DetectedObjects> 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<Image, DetectedObjects> 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<Image, DetectedObjects> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Long> 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<FaceData> 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Predictor<Image, DetectedObjects>> 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<Image, DetectedObjects> 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<Image, DetectedObjects> 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<Image, DetectedObjects> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.*;
|
||||
@@ -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<Image, float[]> {
|
||||
|
||||
@@ -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<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
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<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
137
smartjavaai-objectdetection/pom.xml
Normal file
137
smartjavaai-objectdetection/pom.xml
Normal file
@@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.8</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
<version>1.0.8</version>
|
||||
<name>smartjavaai-objectdetection</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.test.skip>true</maven.test.skip>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>dengwenjie</publishingServerId>
|
||||
<tokenAuth>true</tokenAuth>
|
||||
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
|
||||
<doclint>none</doclint>
|
||||
<additionalJOptions>
|
||||
<additionalJOption>-Xdoclint:none</additionalJOption>
|
||||
</additionalJOptions>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<!-- 必须添加:SCM信息 -->
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
|
||||
<developerConnection>scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git</developerConnection>
|
||||
<url>http://github.com/geekwenjie/SmartJavaAI/tree/master</url>
|
||||
</scm>
|
||||
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>dengwenjie</id>
|
||||
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>dengwenjie</name>
|
||||
<email>775747758@qq.com</email>
|
||||
<roles>
|
||||
<role>Project Manager</role>
|
||||
<role>Architect</role>
|
||||
</roles>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
</project>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Image, DetectedObjects> model;
|
||||
|
||||
//private Predictor<Image, DetectedObjects> predictor;
|
||||
|
||||
private static final String DJL_MODEL_PREFIX = "djl://";
|
||||
|
||||
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
|
||||
public void loadModel(DetectorModelConfig config){
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
Criteria<Image, DetectedObjects> 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<Image, DetectedObjects> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, DetectorModel> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
List<DetectedObjects.DetectedObject> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>ink.numberone</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.8</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-seetaface6-lib</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user