From bca9462331cc4ad1a97902a893d9554cc66fa35e Mon Sep 17 00:00:00 2001 From: dengwenjie <775747758@qq.com> Date: Mon, 9 Jun 2025 12:12:10 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=9A=E4=BA=BA=E8=84=B8=E6=9F=A5=E8=AF=A2=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20=E5=90=91=E9=87=8F=E6=95=B0=E6=8D=AE=E5=BA=93Milvus=20?= =?UTF-8?q?=E5=92=8C=20SQLite=202=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=9AFaceNet=E4=BA=BA=E8=84=B8=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=B9=9F=E6=94=AF=E6=8C=81=E4=BA=BA=E8=84=B8=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=EF=BC=8C=E6=9F=A5=E8=AF=A2=E7=AD=89=E5=8A=9F=E8=83=BD=203?= =?UTF-8?q?=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1=E5=9D=97=EF=BC=9ASeetaface?= =?UTF-8?q?6=20=E8=87=AA=E5=8A=A8=E4=B8=8B=E8=BD=BD=E4=BA=BA=E8=84=B8?= =?UTF-8?q?=E5=BA=93=204=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=9ASeetaface6=E8=A7=A3=E5=86=B3=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=BA=93=E9=87=8D=E5=A4=8D=E4=B8=8B=E8=BD=BD=E9=97=AE=E9=A2=98?= =?UTF-8?q?=205=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1=E5=9D=97=EF=BC=9A?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=8B=E5=8A=A8=E5=8A=A0=E8=BD=BD=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E5=BA=93=206=E3=80=81=E4=BA=BA=E8=84=B8=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=9A=E4=BA=BA=E8=84=B8=E8=AF=86=E5=88=AB=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +- examples/pom.xml | 2 +- .../examples/face/facerec/FaceNetDemo.java | 348 +++++++-- .../examples/face/facerec/SeetaFace6Demo.java | 397 ++++++---- .../examples/ocr/OcrDetectionDemo.java | 2 +- .../examples/ocr/OcrDirectionDetDemo.java | 4 +- .../examples/ocr/OcrRecognizeDemo.java | 32 +- examples/src/main/resources/face/iu_1.jpg | Bin 0 -> 65536 bytes examples/src/main/resources/face/iu_2.jpg | Bin 0 -> 58465 bytes examples/src/main/resources/face/iu_3.jpg | Bin 0 -> 126348 bytes pom.xml | 3 +- smartjavaai-all/pom.xml | 4 +- smartjavaai-bom/pom.xml | 4 +- smartjavaai-common/pom.xml | 3 +- .../common/entity/DetectionResponse.java | 1 + .../smartjavaai/common/entity/FaceInfo.java | 10 + .../common/entity/FaceSearchResult.java | 37 + .../java/cn/smartjavaai/common/entity/R.java | 84 ++ smartjavaai-face/pom.xml | 10 +- .../face/config/FaceExtractConfig.java | 2 +- .../face/config/FaceModelConfig.java | 15 +- .../face/constant/FaceDetectConstant.java | 5 + .../java/cn/smartjavaai/face/dao/FaceDao.java | 194 +++-- .../face/entity/FaceRegisterInfo.java | 30 + .../face/entity/FaceSearchParams.java | 43 ++ .../smartjavaai/face/enums/FaceModelEnum.java | 2 +- .../cn/smartjavaai/face/enums/IdStrategy.java | 13 + .../face/enums/SimilarityType.java | 13 + .../smartjavaai/face/enums/VectorDBType.java | 27 + .../factory/FaceAttributeModelFactory.java | 2 +- .../face/factory/FaceModelFactory.java | 4 +- .../face/factory/LivenessModelFactory.java | 2 +- .../Seetaface6FaceAttributeModel.java | 2 +- .../face/model/facerec/AbstractFaceModel.java | 159 ---- .../face/model/facerec/FaceModel.java | 293 +++++-- .../face/model/facerec/FaceNetModel.java | 722 ++++++++++++++++++ .../model/facerec/FeatureExtractionModel.java | 367 --------- .../face/model/facerec/RetinaFaceModel.java | 4 +- .../face/model/facerec/SeetaFace6Model.java | 688 ++++++++++------- .../UltraLightFastGenericFaceModel.java | 2 +- .../liveness/Seetaface6LivenessModel.java | 4 +- .../face/seetaface/NativeLoader.java | 23 +- .../face/sqllite/SqliteHelper.java | 422 +++++----- .../translator/FaceFeatureTranslator.java | 4 +- .../cn/smartjavaai/face/utils/FaceUtils.java | 75 +- .../face/utils/SimilarityUtil.java | 129 ++++ .../smartjavaai/face/utils/VectorUtils.java | 42 + .../face/vector/config/MilvusConfig.java | 82 ++ .../face/vector/config/SQLiteConfig.java | 30 + .../face/vector/config/VectorDBConfig.java | 20 + .../vector/constant/VectorDBConstants.java | 48 ++ .../face/vector/core/MilvusClient.java | 594 ++++++++++++++ .../face/vector/core/SQLiteClient.java | 266 +++++++ .../face/vector/core/VectorDBClient.java | 114 +++ .../face/vector/core/VectorDBFactory.java | 52 ++ .../face/vector/entity/FaceVector.java | 68 ++ .../vector/exception/VectorDBException.java | 26 + .../src/main/resources/db/schema.sql | 14 + smartjavaai-objectdetection/pom.xml | 4 +- .../objectdetection/model/DetectorModel.java | 4 +- .../model/ObjectDetectionModelFactory.java | 2 +- smartjavaai-ocr/pom.xml | 4 +- .../ocr/factory/OcrModelFactory.java | 2 +- .../model/common/detect/PpOCRV5DetModel.java | 6 +- .../common/direction/PPOCRMobileV2Model.java | 6 +- .../common/recognize/PpOCRV5RecModel.java | 4 +- .../cn/smartjavaai/ocr/utils/OcrUtils.java | 2 +- 67 files changed, 4170 insertions(+), 1438 deletions(-) create mode 100644 examples/src/main/resources/face/iu_1.jpg create mode 100644 examples/src/main/resources/face/iu_2.jpg create mode 100644 examples/src/main/resources/face/iu_3.jpg create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/FaceSearchResult.java create mode 100644 smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/R.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceRegisterInfo.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/entity/FaceSearchParams.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/IdStrategy.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/SimilarityType.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/enums/VectorDBType.java delete mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceNetModel.java delete mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FeatureExtractionModel.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/SimilarityUtil.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/utils/VectorUtils.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/MilvusConfig.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/SQLiteConfig.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/config/VectorDBConfig.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/constant/VectorDBConstants.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/MilvusClient.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/SQLiteClient.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBClient.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/core/VectorDBFactory.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/entity/FaceVector.java create mode 100644 smartjavaai-face/src/main/java/cn/smartjavaai/face/vector/exception/VectorDBException.java create mode 100644 smartjavaai-face/src/main/resources/db/schema.sql 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 0000000000000000000000000000000000000000..06bb29f199ebc7248e313edb350b4ed81e03ae9b GIT binary patch literal 65536 zcmbUJc|4Tu8$S-8*Q}N?wxPk;#n5J%WN(l)Wh`ylWNT8%T2kslCHr0o4cSYj2$egs zD@?XZlI%;UP|^0B)93kqU(cV<@7Mjh-E&{pxMt4tIF9%6KHkT1_V>--VIX2_X=4cx z1Ocw_AMm#hi1J|%Vr@zrM)9XJ1QegVlfR4M7T zs%ms~4aR2OEqeL}=G!bRt*mWqot$^=-m`a~i@S%X*HQK{?|{Id(`SOuhD2Ptd?oT~ zRCIDmYTC8y=^2@MxAF@Li;7E1A3Ut6d{k9k^Z0o~V^ecWYg>DFPj6rU>o;!)K8}oz zjZaKYP0xJ!y12Cb?fZ|FRRm!Fx19gGqW_=uz=b0i91e>k^45c3&cJ`MqBy*w7D3F^ zfq2AETuD2Ok7AaTTVBtvtmF7u!p;AcfTRlJ^?2o*ymQ4$Mb;=u+$0qzMQfDW43pv47f!E3TpA}q{z zj$0@laPS=L3Kug1e?tHmWU(+@j3Zcp@7ZJv47nHH919c3 z`!x!Bh>jRY0|R&waIj@SaRPp-01(m?uLKm7cdeKLQtKR;ARl2889>+;Xti>%NlZcB z|6QOU)Iwx|Dfq4ctOZgW)1?VixH~Me8zrZLJl?lYC;*Dn@XdJm`DZxT|MQXszb(A` zE*jVZ1Fj+)a9jxCjrgaO=@Q0&~lPvBjqurO{Yc9n(s1|KYvG}{3->T_^LqD(Re$NQo= zY|sVw7;ckz0naXY1xZw#0)#_=w(%6)_X42$iHp1*KuKK8w?d2v+#|faEl>w;yv@zY zVj)eM4I{EbL5~cxRzhu64zp4w_TAY8sYeMH+}oa4pwK5>)uhn z#B$e^l~=yyMMc5M$70sq8&E#{beHa9VZRTb^FF$t-mrTh7?-5wX@Y*Sxa{_FkDqnT z9!L8Vix%OHAA4i3;G}m8#t#Zf3dUrvM>5yLbvUvSoemgqm_+{9(Lg=cArlx#o2@B> zQAhy+(7kz3pw$64(+~zfllak^cWF)Szx!^yd(g4Lp$a}akT1Y07xE(T$N)+X55vQ! z9*G8cAh!hV!R2C>IMCgBp1#Tg@LdqF2yDdOm=JJx$tGZmIb#B&}!HO35%)ZSC z<+!q;~zq`)Vtn3z1rvImLDUfjn{MgHh7)j zXPR7k<+_p3+NxiIysPE_FBoZSzDy)S@006o-T>Ar5;WQWb{Nxx(18*vUJEuTV43{g z>0n`&Dfqfp0pi^ilw3On0|rKh0N5%GL+Q{G1W_i0tP9Sh`1H#Zc%295RCgE^5Cw$2 zp@YvcBc@?IP+0#9I{X}y|DWGOnB##0hKC+3jCk~VtlTcs6btm8%J{>t!xs%sOSA{* zX9kZ9n)$S~gyh}b?iYlAEYe9q+BWR$mxwpt)qhgKFxSgdZ9T>Prh}9Joq#%!$pc^432V^?$)NpJ25&eF5U_fy~LA-T{dy z0-wYdm}BPYh&m8|#{k*tXdX&lAtb^KR!0ZMA&!V47qMf~K?^UMWWPk4L)?ZK63<4? zL6G4kG1)0F`pg9g0Rk$SF3MHlVGhqs|1Ck3`G>K{GE6Ok%(@F#&^4c-O(anagi>C5 z)A|8%_8SL#s)PYf%8zluu27gZlxJ7CxQ#!9rMg}h>_R)QJ3fl~#)$Qhe@?K;t8@1@ z*%;pO^gf#NGyS6N9(nby1Ht{4NwGHfk;`wq&qj}HO}x&pwjY{5-F2E#%lX8rev&*M z<_2hhpy9JC>_93`E}Jd#+0;zhKFx%amyr zmSaRaUpYWGgo{ZQCaS>=>n^$Z7@{p^50#o%C)2i7dm;vW zwzB&}iS)%1{mW%%?(FEiZTiLeQE>Ffj8WQ=c)z91=UuFR4Z2Ht_Bv(FESlZitI8R^od>sL(&6#k0@Y1cXRB z7=@gv7!oGzc2=Uq*bS*qM8_SDSND!DKNBiitr#f3zf;EFHk<0AZ|Zx{$W!)O#4VPqLQYFPDIZK*NVjvdrq03v^^^A*TiU zi@(eQNgCLRen{v<*MQH#aQmyFt6*Ho(6Q8N0nykrAAMFdyT#yqd+R5ToK+b+T`nv? z_|d=$#_oKLG5>Ji;$YXcmK~%Q3hnom-_=UiY$?}NDy-WyYJoyZfEX-6@5J(aIvBnn zt+J2~04X0+z@QuX2T5AI&CcSnMo44e2J_y_OQr*^QXM>!dMgu{LdpZaaopgR185Ll zG*rWf!F&YS#a01u;LE??@j}TJ;GzbI%AXX_VM@LJ-sYWg=2vxT2uAPZna1*G^SMds z!cURe<;_Ol7}jGO@5xO#HZhk#;GMJcyRLpKmvT>N&iQy`m_8H06p-3;pTm!h^~D3a zLe>%k_=gm)MuW>y#4=#n88DDYk}^pJzw52AnLtn;y1grTr%fR)`o&wTk4)hYVecp) zoMn&%cQVrH*Wof?q}5{ngN2bI`awcBz8@zI4h;r<=inN!uRk+D1QBx3BZvISbU-ql zwI+IKPQ;+f6(`g5S{U2|GmKLd8(^LNMzqf7*4Hvh)VSeq!@c2Om|nXAdU9l@oxQxV zO<-%8HQzsk?xJ#V6X+TS5B@7aAOmE;gkXn*KQft8kTOAL3!oVwro+s!%7vtK6mB0} zmv%bLzs6G{6@aBV$qRr?9{Yz5Uk2zr=FPJUnSKvhGhtBi}l^ixQEdJ^k znx95Xgzi~4YQ(~veP?eA&J6e)I3br8xjEi@`6$<#6z-Q6WIKOh?j=eo-?Rlj!H_)+ z-|9~nkhav5f?S_sHA9@VBd(Z47tJ!sZ>&5qC}kbOl?J|EvR64opSE!}Vh)ZC60Z=8 z@$71nLSVCng1AW*7b8;E>)MSx^Zo2kN80ORA>Fe(IYwg{LEqneU+* zLQc7R_OfDHgpd5Xkp3GP6@iM2U9_LQd$*wxO_Inll!ZLW1Ux53A)!($`v(D{u`HyQ zM_R?wq2n7*Lzv*z6Ndl5eJ$L^jEQtUHlXlr&4#rA>A+7e!ZQTC0T~ku$%N4H#~B)6 z5!wu9?E_p$bqQ?D79Z;=LoOj`?ne}L9RTAAO!Y?5i9-tgai1pi7RNW9`#n0lulF>= zdh(Fz>ykV32d|!W@3I9+dyJMSjp{-2fky|qNX?t!qn``0*R-t|pm6i_5GtBDI-kN6 z+G{;i^o4R4fAu7(2x9ZAhdmc8k6Br$X6)9H(4NE+&~i#Tb|~DsZ+ie!m^v!hoh}9% zLkEDbgJJ>|8>1{#7QEauX=28!_NHI2&P( zMgw!`x@*B&%{9D|6eizpRGa~x#Q`-3Cc?o|p+8H(5&+hHIlK}^6P63m5*2(qwU~lo zOoAQ<%i&=U^D+a}eB&JHhTiW2+#lgd7BU&ni-ekWQhs$f2dc}R8@VX5^0REiJHBAA zKg-+Qtw&{l?q2^&bkr{&Gmni1Chql&x-_>o32S};M(-`<o5Ht1N8T!CU@!t!e>ceX!lC8^y40W&@KvR54ieYRg}6C{d!_MqFG*bZ(X@9RK;~ ziQ5r_TzsO$iuo!RE0EM6+z7qg_o6gl{R}?xgNsWiCNZT~B{?VW-m;-yCMw)?sV>*) z4(zb-5-7Mo=5h4L<>Tiv9v(2+>Aha@ZC;&WRuAN+(&lTP!FVWCfQa*7FA$CYM{y8q zSTkhs1coYaU}g-f1ZYCIg}fS{^h^CJOvo4%ft5uWR@jBdYGKgeU^Ro1AxM#+B%%L{ z&ciXS0BBakcnXuO(ny`;g{| zM~d|`JFNNa$L)w3T2op~@qF)yl#~TK;_^2MYrGi#aN0$2Yn<~jdo`?4CP=rSg%U<* z*uYqe$!A4d;t+2gxiToe!X?C2HYjvmPnwN&|B|o$eyC7RDAqb=epKaVQ8%ioV%Qma z(bjBTQpIo(2mk(qR4E|U1yVqqN<^MlqR=9BIpM1w1TQn4_M0KDjJqMEpAk&Ka3U<8 z*LundkI*ruzJ^@v_jTt4fbLwUfqbJQ5~=)XH!7uKqw(1{l#QucP&gq6MGlwf;VnY- z9Dad#0YAv&JN=k^{N{;z^LStvFP~-<^2+aF>F1%llG`e){hyA@Zw>6f?{#8fZgSDY zDegsoaroDG9VU4qQLxJesFNUkWx9bKVB=X1o&nM)P#8!Uf;z~8^3P9Rwt!L$FFEiu zCz!6WQXIZ6BPYnNj>6*PAGX18!-T~^Q%XVgCIIqRf<6b^k3z}91(=&*)Mc0WyGVPMi7~Sl?gsTte43ad{IA`q#Ql|WNfk9gCaNybN-Gs&%L zH&cJpk$iIPZ>tdII-wVp%^#T(a*{jusD!Yv-HwJ|n3C8?xo0m~7?_T%4&0N=3%kZi zzrF6>E)Mzowtn2VSM#Tan)Rf{Ba8YU#VBUSl|ApEx&|wiWIPCPgm!=hIm|-+D1C z1voGTVFrW35HRmXrCZAQNM*a1PtJ8t3LJjMk@ZfLbGS2;5PG_+dXBcKyF7(a9v^yq zbBT3adKEUzj;J6&2ZQ#qCFm_!Q7?1&9Tjuv7{AMNDRa(f$l`0-1!o1?-n7yA)jrUb z>Q1{ElBleZ5}#3Wg4md0Yz^_I^SWicNW)8k(L$?JAp)D1)BE>_eknhpcBFIud6>@C zYO}kRIysZIxCggs{x%HsY8DGndyo;Q?h^1(hva(}bI{c>)Vm&S-& z<*{ATGr%A@be+3yU~zq~bh4E@Z{I0gB%Y@L(lX(u^8n0{2NgFSGnkxZLfdk1u)<_hj}`zrbWUh*gntBG_wpwztaqu@ z%qr{0v%QOIY&Tctip^gj&}sKdIm+vW*Ms$OH`Jgqh!ubUiRimun`R0FB(?_`#r|R= z?-Jajfq(K#C@njq_itAW$L2gwk4&0f+){LeDWrU?Wys`xsio@kv-+y&TDwg{49w;` zKD+pyiy0HN+3GUghxpy4E)5^quQwd44qk4TAZCNv4Is-TnLFtuDaMAW_QFxEN_DJWByf*S1Z{T%ltuG7b!LG53E#!Jsjef@_4gUYw&4@^jGryrNGT+bjojK zCa$T~Bo{>khfIiM&=hrckfK4s^`F=dad0VLfk)UH%7E;TLW~!Rfweo-h80DfAc264 z+MG#*KIat+i{EkPc`5Kbo-qT9Y?Kh3G2#Kj1r_U~8DEm7*YwP`NE z+VJYTwn+-o(FGiLZaG@1WGhC(G9}*W=i@vUqd}65>}5^aCy+<;OVl@?g-O4C9T`Bs z`EMUlfs4fEFgpOaKdJ&H{fvE2NC~!px;`Sl0udz%D`bKL7tat-3N47n!rCT$+_DXsd!Cch5obeHD=xJ>ks4qPb=X8AY-kKXqOHgQelVxtf$A|R zOLj3xT1>nqubiZI0BnJxs8{;@2E6oHJ zRXIMm3221xg*83CvHELQYd~1r5`!c?mu;h#HI~BUS9mW{XY>{7q zaov7~5FTR3#%ShKzCU(j4?z_&Cf%=)>bdhmnZ?7Z>ulP|}7 zB9g3=-j4jsE_5ix^8{rf2x>Gg<_rff%LX0)D8U?TRE&z`u!|oHj&A~x8 zvx4NngeO?SX3IYz1(yCtGC^ECUW+zVTOzZ454&qY9u39gm0z6l5$bzkGfn1)ytsf# z=kr+aOQax5M>}R}OTi#_ng^b_VBvMHGeWmS!4nxq_rhE;* z|1PYTMexWCtCw3s_T6CcUCjh`82x;#EeliJC3~TwE+pK*gN3{C_QH?%&`dUP+5a7W z$z*_*xKt0}>)oi#Q_)4?FC2bBF|F)|We#C1x6{866)E;hn;m4~*7HqU6LQ!{-IyyE zSVGYuNUj3i;uq1%;mZ)9#ix?WDK&Kr#5F#a{)Rncq*HP!Xiw;!B5>1oz#yY_DcF-- zLp1rXIU_@Rkx7QZSPWnb2ZBAXKKftMF^`71h|bfwMqwmCPTq|%a3eu@<7dJ&4y6JZ zR8Vwb@cLXl({#ZFERl{ekliz8iH}Ua4DaHX3mPe7>O56^&@fK@?XA)w+N|TgIm$z2 z6w>+ZaweJA8G|pEDTsmsDG+XCoa$&8TzLvpKnBuge#*ltSq=0-RK4uaK13}qEa^>1 zFM2CthK*?YZigHhjB=qsXgJK(ZpxdJdALa9;=U5pd$TSu5ZePHyAot;I07ep$E3}q zO=k?CglEzVmQRqjD5119w0H|mif@QCNU|D%o16(#Aqy--lvD zJQO;Gsfd1+-EIT5!fh+GuR;o_IGT`EJZ$r}c-ps!i2% zru0w+nmz?^ppc#l`50`Q@QNNUNSwi0UaKVgC*0|-Nr+rfAEqOze6R$C$!8R5U(j~E zQV2?8P}aN*)GB#d1$GbdSn4N=GYdjcB30Db1lzFbJl_?38t(z z@vuvxJQ`CCT~lGGDdD|rF~&+^6D-VPPIdLNwCAcPO|nfYz@%FZZV5WYkoKjEkY6Zx%)$!Ku#J*@pkII;1;9Ks~E z#oim3UZI69OfrdNO-m{Zzwe0hgk$J5B2Z%v)r>3=%iSnxk;!KXi9*IQ15#<97Q@a60VP4Yafl8^(x0OuS&n8(?(sWVv}e%^7dnbGg(3rf)06y+9_&$p zHkeGxJ&={jRCCG%hk8La8)=A#+P@^El{%fgVprJWeH+v?daa55#r(}S(@?d*r2JA~ zVV{5c6A0eFtAWMD6#r~r z-8jz*4bB_hb)-#JV;cjtq5!S{q~jLI?>H7=D6|;4VXJGUgsSZ(Z!H7*Hl5wD>HUN~ z{F#m1DO%VhG{*!d-$OfKv_8DPLP4a6QVWAluFNly(+$pH(qeH5-HBqK_b8$GBRQ!+ z?jj_Y(3|uR$n}EAMY#%qo$ZHh*!QsgP_ElQn#&PPw06r-aSA6}=-oFyZ1Ze?hsnCi zt-&WRdP!8i96Mq8?)BD2x=Nx*z^`H?goWfl+3a7&fNl=uQTS7Y*Od=PCBveIP_jB- z%uu=@FB)!f|Cc$ikYapc3bjG}Uvmm(_iE~N6OgflRZT*OyUf$o3k*;cwqU~-?$n|d z%`tk{{QBI2niNF9RAm9926M zLPDHzZ-)`4@GgNH3HK=A=JS24rzzm&Vg-2zu(j-a-iJti;`D}XJWaj@1S%a0nGRb|&*(2K^@LlS0H3elbT_*G z)=|nMb=fl!L5f0+*L36uJN3Su@*Yt2H=Nz0OmCTRig5NixRAsoLtNTw0PsM*6NQr= z8=wJryf*|+qEKOuaD(0)5s_)8R0!pMSy;oc;M_q4j3PHV3jkoRq^89Mm_1J?*mt9A zXk%a5NN@`S5!W61#lr6|!90JtQzVE%NsxQJU@=j!?%w-0%)=Wsd0J6zcScy<0Z>N3 z!!VGeBQ+c*6iSd3rr150+FA<0&Wt+HhcH~RS(A{j#t==SVb%Rv>KU|Dl=Z>JU(Ln* ze64MB-B+;62ikHP%Dmq0R^gKk|E`)}oHy&CSvQ@IYwT#!Kz7~L?7?`xykWf>zpP(<3R})dEii>nXm*7#HRDE!#W0O< z79(2VEQNU!&=YA`;R1iyCG>;RnorGG5G@+)Qi#8K83@(2&d@RJck7sIPO7riY@8FF zCyIsl_T1e)!#6J)wjLdQov^sFd#b(pj(t|BhDTJn`e-I$69g%!eL_V0pQ%3npJ9}> zfXV?-GcsZ+z&>(;7F1>Q@_{BB*?|=wK!s}Nn1ok3;js6w$s6o3XNt6*@jTlS0w#Ys zU0A<#cBgOSNlbXsnD(OyljW{+nM$<9IaB5ipuoXHITlW~%!qXW;_9h<(Od|e9kC@PzL-B7jBen%s;*VgvM4R6BQ zhkFDu%%1&ZlOru3nS1Y2uGAA1zS32n??9ZMRJma4$to-Myx4hQBXz#MGP1Lhxrfxh zFgKJKwVkxEGZl2#U$N>(m3+u-Ae^&9K}JzA;@&uuv>|PpYFH9a=mc-Msk{xOb?uP;wdn6@QngUZYb<%Q$QeuK-eqe zO==KOrLHnra8&e0EV-x_m<^u!b&PZL#*c45 zL%$B(7An?0yRfe<(}HFwcU;-b0BHI#FHco~$V`Ho6a@_HFe4Erd6f%xT66eC$V&+0 zd=|L^F%&`RMxzpYBrdV`CwygVG`(>b$bxC^j-KkIT9vmzxmv0<2;C}hg%M-6-YO@8 zBUgDt*Se>o;&-b zOLf>+?!~ zc$S6z$--K}DJbj{E(FDYLCA~6Kn~28urB6h&3hDtd@NZCGKVb`M|X@9q^F#;T%v0cMtxFw zBH;#w%ka`*_~^_=zw&llQayh1i*M29xQ;3f5TnT^qHsfYvvcxLUA1Nf(KP@s5@4<_!gMkxsoCHpYSimdCd9+%kREln`fQ>IYfHqGb~44)#X)r>}z_D03G+KF0E?)8^Q zz3GYG@EN1waZJpb(rlX)f6>-L;uP#h>8^1X z`xao!DHo>YiY@Q*ySS$*tjr!!9mNdoiG#y!1m@+372lLM+r7-**;A1t zY*43}+9BFiPWq zTrY_ZQ5Ra4rNAL*tvnL|(Uw5c2w}<{9X;2uq4C(0VwFvtIVUHxVL#Z#$d(~3mx{Qd zv{1Up5FK$o!#M&M0q3@${u=v;jXC_4j@g@{*J1tYH1)e?hs^`aEWh+o+-LQ?{5v+k zcvzE0}UK4GqusxM0y6i(#N72EXT@KfRz?I&No9}L_d{qo_8vvaT%VG&sX%lZYC2(^ z{80VoYY<^$)=()Vg%dOu{{AtoTc?;cB8NBb+;Se~0BvK(c6)6VeIiz>EFvq6L>TQx%TH6=rX7z_yx;AgL*i_nI;?;3=XIrnw!GLC*Rju8ePS`+ zUI|^b56Dq}wCnE4eft+@hsuv`6S6t-Qk`n_1o`18vw2Rr@oY8lbqo+uaSojHNogqEXFRCx_T@5Xjs>Gn0lntVXn0}bbtWq zzVfL4;1+Bx+mmtPP4(mZmU?QZHfP1D8%j*X`1rlyI<(L19@_QJeZE!Oq|#Ey691g) zxe(EPcS4A-A>^XLHNGpVnjKy5jYQ|{uHF{TI3Klz?65+i)LRRogK}&|F$p;@!_9=N zWef{y2}RsZ=!t!$gYtf3)9DV3ic<}ovk75otG2>vDIDNpuvOEUAmIFqoDozhC9MOi zXxYHUbD^+u+CMeO(?a}Hx;;@)xUd8TIset)D^64o60sHA^eS@onC^mkk{Q!mH)0{a5_g#+|QrceI?;hYT;zq2`pZ zc?%;WfAf|(#Q_7qy$}5dinrKS|1901y?^t^9pz!BH#TrM9=E$u!h-2qo9i383h>l3 zNhdXaEJXY&%_9WmC_UCnd7c$8?OfL#B&dMRo#33=@JU#Bt1V_?WA5>Z{RTO@H@i!; zbhen?Xv;b7c-B18*fcks+glwo*&bhen{gtkYvx;y$9_~X^^3DzQpW+NS5&i!=a^GX zhl-c^_voBkl@1vyNdz;ip+gt)(#P&-Z~D;4)fyw+Wv`X+b+~VD%V!xrckR7ELWUJX zT512)^Dlyes9fm6EP_W4YNhIe-ncK^;Gs9O#N-~n;EwIv-ErSM7Q23Jy zA|eu+AqnOkaDYmTAMAI+mYS>(kY!`Ysco>~0h#iqZd8Pe`3=X_;Y7#(FdQ~23Jj%G z_z34R^ZCYpc?Op6Z}Bkz+l3Elcyx$PJb2_%^Fq;h2cW}pUp$Nn+TcgQB9mxKfbv3t zweX=-@S}O6@Qw7f@{~^fDUBVRxOCUI^2!BXK1o_@m5#P3_&Jkuah4-HYGXVTDs_~Z zz$YNAZa9RsEAF?6_E);XPsgN+o#B*)=v1e*F%#9Pqt%p`9f8dh=j686kMLRvJSFM< z6o&sg1Gy0(`NM-cCJiP$(;R>Esb%?Vpl=h;M{T;#(7NunlBYN4h(DKZ>P7q5GHWK) z@bFRg2rvF^zC+*B5M+NSpcCd{g2u_VI-`44mADGs(FrAsUlv;0+xZ<-6r<`#DHz@L z!zSnU&$k{)HK|%#md))5Sx9gW>yo9LA zncXhw54xA~wSrspID~ucr{%JX5&L@u|D=Wr-beutpmET$l%FgdwMp9?_E4wbCD<&0 zWfDxsyjC|;)b^)yCMeF>b(Xs5>}WLFyylSS(95yYn`7VRS;Po`n;1 z91h{8Ig^hKwXRJ6GCv{n1e;|%$_c}j_S&Ixs zR@zu?DRq%ZYWW^1?{bC^A9C)LuCkQ8=GZPNz4Ue$RgL#LE>y9nCT>UU4_Wmm?be8B z@Hr44YpkB3eV?Fiz~!3?oR;22mLU;ebosUZsj#lLe)Fti`%mFY%Ui+n+N&Y`UM*?? z{0Hme(=0bgebUOjC8;k=>jM+tIE150sL~OhC*US?eor9@9l$_!MtEl`LW&7D{YA$F z^EF{rvu8Sl*-;&-`%sXTi?%m~eXixgd@} zRX1PR4cW_BtJELKuB#E7JnyKoV+kenBhWd+IOhE*?V>F~2Wo~#89?9~7H#7Yi&aL@ zP5bXoZfg?CWKKDpuQrSCyJ6r^RAl<}ln-$!-{D3{*TezzR&Lv$G)ws3)~G&MrEt?_E>5?oR=W>*2qO-bnMy6vjS}l(i|esa!+tz+Gc< z+#{jh02fNwvJ;9iwRHpSqhUjdwCl010*|V;H@DP}mX|V*vG1;ZXquIJ!U=QE(XFrW z>QeJZM?%Jdgklb9gS-8j-4E&-46m66TGFgxbuv+=7gQ(v+rhvT{>>v zB{lc0ciJ>gyKEFnH@{Z2nPG8&Bp19n?$`7Ro7 z6i_*>&M7B*IyMRsIIzq>GHzFk9(jCb@(YLk^`w`>hNVli#pvf=S6{d{*_gu#PmN^? zUzRT%_hM5o4YM#%Bf%7)8^CtyF)pDn=SJcC_{ivM9j+(m7FDdTFYG=sZP`11qN0E7 zsLVxiJAdxvsO6KV)L{%kBevt*FNW{_h_AoDwQ#0?vn_O-%&P`Twf-%{H_ola*$N5N zeC_jYYTqbgI(8GAM{6~8Su7g^uGVwdrodXSurc~6fgV94iBuECisQ9p&-pdVGy0`W z_aBEQ$JC!JSW?f4+L7aTIrnXt-1@cev~yT)EMmI$cx#pAzL0w_>Ga{s;P|AwO~sbN zNy_IxB)GUP8S)9(pRAxow3_rOWC%7M)YTmG+u78VzIyp;^&<~s`@`;zPWn+B1k@|j zv$mLdzs!&K3C^#mq5Df+OmnzYvEKT2fQT9`TEJ5zpAh%o>e6=!%aamR>Nxo+sZU!j z^5Dnc+iX!?qc`y%|8#%9^R`y^xOYFtX0q92AHgrWL%M9I*&o>lT8FazT$DAgzRVHR zFARut%975<=*`2S;AaVtf5(TgaC_@#q0+nUtq@bNI*BQK^|cMry56{^SU2iPex1kljw4GQ zHm}4^dZN}nx#OJ|?YX-Ie8x9uyshzTq5a;0Ypl}Toka>j!MDF)`J%yzqx%a6R#-|< z+Gl)=?Tev_DZiYC{q)pTmZg_vEztaHY zcs1`{WRS|@33-&P3@j*G~5-rFDCr7qTSQR*MqVWz#y2tgyVTh+RW96BBzhuIA6KyrVjn z=W?UXii*5%>B?HFUyTii6F{Po=UKR&6$q9E+5UPP`9sGB*YE3+;_$PwO7e9B|4?g0 z54&a^oghx{H%;!ckNaL|<(K55Bi5Z2eGS%u?`D~z_w3-nipuQ&5y*L`W)(&UP^E>T z3`I!z+jz`^%5j5w-7}t_u(152XU3VLjaziIR-LF1baj}tJyAZo=Kd?q2zD@2E!TSS zs7m>rEsSld`R+S_F`|m9HN$QMSFr*NtCIpkKQaYH20xU6eNo!O=Xz7)o_K70)mYoL z)ixqe?eGykYb)=R;BIp=M;Vx|9;lB(xj_Xab>+i3C0YMQ5kM(0?ynv-AUs!Rf`Ng|0E{}Q9ML;%- zcLZ060ebpT<fy)N*VSD7KuZ`mwc5wGzheV;DQRmuC%H3QG5NVz z%f!0~*u^(IUo?QO8#1rmf?PXf^@COS@l@EWr=}9Cr2T{ElN0Q|m&WI3pV#JhlWiSy zL>`32HRR5kTAqCAIwt@4m}GvynrkE7XVjE*w`wk#JhRR7=-j{i!B3+nuWtn^Wn8LQ zXR&d-OKv3G!DZXmvAUmS9S+(`i5_PoiuIJj9R`9=IiEe56owy0te0IfltWJR^p)+N zQC=VF>>K*{FSuFY6@Mk(@wnN$>ov4pQZ`a&G$rHGNok{>OYJ6;SpNSld+_mQnTQ7q zQAOVnFI1tv%O%kwwQ9vwxr`8n*pX9H2x#cwx!qSglGR)Dn>$V_oxIKtoyluEdgoY* zlxspxgar5H4$-*2_56C!pmZeWPx%!?E-o8F%)ilbIIsW}c;1+w)lh~K@2JJ0E2seB z-w7D;FOX!W4?}sdZ6}=0#6?~WTb@X7ojieE;s`Uoc(x9_8cXiFS?Ao^ttNe3thK|M zuekFK%f4)l{D!ty`>d2H^9l@nGgnb36C|mVWJ(ywdh@ZaGQ|g<)Yu;YQl^2>;m1zagCTczLO}ATAjjG1v=Q9P$vu}Bqk-R!TMh+;9 z-W)m9oSw`$?36%+(`OE5q1#+XiSlKVQZW}#SBa*z5i2FJ+0}^;(hp3^2li20+fFpV zGdBYfIo34K;ECcw(3DK)YG{XvB0_ErQn zWt{2H**0Uf;|(F1p*??6Qs1cQfMp9VWOr102X)J}k)ey%4TbH!A&uGl7Z~KR`VTFU zqPGn`i9ED~07si|x#5gkK|eQ6%oxJ+E+T{vS+U~XH*l6jwSiKV)?Iq4#mRFi_-eZ> zQal^myR;qUs`43;Lys3oSx@OS2Xq%&cZgkJP$-~r{LMEGKV&Jq*&K*HJUwHTrL0RvikifT&j$Ea`~&vA z>{c^5E^ek{t}^w@xJdA7R#F>Hy5nJKs*g=T;faUZnso&E~|nK5y-k69Q5kkBOJ{m~9mF#j6J9-HY1HvQLp3 z&CR9udW0Om5xTYQ!1}67ivA%Avv#^0I*Qj$Jj5{*9t;TY+}dR;V6a_w*Pip2Tx+_F zBfSO7gOB^=jQ!;9xNPuvi_y&j#Wx|xMTOdTYHQO4DNAKL88vDAb?-Bi$Hh;c;+p8d zk(;&Ks;>*c0nF{vH3lbcCw%0v$CqMyPL4v9|AIbiewD}k^$f9>)5{w&SZ66O#>(T)lrW0xZYFXackADH z7b%wi<>ru8v(3+j?mKPwrSC40gozqO#F7UOu1Z@{AsV#&_4MirMjfN zRyRnyMRqPRh<1Ot-^x};3Nd?tjc5>_wP;J$fA5G%rrwk*{(ZG<=doLB5^Zz(EESo} zX*0%bOkmZnuo(9usny<8^Qs92-wK4!O-8x!n0Dgi$NwrFRL%8ERdUqoo#5%RrNg8H z{2O+`VPLPS7$qxDaj|q`3nv{_Vss+$Wa$o|;x5<<%b+R1;2jyO0P-R5WH9U&W0VRN z;a>#7W6W%7Dx}(0FE6Bm84&fO{`;Q_aM$EOCg`luPcez!+eb-Ie(j+>lOn%!U5;gv zwsZldS*|cKsb}hYUjGZ<+304ggSr-H?-(~~=TgP|txA?}M`hd4zF)A~oEzQi%OQ@S zver0<-18*?{|?$S35SmEj(dgwr5<`m_y8Sz)pCmQ(?brPy~EjPM+T@1*IohWXa zVP{0I+n#k<#>p6P?pF!X6mLYS=&P)<)@&^3S?6z4xJG4*zg*;JCA2zkWbA8xLTg0zt9*@<|`~{_XO@)D)2mUjwQM8^YY>v7i*^|u?&pDRdp*KOKGNc}i z?c5j89UddVMwY;Hds`4AHOM#o`IuO|FzZor|F>bac8A8Oc|$Hc?(N2FKX+6;iJg=W z4mr&bNfJ(m8vgh=t(Cx~o66KVY9U z$ha@@xb!cGTchTrdmNvr{b`*>(4plsI+ErbGIA;gOI7#nN6y3~0n zs9x=>NoAgXrZP!A%uVkv+yKCJlzVe}BL9rEa_53Arb+we)2%L79Aykad+kh~P+S=) zQk_HYdHx#C4d?jleJ^usJDS(*kH}?6+|u@#^5=9T@+q$ifDs0^Ig5E1hy6Fci!B3c z-C?3*|BjBpBYto~4z|l=?h641NdVGoiFuQ01^5?*sJK^$4WK&nrtkFUouc!SMl+MV zo>jXi{C6CBpH7FZdMu-^x5xg5U`vjbZL`;B52srS3O!d#x7{tN{<*sQ$~x1b!<)At zr-vPUrGKlVrA6%3k$fQ>!A8Q0BVT}TeA>F7uV%E2kYKtAsX{GH7Fp`z`;R57T}N=+ z6$U?iicZdzxwa!69(gB`g7QEc^P;@EC zc*}*r0OCXG$4-i^YMASqHT-c&3szdTYXeTW)g;6h5waWePS%UowM}{Zu=9>JHTQIA z@4Fy5#`5Z*9)^|eK1&Y2=?E&C0MFGdCLR-uJ?QpEQ#-8Bm(8#giuj#8_i5td;QxoG z^9*Y;>%RV-5Fqpxq}NbF5h(%bh8_?wgd!p;(hLYlua?juKTDA$Vm1dWfvDsY2}`#q5FT|L1!^AgG{m&4ymm%IwrQ9gYpz|UEB`QiON zW4xy6d|yhdjlP<+;|04o1?THioQ9GQ?NeS_4bX6;X2U6d+14kMbK^4ZZ=pqrsCrjR z*Lsygju%letk#wJVt@J^`xMP_CjLrBs+%vK{WIk{kVid940hn?soSggG^}zvdOF8X zzg&MQv^FNEMem(R%9qb^N?*k6OBNpH*%sjYr<$7|M19?>Ni7_ngF?n_!TvjAUq0^Z zE=x5|kNUVztFpbz#ZX?)EAvrh);pzUGnHk(-tE#$si@T5bw@rObwVV+v6#?73XS6f z*Bth=2{QvF(DUJR<;CSBG2P7mN9=Ficm%h`t&BzEn>Q!_fRCGeD4pnofTj{m+a;41%j;vSZLBxL z#BWHqWRIoY^P_oYDO!1*L2)K)nr3N%!8mmFMi73lp5gop<>f9$xl#8F!F;||k8aVT z+>@!bFWP^6jZ*y=GH+iMOZ}`EizZbJAa3z3j^0|yowD#0-;=Gfsd0XkX&45Z`|@sm z{eajRRCAee?ww^l*7wE0vR3d%w~y(&v~axha_gUuJ_49t55q~mP#4qot6O2(Te!}< zT%?4+7T3FH3vcZg9OMP-NF=H`cd|0Y$}q<X97kb@# zd+gjmrmvsS`TZrCIPIrC_+J%Ob?Gp-w9s@WrZX8_#5T2izfUqYDHp-Vr(ZNzT;vlv?dAPlberdqB?|+Cyrzv%!$`u0SHF<`#xSO}f5mUcH`||w4YSFfA{OsFs*WXsV z?2^SXH{DSm)zkf_%Rig>bS8E(cC=GB(E1EN;=kVehiL6+@EtqqbW(v;&G3vqInbh` zK_j!n@59_c2v-p@&>YwvVm7}L{1xVwS7lxQ_)?>kQsNcOHU21)JyTmVMWg9QxN~Yj zfun27y5*Z|Cao}dkg`kjFH}Z?Lf5ud!J4`J&l-(GO!t)HI@ePSSz|_dd8e`b%2&*2 zXp{9BzvG2*dcg|F2Wqi6&acy1j1UCaJ_}^7RnP?mRutfmSjX|KfO-T9Dq=dEdI9|( zuorLwElL*8$*JqDIP*^8=?@gBJ}RR(I__}))@8JVGTy`)|4L{J48O2@)qzl8HwC`0 zn<)L&6cqC%!lNRCLk@5vo5$RAnmv=%Yr{Ei7K05duS+sT75G6J9vl~riP?_GY~mwu z%d>HSv9nIs&#|NMuBed^EF><(A-klo>9Vq&g{Y<6yU3)djjc^eu5;1Nx&X0Dw^HaU z+T$UA7Kvji6@AlpP)g*e&y0Fp1$|v_YA*ixU7;(!=US`3;XLGT{AI;txh041t4pw8 zjQOzr`BS1JwaM}&c7rxEYkQYx4v3q%lita&hMM`(MUf6c<&iVdSNDoj8ydr=f3~(& z^X5sODE~?bGn#An4ix8mZomBp!&31@Z8lbI*gS@QQu)OVMOdXV+DJCBUm;c!1@TuQ z0-gYAGJ$6zR?clh*fc^uT8(Nn7ka{lgQuwIdPCEF8+M;R-*a$3?Y-0^KbOCrs=DMO ztN$c(ax$Oq)>Ny)JvkB$QL)Q#F!e%?UoKeVj{ZO4{+-Var`iTk)xoEljZtC;9Q0Mq z2Jlelb1u`rYpduPi@owsh)pJu>#}Y}Pd@0VB3rM)7p;mhf&*^?>x)#-%2y7Ymk*o+ zMIEsIwx@Of82S(Mf4}|pm{ngXX=1x4og4X+=-cBW6!BN0^+|fV;7q7=qPi?+NdBYO z0im6iL!^J9uFO>@_r+nSoGF#$LLu0%d)8BO-isqdF07D{_7^>S4Ns!wvnHa{U?`6N zxr~HMc7+=QZzFW^K$wG9_*>96^%{m5-jXsL&HZ7lq2C|y0gSmWL^B58sb>~!z{7Nd z@XWOzn1ICmoh#4GN=`UabZzpNzptwLnLnJU4(;wMML>!$T&jI7eSs`7dB`DS29uqk zJ+NU9$MwKZPlBubsn3y%Iz(K7=`OY(@q_lCERGfO2<|bcg9~vEp~}c~^`;-~*A=tG zP3~GO`@T{7`?gW$GN0@n^@3R((9H*s_`dMYisu|-n9r`3_i|ToHwuEyFj(G4^ z-V@IoLbF6o&+ANCjfB8aDaQsM3s;gBJ{WJQBGrU<{yvgBJ|7wu(?!guBm#bp7 zm+_Ta)TbY8l6q`%JaHeS+%88RMP9o7SZVCbh>^U3lE?&QQ+? z1{a^Y?t^;C8uEt1{??mYRSlQ+T|AH1#TK_q^DHO9W@y>z7LW7-b{5oNR<&*IjyL7fypJlPDsdBEq1R~;G(7L`fnp)eRi^v9y@F5;CiL? z!rf2@c2*U^bk(iBMwQSO$;@c zx4h!t3>(m{_nTLIF)o6TNBqHHRQCO46ZF2d7mre zylGUd+}nbD%uBZn&;-Uneqc+*$Xr$d`!ntNLY$qsbMq)uO11J;K8p1;=c;{3)#|wYzEH49!*rgZ{)wuNc-;E@#v((PHRYs9;^ip2{YR?k zyJluX4uzG~Z_l|}*9&X)Ozu}K$H+xfvk}7Q3Ni>nK|`9va4bP0Fim0pcjp@8MFp{z zMfa>`9|>h8y&~7gOeN~te=waTRlY`^sB3eMLRx3JzZ(^M2G1rlQ(UVj{E{+fY%yZg`}MneF&-a3wbEZ88L&g8+Rvb?y)HKItdU|g)z+Ta;xX76b9 z<>#YboNYEw@CMFHMbGUV$xs5iS(5*DY255L!5((@3qN7O`E)BG{qN zeA%*&(D>3)Eg#Oo=sM8Rre=k_@FK^fx!gnYBVKK`g(#o*F3$0MMu+e;Uvr|>Y4x!8 ztYYWY*We?}cJn1QZgnMoPf?TTtcLQ78;Sv}sm+yr?EIJ>-Z)1s3jOT67PHjskdF`3UU|q;ti&wTM^x*$bL2JN zs}9f=Rm`6dq`O^M#M%l{-U25sx;ZC5NnY6#ny*&N+ec_Zws)tswnpfS!jp+9EQYdryCi!LcSGPu?J*R_P{vywd<|6PA^og#hi;b_7o%*~e0C!Jgc ziE)2+zh=7lb*S&XK00=UNm@BvvZHYA^zW zS&cFb&VQEg%92&pH>027P2A#zwWCjG)uVad{Jv~|gk*jl2+dMYxFCR21n>n>P##$o z31CD3jsxobl*dy@&`kjnUH~M#-NeB6&l(Jvcn?ogZs3jKR0lT2@h;-4LO#8`Rem#c z*7qFcpP?GDxo0L7vsPhr6!G!1>Vf3X9}OCL)+Za2wRP^8vrkNPmc*3*%~#i| ziFkChZHe8&WpG2+*{yrs=xFYNXlRX6_Q?QbAEWE=qx=5kt?YB9*o*;EZQuj(P4sbfZUan5{Lrzu-e$2; zBg>ql4@y{-O&grs5mLxmH6y-bP>_R1knu{k=U~Sc?nng+*>a~3k=8CZY|_k z`i_yAC0ZWqT=SVwq=ooymX=Prl0E76%HzDd*rHCK54VftF;r_C|uU11-nQ1FmNBsQy1Gt6$>rcY{rvpRX~(#$2eculOxesd0H};Z&PAzDa8IUUXXlzdZLf5YSq%-?h8(*+; z=fvq#W@R3p?0J=VTx&J7=9i<=v~z34tr*C9^|m%}Y|_}rz})TuN*pU3$s zSth9JfAnkx=pJ1TmUTfpMrk2ZqI?e2GeyF@UnZMwO@hmbXb^GA)%2Ow$lP>#AVxNq zhU(V`wmQ>qE2ZRnviGshck*Q(Ahug5tJ3~Dr@m1bO6=SP{_#y5!{3}Sz{nTe&vN6$ zNDVfNUZ3O(b}OiongI|C9BUT|(<t^I^ca(F4tn+uH<1#En0r4Z`jt-hTu%!^;0)P?VX+UBM{HvPP zvj2U5IfYhN*v8FVhI{{7BEtlm>Sr#@l&t-CeHSZTcpjgSWvK0@BVIXQudW=V`%c;# z{Z}Qa%|iAZ`z1oO#8_kZJ!MaLP#**>5#XSG;X*#@Qu5%o70rOFhbH*D@oJNxyk?8# zMK(=pJ5nCmT|R#G6%T$>+764>SNpo=TUaQguHJt0Unuf=3;6*jklaQCtpyx(CV+Vm z7|;pQisdZ8*&YL)Brtt~+95In^eO{tSxTp5Te_&sa`q)ABM}w&)>GZd^|3ayK?UNLtQn7Kj=7GFtR`FGnCfUU9`+41^ z<|LSvcb$o~15CuIY_j`e5?T1-svMb1X`9B}czrSf58QF6bOM77q($bBrZX1Q=bW?6 zH>GY_BWE09g-~P+R0XuD9Euo>F`ydwp&&niAf=UmGJ~`a`MI!i@@ryuMx9LJI11&E z_ss{ceT*kptEE!NA^mTtu(TdZkDi?#PkdGGR(FQwb2L2*rO9H}fPMR`az$buwUB6U zGS(_m84)Ej^Rc-v)O4r8z^YTONYb|x5kJ@fDRNq6zR2QvW(7U;`Y}-;o+>LAFdi$DB$%pq0|4jXiOZ3_d1k^^awO#lvXfb2AegZaH&i(d3fs5yYBbS z)^7zp-LeUUi~$r>0QdpB;(4&}{viJdkuvg*WyShKI_zYZ+FNsJwi3>j=4i3z)9PiK zxp?irD-(Jv^?aJ;$>q;(-8tJ{RZw2C+ipM^`WFhleYjW|JH+k>8P;sbNtpT+w?5!u zy=Kc$z`6Qd2*9vqDB)aHuPLyb6hW7Tq4Xk9^TURtfEy&(=f9=OG*BS-*TXgN2mpu1 zSOW>!A?6U=I(-m^R{jErgShDoBsXl)O@tpwCKfD{ePIVSR@CeB^=ot6VLe+C`AjL4 z{$uLfBEi2lwe;pD%NSL{RHZ_W{S@T+$m zy2mUGE}MqP2h}#oz*V0ElQ&}M^r)ohX2B1xezLO9;*T|h_1Lwa`X)oxKQ8{>tY8Y( zY}MA^(#(Gx*SWV_p342tb>g(@9dQekBx%hIksdmR)JB)nn3b+bsP=GHPLM^pJaAxE z1wO6mET*8tG2WPgZ-TrFFtNbt3}?wC!49yhK)wc+5jZKH@6$Yh(orh+%k65S(8_tW z_~FSPoYc-nUt!bb%n;|B$3+=%&}JXN!2|#g8SgPf*{-VY4UWr3?lD3$u=x8$qm>Q$ zXGL5B%G!?{R|#;`>VzAvg7I9$Bkj%Os_&IT6(lv~^&<@8t@AV+{mW<$5AV+;HsIPa z`XrUn1jO28FJ1%-)Fr?=4@lj!kPixafq|=&;Bvl@_XZSw919B&$Jbb1({tTy{XR!|D?mK&<2Qy_Y? z#5(DE-U4&wI#o+Ib}ah3&fS#4!4GW+IW9zc5z_leHBYj0+!8MjbwFb9Z$(_imrzwh z5BnJk3vliw$A2h-(r+K#`OO{$kVMDY4t>0U+Dex)1NcZvoe|*rA9i^-{{W#l_<(!? z_YSZu7$MCr47~Aida;GIcZqr6UoB7;{{6vUGAbSTS^Y+-oAcN%)W7r^xcOk~^Reur zfJh=pHR@v!>-d7mvGsKpPyG1!!StwzxWjis*z?knqeZ!v0%%{{857(QZaVnQ!xW3$ zWdITS@Kh3x<<{9WGV>A7)>6{jnXXe-HFPElg>wesgjJi=Pf`?9C8!u4zR5hwm#8Xr z=}Bl}5?~(~04(S^jd`U2NJc=M0O^o!kc(rN%i_t!@ibQr_(a0Qxaw#ub+Fp_?!}2LRt)SEm<>lXa07BV z9S(l~eRANfaA@KZTp)7-WJRDZ0l}#@xM_>lz9IDS9n;=*C7mMEPv*fkZfAg~;WDQJ z8F*@BPOalo0D~Z+nnBdpS?qLz%9A@=3E!recHgeIeeJH-6^hnc$U4H)wFVy+M%X5i zk$2PUZxY-}|Q4}okw5DBonU_$f%7o(YmocWGt z-3F_X@x5y(-n5{8)dyvIt3v;@1w(oVCN^qxW@^;%;W9r_;Z7mH8r!U zv#qOWszYf0*>qeLHPtriGf6`RxT46Xx*&BknE;8%V1OI>G|4l6D9~6Rq%ksA!=)QQ z9s~jQAFc$*a3m7J6F|J>KU$M;^lAqUd0X_giTNr{DR8~i`Z_X z55QU+>5J0`z{5)L{>wWG8Nb}-nJ?fljt18G$JPHr_l&52HfZc$X)vPD(8s4-+jrx& zLfLkVnd|zbnI!w&4fKSezR88lF~u>hV+{T`++ygY0NkhCO{0?3=L5hBw)Q_~6S_O~ z4}-)sS#}(SWT7E+n5DDW8@PobFrN+w^E}{w0ZT!EgD@B{_S?e2J`xzCihcQP*msWc zTdrL2-{sFR!Ivzi!@fFK^k=tRny7=0VUcz}>Yxn{$oTN3&ZO6R5*~AD#xdE^%}0G9 zx-C9f?taF@G9|4r6MI_f0}SRt!U|a#P2|yS!@$FQ5!S+FQ4UBi3E{)gqYR9NeZ-tY9nS>7Q6|U>bjqIU{rQlmm-%mx~$sIhg2H>k029$7o=7FFv zDajmUh3*{_>{I0-93krR9T1pSO2MFQ%@?ebG2g`j=C52P6F@2OIH0?ste2fAnUz^= zSW1Go&cR8ZiBC7jmZPE$8&OqPS;gJ4SUnJ_$ zhhdo|;QGFss~0I;^nK&_l@oIEM6Uj_e0_aP>q^=bnLr&{luM9 zl3`drOp)4PfQ;a;%Dp@4SRQR!wiOXng}{o#_}}Fef&=P%(6^cJfeC@9Tp7mQ+wfj! z+jQ{o0L-DNE-qZ3_wVxEudAhx9`m`B>_jL9Ib7mCw0uf*b4ZG+r4tZ!fr|#ZbjC&r zW#AVx{>an*8e~GC;^XObruXTdyJF4LEx56bi7UtPt_(S9x1*^(sHoa7g`p;(_J%>i#Yk1%<9QG{2Dcl z6~7d&pD-}TzzJBht%7*#MY}`~E zyhcP%g!vo$?VX+w;uV{+;>(hrh|qdnwlzR85H^-`qOs)H@u;^#cww?Q2c%oT0yxDe z-r;t&WFm5u(R3w@L&bQh7e8)C9Q8oUw%qba7Ai;0+W1jcQBrP#M+PaPDz9 ziW$#?|GGM$$N;z>5)3G~CSVD4#;6Mry*IGTVDjcWJn7-iNO)$ogZ0wqD3%jyO?Klk zi3Bdt5UvCysoS(F_$BbI7e*)1*zW9@W8hCi&?*?%%QsBq<1I-d`?qbUfECuo4z5ER z9B#7!A~U<<Y1DJor;0I3yh>0FOazxk{AGV zZw|c70jKdO1ZqVq$cL75TpI;5U$K_L7TiqE_~#w*`0PXTTD6|-%zM$Zg~0RX(M0kD zve04*WDMA&=fT?)P_ug@pKs#XZiK;sHxqI70KloTAsGTwTPd5R9GJfSn!<4=y&87C zejuUTF_+xrds3IwBbdKX@*wk5K&$61k8q`=4FCcAoz}r?CWXi#3x?~^Sfdl$f$LJ} z;_v$OqnBh7vVJdCg6ip+$nrC;v8>eCQSd`!m+)L3;F%VJOR-$HHl`oOR>7{V2?F^& zn9cs@?-6El?97t61bA8ud6V>EtD6VtgM!$p4Bn%n8Jj8#s8y!UB^QmfuvMHJ8-M{bCeZ;L(k)c&m` z7y3Fv?@k1u){H!=iL;#i254(o7zPV{n}kUM@~s~crx)3P6RDH&0U&Kaop2)ak-p-b z!|52s9syWAN5!NEmd`y5H6#Iok#PO@fg%2So~rZ?hy!{%9?c3$00Jm;4{IiAv5&9L7aLq}$RC-Jx@S%_Zk$sCKb(O$M>VhI1ejtntvRHwP^0aPFg zuCXkh{AIPqKq0+S-5T1De^>HFAVvgpE~{6Fi<`;Eq}1wMCGVkP8&3V~0!8 z=u=|@G&~kiXK_sfWcYt=ibFoNS~tzy?=I(x0rQ40#X_@D?B?gXpfY@2VUTv`g&@h( zI@S%p!T%=Lxoj;QOWR-~^5ayP@fHMAJLahSJK9_G>i_ z4$ORB=k+?=z_vf|`yGZX^UGRcHNO1^@T}A+@ZtohL@abgI*XGx2*=TI^tEO3TBb(M7n50UjeKk=P>Um|V152rhW8M0>y`LOwf_9n-1D zIj6v^C^({qYfLHZBk-Ri>kDV`d;X}hliPs8Nw61W2E2(SC;%3*$m$?Dm`+7^6c=^x zc}!iJcClLbb-xJT3FKI1=g=jk-KKOqadjUg3IMJxu-!g@@r{DWyd>Dcn!xueMBP6@ zz{h^SCqN8%Q`X-m+x?_7{mmI}x0>nJpU&c$Xq0MPn!O%5#IE*cqD|)Y#PSC?Z(n5p zjs7@K(h#p@Z(XC>bR6LoYu^tDy;p^3#;3!n$j>C$1lYk8(_xQ$%`{d^q15}9$bscu z=81;IqfBaKp!e0ht26K0s`X`+qBW_~PLn>|WvwnvNBE z-J$R1h@zh8wa4TRU3hx!r-OAz$=mco?IqS#g?t%ArwbI$1OdG-zpXh2Ho+51ftnP? zt(Z(ft)K1W>Vn=*XuD;54{$wTy~liy~| z*aV9@qmfy)FH|amg{fkpH$Nxf(*MwyT4pwU^4(9gkag)txLSYFHQA`Yf?+Qy9Q-0# zmW}3xJ>J<3G{kM$ z?aW9Skc+cZg^MIE2unV-hTceivu$#uZt?G#0DTkXg z-MQWtq;YIi-V9!2CK_5{G4h~UeK4{Pi!S4mM!$(AGTD6@sIP-D=?H;H!$<=B84;Kv z9`6i9GpSjOjzT&~jF36#^S1f7dC}PH+5a+ELk=NSz5Oa)`3X(t`%kHk`j^-Z>aalc zD|~JC6zd}`6V~UIcS7vWRwH#RUv;hVc$`u`L)TvE~`ywQ|cwcn=hZP znlOEuE2?O8IE8u$m$fi9P4;+m!gF60&Q_O-cW zrZmpAXkWpr0qbUz6|>$+OK7!7`RoJKhx(@|3GpV|Hgba-A^6 zX_>Be!ya2r%}ys!&AGmne|)VnykFi~h%m`ict@Q?t<0Qk^1T&BomMYvw6)P-DbjU_ zPVI5kIl4)73pA4IlVb9p(5(N=W3*Ilk`3qD> zUwT$v#Z=TRc0GAE&N2|ml$Ed+CG>;F>;Xao`P%P-Q%q+Fg%mPj2ZwfSENE}oodP3M zri=l!G+C%D$Qo=G5QqcPCITWs1QrGKe*h3o_*8v&jr#E$x0XMG84O!zzR^n$>U)xz z=*a$PMBs=elF4~;u;TG-lK)5-qY~rD9~UFB2Yv1A(UXZ|*B*ZO)<|IlrSkF$Sx9Oe z3u7wM5YZs;t(yam-d{R6lN*~i1l2ie+|M)yG*cy$uG8}EZSgyUH zjfK7{xQvg8IedJ5|E`wdi5F>%qt`H*$Dwa#4iO@HY?yAd&GG6hd|Ix7?q$dQxO;@E zm?#>s9ZzZ@Eho(FJr_3NL^c374T2e<5i>~i8_*FIMriYmMIQEN0V)kPL_>D1lcU|q z?2AQ*35Xn!L?-APO&9d&htiPm5IaM=G}fJyv4FTvVHeO{9l}dK3nwleEjqKaF~YDQW6Gi!Rar z+nQ$KuOp^>swnOyW|H?M@@m^W|Aihn&V2S- zxTO#w^d}3q8gT8g-LQbX4Zri%nQ>QElw}xOwo4E1CqCwN7a5bR-Q;826!vAbZ~sdY zdp99!%G(FIVS722Lhcli(<8#bj7&d}5z-fiGTxyQF3pcNb8^X7t7ocyZ%lrCd3(xDa#wAeihvmCNM8!`obwCdyrH|6%Kliz8ppqAfRTAh zZm2hT&Jd5|Wjt@Iw!tXHiHHTMCtcUN>bM_AjYmW0D~ljiF6O5Hb;aUw+(94;5VTyt zxOo@kHIzls5J#yWn$$A%3Oq{yQI45mhsK0*TonRhVy*P{;U_mD>1q!px5F2yd9bmK zuS@m#19#O^)e0P;2~gCB#P&g^U@jo@DLk`3Hl-2*zy{Z= zlTg}^KFCsPaUGW}`|y)i5v%db9~SK>-qXNPM-K*$tlEl=V4que6mQ=;q59d(CgUP{ zs%3NL>(L2PvEqySdK?aZ6c(=pY=SjL=DM0OMw=g}`N*EnNpX~0XOPx0+|U2Fh~=2z zZ;W33qxJda*c6*C;wr8L@%fNJ8i^t7NL+`SSBBXo24=%Zt<+&=K zrP{a4x7y`Zy1R*=?gph%=eUCNn+MH2pKb#P$3m&SoxnD2C18o{UX_gmZ&M;B0Hh=1 zkhYAFkg`3*}R|)dTh3nBu()PAHs~p<4#Se&qpgHH(Eo^aS^(0l=1n!uDxK zpx1+fo&Yf!K&K-Pqf^rK7YbNn-yEW00Q3+S<8MgPMMs-~OdMLzx>?$2%)&ZTm zj5p9V8wSO>VYGcN_^$*z_ZjzC?TSTp%6Jc-vUinsiC#qHE>5nmqF+#|Lu4j55o?DA)opkCw3)K zGorO~TP!v8akTK5_vD#%tsQ1U_mXh$`pxPk9a_#XQ0)LE+&$0)(|wIF-5jvu;8Q%w zgB?z5zHQK8S=s}n@AH~|mn<*F_pla=DK-ndtv;XelkRt?+)3ymnL~Adut+ItXdM^h z>GRIH86D%XZZxXI@I%{)h_}Fq zPD7wb9Dk&3Z)8#YPp>z_=MWduvHG5fCR-ye7dN(PvOLg7yPPlU1nFh(HmyxugGq3; z846VVrvZA%dvzGUh&APG8|HS>dPk23V<2VAtzO-=ph0wzlMqwA*Ycbd&y86@vAX+$ z_2Ls6AF?isaEP7HR>MzjEwQIZG|{XxUMmi@P>m2%+@#r(=jp3O&}|`90+gczV63)5 zeQ*%uR*L{ZgyVNHu(ve8ec7BKOD_~0=H&r}6FW#|0sUT371;}R^g)8pfv$Bdl7^i7 z6q1V@IoKyp4AHp&YiszNOY!FO+{rzI#FGl5d;esp$go}~1S-jqD3V?e2~xizL|Gwo zNR%D4ws*iE;U7VGRXdO})F8GoF{GJrK^p7mYiyF)tB1HF4qwti)QnVa`vh4MY$k`_ z-2|qa#d0rO(A3L1w`|I8Rln!q_5AK!nmxHj%TuS4;Gg}0uSB1K|IG@qe(zHiB!V-x zFWcHDzwAx8MH6F{qrZMAkR6_Q7%Q_yyX$TBD4IH%x}V-GmLk!Ei!paOiBsFAUy8|X z)Qdu~zLB@<18+GRqjVN%_VrM3=94hC5+bJY%PBZOGWFqw-b5*qFgxQ%IOrXH{(QQ% zNM_CYL8)K;1xO6sQ=b)T>tX~|6~oMHZfzQ}830V=mEv(nnvC29?rGXFw7eQ~H}jw` zuAl9_=`!WdGI?~~@HiMA6mBK<`kgUf5d;)*o#J9N$8&S$uWoKm{zo#J6#0%eGo+&R zC0y!q(L||bE!-eFd)>jP(zn|jImUIt7wV{;&q^5MYK8Q$NRxORfOi2lpt{};j#>Y| zly@pn8UGifF3TVQFi-yx;lF;hM$nMn+KWOo_LzwsHS13n^0RU2Y(s5F@bB2o1%Ej_ z$R3{as+JVjaDI@I#whAQuAc#VqX(ivc+a-*j1X1F-`5%%Pl`kdo-9qH6Twt35M#!Y zF5d}6m#L4QlSS)S>R74TKOJIWps>V`k1&COO7Ha!a4Rrv;*da^`q=NCx5-!gqmuhZdy10GNLi^EQK~e+eTZU z-+xI=4F|Q0gS2}9FrNmBCPbPTBJCjmwbZt__c)`#TOY2uXAX`gcoMm@V$7Bb09V?Kg@0 zhlC^ziKj95li+_+`?|;PxdbtVw;B0=8Sz|pWf~1NP4Ye0@98Wn`q_;`?TGQcBIJ4& z*CI~vXJUn=OwJ{QA)_VUM#@@%SIXdQI)VMYbAIX~V#mQ7-vGq>hTyd%J=po2@l=(!&;pPEbN&vcG1f4gbSEoW z%`j%l->0$b9fa4L$wv^ctzlO7jyPw9+`KX}?FYckz0DsNn*GYFjR@dG=LBSC8E>Ds zW6A^DHA$iSZ9HdQ6f75n>^0@c7R)}9o?kF5SgFAC6s%4^G~>#M7^czDR)Xn-vC`|V zMKtwd{go;tV9jcyfw00I2~gTs0N7h1nylP=s=%;SeWp%qK2<>#^S>2c)F)z9jC9h; zr%;R;mt)A?A=V?0a=f#T@+s|DdnKSzFm(z|955N!aBOX4?piD_22l0caGdXOO!K}t z(55r^PmhAgq(GI}_sMc`DS3Qvn!sd=(eh{(KKN8CBUo6-}qw#s4#fyyOc7PvFj`e{03zKSch)a90-G2OC3_4&y#(-oyzsamowz>k5M< zosDk1NgrI4x!QR*C2KTn#7;doi_@Y@?FgqUXE-W&&@vF11tqJY;JroDH2Mstu8wQt4q9gcpomciQm`_fl@q6Xoi! zs^svW|FT?lqvOet%#^Qu^WjkY11y!u>2K&+yu2}!| zWp-sW+qir=C-}_-Q)yetO0#lZ)Fu%zZ4&lvk=BqRu;iYjzO#dAaPuD4(JbKR0U6U_ zSaueWCC3re?@QnTka9k=AAJg(dZl+M?~F6;T(|WUN=^02zBZP_O9VvL$gAV$F+BB* zgKj#sVgX(k5|ua-9NNHkq%z3blSXIgp) z7jiZuaDaxg^vET^ISaO^Ml#btM4kuL2TA%!XH0qL+3=pkaQ}6!A)ak`@r2b{ylknA zo;R7ZinW3zaFkmNzxa*DoY!k2mc#+cKH$lD4^rTc`ptr>PKs9n#7~1F)-^&5XzSnB z!9wD3Op!FBvKM5o;Ve)tv3#{=+QIdJC%g$5VjKf@<`bX}47LHBf8od#vQ*&FTE{#22QGdrR7chAx++=%1m)Pni15y0Nfebin0*DY#8>4@E# z)VH}4RZw6jjblr|PmB7IPv-VfA0EC9UgS|z&ut6ze0@%c`u%y^%j$#ASE|^eefnM- zYQ#EnA9H;$6ef7S*mz1+Bfx>B(BiI=s?z-f5?tyHmi>~x%d#WSyWBjY^rYh}-pkT^s2jLuW6Qv# z6i5!l5+!+VF%&^}PT00erk`CTldu+CO*`2xro-hg)&G8*9zZKtj*RX#z`(_i>x0W= zVqt#lWD?FmJEtCcD8+2Bj!drqL1VdwOv)n#C zJ9;3r1V{)@*p~4ex+?P4ve@F%rlG{&RnRCMs&Kr%rTohu=ed;c}D7vg!6~x`Sq*+E@SCdH;+zn`c@GHHOC5pN>2t4o2su7tP9){H+aJ9houUxMX0Z!G$h5EU*NyE37$uwvx7QFC0x- zPBOEHr$39#otunBm4G}_?j|V;n2bQ>RgCV2zEHE@<7$_i4z`a^39@Wa`bzC|i_H(r zESqUvU`Q2$JM=%8;dR7F%0(hwO|4X}CWzD*GT>5aNL)Hh(g@CFS*P^I1>u|ni%R=0 zE?kLLIU*{zl5Kpj%PUn(F_wyYw1eX>Gk&k@kdD&;E6#vIMxvnK1jLnjvciZ3HYxq6 zQHOMPg?874T*@mU{Hhn>xbR!i2O6h7oN%dNkWe`E8c51e$w!YVOc%ixlkGuF;v>6r zgt7#;DK(T(__K*B=KeqqEBI7P6Jq1zj|N?yu?HIno!`&o#`X&8oO?msw#UvdtHK{5)qyQJ3njJmC3yCcTOCYOk0L zxPgqmoK zqpE`TOU#dYILr0#VDcmT>&b*5Cm237vo}d9-S5f92`KZ0E>z#*Yiv1px1;gQ8@|dG zzb4D_suBv6Zw_{{7i2z!8~#$V+69@)=J0Lb?xs7wRP%M#4$2I|xps-|UU}80=6pYw za{$YjYWjYi($hP?%a|NBze_&V0-Li7HW`<%mO`=shOY_;GG89AhOF)sA*?|wNF2On zJ=bw4d3~a=YAZa@M;M(sc@W})2JiiW_nHrMJiUKtnzEk$;V};ssN~RdB>2cVH5(X4 z$kkEu*x8E52IQrdS0^HaugAsa&k9wEF5TV-2f(wpSs$_8dSUxjsuaZP%aouO zWDNX;%R`BI)qx+UTh)#bWsMRD7Xj=vx@(1d3T37oj8{K3d(1O8khSNz9$=ZJC=qkogqLs)mUoA4k5m#agp`?ySp zn69HK+pUdeeLJevy-|nmibllSi)H`R!yA*8`^kcaD?aSjfh4%Ijti9J*jxCc?<9l0 zRhl|eop;Nt$+xv5Pxp#-Uo}IR0WsZ4pcE*UBv^U^J>Exc8dMD#$$$r6y^MFES-7hN z$N1?95w4{|N3f`7xL8&62Md<8j%QHXi&DC`_D<_=>m$S<(zHi;Gakx4QpFZ*;QUVI-|KzYITTpHFOy7Y$KT-(j zT-hCa*kAEHygL!_1EctlU&uKgZ+|h_CkwV+W(eozvQlsgA7cio9w^A23~U)dh9&>~ zFhh(Pbep;C@jxdB(Gk~t$+}>${1n(S!57Ye7fT|+w06ocLgI8=OBxgM$@FtU7h^dG zo^%WfS~_=UZ`1ms)IlH@7G{-5y?j>H19+{5fD+hD0j^wm01C%=mS zYwz3tp-lh(?>QP{oW~j(8HX4u8AdtgrZA+8Q%P!t7@gLkqqg4nl;fl^<4{N%8baAp zDk`lGj-`ZbR92Ew>D*RZ?dSWt?dSdY{QCU^W_zrM#!T+(d0p4*dR?!>^LcQpqU8m8 zu>0@Ao`sdLRPD`&TQoug137bLHi`!cNaRw<$yFT*+DD_O;+A zm$p#isdxGzlx=M2Dai%h=NV8gd0`SI=akq+%(xiG3VtcPoo!KdE;l#1?`-_U$=O|F z%JZuQT}-{e=HQos>N+R3K)y6eTG++vrnTEJSL82jWQOgy9mFK4=au#HIqW_gecOTT zv&OxYrncZYXK?~tgWX)q!nx%0&I}>%g*wQsK(RF)IndxWE76%9%DvO{Jdv5J@mKrq z$x@4hS6(C@Gh$D5j+cUYwjmrcnjGlo%qW8ZeN;joh<<_;xOygfn@d~=dMts}kz9Z5H|JZ75}#k8X!<8)*F0`{j0MswmE*)pRbgUYqB zCr`JG)U}Qdrt>%Y&40ld@@zroc|0fl2RFw=J)8t~VI%Tt0-M(7#R8B}R5wAQfb-Y{ zuN<_FM6_pqLPo!hUX&CX16iQe(V4yHyZ1PJgd~F*s#Ne&y>c$OG$lD5SBZhdSSKp;CVkw$&U6XQ{q7J zgOvM_sh_iZ5;K2?>H)Z>Mle0=#=Iucp}V5@vroxx#HCbUPoB4tiZjvF&Png>q>!Nnqf>0Q@d-i&-Jc;@@j9k)vE#PBQHMC4 z^*Q|5f|PGU^Qcb>k_9vkr0fQ_sl`k~EG6v@;jXKvK z%1fT@$Fe9m`g47t#J==?$Cl0x>%j(}QrZ1i*v(!-q$O$d!<~uD1cwEvn=t+7{#WkV z_asj*mfee>FBrc$|6W&u=Mjd$YJcaMLjCW~ZT`M%`o(8pC|aP%ZYohQe}n5~(8fL^4GwXjFtGVyA$A3yyE&kP!mc8*Ci(TxvjiR zx_Ol73BFanZKpk2jehx>vYQKW8G5t_6?!*>_Sb>P5cHmV$xWSqf2=rAx^1JE9_Q=9 z#{IL7_*|NMYgH@ZulMKbwwpa)ex?7{&M!Y#)E3fn?sRRAi8?(KQ~mv^4`j z*-%V{=o21!Lg~B_jMHOp2w~ml1O>c_k6^us`iKa5?Fxon0g*>2>|?#jfNBl*-@H1Y zZBw3?Y?*QXnnu8*J8nMy-m50NYY$pBzYvj3!?3nwc9lJIbgZhPa$8C4Ic;n2h2a&< z%*pd1g7(*&p^L?%Mq)Y8lG2a?ogrHdDy$YK6{Jq+AM$9i4PG2sxUsjv-8S#mKqtd( zYSW& zCZ|1Fa_f1{-J$3jr&*5PPPY^Dm+b>jE!r(a=N0tQK`s_jy7URmV%@~v%vXvNS%>Zf z5XaJ|Bve5TbRk5CI?f{Tb7HUcv);8Ku6SI)MahFZ=7GuV`gsA!?IxmJbiKt$Gw?2Z z@c6Y&f0s6u1U`#5@o26$>hAt|kI<%4U6#~wA+mC%hRJ)xLuBNpdHPwif^e}EAy_Pg z9Q=P&L%w^vPL4E$Xm# zR`?iw?tBmyEez4qh(3_-5VX9qHBdff`6-5b+GxRFUA(QE{i5z#I?fy(SyEeMY0)h` z%N@y`*J+v^JJ}I=*F0dss)?hzb_=$9WH2h~^sLvhfA?N5G>W|WYPH0k*9&vYcMJ8< zu8$99W~C;)gY zKe6DTC$+dE24g~-R2Jwlv#x0(_8$U$65a6jDPM~D-K-tI*H(+931Oxe<$2p$PvjKd zPJ4;%DLUq|(qHX%dnreDq~YkAdy5-nwRW1MIo-$TuDztFq>g?Ga$wcO%v>I-S3B$@ zxY-~ufLx@j!gV$)HFb3h<&SSiCL>(3t}WPo@48lHa9t$7Lv^Gr@9;?`;kCt9P|OA84N@8(dG!I+w9dtQ$1LKz zuDur#9h$!Sjd(mKE)|C{lF`n%LSH4}AOy!NQZ2qp`ypYlAVyH?d|D|F5y|IRAO(wzQU;RzYv+saCO$K_cy7ObWu@!v zWtX?qa{mxJI5=1PFMRmUYc9rq(kE*pSz$A&T-IZtqZeyj09D|?Qe=KFF$4PguJw|x zeE>B-P_u_};+IepwnfR|BmI4t@t={%fjpLCv34KEeJItWEWX;}7S=0DHST_ekp{}{ z-MF2wAg#loqPzR%wf@Cg|G9XqH=Ho{%+r4g5#GZc5c%`-G&6eHgRIc(XyOy%wrC2} zk8#)u`@94L{imLuF5Bd3GnyAE3jF9XFS4QOd~TBlr`5u=eq+gDKQFdU6c$_ss+Km(Y$XLqh9$uQoSq zGzh-kIR%TGDE0iY1%dFHBV3P#$Wp|sG@5kk3+UzL3JG6&i3z2M+bD>we}ma2Nog($ z0*shUE(gpThA(gBBfdwv6oe~l(a!HjUd^{#^+z8z>l?Z0VV(bH=)vwgew1>b`>`XE zZdSrMn_aunG-`_?!KvoYj5%hppPc3vru?xWWC6Lf`^ju%RlR^@ANE(~p6=25p)&(<`5zGp@#=TgE=ZU=)GvWc|q=D2KL z+Ch=@;Ju{qpglhRzK>3K_d9MZ>+rIm0pF!O7K4h&R%1#s+=cf{>ocltE@Z;ZVD#t_ zV$*0Nr9+GY-8TqHo1hx)z>$aWKPqUQeP6gu1Bs=BAmtK;kg*8wRo=j~%1l6IBqq~>O z^v%?4(~RjpovW~uXToJWn$|ks;<4V(^?~EkYw89aAs%@F zjINo3Mrs^=RB|S<(*>^7j>r4EJ#Y5y7YCml4=v~}wWDmyrd}N@*1h=ZZy{+9)~{3c z4jNZ}S&LGEOrMV=3}QqPk^QY7_pH^92jJzr0kMqnUSjz_1L;m@2a-|hLMSnR;@nQ% zB_!=lE{g*(>kB*85o6iB>&M*Qc94c;#z~XOiygw^Kt!PZApuj+7uh~+qlIW&Zc}J^ zM*Qq_Bz%U&d1vMWr=0djwSJqPCI6trbjR~wRCTex^;z9eDP7|RF3x9u&0Ji?MpRv zyIzi~8xA+-l--U`ZSJazC+`^c42X~2JN_-4;ASeDx4DtF*woE*8Ds@|u%_3k0;==4 z{S>OX;yyHU#vM_*euT=pTS5JON@e0Ixy%az(;c7v^0Lb=);j-X{pIO#%X8%XG=tnL z(KA*#PmMJC&M@xUt2UxGqEt^$fV=L2&Gq-ZzT0Hyypm@$;}eQfqgUi# z{$X2SdCb!GIO}_&SbhKLXCs(}sJnbUwh1xwm_(q{(0ULv^Q^aLe4F0KQBXpv)!6Q~ zR*jDw_O~X=GU)zoh)YE@_VL;8X-G=@tyZ}G8*Zv!`9&jtJ^Fm($>n?s)*~<+h8j?rZnTHdk{O^VX8w_vfs7=Z}TFwfQ)qY~11+R#syTLT3qndhg2H zONzSCHnPNrW3jFibiRmvthdc@?dqWr0+4OPNH|cn(2*#dNj^%5s6k90ch+R)c&{{c z`C9OAEC&6egw=V4F!MjUkCt58rnt*q)08rX&-~6mCah|Ub<2-^sE!ZVSE^v&i1m1f zW>Ide)>EUo01fXEzKO6K^{hCg6vtBn>SM0Iv8fyde!;J;JwoOhhZbmWCi?CQB>;B_>C4}sEn;}oX zxP|+-PN8gGV89cPFLf>lTC6`GEPkD3V7=I=_kLT}u&)hMjJdEF-{ z4Agq%_0`!8eeN7yF=?*@(G+NhYl0dJz4I9fK@70Jgd z%VT?xggeoP{prhV<|K5{e24v+dfs>Ao*A{doE+cU`t%b?kBT~s z?2jldYZ<=8E~2~9;~i{H&Ixkz6<^%QDRCqDDOrtPcnco!D^^Ihf8 z*75Q@;M+eZ)vU|sKDRM8re!}^)i5eXf70V=i1QtLd#w`N7O*yJQa4+cjHiXNUW3et z%Y-ZkxvEg-2`aXMH&{*Di+_boLl1EvpmiY)>);xG$)__5Xmy2rYz-0`**U#3GTrK8Kyhli`H3g**4$X z`H;ng`{3Lo_BrcUy9-7YW(|Y#Q#LDKy`Nt=v3F0bAMMnZ-;KHid#F*`6zUfo7*+~ql^wum1Px7_JZeHpOKhsV7J2+)! z@o=K$)T1x`O}S2F8SVCL)S5Km=G*7W2ov*@+z&K%6ud-@&%64s{lFlFP;VJ;6n>B_ zva6%8>Bt_n03UTJs3*79>QDeq19U>x@r2*a5E6A@pRuaVtGaExxulH2SBvhn?{ptK zQ{Wn%8kv!vWHr`h)M5F4X}ok_ak<$XrFT`4dpLD1=WZ*X*q>!{KQWCt z|MzR^pZJ+QN~!Y->$i%Y3hxgsUpHUxE4pbkJC0(q>uwUKfEsS})z5lFo@g{&@FJ1dmYaRth~f!2_yUW`t3g6ZJpuuMEcJ;>iKzIy36A{x4ehK zl^6RiY>;}0IOR_6vk|{~BqHKAfjm~6rOjJL>x9H)d1Vn+$e_<59s12CUjSMC$XJu} znC4|62W*Oq0>PPU_AwD%R2@2#yu1zIQmiD=Asr?7Ow=?pGJLy^#x(XRn}2Ro`Y))K z$7E$yZD-HS0glv1BwcA?O^7&K zYrdmCB8BQJBKnCz5VaXMYc-<1)$xY)QyZX|K{_7e(2bvyChQ+U5=C?*f3>L&< zxRGSx6im8u2@;qZW;Xu=N?R_=EHps9-&$-94lKx)bo>6A!P#{$KXG_6aNM!h0&->d z_XSCvjM%;1b|=>&NSJt@S70<`?$pO*>H~0no@*xUfHp5)h}q7`K-hDH3y_bd+T_+J zDJnqac}8cX|DWV%9+b5fm1-Wl6gPJ*jI)k@r5V3@<%PFXmr-P&sJ1DpZ%HugTCR40 zouQ!|JLdecnqzk51LH{*VJ&0xeG3<*{re~z*58nZW{3DUpWexDFEQyUaKQW`>4D?l z{i+NO#3K-4B#PP^@_P0Pvb$dV{)SGlXngx(=lSdI?y-#H{e!Xxu5ss98p<=7M$P(4 z(`(n|gW{+nxHwsS6&lXuw01XCJNSeweE&}vffM-G@#vX)`;j;E`u7RssQUysp)p`G zV#Lt%4_qEG;>>BvoHbkoA|)}{jPpk@@5Tt)kWIwKs;%lW0~5E>IqGH7v#}F`>t||v zEZ3iN{~shcyUm8$=o_wEd?I%+d!lg3-&+p{*(JCB04+|l8Aqy*R;TB%b_eyHX}&M- zeUSJiuX@)&flb84gKl9&XT_c3;>qWa!ghysA)X1&2rtxQR!XM;r|E;)etI_!!Nu@tgdl_{8 z>-Nbya2E^u3ZRd1Mm@p@mF7&mBP+SH4&lTXwcgIWYC+2F*G)Fpz@Ym16emU*{b+{* zktmFZpyr&-gG2dE2uV%96DPFRWb}Gqp8x;Dm%-Ic=snF*;I{1Y} ztYTUZL5nZ8=ogs}qOrctnc0zCO}J!|K}f##;EE09>eOe=D=m8pDeWEWnG8lq!wTcS zE>#(|Bb8TE9fSK1#@YI=@UST?>HbfwM`rD}#?|?nUF9VvHFF&+8%_`3wDj8`F8a%4 zpr6|Fa%(qseFL4b(k1U0gsGEqfiA__*9$g1ucFcBSv%-#7U@nWQH-Ef?Nx3!GdzSCRN5Ju{X}&Wn+k5UU@$LWSjw*O^ReV@7Z>KQ4Lnos=X`SI5b6BchDGEZGfR|<;v2WsH4%0wdh&l930jNN z%~+GD##6*q=$11szRlIh-H_B#o+*op&Z;{-zWtNJ&S%!|w~w-E zfRjmWK161w-D42TfsW6tdc^)yM-@*k8KtF3$Y$dbM9W#R_B>_?ttGLgWHYE9)gGxM zc~Kn9fIBkA+)FvZqh_C5K0jLQi9YsK|;{E)f~d*-sEgHKO?V6rxyyvKAffs z3L(js7Z@Aj{vL|j*uE5wZfNZ%1v2mEva8AS>2nOtcNiRE*bvvb9VQp~K2=NXtZdJb zN(b7n9y(_hb=g8q=Aw{)OAZ6jDGx&6Fs4(9(?WCsfIAOPH5PumYZ}8l7$E)#_-g|C zIzm!Jav*Ki8g+%%&yhvTU94n}b)xA7qVn+CWt-ouewT3@tv62)-tXOVephy!1^K;A zRL!-HV=GtM-Z&<@v#iL>C1Pl=K^TcGiC;0tR{okx7}X9SKJJ|W{^U~Fe1%`THD9m>ky+TiloLYgG_ zJ)2HBgfYN+@?3qBf($d2*WQ}3^tZI96Y=$J(IFygrK9OK- zDwi;TT)7XTF}XMq391gn7f>bvcma45Ad-mRmSS)X$Y}?MPM(YZKtcAfP4C_wbWR)C zyg!^WSijWba0E4M_p^Tr4}8^H|EHidF?My9-F4}Y^*Q4nvxDwF5z8pw6rr=uuD9fv zu4fEwGL(@0C2%E&J;7yS5GIu%YugY9Cr#(pdh@`Z;w#VVjpSUV*&|31QD75+CGl_N zBiE8#6x471IugG~L~RdHa{&dwBCw*c{%Fa8-daEvJ^n+a)9Vt8YKWVF;L-~6iqJ2q z1#a#2`*78lX`ob(=VabLlZs8|6p0G9rrX|1T=;*LQhk!&PU^4>+N}U7E&&24Tx4<>W|)h# zi;y-VJe&|3sI|>&Hun7Px0PG=_qVVj{SsGVPx6OTomt?AoN_rv%`*;^G8vFcd$u4p zoluq<*gk&3bP=i@-l|yCfNDWJ3ZDH47BGjj^e3X>=nv6py_LUBs=o=>i{HQ*Ym8si zsx*V-8f;w1sPU0@1+^S_lAb4Kd-cX(?Bh>+5RFb)KSv`MF@fI4GrJe@gB%4+u(4WH zTEAt`7$3o-Ku_usQmrDUmz2R_y9r5G3X$C+IF1;;2-L4Vva{+m(Bo&{?W;1@_>)P4 zsbfJd2e{sxt9j5yHi)m1LNSG6rj#7UV(LciY@ukpk%4Q0D2ONV-3iMH3?ssuUSQK3 zGzQZX!<0C~6zj8vG&GU{KRKZE=BC3;Nq=5B7;iFsd{2hLhN0-#(!Epd%@{;BVHn zD(zh!K@)@3PR{_G0x2OMruYt&FLfCQ^*~YBksnZ(ydByeM#NGD5<9DUzLt=xyfGGtOZy3QxpqdGorgVx% zTDL1S1^Vmhy!E=HI(+>cv4>E>9Juna$RHD(^I{`7IE>a6(=SoMCKZ0!?)v{qk zL06u!-z_lRnxL}zxrUJb>&QSCyrPMy&4AvjnQ&O`dCO-6qjVeOb&Dp#WhvCQ1DOa3 zemi}Ie|e(A-H0t?XXCRY5jdxp4ZvOy#DEH@IHwc|`~Zdnnq4T6rcaaaM=8R+01O>p zm-r&UAJecD3Ha~1XhVbL3P|9`4FYi(D58OnWWbJr6oNkgHNmI#z#V3BFRHxRmZrYsO5zOqfZK|G_}nkVxMIxmA)_r9FSB&j1>A7hz@>yn%+6W^nQPXpwtiL&b>*bH2>-pUrGDTp;tVXqNlub>!s(-GHx7MwM3+DP!>POtiZ z$A+sIn!(WExeFn%Y>4;G6n6y3KsU7lC+nmjvp5i!T`;*c=ckSBJDrq)<2PT1p|5b` z8wq~`;JDy{LLJdzhk|P7y)Ty$I!ofZTf{oi`k9YBRq`As!Hm8T5ti*_BFcEo_z~Qv z+C>Cul4GF!)Mqa^rGg&0@Dcm&Po5dbJU0>RSP{$-a3xMF4w`_?0^=h3)C+F;mWXbEx7sdHLy%!j{a-$U~2@fMJe#k>6HyT9cC0! zngKpa_+9}N)jcHG`1qA)o00>bK|en2dYGOJq%$8?No{*eDmKl$SFgK&j<}5okMn=K zcuy9eph3DoGMWrCYi{i@N#YVdSr|IQFdvH$HgHxOP_-YUZEEIXAU`FPK{fF|g#`A| zq;K@NY^@b<734>SFF}&}_)`VK&K2_ffW23V^MyH2&lFEYm{G;2-EK0y+>>At=O?Iu z(uC<1$wR5OD170vAT$Vu?q5X$UK_p_5g=8z$;CY-TucL(B(QD$_ha59lp=un6v9-~ zAEQweSXFdb88#RIsSdonRDB>kTw?De%1nglU3Oa@7~6jjTb#!)r}H+Kkh^DXZcL5$ zCQ4QiRN{P)+WH!e+W;GuT7n$B$_Ee)PB0f^E|V{D-Cdn63U9`HO~la}8I=z7s&|?O zR7Tu*3W<1w6iAIBSDh@NXW8V>dV~wo&V72OUbrQriHKFVc0v`yx@caTmncdoY`kH` zB_z@U5MChuKyWPu0}+5AQz1lM9E9mmxsVV7%NzVU1*Hy9f(r|QtJ8=G&G6L@Yvf;- z3))2Sbqj#t+Z~J|e*)7}RvZYE&w3E|#Hn__9lbK55rZntX`mP3IYhsQN3UUK!jV`# zFO#}-Pa_uvsd+rLPZ=v#b8bVd9ygV}>k~_Q{wRgE_uMfta{EbnA!AJmI6}{uv|rxM zEXlqtvnUiE%vm}U31$87(}VXIhNZ(_z^Nlnk4Xk+c`-h!8u(uv#ns3`uVHjJ zv%s7#@<#b~69$V*?XB(^#BVGM*J}e(Zrl{$Gag&U=p}D#Wt2m+ zkm3#kjC}KqtT3kOA?umvESS6hOhY`=^&ixeD7PQ>!o>(`;dNSofF&*t=oHKB$1t7u zsV}(1`5Of;{9bz6hp;6UcJ4o#$MXVYRI+`C2ZX=1jseqdLKWg3Euk`z$FS$MnaHbN zFj)%nS~<$B_O378w@OaWm%%AC?V8%;{&zmX(5B<-f-x;(2#*{CUN$~lSQ0w$b$S%j zbKM3%4272jU!m2#D5WdS{>CDba1pj(j<#M zQ0`dOP&Ow~T|Oy^r=b^#j~I;RhOk}{#3=16hX94^p`ESH3i89yqpjf31euF9LEB1! z$mL-E8#&2Nsg*fo6g#^VX zZ|XU07btQ>+wg}Gl0$x5QDs&g>h9!!M*p1P-vKnXPY)FC5j}_F*8YTqM=;D?ZlSnT-Jy$7^9S3$%B|~ zL+V-!yB^8>{Njs;2X76gqkh6Jlj%Sv1hk_hfcwPIN z>_({k;}|yc7(0X*fI!esNNR(^ar~4bTbO6ks`zmWH_Sg!V1!aA>LG{^GK&W%7B6FZ zzCvJXaT5>N7@&X)7=Z~TSwVuLdHj|oM5vlVc()lqmpKjF^qB@9oxW4t=n1y`>)rRE z$0z+E2XdXGX zibqeMVt?c#b$enwkC^4OS~3_PP%SNl+(!(Com6~Pp=L9I&2$Z6H6kt3hc<0IP)hm9 z_Ngz+sYEeMr$f>Rzf;=ef}hw6s~r{tAu$)$0saHUzmxqpxSs#r4`J{wfft~GV+R#* zHE@@PI{{c-SS#sf!AqQiXWcIl)rUCB?JM##rV+!TfLlVfOsBg8qwZcW(~QkmTOUL1 zYBl*GBCL{99q)(b;w5q#UqELYYET+Rirc?pxd}k={x&7W5v4Cnq(yVKvXGAolDOTO zis|m?>_Fb3nl5UJ(R(z*=UhT|a#J+JIL1__xJuX8|>r8 zf0)P<0g{YrtMN14rw>Tn%c5E3xl8iLUafGlIs`@Ci>7z@w&~3PtNh>9Z%Y+ z!$PD_L~NdQ*vuA+nUspbF(bkslBios9wIk9 zk%vGX=Xy=tJ%Eux7v2DFO>pCg!-D(0_{1wY3UW0Mnk~bye+^$CzBcy%*G}Ri#*u5h z0X0*U>`Ig59x9cDOrpS{~#E|HhPB8mG-<{+&vKPhwuV*EQ_ zBnv8Dc6mZRQ>Nz+DZkgv)jXOD__Z{20%M`ptd|{+QR1i@d&{U$?SF<4)qg4kR0yaL zP$8f~K!tz`0Tlu&1XKv95KtkYLO_Lp3IP=YDg;yrs1Q&gph7@}fC>Q>0xASl2&fQH zA)rD)g@6hH6#^;*R0yaLP$8f~K!tz`0Tlu&1XKv95KtkYLO_Lp3IP=YDg;yrs1Q&g pph7@}fC>Q>0xASl2&fQHA)rD)g@6hH6#^;*R0yaL`2RuR{{hjATB-m5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..19ad235dae9b84f36d998184366570efd95535aa GIT binary patch literal 58465 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 z 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 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);