mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-09 19:18:52 +00:00
1、人脸模块:人脸查询支持 向量数据库Milvus 和 SQLite
2、人脸模块:FaceNet人脸模型也支持人脸注册,查询等功能 3、人脸模块:Seetaface6 自动下载人脸库 4、人脸模块:Seetaface6解决依赖库重复下载问题 5、人脸模块:支持手动加载人脸库 6、人脸模块:人脸识别相关功能支持更多参数
This commit is contained in:
31
README.md
31
README.md
@@ -185,7 +185,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
### ✅ 已实现功能
|
||||
|
||||
- **人脸识别**
|
||||
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
|
||||
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N(支持向量数据库milvus/sqlite)、人脸库注册、人脸库删除
|
||||
- 5点人脸关键点定位
|
||||
- 人脸属性检测(性别、年龄、口罩、眼睛状态、脸部姿态)
|
||||
- 人脸活体检测:图片、视频活体检测
|
||||
@@ -263,7 +263,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</dependency>
|
||||
```
|
||||
### 3、完整示例代码
|
||||
@@ -298,6 +298,15 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
|
||||
## 近期更新日志
|
||||
|
||||
## [v1.0.15] - 2025-06-09
|
||||
- 人脸模块:人脸查询支持 Milvus 和 SQLite
|
||||
- 人脸模块:FaceNet人脸模型也支持人脸注册,查询等功能
|
||||
- 人脸模块:Seetaface6 自动下载人脸库
|
||||
- 人脸模块:Seetaface6解决依赖库重复下载问题
|
||||
- 人脸模块:支持手动加载人脸库
|
||||
- 人脸模块:人脸识别相关功能支持更多参数
|
||||
|
||||
|
||||
## [v1.0.15] - 2025-05-17
|
||||
- 新增OCR文字识别模块:支持最新 PP-OCRv5
|
||||
- OCR文本识别:支持文字方向检测与自动校正
|
||||
@@ -316,22 +325,4 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
- 优化检测返回与包结构
|
||||
- 新增 dependencyManagement 统一依赖版本管理
|
||||
|
||||
## [v1.0.11] - 2025-04-28
|
||||
- FaceNet 特征提取新增人脸对齐
|
||||
- 人脸检测新5点人脸关键点定位
|
||||
- 特征提取接口支持多人脸和最佳人脸提取
|
||||
- 修复人脸框边界精度问题
|
||||
- 更新 Maven 发布的 groupId
|
||||
## [v1.0.10] - 2025-04-19
|
||||
- 兼容 SeetaFace6 在 Linux 系统下的运行
|
||||
- 新增全局缓存路径设置功能
|
||||
- 优化若干功能细节,提升稳定性与性能
|
||||
## [v1.0.8] - 2025-04-13
|
||||
- 新增目标检测功能
|
||||
- 模型调用接口统一封装
|
||||
- 修复若干已知问题
|
||||
- 支持自定义选择使用 GPU 或 CPU 运算
|
||||
- 人脸识别模块新增多种接口,功能更加完善
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<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.15</smartjavaai.version>
|
||||
<smartjavaai.version>1.0.16</smartjavaai.version>
|
||||
<!--如果打包运行,需要替换成你的main-->
|
||||
<exec.mainClass>smartai.examples.face.facerec.RetinaFaceDemo</exec.mainClass>
|
||||
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
package smartai.examples.face.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.enums.IdStrategy;
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
import cn.smartjavaai.face.factory.FaceModelFactory;
|
||||
import cn.smartjavaai.face.model.facerec.FaceModel;
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -21,7 +32,8 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* FaceNet人脸算法模型demo
|
||||
* 支持功能:人脸特征提取、人脸比对(1:1)
|
||||
* 支持系统:windows 64位,linux 64位,macOS M系列芯片
|
||||
* 支持功能:人脸特征提取、人脸比对(1:1)、人脸比对(1:N)、人脸注册
|
||||
* @author dwj
|
||||
* @date 2025/4/11
|
||||
*/
|
||||
@@ -29,106 +41,90 @@ import java.util.List;
|
||||
public class FaceNetDemo {
|
||||
|
||||
/**
|
||||
* 提取人脸特征(支持多人脸)
|
||||
* 默认使用检测模型:FACENET_FEATURE_EXTRACTION
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
* 提取人脸特征(多人脸场景)
|
||||
* 默认使用检测模型:ULTRA_LIGHT_FAST_GENERIC_FACE
|
||||
* 自动裁剪人脸(处理耗时略有增加)
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
*/
|
||||
@Test
|
||||
public void testExtractFeatures(){
|
||||
try {
|
||||
//人脸特征提取模型
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
log.info("人脸特征提取模型加载成功");
|
||||
//提取图片中所有人脸特征
|
||||
R<DetectionResponse> faceResult = faceModel.extractFeatures("src/main/resources/face/iu_1.jpg");
|
||||
if(faceResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(faceResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", faceResult.getMessage());
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取人脸特征(支持多人脸,自定义配置)
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
* 提取人脸特征(自定义配置)
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
*/
|
||||
@Test
|
||||
public void testExtractFeaturesWithCustomConfig(){
|
||||
try {
|
||||
//人脸模型参数
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
//人脸特征提取参数
|
||||
FaceExtractConfig extractConfig = new FaceExtractConfig();
|
||||
//人脸检测模型配置
|
||||
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE)));
|
||||
//当关闭人脸裁剪时,程序将跳过人脸检测与裁剪流程,直接进行特征提取,适用于输入已为标准人脸区域的图像,有助于提升处理效率。
|
||||
extractConfig.setCropFace(true);
|
||||
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
|
||||
extractConfig.setAlign(true);
|
||||
//人脸检测模型配置,指定人脸检测模型:ULTRA_LIGHT_FAST_GENERIC_FACE
|
||||
FaceModelConfig detectModelConfig = new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||
//设置人脸检测置信度阈值
|
||||
detectModelConfig.setConfidenceThreshold(0.98);
|
||||
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(detectModelConfig));
|
||||
config.setExtractConfig(extractConfig);
|
||||
//人脸特征提取模型
|
||||
//获取人脸模型
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取人脸特征(分数最高人脸)
|
||||
* 默认使用检测模型:FACENET_FEATURE_EXTRACTION
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
*/
|
||||
@Test
|
||||
public void testExtractTopFaceFeature(){
|
||||
try {
|
||||
//人脸特征提取模型
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取人脸特征(分数最高人脸,自定义配置)
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
*/
|
||||
@Test
|
||||
public void testExtractTopFaceFeatureWithCustomConfig(){
|
||||
try {
|
||||
//人脸模型参数
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
|
||||
//人脸特征提取参数
|
||||
FaceExtractConfig extractConfig = new FaceExtractConfig();
|
||||
//人脸检测模型配置
|
||||
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE)));
|
||||
config.setExtractConfig(extractConfig);
|
||||
//人脸特征提取模型
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult.getMessage());
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 人脸比对(1:1)-在线模型
|
||||
* 图片参数:图片路径
|
||||
* 人脸比对1:1(基于图像直接比对)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。(接口内自动完成)
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void featureComparison(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);//人脸模型
|
||||
config.setModelPath("/Users/wenjie/Documents/develop/face_model/face_feature.pt");
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//自动裁剪人脸并比对人脸特征
|
||||
float similar = faceModel.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
|
||||
//基于图像直接比对人脸特征
|
||||
float similar = faceModel.featureComparison("src/main/resources/face/iu_1.jpg","src/main/resources/face/iu_2.jpg");
|
||||
log.info("相似度:{}", similar);
|
||||
}
|
||||
catch (Exception e){
|
||||
@@ -137,27 +133,239 @@ public class FaceNetDemo {
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸比对(1:1)- 使用离线模型
|
||||
* 图片参数:图片路径
|
||||
* 人脸比对1:1(基于特征值比对)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void featureComparison2(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult1 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult1.isSuccess()){
|
||||
log.info("图片1人脸特征提取成功:{}", JSONObject.toJSONString(featureResult1.getData()));
|
||||
}else{
|
||||
log.info("图片1人脸特征提取失败:{}", featureResult1.getMessage());
|
||||
return;
|
||||
}
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("图片2人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("图片2人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
//计算相似度
|
||||
float similar = faceModel.calculSimilar(featureResult1.getData(), featureResult2.getData());
|
||||
log.info("相似度:{}", similar);
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除(使用向量数据库Milvus)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void searchFace(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
//初始化向量数据库:Milvus数据库配置
|
||||
MilvusConfig vectorDBConfig = new MilvusConfig();
|
||||
vectorDBConfig.setHost("127.0.0.1");
|
||||
vectorDBConfig.setPort(19530);
|
||||
//vectorDBConfig.setCollectionName("face5");
|
||||
//ID策略:自动生成
|
||||
vectorDBConfig.setIdStrategy(IdStrategy.AUTO);
|
||||
//索引类型:内积 (Inner Product) 不建议修改
|
||||
//vectorDBConfig.setMetricType(MetricType.IP);
|
||||
config.setVectorDBConfig(vectorDBConfig);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//等待加载人脸库结束
|
||||
while (!faceModel.isLoadFaceCompleted()){
|
||||
Thread.sleep(100);
|
||||
}
|
||||
log.info("====================人脸注册==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult.getMessage());
|
||||
return;
|
||||
}
|
||||
//人脸注册信息
|
||||
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJson = new JSONObject();
|
||||
metadataJson.put("name", "iu");
|
||||
metadataJson.put("age", "25");
|
||||
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
|
||||
//人脸注册,返回人脸库ID
|
||||
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
|
||||
if(registerResult.isSuccess()){
|
||||
log.info("注册成功:ID-{}", registerResult.getData());
|
||||
}else{
|
||||
log.info("注册失败:{}", registerResult.getMessage());
|
||||
}
|
||||
/*log.info("====================人脸更新==========================");
|
||||
//更新人脸 只支持自定义ID:vectorDBConfig.setIdStrategy(IdStrategy.CUSTOM);
|
||||
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJsonUpdate = new JSONObject();
|
||||
metadataJsonUpdate.put("name", "iu_update");
|
||||
metadataJsonUpdate.put("age", "25");
|
||||
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
|
||||
//更新必须设置ID,只有
|
||||
updateInfo.setId(registerResult.getData());
|
||||
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
|
||||
log.info("更新人脸成功");*/
|
||||
log.info("====================人脸查询==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
FaceSearchParams faceSearchParams = new FaceSearchParams();
|
||||
faceSearchParams.setTopK(1);
|
||||
faceSearchParams.setThreshold(0.8f);
|
||||
|
||||
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
|
||||
// R<DetectionResponse> faceSearchResults = faceModel.search("src/main/resources/face/iu_3.jpg", faceSearchParams);
|
||||
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
|
||||
log.info("====================人脸删除==========================");
|
||||
faceModel.removeRegister(registerResult.getData());
|
||||
log.info("人脸删除成功");
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除(使用轻量数据库SQLite)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向不正,可开启人脸对齐以提升特征提取准确度。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void searchFace2(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
|
||||
//初始化向量数据库:Milvus数据库配置
|
||||
SQLiteConfig vectorDBConfig = new SQLiteConfig();
|
||||
vectorDBConfig.setDbPath("/Users/wenjie/Downloads/face.db");
|
||||
vectorDBConfig.setSimilarityType(SimilarityType.IP);
|
||||
config.setVectorDBConfig(vectorDBConfig);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//等待加载人脸库结束
|
||||
while (!faceModel.isLoadFaceCompleted()){
|
||||
Thread.sleep(100);
|
||||
}
|
||||
log.info("====================人脸注册==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult.getMessage());
|
||||
return;
|
||||
}
|
||||
//人脸注册信息
|
||||
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJson = new JSONObject();
|
||||
metadataJson.put("name", "iu");
|
||||
metadataJson.put("age", "25");
|
||||
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
|
||||
//可自定义 ID,若未设置则自动生成。
|
||||
//faceRegisterInfo.setId("00001");
|
||||
//人脸注册,返回人脸库ID
|
||||
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
|
||||
if(registerResult.isSuccess()){
|
||||
log.info("注册成功:ID-{}", registerResult.getData());
|
||||
}else{
|
||||
log.info("注册失败:{}", registerResult.getMessage());
|
||||
}
|
||||
log.info("====================人脸更新==========================");
|
||||
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJsonUpdate = new JSONObject();
|
||||
metadataJsonUpdate.put("name", "iu_update");
|
||||
metadataJsonUpdate.put("age", "25");
|
||||
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
|
||||
//更新必须设置ID,只有
|
||||
updateInfo.setId(registerResult.getData());
|
||||
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
|
||||
log.info("更新人脸成功");
|
||||
log.info("====================人脸查询==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
FaceSearchParams faceSearchParams = new FaceSearchParams();
|
||||
faceSearchParams.setTopK(1);
|
||||
faceSearchParams.setThreshold(0.8f);
|
||||
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
|
||||
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
|
||||
log.info("====================人脸删除==========================");
|
||||
faceModel.removeRegister(registerResult.getData());
|
||||
log.info("人脸删除成功");
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用离线模型
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void featureComparisonOffline(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.FACENET_MODEL);//人脸模型
|
||||
//设置人脸识别模型文件路径,请根据实际情况替换为本地模型文件的绝对路径
|
||||
config.setModelPath("/Users/xxx/Documents/develop/face_model/face_feature.pt");
|
||||
//人脸特征提取参数
|
||||
FaceExtractConfig extractConfig = new FaceExtractConfig();
|
||||
FaceModelConfig detectModelConfig = new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||
//设置人脸检测模型文件路径,请根据实际情况替换为本地模型文件的绝对路径
|
||||
detectModelConfig.setModelPath("/Users/xxx/Documents/develop/face_model/ultranet.pt");
|
||||
//人脸检测模型配置
|
||||
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(detectModelConfig));
|
||||
config.setExtractConfig(extractConfig);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//自动裁剪人脸并比对人脸特征
|
||||
float similar = faceModel.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
|
||||
log.info("相似度:{}", similar);
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package smartai.examples.face.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.enums.IdStrategy;
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
import cn.smartjavaai.face.factory.FaceModelFactory;
|
||||
import cn.smartjavaai.face.model.facerec.FaceModel;
|
||||
import cn.smartjavaai.face.utils.SimilarityUtil;
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -21,7 +33,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* SeetaFace6人脸算法模型demo
|
||||
* 支持系统:windows 64位
|
||||
* 支持系统:windows 64位,linux 64位
|
||||
* 支持功能:人脸检测、人脸特征提取、人脸比对(1:1)、人脸比对(1:N)、人脸注册
|
||||
* @author dwj
|
||||
* @date 2025/4/11
|
||||
@@ -31,84 +43,56 @@ public class SeetaFace6Demo {
|
||||
|
||||
|
||||
/**
|
||||
* 人脸检测(自定义模型参数)
|
||||
* 图片参数:图片路径
|
||||
*/
|
||||
@Test
|
||||
public void testFaceDetectCustomConfig(){
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
DetectionResponse detectedResult = faceModel.detect("src/main/resources/largest_selfie.jpg");
|
||||
log.info("人脸检测结果:{}", JSONObject.toJSONString(detectedResult));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 人脸检测并绘制人脸框
|
||||
*/
|
||||
@Test
|
||||
public void testFaceDetectAndDraw(){
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
faceModel.detectAndDraw("src/main/resources/largest_selfie.jpg","output/largest_selfie_detected.png");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸检测并绘制人脸框,返回BufferedImage
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testFaceDetectAndDraw2(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
BufferedImage image = null;
|
||||
String imagePath = "src/main/resources/largest_selfie.jpg";
|
||||
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
//可以根据后续业务场景使用detectedImage
|
||||
BufferedImage detectedImage = faceModel.detectAndDraw(image);
|
||||
Assert.assertNotNull("detectedImage null", detectedImage);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提取人脸特征(支持多人脸)
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
* 提取人脸特征(多人脸场景)
|
||||
* 默认使用SEETA_FACE6_MODEL自己的检测模型
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
*/
|
||||
@Test
|
||||
public void testExtractFeatures(){
|
||||
try {
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.SEETA_FACE6_MODEL,
|
||||
"C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models"));
|
||||
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
//人脸特征提取模型
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//指定模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//提取图片中所有人脸特征
|
||||
R<DetectionResponse> faceResult = faceModel.extractFeatures("src/main/resources/face/iu_1.jpg");
|
||||
if(faceResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(faceResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", faceResult.getMessage());
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提取人脸特征(分数最高人脸)
|
||||
* 自动裁剪人脸 + 人脸对齐
|
||||
* 提取人脸特征(只提取图片中分数最高人脸特征)
|
||||
* 默认使用SEETA_FACE6_MODEL自己的检测模型
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
*/
|
||||
@Test
|
||||
public void testExtractTopFaceFeature(){
|
||||
public void testExtractFeatures2(){
|
||||
try {
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.SEETA_FACE6_MODEL,
|
||||
"C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models"));
|
||||
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
|
||||
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
|
||||
//人脸特征提取模型
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//指定模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//提取图片中检测分数最高人脸特征
|
||||
R<float[]> faceResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(faceResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", faceResult.getData());
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", faceResult.getMessage());
|
||||
}
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -117,20 +101,66 @@ public class SeetaFace6Demo {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 人脸比对(1:1)
|
||||
* 图片参数:图片路径
|
||||
* 人脸比对1:1(基于图像直接比对)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。(接口内自动完成)
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void featureComparison(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
//指定模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//自动裁剪人脸并比对人脸特征
|
||||
float similar = faceModel.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
|
||||
//基于图像直接比对人脸特征
|
||||
float similar = faceModel.featureComparison("src/main/resources/face/iu_1.jpg","src/main/resources/face/iu_2.jpg");
|
||||
log.info("相似度:{}", similar);
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸比对1:1(基于特征值比对)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void featureComparison2(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
//指定模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult1 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult1.isSuccess()){
|
||||
log.info("图片1人脸特征提取成功:{}", JSONObject.toJSONString(featureResult1.getData()));
|
||||
}else{
|
||||
log.info("图片1人脸特征提取失败:{}", featureResult1.getMessage());
|
||||
return;
|
||||
}
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("图片2人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("图片2人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
//计算相似度
|
||||
float similar = faceModel.calculSimilar(featureResult1.getData(), featureResult2.getData());
|
||||
log.info("相似度:{}", similar);
|
||||
}
|
||||
catch (Exception e){
|
||||
@@ -140,81 +170,90 @@ public class SeetaFace6Demo {
|
||||
|
||||
|
||||
/**
|
||||
* 人脸比对(1:1)
|
||||
* 先特征提取,后比对人脸特征
|
||||
* 提取人脸特征图片参数:图片路径
|
||||
*/
|
||||
@Test
|
||||
public void featureExtractionAndCompare(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//提取图像中最大人脸的特征
|
||||
float[] feature1 = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
|
||||
float[] feature2 = faceModel.extractTopFaceFeature("src/main/resources/kana2.jpg");
|
||||
if(feature1 != null && feature2 != null){
|
||||
float similar = faceModel.calculSimilar(feature1, feature2);
|
||||
log.info("相似度:{}", similar);
|
||||
}else{
|
||||
log.warn("人脸特征提取失败");
|
||||
}
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册人脸
|
||||
* 图片参数:图片路径
|
||||
*/
|
||||
@Test
|
||||
public void registerFace(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
//人脸库路径,从项目中 db/faces-data.db下载到本地
|
||||
config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//等待人脸库加载完毕
|
||||
Thread.sleep(1000);
|
||||
//注册kana1人脸,参数key建议设置为人名
|
||||
boolean isSuccss = faceModel.register("kana1","src/main/resources/kana1.jpg");
|
||||
log.info("注册结果:{}", isSuccss);
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 搜索人脸(1:N)
|
||||
* 图片参数:图片路径
|
||||
* 注意事项:请先注册人脸
|
||||
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除(使用向量数据库Milvus)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向较正,可关闭人脸对齐以提升性能。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void searchFace(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
//人脸库路径,从项目中 db/faces-data.db下载到本地
|
||||
config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
//初始化向量数据库:Milvus数据库配置
|
||||
MilvusConfig vectorDBConfig = new MilvusConfig();
|
||||
vectorDBConfig.setHost("127.0.0.1");
|
||||
vectorDBConfig.setPort(19530);
|
||||
//vectorDBConfig.setCollectionName("face10");
|
||||
//ID策略:自动生成
|
||||
vectorDBConfig.setIdStrategy(IdStrategy.AUTO);
|
||||
//索引类型:内积 (Inner Product) 不建议修改
|
||||
vectorDBConfig.setMetricType(MetricType.COSINE);
|
||||
config.setVectorDBConfig(vectorDBConfig);
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//等待人脸库加载完毕
|
||||
Thread.sleep(1000);
|
||||
FaceResult faceResult = faceModel.search("src/main/resources/kana1.jpg");
|
||||
if(faceResult != null){
|
||||
log.info("查询到人脸:{}", faceResult.toString());
|
||||
}else{
|
||||
log.info("未查询到人脸");
|
||||
|
||||
//等待加载人脸库结束
|
||||
while (!faceModel.isLoadFaceCompleted()) {
|
||||
Thread.sleep(50); // 避免 CPU 占用过高
|
||||
}
|
||||
|
||||
log.info("====================人脸注册==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
//人脸注册信息
|
||||
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJson = new JSONObject();
|
||||
metadataJson.put("name", "iu");
|
||||
metadataJson.put("age", "25");
|
||||
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
|
||||
//人脸注册,返回人脸库ID
|
||||
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
|
||||
if(registerResult.isSuccess()){
|
||||
log.info("注册成功:ID-{}", registerResult.getData());
|
||||
}else{
|
||||
log.info("注册失败:{}", registerResult.getMessage());
|
||||
}
|
||||
/*log.info("====================人脸更新==========================");
|
||||
//更新人脸 只支持自定义ID:vectorDBConfig.setIdStrategy(IdStrategy.CUSTOM);
|
||||
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJsonUpdate = new JSONObject();
|
||||
metadataJsonUpdate.put("name", "iu_update");
|
||||
metadataJsonUpdate.put("age", "25");
|
||||
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
|
||||
//更新必须设置ID,只有
|
||||
updateInfo.setId(registerResult.getData());
|
||||
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
|
||||
log.info("更新人脸成功");*/
|
||||
log.info("====================人脸查询==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
FaceSearchParams faceSearchParams = new FaceSearchParams();
|
||||
faceSearchParams.setTopK(1);
|
||||
faceSearchParams.setThreshold(0.8f);
|
||||
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
|
||||
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
|
||||
log.info("====================人脸删除==========================");
|
||||
faceModel.removeRegister(registerResult.getData());
|
||||
log.info("人脸删除成功");
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
@@ -222,25 +261,80 @@ public class SeetaFace6Demo {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除已注册人脸
|
||||
* 注意事项:请先注册人脸
|
||||
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除(使用轻量数据库SQLite)
|
||||
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
|
||||
* 注意事项:
|
||||
* 1、首次调用接口,可能会较慢。只要不关闭程序,后续调用会明显加快。若每次重启程序,则每次首次调用都将重新加载,仍会较慢。
|
||||
* 2、若人脸朝向较正,可关闭人脸对齐以提升性能。(方法参考自定义配置人脸特征提取)
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void removeRegisterFace(){
|
||||
public void searchFace2(){
|
||||
try {
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
|
||||
//人脸库路径,从项目中 db/faces-data.db下载到本地
|
||||
config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
|
||||
//人脸模型
|
||||
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
|
||||
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
|
||||
//使用轻量数据库SQLite
|
||||
config.setVectorDBConfig(new SQLiteConfig());
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
|
||||
//等待人脸库加载完毕
|
||||
Thread.sleep(1000);
|
||||
//使用注册人脸时的key值删除,可一次性删除单个
|
||||
long num = faceModel.removeRegister("kana1");
|
||||
//删除全部人脸
|
||||
//long num = currentAlgorithm.clearFace();
|
||||
log.info("删除成功数量:" + num);
|
||||
log.info("====================人脸注册==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
|
||||
if(featureResult.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult.getMessage());
|
||||
return;
|
||||
}
|
||||
//人脸注册信息
|
||||
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJson = new JSONObject();
|
||||
metadataJson.put("name", "iu");
|
||||
metadataJson.put("age", "25");
|
||||
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
|
||||
//可自定义 ID,若未设置则自动生成。
|
||||
//faceRegisterInfo.setId("00001");
|
||||
//人脸注册,返回人脸库ID
|
||||
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
|
||||
if(registerResult.isSuccess()){
|
||||
log.info("注册成功:ID-{}", registerResult.getData());
|
||||
}else{
|
||||
log.info("注册失败:{}", registerResult.getMessage());
|
||||
}
|
||||
log.info("====================人脸更新==========================");
|
||||
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
|
||||
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
|
||||
JSONObject metadataJsonUpdate = new JSONObject();
|
||||
metadataJsonUpdate.put("name", "iu_update");
|
||||
metadataJsonUpdate.put("age", "25");
|
||||
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
|
||||
//更新必须设置ID,只有
|
||||
updateInfo.setId(registerResult.getData());
|
||||
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
|
||||
log.info("更新人脸成功");
|
||||
log.info("====================人脸查询==========================");
|
||||
//特征提取(提取分数最高人脸特征),适用于单人脸场景
|
||||
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
|
||||
if(featureResult2.isSuccess()){
|
||||
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
|
||||
}else{
|
||||
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
|
||||
return;
|
||||
}
|
||||
FaceSearchParams faceSearchParams = new FaceSearchParams();
|
||||
faceSearchParams.setTopK(1);
|
||||
faceSearchParams.setThreshold(0.62f);
|
||||
//等待加载人脸库结束
|
||||
while (!faceModel.isLoadFaceCompleted()) {
|
||||
Thread.sleep(50); // 避免 CPU 占用过高
|
||||
}
|
||||
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
|
||||
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
|
||||
log.info("====================人脸删除==========================");
|
||||
faceModel.removeRegister(registerResult.getData());
|
||||
log.info("人脸删除成功");
|
||||
}
|
||||
catch (Exception e){
|
||||
e.printStackTrace();
|
||||
@@ -248,4 +342,5 @@ public class SeetaFace6Demo {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class OcrDetectionDemo {
|
||||
//指定检测模型
|
||||
config.setModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
|
||||
//指定模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
config.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
config.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
OcrCommonDetModel model = OcrModelFactory.getInstance().getDetModel(config);
|
||||
List<OcrBox> boxes = model.detect("src/main/resources/ocr_1.jpg");
|
||||
log.info("OCR检测结果:{}", JSONObject.toJSONString(boxes));
|
||||
|
||||
@@ -37,11 +37,11 @@ public class OcrDirectionDetDemo {
|
||||
//指定检测模型
|
||||
directionModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
|
||||
//指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
directionModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
directionModelConfig.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
//指定文本方向检测模型
|
||||
directionModelConfig.setModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
|
||||
//指定文本方向检测模型路径,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
directionModelConfig.setModelPath("/cls/ch_ppocr_mobile_v2.0_cls.onnx");
|
||||
directionModelConfig.setModelPath("/Users/wenjie/Documents/develop/ocr模型/ch_ppocr_mobile_v2.0_cls.onnx");
|
||||
OcrDirectionModel directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
|
||||
List<OcrItem> itemList = directionModel.detect("src/main/resources/ocr_3.jpg");
|
||||
log.info("OCR方向检测结果:{}", JSONObject.toJSONString(itemList));
|
||||
|
||||
@@ -27,8 +27,7 @@ public class OcrRecognizeDemo {
|
||||
|
||||
/**
|
||||
* 文本识别
|
||||
* 本方法支持旋转角度范围为 -90 到 90 度的文字
|
||||
* 同时兼容印刷体和手写体文字。
|
||||
* 支持简体中文、繁体中文、英文、日文四种主要语言,以及手写、竖版、拼音、生僻字
|
||||
* 流程:文本检测 -> 文本识别
|
||||
* 模型需要放在单独文件夹
|
||||
*/
|
||||
@@ -38,21 +37,20 @@ public class OcrRecognizeDemo {
|
||||
//指定检测模型
|
||||
recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
|
||||
//指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
recModelConfig.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
//指定识别模型
|
||||
recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
|
||||
//指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
|
||||
recModelConfig.setRecModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
|
||||
OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
|
||||
OcrInfo ocrInfo = recModel.recognize("src/main/resources/general_ocr_002.png");
|
||||
OcrInfo ocrInfo = recModel.recognize("src/main/resources/ocr_1.jpg");
|
||||
log.info("OCR识别结果:{}", JSONObject.toJSONString(ocrInfo));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本识别(手写字)
|
||||
* 本方法支持旋转角度范围为 -90 到 90 度的文字
|
||||
* 同时兼容印刷体和手写体文字。
|
||||
* 支持简体中文、繁体中文、英文、日文四种主要语言,以及手写、竖版、拼音、生僻字
|
||||
* 流程:文本检测 -> 文本识别
|
||||
* 模型需要放在单独文件夹
|
||||
*/
|
||||
@@ -74,8 +72,8 @@ public class OcrRecognizeDemo {
|
||||
|
||||
/**
|
||||
* 文本识别(带方向矫正)
|
||||
* 本方法支持任意角度文字识别
|
||||
* 同时兼容印刷体和手写体文字。
|
||||
* 支持简体中文、繁体中文、英文、日文四种主要语言,以及手写、竖版、拼音、生僻字
|
||||
* 本方法支持多角度文字识别
|
||||
* 流程:文本检测 -> 方向检测 -> 方向矫正 -> 文本识别
|
||||
* 模型需要放在单独文件夹
|
||||
*/
|
||||
@@ -103,8 +101,7 @@ public class OcrRecognizeDemo {
|
||||
|
||||
/**
|
||||
* 文本识别并绘制结果
|
||||
* 本方法支持旋转角度范围为 -90 到 90 度的文字
|
||||
* 同时兼容印刷体和手写体文字。
|
||||
* 支持简体中文、繁体中文、英文、日文四种主要语言,以及手写、竖版、拼音、生僻字
|
||||
* 流程:文本检测 -> 文本识别
|
||||
* 模型需要放在单独文件夹
|
||||
*/
|
||||
@@ -114,16 +111,19 @@ public class OcrRecognizeDemo {
|
||||
//指定检测模型
|
||||
recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
|
||||
//指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
recModelConfig.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
|
||||
//指定识别模型
|
||||
recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
|
||||
//directionModelConfig.setDirectionModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
|
||||
//指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
|
||||
//directionModelConfig.setDirectionModelPath("/Users/wenjie/Documents/develop/ocr模型/ch_ppocr_mobile_v2.0_cls.onnx");
|
||||
recModelConfig.setRecModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
|
||||
//指定方向检测模型
|
||||
recModelConfig.setDirectionModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
|
||||
//指定方向模型位置,需要更改为自己的模型路径(下载地址请查看文档)
|
||||
recModelConfig.setDirectionModelPath("/Users/wenjie/Documents/develop/ocr模型/ch_ppocr_mobile_v2.0_cls.onnx");
|
||||
OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
|
||||
int fontSize = 20;
|
||||
recModel.recognizeAndDraw("src/main/resources/general_ocr_002.png", "output/general_ocr_002_recognized.png", fontSize);
|
||||
int fontSize = 25;
|
||||
recModel.recognizeAndDraw("src/main/resources/ocr_4.jpg", "output/ocr_4_recognized.jpg", fontSize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
examples/src/main/resources/face/iu_1.jpg
Normal file
BIN
examples/src/main/resources/face/iu_1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
examples/src/main/resources/face/iu_2.jpg
Normal file
BIN
examples/src/main/resources/face/iu_2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
examples/src/main/resources/face/iu_3.jpg
Normal file
BIN
examples/src/main/resources/face/iu_3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
3
pom.xml
3
pom.xml
@@ -4,9 +4,10 @@
|
||||
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>
|
||||
|
||||
<name>SmartJavaAI</name>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<packaging>pom</packaging>
|
||||
<description>SmartJavaAI</description>
|
||||
<modules>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<artifactId>smartjavaai-bom</artifactId>
|
||||
<name>smartjavaai-bom</name>
|
||||
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<name>smartjavaai-common</name>
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -14,6 +14,7 @@ public class DetectionResponse {
|
||||
|
||||
private List<DetectionInfo> detectionInfoList;
|
||||
|
||||
|
||||
public DetectionResponse() {
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@ public class FaceInfo {
|
||||
*/
|
||||
private LivenessStatus livenessStatus;
|
||||
|
||||
/**
|
||||
* 人脸查询结果
|
||||
*/
|
||||
private List<FaceSearchResult> faceSearchResults;
|
||||
|
||||
/**
|
||||
* 人脸特征
|
||||
*/
|
||||
private float[] feature;
|
||||
|
||||
public FaceInfo() {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 人脸向量搜索结果
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class FaceSearchResult {
|
||||
|
||||
/**
|
||||
* 向量ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 相似度分数
|
||||
*/
|
||||
private float similarity;
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
private String metadata;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param id 向量ID
|
||||
* @param similarity 相似度分数
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public FaceSearchResult(String id, float similarity, String metadata) {
|
||||
this.id = id;
|
||||
this.similarity = similarity;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 通用响应封装类,用于统一接口返回结构
|
||||
* @author dwj
|
||||
* @date 2025/6/4
|
||||
*/
|
||||
@Data
|
||||
public class R<T> {
|
||||
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
|
||||
|
||||
public static <T> R<T> ok(T data) {
|
||||
R<T> r = new R<>();
|
||||
r.code = 0;
|
||||
r.message = "成功";
|
||||
r.data = data;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(Integer code, String message) {
|
||||
R<T> r = new R<>();
|
||||
r.code = code;
|
||||
r.message = message;
|
||||
r.data = null;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(Status status) {
|
||||
R<T> r = new R<>();
|
||||
r.code = status.code;
|
||||
r.message = status.message;
|
||||
r.data = null;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
public enum Status {
|
||||
SUCCESS(0, "成功"),
|
||||
INVALID_IMAGE(1, "图像无效"),
|
||||
FILE_NOT_FOUND(2, "图像文件不存在"),
|
||||
NO_FACE_DETECTED(3, "未检测到人脸"),
|
||||
Unknown(-1, "未知错误");
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
||||
|
||||
|
||||
|
||||
Status(int code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public static Status valueOf(int val) {
|
||||
Optional<Status> search = Arrays.stream(values()).filter((status) -> {
|
||||
return status.code == val;
|
||||
}).findFirst();
|
||||
return (Status)search.orElse(Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return code != null && code.equals(Status.SUCCESS.code);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<name>smartjavaai-face</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
@@ -50,6 +50,12 @@
|
||||
<version>3.36.0.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.milvus</groupId>
|
||||
<artifactId>milvus-sdk-java</artifactId>
|
||||
<version>2.5.7</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public class FaceExtractConfig {
|
||||
/**
|
||||
* 是否对齐人脸
|
||||
*/
|
||||
private boolean align = true;
|
||||
private boolean align = false;
|
||||
|
||||
/**
|
||||
* 人脸检测模型
|
||||
|
||||
@@ -3,6 +3,8 @@ package cn.smartjavaai.face.config;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.enums.VectorDBType;
|
||||
import cn.smartjavaai.face.vector.config.VectorDBConfig;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -25,7 +27,7 @@ public class FaceModelConfig {
|
||||
/**
|
||||
* 相似度阈值 作用:判断是否为同一人脸
|
||||
*/
|
||||
private double similarityThreshold = 0D;
|
||||
//private double similarityThreshold = 0D;
|
||||
|
||||
/**
|
||||
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
|
||||
@@ -57,6 +59,17 @@ public class FaceModelConfig {
|
||||
*/
|
||||
private FaceExtractConfig extractConfig;
|
||||
|
||||
|
||||
/**
|
||||
* 向量数据库配置
|
||||
*/
|
||||
private VectorDBConfig vectorDBConfig;
|
||||
|
||||
/**
|
||||
* 是否自动加载人脸到内存
|
||||
*/
|
||||
private boolean isAutoLoadFace = true;
|
||||
|
||||
public FaceModelConfig() {
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ public class FaceDetectConstant {
|
||||
*/
|
||||
public static final float SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD = 0.85F;
|
||||
|
||||
/**
|
||||
* 默认相似度阈值
|
||||
*/
|
||||
public static final float FACENET_DEFAULT_SIMILARITY_THRESHOLD = 0.8F;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ package cn.smartjavaai.face.dao;
|
||||
import cn.smartjavaai.face.entity.FaceData;
|
||||
import cn.smartjavaai.face.sqllite.RowMapper;
|
||||
import cn.smartjavaai.face.sqllite.SqliteHelper;
|
||||
import cn.smartjavaai.face.utils.VectorUtils;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.beanutils.BeanUtils;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 人脸库持久层
|
||||
@@ -21,131 +22,168 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
public class FaceDao {
|
||||
|
||||
private static final String TABLE_NAME_IMG = "face";
|
||||
private static final String FACE_TABLE_NAME = "face";
|
||||
|
||||
private static final String SCHEMA_RESOURCE = "db/schema.sql";
|
||||
|
||||
private String dbFilePath;
|
||||
private static final ConcurrentHashMap<String, FaceDao> INSTANCES = new ConcurrentHashMap<>();
|
||||
|
||||
public FaceDao(String dbFilePath) {
|
||||
this.dbFilePath = dbFilePath;
|
||||
private final String dbFilePath;
|
||||
|
||||
/**
|
||||
* 获取FaceDao实例(单例模式)
|
||||
* @param dbFilePath 数据库文件路径
|
||||
* @return FaceDao实例
|
||||
*/
|
||||
public static FaceDao getInstance(String dbFilePath) {
|
||||
return INSTANCES.computeIfAbsent(dbFilePath, path -> new FaceDao(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
* @param dbFilePath 数据库文件路径
|
||||
*/
|
||||
private FaceDao(String dbFilePath) {
|
||||
this.dbFilePath = dbFilePath;
|
||||
try {
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
//自动创建数据库+表
|
||||
sqliteHelper.initializeDatabase(FACE_TABLE_NAME, SCHEMA_RESOURCE);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(FaceData faceData) throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
sqliteHelper.executeUpdate("INSERT OR REPLACE INTO " + TABLE_NAME_IMG + " (\"index\",\"key\",\"img_data\",\"width\",\"height\",\"channel\") VALUES (?,?,?,?,?,?)", new Object[]{faceData.getIndex(),faceData
|
||||
.getKey(), faceData.getImgData(), faceData.getWidth(), faceData.getHeight(), faceData.getChannel()});
|
||||
/**
|
||||
* 插入或更新人脸向量
|
||||
* @param faceVector 人脸向量
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public void insertOrUpdate(FaceVector faceVector) throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("id", faceVector.getId());
|
||||
params.put("vector", VectorUtils.toByteArray(faceVector.getVector()));
|
||||
params.put("metadata", faceVector.getMetadata());
|
||||
sqliteHelper.executeInsertOrUpdate(FACE_TABLE_NAME, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用index查询key
|
||||
*
|
||||
* @param index
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @param id 人脸ID
|
||||
* @return 人脸向量
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public String findKeyByIndex(long index) throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
return sqliteHelper.executeQuery("select \"key\" from " + TABLE_NAME_IMG + " where \"index\"=" + index);
|
||||
public FaceVector findById(String id) throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME + " where \"id\"=" + id;
|
||||
List<FaceVector> faceVectors = sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
|
||||
@Override
|
||||
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
|
||||
FaceVector face = new FaceVector();
|
||||
face.setId(rs.getString("id"));
|
||||
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
|
||||
face.setMetadata(rs.getString("metadata"));
|
||||
return face;
|
||||
}
|
||||
});
|
||||
return CollectionUtils.isNotEmpty(faceVectors) ? faceVectors.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部
|
||||
*
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @return 删除的行数
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public long deleteAll() throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
long rows = sqliteHelper.executeUpdate("delete from " + TABLE_NAME_IMG);
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
long rows = sqliteHelper.executeUpdate("delete from " + FACE_TABLE_NAME);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询index
|
||||
*
|
||||
* @param keys
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* 使用id数组查询
|
||||
* @param ids ID数组
|
||||
* @return 人脸向量列表
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public List<Long> findIndexList(String... keys) throws SQLException, ClassNotFoundException {
|
||||
public List<FaceVector> findByIds(String... ids) throws SQLException, ClassNotFoundException {
|
||||
// 使用 Stream API
|
||||
String inKeys = Arrays.stream(keys)
|
||||
String inKeys = Arrays.stream(ids)
|
||||
.map(s -> "'" + s + "'")
|
||||
.reduce((s1, s2) -> s1 + "," + s2)
|
||||
.orElse("");
|
||||
String sql = "select \"index\" from " + TABLE_NAME_IMG + " where \"key\" in (" + inKeys + ")";
|
||||
log.info("sql:{}", sql.toString());
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
return sqliteHelper.executeQuery(sql, new RowMapper<Long>() {
|
||||
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME + " where \"id\" in (" + inKeys + ")";
|
||||
log.debug("sql:{}", sql);
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
List<FaceVector> faceVectors = sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
|
||||
@Override
|
||||
public Long mapRow(ResultSet rs, int index) throws SQLException {
|
||||
return rs.getLong(1);
|
||||
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
|
||||
FaceVector face = new FaceVector();
|
||||
face.setId(rs.getString("id"));
|
||||
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
|
||||
face.setMetadata(rs.getString("metadata"));
|
||||
return face;
|
||||
}
|
||||
});
|
||||
return faceVectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人脸
|
||||
* @param keys
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @param ids ID数组
|
||||
* @return 是否成功
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public boolean deleteFace(String... keys) throws SQLException, ClassNotFoundException {
|
||||
String inKeys = Arrays.stream(keys)
|
||||
public boolean deleteFace(String... ids) throws SQLException, ClassNotFoundException {
|
||||
String inKeys = Arrays.stream(ids)
|
||||
.map(s -> "'" + s + "'")
|
||||
.reduce((s1, s2) -> s1 + "," + s2)
|
||||
.orElse("");
|
||||
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
String sql = "delete from " + TABLE_NAME_IMG + " where \"key\" in (" + inKeys + ")";
|
||||
log.info("sql:{}", sql.toString());
|
||||
sqliteHelper.executeUpdate(sql);
|
||||
return true;
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
String sql = "delete from " + FACE_TABLE_NAME + " where \"id\" in (" + inKeys + ")";
|
||||
int rows = sqliteHelper.executeUpdate(sql);
|
||||
log.debug("删除了{}行数据", rows);
|
||||
return rows == ids.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询人脸
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 人脸向量列表
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public List<FaceData> findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
|
||||
String sql = "select \"key\",\"img_data\",\"width\",\"height\",\"channel\" from " + TABLE_NAME_IMG +
|
||||
public List<FaceVector> findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
|
||||
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME +
|
||||
" limit " + pageNo * pageSize + "," + pageSize;
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
return sqliteHelper.executeQuery(sql, new RowMapper<FaceData>() {
|
||||
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
|
||||
return sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
|
||||
@Override
|
||||
public FaceData mapRow(ResultSet rs, int index) throws SQLException {
|
||||
FaceData face = new FaceData();
|
||||
face.setKey(rs.getString("key"));
|
||||
face.setImgData(rs.getBytes("img_data"));
|
||||
face.setWidth(rs.getInt("width"));
|
||||
face.setHeight(rs.getInt("height"));
|
||||
face.setChannel(rs.getInt("channel"));
|
||||
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
|
||||
FaceVector face = new FaceVector();
|
||||
face.setId(rs.getString("id"));
|
||||
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
|
||||
face.setMetadata(rs.getString("metadata"));
|
||||
return face;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新index
|
||||
* @param index
|
||||
* @param faceData
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* 关闭所有实例
|
||||
*/
|
||||
public int updateIndex(long index,FaceData faceData) throws SQLException, ClassNotFoundException {
|
||||
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
|
||||
return sqliteHelper.executeUpdate("INSERT OR REPLACE INTO " + TABLE_NAME_IMG + " (\"index\",\"key\",\"img_data\",\"width\",\"height\",\"channel\") VALUES (?,?,?,?,?,?)", new Object[]{index,faceData
|
||||
.getKey(), faceData.getImgData(), faceData.getWidth(), faceData.getHeight(), faceData.getChannel()});
|
||||
public static void closeAll() {
|
||||
INSTANCES.clear();
|
||||
log.debug("所有FaceDao实例已关闭");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.smartjavaai.face.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 人脸注册信息
|
||||
* @author dwj
|
||||
* @date 2025/5/29
|
||||
*/
|
||||
@Data
|
||||
public class FaceRegisterInfo {
|
||||
|
||||
/**
|
||||
* 向量ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 元数据,可以存储人脸相关的其他信息(JSON格式)
|
||||
*/
|
||||
private String metadata;
|
||||
|
||||
public FaceRegisterInfo(String id, String metadata) {
|
||||
this.id = id;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public FaceRegisterInfo() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.smartjavaai.face.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 人脸查询参数
|
||||
* @author dwj
|
||||
* @date 2025/5/30
|
||||
*/
|
||||
@Data
|
||||
public class FaceSearchParams {
|
||||
|
||||
/**
|
||||
* 搜索结果数量
|
||||
*/
|
||||
private Integer topK = 1;
|
||||
|
||||
/**
|
||||
* 搜索阈值
|
||||
*/
|
||||
private Float threshold;
|
||||
|
||||
/**
|
||||
* 是否对查询结果进行归一化
|
||||
*/
|
||||
private Boolean normalizeSimilarity;
|
||||
|
||||
|
||||
public FaceSearchParams() {
|
||||
}
|
||||
|
||||
public FaceSearchParams(Integer topK, Float threshold) {
|
||||
this.topK = topK;
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public FaceSearchParams(Integer topK, Float threshold, Boolean normalizeSimilarity) {
|
||||
this.topK = topK;
|
||||
this.threshold = threshold;
|
||||
this.normalizeSimilarity = normalizeSimilarity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public enum FaceModelEnum {
|
||||
|
||||
RETINA_FACE("RetinaFaceModel"),
|
||||
ULTRA_LIGHT_FAST_GENERIC_FACE("UltraLightFastGenericFaceModel"),
|
||||
FACENET_FEATURE_EXTRACTION("FeatureExtractionModel"),
|
||||
FACENET_MODEL("FaceNetModel"),
|
||||
SEETA_FACE6_MODEL("SeetaFace6Model");
|
||||
|
||||
private final String modelClassName;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* ID生成策略
|
||||
* @author dwj
|
||||
* @date 2025/5/29
|
||||
*/
|
||||
public enum IdStrategy {
|
||||
|
||||
AUTO, // 自动生成
|
||||
CUSTOM // 用户自定义 ID(由 config.idValue 指定)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/5/31
|
||||
*/
|
||||
public enum SimilarityType {
|
||||
|
||||
IP, // 内积 (Inner Product)
|
||||
L2, // 欧氏距离 (Euclidean Distance)
|
||||
COSINE // 余弦相似度 (Cosine Similarity)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* 向量数据库类型枚举
|
||||
* @author dwj
|
||||
* @date 2025/5/29
|
||||
*/
|
||||
public enum VectorDBType {
|
||||
|
||||
/**
|
||||
* Sqlite,非专用向量库的备用方案
|
||||
*/
|
||||
SQLITE,
|
||||
|
||||
/**
|
||||
* Milvus向量数据库
|
||||
*/
|
||||
MILVUS;
|
||||
|
||||
/**
|
||||
* 未来可以添加其他向量数据库类型
|
||||
*/
|
||||
// FAISS,
|
||||
// ELASTICSEARCH,
|
||||
// PINECONE
|
||||
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public class FaceAttributeModelFactory {
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("seetaface6model", Seetaface6FaceAttributeModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -121,9 +121,9 @@ public class FaceModelFactory {
|
||||
registerAlgorithm("retinafacemodel", RetinaFaceModel.class);
|
||||
registerAlgorithm("ultralightfastgenericfacemodel", UltraLightFastGenericFaceModel.class);
|
||||
//人脸特征提取
|
||||
registerAlgorithm("featureextractionmodel", FeatureExtractionModel.class);
|
||||
registerAlgorithm("facenetmodel", FaceNetModel.class);
|
||||
registerAlgorithm("seetaface6model", SeetaFace6Model.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class LivenessModelFactory {
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("seetaface6model", Seetaface6LivenessModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class Seetaface6FaceAttributeModel implements FaceAttributeModel {
|
||||
this.config = config;
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("Loading seetaFace6 library successfully.");
|
||||
log.debug("Loading seetaFace6 library successfully.");
|
||||
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
|
||||
String[] genderPredictorModelPath = {config.getModelPath() + File.separator + "gender_predictor.csta"};
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸识别模型
|
||||
* @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 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 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("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
@@ -27,42 +29,54 @@ public interface FaceModel {
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
DetectionResponse detect(String imagePath);
|
||||
default DetectionResponse detect(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸检测
|
||||
* @param imageInputStream 图片输入流
|
||||
* @return
|
||||
*/
|
||||
DetectionResponse detect(InputStream imageInputStream);
|
||||
default DetectionResponse detect(InputStream imageInputStream){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
DetectionResponse detect(BufferedImage image);
|
||||
default DetectionResponse detect(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸检测
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
DetectionResponse detect(byte[] imageData);
|
||||
default DetectionResponse detect(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制人脸
|
||||
* @param imagePath 图片输入路径(包含文件名称)
|
||||
* @param outputPath 图片输出路径(包含文件名称)
|
||||
*/
|
||||
void detectAndDraw(String imagePath, String outputPath);
|
||||
default void detectAndDraw(String imagePath, String outputPath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制人脸
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
BufferedImage detectAndDraw(BufferedImage sourceImage);
|
||||
default BufferedImage detectAndDraw(BufferedImage sourceImage){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相似度
|
||||
@@ -70,7 +84,9 @@ public interface FaceModel {
|
||||
* @param feature2 图2特征
|
||||
* @return
|
||||
*/
|
||||
float calculSimilar(float[] feature1, float[] feature2);
|
||||
default float calculSimilar(float[] feature1, float[] feature2){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征比较
|
||||
@@ -78,7 +94,9 @@ public interface FaceModel {
|
||||
* @param imagePath2 图2路径
|
||||
* @return
|
||||
*/
|
||||
float featureComparison(String imagePath1, String imagePath2);
|
||||
default float featureComparison(String imagePath1, String imagePath2){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征比较
|
||||
@@ -86,15 +104,9 @@ public interface FaceModel {
|
||||
* @param sourceImag2 图2BufferedImage
|
||||
* @return
|
||||
*/
|
||||
float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2);
|
||||
|
||||
/**
|
||||
* 特征比较
|
||||
* @param inputStream1 图1输入流
|
||||
* @param inputStream2 图2输入流
|
||||
* @return
|
||||
*/
|
||||
float featureComparison(InputStream inputStream1, InputStream inputStream2);
|
||||
default float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -103,124 +115,291 @@ public interface FaceModel {
|
||||
* @param imageData2
|
||||
* @return
|
||||
*/
|
||||
float featureComparison(byte[] imageData1, byte[] imageData2);
|
||||
default float featureComparison(byte[] imageData1, byte[] imageData2){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册人脸
|
||||
* @param key
|
||||
* @param imagePath
|
||||
* 提取分数最高人脸进行注册
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
boolean register(String key, String imagePath);
|
||||
default R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册人脸
|
||||
* @param key
|
||||
* 提取分数最高人脸进行注册
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param inputStream
|
||||
* @return
|
||||
*/
|
||||
boolean register(String key, InputStream inputStream);
|
||||
default R<String> register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册人脸
|
||||
* @param key
|
||||
* 提取分数最高人脸进行注册
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
boolean register(String key, BufferedImage sourceImage);
|
||||
default R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册人脸
|
||||
* @param key
|
||||
* 提取分数最高人脸进行注册
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
boolean register(String key, byte[] imageData);
|
||||
default R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 注册人脸
|
||||
* 提取分数最高人脸进行注册
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param feature 人脸特征
|
||||
* @return
|
||||
*/
|
||||
default R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新或注册人脸
|
||||
* 自动提取分数最高人脸进行更新
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param imagePath
|
||||
* @return
|
||||
*/
|
||||
FaceResult search(String imagePath);
|
||||
default void upsertFace(FaceRegisterInfo faceRegisterInfo, String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* @param inputStream
|
||||
* @return
|
||||
*/
|
||||
FaceResult search(InputStream inputStream);
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 更新或注册人脸
|
||||
* 自动提取分数最高人脸进行更新
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
FaceResult search(BufferedImage sourceImage);
|
||||
default void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 更新或注册人脸
|
||||
* 自动提取分数最高人脸进行更新
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param feature 人脸特征
|
||||
* @return
|
||||
*/
|
||||
default void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新或注册人脸
|
||||
* 自动提取分数最高人脸进行更新
|
||||
* @param faceRegisterInfo 注册人脸信息
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
FaceResult search(byte[] imageData);
|
||||
default void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除已标记人脸
|
||||
* 查询人脸(查询图片中所有人脸)
|
||||
* @param imagePath
|
||||
* @param params 人脸查询参数
|
||||
* @return
|
||||
*/
|
||||
default R<DetectionResponse> search(String imagePath, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询人脸(查询图片中所有人脸)
|
||||
* 适用于多人脸场景
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
default R<DetectionResponse> search(BufferedImage sourceImage, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸(查询图片中所有人脸)
|
||||
* 适用于多人脸场景
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
default R<DetectionResponse> search(byte[] imageData, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 适用于多人脸场景
|
||||
* @param feature 人脸特征
|
||||
* @return
|
||||
*/
|
||||
default List<FaceSearchResult> search(float[] feature, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
|
||||
* 适用于单人脸场景
|
||||
* @param imagePath
|
||||
* @param params 人脸查询参数
|
||||
* @return
|
||||
*/
|
||||
default R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
|
||||
* 适用于单人脸场景
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
default R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人脸
|
||||
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
|
||||
* 适用于单人脸场景
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
default R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除已注册人脸
|
||||
* @param keys
|
||||
* @return
|
||||
*/
|
||||
long removeRegister(String... keys);
|
||||
default void removeRegister(String... keys){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空人脸库数据
|
||||
*/
|
||||
long clearFace();
|
||||
default void clearFace(){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征提取(使用默认配置)
|
||||
* 特征提取(所有人脸)
|
||||
* 适用于多人脸场景
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
List<float[]> extractFeatures(String imagePath);
|
||||
default R<DetectionResponse> extractFeatures(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征提取(使用默认配置)
|
||||
* 特征提取(所有人脸)
|
||||
* 适用于多人脸场景
|
||||
* @param imageData 图片字节流
|
||||
* @return
|
||||
*/
|
||||
List<float[]> extractFeatures(byte[] imageData);
|
||||
default R<DetectionResponse> extractFeatures(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征提取(使用默认配置)
|
||||
* 特征提取(所有人脸)
|
||||
* 适用于多人脸场景
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
List<float[]> extractFeatures(BufferedImage image);
|
||||
default R<DetectionResponse> extractFeatures(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提取分数最高人脸特征(使用默认配置)
|
||||
* 特征提取(提取分数最高人脸特征)
|
||||
* 适用于单人脸场景
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
float[] extractTopFaceFeature(BufferedImage image);
|
||||
default R<float[]> extractTopFaceFeature(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取分数最高人脸特征(使用默认配置)
|
||||
* 特征提取(提取分数最高人脸特征)
|
||||
* 适用于单人脸场景
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
float[] extractTopFaceFeature(String imagePath);
|
||||
default R<float[]> extractTopFaceFeature(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取分数最高人脸特征(使用默认配置)
|
||||
* 特征提取(提取分数最高人脸特征)
|
||||
* 适用于单人脸场景
|
||||
* @param imageData 图片字节流
|
||||
* @return
|
||||
*/
|
||||
float[] extractTopFaceFeature(byte[] imageData);
|
||||
default R<float[]> extractTopFaceFeature(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载人脸特征
|
||||
*/
|
||||
default void loadFaceFeatures(){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放人脸特征缓存
|
||||
*/
|
||||
default void releaseFaceFeatures(){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否加载人脸库完成
|
||||
* @return
|
||||
*/
|
||||
default boolean isLoadFaceCompleted(){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
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.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
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.*;
|
||||
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.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.factory.FaceModelFactory;
|
||||
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
|
||||
import cn.smartjavaai.face.utils.*;
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import cn.smartjavaai.face.vector.constant.VectorDBConstants;
|
||||
import cn.smartjavaai.face.vector.core.VectorDBClient;
|
||||
import cn.smartjavaai.face.vector.core.VectorDBFactory;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* FaceNet 人脸特征提取模型
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class FaceNetModel implements FaceModel, AutoCloseable{
|
||||
|
||||
/**
|
||||
* 特征维度
|
||||
*/
|
||||
private static final int DIMENSION = 512;
|
||||
|
||||
/**
|
||||
* 是否加载人脸库完毕
|
||||
*/
|
||||
private static volatile boolean isLoadCompleted = false;
|
||||
|
||||
|
||||
private ObjectPool<Predictor<Image, float[]>> predictorPool;
|
||||
|
||||
|
||||
private ZooModel<Image, float[]> model;
|
||||
|
||||
private FaceModelConfig config;
|
||||
|
||||
/**
|
||||
* 是否归一化相似度
|
||||
*/
|
||||
public static final boolean NORMALIZE_SIMILARITY = true;
|
||||
|
||||
|
||||
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);
|
||||
|
||||
private VectorDBClient vectorDBClient;
|
||||
|
||||
|
||||
/**
|
||||
* 加载人脸特征提取模型
|
||||
* @param config
|
||||
*/
|
||||
@Override
|
||||
public void loadModel(FaceModelConfig config) {
|
||||
if(Objects.isNull(config)){
|
||||
throw new FaceException("config为null");
|
||||
}
|
||||
if(Objects.isNull(config.getExtractConfig())){
|
||||
config.setExtractConfig(getDefaultConfig());
|
||||
}else{
|
||||
if(Objects.isNull(config.getExtractConfig().getDetectModel())){
|
||||
throw new FaceException("请设置人脸检测模型");
|
||||
}
|
||||
}
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
this.config = config;
|
||||
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);
|
||||
}
|
||||
|
||||
//初始化人脸库
|
||||
if(config.getVectorDBConfig() != null && config.getVectorDBConfig().getType() != null){
|
||||
if(config.getVectorDBConfig() instanceof MilvusConfig){
|
||||
MilvusConfig milvusConfig = ((MilvusConfig) config.getVectorDBConfig());
|
||||
//设置向量维度
|
||||
milvusConfig.setDimension(DIMENSION);
|
||||
//相似度计算方式 为空,设置默认值
|
||||
if(Objects.isNull(milvusConfig.getMetricType())){
|
||||
//FaceNet 默认使用内积
|
||||
milvusConfig.setMetricType(MetricType.IP);
|
||||
}
|
||||
|
||||
}else if (config.getVectorDBConfig() instanceof SQLiteConfig){
|
||||
SQLiteConfig sqliteConfig = (SQLiteConfig) config.getVectorDBConfig();
|
||||
if(Objects.isNull(sqliteConfig.getSimilarityType())){
|
||||
//seetaface6 默认使用内积
|
||||
sqliteConfig.setSimilarityType(SimilarityType.IP);
|
||||
}
|
||||
}
|
||||
vectorDBClient = VectorDBFactory.createClient(config.getVectorDBConfig());
|
||||
|
||||
// 加载人脸数据库
|
||||
if(config.isAutoLoadFace()){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
log.debug("start load face...");
|
||||
vectorDBClient.initialize();
|
||||
isLoadCompleted = true;
|
||||
log.debug("Load face success!");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算相似度,返回归一化结果
|
||||
* @param feature1 图1特征
|
||||
* @param feature2 图2特征
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public float calculSimilar(float[] feature1, float[] feature2) {
|
||||
//默认返回归一化结果
|
||||
return SimilarityUtil.calculate(feature1, feature2, SimilarityType.IP, NORMALIZE_SIMILARITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征比较
|
||||
* @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("图像文件不存在");
|
||||
}
|
||||
R<float[]> feature1 = extractTopFaceFeature(imagePath1);
|
||||
if (!feature1.isSuccess()){
|
||||
throw new FaceException(feature1.getMessage());
|
||||
}
|
||||
R<float[]> feature2 = extractTopFaceFeature(imagePath2);
|
||||
if (!feature2.isSuccess()){
|
||||
throw new FaceException(feature2.getMessage());
|
||||
}
|
||||
float ret = calculSimilar(feature1.getData(), feature2.getData());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
|
||||
if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
R<float[]> feature1 = extractTopFaceFeature(sourceImage1);
|
||||
if (!feature1.isSuccess()){
|
||||
throw new FaceException(feature1.getMessage());
|
||||
}
|
||||
R<float[]> feature2 = extractTopFaceFeature(sourceImag2);
|
||||
if (!feature2.isSuccess()){
|
||||
throw new FaceException(feature2.getMessage());
|
||||
}
|
||||
float ret = calculSimilar(feature1.getData(), feature2.getData());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float featureComparison(byte[] imageData1, byte[] imageData2) {
|
||||
if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
R<float[]> feature1 = extractTopFaceFeature(imageData1);
|
||||
if (!feature1.isSuccess()){
|
||||
throw new FaceException(feature1.getMessage());
|
||||
}
|
||||
R<float[]> feature2 = extractTopFaceFeature(imageData2);
|
||||
if (!feature2.isSuccess()){
|
||||
throw new FaceException(feature2.getMessage());
|
||||
}
|
||||
float ret = calculSimilar(feature1.getData(), feature2.getData());
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认特征提取配置
|
||||
* @return
|
||||
*/
|
||||
private FaceExtractConfig getDefaultConfig() {
|
||||
FaceExtractConfig config = new FaceExtractConfig();
|
||||
FaceModelConfig detectModelConfig = new FaceModelConfig();
|
||||
detectModelConfig.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||
detectModelConfig.setConfidenceThreshold(0.98);
|
||||
log.debug("创建默认检测模型:ULTRA_LIGHT_FAST_GENERIC_FACE");
|
||||
FaceModel detectModel = FaceModelFactory.getInstance().getModel(detectModelConfig);
|
||||
log.debug("创建检测模型完毕");
|
||||
config.setDetectModel(detectModel);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> extractFeatures(BufferedImage image) {
|
||||
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||
return R.fail(R.Status.NO_FACE_DETECTED);
|
||||
}
|
||||
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
NDManager manager = NDManager.newBaseManager();
|
||||
for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
FaceInfo faceInfo = detectionInfo.getFaceInfo();
|
||||
float[] features = null;
|
||||
//裁剪人脸
|
||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||
//人脸对齐
|
||||
if(config.getExtractConfig().isAlign()){
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
|
||||
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
|
||||
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
|
||||
features = featureExtraction(alignedImg);
|
||||
}else{
|
||||
//不对齐人脸
|
||||
features = featureExtraction(subImage);
|
||||
}
|
||||
faceInfo.setFeature(features);
|
||||
}
|
||||
return R.ok(detectedResult);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> extractFeatures(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> extractFeatures(String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
// 将图片路径转换为 BufferedImage
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return extractFeatures(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<float[]> extractTopFaceFeature(BufferedImage image) {
|
||||
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
float[] features = null;
|
||||
if(config.getExtractConfig().isCropFace()){
|
||||
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||
return R.fail(R.Status.NO_FACE_DETECTED);
|
||||
}
|
||||
//只取第一个人脸
|
||||
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
//裁剪人脸
|
||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||
//人脸对齐
|
||||
if(config.getExtractConfig().isAlign()){
|
||||
NDManager manager = NDManager.newBaseManager();
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
|
||||
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
|
||||
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
|
||||
features = featureExtraction(alignedImg);
|
||||
}else{
|
||||
//不对齐人脸
|
||||
features = featureExtraction(subImage);
|
||||
}
|
||||
}else{
|
||||
//不裁剪人脸直接提取特征
|
||||
features = featureExtraction(djlImage);
|
||||
}
|
||||
return Objects.isNull(features) ? R.fail(R.Status.Unknown) : R.ok(features);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<float[]> extractTopFaceFeature(String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
// 将图片路径转换为 BufferedImage
|
||||
BufferedImage image = null;
|
||||
try {
|
||||
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return extractTopFaceFeature(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<float[]> extractTopFaceFeature(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return register(faceRegisterInfo, bufferedImage);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
//提取最大人脸特征
|
||||
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
|
||||
if(!featureResponse.isSuccess()){
|
||||
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
|
||||
}
|
||||
return register(faceRegisterInfo, featureResponse.getData());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, 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 register(faceRegisterInfo, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return register(faceRegisterInfo, ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
if(Objects.isNull(feature)){
|
||||
throw new FaceException("人脸特征为空");
|
||||
}
|
||||
FaceVector faceVector = new FaceVector();
|
||||
if(faceRegisterInfo != null){
|
||||
faceVector.setId(faceRegisterInfo.getId());
|
||||
faceVector.setMetadata(faceRegisterInfo.getMetadata());
|
||||
}
|
||||
faceVector.setVector(feature);
|
||||
return R.ok(vectorDBClient.insert(faceVector));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegister(String... keys) {
|
||||
vectorDBClient.deleteBatch(Arrays.asList(keys));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFace() {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
vectorDBClient.dropCollection(VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, 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);
|
||||
}
|
||||
upsertFace(faceRegisterInfo, bufferedImage);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
if(Objects.isNull(faceRegisterInfo)){
|
||||
throw new FaceException("注册信息为空");
|
||||
}
|
||||
if(StringUtils.isBlank(faceRegisterInfo.getId())){
|
||||
throw new FaceException("注册信息中ID为空");
|
||||
}
|
||||
//提取最大人脸特征
|
||||
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
|
||||
if(!featureResponse.isSuccess()){
|
||||
throw new FaceException(featureResponse.getMessage());
|
||||
}
|
||||
upsertFace(faceRegisterInfo, featureResponse.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature) {
|
||||
if(Objects.isNull(feature)){
|
||||
throw new FaceException("人脸特征为空");
|
||||
}
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
FaceVector faceVector = new FaceVector();
|
||||
if(faceRegisterInfo != null){
|
||||
faceVector.setId(faceRegisterInfo.getId());
|
||||
faceVector.setMetadata(faceRegisterInfo.getMetadata());
|
||||
}
|
||||
faceVector.setVector(feature);
|
||||
vectorDBClient.upsert(faceVector);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
upsertFace(faceRegisterInfo, ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return searchByTopFace(bufferedImage, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return searchByTopFace(ImageIO.read(new ByteArrayInputStream(imageData)), params);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
//提取最大人脸特征
|
||||
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
|
||||
if(!featureResponse.isSuccess()){
|
||||
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
|
||||
}
|
||||
return R.ok(search(featureResponse.getData(), params));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<FaceSearchResult> search(float[] feature, FaceSearchParams params) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
if(Objects.isNull(feature)){
|
||||
throw new FaceException("人脸特征为空");
|
||||
}
|
||||
if(Objects.isNull(params)){
|
||||
throw new FaceException("人脸查询参数为空");
|
||||
}
|
||||
//设置默认值
|
||||
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.FACENET_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
|
||||
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
|
||||
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
|
||||
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
|
||||
List<FaceSearchResult> searchResults = vectorDBClient.search(feature, searchParams);
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> search(String imagePath, FaceSearchParams params) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return search(bufferedImage, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> search(BufferedImage sourceImage, FaceSearchParams params) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
//提取所有人脸特征
|
||||
R<DetectionResponse> detectionResponse = extractFeatures(sourceImage);
|
||||
if(!detectionResponse.isSuccess()){
|
||||
return detectionResponse;
|
||||
}
|
||||
//设置默认值
|
||||
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.FACENET_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
|
||||
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
|
||||
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
|
||||
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
|
||||
for (DetectionInfo detectionInfo : detectionResponse.getData().getDetectionInfoList()){
|
||||
if(Objects.nonNull(detectionInfo.getFaceInfo()) && Objects.nonNull(detectionInfo.getFaceInfo().getFeature())){
|
||||
List<FaceSearchResult> searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
|
||||
detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
|
||||
}
|
||||
}
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<DetectionResponse> search(byte[] imageData, FaceSearchParams params) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return search(ImageIO.read(new ByteArrayInputStream(imageData)), params);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFaceFeatures() {
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
vectorDBClient.loadFaceFeatures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseFaceFeatures() {
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
vectorDBClient.releaseFaceFeatures();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (predictorPool != null) {
|
||||
predictorPool.close();
|
||||
}
|
||||
if(Objects.nonNull(vectorDBClient)){
|
||||
vectorDBClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadFaceCompleted() {
|
||||
return isLoadCompleted;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
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.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.face.config.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.factory.FaceModelFactory;
|
||||
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
|
||||
import cn.smartjavaai.face.utils.FaceAlignUtils;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import cn.smartjavaai.face.utils.OpenCVUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.face.Face;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
|
||||
private FaceModelConfig config;
|
||||
|
||||
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) {
|
||||
if(Objects.isNull(config)){
|
||||
throw new FaceException("config为null");
|
||||
}
|
||||
if(Objects.isNull(config.getExtractConfig())){
|
||||
config.setExtractConfig(getDefaultConfig());
|
||||
}else{
|
||||
if(Objects.isNull(config.getExtractConfig().getDetectModel())){
|
||||
throw new FaceException("请设置人脸检测模型");
|
||||
}
|
||||
}
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
this.config = config;
|
||||
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); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算相似度
|
||||
* @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 = extractTopFaceFeature(imagePath1);
|
||||
float[] feature2 = extractTopFaceFeature(imagePath2);
|
||||
float ret = calculSimilar(feature1, feature2);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
|
||||
if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
float[] feature1 = extractTopFaceFeature(sourceImage1);
|
||||
float[] feature2 = extractTopFaceFeature(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 = extractTopFaceFeature(imageData1);
|
||||
float[] feature2 = extractTopFaceFeature(imageData2);
|
||||
return calculSimilar(feature1, feature2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认特征提取配置
|
||||
* @return
|
||||
*/
|
||||
private FaceExtractConfig getDefaultConfig() {
|
||||
FaceExtractConfig config = new FaceExtractConfig();
|
||||
FaceModelConfig detectModelConfig = new FaceModelConfig();
|
||||
detectModelConfig.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||
log.debug("创建默认检测模型:ULTRA_LIGHT_FAST_GENERIC_FACE");
|
||||
FaceModel detectModel = FaceModelFactory.getInstance().getModel(detectModelConfig);
|
||||
log.debug("创建检测模型完毕");
|
||||
config.setDetectModel(detectModel);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(BufferedImage image) {
|
||||
|
||||
List<float[]> featureList = new ArrayList<float[]>();
|
||||
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("未检测到人脸");
|
||||
}
|
||||
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
NDManager manager = NDManager.newBaseManager();
|
||||
for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
|
||||
float[] features = null;
|
||||
//裁剪人脸
|
||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||
//人脸对齐
|
||||
if(config.getExtractConfig().isAlign()){
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
|
||||
/*Mat sourceMat = OpenCVUtils.image2Mat(image);
|
||||
Mat mat = FaceAlignUtils.warpAffine(sourceMat, affine_matrix);
|
||||
//OpenCVUtils.mat2Image(mat);
|
||||
Image alignedImg = ImageFactory.getInstance().fromImage(mat);
|
||||
features = featureExtraction(alignedImg);*/
|
||||
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
|
||||
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
|
||||
features = featureExtraction(alignedImg);
|
||||
}else{
|
||||
//不对齐人脸
|
||||
features = featureExtraction(subImage);
|
||||
}
|
||||
if(Objects.nonNull(features)){
|
||||
featureList.add(features);
|
||||
}
|
||||
}
|
||||
return featureList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(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 extractFeatures(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(BufferedImage image) {
|
||||
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
float[] features = null;
|
||||
if(config.getExtractConfig().isCropFace()){
|
||||
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("未检测到人脸");
|
||||
}
|
||||
//只取第一个人脸
|
||||
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
//裁剪人脸
|
||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||
//人脸对齐
|
||||
if(config.getExtractConfig().isAlign()){
|
||||
NDManager manager = NDManager.newBaseManager();
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
|
||||
/*Mat sourceMat = OpenCVUtils.image2Mat(image);
|
||||
Mat mat = FaceAlignUtils.warpAffine(sourceMat, affine_matrix);
|
||||
OpenCVUtils.mat2Image(mat);
|
||||
Image alignedImg = ImageFactory.getInstance().fromImage(OpenCVUtils.mat2Image(mat));
|
||||
features = featureExtraction(alignedImg);*/
|
||||
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
|
||||
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
|
||||
features = featureExtraction(alignedImg);
|
||||
}else{
|
||||
//不对齐人脸
|
||||
features = featureExtraction(subImage);
|
||||
}
|
||||
}else{
|
||||
//不裁剪人脸直接提取特征
|
||||
features = featureExtraction(djlImage);
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(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 extractTopFaceFeature(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (predictorPool != null) {
|
||||
predictorPool.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import java.util.Objects;
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
|
||||
public class RetinaFaceModel implements FaceModel, AutoCloseable{
|
||||
|
||||
|
||||
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
@@ -174,7 +174,7 @@ public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
|
||||
}
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
log.debug("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
} catch (IOException e) {
|
||||
throw new FaceException(e);
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.dao.FaceDao;
|
||||
import cn.smartjavaai.face.entity.FaceData;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.entity.FaceRegisterInfo;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import cn.smartjavaai.face.vector.core.VectorDBClient;
|
||||
import cn.smartjavaai.face.vector.core.VectorDBFactory;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
import com.seeta.pool.*;
|
||||
import com.seeta.sdk.*;
|
||||
import cn.smartjavaai.face.seetaface.NativeLoader;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -23,6 +36,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -32,7 +46,12 @@ import java.util.Objects;
|
||||
*/
|
||||
@SuppressWarnings("AliMissingOverrideAnnotation")
|
||||
@Slf4j
|
||||
public class SeetaFace6Model extends AbstractFaceModel {
|
||||
public class SeetaFace6Model implements FaceModel , AutoCloseable{
|
||||
|
||||
/**
|
||||
* 特征维度
|
||||
*/
|
||||
private static final int DIMENSION = 1024;
|
||||
|
||||
|
||||
private FaceModelConfig config;
|
||||
@@ -43,10 +62,18 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
|
||||
private FaceDatabasePool faceDatabasePool;
|
||||
|
||||
private VectorDBClient vectorDBClient = null;
|
||||
|
||||
/**
|
||||
* 默认相似度阈值
|
||||
* 是否加载人脸库完毕
|
||||
*/
|
||||
public static final float SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD = 0.62F;
|
||||
private static volatile boolean isLoadCompleted = false;
|
||||
|
||||
/**
|
||||
* 是否归一化相似度
|
||||
*/
|
||||
public static final boolean NORMALIZE_SIMILARITY = false;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@@ -55,13 +82,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new FaceException("modelPath is null");
|
||||
}
|
||||
//设置默认相似度阈值
|
||||
if(config.getSimilarityThreshold() <= 0){
|
||||
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
|
||||
}
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("Loading seetaFace6 library successfully.");
|
||||
log.debug("Loading seetaFace6 library successfully.");
|
||||
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
|
||||
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
|
||||
@@ -91,18 +114,45 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
|
||||
this.faceDatabasePool = new FaceDatabasePool(faceDatabasePoolConfSetting);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
log.info("start load faceDb...");
|
||||
loadFaceDb();
|
||||
log.info("Load faceDb success!");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
//初始化人脸库
|
||||
if(config.getVectorDBConfig() != null && config.getVectorDBConfig().getType() != null){
|
||||
if(config.getVectorDBConfig() instanceof MilvusConfig){
|
||||
MilvusConfig milvusConfig = ((MilvusConfig) config.getVectorDBConfig());
|
||||
//设置向量维度
|
||||
milvusConfig.setDimension(DIMENSION);
|
||||
//相似度计算方式 为空,设置默认值
|
||||
if(Objects.isNull(milvusConfig.getMetricType())){
|
||||
//seetaface6 默认使用余弦相似度
|
||||
milvusConfig.setMetricType(MetricType.COSINE);
|
||||
}
|
||||
|
||||
}else if (config.getVectorDBConfig() instanceof SQLiteConfig){
|
||||
SQLiteConfig sqliteConfig = (SQLiteConfig) config.getVectorDBConfig();
|
||||
if(Objects.isNull(sqliteConfig.getSimilarityType())){
|
||||
//seetaface6 默认使用余弦相似度
|
||||
sqliteConfig.setSimilarityType(SimilarityType.COSINE);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
//创建向量数据库
|
||||
vectorDBClient = VectorDBFactory.createClient(config.getVectorDBConfig());
|
||||
// 加载人脸数据库
|
||||
if(config.isAutoLoadFace()){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
log.debug("start load face...");
|
||||
vectorDBClient.initialize();
|
||||
isLoadCompleted = true;
|
||||
log.debug("Load face success!");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
@@ -158,7 +208,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
faceLandmarker.mark(imageData, seetaRect, pointFS);
|
||||
seetaPointFSList.add(pointFS);
|
||||
}
|
||||
return FaceUtils.convertToDetectionResponse(seetaResult, config, seetaPointFSList);
|
||||
return FaceUtils.convertToDetectionResponse(seetaResult, seetaPointFSList);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("目标检测错误", e);
|
||||
}finally {
|
||||
@@ -342,21 +392,6 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
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
|
||||
@@ -391,8 +426,8 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
faceLandmarker.mark(imageData1, seetaResult[0], pointFS1);
|
||||
|
||||
//裁剪人脸
|
||||
SeetaImageData cropImageData1 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
|
||||
faceRecognizer.CropFaceV2(imageData1, pointFS1, cropImageData1);
|
||||
// SeetaImageData cropImageData1 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
|
||||
// faceRecognizer.CropFaceV2(imageData1, pointFS1, cropImageData1);
|
||||
|
||||
//图片2:检测人脸
|
||||
SeetaRect[] seetaResult2 = faceDetector.Detect(imageData2);
|
||||
@@ -404,10 +439,10 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
faceLandmarker.mark(imageData2, seetaResult2[0], pointFS2);
|
||||
|
||||
//图片2:裁剪人脸
|
||||
SeetaImageData cropImageData2 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
|
||||
faceRecognizer.CropFaceV2(imageData2, pointFS2, cropImageData2);
|
||||
|
||||
return faceDatabase.CompareByCroppedFace(cropImageData1, cropImageData2);
|
||||
// SeetaImageData cropImageData2 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
|
||||
// faceRecognizer.CropFaceV2(imageData2, pointFS2, cropImageData2);
|
||||
// return faceDatabase.CompareByCroppedFace(cropImageData1, cropImageData2);
|
||||
return faceDatabase.Compare(imageData1, pointFS1, imageData2, pointFS2);
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -465,9 +500,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(String key, String imagePath) {
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new FaceException("图像文件不存在");
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
@@ -475,59 +510,27 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return register(key, bufferedImage);
|
||||
return register(faceRegisterInfo, bufferedImage);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean register(String key, BufferedImage image) {
|
||||
if(!checkFaceDb()){
|
||||
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
|
||||
}
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
FaceDatabase faceDatabase = null;
|
||||
try {
|
||||
faceDatabase = faceDatabasePool.borrowObject();
|
||||
SeetaImageData cropImageData = getMaxCropFace(imageData);
|
||||
long index = faceDatabase.RegisterByCroppedFace(cropImageData);
|
||||
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(cropImageData.data);
|
||||
try {
|
||||
new FaceDao(config.getFaceDbPath()).save(face);
|
||||
} catch (SQLException | ClassNotFoundException e) {
|
||||
throw new FaceException("保存人脸库失败", e);
|
||||
}
|
||||
return true;
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
}finally {
|
||||
if (faceDatabase != null) {
|
||||
try {
|
||||
faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
//提取特征向量
|
||||
R<float[]> featureResponse = extractTopFaceFeature(image);
|
||||
if(!featureResponse.isSuccess()){
|
||||
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
|
||||
}
|
||||
return register(faceRegisterInfo, featureResponse.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(String key, byte[] imageData) {
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
@@ -535,14 +538,11 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
} catch (IOException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
return register(key, bufferedImage);
|
||||
return register(faceRegisterInfo, bufferedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(String key, InputStream inputStream) {
|
||||
if(!checkFaceDb()){
|
||||
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
|
||||
}
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream) {
|
||||
if(Objects.isNull(inputStream)){
|
||||
throw new FaceException("图像输入流无效");
|
||||
}
|
||||
@@ -552,54 +552,70 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效的图片输入流", e);
|
||||
}
|
||||
return register(key, image);
|
||||
return register(faceRegisterInfo, image);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册已裁剪后人脸
|
||||
* @param key
|
||||
* @param faceData
|
||||
* @return
|
||||
*/
|
||||
private boolean registerCroppedFace(String key, FaceData faceData) {
|
||||
FaceDatabase faceDatabase = null;
|
||||
try {
|
||||
faceDatabase = faceDatabasePool.borrowObject();
|
||||
SeetaImageData cropImageData = new SeetaImageData(faceData.getWidth(), faceData.getHeight(), faceData.getChannel());
|
||||
cropImageData.data = faceData.getImgData();
|
||||
long index = faceDatabase.RegisterByCroppedFace(cropImageData);
|
||||
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;
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
}finally {
|
||||
if (faceDatabase != null) {
|
||||
try {
|
||||
faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature) {
|
||||
if(Objects.isNull(feature)){
|
||||
return R.fail(R.Status.Unknown.getCode(), "人脸注册失败:人脸特征为空");
|
||||
}
|
||||
FaceVector faceVector = new FaceVector();
|
||||
if(Objects.nonNull(faceRegisterInfo)){
|
||||
faceVector.setId(faceRegisterInfo.getId());
|
||||
faceVector.setMetadata(faceRegisterInfo.getMetadata());
|
||||
}
|
||||
faceVector.setVector(feature);
|
||||
return R.ok(vectorDBClient.insert(faceVector));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// /**
|
||||
// * 注册已裁剪后人脸
|
||||
// * @param key
|
||||
// * @param faceData
|
||||
// * @return
|
||||
// */
|
||||
// private boolean registerCroppedFace(String key, FaceData faceData) {
|
||||
// FaceDatabase faceDatabase = null;
|
||||
// try {
|
||||
// faceDatabase = faceDatabasePool.borrowObject();
|
||||
// SeetaImageData cropImageData = new SeetaImageData(faceData.getWidth(), faceData.getHeight(), faceData.getChannel());
|
||||
// cropImageData.data = faceData.getImgData();
|
||||
// long index = faceDatabase.RegisterByCroppedFace(cropImageData);
|
||||
// if (index < 0) {
|
||||
// log.debug("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;
|
||||
// } catch (FaceException e) {
|
||||
// throw e;
|
||||
// } catch (Exception e) {
|
||||
// throw new FaceException(e);
|
||||
// }finally {
|
||||
// if (faceDatabase != null) {
|
||||
// try {
|
||||
// faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
// } catch (Exception e) {
|
||||
// log.warn("归还Predictor失败", e);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public FaceResult search(String imagePath) {
|
||||
public R<DetectionResponse> search(String imagePath, FaceSearchParams params) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new FaceException("图像文件不存在");
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
@@ -607,69 +623,38 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return search(bufferedImage);
|
||||
return search(bufferedImage, params);
|
||||
}
|
||||
|
||||
@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) {
|
||||
long time1 = System.currentTimeMillis();
|
||||
public R<DetectionResponse> search(BufferedImage image, FaceSearchParams params) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
FaceDatabase faceDatabase = null;
|
||||
try {
|
||||
faceDatabase = faceDatabasePool.borrowObject();
|
||||
SeetaPointF[] points = getMaskPoint(imageData);
|
||||
long[] index = new long[1];
|
||||
float[] similarity = new float[1];
|
||||
long result = faceDatabase.QueryTop(imageData, points, 1, index, similarity);
|
||||
if(result < 1){
|
||||
return null;
|
||||
}
|
||||
//检查相似度
|
||||
if(similarity[0] < config.getSimilarityThreshold()){
|
||||
return null;
|
||||
}
|
||||
FaceResult faceResult = searchFaceDb(index[0], similarity[0]);
|
||||
return faceResult;
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
}finally {
|
||||
if (faceDatabase != null) {
|
||||
try {
|
||||
faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
//提取所有人脸特征
|
||||
R<DetectionResponse> detectionResponse = extractFeatures(image);
|
||||
if(!detectionResponse.isSuccess()){
|
||||
return detectionResponse;
|
||||
}
|
||||
//设置默认值
|
||||
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
|
||||
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
|
||||
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
|
||||
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
|
||||
for (DetectionInfo detectionInfo : detectionResponse.getData().getDetectionInfoList()){
|
||||
if(Objects.nonNull(detectionInfo.getFaceInfo()) && Objects.nonNull(detectionInfo.getFaceInfo().getFeature())){
|
||||
List<FaceSearchResult> searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
|
||||
detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
|
||||
}
|
||||
}
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaceResult search(byte[] imageData) {
|
||||
public R<DetectionResponse> search(byte[] imageData, FaceSearchParams params) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
@@ -677,75 +662,89 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
} catch (IOException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
return search(bufferedImage);
|
||||
return search(bufferedImage, params);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<FaceSearchResult> search(float[] feature, FaceSearchParams params) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
if(Objects.isNull(feature)){
|
||||
throw new FaceException("人脸特征为空");
|
||||
}
|
||||
if(Objects.isNull(params)){
|
||||
throw new FaceException("人脸查询参数为空");
|
||||
}
|
||||
//设置默认值
|
||||
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
|
||||
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
|
||||
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
|
||||
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
|
||||
List<FaceSearchResult> searchResults = vectorDBClient.search(feature, searchParams);
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
return searchByTopFace(bufferedImage, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long removeRegister(String... keys) {
|
||||
public R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
//提取分数最高人脸特征
|
||||
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
|
||||
if(!featureResponse.isSuccess()){
|
||||
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
|
||||
}
|
||||
//设置默认值
|
||||
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
|
||||
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
|
||||
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
|
||||
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
|
||||
List<FaceSearchResult> searchResults = vectorDBClient.search(featureResponse.getData(), searchParams);
|
||||
return R.ok(searchResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params) {
|
||||
if(Objects.isNull(imageData)){
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
BufferedImage bufferedImage = null;
|
||||
try {
|
||||
bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
return searchByTopFace(bufferedImage, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegister(String... keys) {
|
||||
if(keys == null || keys.length == 0){
|
||||
throw new FaceException("keys不允许为空");
|
||||
}
|
||||
if(!checkFaceDb()){
|
||||
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
|
||||
}
|
||||
|
||||
FaceDatabase faceDatabase = null;
|
||||
try {
|
||||
List<Long> list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
|
||||
if (list == null) {
|
||||
return 0;
|
||||
}
|
||||
faceDatabase = faceDatabasePool.borrowObject();
|
||||
int rows = 0;
|
||||
for (long index : list) {
|
||||
int row = faceDatabase.Delete(index);
|
||||
rows += row;
|
||||
}
|
||||
new FaceDao(config.getFaceDbPath()).deleteFace(keys);
|
||||
return rows;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
}finally {
|
||||
if (faceDatabase != null) {
|
||||
try {
|
||||
faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
vectorDBClient.deleteBatch(Arrays.asList(keys));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long clearFace(){
|
||||
if(!checkFaceDb()){
|
||||
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
|
||||
}
|
||||
|
||||
FaceDatabase faceDatabase = null;
|
||||
try {
|
||||
faceDatabase = faceDatabasePool.borrowObject();
|
||||
faceDatabase.Clear();
|
||||
long rows = 0;
|
||||
try {
|
||||
rows = new FaceDao(config.getFaceDbPath()).deleteAll();
|
||||
} catch (SQLException | ClassNotFoundException e) {
|
||||
throw new FaceException("删除人脸库失败", e);
|
||||
}
|
||||
return rows;
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
}finally {
|
||||
if (faceDatabase != null) {
|
||||
try {
|
||||
faceDatabasePool.returnObject(faceDatabase); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void clearFace(){
|
||||
vectorDBClient.dropCollection(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -763,11 +762,11 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
private FaceResult searchFaceDb(long index,float similar) {
|
||||
if(index >= 0){
|
||||
String key = null;
|
||||
try {
|
||||
key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(index);
|
||||
} catch (SQLException | ClassNotFoundException e) {
|
||||
throw new FaceException("查询人脸库失败", e);
|
||||
}
|
||||
// try {
|
||||
// key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(index);
|
||||
// } catch (SQLException | ClassNotFoundException e) {
|
||||
// throw new FaceException("查询人脸库失败", e);
|
||||
// }
|
||||
return new FaceResult(key, similar);
|
||||
}
|
||||
return null;
|
||||
@@ -779,42 +778,42 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
* @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 {
|
||||
registerCroppedFace(face.getKey(), face);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
if (list.size() < pageSize) {
|
||||
break;
|
||||
}
|
||||
pageNo++;
|
||||
}
|
||||
}
|
||||
// private void loadFaceDb() {
|
||||
// if(!checkFaceDb()){
|
||||
// log.debug("未配置人脸库");
|
||||
// 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 {
|
||||
// registerCroppedFace(face.getKey(), face);
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// });
|
||||
// if (list.size() < pageSize) {
|
||||
// break;
|
||||
// }
|
||||
// pageNo++;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(String imagePath) {
|
||||
public R<DetectionResponse> extractFeatures(String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new FaceException("图像文件不存在");
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
// 将图片路径转换为 BufferedImage
|
||||
BufferedImage image = null;
|
||||
@@ -827,23 +826,24 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(byte[] imageData) {
|
||||
public R<DetectionResponse> extractFeatures(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
throw new FaceException("特征提取异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<float[]> extractFeatures(BufferedImage image) {
|
||||
public R<DetectionResponse> extractFeatures(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
List<float[]> featureList = new ArrayList<float[]>();
|
||||
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
FaceDetector faceDetector = null;
|
||||
@@ -856,7 +856,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = faceDetector.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||
throw new FaceException("未检测到人脸");
|
||||
return R.fail(R.Status.NO_FACE_DETECTED);
|
||||
}
|
||||
for(SeetaRect seetaRect : seetaResult){
|
||||
//提取人脸的5点人脸标识
|
||||
@@ -867,14 +867,16 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
//CropFaceV2 + ExtractCroppedFace 已包含裁剪+人脸对齐
|
||||
boolean isSuccess = faceRecognizer.Extract(imageData, pointFS, features);
|
||||
if(!isSuccess){
|
||||
throw new FaceException("人脸特征提取失败");
|
||||
log.warn("人脸特征提取失败");
|
||||
}
|
||||
featureList.add(features);
|
||||
seetaPointFSList.add(pointFS);
|
||||
}
|
||||
return R.ok(FaceUtils.featuresConvertToResponse(seetaResult, seetaPointFSList, featureList));
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("目标检测错误", e);
|
||||
throw new FaceException("人脸特征提取异常", e);
|
||||
}finally {
|
||||
if (faceDetector != null) {
|
||||
try {
|
||||
@@ -898,14 +900,13 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
return featureList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(BufferedImage image) {
|
||||
public R<float[]> extractTopFaceFeature(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
float[] features = null;
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
@@ -920,7 +921,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = faceDetector.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||
throw new FaceException("未检测到人脸");
|
||||
return R.fail(R.Status.NO_FACE_DETECTED);
|
||||
}
|
||||
//提取人脸的5点人脸标识
|
||||
SeetaPointF[] pointFS = new SeetaPointF[faceLandmarker.number()];
|
||||
@@ -930,8 +931,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
//CropFaceV2 + ExtractCroppedFace 已包含裁剪+人脸对齐
|
||||
boolean isSuccess = faceRecognizer.Extract(imageData, pointFS, features);
|
||||
if(!isSuccess){
|
||||
throw new FaceException("人脸特征提取失败");
|
||||
return R.fail(R.Status.Unknown.getCode(), "人脸特征提取失败");
|
||||
}
|
||||
return R.ok(features);
|
||||
} catch (FaceException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -959,13 +961,12 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(String imagePath) {
|
||||
public R<float[]> extractTopFaceFeature(String imagePath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new FaceException("图像文件不存在");
|
||||
return R.fail(R.Status.FILE_NOT_FOUND);
|
||||
}
|
||||
// 将图片路径转换为 BufferedImage
|
||||
BufferedImage image = null;
|
||||
@@ -978,9 +979,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] extractTopFaceFeature(byte[] imageData) {
|
||||
public R<float[]> extractTopFaceFeature(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
return R.fail(R.Status.INVALID_IMAGE);
|
||||
}
|
||||
try {
|
||||
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
@@ -989,5 +990,100 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFaceFeatures() {
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
vectorDBClient.loadFaceFeatures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseFaceFeatures() {
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
vectorDBClient.releaseFaceFeatures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, 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);
|
||||
}
|
||||
upsertFace(faceRegisterInfo, bufferedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
|
||||
if(vectorDBClient == null){
|
||||
throw new VectorDBException("向量数据库未初始化成功");
|
||||
}
|
||||
if(Objects.isNull(faceRegisterInfo)){
|
||||
throw new FaceException("注册信息为空");
|
||||
}
|
||||
if(StringUtils.isBlank(faceRegisterInfo.getId())){
|
||||
throw new FaceException("注册信息中ID为空");
|
||||
}
|
||||
//提取最大人脸特征
|
||||
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
|
||||
if(!featureResponse.isSuccess()){
|
||||
throw new FaceException(featureResponse.getMessage());
|
||||
}
|
||||
upsertFace(faceRegisterInfo, featureResponse.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature) {
|
||||
if(Objects.isNull(feature)){
|
||||
throw new FaceException("人脸特征为空");
|
||||
}
|
||||
if(Objects.isNull(vectorDBClient)){
|
||||
throw new FaceException("未初始化人脸库");
|
||||
}
|
||||
FaceVector faceVector = new FaceVector();
|
||||
if(faceRegisterInfo != null){
|
||||
faceVector.setId(faceRegisterInfo.getId());
|
||||
faceVector.setMetadata(faceRegisterInfo.getMetadata());
|
||||
}
|
||||
faceVector.setVector(feature);
|
||||
vectorDBClient.upsert(faceVector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
|
||||
FaceModel.super.upsertFace(faceRegisterInfo, imageData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if(Objects.nonNull(faceDetectorPool)){
|
||||
faceDetectorPool.close();
|
||||
}
|
||||
if(Objects.nonNull(faceRecognizerPool)){
|
||||
faceRecognizerPool.close();
|
||||
}
|
||||
if(Objects.nonNull(faceLandmarkerPool)){
|
||||
faceLandmarkerPool.close();
|
||||
}
|
||||
if(Objects.nonNull(faceDatabasePool)){
|
||||
faceDatabasePool.close();
|
||||
}
|
||||
if(Objects.nonNull(vectorDBClient)){
|
||||
vectorDBClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadFaceCompleted() {
|
||||
return isLoadCompleted;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import java.util.Objects;
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements AutoCloseable{
|
||||
public class UltraLightFastGenericFaceModel implements FaceModel, AutoCloseable{
|
||||
|
||||
|
||||
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
|
||||
@@ -46,7 +46,7 @@ public class Seetaface6LivenessModel implements LivenessDetModel{
|
||||
}
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("Loading seetaFace6 library successfully.");
|
||||
log.debug("Loading seetaFace6 library successfully.");
|
||||
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||
String[] faceAntiSpoofingModelPath = {config.getModelPath() + File.separator + "fas_first.csta",config.getModelPath() + File.separator + "fas_second.csta"};
|
||||
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
|
||||
@@ -511,7 +511,7 @@ public class Seetaface6LivenessModel implements LivenessDetModel{
|
||||
// 获取视频总帧数
|
||||
int totalFrames = grabber.getLengthInFrames();
|
||||
int videoFrameCountConfig = faceAntiSpoofing.GetVideoFrameCount();
|
||||
log.info("视频总帧数:{},检测帧数:{}", totalFrames, videoFrameCountConfig);
|
||||
log.debug("视频总帧数:{},检测帧数:{}", totalFrames, videoFrameCountConfig);
|
||||
if(totalFrames < videoFrameCountConfig){
|
||||
throw new FaceException("视频帧数低于检测帧数");
|
||||
}
|
||||
|
||||
@@ -64,21 +64,22 @@ public class NativeLoader {
|
||||
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
|
||||
//创建目录
|
||||
FileUtil.mkdir(seetaface6NativePath);
|
||||
log.info("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
|
||||
log.debug("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
|
||||
//拷贝依赖库到缓存目录
|
||||
List<File> fileList = getLibFiles(osInfo, device);
|
||||
if(fileList != null && !fileList.isEmpty()){
|
||||
// 加载依赖库文件
|
||||
fileList.forEach(file -> {
|
||||
System.load(file.getAbsolutePath());
|
||||
log.info(String.format("load %s finish", file.getAbsolutePath()));
|
||||
//log.debug(String.format("load %s finish", file.getAbsolutePath()));
|
||||
});
|
||||
}
|
||||
log.debug("seetaface6 依赖库加载完毕");
|
||||
isDllLoaded = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("SeetaFace DLL is already loaded.");
|
||||
log.debug("SeetaFace DLL is already loaded.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -94,7 +95,7 @@ public class NativeLoader {
|
||||
private static List<File> getLibFiles(OsInfo osInfo,DeviceEnum deviceEnum){
|
||||
try {
|
||||
String device = getDevice(deviceEnum);
|
||||
log.info("当前设备:{}", device);
|
||||
log.debug("当前设备:{}", device);
|
||||
//获取dll文件列表
|
||||
List<DllItem> baseList = new ArrayList<>();
|
||||
List<DllItem> jniList = new ArrayList<>();
|
||||
@@ -206,11 +207,15 @@ public class NativeLoader {
|
||||
Path path = Paths.get(resourcePath);
|
||||
String fileName = path.getFileName().toString();
|
||||
Path targetPath = seetaface6NativePath.resolve(fileName);
|
||||
Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("copy target path success : {}", targetPath.toAbsolutePath().toString());
|
||||
// 设置可执行权限
|
||||
if (!SystemUtil.getOsInfo().getName().toLowerCase().contains("win")) {
|
||||
targetPath.toFile().setExecutable(true);
|
||||
if (Files.exists(targetPath)) {
|
||||
//log.debug("target file already exists, skip copy: {}", targetPath.toAbsolutePath());
|
||||
} else {
|
||||
Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.debug("copy target path success: {}", targetPath.toAbsolutePath());
|
||||
// 设置可执行权限
|
||||
if (!SystemUtil.getOsInfo().getName().toLowerCase().contains("win")) {
|
||||
targetPath.toFile().setExecutable(true);
|
||||
}
|
||||
}
|
||||
return targetPath.toFile();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package cn.smartjavaai.face.sqllite;
|
||||
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.sqlite.SQLiteConfig;
|
||||
import org.sqlite.SQLiteDataSource;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* sqlite帮助类
|
||||
@@ -18,34 +23,61 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
public class SqliteHelper {
|
||||
|
||||
private Connection connection;
|
||||
private Statement statement;
|
||||
private ResultSet resultSet;
|
||||
private String dbFilePath;
|
||||
private static final ConcurrentHashMap<String, SqliteHelper> INSTANCES = new ConcurrentHashMap<>();
|
||||
private static final int MAX_CONNECTIONS = 10; // 最大连接数
|
||||
|
||||
private final String dbFilePath;
|
||||
private final DataSource dataSource;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 获取SqliteHelper实例(单例模式)
|
||||
* @param dbFilePath sqlite db 文件路径
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException
|
||||
* @return SqliteHelper实例
|
||||
* @throws SQLException SQL异常
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
*/
|
||||
public SqliteHelper(String dbFilePath) throws ClassNotFoundException, SQLException {
|
||||
public static SqliteHelper getInstance(String dbFilePath) throws SQLException, ClassNotFoundException {
|
||||
return INSTANCES.computeIfAbsent(dbFilePath, path -> {
|
||||
try {
|
||||
return new SqliteHelper(path);
|
||||
} catch (Exception e) {
|
||||
log.error("创建SqliteHelper实例失败", e);
|
||||
throw new RuntimeException("创建SqliteHelper实例失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
* @param dbFilePath sqlite db 文件路径
|
||||
* @throws ClassNotFoundException 类未找到异常
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
private SqliteHelper(String dbFilePath) throws ClassNotFoundException, SQLException {
|
||||
this.dbFilePath = dbFilePath;
|
||||
connection = getConnection(dbFilePath);
|
||||
createDatabaseIfNotExists();
|
||||
|
||||
// 初始化连接池
|
||||
SQLiteDataSource sqLiteDataSource = new SQLiteDataSource();
|
||||
sqLiteDataSource.setUrl("jdbc:sqlite:" + dbFilePath);
|
||||
|
||||
// 配置SQLite连接
|
||||
SQLiteConfig sqLiteConfig = new SQLiteConfig();
|
||||
sqLiteConfig.setSharedCache(true);
|
||||
sqLiteConfig.enableLoadExtension(true);
|
||||
sqLiteConfig.setBusyTimeout(5000); // 5秒超时
|
||||
sqLiteDataSource.setConfig(sqLiteConfig);
|
||||
|
||||
this.dataSource = sqLiteDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库连接
|
||||
* @param dbFilePath db文件路径
|
||||
* @return 数据库连接
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public Connection getConnection(String dbFilePath) throws ClassNotFoundException, SQLException {
|
||||
Connection conn = null;
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
conn = DriverManager.getConnection("jdbc:sqlite:" + dbFilePath);
|
||||
return conn;
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,168 +85,117 @@ public class SqliteHelper {
|
||||
* @param sql sql select 语句
|
||||
* @param rse 结果集处理类对象
|
||||
* @return 查询结果
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public <T> T executeQuery(String sql, ResultSetExtractor<T> rse) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
resultSet = getStatement().executeQuery(sql);
|
||||
T rs = rse.extractData(resultSet);
|
||||
return rs;
|
||||
} finally {
|
||||
destroyed();
|
||||
public <T> T executeQuery(String sql, ResultSetExtractor<T> rse) throws SQLException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
return rse.extractData(rs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行select查询,返回结果列表
|
||||
*
|
||||
* @param sql sql select 语句
|
||||
* @param rm 结果集的行数据处理类对象
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @return 查询结果列表
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public <T> List<T> executeQuery(String sql, RowMapper<T> rm) throws SQLException, ClassNotFoundException {
|
||||
List<T> rsList = new ArrayList<T>();
|
||||
try {
|
||||
resultSet = getStatement().executeQuery(sql);
|
||||
while (resultSet.next()) {
|
||||
rsList.add(rm.mapRow(resultSet, resultSet.getRow()));
|
||||
public <T> List<T> executeQuery(String sql, RowMapper<T> rm) throws SQLException {
|
||||
List<T> rsList = new ArrayList<>();
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
rsList.add(rm.mapRow(rs, rs.getRow()));
|
||||
}
|
||||
} finally {
|
||||
destroyed();
|
||||
return rsList;
|
||||
}
|
||||
return rsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单查询某个字段
|
||||
* @param sql
|
||||
* @return
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @param sql SQL查询语句
|
||||
* @return 查询结果
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public String executeQuery(String sql) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
resultSet = getStatement().executeQuery(sql);
|
||||
if(resultSet.next()){
|
||||
return resultSet.getString(1);
|
||||
public String executeQuery(String sql) throws SQLException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
if (rs.next()) {
|
||||
return rs.getString(1);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
destroyed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库更新sql语句
|
||||
* @param sql
|
||||
* @param sql SQL更新语句
|
||||
* @return 更新行数
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public int executeUpdate(String sql) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
int c = getStatement().executeUpdate(sql);
|
||||
return c;
|
||||
} finally {
|
||||
destroyed();
|
||||
public int executeUpdate(String sql) throws SQLException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement()) {
|
||||
return stmt.executeUpdate(sql);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行多个sql更新语句
|
||||
* @param sqls
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @param sqls SQL更新语句数组
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public void executeUpdate(String...sqls) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
public void executeUpdate(String... sqls) throws SQLException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement()) {
|
||||
for (String sql : sqls) {
|
||||
getStatement().executeUpdate(sql);
|
||||
stmt.executeUpdate(sql);
|
||||
}
|
||||
} finally {
|
||||
destroyed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库更新 sql List
|
||||
* @param sqls sql列表
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public void executeUpdate(List<String> sqls) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
public void executeUpdate(List<String> sqls) throws SQLException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement()) {
|
||||
for (String sql : sqls) {
|
||||
getStatement().executeUpdate(sql);
|
||||
stmt.executeUpdate(sql);
|
||||
}
|
||||
} finally {
|
||||
destroyed();
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection() throws ClassNotFoundException, SQLException {
|
||||
if (null == connection) connection = getConnection(dbFilePath);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private Statement getStatement() throws SQLException, ClassNotFoundException {
|
||||
if (null == statement) statement = getConnection().createStatement();
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库资源关闭和释放
|
||||
*/
|
||||
public void destroyed() {
|
||||
try {
|
||||
if (null != statement) {
|
||||
statement.close();
|
||||
statement = null;
|
||||
}
|
||||
|
||||
if (null != connection) {
|
||||
connection.close();
|
||||
connection = null;
|
||||
}
|
||||
|
||||
if (null != resultSet) {
|
||||
resultSet.close();
|
||||
resultSet = null;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
log.error("Sqlite数据库关闭时异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行select查询,返回结果列表
|
||||
*
|
||||
* @param sql sql select 语句
|
||||
* @param clazz 实体泛型
|
||||
* @return 实体集合
|
||||
* @throws SQLException 异常信息
|
||||
* @throws ClassNotFoundException 异常信息
|
||||
* @throws IllegalAccessException 非法访问异常
|
||||
* @throws InstantiationException 实例化异常
|
||||
*/
|
||||
public <T> List<T> executeQueryList(String sql, Class<T> clazz) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
List<T> rsList = new ArrayList<T>();
|
||||
try {
|
||||
resultSet = getStatement().executeQuery(sql);
|
||||
while (resultSet.next()) {
|
||||
public <T> List<T> executeQueryList(String sql, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
|
||||
List<T> rsList = new ArrayList<>();
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
T t = clazz.newInstance();
|
||||
for (Field field : t.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
field.set(t,resultSet.getObject(field.getName()));
|
||||
field.set(t, rs.getObject(field.getName()));
|
||||
}
|
||||
rsList.add(t);
|
||||
}
|
||||
} finally {
|
||||
destroyed();
|
||||
return rsList;
|
||||
}
|
||||
return rsList;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,20 +203,23 @@ public class SqliteHelper {
|
||||
* @param sql sql select 语句
|
||||
* @param clazz 结果集处理类对象
|
||||
* @return 查询结果
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException SQL异常
|
||||
* @throws IllegalAccessException 非法访问异常
|
||||
* @throws InstantiationException 实例化异常
|
||||
*/
|
||||
public <T> T executeQuery(String sql, Class<T> clazz) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
try {
|
||||
resultSet = getStatement().executeQuery(sql);
|
||||
T t = clazz.newInstance();
|
||||
for (Field field : t.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
field.set(t,resultSet.getObject(field.getName()));
|
||||
public <T> T executeQuery(String sql, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
|
||||
try (Connection conn = getConnection();
|
||||
Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql)) {
|
||||
if (rs.next()) {
|
||||
T t = clazz.newInstance();
|
||||
for (Field field : t.getClass().getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
field.set(t, rs.getObject(field.getName()));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
return t;
|
||||
} finally {
|
||||
destroyed();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,53 +228,149 @@ public class SqliteHelper {
|
||||
* @param tableName 表名
|
||||
* @param param key-value键值对,key:表中字段名,value:值
|
||||
* @return 更新行数
|
||||
* @throws SQLException
|
||||
* @throws ClassNotFoundException
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public int executeInsertOrUpdate(String tableName, Map<String,Object> param) throws SQLException, ClassNotFoundException {
|
||||
try {
|
||||
StringBuffer sql = new StringBuffer();
|
||||
public int executeInsertOrUpdate(String tableName, Map<String, Object> param) throws SQLException {
|
||||
try (Connection conn = getConnection()) {
|
||||
// 保证字段和值顺序一致
|
||||
List<String> keys = new ArrayList<>(param.keySet());
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("INSERT OR REPLACE INTO ");
|
||||
sql.append(tableName);
|
||||
sql.append(" ( ");
|
||||
for (String key : param.keySet()) {
|
||||
sql.append("'" + key + "'");
|
||||
sql.append(",");
|
||||
sql.append(" (");
|
||||
|
||||
for (String key : keys) {
|
||||
sql.append(key).append(",");
|
||||
}
|
||||
sql.delete(sql.length()-1,sql.length());
|
||||
sql.append(") VALUES ( ");
|
||||
for (String key : param.keySet()) {
|
||||
sql.append("'");
|
||||
sql.append(param.get(key));
|
||||
sql.append("',");
|
||||
sql.deleteCharAt(sql.length() - 1);
|
||||
sql.append(") VALUES (");
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
sql.append("?,");
|
||||
}
|
||||
sql.delete(sql.length()-1,sql.length());
|
||||
sql.deleteCharAt(sql.length() - 1);
|
||||
sql.append(");");
|
||||
log.info("sql:{}", sql.toString());
|
||||
int c = getStatement().executeUpdate(sql.toString());
|
||||
return c;
|
||||
} finally {
|
||||
destroyed();
|
||||
|
||||
log.debug("sql: {}", sql.toString());
|
||||
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
Object value = param.get(keys.get(i));
|
||||
if (value instanceof byte[]) {
|
||||
pstmt.setBytes(i + 1, (byte[]) value);
|
||||
} else {
|
||||
pstmt.setObject(i + 1, value);
|
||||
}
|
||||
}
|
||||
return pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int executeUpdate(String sql, Object[] args) throws SQLException, ClassNotFoundException {
|
||||
Connection conn = getConnection();
|
||||
if (args == null || args.length == 0) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
return stmt.executeUpdate(sql);
|
||||
} finally {
|
||||
destroyed();
|
||||
}
|
||||
} else {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
stmt.setObject(i + 1, args[i]);
|
||||
|
||||
/**
|
||||
* 使用预编译语句执行更新
|
||||
* @param sql SQL语句
|
||||
* @param args 参数
|
||||
* @return 更新行数
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
public int executeUpdate(String sql, Object[] args) throws SQLException {
|
||||
try (Connection conn = getConnection()) {
|
||||
if (args == null || args.length == 0) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
return stmt.executeUpdate(sql);
|
||||
}
|
||||
} else {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
stmt.setObject(i + 1, args[i]);
|
||||
}
|
||||
return stmt.executeUpdate();
|
||||
}
|
||||
return stmt.executeUpdate();
|
||||
} finally {
|
||||
destroyed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据库文件(如果不存在)
|
||||
*/
|
||||
private void createDatabaseIfNotExists() {
|
||||
File dbFile = new File(dbFilePath);
|
||||
if (!dbFile.exists()) {
|
||||
try {
|
||||
// 创建数据库文件
|
||||
dbFile.getParentFile().mkdirs(); // 创建父目录
|
||||
dbFile.createNewFile();
|
||||
log.debug("Created new SQLite database file: {}", dbFilePath);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create database file: {}", dbFilePath, e);
|
||||
throw new RuntimeException("Database file creation failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库表结构
|
||||
*/
|
||||
public void initializeDatabase(String tableName, String schemaResourcePath) throws SQLException {
|
||||
// 检查表是否存在
|
||||
if (isTableExists(tableName)) {
|
||||
log.debug("Database table already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Creating database tables...");
|
||||
|
||||
// 使用 Hutool 读取 SQL 资源文件
|
||||
List<String> sqlStatements = readSqlResource(schemaResourcePath);
|
||||
|
||||
// 执行所有 SQL 语句
|
||||
for (String sql : sqlStatements) {
|
||||
if (!sql.trim().isEmpty()) {
|
||||
executeUpdate(sql);
|
||||
}
|
||||
}
|
||||
log.debug("Database tables created successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查表是否存在
|
||||
*/
|
||||
private boolean isTableExists(String tableName) throws SQLException {
|
||||
try (Connection conn = getConnection()) {
|
||||
ResultSet rs = conn.getMetaData().getTables(null, null, tableName, null);
|
||||
return rs.next();
|
||||
} catch (SQLException e) {
|
||||
log.warn("Error checking table existence: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Hutool 读取 SQL 资源文件并分割为语句列表
|
||||
*/
|
||||
private List<String> readSqlResource(String resourcePath) {
|
||||
try {
|
||||
// 读取整个资源文件内容
|
||||
String content = ResourceUtil.readUtf8Str(resourcePath);
|
||||
|
||||
// 分割 SQL 语句(按分号分割)
|
||||
return Arrays.stream(content.split(";"))
|
||||
.map(String::trim)
|
||||
.filter(sql -> !sql.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to read SQL resource: {}", resourcePath, e);
|
||||
throw new RuntimeException("SQL resource read error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有连接池
|
||||
*/
|
||||
public static void closeAll() {
|
||||
INSTANCES.clear();
|
||||
log.debug("所有SqliteHelper实例已关闭");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ public final class FaceFeatureTranslator implements Translator<Image, float[]> {
|
||||
*/
|
||||
@Override
|
||||
public float[] processOutput(TranslatorContext ctx, NDList list) {
|
||||
return list.singletonOrThrow().toFloatArray();
|
||||
NDArray embedding = list.singletonOrThrow();
|
||||
embedding = embedding.div(embedding.norm()); // L2归一化
|
||||
return embedding.toFloatArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -79,7 +79,7 @@ public class FaceUtils {
|
||||
* @param seetaResult
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, FaceModelConfig config,List<SeetaPointF[]> seetaPointFSList){
|
||||
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList){
|
||||
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||
return null;
|
||||
}
|
||||
@@ -104,6 +104,57 @@ public class FaceUtils {
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换为FaceDetectedResult(人脸特征提取)
|
||||
* @param seetaResult
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse featuresConvertToResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<float[]> featureList){
|
||||
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||
return null;
|
||||
}
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
for(int i = 0; i < seetaResult.length; i++){
|
||||
SeetaRect rect = seetaResult[i];
|
||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||
FaceInfo faceInfo = new FaceInfo();
|
||||
if(seetaPointFSList != null && seetaPointFSList.size() > 0){
|
||||
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||
.map(p -> new Point(p.x, p.y))
|
||||
.collect(Collectors.toList());
|
||||
faceInfo.setKeyPoints(keyPoints);
|
||||
}
|
||||
if(featureList != null && featureList.size() > 0){
|
||||
faceInfo.setFeature(featureList.get(i));
|
||||
}
|
||||
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
|
||||
}
|
||||
return new DetectionResponse(detectionInfoList);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换为FaceDetectedResult(人脸特征提取)
|
||||
* @param rect
|
||||
* @param seetaPointFS
|
||||
* @param feature
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse featuresConvertToResponse(SeetaRect rect, SeetaPointF[] seetaPointFS, float[] feature){
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||
FaceInfo faceInfo = new FaceInfo();
|
||||
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||
.map(p -> new Point(p.x, p.y))
|
||||
.collect(Collectors.toList());
|
||||
faceInfo.setKeyPoints(keyPoints);
|
||||
faceInfo.setFeature(feature);
|
||||
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
|
||||
return new DetectionResponse(detectionInfoList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制人脸框
|
||||
* @param sourceImage
|
||||
@@ -572,6 +623,28 @@ public class FaceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Milvus 查询返回的得分转换为 0~1 范围的相似度
|
||||
* @param metricType 向量度量方式:IP 或 L2
|
||||
* @param score 原始得分(L2 为距离,IP 为相似度)
|
||||
* @return 映射后的相似度(0~1)
|
||||
*/
|
||||
public static float convertScoreToSimilarity(String metricType, float score) {
|
||||
switch (metricType.toUpperCase()) {
|
||||
case "IP":
|
||||
// 内积 IP 的范围为 [-1, 1],归一化为 [0, 1]
|
||||
return (score + 1.0f) / 2.0f;
|
||||
case "L2":
|
||||
// 欧氏距离 L2:距离越小越相似,1 / (1 + 距离) 映射到 (0, 1]
|
||||
return 1.0f / (1.0f + score);
|
||||
case "COSINE":
|
||||
// 余弦相似度 COSINE:本身范围为 [-1, 1],也需要归一化到 [0, 1]
|
||||
return (score + 1.0f) / 2.0f;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported metricType: " + metricType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package cn.smartjavaai.face.utils;
|
||||
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
|
||||
/**
|
||||
* 特征相似度计算工具类
|
||||
* 支持三种计算方式:IP(内积)、L2(欧氏距离)、COSINE(余弦相似度)
|
||||
* 所有计算结果归一化到[0,1]范围
|
||||
*/
|
||||
public class SimilarityUtil {
|
||||
|
||||
/**
|
||||
* 计算特征相似度
|
||||
* @param features1 特征向量1
|
||||
* @param features2 特征向量2
|
||||
* @param similarityType 计算类型 (IP, L2, COSINE)
|
||||
* @param normalizeScore 是否归一化结果到 [0,1]
|
||||
* @return 相似度
|
||||
*/
|
||||
public static float calculate(float[] features1, float[] features2,
|
||||
SimilarityType similarityType,
|
||||
boolean normalizeScore) {
|
||||
validateInput(features1, features2);
|
||||
|
||||
switch (similarityType) {
|
||||
case IP:
|
||||
return innerProductSimilarity(features1, features2, normalizeScore);
|
||||
case L2:
|
||||
return euclideanSimilarity(features1, features2, normalizeScore);
|
||||
case COSINE:
|
||||
return cosineSimilarity(features1, features2, normalizeScore);
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的相似度计算类型: " + similarityType);
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 私有计算方法 ================
|
||||
|
||||
/**
|
||||
* 计算内积相似度(归一化到[0,1])
|
||||
* 适用于归一化向量(结果范围[-1,1] -> [0,1])
|
||||
*/
|
||||
private static float innerProductSimilarity(float[] v1, float[] v2, boolean normalize) {
|
||||
float dot = dotProduct(v1, v2);
|
||||
return normalize ? (dot + 1.0f) / 2.0f : dot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算欧氏距离相似度(归一化到[0,1])
|
||||
* 距离越小相似度越高,距离为0时相似度为1
|
||||
*/
|
||||
private static float euclideanSimilarity(float[] v1, float[] v2, boolean normalize) {
|
||||
float dist = euclideanDistance(v1, v2);
|
||||
return normalize ? 1.0f / (1.0f + dist) : dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算余弦相似度(归一化到[0,1])
|
||||
* 适用于非归一化向量(结果范围[-1,1] -> [0,1])
|
||||
*/
|
||||
private static float cosineSimilarity(float[] v1, float[] v2, boolean normalize) {
|
||||
float dot = dotProduct(v1, v2);
|
||||
float norm1 = vectorNorm(v1);
|
||||
float norm2 = vectorNorm(v2);
|
||||
|
||||
if (norm1 <= 0 || norm2 <= 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float cosine = dot / (norm1 * norm2);
|
||||
return normalize ? (cosine + 1.0f) / 2.0f : cosine;
|
||||
}
|
||||
|
||||
// ================ 基础向量操作 ================
|
||||
|
||||
/**
|
||||
* 计算点积(内积)
|
||||
*/
|
||||
public static float dotProduct(float[] v1, float[] v2) {
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < v1.length; i++) {
|
||||
sum += v1[i] * v2[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算欧氏距离
|
||||
*/
|
||||
public static float euclideanDistance(float[] v1, float[] v2) {
|
||||
float sumSquaredDiff = 0.0f;
|
||||
for (int i = 0; i < v1.length; i++) {
|
||||
float diff = v1[i] - v2[i];
|
||||
sumSquaredDiff += diff * diff;
|
||||
}
|
||||
return (float) Math.sqrt(sumSquaredDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算向量模长
|
||||
*/
|
||||
public static float vectorNorm(float[] vector) {
|
||||
float sum = 0.0f;
|
||||
for (float v : vector) {
|
||||
sum += v * v;
|
||||
}
|
||||
return (float) Math.sqrt(sum);
|
||||
}
|
||||
|
||||
// ================ 输入验证 ================
|
||||
|
||||
/**
|
||||
* 验证输入向量
|
||||
*/
|
||||
private static void validateInput(float[] v1, float[] v2) {
|
||||
if (v1 == null || v2 == null) {
|
||||
throw new IllegalArgumentException("特征向量不能为null");
|
||||
}
|
||||
|
||||
if (v1.length == 0 || v2.length == 0) {
|
||||
throw new IllegalArgumentException("特征向量不能为空");
|
||||
}
|
||||
|
||||
if (v1.length != v2.length) {
|
||||
throw new IllegalArgumentException("特征向量长度不一致: " +
|
||||
v1.length + " vs " + v2.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.smartjavaai.face.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
/**
|
||||
* 人脸向量工具类
|
||||
* @author dwj
|
||||
* @date 2025/5/30
|
||||
*/
|
||||
public class VectorUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 将 float 数组转换为 byte 数组
|
||||
* @param floats 人脸特征向量
|
||||
* @return 转换后的字节数组
|
||||
*/
|
||||
public static byte[] toByteArray(float[] floats) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(floats.length * 4); // 每个 float 占 4 个字节
|
||||
buffer.asFloatBuffer().put(floats);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 byte 数组转换回 float 数组
|
||||
* @param bytes 从数据库读取的字节数组
|
||||
* @return 原始的人脸特征向量
|
||||
*/
|
||||
public static float[] toFloatArray(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return new float[0];
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
FloatBuffer floatBuffer = buffer.asFloatBuffer();
|
||||
float[] floats = new float[floatBuffer.remaining()];
|
||||
floatBuffer.get(floats);
|
||||
return floats;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.smartjavaai.face.vector.config;
|
||||
|
||||
|
||||
import cn.smartjavaai.face.enums.IdStrategy;
|
||||
import cn.smartjavaai.face.enums.VectorDBType;
|
||||
import io.milvus.param.IndexType;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Milvus配置类
|
||||
* @author smartjavaai
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MilvusConfig extends VectorDBConfig {
|
||||
|
||||
/**
|
||||
* 服务器地址
|
||||
*/
|
||||
private String host = "localhost";
|
||||
|
||||
/**
|
||||
* 服务器端口
|
||||
*/
|
||||
private int port = 19530;
|
||||
|
||||
/**
|
||||
* 索引类型
|
||||
*/
|
||||
private IndexType indexType = IndexType.IVF_FLAT;
|
||||
|
||||
/**
|
||||
* 聚类数量,用于IVF索引
|
||||
*/
|
||||
private int nlist = 1024;
|
||||
|
||||
/**
|
||||
* 向量维度
|
||||
*/
|
||||
private int dimension;
|
||||
|
||||
/**
|
||||
* ID策略
|
||||
*/
|
||||
private IdStrategy idStrategy = IdStrategy.AUTO;
|
||||
|
||||
|
||||
/**
|
||||
* 相似度计算方式
|
||||
*/
|
||||
private MetricType metricType;
|
||||
|
||||
/**
|
||||
* 集合名称
|
||||
*/
|
||||
private String collectionName;
|
||||
|
||||
/**
|
||||
* 是否使用内存缓存
|
||||
*/
|
||||
private boolean useMemoryCache = true;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public MilvusConfig() {
|
||||
setType(VectorDBType.MILVUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param host 服务器地址
|
||||
* @param port 服务器端口
|
||||
*/
|
||||
public MilvusConfig(String host, int port) {
|
||||
this();
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.smartjavaai.face.vector.config;
|
||||
|
||||
import cn.smartjavaai.face.enums.SimilarityType;
|
||||
import cn.smartjavaai.face.enums.VectorDBType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/5/31
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SQLiteConfig extends VectorDBConfig {
|
||||
|
||||
/**
|
||||
* 数据库路径(包含文件名称)
|
||||
*/
|
||||
private String dbPath;
|
||||
|
||||
/**
|
||||
* 相似度计算方式
|
||||
*/
|
||||
private SimilarityType similarityType;
|
||||
|
||||
|
||||
public SQLiteConfig() {
|
||||
setType(VectorDBType.SQLITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.smartjavaai.face.vector.config;
|
||||
|
||||
import cn.smartjavaai.face.enums.VectorDBType;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 向量数据库基础配置
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public abstract class VectorDBConfig {
|
||||
|
||||
/**
|
||||
* 向量数据库类型
|
||||
*/
|
||||
private VectorDBType type;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.smartjavaai.face.vector.constant;
|
||||
|
||||
/**
|
||||
* 向量数据库常量类
|
||||
* @author dwj
|
||||
*/
|
||||
public class VectorDBConstants {
|
||||
/**
|
||||
* 字段名称常量
|
||||
*/
|
||||
public static class FieldNames {
|
||||
/** ID字段名 */
|
||||
public static final String ID_FIELD = "id";
|
||||
/** 向量字段名 */
|
||||
public static final String VECTOR_FIELD = "vector";
|
||||
/** 元数据字段名 */
|
||||
public static final String METADATA_FIELD = "metadata";
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置常量
|
||||
*/
|
||||
public static class Defaults {
|
||||
/** 默认搜索探针数 */
|
||||
public static final int DEFAULT_SEARCH_PARAM_NPROBE = 10;
|
||||
/** 默认向量维度 */
|
||||
public static final int DEFAULT_VECTOR_DIMENSION = 512;
|
||||
/** 默认元数据最大长度 */
|
||||
public static final int DEFAULT_METADATA_MAX_LENGTH = 32 * 1024;
|
||||
/** 默认ID字段最大长度 */
|
||||
public static final int DEFAULT_ID_MAX_LENGTH = 36;
|
||||
|
||||
/**
|
||||
* 默认集合名称
|
||||
*/
|
||||
public static final String DEFAULT_COLLECTION_NAME = "face";
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索参数常量
|
||||
*/
|
||||
public static class SearchParams {
|
||||
/** 默认相似度阈值 */
|
||||
public static final float DEFAULT_SIMILARITY_THRESHOLD = 0.7f;
|
||||
/** 默认返回TOP-K结果数 */
|
||||
public static final int DEFAULT_TOP_K = 10;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package cn.smartjavaai.face.vector.core;
|
||||
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.enums.IdStrategy;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.constant.VectorDBConstants;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
import io.milvus.client.MilvusServiceClient;
|
||||
import io.milvus.grpc.*;
|
||||
import io.milvus.param.*;
|
||||
import io.milvus.param.collection.*;
|
||||
import io.milvus.param.dml.*;
|
||||
import io.milvus.param.index.CreateIndexParam;
|
||||
import io.milvus.response.QueryResultsWrapper;
|
||||
import io.milvus.response.SearchResultsWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Milvus向量数据库客户端实现
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class MilvusClient implements VectorDBClient {
|
||||
|
||||
private final MilvusConfig config;
|
||||
private MilvusServiceClient serviceClient;
|
||||
|
||||
private String collectionName;
|
||||
|
||||
/**
|
||||
* 是否初始化完毕
|
||||
*/
|
||||
private boolean isInit;
|
||||
|
||||
public MilvusClient(MilvusConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
try {
|
||||
ConnectParam connectParam = ConnectParam.newBuilder()
|
||||
.withHost(config.getHost())
|
||||
.withPort(config.getPort())
|
||||
.build();
|
||||
serviceClient = new MilvusServiceClient(connectParam);
|
||||
collectionName = StringUtils.isNotBlank(config.getCollectionName()) ? config.getCollectionName() : VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME;
|
||||
createCollection(collectionName, config.getDimension());
|
||||
if(config.isUseMemoryCache()){
|
||||
// 加载集合到内存
|
||||
loadFaceFeatures();
|
||||
}
|
||||
isInit = true;
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("初始化Milvus客户端失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createCollection(String collectionName, int dimension) {
|
||||
try {
|
||||
if (hasCollection(collectionName)) {
|
||||
log.debug("集合已存在:{}", collectionName);
|
||||
return;
|
||||
}
|
||||
// 创建集合字段
|
||||
FieldType idField = null;
|
||||
|
||||
switch (config.getIdStrategy()){
|
||||
case AUTO://自动生成ID
|
||||
idField = FieldType.newBuilder()
|
||||
.withName(VectorDBConstants.FieldNames.ID_FIELD)
|
||||
.withDataType(DataType.Int64)
|
||||
.withPrimaryKey(true)
|
||||
.withAutoID(true)
|
||||
.build();
|
||||
break;
|
||||
case CUSTOM://自定义ID
|
||||
idField = FieldType.newBuilder()
|
||||
.withName(VectorDBConstants.FieldNames.ID_FIELD)
|
||||
.withDataType(DataType.VarChar)
|
||||
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_ID_MAX_LENGTH)
|
||||
.withPrimaryKey(true)
|
||||
.withAutoID(false)
|
||||
.build();
|
||||
break;
|
||||
}
|
||||
|
||||
FieldType vectorField = FieldType.newBuilder()
|
||||
.withName(VectorDBConstants.FieldNames.VECTOR_FIELD)
|
||||
.withDataType(DataType.FloatVector)
|
||||
.withDimension(dimension)
|
||||
.build();
|
||||
|
||||
FieldType metadataField = FieldType.newBuilder()
|
||||
.withName(VectorDBConstants.FieldNames.METADATA_FIELD)
|
||||
.withDataType(DataType.VarChar)
|
||||
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_METADATA_MAX_LENGTH)
|
||||
.withNullable(true)//允许为空值
|
||||
.build();
|
||||
|
||||
// 创建集合参数
|
||||
CreateCollectionParam createCollectionParam = CreateCollectionParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withDescription("人脸特征向量集合")
|
||||
.addFieldType(idField)
|
||||
.addFieldType(vectorField)
|
||||
.addFieldType(metadataField)
|
||||
.build();
|
||||
|
||||
R<RpcStatus> response = serviceClient.createCollection(createCollectionParam);
|
||||
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 创建集合失败:" + response.getMessage());
|
||||
}
|
||||
|
||||
log.debug("创建集合成功");
|
||||
|
||||
// 创建索引
|
||||
IndexType indexType = IndexType.IVF_FLAT;
|
||||
if (config.getIndexType() != null) {
|
||||
indexType = config.getIndexType();
|
||||
}
|
||||
|
||||
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
|
||||
.withIndexType(indexType)
|
||||
.withMetricType(config.getMetricType())
|
||||
.withExtraParam(String.format("{\"nlist\":%d}", config.getNlist()))
|
||||
.withSyncMode(Boolean.TRUE)//调用方法后等待 Milvus 执行完成
|
||||
.build();
|
||||
|
||||
R<RpcStatus> createIndexResponse = serviceClient.createIndex(indexParam);
|
||||
|
||||
if (createIndexResponse.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 创建索引失败:" + createIndexResponse.getMessage());
|
||||
}
|
||||
|
||||
log.debug("创建索引成功");
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("创建Milvus集合失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropCollection(String collectionName) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
if (hasCollection(collectionName)) {
|
||||
R<RpcStatus> response = serviceClient.dropCollection(DropCollectionParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.build());
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 删除集合失败:" + response.getMessage());
|
||||
}
|
||||
isInit = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("删除Milvus集合失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCollection(String collectionName) {
|
||||
|
||||
try {
|
||||
R<Boolean> response = serviceClient.hasCollection(
|
||||
HasCollectionParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.build()
|
||||
);
|
||||
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 查询失败:" + response.getMessage());
|
||||
}
|
||||
return Boolean.TRUE.equals(response.getData());
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("检查Milvus集合是否保存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String insert(FaceVector faceVector) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
//验证
|
||||
if(faceVector == null){
|
||||
throw new VectorDBException("插入数据失败:faceVector不能为空");
|
||||
}
|
||||
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
|
||||
throw new VectorDBException("插入数据失败:vector不能为空");
|
||||
}
|
||||
//自定义ID
|
||||
if(config.getIdStrategy() == IdStrategy.CUSTOM){
|
||||
if(StringUtils.isBlank(faceVector.getId())){
|
||||
throw new VectorDBException("插入数据失败:ID生成策略-自定义ID,id不能为空");
|
||||
}
|
||||
}
|
||||
// 转 float[] 为 List<Float>
|
||||
List<Float> vectorList = new ArrayList<>();
|
||||
for (float v : faceVector.getVector()) {
|
||||
vectorList.add(v);
|
||||
}
|
||||
|
||||
List<List<Float>> vectors = Collections.singletonList(vectorList);
|
||||
//List<String> metadataList = Collections.singletonList(faceVector.getMetadata());
|
||||
|
||||
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
|
||||
.map(Collections::singletonList)
|
||||
.orElse(Collections.emptyList());
|
||||
List<InsertParam.Field> fields = null;
|
||||
switch (config.getIdStrategy()){
|
||||
case AUTO://自动生成ID
|
||||
fields = Arrays.asList(
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
|
||||
);
|
||||
break;
|
||||
case CUSTOM://自定义ID
|
||||
List<String> ids = Collections.singletonList(faceVector.getId());
|
||||
fields = Arrays.asList(
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
InsertParam insertParam = InsertParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withFields(fields)
|
||||
.build();
|
||||
|
||||
R<MutationResult> response = serviceClient.insert(insertParam);
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("插入失败: " + response.getMessage());
|
||||
}
|
||||
|
||||
List<Long> autoIds = response.getData().getIDs().getIntId().getDataList();
|
||||
|
||||
|
||||
return config.getIdStrategy() == IdStrategy.AUTO ? String.valueOf(autoIds.get(0)) : faceVector.getId();
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("插入Milvus向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsert(FaceVector faceVector) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
if(config.getIdStrategy() == IdStrategy.AUTO){
|
||||
throw new VectorDBException("idStrategy为AUTO时,不支持更新操作");
|
||||
}
|
||||
//验证
|
||||
if(faceVector == null){
|
||||
throw new VectorDBException("更新数据失败:faceVector不能为空");
|
||||
}
|
||||
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
|
||||
throw new VectorDBException("更新数据失败:vector不能为空");
|
||||
}
|
||||
if(StringUtils.isBlank(faceVector.getId())){
|
||||
throw new VectorDBException("更新数据失败:id不能为空");
|
||||
}
|
||||
|
||||
// 转换向量为 List<Float>
|
||||
List<Float> vectorList = new ArrayList<>();
|
||||
for (float v : faceVector.getVector()) {
|
||||
vectorList.add(v);
|
||||
}
|
||||
|
||||
List<List<Float>> vectors = Collections.singletonList(vectorList);
|
||||
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
|
||||
.map(Collections::singletonList)
|
||||
.orElse(Collections.emptyList());
|
||||
|
||||
// 准备字段列表
|
||||
List<UpsertParam.Field> fields = new ArrayList<>();
|
||||
|
||||
fields.add(UpsertParam.Field.builder()
|
||||
.name(VectorDBConstants.FieldNames.ID_FIELD)
|
||||
.values(Collections.singletonList(faceVector.getId()))
|
||||
.build());
|
||||
|
||||
// 添加向量和元数据字段
|
||||
fields.add(UpsertParam.Field.builder()
|
||||
.name(VectorDBConstants.FieldNames.VECTOR_FIELD)
|
||||
.values(vectors)
|
||||
.build());
|
||||
|
||||
fields.add(UpsertParam.Field.builder()
|
||||
.name(VectorDBConstants.FieldNames.METADATA_FIELD)
|
||||
.values(metadataList)
|
||||
.build());
|
||||
|
||||
// 构建Upsert参数
|
||||
UpsertParam upsertParam = UpsertParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withFields(fields)
|
||||
.build();
|
||||
|
||||
// 执行Upsert操作
|
||||
R<MutationResult> response = serviceClient.upsert(upsertParam);
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Upsert操作失败: " + response.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("Milvus Upsert操作失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<String> insertBatch(List<FaceVector> faceVectors) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
List<String> ids = faceVectors.stream()
|
||||
.map(FaceVector::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
List<List<Float>> vectors = new ArrayList<>();
|
||||
for (FaceVector fv : faceVectors) {
|
||||
List<Float> list = new ArrayList<>();
|
||||
for (float f : fv.getVector()) {
|
||||
list.add(f);
|
||||
}
|
||||
vectors.add(list);
|
||||
}
|
||||
|
||||
List<String> metadataList = faceVectors.stream()
|
||||
.map(FaceVector::getMetadata)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
InsertParam insertParam = InsertParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withFields(Arrays.asList(
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
|
||||
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
|
||||
))
|
||||
.build();
|
||||
|
||||
R<MutationResult> insertResult = serviceClient.insert(insertParam);
|
||||
return ids;
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("批量插入Milvus向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
String expr = String.format("%s == \"%s\"", VectorDBConstants.FieldNames.ID_FIELD, id);
|
||||
DeleteParam deleteParam = DeleteParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withExpr(expr)
|
||||
.build();
|
||||
|
||||
R<MutationResult> response = serviceClient.delete(deleteParam);
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("删除操作失败: " + response.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("删除Milvus向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBatch(List<String> ids) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
// 构建IN表达式: id in ["id1", "id2", ...]
|
||||
StringBuilder expr = new StringBuilder(VectorDBConstants.FieldNames.ID_FIELD + " in [");
|
||||
for (int i = 0; i < ids.size(); i++) {
|
||||
expr.append(ids.get(i)); // 不加引号
|
||||
if (i < ids.size() - 1) {
|
||||
expr.append(", ");
|
||||
}
|
||||
}
|
||||
expr.append("]");
|
||||
DeleteParam deleteParam = DeleteParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withExpr(expr.toString())
|
||||
.build();
|
||||
|
||||
R<MutationResult> response = serviceClient.delete(deleteParam);
|
||||
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("删除操作失败: " + response.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("批量删除Milvus向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
// 1. 包装查询向量
|
||||
List<List<Float>> vectors = new ArrayList<>();
|
||||
List<Float> floatList = new ArrayList<>();
|
||||
for (float f : queryVector) {
|
||||
floatList.add(f);
|
||||
}
|
||||
vectors.add(floatList);
|
||||
|
||||
// 2. 构造搜索参数
|
||||
SearchParam searchParam = SearchParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withVectorFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
|
||||
.withTopK(faceSearchParams.getTopK())
|
||||
.withMetricType(config.getMetricType())
|
||||
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
|
||||
.withVectors(vectors)
|
||||
.withParams("{\"nprobe\": 10}")//和nlist有关
|
||||
.build();
|
||||
R<SearchResults> resp = serviceClient.search(searchParam);
|
||||
if (resp.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 查询失败: " + resp.getMessage());
|
||||
}
|
||||
|
||||
SearchResults results = resp.getData();
|
||||
SearchResultsWrapper wrapper = new SearchResultsWrapper(results.getResults());
|
||||
|
||||
// 3. 获取字段数据(FieldData)
|
||||
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0); // 默认只有一条 query 向量
|
||||
|
||||
|
||||
List<FaceSearchResult> finalResults = new ArrayList<>();
|
||||
for (int i = 0; i < scores.size(); i++) {
|
||||
SearchResultsWrapper.IDScore score = scores.get(i);
|
||||
float similarity = score.getScore();
|
||||
if (faceSearchParams.getNormalizeSimilarity()) {
|
||||
// 将分数转换为相似度
|
||||
similarity = FaceUtils.convertScoreToSimilarity(config.getMetricType().name(), score.getScore());
|
||||
}
|
||||
if (similarity >= faceSearchParams.getThreshold()) {
|
||||
// 获取 Metadata
|
||||
String metadata = wrapper.getFieldData(VectorDBConstants.FieldNames.METADATA_FIELD, i).get(0).toString();
|
||||
// 获取 ID
|
||||
String id = wrapper.getFieldData(VectorDBConstants.FieldNames.ID_FIELD, i).get(0).toString();
|
||||
finalResults.add(new FaceSearchResult(id, similarity, metadata));
|
||||
}
|
||||
}
|
||||
return finalResults;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("搜索 Milvus 向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(String collectionName) {
|
||||
try {
|
||||
if (serviceClient == null){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
R<QueryResults> response = serviceClient.query(
|
||||
QueryParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withOutFields(Collections.singletonList("count(*)"))
|
||||
.build()
|
||||
);
|
||||
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 查询失败,msg: " + response.getMessage());
|
||||
}
|
||||
|
||||
List<FieldData> fields = response.getData().getFieldsDataList();
|
||||
if (fields.isEmpty()) {
|
||||
throw new VectorDBException("Milvus 返回空字段数据");
|
||||
}
|
||||
|
||||
FieldData countField = fields.get(0);
|
||||
List<Long> countValues = countField.getScalars().getLongData().getDataList();
|
||||
if (countValues.isEmpty()) {
|
||||
throw new VectorDBException("Milvus count(*) 返回为空");
|
||||
}
|
||||
|
||||
return countValues.get(0); // count(*) 查询的结果
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("获取 Milvus 集合数量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (serviceClient != null) {
|
||||
serviceClient.close();
|
||||
isInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseCollection(String collectionName) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
R<RpcStatus> response = serviceClient.releaseCollection(ReleaseCollectionParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.build());
|
||||
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus releaseCollection失败,msg: " + response.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public FaceSearchResult getById(String id) {
|
||||
try {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("Milvus未初始化完毕");
|
||||
}
|
||||
// 构造搜索参数
|
||||
SearchParam searchParam = SearchParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
|
||||
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
|
||||
.build();
|
||||
|
||||
|
||||
// 5. 执行查询
|
||||
R<QueryResults> response = serviceClient.query(
|
||||
QueryParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
|
||||
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
|
||||
.build()
|
||||
);
|
||||
|
||||
// 处理响应
|
||||
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("查询失败: " + response.getMessage());
|
||||
}
|
||||
|
||||
QueryResultsWrapper wrapper = new QueryResultsWrapper(response.getData());
|
||||
List<QueryResultsWrapper.RowRecord> records = wrapper.getRowRecords();
|
||||
if (records.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 提取第一条记录
|
||||
QueryResultsWrapper.RowRecord row = records.get(0);
|
||||
return new FaceSearchResult(id, 1,(String)row.get(VectorDBConstants.FieldNames.METADATA_FIELD));
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("搜索 Milvus 向量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFaceFeatures() {
|
||||
// 加载集合到内存
|
||||
R<RpcStatus> loadResponse = serviceClient.loadCollection(LoadCollectionParam.newBuilder()
|
||||
.withCollectionName(collectionName)
|
||||
.build());
|
||||
if (loadResponse.getStatus() != R.Status.Success.getCode()) {
|
||||
throw new VectorDBException("Milvus 加载集合到内存失败:" + loadResponse.getMessage());
|
||||
}
|
||||
long count = count(collectionName);
|
||||
log.debug("加载集合到内存成功,人脸数量:{}", count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseFaceFeatures() {
|
||||
releaseCollection(collectionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package cn.smartjavaai.face.vector.core;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.face.dao.FaceDao;
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import cn.smartjavaai.face.utils.SimilarityUtil;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class SQLiteClient implements VectorDBClient {
|
||||
|
||||
private final FaceDao faceDao;
|
||||
private final List<FaceVector> memoryIndex = new CopyOnWriteArrayList<>();
|
||||
private int featureDimension; // 维度
|
||||
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
|
||||
private SQLiteConfig config;
|
||||
|
||||
/**
|
||||
* 是否初始化完毕
|
||||
*/
|
||||
private boolean isInit;
|
||||
|
||||
public SQLiteClient(SQLiteConfig config) {
|
||||
this.config = config;
|
||||
String dbPath = config.getDbPath();
|
||||
//如果未指定db路径,则使用默认路径
|
||||
if(StringUtils.isBlank(config.getDbPath())){
|
||||
dbPath = Config.getCachePath() + File.separator + "face.db";
|
||||
log.debug("使用默认SQLite人脸库路径: {}", dbPath);
|
||||
}
|
||||
this.faceDao = FaceDao.getInstance(dbPath);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
try {
|
||||
// 加载所有特征到内存
|
||||
loadAllFeaturesToMemory();
|
||||
isInit = true;
|
||||
log.debug("SQLiteVectorDB initialized with {} faces", memoryIndex.size());
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 以下方法保持接口兼容但忽略collectionName参数
|
||||
@Override
|
||||
public void createCollection(String collectionName, int dimension) {
|
||||
this.featureDimension = dimension;
|
||||
log.debug("特征维度设置为: {}", dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropCollection(String collectionName) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
clearAllData();
|
||||
log.warn("所有数据已被清空");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCollection(String collectionName) {
|
||||
throw new UnsupportedOperationException("Sqlite 不支持此操作");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String insert(FaceVector faceVector) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
return insertBatch(Collections.singletonList(faceVector)).get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upsert(FaceVector faceVector) {
|
||||
// if (faceVector.getId() != null) {
|
||||
// delete(faceVector.getId());
|
||||
// }
|
||||
insert(faceVector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> insertBatch(List<FaceVector> faceVectors) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
List<String> ids = new ArrayList<>();
|
||||
try {
|
||||
for (FaceVector faceVector : faceVectors) {
|
||||
String id = faceVector.getId() != null ?
|
||||
faceVector.getId() : IdUtil.simpleUUID();
|
||||
faceVector.setId(id);
|
||||
// 保存到数据库
|
||||
faceDao.insertOrUpdate(faceVector);
|
||||
// 添加到内存索引
|
||||
addToMemoryIndex(faceVector);
|
||||
ids.add(id);
|
||||
}
|
||||
log.debug("插入了 {} 个人脸向量", faceVectors.size());
|
||||
return ids;
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("批量插入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
deleteBatch(Collections.singletonList(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBatch(List<String> ids) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
try {
|
||||
// 从数据库中删除
|
||||
boolean isSuccess = faceDao.deleteFace(ids.toArray(new String[0]));
|
||||
// 从内存中删除
|
||||
memoryIndex.removeIf(v -> ids.contains(v.getId()));
|
||||
if(!isSuccess){
|
||||
throw new VectorDBException("删除失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("批量删除失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
if (memoryIndex.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 并行计算相似度
|
||||
List<CompletableFuture<FaceSearchResult>> futures = memoryIndex.stream()
|
||||
.map(vector -> CompletableFuture.supplyAsync(() -> {
|
||||
float similarity = SimilarityUtil.calculate(queryVector, vector.getVector(), config.getSimilarityType(), faceSearchParams.getNormalizeSimilarity());
|
||||
return similarity >= faceSearchParams.getThreshold() ?
|
||||
new FaceSearchResult(vector.getId(), similarity, vector.getMetadata()) :
|
||||
null;
|
||||
}, executor))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 收集结果并过滤null
|
||||
List<FaceSearchResult> allResults = futures.stream()
|
||||
.map(CompletableFuture::join)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取TopK结果
|
||||
return allResults.stream()
|
||||
.sorted(Comparator.comparingDouble(FaceSearchResult::getSimilarity).reversed())
|
||||
.limit(faceSearchParams.getTopK())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count(String collectionName) {
|
||||
return memoryIndex.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// ============= 私有辅助方法 =============
|
||||
|
||||
private void loadAllFeaturesToMemory() {
|
||||
try {
|
||||
int pageSize = 1000;
|
||||
int page = 0;
|
||||
while (true) {
|
||||
List<FaceVector> batch = faceDao.findFace(page, pageSize);
|
||||
if (CollectionUtils.isEmpty(batch)) {
|
||||
break;
|
||||
}
|
||||
for (FaceVector vector : batch) {
|
||||
addToMemoryIndex(vector);
|
||||
}
|
||||
page++;
|
||||
}
|
||||
log.debug("从数据库加载了 {} 个特征向量到内存", memoryIndex.size());
|
||||
} catch (Exception e) {
|
||||
throw new VectorDBException("加载特征到内存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addToMemoryIndex(FaceVector faceVector) {
|
||||
memoryIndex.add(faceVector);
|
||||
}
|
||||
|
||||
private void clearAllData() {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
try {
|
||||
faceDao.deleteAll();
|
||||
memoryIndex.clear();
|
||||
} catch (Exception e) {
|
||||
log.error("清空数据库失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaceSearchResult getById(String id) {
|
||||
try {
|
||||
FaceVector faceVector = faceDao.findById(id);
|
||||
if(faceVector != null){
|
||||
return new FaceSearchResult(faceVector.getId(), 1.0f, faceVector.getMetadata());
|
||||
}
|
||||
return null;
|
||||
} catch (SQLException | RuntimeException | ClassNotFoundException e ) {
|
||||
throw new VectorDBException("SQLite查询异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFaceFeatures() {
|
||||
// 加载所有特征到内存
|
||||
loadAllFeaturesToMemory();
|
||||
isInit = true;
|
||||
log.debug("SQLiteVectorDB load success {} faces", memoryIndex.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseFaceFeatures() {
|
||||
if (!isInit){
|
||||
throw new VectorDBException("人脸库未加载完毕");
|
||||
}
|
||||
memoryIndex.clear();
|
||||
isInit = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package cn.smartjavaai.face.vector.core;
|
||||
|
||||
import cn.smartjavaai.face.entity.FaceSearchParams;
|
||||
import cn.smartjavaai.face.vector.entity.FaceVector;
|
||||
import cn.smartjavaai.common.entity.FaceSearchResult;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 向量数据库客户端接口
|
||||
* 定义与向量数据库交互的通用操作
|
||||
* @author dwj
|
||||
*/
|
||||
public interface VectorDBClient extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* 初始化连接和集合
|
||||
* @throws VectorDBException 初始化异常
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* 创建集合
|
||||
* @param collectionName 集合名称
|
||||
* @param dimension 向量维度
|
||||
*/
|
||||
void createCollection(String collectionName, int dimension);
|
||||
|
||||
/**
|
||||
* 删除集合
|
||||
* @param collectionName 集合名称
|
||||
*/
|
||||
void dropCollection(String collectionName);
|
||||
|
||||
/**
|
||||
* 检查集合是否存在
|
||||
* @param collectionName 集合名称
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean hasCollection(String collectionName);
|
||||
|
||||
|
||||
/**
|
||||
* 插入人脸向量
|
||||
* @param faceVector
|
||||
* @return
|
||||
*/
|
||||
String insert(FaceVector faceVector);
|
||||
|
||||
/**
|
||||
* 更新或新增人脸向量
|
||||
* @param faceVector
|
||||
*/
|
||||
void upsert(FaceVector faceVector);
|
||||
|
||||
/**
|
||||
* 批量插入人脸向量
|
||||
* @param faceVectors
|
||||
* @return
|
||||
*/
|
||||
List<String> insertBatch(List<FaceVector> faceVectors);
|
||||
|
||||
/**
|
||||
* 根据ID删除向量
|
||||
* @param id
|
||||
*/
|
||||
void delete(String id);
|
||||
|
||||
/**
|
||||
* 批量删除向量
|
||||
* @param ids
|
||||
*/
|
||||
void deleteBatch(List<String> ids);
|
||||
|
||||
/**
|
||||
* 搜索相似人脸
|
||||
* @param queryVector
|
||||
* @param faceSearchParams
|
||||
* @return
|
||||
*/
|
||||
List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams);
|
||||
|
||||
/**
|
||||
* 获取集合中的向量数量
|
||||
* @param collectionName 集合名称
|
||||
* @return 向量数量
|
||||
*/
|
||||
long count(String collectionName);
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定ID的向量
|
||||
* @return
|
||||
*/
|
||||
FaceSearchResult getById(String id);
|
||||
|
||||
/**
|
||||
* 加载人脸特征到内存
|
||||
*/
|
||||
void loadFaceFeatures();
|
||||
|
||||
/**
|
||||
* 释放人脸特征缓存
|
||||
*/
|
||||
void releaseFaceFeatures();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.smartjavaai.face.vector.core;
|
||||
|
||||
|
||||
|
||||
import cn.smartjavaai.face.vector.config.MilvusConfig;
|
||||
import cn.smartjavaai.face.vector.config.SQLiteConfig;
|
||||
import cn.smartjavaai.face.vector.config.VectorDBConfig;
|
||||
import cn.smartjavaai.face.vector.exception.VectorDBException;
|
||||
|
||||
/**
|
||||
* 向量数据库工厂类
|
||||
* 用于创建不同类型的向量数据库客户端
|
||||
* @author dwj
|
||||
*/
|
||||
public class VectorDBFactory {
|
||||
|
||||
private VectorDBFactory() {
|
||||
// 私有构造函数,防止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建向量数据库客户端
|
||||
* @param config 配置信息
|
||||
* @return 向量数据库客户端
|
||||
* @throws VectorDBException 创建异常
|
||||
*/
|
||||
public static VectorDBClient createClient(VectorDBConfig config) {
|
||||
if (config == null) {
|
||||
throw new VectorDBException("配置不能为空");
|
||||
}
|
||||
VectorDBClient client;
|
||||
|
||||
switch (config.getType()) {
|
||||
case SQLITE:
|
||||
if (!(config instanceof SQLiteConfig)) {
|
||||
throw new VectorDBException("SQLite类型需要SQLiteConfig配置");
|
||||
}
|
||||
client = new SQLiteClient((SQLiteConfig) config);
|
||||
break;
|
||||
case MILVUS:
|
||||
if (!(config instanceof MilvusConfig)) {
|
||||
throw new VectorDBException("Milvus类型需要MilvusConfig配置");
|
||||
}
|
||||
client = new MilvusClient((MilvusConfig) config);
|
||||
break;
|
||||
// 未来可以在这里添加其他向量数据库的支持
|
||||
default:
|
||||
throw new VectorDBException("不支持的向量数据库类型: " + config.getType());
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.smartjavaai.face.vector.entity;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 人脸向量实体类
|
||||
* @author smartjavaai
|
||||
*/
|
||||
@Data
|
||||
public class FaceVector {
|
||||
|
||||
/**
|
||||
* 向量ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 人脸特征向量
|
||||
*/
|
||||
private float[] vector;
|
||||
|
||||
/**
|
||||
* 元数据,可以存储人脸相关的其他信息(JSON格式)
|
||||
*/
|
||||
private String metadata;
|
||||
|
||||
/**
|
||||
* 默认构造函数
|
||||
*/
|
||||
public FaceVector() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param vector 人脸特征向量
|
||||
*/
|
||||
public FaceVector(float[] vector) {
|
||||
this();
|
||||
this.vector = vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param vector 人脸特征向量
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public FaceVector(float[] vector, String metadata) {
|
||||
this();
|
||||
this.vector = vector;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param id 向量ID
|
||||
* @param vector 人脸特征向量
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public FaceVector(String id, float[] vector, String metadata) {
|
||||
this.id = id;
|
||||
this.vector = vector;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.smartjavaai.face.vector.exception;
|
||||
|
||||
|
||||
/**
|
||||
* 向量数据库异常
|
||||
* @author smartjavaai
|
||||
*/
|
||||
public class VectorDBException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param message 异常信息
|
||||
*/
|
||||
public VectorDBException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param message 异常信息
|
||||
* @param cause 原始异常
|
||||
*/
|
||||
public VectorDBException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
14
smartjavaai-face/src/main/resources/db/schema.sql
Normal file
14
smartjavaai-face/src/main/resources/db/schema.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
PRAGMA foreign_keys = false;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for face
|
||||
-- ----------------------------
|
||||
CREATE TABLE "face" (
|
||||
"id" TEXT NOT NULL,
|
||||
"vector" blob NOT NULL,
|
||||
"metadata" TEXT,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "id" UNIQUE ("id" ASC)
|
||||
);
|
||||
|
||||
PRAGMA foreign_keys = true;
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<name>smartjavaai-objectdetection</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -57,7 +57,7 @@ public class DetectorModel implements AutoCloseable{
|
||||
model = criteria.loadModel();
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
|
||||
log.info("当前设备: " + model.getNDManager().getDevice());
|
||||
log.debug("当前设备: " + model.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new DetectionException("模型加载失败", e);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ public class DetectorModel implements AutoCloseable{
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
log.info("释放资源");
|
||||
log.debug("释放资源");
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ObjectDetectionModelFactory {
|
||||
private static final ConcurrentHashMap<String, DetectorModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
static{
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
// 私有构造函数,防止外部创建实例
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-ocr</artifactId>
|
||||
@@ -20,7 +20,7 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<version>1.0.15</version>
|
||||
<version>1.0.16</version>
|
||||
<name>smartjavaai-ocr</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -205,7 +205,7 @@ public class OcrModelFactory {
|
||||
registerCommonDetModel("PADDLEOCR_V5_DET_MODEL", PpOCRV5DetModel.class);
|
||||
registerCommonRecModel("PADDLEOCR_V5_REC_MODEL", PpOCRV5RecModel.class);
|
||||
registerDirectionModel("CH_PPOCR_MOBILE_V2_CLS", PPOCRMobileV2Model.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ public class PpOCRV5DetModel implements OcrCommonDetModel {
|
||||
ZooModel detectionModel = ModelZoo.loadModel(detCriteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.detPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
log.debug("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
log.debug("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("检测模型加载失败", e);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public class PpOCRV5DetModel implements OcrCommonDetModel {
|
||||
}
|
||||
OcrUtils.drawRect((Mat)img.getWrappedImage(), boxList);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
log.debug("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -89,8 +89,8 @@ public class PPOCRMobileV2Model implements OcrDirectionModel {
|
||||
ZooModel model = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
|
||||
log.info("当前设备: " + model.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
log.debug("当前设备: " + model.getNDManager().getDevice());
|
||||
log.debug("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ public class PPOCRMobileV2Model implements OcrDirectionModel {
|
||||
}
|
||||
OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), itemList);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
log.debug("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -90,8 +90,8 @@ public class PpOCRV5RecModel implements OcrCommonRecModel {
|
||||
try{
|
||||
ZooModel recognitionModel = ModelZoo.loadModel(recCriteria);
|
||||
this.recPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(recognitionModel));
|
||||
log.info("当前设备: " + recognitionModel.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
log.debug("当前设备: " + recognitionModel.getNDManager().getDevice());
|
||||
log.debug("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("识别模型加载失败", e);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class OcrUtils {
|
||||
List<OcrBox> boxList = new ArrayList<OcrBox>();
|
||||
for(NDArray box : dt_boxes){
|
||||
float[] pointsArr = box.toFloatArray();
|
||||
//log.info("points: {}", pointsArr);
|
||||
//log.debug("points: {}", pointsArr);
|
||||
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
|
||||
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
|
||||
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
|
||||
|
||||
Reference in New Issue
Block a user