1、人脸模块:人脸查询支持 向量数据库Milvus 和 SQLite

2、人脸模块:FaceNet人脸模型也支持人脸注册,查询等功能
3、人脸模块:Seetaface6 自动下载人脸库
4、人脸模块:Seetaface6解决依赖库重复下载问题
5、人脸模块:支持手动加载人脸库
6、人脸模块:人脸识别相关功能支持更多参数
This commit is contained in:
dengwenjie
2025-06-09 12:12:10 +08:00
parent ad6706f559
commit bca9462331
67 changed files with 4170 additions and 1438 deletions

View File

@@ -185,7 +185,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
### ✅ 已实现功能
- **人脸识别**
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N、人脸库注册、人脸库、人脸库删除
- 人脸检测、人脸识别、人脸比对1:1、人脸比对1:N(支持向量数据库milvus/sqlite)、人脸库注册、人脸库删除
- 5点人脸关键点定位
- 人脸属性检测(性别、年龄、口罩、眼睛状态、脸部姿态)
- 人脸活体检测:图片、视频活体检测
@@ -263,7 +263,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
<dependency>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-all</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</dependency>
```
### 3、完整示例代码
@@ -298,6 +298,15 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
## 近期更新日志
## [v1.0.15] - 2025-06-09
- 人脸模块:人脸查询支持 Milvus 和 SQLite
- 人脸模块FaceNet人脸模型也支持人脸注册查询等功能
- 人脸模块Seetaface6 自动下载人脸库
- 人脸模块Seetaface6解决依赖库重复下载问题
- 人脸模块:支持手动加载人脸库
- 人脸模块:人脸识别相关功能支持更多参数
## [v1.0.15] - 2025-05-17
- 新增OCR文字识别模块支持最新 PP-OCRv5
- OCR文本识别支持文字方向检测与自动校正
@@ -316,22 +325,4 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
- 优化检测返回与包结构
- 新增 dependencyManagement 统一依赖版本管理
## [v1.0.11] - 2025-04-28
- FaceNet 特征提取新增人脸对齐
- 人脸检测新5点人脸关键点定位
- 特征提取接口支持多人脸和最佳人脸提取
- 修复人脸框边界精度问题
- 更新 Maven 发布的 groupId
## [v1.0.10] - 2025-04-19
- 兼容 SeetaFace6 在 Linux 系统下的运行
- 新增全局缓存路径设置功能
- 优化若干功能细节,提升稳定性与性能
## [v1.0.8] - 2025-04-13
- 新增目标检测功能
- 模型调用接口统一封装
- 修复若干已知问题
- 支持自定义选择使用 GPU 或 CPU 运算
- 人脸识别模块新增多种接口,功能更加完善

View File

@@ -12,7 +12,7 @@
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<smartjavaai.version>1.0.15</smartjavaai.version>
<smartjavaai.version>1.0.16</smartjavaai.version>
<!--如果打包运行需要替换成你的main-->
<exec.mainClass>smartai.examples.face.facerec.RetinaFaceDemo</exec.mainClass>

View File

@@ -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
* 支持功能人脸特征提取、人脸比对11
* 支持系统windows 64位linux 64位macOS M系列芯片
* 支持功能人脸特征提取、人脸比对11、人脸比对1N、人脸注册
* @author dwj
* @date 2025/4/11
*/
@@ -29,106 +41,90 @@ import java.util.List;
public class FaceNetDemo {
/**
* 提取人脸特征(支持多人脸)
* 默认使用检测模型:FACENET_FEATURE_EXTRACTION
* 自动裁剪人脸 + 人脸对齐
* 提取人脸特征(多人脸场景)
* 默认使用检测模型:ULTRA_LIGHT_FAST_GENERIC_FACE
* 自动裁剪人脸(处理耗时略有增加)
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向不正可开启人脸对齐以提升特征提取准确度。方法参考自定义配置人脸特征提取
*/
@Test
public void testExtractFeatures(){
try {
//人脸特征提取模型
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
log.info("人脸特征提取模型加载成功");
//提取图片中所有人脸特征
R<DetectionResponse> faceResult = faceModel.extractFeatures("src/main/resources/face/iu_1.jpg");
if(faceResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(faceResult.getData()));
}else{
log.info("人脸特征提取失败:{}", faceResult.getMessage());
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 提取人脸特征(支持多人脸,自定义配置)
* 自动裁剪人脸 + 人脸对齐
* 提取人脸特征(自定义配置)
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向不正可开启人脸对齐以提升特征提取准确度。方法参考自定义配置人脸特征提取
*/
@Test
public void testExtractFeaturesWithCustomConfig(){
try {
//人脸模型参数
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
//人脸特征提取参数
FaceExtractConfig extractConfig = new FaceExtractConfig();
//人脸检测模型配置
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE)));
//当关闭人脸裁剪时,程序将跳过人脸检测与裁剪流程,直接进行特征提取,适用于输入已为标准人脸区域的图像,有助于提升处理效率。
extractConfig.setCropFace(true);
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
extractConfig.setAlign(true);
//人脸检测模型配置指定人脸检测模型ULTRA_LIGHT_FAST_GENERIC_FACE
FaceModelConfig detectModelConfig = new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
//设置人脸检测置信度阈值
detectModelConfig.setConfidenceThreshold(0.98);
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(detectModelConfig));
config.setExtractConfig(extractConfig);
//人脸特征提取模型
//获取人脸模型
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 提取人脸特征(分数最高人脸)
* 默认使用检测模型FACENET_FEATURE_EXTRACTION
* 自动裁剪人脸 + 人脸对齐
*/
@Test
public void testExtractTopFaceFeature(){
try {
//人脸特征提取模型
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 提取人脸特征(分数最高人脸,自定义配置)
* 自动裁剪人脸 + 人脸对齐
*/
@Test
public void testExtractTopFaceFeatureWithCustomConfig(){
try {
//人脸模型参数
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);
//人脸特征提取参数
FaceExtractConfig extractConfig = new FaceExtractConfig();
//人脸检测模型配置
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE)));
config.setExtractConfig(extractConfig);
//人脸特征提取模型
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult.getMessage());
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 人脸比对11-在线模型
* 图片参数:图片路径
* 人脸比对11(基于图像直接比对
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。(接口内自动完成)
* 注意事项:
* 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 {
}
/**
* 人脸比对11- 使用离线模型
* 图片参数:图片路径
* 人脸比对11(基于特征值比对)
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向不正可开启人脸对齐以提升特征提取准确度。方法参考自定义配置人脸特征提取
* @throws Exception
*/
@Test
public void featureComparison2(){
try {
FaceModelConfig config = new FaceModelConfig();
//人脸模型
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult1 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult1.isSuccess()){
log.info("图片1人脸特征提取成功{}", JSONObject.toJSONString(featureResult1.getData()));
}else{
log.info("图片1人脸特征提取失败{}", featureResult1.getMessage());
return;
}
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
if(featureResult2.isSuccess()){
log.info("图片2人脸特征提取成功{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("图片2人脸特征提取失败{}", featureResult2.getMessage());
return;
}
//计算相似度
float similar = faceModel.calculSimilar(featureResult1.getData(), featureResult2.getData());
log.info("相似度:{}", similar);
}
catch (Exception e){
e.printStackTrace();
}
}
/**
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除使用向量数据库Milvus
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向不正可开启人脸对齐以提升特征提取准确度。方法参考自定义配置人脸特征提取
* @throws Exception
*/
@Test
public void searchFace(){
try {
FaceModelConfig config = new FaceModelConfig();
//人脸模型
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
//初始化向量数据库Milvus数据库配置
MilvusConfig vectorDBConfig = new MilvusConfig();
vectorDBConfig.setHost("127.0.0.1");
vectorDBConfig.setPort(19530);
//vectorDBConfig.setCollectionName("face5");
//ID策略自动生成
vectorDBConfig.setIdStrategy(IdStrategy.AUTO);
//索引类型:内积 (Inner Product) 不建议修改
//vectorDBConfig.setMetricType(MetricType.IP);
config.setVectorDBConfig(vectorDBConfig);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//等待加载人脸库结束
while (!faceModel.isLoadFaceCompleted()){
Thread.sleep(100);
}
log.info("====================人脸注册==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult.getMessage());
return;
}
//人脸注册信息
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJson = new JSONObject();
metadataJson.put("name", "iu");
metadataJson.put("age", "25");
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
//人脸注册返回人脸库ID
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
if(registerResult.isSuccess()){
log.info("注册成功ID-{}", registerResult.getData());
}else{
log.info("注册失败:{}", registerResult.getMessage());
}
/*log.info("====================人脸更新==========================");
//更新人脸 只支持自定义IDvectorDBConfig.setIdStrategy(IdStrategy.CUSTOM);
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJsonUpdate = new JSONObject();
metadataJsonUpdate.put("name", "iu_update");
metadataJsonUpdate.put("age", "25");
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
//更新必须设置ID,只有
updateInfo.setId(registerResult.getData());
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
log.info("更新人脸成功");*/
log.info("====================人脸查询==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
if(featureResult2.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
return;
}
FaceSearchParams faceSearchParams = new FaceSearchParams();
faceSearchParams.setTopK(1);
faceSearchParams.setThreshold(0.8f);
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
// R<DetectionResponse> faceSearchResults = faceModel.search("src/main/resources/face/iu_3.jpg", faceSearchParams);
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
log.info("====================人脸删除==========================");
faceModel.removeRegister(registerResult.getData());
log.info("人脸删除成功");
}
catch (Exception e){
e.printStackTrace();
}
}
/**
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除使用轻量数据库SQLite
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向不正可开启人脸对齐以提升特征提取准确度。方法参考自定义配置人脸特征提取
* @throws Exception
*/
@Test
public void searchFace2(){
try {
FaceModelConfig config = new FaceModelConfig();
//人脸模型
config.setModelEnum(FaceModelEnum.FACENET_MODEL);
//初始化向量数据库Milvus数据库配置
SQLiteConfig vectorDBConfig = new SQLiteConfig();
vectorDBConfig.setDbPath("/Users/wenjie/Downloads/face.db");
vectorDBConfig.setSimilarityType(SimilarityType.IP);
config.setVectorDBConfig(vectorDBConfig);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//等待加载人脸库结束
while (!faceModel.isLoadFaceCompleted()){
Thread.sleep(100);
}
log.info("====================人脸注册==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult.getMessage());
return;
}
//人脸注册信息
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJson = new JSONObject();
metadataJson.put("name", "iu");
metadataJson.put("age", "25");
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
//可自定义 ID若未设置则自动生成。
//faceRegisterInfo.setId("00001");
//人脸注册返回人脸库ID
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
if(registerResult.isSuccess()){
log.info("注册成功ID-{}", registerResult.getData());
}else{
log.info("注册失败:{}", registerResult.getMessage());
}
log.info("====================人脸更新==========================");
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJsonUpdate = new JSONObject();
metadataJsonUpdate.put("name", "iu_update");
metadataJsonUpdate.put("age", "25");
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
//更新必须设置ID,只有
updateInfo.setId(registerResult.getData());
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
log.info("更新人脸成功");
log.info("====================人脸查询==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
if(featureResult2.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
return;
}
FaceSearchParams faceSearchParams = new FaceSearchParams();
faceSearchParams.setTopK(1);
faceSearchParams.setThreshold(0.8f);
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
log.info("====================人脸删除==========================");
faceModel.removeRegister(registerResult.getData());
log.info("人脸删除成功");
}
catch (Exception e){
e.printStackTrace();
}
}
/**
* 使用离线模型
* @throws Exception
*/
@Test
public void featureComparisonOffline(){
try {
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.FACENET_FEATURE_EXTRACTION);//人脸模型
config.setModelEnum(FaceModelEnum.FACENET_MODEL);//人脸模型
//设置人脸识别模型文件路径,请根据实际情况替换为本地模型文件的绝对路径
config.setModelPath("/Users/xxx/Documents/develop/face_model/face_feature.pt");
//人脸特征提取参数
FaceExtractConfig extractConfig = new FaceExtractConfig();
FaceModelConfig detectModelConfig = new FaceModelConfig(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
//设置人脸检测模型文件路径,请根据实际情况替换为本地模型文件的绝对路径
detectModelConfig.setModelPath("/Users/xxx/Documents/develop/face_model/ultranet.pt");
//人脸检测模型配置
extractConfig.setDetectModel(FaceModelFactory.getInstance().getModel(detectModelConfig));
config.setExtractConfig(extractConfig);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//自动裁剪人脸并比对人脸特征
float similar = faceModel.featureComparison("src/main/resources/kana1.jpg","src/main/resources/kana2.jpg");
log.info("相似度:{}", similar);
}
catch (Exception e){
e.printStackTrace();

View File

@@ -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位
* 支持功能人脸检测、人脸特征提取、人脸比对11、人脸比对1N、人脸注册
* @author dwj
* @date 2025/4/11
@@ -31,84 +43,56 @@ public class SeetaFace6Demo {
/**
* 人脸检测(自定义模型参数)
* 图片参数:图片路径
*/
@Test
public void testFaceDetectCustomConfig(){
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
DetectionResponse detectedResult = faceModel.detect("src/main/resources/largest_selfie.jpg");
log.info("人脸检测结果:{}", JSONObject.toJSONString(detectedResult));
}
/**
* 人脸检测并绘制人脸框
*/
@Test
public void testFaceDetectAndDraw(){
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
faceModel.detectAndDraw("src/main/resources/largest_selfie.jpg","output/largest_selfie_detected.png");
}
/**
* 人脸检测并绘制人脸框,返回BufferedImage
*
*/
@Test
public void testFaceDetectAndDraw2(){
try {
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
BufferedImage image = null;
String imagePath = "src/main/resources/largest_selfie.jpg";
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
//可以根据后续业务场景使用detectedImage
BufferedImage detectedImage = faceModel.detectAndDraw(image);
Assert.assertNotNull("detectedImage null", detectedImage);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 提取人脸特征(支持多人脸)
* 自动裁剪人脸 + 人脸对齐
* 提取人脸特征(多人脸场景)
* 默认使用SEETA_FACE6_MODEL自己的检测模型
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
*/
@Test
public void testExtractFeatures(){
try {
FaceModel faceModel = FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.SEETA_FACE6_MODEL,
"C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models"));
List<float[]> faceResult = faceModel.extractFeatures("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
//人脸特征提取模型
FaceModelConfig config = new FaceModelConfig();
//指定模型
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//提取图片中所有人脸特征
R<DetectionResponse> faceResult = faceModel.extractFeatures("src/main/resources/face/iu_1.jpg");
if(faceResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(faceResult.getData()));
}else{
log.info("人脸特征提取失败:{}", faceResult.getMessage());
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 提取人脸特征(分数最高人脸)
* 自动裁剪人脸 + 人脸对齐
* 提取人脸特征(只提取图片中分数最高人脸特征)
* 默认使用SEETA_FACE6_MODEL自己的检测模型
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
*/
@Test
public void testExtractTopFaceFeature(){
public void testExtractFeatures2(){
try {
FaceModel faceModel = FaceModelFactory.getInstance().getModel(new FaceModelConfig(FaceModelEnum.SEETA_FACE6_MODEL,
"C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models"));
float[] faceResult = faceModel.extractTopFaceFeature("src/main/resources/kana1.jpg");
log.info("人脸特征提取结果:{}", JSONObject.toJSONString(faceResult));
//人脸特征提取模型
FaceModelConfig config = new FaceModelConfig();
//指定模型
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//提取图片中检测分数最高人脸特征
R<float[]> faceResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(faceResult.isSuccess()){
log.info("人脸特征提取成功:{}", faceResult.getData());
}else{
log.info("人脸特征提取失败:{}", faceResult.getMessage());
}
}catch (Exception e){
e.printStackTrace();
}
@@ -117,20 +101,66 @@ public class SeetaFace6Demo {
/**
* 人脸比对11
* 图片参数:图片路径
* 人脸比对11(基于图像直接比对
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。(接口内自动完成)
* 注意事项:
* 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();
}
}
/**
* 人脸比对11基于特征值比对
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 比对两张图片中提取的人脸特征。
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* @throws Exception
*/
@Test
public void featureComparison2(){
try {
FaceModelConfig config = new FaceModelConfig();
//指定模型
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
//指定模型路径:请根据实际情况替换为本地模型文件的绝对路径(模型下载地址请查看文档)
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult1 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult1.isSuccess()){
log.info("图片1人脸特征提取成功{}", JSONObject.toJSONString(featureResult1.getData()));
}else{
log.info("图片1人脸特征提取失败{}", featureResult1.getMessage());
return;
}
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
if(featureResult2.isSuccess()){
log.info("图片2人脸特征提取成功{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("图片2人脸特征提取失败{}", featureResult2.getMessage());
return;
}
//计算相似度
float similar = faceModel.calculSimilar(featureResult1.getData(), featureResult2.getData());
log.info("相似度:{}", similar);
}
catch (Exception e){
@@ -140,81 +170,90 @@ public class SeetaFace6Demo {
/**
* 人脸比对11
* 先特征提取,后比对人脸特征
* 提取人脸特征图片参数:图片路径
*/
@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();
}
}
/**
* 搜索人脸1N
* 图片参数:图片路径
* 注意事项:请先注册人脸
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除使用向量数据库Milvus
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向较正可关闭人脸对齐以提升性能。方法参考自定义配置人脸特征提取
* @throws Exception
*/
@Test
public void searchFace(){
try {
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
//人脸库路径,从项目中 db/faces-data.db下载到本地
config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
//人脸模型
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
//初始化向量数据库Milvus数据库配置
MilvusConfig vectorDBConfig = new MilvusConfig();
vectorDBConfig.setHost("127.0.0.1");
vectorDBConfig.setPort(19530);
//vectorDBConfig.setCollectionName("face10");
//ID策略自动生成
vectorDBConfig.setIdStrategy(IdStrategy.AUTO);
//索引类型:内积 (Inner Product) 不建议修改
vectorDBConfig.setMetricType(MetricType.COSINE);
config.setVectorDBConfig(vectorDBConfig);
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//等待人脸库加载完毕
Thread.sleep(1000);
FaceResult faceResult = faceModel.search("src/main/resources/kana1.jpg");
if(faceResult != null){
log.info("查询到人脸:{}", faceResult.toString());
}else{
log.info("未查询到人脸");
//等待加载人脸库结束
while (!faceModel.isLoadFaceCompleted()) {
Thread.sleep(50); // 避免 CPU 占用过高
}
log.info("====================人脸注册==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult.getMessage());
return;
}
//人脸注册信息
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJson = new JSONObject();
metadataJson.put("name", "iu");
metadataJson.put("age", "25");
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
//人脸注册返回人脸库ID
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
if(registerResult.isSuccess()){
log.info("注册成功ID-{}", registerResult.getData());
}else{
log.info("注册失败:{}", registerResult.getMessage());
}
/*log.info("====================人脸更新==========================");
//更新人脸 只支持自定义IDvectorDBConfig.setIdStrategy(IdStrategy.CUSTOM);
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJsonUpdate = new JSONObject();
metadataJsonUpdate.put("name", "iu_update");
metadataJsonUpdate.put("age", "25");
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
//更新必须设置ID,只有
updateInfo.setId(registerResult.getData());
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
log.info("更新人脸成功");*/
log.info("====================人脸查询==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_2.jpg");
if(featureResult2.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
return;
}
FaceSearchParams faceSearchParams = new FaceSearchParams();
faceSearchParams.setTopK(1);
faceSearchParams.setThreshold(0.8f);
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
log.info("====================人脸删除==========================");
faceModel.removeRegister(registerResult.getData());
log.info("人脸删除成功");
}
catch (Exception e){
e.printStackTrace();
@@ -222,25 +261,80 @@ public class SeetaFace6Demo {
}
/**
* 删除已注册人脸
* 注意事项:请先注册人脸
* 人脸注册 + 人脸更新 + 人脸查询 + 人脸删除使用轻量数据库SQLite
* 流程:从输入图像中裁剪分数最高的人脸 → 提取其人脸特征 → 注册人脸
* 注意事项:
* 1、首次调用接口可能会较慢。只要不关闭程序后续调用会明显加快。若每次重启程序则每次首次调用都将重新加载仍会较慢。
* 2、若人脸朝向较正可关闭人脸对齐以提升性能。方法参考自定义配置人脸特征提取
* @throws Exception
*/
@Test
public void removeRegisterFace(){
public void searchFace2(){
try {
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);//人脸模型
//人脸库路径,从项目中 db/faces-data.db下载到本地
config.setFaceDbPath("C:/Users/Administrator/Downloads/faces-data.db");
//人脸模型
config.setModelEnum(FaceModelEnum.SEETA_FACE6_MODEL);
config.setModelPath("C:/Users/Administrator/Downloads/sf3.0_models/sf3.0_models");
//使用轻量数据库SQLite
config.setVectorDBConfig(new SQLiteConfig());
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config);
//等待人脸库加载完毕
Thread.sleep(1000);
//使用注册人脸时的key值删除可一次性删除单个
long num = faceModel.removeRegister("kana1");
//删除全部人脸
//long num = currentAlgorithm.clearFace();
log.info("删除成功数量:" + num);
log.info("====================人脸注册==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult = faceModel.extractTopFaceFeature("src/main/resources/face/iu_1.jpg");
if(featureResult.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult.getMessage());
return;
}
//人脸注册信息
FaceRegisterInfo faceRegisterInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJson = new JSONObject();
metadataJson.put("name", "iu");
metadataJson.put("age", "25");
faceRegisterInfo.setMetadata(metadataJson.toJSONString());
//可自定义 ID若未设置则自动生成。
//faceRegisterInfo.setId("00001");
//人脸注册返回人脸库ID
R<String> registerResult = faceModel.register(faceRegisterInfo, featureResult.getData());
if(registerResult.isSuccess()){
log.info("注册成功ID-{}", registerResult.getData());
}else{
log.info("注册失败:{}", registerResult.getMessage());
}
log.info("====================人脸更新==========================");
FaceRegisterInfo updateInfo = new FaceRegisterInfo();
//设置人脸注册的自定义元数据,本例中使用 JSON 格式存储用户信息
JSONObject metadataJsonUpdate = new JSONObject();
metadataJsonUpdate.put("name", "iu_update");
metadataJsonUpdate.put("age", "25");
updateInfo.setMetadata(metadataJsonUpdate.toJSONString());
//更新必须设置ID,只有
updateInfo.setId(registerResult.getData());
faceModel.upsertFace(updateInfo, "src/main/resources/face/iu_2.jpg");
log.info("更新人脸成功");
log.info("====================人脸查询==========================");
//特征提取(提取分数最高人脸特征),适用于单人脸场景
R<float[]> featureResult2 = faceModel.extractTopFaceFeature("src/main/resources/face/iu_3.jpg");
if(featureResult2.isSuccess()){
log.info("人脸特征提取成功:{}", JSONObject.toJSONString(featureResult2.getData()));
}else{
log.info("人脸特征提取失败:{}", featureResult2.getMessage());
return;
}
FaceSearchParams faceSearchParams = new FaceSearchParams();
faceSearchParams.setTopK(1);
faceSearchParams.setThreshold(0.62f);
//等待加载人脸库结束
while (!faceModel.isLoadFaceCompleted()) {
Thread.sleep(50); // 避免 CPU 占用过高
}
List<FaceSearchResult> faceSearchResults = faceModel.search(featureResult2.getData(), faceSearchParams);
log.info("人脸查询结果:{}", JSONArray.toJSONString(faceSearchResults));
log.info("====================人脸删除==========================");
faceModel.removeRegister(registerResult.getData());
log.info("人脸删除成功");
}
catch (Exception e){
e.printStackTrace();
@@ -248,4 +342,5 @@ public class SeetaFace6Demo {
}
}

View File

@@ -38,7 +38,7 @@ public class OcrDetectionDemo {
//指定检测模型
config.setModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
//指定模型位置,需要更改为自己的模型路径(下载地址请查看文档)
config.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
config.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
OcrCommonDetModel model = OcrModelFactory.getInstance().getDetModel(config);
List<OcrBox> boxes = model.detect("src/main/resources/ocr_1.jpg");
log.info("OCR检测结果{}", JSONObject.toJSONString(boxes));

View File

@@ -37,11 +37,11 @@ public class OcrDirectionDetDemo {
//指定检测模型
directionModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
//指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
directionModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
directionModelConfig.setDetModelPath("/Users/wenjie/Documents/develop/ocr模型/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
//指定文本方向检测模型
directionModelConfig.setModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
//指定文本方向检测模型路径,需要更改为自己的模型路径(下载地址请查看文档)
directionModelConfig.setModelPath("/cls/ch_ppocr_mobile_v2.0_cls.onnx");
directionModelConfig.setModelPath("/Users/wenjie/Documents/develop/ocr模型/ch_ppocr_mobile_v2.0_cls.onnx");
OcrDirectionModel directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
List<OcrItem> itemList = directionModel.detect("src/main/resources/ocr_3.jpg");
log.info("OCR方向检测结果{}", JSONObject.toJSONString(itemList));

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -4,9 +4,10 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>SmartJavaAI</name>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
<packaging>pom</packaging>
<description>SmartJavaAI</description>
<modules>

View File

@@ -6,11 +6,11 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<artifactId>smartjavaai-all</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
<name>${project.artifactId}</name>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>

View File

@@ -6,10 +6,10 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<version>1.0.15</version>
<version>1.0.16</version>
<artifactId>smartjavaai-bom</artifactId>
<name>smartjavaai-bom</name>
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>

View File

@@ -6,9 +6,10 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<name>smartjavaai-common</name>
<artifactId>smartjavaai-common</artifactId>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>

View File

@@ -14,6 +14,7 @@ public class DetectionResponse {
private List<DetectionInfo> detectionInfoList;
public DetectionResponse() {
}

View File

@@ -28,6 +28,16 @@ public class FaceInfo {
*/
private LivenessStatus livenessStatus;
/**
* 人脸查询结果
*/
private List<FaceSearchResult> faceSearchResults;
/**
* 人脸特征
*/
private float[] feature;
public FaceInfo() {
}

View File

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

View File

@@ -0,0 +1,84 @@
package cn.smartjavaai.common.entity;
import lombok.Data;
import java.util.Arrays;
import java.util.Optional;
/**
* 通用响应封装类,用于统一接口返回结构
* @author dwj
* @date 2025/6/4
*/
@Data
public class R<T> {
private Integer code;
private String message;
private T data;
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.code = 0;
r.message = "成功";
r.data = data;
return r;
}
public static <T> R<T> fail(Integer code, String message) {
R<T> r = new R<>();
r.code = code;
r.message = message;
r.data = null;
return r;
}
public static <T> R<T> fail(Status status) {
R<T> r = new R<>();
r.code = status.code;
r.message = status.message;
r.data = null;
return r;
}
public enum Status {
SUCCESS(0, "成功"),
INVALID_IMAGE(1, "图像无效"),
FILE_NOT_FOUND(2, "图像文件不存在"),
NO_FACE_DETECTED(3, "未检测到人脸"),
Unknown(-1, "未知错误");
private final int code;
private final String message;
Status(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public static Status valueOf(int val) {
Optional<Status> search = Arrays.stream(values()).filter((status) -> {
return status.code == val;
}).findFirst();
return (Status)search.orElse(Unknown);
}
}
public boolean isSuccess() {
return code != null && code.equals(Status.SUCCESS.code);
}
}

View File

@@ -6,11 +6,11 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<artifactId>smartjavaai-face</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
<name>smartjavaai-face</name>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>
@@ -50,6 +50,12 @@
<version>3.36.0.3</version>
</dependency>
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.5.7</version>
</dependency>
</dependencies>

View File

@@ -19,7 +19,7 @@ public class FaceExtractConfig {
/**
* 是否对齐人脸
*/
private boolean align = true;
private boolean align = false;
/**
* 人脸检测模型

View File

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

View File

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

View File

@@ -3,16 +3,17 @@ package cn.smartjavaai.face.dao;
import cn.smartjavaai.face.entity.FaceData;
import cn.smartjavaai.face.sqllite.RowMapper;
import cn.smartjavaai.face.sqllite.SqliteHelper;
import cn.smartjavaai.face.utils.VectorUtils;
import cn.smartjavaai.face.vector.entity.FaceVector;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections4.CollectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 人脸库持久层
@@ -21,131 +22,168 @@ import java.util.Map;
@Slf4j
public class FaceDao {
private static final String TABLE_NAME_IMG = "face";
private static final String FACE_TABLE_NAME = "face";
private static final String SCHEMA_RESOURCE = "db/schema.sql";
private String dbFilePath;
private static final ConcurrentHashMap<String, FaceDao> INSTANCES = new ConcurrentHashMap<>();
public FaceDao(String dbFilePath) {
this.dbFilePath = dbFilePath;
private final String dbFilePath;
/**
* 获取FaceDao实例单例模式
* @param dbFilePath 数据库文件路径
* @return FaceDao实例
*/
public static FaceDao getInstance(String dbFilePath) {
return INSTANCES.computeIfAbsent(dbFilePath, path -> new FaceDao(path));
}
/**
* 私有构造函数
* @param dbFilePath 数据库文件路径
*/
private FaceDao(String dbFilePath) {
this.dbFilePath = dbFilePath;
try {
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
//自动创建数据库+表
sqliteHelper.initializeDatabase(FACE_TABLE_NAME, SCHEMA_RESOURCE);
} catch (SQLException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public void save(FaceData faceData) throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
sqliteHelper.executeUpdate("INSERT OR REPLACE INTO " + TABLE_NAME_IMG + " (\"index\",\"key\",\"img_data\",\"width\",\"height\",\"channel\") VALUES (?,?,?,?,?,?)", new Object[]{faceData.getIndex(),faceData
.getKey(), faceData.getImgData(), faceData.getWidth(), faceData.getHeight(), faceData.getChannel()});
/**
* 插入或更新人脸向量
* @param faceVector 人脸向量
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public void insertOrUpdate(FaceVector faceVector) throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
Map<String, Object> params = new HashMap<>();
params.put("id", faceVector.getId());
params.put("vector", VectorUtils.toByteArray(faceVector.getVector()));
params.put("metadata", faceVector.getMetadata());
sqliteHelper.executeInsertOrUpdate(FACE_TABLE_NAME, params);
}
/**
* 使用index查询key
*
* @param index
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @param id 人脸ID
* @return 人脸向量
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public String findKeyByIndex(long index) throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
return sqliteHelper.executeQuery("select \"key\" from " + TABLE_NAME_IMG + " where \"index\"=" + index);
public FaceVector findById(String id) throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME + " where \"id\"=" + id;
List<FaceVector> faceVectors = sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
@Override
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
FaceVector face = new FaceVector();
face.setId(rs.getString("id"));
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
face.setMetadata(rs.getString("metadata"));
return face;
}
});
return CollectionUtils.isNotEmpty(faceVectors) ? faceVectors.get(0) : null;
}
/**
* 删除全部
*
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @return 删除的行数
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public long deleteAll() throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
long rows = sqliteHelper.executeUpdate("delete from " + TABLE_NAME_IMG);
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
long rows = sqliteHelper.executeUpdate("delete from " + FACE_TABLE_NAME);
return rows;
}
/**
* 查询index
*
* @param keys
* @return
* @throws SQLException
* @throws ClassNotFoundException
* 使用id数组查询
* @param ids ID数组
* @return 人脸向量列表
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public List<Long> findIndexList(String... keys) throws SQLException, ClassNotFoundException {
public List<FaceVector> findByIds(String... ids) throws SQLException, ClassNotFoundException {
// 使用 Stream API
String inKeys = Arrays.stream(keys)
String inKeys = Arrays.stream(ids)
.map(s -> "'" + s + "'")
.reduce((s1, s2) -> s1 + "," + s2)
.orElse("");
String sql = "select \"index\" from " + TABLE_NAME_IMG + " where \"key\" in (" + inKeys + ")";
log.info("sql{}", sql.toString());
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
return sqliteHelper.executeQuery(sql, new RowMapper<Long>() {
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME + " where \"id\" in (" + inKeys + ")";
log.debug("sql{}", sql);
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
List<FaceVector> faceVectors = sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
@Override
public Long mapRow(ResultSet rs, int index) throws SQLException {
return rs.getLong(1);
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
FaceVector face = new FaceVector();
face.setId(rs.getString("id"));
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
face.setMetadata(rs.getString("metadata"));
return face;
}
});
return faceVectors;
}
/**
* 删除人脸
* @param keys
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @param ids ID数组
* @return 是否成功
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public boolean deleteFace(String... keys) throws SQLException, ClassNotFoundException {
String inKeys = Arrays.stream(keys)
public boolean deleteFace(String... ids) throws SQLException, ClassNotFoundException {
String inKeys = Arrays.stream(ids)
.map(s -> "'" + s + "'")
.reduce((s1, s2) -> s1 + "," + s2)
.orElse("");
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
String sql = "delete from " + TABLE_NAME_IMG + " where \"key\" in (" + inKeys + ")";
log.info("sql{}", sql.toString());
sqliteHelper.executeUpdate(sql);
return true;
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
String sql = "delete from " + FACE_TABLE_NAME + " where \"id\" in (" + inKeys + ")";
int rows = sqliteHelper.executeUpdate(sql);
log.debug("删除了{}行数据", rows);
return rows == ids.length;
}
/**
* 分页查询人脸
* @param pageNo
* @param pageSize
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @param pageNo 页码
* @param pageSize 每页大小
* @return 人脸向量列表
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public List<FaceData> findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
String sql = "select \"key\",\"img_data\",\"width\",\"height\",\"channel\" from " + TABLE_NAME_IMG +
public List<FaceVector> findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME +
" limit " + pageNo * pageSize + "," + pageSize;
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
return sqliteHelper.executeQuery(sql, new RowMapper<FaceData>() {
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
return sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
@Override
public FaceData mapRow(ResultSet rs, int index) throws SQLException {
FaceData face = new FaceData();
face.setKey(rs.getString("key"));
face.setImgData(rs.getBytes("img_data"));
face.setWidth(rs.getInt("width"));
face.setHeight(rs.getInt("height"));
face.setChannel(rs.getInt("channel"));
public FaceVector mapRow(ResultSet rs, int id) throws SQLException {
FaceVector face = new FaceVector();
face.setId(rs.getString("id"));
face.setVector(VectorUtils.toFloatArray(rs.getBytes("vector")));
face.setMetadata(rs.getString("metadata"));
return face;
}
});
}
/**
* 更新index
* @param index
* @param faceData
* @return
* @throws SQLException
* @throws ClassNotFoundException
* 关闭所有实例
*/
public int updateIndex(long index,FaceData faceData) throws SQLException, ClassNotFoundException {
SqliteHelper sqliteHelper = new SqliteHelper(dbFilePath);
return sqliteHelper.executeUpdate("INSERT OR REPLACE INTO " + TABLE_NAME_IMG + " (\"index\",\"key\",\"img_data\",\"width\",\"height\",\"channel\") VALUES (?,?,?,?,?,?)", new Object[]{index,faceData
.getKey(), faceData.getImgData(), faceData.getWidth(), faceData.getHeight(), faceData.getChannel()});
public static void closeAll() {
INSTANCES.clear();
log.debug("所有FaceDao实例已关闭");
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package cn.smartjavaai.face.enums;
/**
* ID生成策略
* @author dwj
* @date 2025/5/29
*/
public enum IdStrategy {
AUTO, // 自动生成
CUSTOM // 用户自定义 ID由 config.idValue 指定)
}

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ public class FaceAttributeModelFactory {
// 初始化默认算法
static {
registerModel("seetaface6model", Seetaface6FaceAttributeModel.class);
log.info("缓存目录:{}", Config.getCachePath());
log.debug("缓存目录:{}", Config.getCachePath());
}
}

View File

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

View File

@@ -95,7 +95,7 @@ public class LivenessModelFactory {
// 初始化默认算法
static {
registerModel("seetaface6model", Seetaface6LivenessModel.class);
log.info("缓存目录:{}", Config.getCachePath());
log.debug("缓存目录:{}", Config.getCachePath());
}
}

View File

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

View File

@@ -1,159 +0,0 @@
package cn.smartjavaai.face.model.facerec;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.face.config.FaceExtractConfig;
import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.entity.FaceResult;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.util.List;
/**
* 人脸识别模型
* @author dwj
*/
public abstract class AbstractFaceModel implements FaceModel {
@Override
public void loadModel(FaceModelConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public DetectionResponse detect(String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public DetectionResponse detect(InputStream imageInputStream) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public DetectionResponse detect(BufferedImage image) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public DetectionResponse detect(byte[] imageData) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public void detectAndDraw(String imagePath, String outputPath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float calculSimilar(float[] feature1, float[] feature2) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(String imagePath1, String imagePath2) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, InputStream inputStream) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, BufferedImage sourceImage) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, byte[] imageData) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(InputStream inputStream) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public long removeRegister(String... keys) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public long clearFace() {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(byte[] imageData1, byte[] imageData2) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(BufferedImage sourceImage) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(byte[] imageData) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(byte[] imageData) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(BufferedImage image) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(BufferedImage image) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(byte[] imageData) {
throw new UnsupportedOperationException("默认不支持该功能");
}
}

View File

@@ -1,9 +1,11 @@
package cn.smartjavaai.face.model.facerec;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.face.config.FaceExtractConfig;
import cn.smartjavaai.common.entity.R;
import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.entity.FaceResult;
import cn.smartjavaai.face.entity.FaceRegisterInfo;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.common.entity.FaceSearchResult;
import java.awt.image.BufferedImage;
import java.io.InputStream;
@@ -27,42 +29,54 @@ public interface FaceModel {
* @param imagePath 图片路径
* @return
*/
DetectionResponse detect(String imagePath);
default DetectionResponse detect(String imagePath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 人脸检测
* @param imageInputStream 图片输入流
* @return
*/
DetectionResponse detect(InputStream imageInputStream);
default DetectionResponse detect(InputStream imageInputStream){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 人脸检测
* @param image BufferedImage
* @return
*/
DetectionResponse detect(BufferedImage image);
default DetectionResponse detect(BufferedImage image){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 人脸检测
* @param imageData
* @return
*/
DetectionResponse detect(byte[] imageData);
default DetectionResponse detect(byte[] imageData){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 检测并绘制人脸
* @param imagePath 图片输入路径(包含文件名称)
* @param outputPath 图片输出路径(包含文件名称)
*/
void detectAndDraw(String imagePath, String outputPath);
default void detectAndDraw(String imagePath, String outputPath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 检测并绘制人脸
* @param sourceImage
* @return
*/
BufferedImage detectAndDraw(BufferedImage sourceImage);
default BufferedImage detectAndDraw(BufferedImage sourceImage){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 计算相似度
@@ -70,7 +84,9 @@ public interface FaceModel {
* @param feature2 图2特征
* @return
*/
float calculSimilar(float[] feature1, float[] feature2);
default float calculSimilar(float[] feature1, float[] feature2){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 特征比较
@@ -78,7 +94,9 @@ public interface FaceModel {
* @param imagePath2 图2路径
* @return
*/
float featureComparison(String imagePath1, String imagePath2);
default float featureComparison(String imagePath1, String imagePath2){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 特征比较
@@ -86,15 +104,9 @@ public interface FaceModel {
* @param sourceImag2 图2BufferedImage
* @return
*/
float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2);
/**
* 特征比较
* @param inputStream1 图1输入流
* @param inputStream2 图2输入流
* @return
*/
float featureComparison(InputStream inputStream1, InputStream inputStream2);
default float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
@@ -103,124 +115,291 @@ public interface FaceModel {
* @param imageData2
* @return
*/
float featureComparison(byte[] imageData1, byte[] imageData2);
default float featureComparison(byte[] imageData1, byte[] imageData2){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 注册人脸
* @param key
* @param imagePath
* 提取分数最高人脸进行注册
* @param faceRegisterInfo 注册人脸信息
* @param imagePath 图片路径
* @return
*/
boolean register(String key, String imagePath);
default R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 注册人脸
* @param key
* 提取分数最高人脸进行注册
* @param faceRegisterInfo 注册人脸信息
* @param inputStream
* @return
*/
boolean register(String key, InputStream inputStream);
default R<String> register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 注册人脸
* @param key
* 提取分数最高人脸进行注册
* @param faceRegisterInfo 注册人脸信息
* @param sourceImage
* @return
*/
boolean register(String key, BufferedImage sourceImage);
default R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 注册人脸
* @param key
* 提取分数最高人脸进行注册
* @param faceRegisterInfo 注册人脸信息
* @param imageData
* @return
*/
boolean register(String key, byte[] imageData);
default R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 注册人脸
* 提取分数最高人脸进行注册
* @param faceRegisterInfo 注册人脸信息
* @param feature 人脸特征
* @return
*/
default R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 更新或注册人脸
* 自动提取分数最高人脸进行更新
* @param faceRegisterInfo 注册人脸信息
* @param imagePath
* @return
*/
FaceResult search(String imagePath);
default void upsertFace(FaceRegisterInfo faceRegisterInfo, String imagePath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* @param inputStream
* @return
*/
FaceResult search(InputStream inputStream);
/**
* 查询人脸
* 更新或注册人脸
* 自动提取分数最高人脸进行更新
* @param faceRegisterInfo 注册人脸信息
* @param sourceImage
* @return
*/
FaceResult search(BufferedImage sourceImage);
default void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 更新或注册人脸
* 自动提取分数最高人脸进行更新
* @param faceRegisterInfo 注册人脸信息
* @param feature 人脸特征
* @return
*/
default void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 更新或注册人脸
* 自动提取分数最高人脸进行更新
* @param faceRegisterInfo 注册人脸信息
* @param imageData
* @return
*/
FaceResult search(byte[] imageData);
default void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 删除已标记人脸
* 查询人脸(查询图片中所有人脸)
* @param imagePath
* @param params 人脸查询参数
* @return
*/
default R<DetectionResponse> search(String imagePath, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸(查询图片中所有人脸)
* 适用于多人脸场景
* @param sourceImage
* @return
*/
default R<DetectionResponse> search(BufferedImage sourceImage, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸(查询图片中所有人脸)
* 适用于多人脸场景
* @param imageData
* @return
*/
default R<DetectionResponse> search(byte[] imageData, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 适用于多人脸场景
* @param feature 人脸特征
* @return
*/
default List<FaceSearchResult> search(float[] feature, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
* 适用于单人脸场景
* @param imagePath
* @param params 人脸查询参数
* @return
*/
default R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
* 适用于单人脸场景
* @param sourceImage
* @return
*/
default R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 查询人脸
* 从图像中提取分数最高的人脸特征,并在人脸库中进行 1:N 查询
* 适用于单人脸场景
* @param imageData
* @return
*/
default R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 删除已注册人脸
* @param keys
* @return
*/
long removeRegister(String... keys);
default void removeRegister(String... keys){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 清空人脸库数据
*/
long clearFace();
default void clearFace(){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 特征提取(使用默认配置
* 特征提取(所有人脸
* 适用于多人脸场景
* @param imagePath 图片路径
* @return
*/
List<float[]> extractFeatures(String imagePath);
default R<DetectionResponse> extractFeatures(String imagePath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 特征提取(使用默认配置
* 特征提取(所有人脸
* 适用于多人脸场景
* @param imageData 图片字节流
* @return
*/
List<float[]> extractFeatures(byte[] imageData);
default R<DetectionResponse> extractFeatures(byte[] imageData){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 特征提取(使用默认配置
* 特征提取(所有人脸
* 适用于多人脸场景
* @param image BufferedImage
* @return
*/
List<float[]> extractFeatures(BufferedImage image);
default R<DetectionResponse> extractFeatures(BufferedImage image){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 提取分数最高人脸特征(使用默认配置
* 特征提取(提取分数最高人脸特征)
* 适用于单人脸场景
* @param image BufferedImage
* @return
*/
float[] extractTopFaceFeature(BufferedImage image);
default R<float[]> extractTopFaceFeature(BufferedImage image){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 提取分数最高人脸特征(使用默认配置
* 特征提取(提取分数最高人脸特征)
* 适用于单人脸场景
* @param imagePath 图片路径
* @return
*/
float[] extractTopFaceFeature(String imagePath);
default R<float[]> extractTopFaceFeature(String imagePath){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 提取分数最高人脸特征(使用默认配置
* 特征提取(提取分数最高人脸特征)
* 适用于单人脸场景
* @param imageData 图片字节流
* @return
*/
float[] extractTopFaceFeature(byte[] imageData);
default R<float[]> extractTopFaceFeature(byte[] imageData){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 加载人脸特征
*/
default void loadFaceFeatures(){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 释放人脸特征缓存
*/
default void releaseFaceFeatures(){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 是否加载人脸库完成
* @return
*/
default boolean isLoadFaceCompleted(){
throw new UnsupportedOperationException("默认不支持该功能");
}
}

View File

@@ -0,0 +1,722 @@
package cn.smartjavaai.face.model.facerec;
import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
import ai.djl.opencv.OpenCVImageFactory;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;
import cn.smartjavaai.common.entity.*;
import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.config.FaceExtractConfig;
import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.constant.FaceDetectConstant;
import cn.smartjavaai.face.entity.FaceRegisterInfo;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.enums.FaceModelEnum;
import cn.smartjavaai.face.enums.SimilarityType;
import cn.smartjavaai.face.exception.FaceException;
import cn.smartjavaai.face.factory.FaceModelFactory;
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
import cn.smartjavaai.face.utils.*;
import cn.smartjavaai.face.vector.config.MilvusConfig;
import cn.smartjavaai.face.vector.config.SQLiteConfig;
import cn.smartjavaai.face.vector.constant.VectorDBConstants;
import cn.smartjavaai.face.vector.core.VectorDBClient;
import cn.smartjavaai.face.vector.core.VectorDBFactory;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import io.milvus.param.MetricType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.opencv.core.Mat;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* FaceNet 人脸特征提取模型
* @author dwj
*/
@Slf4j
public class FaceNetModel implements FaceModel, AutoCloseable{
/**
* 特征维度
*/
private static final int DIMENSION = 512;
/**
* 是否加载人脸库完毕
*/
private static volatile boolean isLoadCompleted = false;
private ObjectPool<Predictor<Image, float[]>> predictorPool;
private ZooModel<Image, float[]> model;
private FaceModelConfig config;
/**
* 是否归一化相似度
*/
public static final boolean NORMALIZE_SIMILARITY = true;
public static final List<Float> mean =
Arrays.asList(
127.5f / 255.0f,
127.5f / 255.0f,
127.5f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f);
private VectorDBClient vectorDBClient;
/**
* 加载人脸特征提取模型
* @param config
*/
@Override
public void loadModel(FaceModelConfig config) {
if(Objects.isNull(config)){
throw new FaceException("config为null");
}
if(Objects.isNull(config.getExtractConfig())){
config.setExtractConfig(getDefaultConfig());
}else{
if(Objects.isNull(config.getExtractConfig().getDetectModel())){
throw new FaceException("请设置人脸检测模型");
}
}
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
this.config = config;
String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
Criteria<Image, float[]> faceFeatureCriteria =
Criteria.builder()
.setTypes(Image.class, float[].class)
.optModelName("face_feature") // specify model file prefix
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
"https://resources.djl.ai/test-models/pytorch/face_feature.zip")
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optTranslator(new FaceFeatureTranslator())
.optArgument("normalize", normalize)
.optDevice(device)
.optEngine("PyTorch") // Use PyTorch engine
.optProgress(new ProgressBar())
.build();
try {
model = faceFeatureCriteria.loadModel();
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
log.info("当前设备: " + model.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new FaceException("模型加载失败", e);
}
//初始化人脸库
if(config.getVectorDBConfig() != null && config.getVectorDBConfig().getType() != null){
if(config.getVectorDBConfig() instanceof MilvusConfig){
MilvusConfig milvusConfig = ((MilvusConfig) config.getVectorDBConfig());
//设置向量维度
milvusConfig.setDimension(DIMENSION);
//相似度计算方式 为空,设置默认值
if(Objects.isNull(milvusConfig.getMetricType())){
//FaceNet 默认使用内积
milvusConfig.setMetricType(MetricType.IP);
}
}else if (config.getVectorDBConfig() instanceof SQLiteConfig){
SQLiteConfig sqliteConfig = (SQLiteConfig) config.getVectorDBConfig();
if(Objects.isNull(sqliteConfig.getSimilarityType())){
//seetaface6 默认使用内积
sqliteConfig.setSimilarityType(SimilarityType.IP);
}
}
vectorDBClient = VectorDBFactory.createClient(config.getVectorDBConfig());
// 加载人脸数据库
if(config.isAutoLoadFace()){
new Thread(new Runnable() {
@Override
public void run() {
try {
log.debug("start load face...");
vectorDBClient.initialize();
isLoadCompleted = true;
log.debug("Load face success!");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
private float[] featureExtraction(Image image){
image.getWrappedImage();
Predictor<Image, float[]> predictor = null;
try {
predictor = predictorPool.borrowObject();
return predictor.predict(image);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
/**
* 计算相似度,返回归一化结果
* @param feature1 图1特征
* @param feature2 图2特征
* @return
*/
@Override
public float calculSimilar(float[] feature1, float[] feature2) {
//默认返回归一化结果
return SimilarityUtil.calculate(feature1, feature2, SimilarityType.IP, NORMALIZE_SIMILARITY);
}
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
*/
@Override
public float featureComparison(String imagePath1, String imagePath2) {
if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
throw new FaceException("图像文件不存在");
}
R<float[]> feature1 = extractTopFaceFeature(imagePath1);
if (!feature1.isSuccess()){
throw new FaceException(feature1.getMessage());
}
R<float[]> feature2 = extractTopFaceFeature(imagePath2);
if (!feature2.isSuccess()){
throw new FaceException(feature2.getMessage());
}
float ret = calculSimilar(feature1.getData(), feature2.getData());
return ret;
}
@Override
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
throw new FaceException("图像无效");
}
R<float[]> feature1 = extractTopFaceFeature(sourceImage1);
if (!feature1.isSuccess()){
throw new FaceException(feature1.getMessage());
}
R<float[]> feature2 = extractTopFaceFeature(sourceImag2);
if (!feature2.isSuccess()){
throw new FaceException(feature2.getMessage());
}
float ret = calculSimilar(feature1.getData(), feature2.getData());
return ret;
}
@Override
public float featureComparison(byte[] imageData1, byte[] imageData2) {
if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
throw new FaceException("图像无效");
}
R<float[]> feature1 = extractTopFaceFeature(imageData1);
if (!feature1.isSuccess()){
throw new FaceException(feature1.getMessage());
}
R<float[]> feature2 = extractTopFaceFeature(imageData2);
if (!feature2.isSuccess()){
throw new FaceException(feature2.getMessage());
}
float ret = calculSimilar(feature1.getData(), feature2.getData());
return ret;
}
/**
* 获取默认特征提取配置
* @return
*/
private FaceExtractConfig getDefaultConfig() {
FaceExtractConfig config = new FaceExtractConfig();
FaceModelConfig detectModelConfig = new FaceModelConfig();
detectModelConfig.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
detectModelConfig.setConfidenceThreshold(0.98);
log.debug("创建默认检测模型ULTRA_LIGHT_FAST_GENERIC_FACE");
FaceModel detectModel = FaceModelFactory.getInstance().getModel(detectModelConfig);
log.debug("创建检测模型完毕");
config.setDetectModel(detectModel);
return config;
}
@Override
public R<DetectionResponse> extractFeatures(BufferedImage image) {
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
return R.fail(R.Status.NO_FACE_DETECTED);
}
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
NDManager manager = NDManager.newBaseManager();
for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
FaceInfo faceInfo = detectionInfo.getFaceInfo();
float[] features = null;
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.getExtractConfig().isAlign()){
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
features = featureExtraction(alignedImg);
}else{
//不对齐人脸
features = featureExtraction(subImage);
}
faceInfo.setFeature(features);
}
return R.ok(detectedResult);
}
@Override
public R<DetectionResponse> extractFeatures(byte[] imageData) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public R<DetectionResponse> extractFeatures(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractFeatures(image);
}
@Override
public R<float[]> extractTopFaceFeature(BufferedImage image) {
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
float[] features = null;
if(config.getExtractConfig().isCropFace()){
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
return R.fail(R.Status.NO_FACE_DETECTED);
}
//只取第一个人脸
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.getExtractConfig().isAlign()){
NDManager manager = NDManager.newBaseManager();
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
features = featureExtraction(alignedImg);
}else{
//不对齐人脸
features = featureExtraction(subImage);
}
}else{
//不裁剪人脸直接提取特征
features = featureExtraction(djlImage);
}
return Objects.isNull(features) ? R.fail(R.Status.Unknown) : R.ok(features);
}
@Override
public R<float[]> extractTopFaceFeature(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractTopFaceFeature(image);
}
@Override
public R<float[]> extractTopFaceFeature(byte[] imageData) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return register(faceRegisterInfo, bufferedImage);
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
//提取最大人脸特征
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
if(!featureResponse.isSuccess()){
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
}
return register(faceRegisterInfo, featureResponse.getData());
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
BufferedImage image = null;
try {
image = ImageIO.read(inputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return register(faceRegisterInfo, image);
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return register(faceRegisterInfo, ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
if(Objects.isNull(feature)){
throw new FaceException("人脸特征为空");
}
FaceVector faceVector = new FaceVector();
if(faceRegisterInfo != null){
faceVector.setId(faceRegisterInfo.getId());
faceVector.setMetadata(faceRegisterInfo.getMetadata());
}
faceVector.setVector(feature);
return R.ok(vectorDBClient.insert(faceVector));
}
@Override
public void removeRegister(String... keys) {
vectorDBClient.deleteBatch(Arrays.asList(keys));
}
@Override
public void clearFace() {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
vectorDBClient.dropCollection(VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME);
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
upsertFace(faceRegisterInfo, bufferedImage);
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
if(Objects.isNull(faceRegisterInfo)){
throw new FaceException("注册信息为空");
}
if(StringUtils.isBlank(faceRegisterInfo.getId())){
throw new FaceException("注册信息中ID为空");
}
//提取最大人脸特征
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
if(!featureResponse.isSuccess()){
throw new FaceException(featureResponse.getMessage());
}
upsertFace(faceRegisterInfo, featureResponse.getData());
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature) {
if(Objects.isNull(feature)){
throw new FaceException("人脸特征为空");
}
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
FaceVector faceVector = new FaceVector();
if(faceRegisterInfo != null){
faceVector.setId(faceRegisterInfo.getId());
faceVector.setMetadata(faceRegisterInfo.getMetadata());
}
faceVector.setVector(feature);
vectorDBClient.upsert(faceVector);
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
upsertFace(faceRegisterInfo, ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return searchByTopFace(bufferedImage, params);
}
@Override
public R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return searchByTopFace(ImageIO.read(new ByteArrayInputStream(imageData)), params);
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
//提取最大人脸特征
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
if(!featureResponse.isSuccess()){
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
}
return R.ok(search(featureResponse.getData(), params));
}
@Override
public List<FaceSearchResult> search(float[] feature, FaceSearchParams params) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
if(Objects.isNull(feature)){
throw new FaceException("人脸特征为空");
}
if(Objects.isNull(params)){
throw new FaceException("人脸查询参数为空");
}
//设置默认值
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.FACENET_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
List<FaceSearchResult> searchResults = vectorDBClient.search(feature, searchParams);
return searchResults;
}
@Override
public R<DetectionResponse> search(String imagePath, FaceSearchParams params) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return search(bufferedImage, params);
}
@Override
public R<DetectionResponse> search(BufferedImage sourceImage, FaceSearchParams params) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
//提取所有人脸特征
R<DetectionResponse> detectionResponse = extractFeatures(sourceImage);
if(!detectionResponse.isSuccess()){
return detectionResponse;
}
//设置默认值
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.FACENET_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
for (DetectionInfo detectionInfo : detectionResponse.getData().getDetectionInfoList()){
if(Objects.nonNull(detectionInfo.getFaceInfo()) && Objects.nonNull(detectionInfo.getFaceInfo().getFeature())){
List<FaceSearchResult> searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
}
}
return detectionResponse;
}
@Override
public R<DetectionResponse> search(byte[] imageData, FaceSearchParams params) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return search(ImageIO.read(new ByteArrayInputStream(imageData)), params);
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public void loadFaceFeatures() {
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
vectorDBClient.loadFaceFeatures();
}
@Override
public void releaseFaceFeatures() {
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
vectorDBClient.releaseFaceFeatures();
}
@Override
public void close() {
if (predictorPool != null) {
predictorPool.close();
}
if(Objects.nonNull(vectorDBClient)){
vectorDBClient.close();
}
}
@Override
public boolean isLoadFaceCompleted() {
return isLoadCompleted;
}
}

View File

@@ -1,367 +0,0 @@
package cn.smartjavaai.face.model.facerec;
import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
import ai.djl.opencv.OpenCVImageFactory;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;
import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.config.FaceExtractConfig;
import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.enums.FaceModelEnum;
import cn.smartjavaai.face.exception.FaceException;
import cn.smartjavaai.face.factory.FaceModelFactory;
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
import cn.smartjavaai.face.utils.FaceAlignUtils;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.utils.OpenCVUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.opencv.core.Mat;
import org.opencv.face.Face;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author dwj
*/
@Slf4j
public class FeatureExtractionModel extends AbstractFaceModel implements AutoCloseable{
private ObjectPool<Predictor<Image, float[]>> predictorPool;
private ZooModel<Image, float[]> model;
private FaceModelConfig config;
public static final List<Float> mean =
Arrays.asList(
127.5f / 255.0f,
127.5f / 255.0f,
127.5f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f);
/**
* 加载人脸特征提取模型
* @param config
*/
@Override
public void loadModel(FaceModelConfig config) {
if(Objects.isNull(config)){
throw new FaceException("config为null");
}
if(Objects.isNull(config.getExtractConfig())){
config.setExtractConfig(getDefaultConfig());
}else{
if(Objects.isNull(config.getExtractConfig().getDetectModel())){
throw new FaceException("请设置人脸检测模型");
}
}
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
this.config = config;
String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
Criteria<Image, float[]> faceFeatureCriteria =
Criteria.builder()
.setTypes(Image.class, float[].class)
.optModelName("face_feature") // specify model file prefix
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
"https://resources.djl.ai/test-models/pytorch/face_feature.zip")
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optTranslator(new FaceFeatureTranslator())
.optArgument("normalize", normalize)
.optDevice(device)
.optEngine("PyTorch") // Use PyTorch engine
.optProgress(new ProgressBar())
.build();
try {
model = faceFeatureCriteria.loadModel();
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
log.info("当前设备: " + model.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new FaceException("模型加载失败", e);
}
}
private float[] featureExtraction(Image image){
image.getWrappedImage();
Predictor<Image, float[]> predictor = null;
try {
predictor = predictorPool.borrowObject();
return predictor.predict(image);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
/**
* 计算相似度
* @param feature1 图1特征
* @param feature2 图2特征
* @return
*/
@Override
public float calculSimilar(float[] feature1, float[] feature2) {
float ret = 0.0f;
float mod1 = 0.0f;
float mod2 = 0.0f;
int length = feature1.length;
for (int i = 0; i < length; ++i) {
ret += feature1[i] * feature2[i];
mod1 += feature1[i] * feature1[i];
mod2 += feature2[i] * feature2[i];
}
return (float) ((ret / Math.sqrt(mod1) / Math.sqrt(mod2) + 1) / 2.0f);
}
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
*/
@Override
public float featureComparison(String imagePath1, String imagePath2) {
if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
throw new FaceException("图像文件不存在");
}
float[] feature1 = extractTopFaceFeature(imagePath1);
float[] feature2 = extractTopFaceFeature(imagePath2);
float ret = calculSimilar(feature1, feature2);
return ret;
}
@Override
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
throw new FaceException("图像无效");
}
float[] feature1 = extractTopFaceFeature(sourceImage1);
float[] feature2 = extractTopFaceFeature(sourceImag2);
return calculSimilar(feature1, feature2);
}
@Override
public float featureComparison(byte[] imageData1, byte[] imageData2) {
if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
throw new FaceException("图像无效");
}
float[] feature1 = extractTopFaceFeature(imageData1);
float[] feature2 = extractTopFaceFeature(imageData2);
return calculSimilar(feature1, feature2);
}
/**
* 获取默认特征提取配置
* @return
*/
private FaceExtractConfig getDefaultConfig() {
FaceExtractConfig config = new FaceExtractConfig();
FaceModelConfig detectModelConfig = new FaceModelConfig();
detectModelConfig.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
log.debug("创建默认检测模型ULTRA_LIGHT_FAST_GENERIC_FACE");
FaceModel detectModel = FaceModelFactory.getInstance().getModel(detectModelConfig);
log.debug("创建检测模型完毕");
config.setDetectModel(detectModel);
return config;
}
@Override
public List<float[]> extractFeatures(BufferedImage image) {
List<float[]> featureList = new ArrayList<float[]>();
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
throw new FaceException("未检测到人脸");
}
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
NDManager manager = NDManager.newBaseManager();
for (DetectionInfo detectionInfo : detectedResult.getDetectionInfoList()){
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
float[] features = null;
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.getExtractConfig().isAlign()){
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
/*Mat sourceMat = OpenCVUtils.image2Mat(image);
Mat mat = FaceAlignUtils.warpAffine(sourceMat, affine_matrix);
//OpenCVUtils.mat2Image(mat);
Image alignedImg = ImageFactory.getInstance().fromImage(mat);
features = featureExtraction(alignedImg);*/
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
features = featureExtraction(alignedImg);
}else{
//不对齐人脸
features = featureExtraction(subImage);
}
if(Objects.nonNull(features)){
featureList.add(features);
}
}
return featureList;
}
@Override
public List<float[]> extractFeatures(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public List<float[]> extractFeatures(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractFeatures(image);
}
@Override
public float[] extractTopFaceFeature(BufferedImage image) {
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
float[] features = null;
if(config.getExtractConfig().isCropFace()){
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
throw new FaceException("未检测到人脸");
}
//只取第一个人脸
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.getExtractConfig().isAlign()){
NDManager manager = NDManager.newBaseManager();
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
// 5点仿射变换
Mat affine_matrix = OpenCVUtils.toOpenCVMat(manager, srcPoints, dstPoints);
/*Mat sourceMat = OpenCVUtils.image2Mat(image);
Mat mat = FaceAlignUtils.warpAffine(sourceMat, affine_matrix);
OpenCVUtils.mat2Image(mat);
Image alignedImg = ImageFactory.getInstance().fromImage(OpenCVUtils.mat2Image(mat));
features = featureExtraction(alignedImg);*/
Mat mat = FaceAlignUtils.warpAffine((Mat) djlImage.getWrappedImage(), affine_matrix);
Image alignedImg = OpenCVImageFactory.getInstance().fromImage(mat);
features = featureExtraction(alignedImg);
}else{
//不对齐人脸
features = featureExtraction(subImage);
}
}else{
//不裁剪人脸直接提取特征
features = featureExtraction(djlImage);
}
return features;
}
@Override
public float[] extractTopFaceFeature(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractTopFaceFeature(image);
}
@Override
public float[] extractTopFaceFeature(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public void close() {
if (predictorPool != null) {
predictorPool.close();
}
}
}

View File

@@ -40,7 +40,7 @@ import java.util.Objects;
* @author dwj
*/
@Slf4j
public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
public class RetinaFaceModel implements FaceModel, AutoCloseable{
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
@@ -174,7 +174,7 @@ public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
}
img.drawBoundingBoxes(detectedObjects);
Path output = Paths.get(outputPath);
log.info("Saving to {}", output.toAbsolutePath().toString());
log.debug("Saving to {}", output.toAbsolutePath().toString());
img.save(Files.newOutputStream(output), "png");
} catch (IOException e) {
throw new FaceException(e);

View File

@@ -1,19 +1,32 @@
package cn.smartjavaai.face.model.facerec;
import cn.smartjavaai.common.entity.DetectionInfo;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.entity.R;
import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.config.FaceModelConfig;
import cn.smartjavaai.face.dao.FaceDao;
import cn.smartjavaai.face.entity.FaceData;
import cn.smartjavaai.face.constant.FaceDetectConstant;
import cn.smartjavaai.face.entity.FaceRegisterInfo;
import cn.smartjavaai.face.entity.FaceResult;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.enums.SimilarityType;
import cn.smartjavaai.face.exception.FaceException;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.vector.config.MilvusConfig;
import cn.smartjavaai.face.vector.config.SQLiteConfig;
import cn.smartjavaai.face.vector.core.VectorDBClient;
import cn.smartjavaai.face.vector.core.VectorDBFactory;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import com.seeta.pool.*;
import com.seeta.sdk.*;
import cn.smartjavaai.face.seetaface.NativeLoader;
import io.milvus.param.MetricType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
@@ -23,6 +36,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -32,7 +46,12 @@ import java.util.Objects;
*/
@SuppressWarnings("AliMissingOverrideAnnotation")
@Slf4j
public class SeetaFace6Model extends AbstractFaceModel {
public class SeetaFace6Model implements FaceModel , AutoCloseable{
/**
* 特征维度
*/
private static final int DIMENSION = 1024;
private FaceModelConfig config;
@@ -43,10 +62,18 @@ public class SeetaFace6Model extends AbstractFaceModel {
private FaceDatabasePool faceDatabasePool;
private VectorDBClient vectorDBClient = null;
/**
* 默认相似度阈值
* 是否加载人脸库完毕
*/
public static final float SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD = 0.62F;
private static volatile boolean isLoadCompleted = false;
/**
* 是否归一化相似度
*/
public static final boolean NORMALIZE_SIMILARITY = false;
@Override
@@ -55,13 +82,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
if(StringUtils.isBlank(config.getModelPath())){
throw new FaceException("modelPath is null");
}
//设置默认相似度阈值
if(config.getSimilarityThreshold() <= 0){
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
}
//加载依赖库
NativeLoader.loadNativeLibraries(config.getDevice());
log.info("Loading seetaFace6 library successfully.");
log.debug("Loading seetaFace6 library successfully.");
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
String[] faceLandmarkerModelPath = {config.getModelPath() + File.separator + "face_landmarker_pts5.csta"};
@@ -91,18 +114,45 @@ public class SeetaFace6Model extends AbstractFaceModel {
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
this.faceDatabasePool = new FaceDatabasePool(faceDatabasePoolConfSetting);
new Thread(new Runnable() {
@Override
public void run() {
try {
log.info("start load faceDb...");
loadFaceDb();
log.info("Load faceDb success!");
} catch (Exception e) {
e.printStackTrace();
//初始化人脸库
if(config.getVectorDBConfig() != null && config.getVectorDBConfig().getType() != null){
if(config.getVectorDBConfig() instanceof MilvusConfig){
MilvusConfig milvusConfig = ((MilvusConfig) config.getVectorDBConfig());
//设置向量维度
milvusConfig.setDimension(DIMENSION);
//相似度计算方式 为空,设置默认值
if(Objects.isNull(milvusConfig.getMetricType())){
//seetaface6 默认使用余弦相似度
milvusConfig.setMetricType(MetricType.COSINE);
}
}else if (config.getVectorDBConfig() instanceof SQLiteConfig){
SQLiteConfig sqliteConfig = (SQLiteConfig) config.getVectorDBConfig();
if(Objects.isNull(sqliteConfig.getSimilarityType())){
//seetaface6 默认使用余弦相似度
sqliteConfig.setSimilarityType(SimilarityType.COSINE);
}
}
}).start();
//创建向量数据库
vectorDBClient = VectorDBFactory.createClient(config.getVectorDBConfig());
// 加载人脸数据库
if(config.isAutoLoadFace()){
new Thread(new Runnable() {
@Override
public void run() {
try {
log.debug("start load face...");
vectorDBClient.initialize();
isLoadCompleted = true;
log.debug("Load face success!");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
} catch (FileNotFoundException e) {
throw new FaceException(e);
}
@@ -158,7 +208,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
faceLandmarker.mark(imageData, seetaRect, pointFS);
seetaPointFSList.add(pointFS);
}
return FaceUtils.convertToDetectionResponse(seetaResult, config, seetaPointFSList);
return FaceUtils.convertToDetectionResponse(seetaResult, seetaPointFSList);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
@@ -342,21 +392,6 @@ public class SeetaFace6Model extends AbstractFaceModel {
return featureComparison(image1, image2);
}
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
if(Objects.isNull(inputStream1) || Objects.isNull(inputStream2)){
throw new FaceException("图像输入流无效");
}
BufferedImage image1 = null;
BufferedImage image2 = null;
try {
image1 = ImageIO.read(inputStream1);
image2 = ImageIO.read(inputStream2);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return featureComparison(image1, image2);
}
@Override
@@ -391,8 +426,8 @@ public class SeetaFace6Model extends AbstractFaceModel {
faceLandmarker.mark(imageData1, seetaResult[0], pointFS1);
//裁剪人脸
SeetaImageData cropImageData1 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
faceRecognizer.CropFaceV2(imageData1, pointFS1, cropImageData1);
// SeetaImageData cropImageData1 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
// faceRecognizer.CropFaceV2(imageData1, pointFS1, cropImageData1);
//图片2检测人脸
SeetaRect[] seetaResult2 = faceDetector.Detect(imageData2);
@@ -404,10 +439,10 @@ public class SeetaFace6Model extends AbstractFaceModel {
faceLandmarker.mark(imageData2, seetaResult2[0], pointFS2);
//图片2裁剪人脸
SeetaImageData cropImageData2 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
faceRecognizer.CropFaceV2(imageData2, pointFS2, cropImageData2);
return faceDatabase.CompareByCroppedFace(cropImageData1, cropImageData2);
// SeetaImageData cropImageData2 = new SeetaImageData(faceRecognizer.GetCropFaceWidthV2(), faceRecognizer.GetCropFaceHeightV2(), faceRecognizer.GetCropFaceChannelsV2());
// faceRecognizer.CropFaceV2(imageData2, pointFS2, cropImageData2);
// return faceDatabase.CompareByCroppedFace(cropImageData1, cropImageData2);
return faceDatabase.Compare(imageData1, pointFS1, imageData2, pointFS2);
} catch (FaceException e) {
throw e;
} catch (Exception e) {
@@ -465,9 +500,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
@Override
public boolean register(String key, String imagePath) {
public R<String> register(FaceRegisterInfo faceRegisterInfo, String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
@@ -475,59 +510,27 @@ public class SeetaFace6Model extends AbstractFaceModel {
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return register(key, bufferedImage);
return register(faceRegisterInfo, bufferedImage);
}
@Override
public boolean register(String key, BufferedImage image) {
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
public R<String> register(FaceRegisterInfo faceRegisterInfo, BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
FaceDatabase faceDatabase = null;
try {
faceDatabase = faceDatabasePool.borrowObject();
SeetaImageData cropImageData = getMaxCropFace(imageData);
long index = faceDatabase.RegisterByCroppedFace(cropImageData);
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
//持久化到sqlite数据库
FaceData face = new FaceData();
face.setKey(key);
face.setIndex(index);
face.setImgData(cropImageData.data);
try {
new FaceDao(config.getFaceDbPath()).save(face);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("保存人脸库失败", e);
}
return true;
} catch (FaceException e) {
throw e;
} catch (Exception e) {
throw new FaceException(e);
}finally {
if (faceDatabase != null) {
try {
faceDatabasePool.returnObject(faceDatabase); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
}
}
//提取特征向量
R<float[]> featureResponse = extractTopFaceFeature(image);
if(!featureResponse.isSuccess()){
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
}
return register(faceRegisterInfo, featureResponse.getData());
}
@Override
public boolean register(String key, byte[] imageData) {
public R<String> register(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
BufferedImage bufferedImage = null;
try {
@@ -535,14 +538,11 @@ public class SeetaFace6Model extends AbstractFaceModel {
} catch (IOException e) {
throw new FaceException(e);
}
return register(key, bufferedImage);
return register(faceRegisterInfo, bufferedImage);
}
@Override
public boolean register(String key, InputStream inputStream) {
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
public R<String> register(FaceRegisterInfo faceRegisterInfo, InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
@@ -552,54 +552,70 @@ public class SeetaFace6Model extends AbstractFaceModel {
} catch (IOException e) {
throw new FaceException("无效的图片输入流", e);
}
return register(key, image);
return register(faceRegisterInfo, image);
}
/**
* 注册已裁剪后人脸
* @param key
* @param faceData
* @return
*/
private boolean registerCroppedFace(String key, FaceData faceData) {
FaceDatabase faceDatabase = null;
try {
faceDatabase = faceDatabasePool.borrowObject();
SeetaImageData cropImageData = new SeetaImageData(faceData.getWidth(), faceData.getHeight(), faceData.getChannel());
cropImageData.data = faceData.getImgData();
long index = faceDatabase.RegisterByCroppedFace(cropImageData);
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
int rows = 0;
try {
rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException(e);
}
return rows > 0;
} catch (FaceException e) {
throw e;
} catch (Exception e) {
throw new FaceException(e);
}finally {
if (faceDatabase != null) {
try {
faceDatabasePool.returnObject(faceDatabase); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
}
}
@Override
public R<String> register(FaceRegisterInfo faceRegisterInfo, float[] feature) {
if(Objects.isNull(feature)){
return R.fail(R.Status.Unknown.getCode(), "人脸注册失败:人脸特征为空");
}
FaceVector faceVector = new FaceVector();
if(Objects.nonNull(faceRegisterInfo)){
faceVector.setId(faceRegisterInfo.getId());
faceVector.setMetadata(faceRegisterInfo.getMetadata());
}
faceVector.setVector(feature);
return R.ok(vectorDBClient.insert(faceVector));
}
// /**
// * 注册已裁剪后人脸
// * @param key
// * @param faceData
// * @return
// */
// private boolean registerCroppedFace(String key, FaceData faceData) {
// FaceDatabase faceDatabase = null;
// try {
// faceDatabase = faceDatabasePool.borrowObject();
// SeetaImageData cropImageData = new SeetaImageData(faceData.getWidth(), faceData.getHeight(), faceData.getChannel());
// cropImageData.data = faceData.getImgData();
// long index = faceDatabase.RegisterByCroppedFace(cropImageData);
// if (index < 0) {
// log.debug("register face fail: key={}, index={}", key, index);
// return false;
// }
// int rows = 0;
// try {
// rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
// } catch (SQLException | ClassNotFoundException e) {
// throw new FaceException(e);
// }
// return rows > 0;
// } catch (FaceException e) {
// throw e;
// } catch (Exception e) {
// throw new FaceException(e);
// }finally {
// if (faceDatabase != null) {
// try {
// faceDatabasePool.returnObject(faceDatabase); //归还
// } catch (Exception e) {
// log.warn("归还Predictor失败", e);
// }
// }
// }
// }
@Override
public FaceResult search(String imagePath) {
public R<DetectionResponse> search(String imagePath, FaceSearchParams params) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
@@ -607,69 +623,38 @@ public class SeetaFace6Model extends AbstractFaceModel {
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return search(bufferedImage);
return search(bufferedImage, params);
}
@Override
public FaceResult search(InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
BufferedImage image = null;
try {
image = ImageIO.read(inputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return search(image);
}
@Override
public FaceResult search(BufferedImage image) {
long time1 = System.currentTimeMillis();
public R<DetectionResponse> search(BufferedImage image, FaceSearchParams params) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
FaceDatabase faceDatabase = null;
try {
faceDatabase = faceDatabasePool.borrowObject();
SeetaPointF[] points = getMaskPoint(imageData);
long[] index = new long[1];
float[] similarity = new float[1];
long result = faceDatabase.QueryTop(imageData, points, 1, index, similarity);
if(result < 1){
return null;
}
//检查相似度
if(similarity[0] < config.getSimilarityThreshold()){
return null;
}
FaceResult faceResult = searchFaceDb(index[0], similarity[0]);
return faceResult;
} catch (FaceException e) {
throw e;
} catch (Exception e) {
throw new FaceException(e);
}finally {
if (faceDatabase != null) {
try {
faceDatabasePool.returnObject(faceDatabase); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
}
//提取所有人脸特征
R<DetectionResponse> detectionResponse = extractFeatures(image);
if(!detectionResponse.isSuccess()){
return detectionResponse;
}
//设置默认值
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
for (DetectionInfo detectionInfo : detectionResponse.getData().getDetectionInfoList()){
if(Objects.nonNull(detectionInfo.getFaceInfo()) && Objects.nonNull(detectionInfo.getFaceInfo().getFeature())){
List<FaceSearchResult> searchResults = vectorDBClient.search(detectionInfo.getFaceInfo().getFeature(), searchParams);
detectionInfo.getFaceInfo().setFaceSearchResults(searchResults);
}
}
return detectionResponse;
}
@Override
public FaceResult search(byte[] imageData) {
public R<DetectionResponse> search(byte[] imageData, FaceSearchParams params) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
BufferedImage bufferedImage = null;
try {
@@ -677,75 +662,89 @@ public class SeetaFace6Model extends AbstractFaceModel {
} catch (IOException e) {
throw new FaceException(e);
}
return search(bufferedImage);
return search(bufferedImage, params);
}
@Override
public List<FaceSearchResult> search(float[] feature, FaceSearchParams params) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
if(Objects.isNull(feature)){
throw new FaceException("人脸特征为空");
}
if(Objects.isNull(params)){
throw new FaceException("人脸查询参数为空");
}
//设置默认值
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
List<FaceSearchResult> searchResults = vectorDBClient.search(feature, searchParams);
return searchResults;
}
@Override
public R<List<FaceSearchResult>> searchByTopFace(String imagePath, FaceSearchParams params) {
if(!FileUtils.isFileExists(imagePath)){
return R.fail(R.Status.FILE_NOT_FOUND);
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return searchByTopFace(bufferedImage, params);
}
@Override
public long removeRegister(String... keys) {
public R<List<FaceSearchResult>> searchByTopFace(BufferedImage sourceImage, FaceSearchParams params) {
if(!ImageUtils.isImageValid(sourceImage)){
return R.fail(R.Status.INVALID_IMAGE);
}
//提取分数最高人脸特征
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
if(!featureResponse.isSuccess()){
return R.fail(featureResponse.getCode(), featureResponse.getMessage());
}
//设置默认值
float threshold = Objects.isNull(params.getThreshold()) ? FaceDetectConstant.SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD : params.getThreshold();
int topK = Objects.isNull(params.getTopK()) ? 1 : params.getTopK();
boolean normalize = Objects.isNull(params.getNormalizeSimilarity()) ? NORMALIZE_SIMILARITY : params.getNormalizeSimilarity();
FaceSearchParams searchParams = new FaceSearchParams(topK, threshold, normalize);
List<FaceSearchResult> searchResults = vectorDBClient.search(featureResponse.getData(), searchParams);
return R.ok(searchResults);
}
@Override
public R<List<FaceSearchResult>> searchByTopFace(byte[] imageData, FaceSearchParams params) {
if(Objects.isNull(imageData)){
return R.fail(R.Status.INVALID_IMAGE);
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
} catch (IOException e) {
throw new FaceException(e);
}
return searchByTopFace(bufferedImage, params);
}
@Override
public void removeRegister(String... keys) {
if(keys == null || keys.length == 0){
throw new FaceException("keys不允许为空");
}
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
FaceDatabase faceDatabase = null;
try {
List<Long> list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
if (list == null) {
return 0;
}
faceDatabase = faceDatabasePool.borrowObject();
int rows = 0;
for (long index : list) {
int row = faceDatabase.Delete(index);
rows += row;
}
new FaceDao(config.getFaceDbPath()).deleteFace(keys);
return rows;
} catch (Exception e) {
throw new FaceException(e);
}finally {
if (faceDatabase != null) {
try {
faceDatabasePool.returnObject(faceDatabase); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
}
}
}
vectorDBClient.deleteBatch(Arrays.asList(keys));
}
@Override
public long clearFace(){
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
FaceDatabase faceDatabase = null;
try {
faceDatabase = faceDatabasePool.borrowObject();
faceDatabase.Clear();
long rows = 0;
try {
rows = new FaceDao(config.getFaceDbPath()).deleteAll();
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("删除人脸库失败", e);
}
return rows;
} catch (FaceException e) {
throw e;
} catch (Exception e) {
throw new FaceException(e);
}finally {
if (faceDatabase != null) {
try {
faceDatabasePool.returnObject(faceDatabase); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
}
}
}
public void clearFace(){
vectorDBClient.dropCollection(null);
}
/**
@@ -763,11 +762,11 @@ public class SeetaFace6Model extends AbstractFaceModel {
private FaceResult searchFaceDb(long index,float similar) {
if(index >= 0){
String key = null;
try {
key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(index);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("查询人脸库失败", e);
}
// try {
// key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(index);
// } catch (SQLException | ClassNotFoundException e) {
// throw new FaceException("查询人脸库失败", e);
// }
return new FaceResult(key, similar);
}
return null;
@@ -779,42 +778,42 @@ public class SeetaFace6Model extends AbstractFaceModel {
* @throws SQLException
* @throws ClassNotFoundException
*/
private void loadFaceDb() {
if(!checkFaceDb()){
log.info("未配置人脸库");
return;
}
//分页查询人脸库
int pageNo = 0, pageSize = 100;
while (true) {
List<FaceData> list = null;
try {
list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("查询人脸库失败", e);
}
if (list == null) {
break;
}
list.forEach(face -> {
try {
registerCroppedFace(face.getKey(), face);
} catch (Exception e) {
e.printStackTrace();
}
});
if (list.size() < pageSize) {
break;
}
pageNo++;
}
}
// private void loadFaceDb() {
// if(!checkFaceDb()){
// log.debug("未配置人脸库");
// return;
// }
// //分页查询人脸库
// int pageNo = 0, pageSize = 100;
// while (true) {
// List<FaceData> list = null;
// try {
// list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
// } catch (SQLException | ClassNotFoundException e) {
// throw new FaceException("查询人脸库失败", e);
// }
// if (list == null) {
// break;
// }
// list.forEach(face -> {
// try {
// registerCroppedFace(face.getKey(), face);
// } catch (Exception e) {
// e.printStackTrace();
// }
// });
// if (list.size() < pageSize) {
// break;
// }
// pageNo++;
// }
// }
@Override
public List<float[]> extractFeatures(String imagePath) {
public R<DetectionResponse> extractFeatures(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
return R.fail(R.Status.FILE_NOT_FOUND);
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
@@ -827,23 +826,24 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
@Override
public List<float[]> extractFeatures(byte[] imageData) {
public R<DetectionResponse> extractFeatures(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
throw new FaceException("特征提取异常", e);
}
}
@Override
public List<float[]> extractFeatures(BufferedImage image) {
public R<DetectionResponse> extractFeatures(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
List<float[]> featureList = new ArrayList<float[]>();
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
FaceDetector faceDetector = null;
@@ -856,7 +856,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
//检测人脸
SeetaRect[] seetaResult = faceDetector.Detect(imageData);
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
throw new FaceException("未检测到人脸");
return R.fail(R.Status.NO_FACE_DETECTED);
}
for(SeetaRect seetaRect : seetaResult){
//提取人脸的5点人脸标识
@@ -867,14 +867,16 @@ public class SeetaFace6Model extends AbstractFaceModel {
//CropFaceV2 + ExtractCroppedFace 已包含裁剪+人脸对齐
boolean isSuccess = faceRecognizer.Extract(imageData, pointFS, features);
if(!isSuccess){
throw new FaceException("人脸特征提取失败");
log.warn("人脸特征提取失败");
}
featureList.add(features);
seetaPointFSList.add(pointFS);
}
return R.ok(FaceUtils.featuresConvertToResponse(seetaResult, seetaPointFSList, featureList));
} catch (FaceException e) {
throw e;
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
throw new FaceException("人脸特征提取异常", e);
}finally {
if (faceDetector != null) {
try {
@@ -898,14 +900,13 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
}
}
return featureList;
}
@Override
public float[] extractTopFaceFeature(BufferedImage image) {
public R<float[]> extractTopFaceFeature(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
float[] features = null;
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
@@ -920,7 +921,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
//检测人脸
SeetaRect[] seetaResult = faceDetector.Detect(imageData);
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
throw new FaceException("未检测到人脸");
return R.fail(R.Status.NO_FACE_DETECTED);
}
//提取人脸的5点人脸标识
SeetaPointF[] pointFS = new SeetaPointF[faceLandmarker.number()];
@@ -930,8 +931,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
//CropFaceV2 + ExtractCroppedFace 已包含裁剪+人脸对齐
boolean isSuccess = faceRecognizer.Extract(imageData, pointFS, features);
if(!isSuccess){
throw new FaceException("人脸特征提取失败");
return R.fail(R.Status.Unknown.getCode(), "人脸特征提取失败");
}
return R.ok(features);
} catch (FaceException e) {
throw e;
} catch (Exception e) {
@@ -959,13 +961,12 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
}
}
return features;
}
@Override
public float[] extractTopFaceFeature(String imagePath) {
public R<float[]> extractTopFaceFeature(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
return R.fail(R.Status.FILE_NOT_FOUND);
}
// 将图片路径转换为 BufferedImage
BufferedImage image = null;
@@ -978,9 +979,9 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
@Override
public float[] extractTopFaceFeature(byte[] imageData) {
public R<float[]> extractTopFaceFeature(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
return R.fail(R.Status.INVALID_IMAGE);
}
try {
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
@@ -989,5 +990,100 @@ public class SeetaFace6Model extends AbstractFaceModel {
}
}
@Override
public void loadFaceFeatures() {
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
vectorDBClient.loadFaceFeatures();
}
@Override
public void releaseFaceFeatures() {
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
vectorDBClient.releaseFaceFeatures();
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
upsertFace(faceRegisterInfo, bufferedImage);
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, BufferedImage sourceImage) {
if(vectorDBClient == null){
throw new VectorDBException("向量数据库未初始化成功");
}
if(Objects.isNull(faceRegisterInfo)){
throw new FaceException("注册信息为空");
}
if(StringUtils.isBlank(faceRegisterInfo.getId())){
throw new FaceException("注册信息中ID为空");
}
//提取最大人脸特征
R<float[]> featureResponse = extractTopFaceFeature(sourceImage);
if(!featureResponse.isSuccess()){
throw new FaceException(featureResponse.getMessage());
}
upsertFace(faceRegisterInfo, featureResponse.getData());
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, float[] feature) {
if(Objects.isNull(feature)){
throw new FaceException("人脸特征为空");
}
if(Objects.isNull(vectorDBClient)){
throw new FaceException("未初始化人脸库");
}
FaceVector faceVector = new FaceVector();
if(faceRegisterInfo != null){
faceVector.setId(faceRegisterInfo.getId());
faceVector.setMetadata(faceRegisterInfo.getMetadata());
}
faceVector.setVector(feature);
vectorDBClient.upsert(faceVector);
}
@Override
public void upsertFace(FaceRegisterInfo faceRegisterInfo, byte[] imageData) {
FaceModel.super.upsertFace(faceRegisterInfo, imageData);
}
@Override
public void close() throws Exception {
if(Objects.nonNull(faceDetectorPool)){
faceDetectorPool.close();
}
if(Objects.nonNull(faceRecognizerPool)){
faceRecognizerPool.close();
}
if(Objects.nonNull(faceLandmarkerPool)){
faceLandmarkerPool.close();
}
if(Objects.nonNull(faceDatabasePool)){
faceDatabasePool.close();
}
if(Objects.nonNull(vectorDBClient)){
vectorDBClient.close();
}
}
@Override
public boolean isLoadFaceCompleted() {
return isLoadCompleted;
}
}

View File

@@ -36,7 +36,7 @@ import java.util.Objects;
* @author dwj
*/
@Slf4j
public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements AutoCloseable{
public class UltraLightFastGenericFaceModel implements FaceModel, AutoCloseable{
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;

View File

@@ -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("视频帧数低于检测帧数");
}

View File

@@ -64,21 +64,22 @@ public class NativeLoader {
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
//创建目录
FileUtil.mkdir(seetaface6NativePath);
log.info("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
log.debug("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
//拷贝依赖库到缓存目录
List<File> fileList = getLibFiles(osInfo, device);
if(fileList != null && !fileList.isEmpty()){
// 加载依赖库文件
fileList.forEach(file -> {
System.load(file.getAbsolutePath());
log.info(String.format("load %s finish", file.getAbsolutePath()));
//log.debug(String.format("load %s finish", file.getAbsolutePath()));
});
}
log.debug("seetaface6 依赖库加载完毕");
isDllLoaded = true;
}
}
} else {
log.info("SeetaFace DLL is already loaded.");
log.debug("SeetaFace DLL is already loaded.");
}
} catch (Exception e) {
@@ -94,7 +95,7 @@ public class NativeLoader {
private static List<File> getLibFiles(OsInfo osInfo,DeviceEnum deviceEnum){
try {
String device = getDevice(deviceEnum);
log.info("当前设备:{}", device);
log.debug("当前设备:{}", device);
//获取dll文件列表
List<DllItem> baseList = new ArrayList<>();
List<DllItem> jniList = new ArrayList<>();
@@ -206,11 +207,15 @@ public class NativeLoader {
Path path = Paths.get(resourcePath);
String fileName = path.getFileName().toString();
Path targetPath = seetaface6NativePath.resolve(fileName);
Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
log.info("copy target path success : {}", targetPath.toAbsolutePath().toString());
// 设置可执行权限
if (!SystemUtil.getOsInfo().getName().toLowerCase().contains("win")) {
targetPath.toFile().setExecutable(true);
if (Files.exists(targetPath)) {
//log.debug("target file already exists, skip copy: {}", targetPath.toAbsolutePath());
} else {
Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
log.debug("copy target path success: {}", targetPath.toAbsolutePath());
// 设置可执行权限
if (!SystemUtil.getOsInfo().getName().toLowerCase().contains("win")) {
targetPath.toFile().setExecutable(true);
}
}
return targetPath.toFile();
}

View File

@@ -1,15 +1,20 @@
package cn.smartjavaai.face.sqllite;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;
import javax.sql.DataSource;
import java.io.File;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* sqlite帮助类
@@ -18,34 +23,61 @@ import java.util.Map;
@Slf4j
public class SqliteHelper {
private Connection connection;
private Statement statement;
private ResultSet resultSet;
private String dbFilePath;
private static final ConcurrentHashMap<String, SqliteHelper> INSTANCES = new ConcurrentHashMap<>();
private static final int MAX_CONNECTIONS = 10; // 最大连接数
private final String dbFilePath;
private final DataSource dataSource;
/**
* 构造函数
* 获取SqliteHelper实例单例模式
* @param dbFilePath sqlite db 文件路径
* @throws ClassNotFoundException
* @throws SQLException
* @return SqliteHelper实例
* @throws SQLException SQL异常
* @throws ClassNotFoundException 类未找到异常
*/
public SqliteHelper(String dbFilePath) throws ClassNotFoundException, SQLException {
public static SqliteHelper getInstance(String dbFilePath) throws SQLException, ClassNotFoundException {
return INSTANCES.computeIfAbsent(dbFilePath, path -> {
try {
return new SqliteHelper(path);
} catch (Exception e) {
log.error("创建SqliteHelper实例失败", e);
throw new RuntimeException("创建SqliteHelper实例失败", e);
}
});
}
/**
* 私有构造函数
* @param dbFilePath sqlite db 文件路径
* @throws ClassNotFoundException 类未找到异常
* @throws SQLException SQL异常
*/
private SqliteHelper(String dbFilePath) throws ClassNotFoundException, SQLException {
this.dbFilePath = dbFilePath;
connection = getConnection(dbFilePath);
createDatabaseIfNotExists();
// 初始化连接池
SQLiteDataSource sqLiteDataSource = new SQLiteDataSource();
sqLiteDataSource.setUrl("jdbc:sqlite:" + dbFilePath);
// 配置SQLite连接
SQLiteConfig sqLiteConfig = new SQLiteConfig();
sqLiteConfig.setSharedCache(true);
sqLiteConfig.enableLoadExtension(true);
sqLiteConfig.setBusyTimeout(5000); // 5秒超时
sqLiteDataSource.setConfig(sqLiteConfig);
this.dataSource = sqLiteDataSource;
}
/**
* 获取数据库连接
* @param dbFilePath db文件路径
* @return 数据库连接
* @throws ClassNotFoundException
* @throws SQLException
* @throws SQLException SQL异常
*/
public Connection getConnection(String dbFilePath) throws ClassNotFoundException, SQLException {
Connection conn = null;
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection("jdbc:sqlite:" + dbFilePath);
return conn;
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
@@ -53,168 +85,117 @@ public class SqliteHelper {
* @param sql sql select 语句
* @param rse 结果集处理类对象
* @return 查询结果
* @throws SQLException
* @throws ClassNotFoundException
* @throws SQLException SQL异常
*/
public <T> T executeQuery(String sql, ResultSetExtractor<T> rse) throws SQLException, ClassNotFoundException {
try {
resultSet = getStatement().executeQuery(sql);
T rs = rse.extractData(resultSet);
return rs;
} finally {
destroyed();
public <T> T executeQuery(String sql, ResultSetExtractor<T> rse) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
return rse.extractData(rs);
}
}
/**
* 执行select查询返回结果列表
*
* @param sql sql select 语句
* @param rm 结果集的行数据处理类对象
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @return 查询结果列表
* @throws SQLException SQL异常
*/
public <T> List<T> executeQuery(String sql, RowMapper<T> rm) throws SQLException, ClassNotFoundException {
List<T> rsList = new ArrayList<T>();
try {
resultSet = getStatement().executeQuery(sql);
while (resultSet.next()) {
rsList.add(rm.mapRow(resultSet, resultSet.getRow()));
public <T> List<T> executeQuery(String sql, RowMapper<T> rm) throws SQLException {
List<T> rsList = new ArrayList<>();
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
rsList.add(rm.mapRow(rs, rs.getRow()));
}
} finally {
destroyed();
return rsList;
}
return rsList;
}
/**
* 简单查询某个字段
* @param sql
* @return
* @throws SQLException
* @throws ClassNotFoundException
* @param sql SQL查询语句
* @return 查询结果
* @throws SQLException SQL异常
*/
public String executeQuery(String sql) throws SQLException, ClassNotFoundException {
try {
resultSet = getStatement().executeQuery(sql);
if(resultSet.next()){
return resultSet.getString(1);
public String executeQuery(String sql) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
if (rs.next()) {
return rs.getString(1);
}
return null;
} finally {
destroyed();
}
}
/**
* 执行数据库更新sql语句
* @param sql
* @param sql SQL更新语句
* @return 更新行数
* @throws SQLException
* @throws ClassNotFoundException
* @throws SQLException SQL异常
*/
public int executeUpdate(String sql) throws SQLException, ClassNotFoundException {
try {
int c = getStatement().executeUpdate(sql);
return c;
} finally {
destroyed();
public int executeUpdate(String sql) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
return stmt.executeUpdate(sql);
}
}
/**
* 执行多个sql更新语句
* @param sqls
* @throws SQLException
* @throws ClassNotFoundException
* @param sqls SQL更新语句数组
* @throws SQLException SQL异常
*/
public void executeUpdate(String...sqls) throws SQLException, ClassNotFoundException {
try {
public void executeUpdate(String... sqls) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
for (String sql : sqls) {
getStatement().executeUpdate(sql);
stmt.executeUpdate(sql);
}
} finally {
destroyed();
}
}
/**
* 执行数据库更新 sql List
* @param sqls sql列表
* @throws SQLException
* @throws ClassNotFoundException
* @throws SQLException SQL异常
*/
public void executeUpdate(List<String> sqls) throws SQLException, ClassNotFoundException {
try {
public void executeUpdate(List<String> sqls) throws SQLException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
for (String sql : sqls) {
getStatement().executeUpdate(sql);
stmt.executeUpdate(sql);
}
} finally {
destroyed();
}
}
private Connection getConnection() throws ClassNotFoundException, SQLException {
if (null == connection) connection = getConnection(dbFilePath);
return connection;
}
private Statement getStatement() throws SQLException, ClassNotFoundException {
if (null == statement) statement = getConnection().createStatement();
return statement;
}
/**
* 数据库资源关闭和释放
*/
public void destroyed() {
try {
if (null != statement) {
statement.close();
statement = null;
}
if (null != connection) {
connection.close();
connection = null;
}
if (null != resultSet) {
resultSet.close();
resultSet = null;
}
} catch (SQLException e) {
log.error("Sqlite数据库关闭时异常", e);
}
}
/**
* 执行select查询返回结果列表
*
* @param sql sql select 语句
* @param clazz 实体泛型
* @return 实体集合
* @throws SQLException 异常信息
* @throws ClassNotFoundException 异常信息
* @throws IllegalAccessException 非法访问异常
* @throws InstantiationException 实例化异常
*/
public <T> List<T> executeQueryList(String sql, Class<T> clazz) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
List<T> rsList = new ArrayList<T>();
try {
resultSet = getStatement().executeQuery(sql);
while (resultSet.next()) {
public <T> List<T> executeQueryList(String sql, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
List<T> rsList = new ArrayList<>();
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
T t = clazz.newInstance();
for (Field field : t.getClass().getDeclaredFields()) {
field.setAccessible(true);
field.set(t,resultSet.getObject(field.getName()));
field.set(t, rs.getObject(field.getName()));
}
rsList.add(t);
}
} finally {
destroyed();
return rsList;
}
return rsList;
}
/**
@@ -222,20 +203,23 @@ public class SqliteHelper {
* @param sql sql select 语句
* @param clazz 结果集处理类对象
* @return 查询结果
* @throws SQLException
* @throws ClassNotFoundException
* @throws SQLException SQL异常
* @throws IllegalAccessException 非法访问异常
* @throws InstantiationException 实例化异常
*/
public <T> T executeQuery(String sql, Class<T> clazz) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
try {
resultSet = getStatement().executeQuery(sql);
T t = clazz.newInstance();
for (Field field : t.getClass().getDeclaredFields()) {
field.setAccessible(true);
field.set(t,resultSet.getObject(field.getName()));
public <T> T executeQuery(String sql, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
if (rs.next()) {
T t = clazz.newInstance();
for (Field field : t.getClass().getDeclaredFields()) {
field.setAccessible(true);
field.set(t, rs.getObject(field.getName()));
}
return t;
}
return t;
} finally {
destroyed();
return null;
}
}
@@ -244,53 +228,149 @@ public class SqliteHelper {
* @param tableName 表名
* @param param key-value键值对,key:表中字段名,value:值
* @return 更新行数
* @throws SQLException
* @throws ClassNotFoundException
* @throws SQLException SQL异常
*/
public int executeInsertOrUpdate(String tableName, Map<String,Object> param) throws SQLException, ClassNotFoundException {
try {
StringBuffer sql = new StringBuffer();
public int executeInsertOrUpdate(String tableName, Map<String, Object> param) throws SQLException {
try (Connection conn = getConnection()) {
// 保证字段和值顺序一致
List<String> keys = new ArrayList<>(param.keySet());
StringBuilder sql = new StringBuilder();
sql.append("INSERT OR REPLACE INTO ");
sql.append(tableName);
sql.append(" ( ");
for (String key : param.keySet()) {
sql.append("'" + key + "'");
sql.append(",");
sql.append(" (");
for (String key : keys) {
sql.append(key).append(",");
}
sql.delete(sql.length()-1,sql.length());
sql.append(") VALUES ( ");
for (String key : param.keySet()) {
sql.append("'");
sql.append(param.get(key));
sql.append("',");
sql.deleteCharAt(sql.length() - 1);
sql.append(") VALUES (");
for (int i = 0; i < keys.size(); i++) {
sql.append("?,");
}
sql.delete(sql.length()-1,sql.length());
sql.deleteCharAt(sql.length() - 1);
sql.append(");");
log.info("sql{}", sql.toString());
int c = getStatement().executeUpdate(sql.toString());
return c;
} finally {
destroyed();
log.debug("sql: {}", sql.toString());
try (PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
for (int i = 0; i < keys.size(); i++) {
Object value = param.get(keys.get(i));
if (value instanceof byte[]) {
pstmt.setBytes(i + 1, (byte[]) value);
} else {
pstmt.setObject(i + 1, value);
}
}
return pstmt.executeUpdate();
}
}
}
public int executeUpdate(String sql, Object[] args) throws SQLException, ClassNotFoundException {
Connection conn = getConnection();
if (args == null || args.length == 0) {
try (Statement stmt = conn.createStatement()) {
return stmt.executeUpdate(sql);
} finally {
destroyed();
}
} else {
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (int i = 0; i < args.length; i++) {
stmt.setObject(i + 1, args[i]);
/**
* 使用预编译语句执行更新
* @param sql SQL语句
* @param args 参数
* @return 更新行数
* @throws SQLException SQL异常
*/
public int executeUpdate(String sql, Object[] args) throws SQLException {
try (Connection conn = getConnection()) {
if (args == null || args.length == 0) {
try (Statement stmt = conn.createStatement()) {
return stmt.executeUpdate(sql);
}
} else {
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (int i = 0; i < args.length; i++) {
stmt.setObject(i + 1, args[i]);
}
return stmt.executeUpdate();
}
return stmt.executeUpdate();
} finally {
destroyed();
}
}
}
/**
* 创建数据库文件(如果不存在)
*/
private void createDatabaseIfNotExists() {
File dbFile = new File(dbFilePath);
if (!dbFile.exists()) {
try {
// 创建数据库文件
dbFile.getParentFile().mkdirs(); // 创建父目录
dbFile.createNewFile();
log.debug("Created new SQLite database file: {}", dbFilePath);
} catch (Exception e) {
log.error("Failed to create database file: {}", dbFilePath, e);
throw new RuntimeException("Database file creation failed", e);
}
}
}
/**
* 初始化数据库表结构
*/
public void initializeDatabase(String tableName, String schemaResourcePath) throws SQLException {
// 检查表是否存在
if (isTableExists(tableName)) {
log.debug("Database table already exists");
return;
}
log.debug("Creating database tables...");
// 使用 Hutool 读取 SQL 资源文件
List<String> sqlStatements = readSqlResource(schemaResourcePath);
// 执行所有 SQL 语句
for (String sql : sqlStatements) {
if (!sql.trim().isEmpty()) {
executeUpdate(sql);
}
}
log.debug("Database tables created successfully");
}
/**
* 检查表是否存在
*/
private boolean isTableExists(String tableName) throws SQLException {
try (Connection conn = getConnection()) {
ResultSet rs = conn.getMetaData().getTables(null, null, tableName, null);
return rs.next();
} catch (SQLException e) {
log.warn("Error checking table existence: {}", e.getMessage());
return false;
}
}
/**
* 使用 Hutool 读取 SQL 资源文件并分割为语句列表
*/
private List<String> readSqlResource(String resourcePath) {
try {
// 读取整个资源文件内容
String content = ResourceUtil.readUtf8Str(resourcePath);
// 分割 SQL 语句(按分号分割)
return Arrays.stream(content.split(";"))
.map(String::trim)
.filter(sql -> !sql.isEmpty())
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Failed to read SQL resource: {}", resourcePath, e);
throw new RuntimeException("SQL resource read error", e);
}
}
/**
* 关闭所有连接池
*/
public static void closeAll() {
INSTANCES.clear();
log.debug("所有SqliteHelper实例已关闭");
}
}

View File

@@ -43,7 +43,9 @@ public final class FaceFeatureTranslator implements Translator<Image, float[]> {
*/
@Override
public float[] processOutput(TranslatorContext ctx, NDList list) {
return list.singletonOrThrow().toFloatArray();
NDArray embedding = list.singletonOrThrow();
embedding = embedding.div(embedding.norm()); // L2归一化
return embedding.toFloatArray();
}
@Override

View File

@@ -79,7 +79,7 @@ public class FaceUtils {
* @param seetaResult
* @return
*/
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, FaceModelConfig config,List<SeetaPointF[]> seetaPointFSList){
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList){
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
return null;
}
@@ -104,6 +104,57 @@ public class FaceUtils {
return detectionResponse;
}
/**
* 转换为FaceDetectedResult(人脸特征提取)
* @param seetaResult
* @return
*/
public static DetectionResponse featuresConvertToResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<float[]> featureList){
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
return null;
}
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
for(int i = 0; i < seetaResult.length; i++){
SeetaRect rect = seetaResult[i];
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
FaceInfo faceInfo = new FaceInfo();
if(seetaPointFSList != null && seetaPointFSList.size() > 0){
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
List<Point> keyPoints = Arrays.stream(seetaPointFS)
.map(p -> new Point(p.x, p.y))
.collect(Collectors.toList());
faceInfo.setKeyPoints(keyPoints);
}
if(featureList != null && featureList.size() > 0){
faceInfo.setFeature(featureList.get(i));
}
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
}
return new DetectionResponse(detectionInfoList);
}
/**
* 转换为FaceDetectedResult(人脸特征提取)
* @param rect
* @param seetaPointFS
* @param feature
* @return
*/
public static DetectionResponse featuresConvertToResponse(SeetaRect rect, SeetaPointF[] seetaPointFS, float[] feature){
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
FaceInfo faceInfo = new FaceInfo();
List<Point> keyPoints = Arrays.stream(seetaPointFS)
.map(p -> new Point(p.x, p.y))
.collect(Collectors.toList());
faceInfo.setKeyPoints(keyPoints);
faceInfo.setFeature(feature);
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
return new DetectionResponse(detectionInfoList);
}
/**
* 绘制人脸框
* @param sourceImage
@@ -572,6 +623,28 @@ public class FaceUtils {
}
}
/**
* 将 Milvus 查询返回的得分转换为 0~1 范围的相似度
* @param metricType 向量度量方式IP 或 L2
* @param score 原始得分L2 为距离IP 为相似度)
* @return 映射后的相似度0~1
*/
public static float convertScoreToSimilarity(String metricType, float score) {
switch (metricType.toUpperCase()) {
case "IP":
// 内积 IP 的范围为 [-1, 1],归一化为 [0, 1]
return (score + 1.0f) / 2.0f;
case "L2":
// 欧氏距离 L2距离越小越相似1 / (1 + 距离) 映射到 (0, 1]
return 1.0f / (1.0f + score);
case "COSINE":
// 余弦相似度 COSINE本身范围为 [-1, 1],也需要归一化到 [0, 1]
return (score + 1.0f) / 2.0f;
default:
throw new IllegalArgumentException("Unsupported metricType: " + metricType);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,594 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.enums.IdStrategy;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.vector.config.MilvusConfig;
import cn.smartjavaai.face.vector.constant.VectorDBConstants;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.*;
import io.milvus.param.*;
import io.milvus.param.collection.*;
import io.milvus.param.dml.*;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.response.QueryResultsWrapper;
import io.milvus.response.SearchResultsWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* Milvus向量数据库客户端实现
* @author dwj
*/
@Slf4j
public class MilvusClient implements VectorDBClient {
private final MilvusConfig config;
private MilvusServiceClient serviceClient;
private String collectionName;
/**
* 是否初始化完毕
*/
private boolean isInit;
public MilvusClient(MilvusConfig config) {
this.config = config;
}
@Override
public void initialize() {
try {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(config.getHost())
.withPort(config.getPort())
.build();
serviceClient = new MilvusServiceClient(connectParam);
collectionName = StringUtils.isNotBlank(config.getCollectionName()) ? config.getCollectionName() : VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME;
createCollection(collectionName, config.getDimension());
if(config.isUseMemoryCache()){
// 加载集合到内存
loadFaceFeatures();
}
isInit = true;
} catch (Exception e) {
throw new VectorDBException("初始化Milvus客户端失败", e);
}
}
@Override
public void createCollection(String collectionName, int dimension) {
try {
if (hasCollection(collectionName)) {
log.debug("集合已存在:{}", collectionName);
return;
}
// 创建集合字段
FieldType idField = null;
switch (config.getIdStrategy()){
case AUTO://自动生成ID
idField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.ID_FIELD)
.withDataType(DataType.Int64)
.withPrimaryKey(true)
.withAutoID(true)
.build();
break;
case CUSTOM://自定义ID
idField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.ID_FIELD)
.withDataType(DataType.VarChar)
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_ID_MAX_LENGTH)
.withPrimaryKey(true)
.withAutoID(false)
.build();
break;
}
FieldType vectorField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withDataType(DataType.FloatVector)
.withDimension(dimension)
.build();
FieldType metadataField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.METADATA_FIELD)
.withDataType(DataType.VarChar)
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_METADATA_MAX_LENGTH)
.withNullable(true)//允许为空值
.build();
// 创建集合参数
CreateCollectionParam createCollectionParam = CreateCollectionParam.newBuilder()
.withCollectionName(collectionName)
.withDescription("人脸特征向量集合")
.addFieldType(idField)
.addFieldType(vectorField)
.addFieldType(metadataField)
.build();
R<RpcStatus> response = serviceClient.createCollection(createCollectionParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 创建集合失败:" + response.getMessage());
}
log.debug("创建集合成功");
// 创建索引
IndexType indexType = IndexType.IVF_FLAT;
if (config.getIndexType() != null) {
indexType = config.getIndexType();
}
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withIndexType(indexType)
.withMetricType(config.getMetricType())
.withExtraParam(String.format("{\"nlist\":%d}", config.getNlist()))
.withSyncMode(Boolean.TRUE)//调用方法后等待 Milvus 执行完成
.build();
R<RpcStatus> createIndexResponse = serviceClient.createIndex(indexParam);
if (createIndexResponse.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 创建索引失败:" + createIndexResponse.getMessage());
}
log.debug("创建索引成功");
} catch (Exception e) {
throw new VectorDBException("创建Milvus集合失败", e);
}
}
@Override
public void dropCollection(String collectionName) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
if (hasCollection(collectionName)) {
R<RpcStatus> response = serviceClient.dropCollection(DropCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 删除集合失败:" + response.getMessage());
}
isInit = false;
}
} catch (Exception e) {
throw new VectorDBException("删除Milvus集合失败", e);
}
}
@Override
public boolean hasCollection(String collectionName) {
try {
R<Boolean> response = serviceClient.hasCollection(
HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()
);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败:" + response.getMessage());
}
return Boolean.TRUE.equals(response.getData());
} catch (Exception e) {
throw new VectorDBException("检查Milvus集合是否保存失败", e);
}
}
@Override
public String insert(FaceVector faceVector) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
//验证
if(faceVector == null){
throw new VectorDBException("插入数据失败faceVector不能为空");
}
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
throw new VectorDBException("插入数据失败vector不能为空");
}
//自定义ID
if(config.getIdStrategy() == IdStrategy.CUSTOM){
if(StringUtils.isBlank(faceVector.getId())){
throw new VectorDBException("插入数据失败ID生成策略-自定义IDid不能为空");
}
}
// 转 float[] 为 List<Float>
List<Float> vectorList = new ArrayList<>();
for (float v : faceVector.getVector()) {
vectorList.add(v);
}
List<List<Float>> vectors = Collections.singletonList(vectorList);
//List<String> metadataList = Collections.singletonList(faceVector.getMetadata());
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
.map(Collections::singletonList)
.orElse(Collections.emptyList());
List<InsertParam.Field> fields = null;
switch (config.getIdStrategy()){
case AUTO://自动生成ID
fields = Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
);
break;
case CUSTOM://自定义ID
List<String> ids = Collections.singletonList(faceVector.getId());
fields = Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
);
break;
}
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build();
R<MutationResult> response = serviceClient.insert(insertParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("插入失败: " + response.getMessage());
}
List<Long> autoIds = response.getData().getIDs().getIntId().getDataList();
return config.getIdStrategy() == IdStrategy.AUTO ? String.valueOf(autoIds.get(0)) : faceVector.getId();
} catch (Exception e) {
throw new VectorDBException("插入Milvus向量失败", e);
}
}
@Override
public void upsert(FaceVector faceVector) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
if(config.getIdStrategy() == IdStrategy.AUTO){
throw new VectorDBException("idStrategy为AUTO时,不支持更新操作");
}
//验证
if(faceVector == null){
throw new VectorDBException("更新数据失败faceVector不能为空");
}
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
throw new VectorDBException("更新数据失败vector不能为空");
}
if(StringUtils.isBlank(faceVector.getId())){
throw new VectorDBException("更新数据失败id不能为空");
}
// 转换向量为 List<Float>
List<Float> vectorList = new ArrayList<>();
for (float v : faceVector.getVector()) {
vectorList.add(v);
}
List<List<Float>> vectors = Collections.singletonList(vectorList);
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
.map(Collections::singletonList)
.orElse(Collections.emptyList());
// 准备字段列表
List<UpsertParam.Field> fields = new ArrayList<>();
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.ID_FIELD)
.values(Collections.singletonList(faceVector.getId()))
.build());
// 添加向量和元数据字段
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.VECTOR_FIELD)
.values(vectors)
.build());
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.METADATA_FIELD)
.values(metadataList)
.build());
// 构建Upsert参数
UpsertParam upsertParam = UpsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build();
// 执行Upsert操作
R<MutationResult> response = serviceClient.upsert(upsertParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Upsert操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("Milvus Upsert操作失败", e);
}
}
@Override
public List<String> insertBatch(List<FaceVector> faceVectors) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
List<String> ids = faceVectors.stream()
.map(FaceVector::getId)
.collect(Collectors.toList());
List<List<Float>> vectors = new ArrayList<>();
for (FaceVector fv : faceVectors) {
List<Float> list = new ArrayList<>();
for (float f : fv.getVector()) {
list.add(f);
}
vectors.add(list);
}
List<String> metadataList = faceVectors.stream()
.map(FaceVector::getMetadata)
.collect(Collectors.toList());
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
))
.build();
R<MutationResult> insertResult = serviceClient.insert(insertParam);
return ids;
} catch (Exception e) {
throw new VectorDBException("批量插入Milvus向量失败", e);
}
}
@Override
public void delete(String id) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
String expr = String.format("%s == \"%s\"", VectorDBConstants.FieldNames.ID_FIELD, id);
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr)
.build();
R<MutationResult> response = serviceClient.delete(deleteParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("删除操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("删除Milvus向量失败", e);
}
}
@Override
public void deleteBatch(List<String> ids) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 构建IN表达式: id in ["id1", "id2", ...]
StringBuilder expr = new StringBuilder(VectorDBConstants.FieldNames.ID_FIELD + " in [");
for (int i = 0; i < ids.size(); i++) {
expr.append(ids.get(i)); // 不加引号
if (i < ids.size() - 1) {
expr.append(", ");
}
}
expr.append("]");
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr.toString())
.build();
R<MutationResult> response = serviceClient.delete(deleteParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("删除操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("批量删除Milvus向量失败", e);
}
}
@Override
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 1. 包装查询向量
List<List<Float>> vectors = new ArrayList<>();
List<Float> floatList = new ArrayList<>();
for (float f : queryVector) {
floatList.add(f);
}
vectors.add(floatList);
// 2. 构造搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withVectorFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withTopK(faceSearchParams.getTopK())
.withMetricType(config.getMetricType())
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.withVectors(vectors)
.withParams("{\"nprobe\": 10}")//和nlist有关
.build();
R<SearchResults> resp = serviceClient.search(searchParam);
if (resp.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败: " + resp.getMessage());
}
SearchResults results = resp.getData();
SearchResultsWrapper wrapper = new SearchResultsWrapper(results.getResults());
// 3. 获取字段数据FieldData
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0); // 默认只有一条 query 向量
List<FaceSearchResult> finalResults = new ArrayList<>();
for (int i = 0; i < scores.size(); i++) {
SearchResultsWrapper.IDScore score = scores.get(i);
float similarity = score.getScore();
if (faceSearchParams.getNormalizeSimilarity()) {
// 将分数转换为相似度
similarity = FaceUtils.convertScoreToSimilarity(config.getMetricType().name(), score.getScore());
}
if (similarity >= faceSearchParams.getThreshold()) {
// 获取 Metadata
String metadata = wrapper.getFieldData(VectorDBConstants.FieldNames.METADATA_FIELD, i).get(0).toString();
// 获取 ID
String id = wrapper.getFieldData(VectorDBConstants.FieldNames.ID_FIELD, i).get(0).toString();
finalResults.add(new FaceSearchResult(id, similarity, metadata));
}
}
return finalResults;
} catch (Exception e) {
throw new VectorDBException("搜索 Milvus 向量失败", e);
}
}
@Override
public long count(String collectionName) {
try {
if (serviceClient == null){
throw new VectorDBException("Milvus未初始化完毕");
}
R<QueryResults> response = serviceClient.query(
QueryParam.newBuilder()
.withCollectionName(collectionName)
.withOutFields(Collections.singletonList("count(*)"))
.build()
);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败msg: " + response.getMessage());
}
List<FieldData> fields = response.getData().getFieldsDataList();
if (fields.isEmpty()) {
throw new VectorDBException("Milvus 返回空字段数据");
}
FieldData countField = fields.get(0);
List<Long> countValues = countField.getScalars().getLongData().getDataList();
if (countValues.isEmpty()) {
throw new VectorDBException("Milvus count(*) 返回为空");
}
return countValues.get(0); // count(*) 查询的结果
} catch (Exception e) {
throw new VectorDBException("获取 Milvus 集合数量失败", e);
}
}
@Override
public void close() {
if (serviceClient != null) {
serviceClient.close();
isInit = false;
}
}
public void releaseCollection(String collectionName) {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
R<RpcStatus> response = serviceClient.releaseCollection(ReleaseCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus releaseCollection失败msg: " + response.getMessage());
}
}
@Override
public FaceSearchResult getById(String id) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 构造搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.build();
// 5. 执行查询
R<QueryResults> response = serviceClient.query(
QueryParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.build()
);
// 处理响应
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("查询失败: " + response.getMessage());
}
QueryResultsWrapper wrapper = new QueryResultsWrapper(response.getData());
List<QueryResultsWrapper.RowRecord> records = wrapper.getRowRecords();
if (records.isEmpty()) {
return null;
}
// 提取第一条记录
QueryResultsWrapper.RowRecord row = records.get(0);
return new FaceSearchResult(id, 1,(String)row.get(VectorDBConstants.FieldNames.METADATA_FIELD));
} catch (Exception e) {
throw new VectorDBException("搜索 Milvus 向量失败", e);
}
}
@Override
public void loadFaceFeatures() {
// 加载集合到内存
R<RpcStatus> loadResponse = serviceClient.loadCollection(LoadCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (loadResponse.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 加载集合到内存失败:" + loadResponse.getMessage());
}
long count = count(collectionName);
log.debug("加载集合到内存成功,人脸数量:{}", count);
}
@Override
public void releaseFaceFeatures() {
releaseCollection(collectionName);
}
}

View File

@@ -0,0 +1,266 @@
package cn.smartjavaai.face.vector.core;
import cn.hutool.core.util.IdUtil;
import cn.smartjavaai.common.config.Config;
import cn.smartjavaai.face.dao.FaceDao;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.utils.SimilarityUtil;
import cn.smartjavaai.face.vector.config.SQLiteConfig;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@Slf4j
public class SQLiteClient implements VectorDBClient {
private final FaceDao faceDao;
private final List<FaceVector> memoryIndex = new CopyOnWriteArrayList<>();
private int featureDimension; // 维度
private final ExecutorService executor = Executors.newFixedThreadPool(4);
private SQLiteConfig config;
/**
* 是否初始化完毕
*/
private boolean isInit;
public SQLiteClient(SQLiteConfig config) {
this.config = config;
String dbPath = config.getDbPath();
//如果未指定db路径则使用默认路径
if(StringUtils.isBlank(config.getDbPath())){
dbPath = Config.getCachePath() + File.separator + "face.db";
log.debug("使用默认SQLite人脸库路径: {}", dbPath);
}
this.faceDao = FaceDao.getInstance(dbPath);
}
@Override
public void initialize() {
try {
// 加载所有特征到内存
loadAllFeaturesToMemory();
isInit = true;
log.debug("SQLiteVectorDB initialized with {} faces", memoryIndex.size());
} catch (Exception e) {
throw new VectorDBException("初始化失败", e);
}
}
// 以下方法保持接口兼容但忽略collectionName参数
@Override
public void createCollection(String collectionName, int dimension) {
this.featureDimension = dimension;
log.debug("特征维度设置为: {}", dimension);
}
@Override
public void dropCollection(String collectionName) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
clearAllData();
log.warn("所有数据已被清空");
}
@Override
public boolean hasCollection(String collectionName) {
throw new UnsupportedOperationException("Sqlite 不支持此操作");
}
@Override
public String insert(FaceVector faceVector) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
return insertBatch(Collections.singletonList(faceVector)).get(0);
}
@Override
public void upsert(FaceVector faceVector) {
// if (faceVector.getId() != null) {
// delete(faceVector.getId());
// }
insert(faceVector);
}
@Override
public List<String> insertBatch(List<FaceVector> faceVectors) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
List<String> ids = new ArrayList<>();
try {
for (FaceVector faceVector : faceVectors) {
String id = faceVector.getId() != null ?
faceVector.getId() : IdUtil.simpleUUID();
faceVector.setId(id);
// 保存到数据库
faceDao.insertOrUpdate(faceVector);
// 添加到内存索引
addToMemoryIndex(faceVector);
ids.add(id);
}
log.debug("插入了 {} 个人脸向量", faceVectors.size());
return ids;
} catch (Exception e) {
throw new VectorDBException("批量插入失败", e);
}
}
@Override
public void delete(String id) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
deleteBatch(Collections.singletonList(id));
}
@Override
public void deleteBatch(List<String> ids) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
try {
// 从数据库中删除
boolean isSuccess = faceDao.deleteFace(ids.toArray(new String[0]));
// 从内存中删除
memoryIndex.removeIf(v -> ids.contains(v.getId()));
if(!isSuccess){
throw new VectorDBException("删除失败");
}
} catch (Exception e) {
throw new VectorDBException("批量删除失败", e);
}
}
@Override
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
if (memoryIndex.isEmpty()) {
return Collections.emptyList();
}
// 并行计算相似度
List<CompletableFuture<FaceSearchResult>> futures = memoryIndex.stream()
.map(vector -> CompletableFuture.supplyAsync(() -> {
float similarity = SimilarityUtil.calculate(queryVector, vector.getVector(), config.getSimilarityType(), faceSearchParams.getNormalizeSimilarity());
return similarity >= faceSearchParams.getThreshold() ?
new FaceSearchResult(vector.getId(), similarity, vector.getMetadata()) :
null;
}, executor))
.collect(Collectors.toList());
// 收集结果并过滤null
List<FaceSearchResult> allResults = futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 获取TopK结果
return allResults.stream()
.sorted(Comparator.comparingDouble(FaceSearchResult::getSimilarity).reversed())
.limit(faceSearchParams.getTopK())
.collect(Collectors.toList());
}
@Override
public long count(String collectionName) {
return memoryIndex.size();
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// ============= 私有辅助方法 =============
private void loadAllFeaturesToMemory() {
try {
int pageSize = 1000;
int page = 0;
while (true) {
List<FaceVector> batch = faceDao.findFace(page, pageSize);
if (CollectionUtils.isEmpty(batch)) {
break;
}
for (FaceVector vector : batch) {
addToMemoryIndex(vector);
}
page++;
}
log.debug("从数据库加载了 {} 个特征向量到内存", memoryIndex.size());
} catch (Exception e) {
throw new VectorDBException("加载特征到内存失败", e);
}
}
private void addToMemoryIndex(FaceVector faceVector) {
memoryIndex.add(faceVector);
}
private void clearAllData() {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
try {
faceDao.deleteAll();
memoryIndex.clear();
} catch (Exception e) {
log.error("清空数据库失败", e);
}
}
@Override
public FaceSearchResult getById(String id) {
try {
FaceVector faceVector = faceDao.findById(id);
if(faceVector != null){
return new FaceSearchResult(faceVector.getId(), 1.0f, faceVector.getMetadata());
}
return null;
} catch (SQLException | RuntimeException | ClassNotFoundException e ) {
throw new VectorDBException("SQLite查询异常", e);
}
}
@Override
public void loadFaceFeatures() {
// 加载所有特征到内存
loadAllFeaturesToMemory();
isInit = true;
log.debug("SQLiteVectorDB load success {} faces", memoryIndex.size());
}
@Override
public void releaseFaceFeatures() {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
memoryIndex.clear();
isInit = false;
}
}

View File

@@ -0,0 +1,114 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import java.util.List;
/**
* 向量数据库客户端接口
* 定义与向量数据库交互的通用操作
* @author dwj
*/
public interface VectorDBClient extends AutoCloseable {
/**
* 初始化连接和集合
* @throws VectorDBException 初始化异常
*/
void initialize();
/**
* 创建集合
* @param collectionName 集合名称
* @param dimension 向量维度
*/
void createCollection(String collectionName, int dimension);
/**
* 删除集合
* @param collectionName 集合名称
*/
void dropCollection(String collectionName);
/**
* 检查集合是否存在
* @param collectionName 集合名称
* @return 是否存在
*/
boolean hasCollection(String collectionName);
/**
* 插入人脸向量
* @param faceVector
* @return
*/
String insert(FaceVector faceVector);
/**
* 更新或新增人脸向量
* @param faceVector
*/
void upsert(FaceVector faceVector);
/**
* 批量插入人脸向量
* @param faceVectors
* @return
*/
List<String> insertBatch(List<FaceVector> faceVectors);
/**
* 根据ID删除向量
* @param id
*/
void delete(String id);
/**
* 批量删除向量
* @param ids
*/
void deleteBatch(List<String> ids);
/**
* 搜索相似人脸
* @param queryVector
* @param faceSearchParams
* @return
*/
List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams);
/**
* 获取集合中的向量数量
* @param collectionName 集合名称
* @return 向量数量
*/
long count(String collectionName);
/**
* 关闭连接
*/
@Override
void close();
/**
* 获取指定ID的向量
* @return
*/
FaceSearchResult getById(String id);
/**
* 加载人脸特征到内存
*/
void loadFaceFeatures();
/**
* 释放人脸特征缓存
*/
void releaseFaceFeatures();
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
PRAGMA foreign_keys = false;
-- ----------------------------
-- Table structure for face
-- ----------------------------
CREATE TABLE "face" (
"id" TEXT NOT NULL,
"vector" blob NOT NULL,
"metadata" TEXT,
PRIMARY KEY ("id"),
CONSTRAINT "id" UNIQUE ("id" ASC)
);
PRAGMA foreign_keys = true;

View File

@@ -6,11 +6,11 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<artifactId>smartjavaai-objectdetection</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
<name>smartjavaai-objectdetection</name>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>

View File

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

View File

@@ -22,7 +22,7 @@ public class ObjectDetectionModelFactory {
private static final ConcurrentHashMap<String, DetectorModel> modelMap = new ConcurrentHashMap<>();
static{
log.info("缓存目录:{}", Config.getCachePath());
log.debug("缓存目录:{}", Config.getCachePath());
}
// 私有构造函数,防止外部创建实例

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.15</version>
<version>1.0.16</version>
</parent>
<artifactId>smartjavaai-ocr</artifactId>
@@ -20,7 +20,7 @@
</dependency>
</dependencies>
<version>1.0.15</version>
<version>1.0.16</version>
<name>smartjavaai-ocr</name>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ public class OcrUtils {
List<OcrBox> boxList = new ArrayList<OcrBox>();
for(NDArray box : dt_boxes){
float[] pointsArr = box.toFloatArray();
//log.info("points: {}", pointsArr);
//log.debug("points: {}", pointsArr);
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);