新增目标检测功能

This commit is contained in:
dengwenjie
2025-04-13 20:33:15 +08:00
parent 4eb02c6d87
commit 241b816e7f
56 changed files with 3300 additions and 1993 deletions

287
README.md
View File

@@ -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 开发者的通用智能工具库。
![retinaface_detected](examples/output/retinaface_detected.jpg)
## 🚀 核心亮点
针对 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高版本

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
package cn.smartjavaai.common.enums;
/**
* 目标检测模型枚举
* @author dwj
* @date 2025/4/4
*/
public enum DeviceEnum {
// resnet50 系列
CPU,
GPU;
}

View File

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

View File

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

View File

@@ -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(); // 确保是文件且存在
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[]> {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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