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 memoryIndex = new CopyOnWriteArrayList<>(); + private int featureDimension; // 维度 + + private final ExecutorService executor = Executors.newFixedThreadPool(4); + + private SQLiteConfig config; + + /** + * 是否初始化完毕 + */ + private boolean isInit; + + public SQLiteClient(SQLiteConfig config) { + this.config = config; + String dbPath = config.getDbPath(); + //如果未指定db路径,则使用默认路径 + if(StringUtils.isBlank(config.getDbPath())){ + dbPath = Config.getCachePath() + File.separator + "face.db"; + log.debug("使用默认SQLite人脸库路径: {}", dbPath); + } + this.faceDao = FaceDao.getInstance(dbPath); + } + + + @Override + public void initialize() { + try { + // 加载所有特征到内存 + loadAllFeaturesToMemory(); + isInit = true; + log.debug("SQLiteVectorDB initialized with {} faces", memoryIndex.size()); + } catch (Exception e) { + throw new VectorDBException("初始化失败", e); + } + } + + // 以下方法保持接口兼容但忽略collectionName参数 + @Override + public void createCollection(String collectionName, int dimension) { + this.featureDimension = dimension; + log.debug("特征维度设置为: {}", dimension); + } + + @Override + public void dropCollection(String collectionName) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + clearAllData(); + log.warn("所有数据已被清空"); + } + + @Override + public boolean hasCollection(String collectionName) { + throw new UnsupportedOperationException("Sqlite 不支持此操作"); + } + + @Override + public String insert(FaceVector faceVector) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + return insertBatch(Collections.singletonList(faceVector)).get(0); + } + + @Override + public void upsert(FaceVector faceVector) { +// if (faceVector.getId() != null) { +// delete(faceVector.getId()); +// } + insert(faceVector); + } + + @Override + public List insertBatch(List faceVectors) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + List ids = new ArrayList<>(); + try { + for (FaceVector faceVector : faceVectors) { + String id = faceVector.getId() != null ? + faceVector.getId() : IdUtil.simpleUUID(); + faceVector.setId(id); + // 保存到数据库 + faceDao.insertOrUpdate(faceVector); + // 添加到内存索引 + addToMemoryIndex(faceVector); + ids.add(id); + } + log.debug("插入了 {} 个人脸向量", faceVectors.size()); + return ids; + } catch (Exception e) { + throw new VectorDBException("批量插入失败", e); + } + } + + @Override + public void delete(String id) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + deleteBatch(Collections.singletonList(id)); + } + + @Override + public void deleteBatch(List ids) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + try { + // 从数据库中删除 + boolean isSuccess = faceDao.deleteFace(ids.toArray(new String[0])); + // 从内存中删除 + memoryIndex.removeIf(v -> ids.contains(v.getId())); + if(!isSuccess){ + throw new VectorDBException("删除失败"); + } + } catch (Exception e) { + throw new VectorDBException("批量删除失败", e); + } + } + + @Override + public List search(float[] queryVector, FaceSearchParams faceSearchParams) { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + if (memoryIndex.isEmpty()) { + return Collections.emptyList(); + } + // 并行计算相似度 + List> futures = memoryIndex.stream() + .map(vector -> CompletableFuture.supplyAsync(() -> { + float similarity = SimilarityUtil.calculate(queryVector, vector.getVector(), config.getSimilarityType(), faceSearchParams.getNormalizeSimilarity()); + return similarity >= faceSearchParams.getThreshold() ? + new FaceSearchResult(vector.getId(), similarity, vector.getMetadata()) : + null; + }, executor)) + .collect(Collectors.toList()); + + // 收集结果并过滤null + List allResults = futures.stream() + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // 获取TopK结果 + return allResults.stream() + .sorted(Comparator.comparingDouble(FaceSearchResult::getSimilarity).reversed()) + .limit(faceSearchParams.getTopK()) + .collect(Collectors.toList()); + } + + @Override + public long count(String collectionName) { + return memoryIndex.size(); + } + + @Override + public void close() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // ============= 私有辅助方法 ============= + + private void loadAllFeaturesToMemory() { + try { + int pageSize = 1000; + int page = 0; + while (true) { + List batch = faceDao.findFace(page, pageSize); + if (CollectionUtils.isEmpty(batch)) { + break; + } + for (FaceVector vector : batch) { + addToMemoryIndex(vector); + } + page++; + } + log.debug("从数据库加载了 {} 个特征向量到内存", memoryIndex.size()); + } catch (Exception e) { + throw new VectorDBException("加载特征到内存失败", e); + } + } + + private void addToMemoryIndex(FaceVector faceVector) { + memoryIndex.add(faceVector); + } + + private void clearAllData() { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + try { + faceDao.deleteAll(); + memoryIndex.clear(); + } catch (Exception e) { + log.error("清空数据库失败", e); + } + } + + @Override + public FaceSearchResult getById(String id) { + try { + FaceVector faceVector = faceDao.findById(id); + if(faceVector != null){ + return new FaceSearchResult(faceVector.getId(), 1.0f, faceVector.getMetadata()); + } + return null; + } catch (SQLException | RuntimeException | ClassNotFoundException e ) { + throw new VectorDBException("SQLite查询异常", e); + } + } + + @Override + public void loadFaceFeatures() { + // 加载所有特征到内存 + loadAllFeaturesToMemory(); + isInit = true; + log.debug("SQLiteVectorDB load success {} faces", memoryIndex.size()); + } + + @Override + public void releaseFaceFeatures() { + if (!isInit){ + throw new VectorDBException("人脸库未加载完毕"); + } + memoryIndex.clear(); + isInit = false; + } +} diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBClient.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBClient.java new file mode 100644 index 0000000..cdab49a --- /dev/null +++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBClient.java @@ -0,0 +1,114 @@ +package cn.smartjavaai.face.vector.core; + +import cn.smartjavaai.face.entity.FaceSearchParams; +import cn.smartjavaai.face.vector.entity.FaceVector; +import cn.smartjavaai.common.entity.FaceSearchResult; +import cn.smartjavaai.face.vector.exception.VectorDBException; + +import java.util.List; + +/** + * 向量数据库客户端接口 + * 定义与向量数据库交互的通用操作 + * @author dwj + */ +public interface VectorDBClient extends AutoCloseable { + + /** + * 初始化连接和集合 + * @throws VectorDBException 初始化异常 + */ + void initialize(); + + /** + * 创建集合 + * @param collectionName 集合名称 + * @param dimension 向量维度 + */ + void createCollection(String collectionName, int dimension); + + /** + * 删除集合 + * @param collectionName 集合名称 + */ + void dropCollection(String collectionName); + + /** + * 检查集合是否存在 + * @param collectionName 集合名称 + * @return 是否存在 + */ + boolean hasCollection(String collectionName); + + + /** + * 插入人脸向量 + * @param faceVector + * @return + */ + String insert(FaceVector faceVector); + + /** + * 更新或新增人脸向量 + * @param faceVector + */ + void upsert(FaceVector faceVector); + + /** + * 批量插入人脸向量 + * @param faceVectors + * @return + */ + List insertBatch(List faceVectors); + + /** + * 根据ID删除向量 + * @param id + */ + void delete(String id); + + /** + * 批量删除向量 + * @param ids + */ + void deleteBatch(List ids); + + /** + * 搜索相似人脸 + * @param queryVector + * @param faceSearchParams + * @return + */ + List search(float[] queryVector, FaceSearchParams faceSearchParams); + + /** + * 获取集合中的向量数量 + * @param collectionName 集合名称 + * @return 向量数量 + */ + long count(String collectionName); + + /** + * 关闭连接 + */ + @Override + void close(); + + + /** + * 获取指定ID的向量 + * @return + */ + FaceSearchResult getById(String id); + + /** + * 加载人脸特征到内存 + */ + void loadFaceFeatures(); + + /** + * 释放人脸特征缓存 + */ + void releaseFaceFeatures(); + +} diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBFactory.java new file mode 100644 index 0000000..630422f --- /dev/null +++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBFactory.java @@ -0,0 +1,52 @@ +package cn.smartjavaai.face.vector.core; + + + +import cn.smartjavaai.face.vector.config.MilvusConfig; +import cn.smartjavaai.face.vector.config.SQLiteConfig; +import cn.smartjavaai.face.vector.config.VectorDBConfig; +import cn.smartjavaai.face.vector.exception.VectorDBException; + +/** + * 向量数据库工厂类 + * 用于创建不同类型的向量数据库客户端 + * @author dwj + */ +public class VectorDBFactory { + + private VectorDBFactory() { + // 私有构造函数,防止实例化 + } + + /** + * 创建向量数据库客户端 + * @param config 配置信息 + * @return 向量数据库客户端 + * @throws VectorDBException 创建异常 + */ + public static VectorDBClient createClient(VectorDBConfig config) { + if (config == null) { + throw new VectorDBException("配置不能为空"); + } + VectorDBClient client; + + switch (config.getType()) { + case SQLITE: + if (!(config instanceof SQLiteConfig)) { + throw new VectorDBException("SQLite类型需要SQLiteConfig配置"); + } + client = new SQLiteClient((SQLiteConfig) config); + break; + case MILVUS: + if (!(config instanceof MilvusConfig)) { + throw new VectorDBException("Milvus类型需要MilvusConfig配置"); + } + client = new MilvusClient((MilvusConfig) config); + break; + // 未来可以在这里添加其他向量数据库的支持 + default: + throw new VectorDBException("不支持的向量数据库类型: " + config.getType()); + } + return client; + } +} diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/entity/FaceVector.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/entity/FaceVector.java new file mode 100644 index 0000000..a0299d5 --- /dev/null +++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/entity/FaceVector.java @@ -0,0 +1,68 @@ +package cn.smartjavaai.face.vector.entity; + + +import lombok.Data; + +import java.util.UUID; + +/** + * 人脸向量实体类 + * @author smartjavaai + */ +@Data +public class FaceVector { + + /** + * 向量ID + */ + private String id; + + /** + * 人脸特征向量 + */ + private float[] vector; + + /** + * 元数据,可以存储人脸相关的其他信息(JSON格式) + */ + private String metadata; + + /** + * 默认构造函数 + */ + public FaceVector() { + this.id = UUID.randomUUID().toString(); + } + + /** + * 构造函数 + * @param vector 人脸特征向量 + */ + public FaceVector(float[] vector) { + this(); + this.vector = vector; + } + + /** + * 构造函数 + * @param vector 人脸特征向量 + * @param metadata 元数据 + */ + public FaceVector(float[] vector, String metadata) { + this(); + this.vector = vector; + this.metadata = metadata; + } + + /** + * 构造函数 + * @param id 向量ID + * @param vector 人脸特征向量 + * @param metadata 元数据 + */ + public FaceVector(String id, float[] vector, String metadata) { + this.id = id; + this.vector = vector; + this.metadata = metadata; + } +} diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/exception/VectorDBException.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/exception/VectorDBException.java new file mode 100644 index 0000000..9438409 --- /dev/null +++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/exception/VectorDBException.java @@ -0,0 +1,26 @@ +package cn.smartjavaai.face.vector.exception; + + +/** + * 向量数据库异常 + * @author smartjavaai + */ +public class VectorDBException extends RuntimeException { + + /** + * 构造函数 + * @param message 异常信息 + */ + public VectorDBException(String message) { + super(message); + } + + /** + * 构造函数 + * @param message 异常信息 + * @param cause 原始异常 + */ + public VectorDBException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/smartjavaai-face/src/main/resources/db/schema.sql b/smartjavaai-face/src/main/resources/db/schema.sql new file mode 100644 index 0000000..fcd923d --- /dev/null +++ b/smartjavaai-face/src/main/resources/db/schema.sql @@ -0,0 +1,14 @@ +PRAGMA foreign_keys = false; + +-- ---------------------------- +-- Table structure for face +-- ---------------------------- +CREATE TABLE "face" ( + "id" TEXT NOT NULL, + "vector" blob NOT NULL, + "metadata" TEXT, + PRIMARY KEY ("id"), + CONSTRAINT "id" UNIQUE ("id" ASC) +); + +PRAGMA foreign_keys = true; diff --git a/smartjavaai-objectdetection/pom.xml b/smartjavaai-objectdetection/pom.xml index 1da7a24..1b80c33 100644 --- a/smartjavaai-objectdetection/pom.xml +++ b/smartjavaai-objectdetection/pom.xml @@ -6,11 +6,11 @@ cn.smartjavaai smartjavaai-parent - 1.0.15 + 1.0.16 smartjavaai-objectdetection - 1.0.15 + 1.0.16 smartjavaai-objectdetection SmartJavaAI https://github.com/geekwenjie/SmartJavaAI diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java index 0f23684..299bf02 100644 --- a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java +++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/DetectorModel.java @@ -57,7 +57,7 @@ public class DetectorModel implements AutoCloseable{ model = criteria.loadModel(); // 创建池子:每个线程独享 Predictor this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model)); - log.info("当前设备: " + model.getNDManager().getDevice()); + log.debug("当前设备: " + model.getNDManager().getDevice()); } catch (IOException | ModelNotFoundException | MalformedModelException e) { throw new DetectionException("模型加载失败", e); } @@ -178,7 +178,7 @@ public class DetectorModel implements AutoCloseable{ if (predictor != null) { try { predictorPool.returnObject(predictor); //归还 - log.info("释放资源"); + log.debug("释放资源"); } catch (Exception e) { log.warn("归还Predictor失败", e); try { diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java index cc99cb1..d65b3ac 100644 --- a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java +++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/model/ObjectDetectionModelFactory.java @@ -22,7 +22,7 @@ public class ObjectDetectionModelFactory { private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>(); static{ - log.info("缓存目录:{}", Config.getCachePath()); + log.debug("缓存目录:{}", Config.getCachePath()); } // 私有构造函数,防止外部创建实例 diff --git a/smartjavaai-ocr/pom.xml b/smartjavaai-ocr/pom.xml index c12254d..b375c7c 100644 --- a/smartjavaai-ocr/pom.xml +++ b/smartjavaai-ocr/pom.xml @@ -6,7 +6,7 @@ cn.smartjavaai smartjavaai-parent - 1.0.15 + 1.0.16 smartjavaai-ocr @@ -20,7 +20,7 @@ - 1.0.15 + 1.0.16 smartjavaai-ocr SmartJavaAI https://github.com/geekwenjie/SmartJavaAI diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java index 223f0d7..4552092 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java @@ -205,7 +205,7 @@ public class OcrModelFactory { registerCommonDetModel("PADDLEOCR_V5_DET_MODEL", PpOCRV5DetModel.class); registerCommonRecModel("PADDLEOCR_V5_REC_MODEL", PpOCRV5RecModel.class); registerDirectionModel("CH_PPOCR_MOBILE_V2_CLS", PPOCRMobileV2Model.class); - log.info("缓存目录:{}", Config.getCachePath()); + log.debug("缓存目录:{}", Config.getCachePath()); } } diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java index cd3592d..efef89f 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java @@ -80,8 +80,8 @@ public class PpOCRV5DetModel implements OcrCommonDetModel { ZooModel detectionModel = ModelZoo.loadModel(detCriteria); // 创建池子:每个线程独享 Predictor this.detPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel)); - log.info("当前设备: " + detectionModel.getNDManager().getDevice()); - log.info("当前引擎: " + Engine.getInstance().getEngineName()); + log.debug("当前设备: " + detectionModel.getNDManager().getDevice()); + log.debug("当前引擎: " + Engine.getInstance().getEngineName()); } catch (IOException | ModelNotFoundException | MalformedModelException e) { throw new OcrException("检测模型加载失败", e); } @@ -142,7 +142,7 @@ public class PpOCRV5DetModel implements OcrCommonDetModel { } OcrUtils.drawRect((Mat)img.getWrappedImage(), boxList); Path output = Paths.get(outputPath); - log.info("Saving to {}", output.toAbsolutePath().toString()); + log.debug("Saving to {}", output.toAbsolutePath().toString()); img.save(Files.newOutputStream(output), "png"); ((Mat) img.getWrappedImage()).release(); } catch (IOException e) { diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java index 8ca5497..73da37d 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java @@ -89,8 +89,8 @@ public class PPOCRMobileV2Model implements OcrDirectionModel { ZooModel model = ModelZoo.loadModel(criteria); // 创建池子:每个线程独享 Predictor this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model)); - log.info("当前设备: " + model.getNDManager().getDevice()); - log.info("当前引擎: " + Engine.getInstance().getEngineName()); + log.debug("当前设备: " + model.getNDManager().getDevice()); + log.debug("当前引擎: " + Engine.getInstance().getEngineName()); } catch (IOException | ModelNotFoundException | MalformedModelException e) { throw new OcrException("模型加载失败", e); } @@ -232,7 +232,7 @@ public class PPOCRMobileV2Model implements OcrDirectionModel { } OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), itemList); Path output = Paths.get(outputPath); - log.info("Saving to {}", output.toAbsolutePath().toString()); + log.debug("Saving to {}", output.toAbsolutePath().toString()); img.save(Files.newOutputStream(output), "png"); ((Mat) img.getWrappedImage()).release(); } catch (IOException e) { diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java index 113c86a..fe1d809 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java @@ -90,8 +90,8 @@ public class PpOCRV5RecModel implements OcrCommonRecModel { try{ ZooModel recognitionModel = ModelZoo.loadModel(recCriteria); this.recPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(recognitionModel)); - log.info("当前设备: " + recognitionModel.getNDManager().getDevice()); - log.info("当前引擎: " + Engine.getInstance().getEngineName()); + log.debug("当前设备: " + recognitionModel.getNDManager().getDevice()); + log.debug("当前引擎: " + Engine.getInstance().getEngineName()); } catch (IOException | ModelNotFoundException | MalformedModelException e) { throw new OcrException("识别模型加载失败", e); } diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java index 9f16d13..6039b4f 100644 --- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java +++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java @@ -52,7 +52,7 @@ public class OcrUtils { List boxList = new ArrayList(); for(NDArray box : dt_boxes){ float[] pointsArr = box.toFloatArray(); - //log.info("points: {}", pointsArr); + //log.debug("points: {}", pointsArr); float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2); float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4); float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);