1、OCR:新增表格识别模型

2、OCR:新增9个通用模型
3、OCR:支持批量检测识别
4、OCR:新增更多参数,使用更加灵活
5、人脸识别:支持ID查询及分页获取人脸信息
6、活体检测:视频检测支持设置最大帧数
This commit is contained in:
dengwenjie
2025-07-18 12:28:06 +08:00
parent 5d1f074de5
commit 6fbca62e5b
77 changed files with 3544 additions and 842 deletions

View File

@@ -23,31 +23,11 @@ public class FaceRecConfig {
*/
private FaceRecModelEnum modelEnum;
/**
* 置信度阈值
*/
private double confidenceThreshold = FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD;
/**
* 相似度阈值 作用:判断是否为同一人脸
*/
//private double similarityThreshold = 0D;
/**
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
*/
private double nmsThresh = FaceDetectConstant.NMS_THRESHOLD;
/**
* 模型路径
*/
private String modelPath;
/**
* 人脸库路径
*/
private String faceDbPath;
/**
* 设备类型
*/

View File

@@ -48,6 +48,11 @@ public class LivenessConfig {
*/
private int frameCount = LivenessConstant.DEFAULT_FRAME_COUNT;
/**
* 视频检测最大帧数
*/
private int maxVideoDetectFrames = LivenessConstant.DEFAULT_MAX_VIDEO_DETECT_FRAMES;
/**
* 真人阈值
*/

View File

@@ -24,4 +24,9 @@ public class LivenessConstant {
* 视频默认检测帧数
*/
public static final int DEFAULT_FRAME_COUNT = 10;
/**
* 视频默认最大检测帧数
*/
public static final int DEFAULT_MAX_VIDEO_DETECT_FRAMES = Integer.MAX_VALUE;
}

View File

@@ -1,6 +1,5 @@
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;
@@ -80,7 +79,7 @@ public class FaceDao {
*/
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;
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 {
@@ -164,8 +163,9 @@ public class FaceDao {
* @throws ClassNotFoundException 类未找到异常
*/
public List<FaceVector> findFace(int pageNo, int pageSize) throws SQLException, ClassNotFoundException {
long offset = (pageNo - 1) * pageSize;
String sql = "select \"id\",\"vector\",\"metadata\" from " + FACE_TABLE_NAME +
" limit " + pageNo * pageSize + "," + pageSize;
" limit " + offset + "," + pageSize;
SqliteHelper sqliteHelper = SqliteHelper.getInstance(dbFilePath);
return sqliteHelper.executeQuery(sql, new RowMapper<FaceVector>() {
@Override

View File

@@ -1,19 +0,0 @@
package cn.smartjavaai.face.entity;
import lombok.Data;
/**
* 人脸数据
* @author dwj
*/
@Data
public class FaceData {
private String key;
private long index;
private byte[] imgData;
private int width = 256;
private int height = 256;
private int channel = 3;
}

View File

@@ -24,6 +24,9 @@ public class ExpressionModelFactory {
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
private static volatile ExpressionModelFactory instance;
/**
* 模型缓存
*/
private static final ConcurrentHashMap<ExpressionModelEnum, ExpressionModel> modelMap = new ConcurrentHashMap<>();
/**

View File

@@ -9,8 +9,6 @@ import ai.djl.ndarray.NDManager;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.lang.generator.UUIDGenerator;
import cn.smartjavaai.common.entity.*;
import cn.smartjavaai.common.entity.face.FaceInfo;
import cn.smartjavaai.common.entity.face.FaceSearchResult;
@@ -28,7 +26,7 @@ import cn.smartjavaai.face.enums.SimilarityType;
import cn.smartjavaai.face.exception.FaceException;
import cn.smartjavaai.face.factory.FaceDetModelFactory;
import cn.smartjavaai.face.model.facedect.FaceDetModel;
import cn.smartjavaai.face.model.facerec.criterial.FaceRecCriteriaFactory;
import cn.smartjavaai.face.model.facerec.criteria.FaceRecCriteriaFactory;
import cn.smartjavaai.face.preprocess.DJLImagePreprocessor;
import cn.smartjavaai.face.utils.*;
import cn.smartjavaai.face.vector.config.MilvusConfig;
@@ -646,6 +644,22 @@ public class CommonFaceRecModel implements FaceRecModel{
}
}
@Override
public R<FaceVector> getFaceInfoById(String id) {
if(vectorDBClient == null){
return R.fail(1000, "向量数据库未初始化成功");
}
return R.ok(vectorDBClient.getFaceInfoById(id));
}
@Override
public R<List<FaceVector>> listFaces(long pageNum, long pageSize) {
if(vectorDBClient == null){
return R.fail(1000, "向量数据库未初始化成功");
}
return R.ok(vectorDBClient.listFaces(pageNum, pageSize));
}
@Override
public void loadFaceFeatures() {
if(Objects.isNull(vectorDBClient)){

View File

@@ -6,6 +6,7 @@ import cn.smartjavaai.face.config.FaceRecConfig;
import cn.smartjavaai.face.entity.FaceRegisterInfo;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.common.entity.face.FaceSearchResult;
import cn.smartjavaai.face.vector.entity.FaceVector;
import java.awt.image.BufferedImage;
import java.io.InputStream;
@@ -245,8 +246,26 @@ public interface FaceRecModel extends AutoCloseable{
}
/**
* 使用人脸ID获取人脸信息
* @param id
* @return
*/
default R<FaceVector> getFaceInfoById(String id){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 获取人脸列表
* @param pageNum
* @param pageSize
* @return
*/
default R<List<FaceVector>> listFaces(long pageNum, long pageSize){
throw new UnsupportedOperationException("默认不支持该功能");
}
/**
* 删除已注册人脸

View File

@@ -486,13 +486,13 @@ public class SeetaFace6FaceRecModel implements FaceRecModel{
* 检查是否存在人脸库
* @return
*/
private boolean checkFaceDb(){
if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
File file = new File(config.getFaceDbPath());
return file.exists() && file.isFile();
}
return false;
}
// private boolean checkFaceDb(){
// if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
// File file = new File(config.getFaceDbPath());
// return file.exists() && file.isFile();
// }
// return false;
// }
private FaceResult searchFaceDb(long index,float similar) {
if(index >= 0){
@@ -837,6 +837,22 @@ public class SeetaFace6FaceRecModel implements FaceRecModel{
FaceRecModel.super.upsertFace(faceRegisterInfo, imageData);
}
@Override
public R<FaceVector> getFaceInfoById(String id) {
if(vectorDBClient == null){
return R.fail(1000, "向量数据库未初始化成功");
}
return R.ok(vectorDBClient.getFaceInfoById(id));
}
@Override
public R<List<FaceVector>> listFaces(long pageNum, long pageSize) {
if(vectorDBClient == null){
return R.fail(1000, "向量数据库未初始化成功");
}
return R.ok(vectorDBClient.listFaces(pageNum, pageSize));
}
@Override
public void close() throws Exception {
if(Objects.nonNull(faceDetectorPool)){

View File

@@ -1,28 +1,15 @@
package cn.smartjavaai.face.model.facerec.criterial;
package cn.smartjavaai.face.model.facerec.criteria;
import ai.djl.Device;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.transform.Normalize;
import ai.djl.modality.cv.transform.Resize;
import ai.djl.modality.cv.transform.ToTensor;
import ai.djl.modality.cv.translator.ImageFeatureExtractor;
import ai.djl.modality.cv.translator.ImageFeatureExtractorFactory;
import ai.djl.repository.zoo.Criteria;
import ai.djl.training.util.ProgressBar;
import ai.djl.translate.Translator;
import cn.smartjavaai.common.enums.DeviceEnum;
import cn.smartjavaai.face.config.FaceDetConfig;
import cn.smartjavaai.face.config.FaceRecConfig;
import cn.smartjavaai.face.constant.FaceDetectConstant;
import cn.smartjavaai.face.constant.FaceNetConstant;
import cn.smartjavaai.face.constant.RetinaFaceConstant;
import cn.smartjavaai.face.constant.UltraLightFastGenericFaceConstant;
import cn.smartjavaai.face.enums.FaceDetModelEnum;
import cn.smartjavaai.face.enums.FaceRecModelEnum;
import cn.smartjavaai.face.model.facerec.translator.FaceFeatureTranslator;
import cn.smartjavaai.face.model.facerec.translator.FaceNetRecTranslator;
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
import org.apache.commons.lang3.StringUtils;
import java.nio.file.Paths;

View File

@@ -354,6 +354,9 @@ public class CommonLivenessModel implements LivenessDetModel{
}
// 逐帧处理视频
for (int frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
if(frameIndex >= config.getMaxVideoDetectFrames()){
return R.fail(10002, "超出最大检测帧数:" + config.getMaxVideoDetectFrames());
}
// 获取当前帧
Frame frame = grabber.grabImage();
if (frame != null) {

View File

@@ -515,6 +515,9 @@ public class Seetaface6LivenessModel implements LivenessDetModel{
}
// 逐帧处理视频
for (int frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
if(frameIndex >= config.getMaxVideoDetectFrames()){
return R.fail(10002, "超出最大检测帧数:" + config.getMaxVideoDetectFrames());
}
// 获取当前帧
Frame frame = grabber.grabImage();
if (frame != null) {

View File

@@ -14,8 +14,11 @@ import io.milvus.param.*;
import io.milvus.param.collection.*;
import io.milvus.param.dml.*;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.response.DescCollResponseWrapper;
import io.milvus.response.QueryResultsWrapper;
import io.milvus.response.SearchResultsWrapper;
import io.milvus.v2.service.collection.request.DescribeCollectionReq;
import io.milvus.v2.service.collection.response.DescribeCollectionResp;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -53,6 +56,14 @@ public class MilvusClient implements VectorDBClient {
serviceClient = new MilvusServiceClient(connectParam);
collectionName = StringUtils.isNotBlank(config.getCollectionName()) ? config.getCollectionName() : VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME;
createCollection(collectionName, config.getDimension());
boolean isAutoID = isAutoID(collectionName);
if(isAutoID && config.getIdStrategy() != IdStrategy.AUTO){
throw new VectorDBException("ID策略与当前Collection不匹配");
}
if(!isAutoID && config.getIdStrategy() == IdStrategy.AUTO){
throw new VectorDBException("ID策略与当前Collection不匹配");
}
if(config.isUseMemoryCache()){
// 加载集合到内存
loadFaceFeatures();
@@ -152,6 +163,21 @@ public class MilvusClient implements VectorDBClient {
}
}
/**
* 判断是否为自增长ID
* @param collectionName
* @return
*/
private boolean isAutoID(String collectionName) {
R<DescribeCollectionResponse> response = serviceClient.describeCollection(
DescribeCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()
);
DescCollResponseWrapper wrapper = new DescCollResponseWrapper(response.getData());
return wrapper.getPrimaryField().isAutoID();
}
@Override
public void dropCollection(String collectionName) {
try {
@@ -204,6 +230,7 @@ public class MilvusClient implements VectorDBClient {
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
throw new VectorDBException("插入数据失败vector不能为空");
}
//自定义ID
if(config.getIdStrategy() == IdStrategy.CUSTOM){
if(StringUtils.isBlank(faceVector.getId())){
@@ -525,40 +552,22 @@ public class MilvusClient implements VectorDBClient {
}
}
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) {
public FaceVector getFaceInfoById(String id) {
try {
if (!isInit){
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();
String expr = VectorDBConstants.FieldNames.ID_FIELD + " == '" + id + "'";
if(config.getIdStrategy() == IdStrategy.AUTO){
expr = VectorDBConstants.FieldNames.ID_FIELD + " == " + id;
}
// 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))
.withExpr(expr)
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.VECTOR_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.build()
);
@@ -574,12 +583,91 @@ public class MilvusClient implements VectorDBClient {
}
// 提取第一条记录
QueryResultsWrapper.RowRecord row = records.get(0);
return new FaceSearchResult(id, 1,(String)row.get(VectorDBConstants.FieldNames.METADATA_FIELD));
Object vectorObj = row.get(VectorDBConstants.FieldNames.VECTOR_FIELD);
float[] vector = null;
if (vectorObj instanceof List<?>) {
// Milvus SDK通常返回List<Float>转成float[]
List<Float> vectorList = (List<Float>) vectorObj;
vector = new float[vectorList.size()];
for (int i = 0; i < vectorList.size(); i++) {
vector[i] = vectorList.get(i);
}
}
return new FaceVector(id, vector, (String) row.get(VectorDBConstants.FieldNames.METADATA_FIELD));
} catch (Exception e) {
throw new VectorDBException("搜索 Milvus 向量失败", e);
throw new RuntimeException(e);
}
}
@Override
public List<FaceVector> listFaces(long pageNum, long pageSize) {
try {
if (!isInit) {
throw new VectorDBException("Milvus未初始化完毕");
}
if (pageNum < 1 || pageSize < 1) {
throw new IllegalArgumentException("pageNum和pageSize必须大于0");
}
long offset = (pageNum - 1) * pageSize;
// 构造查询参数使用offset和limit实现分页
QueryParam queryParam = QueryParam.newBuilder()
.withCollectionName(collectionName)
.withOutFields(Arrays.asList(
VectorDBConstants.FieldNames.ID_FIELD,
VectorDBConstants.FieldNames.VECTOR_FIELD,
VectorDBConstants.FieldNames.METADATA_FIELD))
.withOffset(offset)
.withLimit(pageSize)
.build();
R<QueryResults> response = serviceClient.query(queryParam);
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 Collections.emptyList();
}
List<FaceVector> result = new ArrayList<>();
for (QueryResultsWrapper.RowRecord row : records) {
String id = (String) row.get(VectorDBConstants.FieldNames.ID_FIELD);
Object vectorObj = row.get(VectorDBConstants.FieldNames.VECTOR_FIELD);
float[] vector = null;
if (vectorObj instanceof List<?>) {
List<Float> vectorList = (List<Float>) vectorObj;
vector = new float[vectorList.size()];
for (int i = 0; i < vectorList.size(); i++) {
vector[i] = vectorList.get(i);
}
}
String metadata = (String) row.get(VectorDBConstants.FieldNames.METADATA_FIELD);
result.add(new FaceVector(id, vector, metadata));
}
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
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 void loadFaceFeatures() {
// 加载集合到内存

View File

@@ -196,6 +196,42 @@ public class SQLiteClient implements VectorDBClient {
}
}
@Override
public FaceVector getFaceInfoById(String id) {
if (!isInit) {
throw new VectorDBException("人脸库未加载完毕");
}
// 先从内存缓存中获取
FaceVector faceVector = memoryIndex.get(id);
if (faceVector == null) {
// 如果内存中没有,则从数据库查询
try {
faceVector = faceDao.findById(id);
} catch (SQLException | ClassNotFoundException e) {
throw new VectorDBException("SQLite查询异常", e);
}
}
return faceVector;
}
@Override
public List<FaceVector> listFaces(long pageNum, long pageSize) {
if (!isInit) {
throw new VectorDBException("人脸库未加载完毕");
}
if (pageNum < 1 || pageSize < 1) {
throw new IllegalArgumentException("pageNum和pageSize必须大于0");
}
// 从数据库中查询指定分页的数据
try {
return faceDao.findFace((int)pageNum, (int)pageSize);
} catch (Exception e) {
throw new VectorDBException("分页查询失败", e);
}
}
// ============= 私有辅助方法 =============
private void loadAllFeaturesToMemory() {
@@ -234,19 +270,6 @@ public class SQLiteClient implements VectorDBClient {
}
}
@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() {
// 加载所有特征到内存

View File

@@ -1,5 +1,6 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.common.entity.R;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.face.FaceSearchResult;
@@ -94,12 +95,21 @@ public interface VectorDBClient extends AutoCloseable {
@Override
void close();
/**
* 获取指定ID的向量
* 使用人脸ID获取人脸信息
* @param id
* @return
*/
FaceSearchResult getById(String id);
FaceVector getFaceInfoById(String id);
/**
* 获取人脸列表
* @param pageNum
* @param pageSize
* @return
*/
List<FaceVector> listFaces(long pageNum, long pageSize);
/**
* 加载人脸特征到内存