diff --git a/README.md b/README.md
index 081e3d0..16299ec 100644
--- a/README.md
+++ b/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 开发者打造的一个功能丰富、开箱即用的
cn.smartjavaai
smartjavaai-all
- 1.0.15
+ 1.0.16
```
### 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 运算
-- 人脸识别模块新增多种接口,功能更加完善
-
-
diff --git a/examples/pom.xml b/examples/pom.xml
index 38216a9..4865b50 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -12,7 +12,7 @@
11
11
UTF-8
- 1.0.15
+ 1.0.16
smartai.examples.face.facerec.RetinaFaceDemo
diff --git a/examples/src/main/java/smartai/examples/face/facerec/FaceNetDemo.java b/examples/src/main/java/smartai/examples/face/facerec/FaceNetDemo.java
index eaaec70..d56ce63 100644
--- a/examples/src/main/java/smartai/examples/face/facerec/FaceNetDemo.java
+++ b/examples/src/main/java/smartai/examples/face/facerec/FaceNetDemo.java
@@ -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 faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
- log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
+ log.info("人脸特征提取模型加载成功");
+ //提取图片中所有人脸特征
+ R 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 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 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 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 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 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 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 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 faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
+// R 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 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 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 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 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();
diff --git a/examples/src/main/java/smartai/examples/face/facerec/SeetaFace6Demo.java b/examples/src/main/java/smartai/examples/face/facerec/SeetaFace6Demo.java
index 34441e2..3cb3d21 100644
--- a/examples/src/main/java/smartai/examples/face/facerec/SeetaFace6Demo.java
+++ b/examples/src/main/java/smartai/examples/face/facerec/SeetaFace6Demo.java
@@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 {
}
+
}
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
index ed2d7ed..151fc23 100644
--- a/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
+++ b/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
@@ -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 boxes = model.detect("src/main/resources/ocr_1.jpg");
log.info("OCR检测结果:{}", JSONObject.toJSONString(boxes));
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
index 7cd68bc..56c3de8 100644
--- a/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
+++ b/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
@@ -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 itemList = directionModel.detect("src/main/resources/ocr_3.jpg");
log.info("OCR方向检测结果:{}", JSONObject.toJSONString(itemList));
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
index 3901083..4c176cf 100644
--- a/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
+++ b/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
@@ -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);
}
diff --git a/examples/src/main/resources/face/iu_1.jpg b/examples/src/main/resources/face/iu_1.jpg
new file mode 100644
index 0000000..06bb29f
Binary files /dev/null and b/examples/src/main/resources/face/iu_1.jpg differ
diff --git a/examples/src/main/resources/face/iu_2.jpg b/examples/src/main/resources/face/iu_2.jpg
new file mode 100644
index 0000000..19ad235
Binary files /dev/null and b/examples/src/main/resources/face/iu_2.jpg differ
diff --git a/examples/src/main/resources/face/iu_3.jpg b/examples/src/main/resources/face/iu_3.jpg
new file mode 100644
index 0000000..2d5906b
Binary files /dev/null and b/examples/src/main/resources/face/iu_3.jpg differ
diff --git a/pom.xml b/pom.xml
index 9f087ed..6d4e7c9 100644
--- a/pom.xml
+++ b/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">
4.0.0
+ SmartJavaAI
cn.smartjavaai
smartjavaai-parent
- 1.0.15
+ 1.0.16
pom
SmartJavaAI
diff --git a/smartjavaai-all/pom.xml b/smartjavaai-all/pom.xml
index 4b4dca0..f6a9712 100644
--- a/smartjavaai-all/pom.xml
+++ b/smartjavaai-all/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.15
+ 1.0.16
smartjavaai-all
- 1.0.15
+ 1.0.16
${project.artifactId}
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-bom/pom.xml b/smartjavaai-bom/pom.xml
index 3bb75ed..65f09f9 100644
--- a/smartjavaai-bom/pom.xml
+++ b/smartjavaai-bom/pom.xml
@@ -6,10 +6,10 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.15
+ 1.0.16
- 1.0.15
+ 1.0.16
smartjavaai-bom
smartjavaai-bom
统一版本管理的 BOM 包,同时支持 import 和全量依赖
diff --git a/smartjavaai-common/pom.xml b/smartjavaai-common/pom.xml
index 4358e15..73804e7 100644
--- a/smartjavaai-common/pom.xml
+++ b/smartjavaai-common/pom.xml
@@ -6,9 +6,10 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.15
+ 1.0.16
+ smartjavaai-common
smartjavaai-common
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
index 7837fa6..394f3ea 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionResponse.java
@@ -14,6 +14,7 @@ public class DetectionResponse {
private List detectionInfoList;
+
public DetectionResponse() {
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java
index 70dc7bf..e3d9261 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceInfo.java
@@ -28,6 +28,16 @@ public class FaceInfo {
*/
private LivenessStatus livenessStatus;
+ /**
+ * 人脸查询结果
+ */
+ private List faceSearchResults;
+
+ /**
+ * 人脸特征
+ */
+ private float[] feature;
+
public FaceInfo() {
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceSearchResult.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceSearchResult.java
new file mode 100644
index 0000000..c0f6aab
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceSearchResult.java
@@ -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;
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/R.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/R.java
new file mode 100644
index 0000000..02ac9cb
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/R.java
@@ -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 {
+
+ private Integer code;
+ private String message;
+ private T data;
+
+
+
+ public static R ok(T data) {
+ R r = new R<>();
+ r.code = 0;
+ r.message = "成功";
+ r.data = data;
+ return r;
+ }
+
+ public static R fail(Integer code, String message) {
+ R r = new R<>();
+ r.code = code;
+ r.message = message;
+ r.data = null;
+ return r;
+ }
+
+ public static R fail(Status status) {
+ R 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 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);
+ }
+
+}
diff --git a/smartjavaai-face/pom.xml b/smartjavaai-face/pom.xml
index 3d07ae9..70ef54b 100644
--- a/smartjavaai-face/pom.xml
+++ b/smartjavaai-face/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.15
+ 1.0.16
smartjavaai-face
- 1.0.15
+ 1.0.16
smartjavaai-face
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
@@ -50,6 +50,12 @@
3.36.0.3
+
+ io.milvus
+ milvus-sdk-java
+ 2.5.7
+
+
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
index 53aa3a4..c4ff178 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceExtractConfig.java
@@ -19,7 +19,7 @@ public class FaceExtractConfig {
/**
* 是否对齐人脸
*/
- private boolean align = true;
+ private boolean align = false;
/**
* 人脸检测模型
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
index 970e466..67717fa 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
@@ -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() {
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
index 202bc2f..e0c1bdc 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/constant/FaceDetectConstant.java
@@ -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;
+
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/dao/FaceDao.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/dao/FaceDao.java
index e3acd7e..5a0c035 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/dao/FaceDao.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/dao/FaceDao.java
@@ -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 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 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 faceVectors = sqliteHelper.executeQuery(sql, new RowMapper() {
+ @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 findIndexList(String... keys) throws SQLException, ClassNotFoundException {
+ public List 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() {
+ String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME + " where \"id\" in (" + inKeys + ")";
+ log.debug("sql:{}", sql);
+ SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
+ List faceVectors = sqliteHelper.executeQuery(sql, new RowMapper() {
@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 findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
- String sql = "select \"key\",\"img_data\",\"width\",\"height\",\"channel\" from " + TABLE_NAME_IMG +
+ public List 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() {
+ SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
+ return sqliteHelper.executeQuery(sql, new RowMapper() {
@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实例已关闭");
}
-
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceRegisterInfo.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceRegisterInfo.java
new file mode 100644
index 0000000..6cc819a
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceRegisterInfo.java
@@ -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() {
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceSearchParams.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceSearchParams.java
new file mode 100644
index 0000000..d65e70a
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceSearchParams.java
@@ -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;
+ }
+}
+
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
index 629a722..38521ca 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/FaceModelEnum.java
@@ -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;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/IdStrategy.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/IdStrategy.java
new file mode 100644
index 0000000..2ecd3e3
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/IdStrategy.java
@@ -0,0 +1,13 @@
+package cn.smartjavaai.face.enums;
+
+/**
+ * ID生成策略
+ * @author dwj
+ * @date 2025/5/29
+ */
+public enum IdStrategy {
+
+ AUTO, // 自动生成
+ CUSTOM // 用户自定义 ID(由 config.idValue 指定)
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/SimilarityType.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/SimilarityType.java
new file mode 100644
index 0000000..805350a
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/SimilarityType.java
@@ -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)
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/VectorDBType.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/VectorDBType.java
new file mode 100644
index 0000000..c928661
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/VectorDBType.java
@@ -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
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
index 116b9d8..695bd27 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
@@ -91,7 +91,7 @@ public class FaceAttributeModelFactory {
// 初始化默认算法
static {
registerModel("seetaface6model", Seetaface6FaceAttributeModel.class);
- log.info("缓存目录:{}", Config.getCachePath());
+ log.debug("缓存目录:{}", Config.getCachePath());
}
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
index 7b1b549..9d46fe9 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
@@ -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());
}
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
index d6447c3..2a066cc 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
@@ -95,7 +95,7 @@ public class LivenessModelFactory {
// 初始化默认算法
static {
registerModel("seetaface6model", Seetaface6LivenessModel.class);
- log.info("缓存目录:{}", Config.getCachePath());
+ log.debug("缓存目录:{}", Config.getCachePath());
}
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java
index ad53426..263a9b7 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/attribute/Seetaface6FaceAttributeModel.java
@@ -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"};
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
deleted file mode 100644
index 53b5d88..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
+++ /dev/null
@@ -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 extractFeatures(String imagePath) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public List extractFeatures(byte[] imageData) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- @Override
- public List 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("默认不支持该功能");
- }
-
-
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
index 01eae36..54b9349 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
@@ -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 register(FaceRegisterInfo faceRegisterInfo, String imagePath){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
* 注册人脸
- * @param key
+ * 提取分数最高人脸进行注册
+ * @param faceRegisterInfo 注册人脸信息
* @param inputStream
* @return
*/
- boolean register(String key, InputStream inputStream);
+ default R register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
* 注册人脸
- * @param key
+ * 提取分数最高人脸进行注册
+ * @param faceRegisterInfo 注册人脸信息
* @param sourceImage
* @return
*/
- boolean register(String key, BufferedImage sourceImage);
+ default R register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
* 注册人脸
- * @param key
+ * 提取分数最高人脸进行注册
+ * @param faceRegisterInfo 注册人脸信息
* @param imageData
* @return
*/
- boolean register(String key, byte[] imageData);
+ default R register(FaceRegisterInfo faceRegisterInfo, byte[] imageData){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 查询人脸
+ * 注册人脸
+ * 提取分数最高人脸进行注册
+ * @param faceRegisterInfo 注册人脸信息
+ * @param feature 人脸特征
+ * @return
+ */
+ default R 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 search(String imagePath, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 查询人脸(查询图片中所有人脸)
+ * 适用于多人脸场景
+ * @param sourceImage
+ * @return
+ */
+ default R search(BufferedImage sourceImage, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 查询人脸(查询图片中所有人脸)
+ * 适用于多人脸场景
+ * @param imageData
+ * @return
+ */
+ default R search(byte[] imageData, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 查询人脸
+ * 适用于多人脸场景
+ * @param feature 人脸特征
+ * @return
+ */
+ default List search(float[] feature, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 查询人脸
+ * 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
+ * 适用于单人脸场景
+ * @param imagePath
+ * @param params 人脸查询参数
+ * @return
+ */
+ default R> searchByTopFace(String imagePath, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 查询人脸
+ * 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
+ * 适用于单人脸场景
+ * @param sourceImage
+ * @return
+ */
+ default R> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 查询人脸
+ * 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
+ * 适用于单人脸场景
+ * @param imageData
+ * @return
+ */
+ default R> 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 extractFeatures(String imagePath);
+ default R extractFeatures(String imagePath){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 特征提取(使用默认配置)
+ * 特征提取(所有人脸)
+ * 适用于多人脸场景
* @param imageData 图片字节流
* @return
*/
- List extractFeatures(byte[] imageData);
+ default R extractFeatures(byte[] imageData){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 特征提取(使用默认配置)
+ * 特征提取(所有人脸)
+ * 适用于多人脸场景
* @param image BufferedImage
* @return
*/
- List extractFeatures(BufferedImage image);
+ default R extractFeatures(BufferedImage image){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 提取分数最高人脸特征(使用默认配置)
+ * 特征提取(提取分数最高人脸特征)
+ * 适用于单人脸场景
* @param image BufferedImage
* @return
*/
- float[] extractTopFaceFeature(BufferedImage image);
+ default R extractTopFaceFeature(BufferedImage image){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 提取分数最高人脸特征(使用默认配置)
+ * 特征提取(提取分数最高人脸特征)
+ * 适用于单人脸场景
* @param imagePath 图片路径
* @return
*/
- float[] extractTopFaceFeature(String imagePath);
+ default R extractTopFaceFeature(String imagePath){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
/**
- * 提取分数最高人脸特征(使用默认配置)
+ * 特征提取(提取分数最高人脸特征)
+ * 适用于单人脸场景
* @param imageData 图片字节流
* @return
*/
- float[] extractTopFaceFeature(byte[] imageData);
+ default R 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("默认不支持该功能");
+ }
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceNetModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceNetModel.java
new file mode 100644
index 0000000..4ddabf3
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceNetModel.java
@@ -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> predictorPool;
+
+
+ private ZooModel model;
+
+ private FaceModelConfig config;
+
+ /**
+ * 是否归一化相似度
+ */
+ public static final boolean NORMALIZE_SIMILARITY = true;
+
+
+ public static final List mean =
+ Arrays.asList(
+ 127.5f / 255.0f,
+ 127.5f / 255.0f,
+ 127.5f / 255.0f,
+ 128.0f / 255.0f,
+ 128.0f / 255.0f,
+ 128.0f / 255.0f);
+
+ 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 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 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 feature1 = extractTopFaceFeature(imagePath1);
+ if (!feature1.isSuccess()){
+ throw new FaceException(feature1.getMessage());
+ }
+ R 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 feature1 = extractTopFaceFeature(sourceImage1);
+ if (!feature1.isSuccess()){
+ throw new FaceException(feature1.getMessage());
+ }
+ R 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 feature1 = extractTopFaceFeature(imageData1);
+ if (!feature1.isSuccess()){
+ throw new FaceException(feature1.getMessage());
+ }
+ R 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 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 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 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 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 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 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 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 register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
+ if(vectorDBClient == null){
+ throw new VectorDBException("向量数据库未初始化成功");
+ }
+ //提取最大人脸特征
+ R featureResponse = extractTopFaceFeature(sourceImage);
+ if(!featureResponse.isSuccess()){
+ return R.fail(featureResponse.getCode(), featureResponse.getMessage());
+ }
+ return register(faceRegisterInfo, featureResponse.getData());
+ }
+
+
+ @Override
+ public R 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 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 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 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> 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> 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> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
+ if(vectorDBClient == null){
+ throw new VectorDBException("向量数据库未初始化成功");
+ }
+ //提取最大人脸特征
+ R featureResponse = extractTopFaceFeature(sourceImage);
+ if(!featureResponse.isSuccess()){
+ return R.fail(featureResponse.getCode(), featureResponse.getMessage());
+ }
+ return R.ok(search(featureResponse.getData(), params));
+ }
+
+
+
+ @Override
+ public List 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 searchResults = vectorDBClient.search(feature, searchParams);
+ return searchResults;
+ }
+
+ @Override
+ public R 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 search(BufferedImage sourceImage, FaceSearchParams params) {
+ if(vectorDBClient == null){
+ throw new VectorDBException("向量数据库未初始化成功");
+ }
+ //提取所有人脸特征
+ R 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 searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
+ detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
+ }
+ }
+ return detectionResponse;
+ }
+
+ @Override
+ public R 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;
+ }
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java
deleted file mode 100644
index 9081b18..0000000
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java
+++ /dev/null
@@ -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> predictorPool;
-
-
- private ZooModel model;
-
- private FaceModelConfig config;
-
- public static final List mean =
- Arrays.asList(
- 127.5f / 255.0f,
- 127.5f / 255.0f,
- 127.5f / 255.0f,
- 128.0f / 255.0f,
- 128.0f / 255.0f,
- 128.0f / 255.0f);
-
-
- /**
- * 加载人脸特征提取模型
- * @param config
- */
- @Override
- public void loadModel(FaceModelConfig config) {
- 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 faceFeatureCriteria =
- Criteria.builder()
- .setTypes(Image.class, float[].class)
- .optModelName("face_feature") // specify model file prefix
- .optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
- "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
- .optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
- .optTranslator(new FaceFeatureTranslator())
- .optArgument("normalize", normalize)
- .optDevice(device)
- .optEngine("PyTorch") // Use PyTorch engine
- .optProgress(new ProgressBar())
- .build();
-
- try {
- model = faceFeatureCriteria.loadModel();
- // 创建池子:每个线程独享 Predictor
- this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
- log.info("当前设备: " + model.getNDManager().getDevice());
- } catch (IOException | ModelNotFoundException | MalformedModelException e) {
- throw new FaceException("模型加载失败", e);
- }
- }
-
- private float[] featureExtraction(Image image){
- image.getWrappedImage();
- Predictor predictor = null;
- try {
- predictor = predictorPool.borrowObject();
- return predictor.predict(image);
- } catch (Exception e) {
- throw new FaceException("目标检测错误", e);
- }finally {
- if (predictor != null) {
- try {
- predictorPool.returnObject(predictor); //归还
- } 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 extractFeatures(BufferedImage image) {
-
- List featureList = new ArrayList();
- 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 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 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();
- }
- }
-
-
-
-}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
index 9f56317..99fb61a 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/RetinaFaceModel.java
@@ -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> 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);
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
index c4274a9..104434f 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
@@ -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 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 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 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 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 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 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 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 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 = 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 searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
+ detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
}
}
+ return detectionResponse;
}
@Override
- public FaceResult search(byte[] imageData) {
+ public R 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 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 searchResults = vectorDBClient.search(feature, searchParams);
+ return searchResults;
+ }
+
+
+ @Override
+ public R> 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> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ return R.fail(R.Status.INVALID_IMAGE);
+ }
+ //提取分数最高人脸特征
+ R 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 searchResults = vectorDBClient.search(featureResponse.getData(), searchParams);
+ return R.ok(searchResults);
+ }
+
+ @Override
+ public R> 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 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 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 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 extractFeatures(String imagePath) {
+ public R 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 extractFeatures(byte[] imageData) {
+ public R 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 extractFeatures(BufferedImage image) {
+ public R extractFeatures(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
- throw new FaceException("图像无效");
+ return R.fail(R.Status.INVALID_IMAGE);
}
List featureList = new ArrayList();
+ List seetaPointFSList = new ArrayList();
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 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 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 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 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;
+ }
+
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
index c523bc0..75388e2 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/UltraLightFastGenericFaceModel.java
@@ -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> predictorPool;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java
index 38cc609..41be082 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/liveness/Seetaface6LivenessModel.java
@@ -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("视频帧数低于检测帧数");
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
index be08045..b4156b9 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/seetaface/NativeLoader.java
@@ -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 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 getLibFiles(OsInfo osInfo,DeviceEnum deviceEnum){
try {
String device = getDevice(deviceEnum);
- log.info("当前设备:{}", device);
+ log.debug("当前设备:{}", device);
//获取dll文件列表
List baseList = new ArrayList<>();
List 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();
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/sqllite/SqliteHelper.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/sqllite/SqliteHelper.java
index cee9c9b..bfa83dd 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/sqllite/SqliteHelper.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/sqllite/SqliteHelper.java
@@ -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 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 executeQuery(String sql, ResultSetExtractor rse) throws SQLException, ClassNotFoundException {
- try {
- resultSet = getStatement().executeQuery(sql);
- T rs = rse.extractData(resultSet);
- return rs;
- } finally {
- destroyed();
+ public T executeQuery(String sql, ResultSetExtractor 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 List executeQuery(String sql, RowMapper rm) throws SQLException, ClassNotFoundException {
- List rsList = new ArrayList();
- try {
- resultSet = getStatement().executeQuery(sql);
- while (resultSet.next()) {
- rsList.add(rm.mapRow(resultSet, resultSet.getRow()));
+ public List executeQuery(String sql, RowMapper rm) throws SQLException {
+ List 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 sqls) throws SQLException, ClassNotFoundException {
- try {
+ public void executeUpdate(List 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 List executeQueryList(String sql, Class clazz) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
- List rsList = new ArrayList();
- try {
- resultSet = getStatement().executeQuery(sql);
- while (resultSet.next()) {
+ public List executeQueryList(String sql, Class clazz) throws SQLException, IllegalAccessException, InstantiationException {
+ List 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 executeQuery(String sql, Class 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 executeQuery(String sql, Class 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 param) throws SQLException, ClassNotFoundException {
- try {
- StringBuffer sql = new StringBuffer();
+ public int executeInsertOrUpdate(String tableName, Map param) throws SQLException {
+ try (Connection conn = getConnection()) {
+ // 保证字段和值顺序一致
+ List 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 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 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实例已关闭");
+ }
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
index ddb2222..53a130a 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/translator/FaceFeatureTranslator.java
@@ -43,7 +43,9 @@ public final class FaceFeatureTranslator implements Translator {
*/
@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
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
index 39fcbd0..9e65043 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/FaceUtils.java
@@ -79,7 +79,7 @@ public class FaceUtils {
* @param seetaResult
* @return
*/
- public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, FaceModelConfig config,List seetaPointFSList){
+ public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, List 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 seetaPointFSList, List featureList){
+ if(Objects.isNull(seetaResult) || seetaResult.length == 0){
+ return null;
+ }
+ List detectionInfoList = new ArrayList();
+ for(int i = 0; i < seetaResult.length; i++){
+ SeetaRect rect = seetaResult[i];
+ DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
+ FaceInfo faceInfo = new FaceInfo();
+ if(seetaPointFSList != null && seetaPointFSList.size() > 0){
+ SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
+ List keyPoints = Arrays.stream(seetaPointFS)
+ .map(p -> new Point(p.x, p.y))
+ .collect(Collectors.toList());
+ faceInfo.setKeyPoints(keyPoints);
+ }
+ if(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 detectionInfoList = new ArrayList();
+ DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
+ FaceInfo faceInfo = new FaceInfo();
+ List 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);
+ }
+ }
+
}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/SimilarityUtil.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/SimilarityUtil.java
new file mode 100644
index 0000000..f949fde
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/SimilarityUtil.java
@@ -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);
+ }
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/VectorUtils.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/VectorUtils.java
new file mode 100644
index 0000000..62bfd40
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/VectorUtils.java
@@ -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;
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/MilvusConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/MilvusConfig.java
new file mode 100644
index 0000000..51b4948
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/MilvusConfig.java
@@ -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;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/SQLiteConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/SQLiteConfig.java
new file mode 100644
index 0000000..3a3ae16
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/SQLiteConfig.java
@@ -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);
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/VectorDBConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/VectorDBConfig.java
new file mode 100644
index 0000000..a268029
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/VectorDBConfig.java
@@ -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;
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/constant/VectorDBConstants.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/constant/VectorDBConstants.java
new file mode 100644
index 0000000..ebd53fc
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/constant/VectorDBConstants.java
@@ -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;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/MilvusClient.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/MilvusClient.java
new file mode 100644
index 0000000..14c70fc
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/MilvusClient.java
@@ -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 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 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 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 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
+ List vectorList = new ArrayList<>();
+ for (float v : faceVector.getVector()) {
+ vectorList.add(v);
+ }
+
+ List> vectors = Collections.singletonList(vectorList);
+ //List metadataList = Collections.singletonList(faceVector.getMetadata());
+
+ List metadataList = Optional.ofNullable(faceVector.getMetadata())
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ List 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 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 response = serviceClient.insert(insertParam);
+ if (response.getStatus() != R.Status.Success.getCode()) {
+ throw new VectorDBException("插入失败: " + response.getMessage());
+ }
+
+ List 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
+ List vectorList = new ArrayList<>();
+ for (float v : faceVector.getVector()) {
+ vectorList.add(v);
+ }
+
+ List> vectors = Collections.singletonList(vectorList);
+ List metadataList = Optional.ofNullable(faceVector.getMetadata())
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+
+ // 准备字段列表
+ List 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 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 insertBatch(List faceVectors) {
+ try {
+ if (!isInit){
+ throw new VectorDBException("Milvus未初始化完毕");
+ }
+ List ids = faceVectors.stream()
+ .map(FaceVector::getId)
+ .collect(Collectors.toList());
+
+
+ List> vectors = new ArrayList<>();
+ for (FaceVector fv : faceVectors) {
+ List list = new ArrayList<>();
+ for (float f : fv.getVector()) {
+ list.add(f);
+ }
+ vectors.add(list);
+ }
+
+ List 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 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 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 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 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 search(float[] queryVector, FaceSearchParams faceSearchParams) {
+ try {
+ if (!isInit){
+ throw new VectorDBException("Milvus未初始化完毕");
+ }
+ // 1. 包装查询向量
+ List> vectors = new ArrayList<>();
+ List 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 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 scores = wrapper.getIDScore(0); // 默认只有一条 query 向量
+
+
+ List 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 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 fields = response.getData().getFieldsDataList();
+ if (fields.isEmpty()) {
+ throw new VectorDBException("Milvus 返回空字段数据");
+ }
+
+ FieldData countField = fields.get(0);
+ List 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 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 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 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 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);
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/SQLiteClient.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/SQLiteClient.java
new file mode 100644
index 0000000..67803b5
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/SQLiteClient.java
@@ -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