mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-13 13:18:58 +00:00
1、新增图片与视频活体检测
2、新增人脸属性识别(性别、年龄、口罩、姿态、眼睛状态) 3、优化检测返回与包结构 4、新增 dependencyManagement 统一依赖版本管理
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package cn.smartjavaai.face.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.enums.FaceAttributeModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 人脸属性识别模型配置
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class FaceAttributeConfig {
|
||||
|
||||
/**
|
||||
* 人脸属性识别模型枚举
|
||||
*/
|
||||
private FaceAttributeModelEnum modelEnum = FaceAttributeModelEnum.SEETA_FACE6_MODEL;
|
||||
|
||||
/**
|
||||
* 模型路径
|
||||
*/
|
||||
private String modelPath;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* gpu设备ID 当device为GPU时生效
|
||||
*/
|
||||
private int gpuId = 0;
|
||||
|
||||
/**
|
||||
* 是否启用年龄检测
|
||||
*/
|
||||
private boolean enableAge = true;
|
||||
|
||||
/**
|
||||
* 是否启用性别检测
|
||||
*/
|
||||
private boolean enableGender = true;
|
||||
|
||||
/**
|
||||
* 是否启用人脸姿态检测
|
||||
*/
|
||||
private boolean enableHeadPose = true;
|
||||
|
||||
/**
|
||||
* 是否启用眼睛状态检测
|
||||
*/
|
||||
private boolean enableEyeStatus = true;
|
||||
|
||||
/**
|
||||
* 是否启用口罩检测
|
||||
*/
|
||||
private boolean enableMask = true;
|
||||
|
||||
|
||||
public FaceAttributeConfig() {
|
||||
}
|
||||
|
||||
public FaceAttributeConfig(FaceAttributeModelEnum modelEnum) {
|
||||
this.modelEnum = modelEnum;
|
||||
}
|
||||
|
||||
public FaceAttributeConfig(FaceAttributeModelEnum modelEnum, String modelPath) {
|
||||
this.modelEnum = modelEnum;
|
||||
this.modelPath = modelPath;
|
||||
}
|
||||
|
||||
public FaceAttributeConfig(String modelPath) {
|
||||
this.modelPath = modelPath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.face;
|
||||
package cn.smartjavaai.face.config;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cn.smartjavaai.face;
|
||||
package cn.smartjavaai.face.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 模型配置
|
||||
* 人脸检测识别模型配置
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
@@ -18,7 +20,7 @@ public class FaceModelConfig {
|
||||
/**
|
||||
* 置信度阈值
|
||||
*/
|
||||
private double confidenceThreshold = FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD;
|
||||
private double confidenceThreshold = FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 相似度阈值 作用:判断是否为同一人脸
|
||||
@@ -28,7 +30,7 @@ public class FaceModelConfig {
|
||||
/**
|
||||
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
|
||||
*/
|
||||
private double nmsThresh = FaceConfig.NMS_THRESHOLD;
|
||||
private double nmsThresh = FaceDetectConstant.NMS_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 模型路径
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.smartjavaai.face.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.constant.LivenessConstant;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.enums.LivenessModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 活体检测模型配置
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class LivenessConfig {
|
||||
|
||||
/**
|
||||
* 活体检测模型枚举
|
||||
*/
|
||||
private LivenessModelEnum modelEnum = LivenessModelEnum.SEETA_FACE6_MODEL;
|
||||
|
||||
/**
|
||||
* 模型路径
|
||||
*/
|
||||
private String modelPath;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* gpu设备ID 当device为GPU时生效
|
||||
*/
|
||||
private int gpuId = 0;
|
||||
|
||||
/**
|
||||
* 人脸清晰度阈值
|
||||
*/
|
||||
private float faceClarityThreshold = LivenessConstant.DEFAULT_FACE_CLARITY_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 活体阈值
|
||||
*/
|
||||
private float realityThreshold = LivenessConstant.DEFAULT_REALITY_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 视频检测帧数
|
||||
*/
|
||||
private int frameCount = LivenessConstant.DEFAULT_FRAME_COUNT;
|
||||
|
||||
public LivenessConfig() {
|
||||
}
|
||||
|
||||
public LivenessConfig(LivenessModelEnum modelEnum) {
|
||||
this.modelEnum = modelEnum;
|
||||
}
|
||||
|
||||
public LivenessConfig(LivenessModelEnum modelEnum, String modelPath) {
|
||||
this.modelEnum = modelEnum;
|
||||
this.modelPath = modelPath;
|
||||
}
|
||||
|
||||
public LivenessConfig(String modelPath) {
|
||||
this.modelPath = modelPath;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package cn.smartjavaai.face;
|
||||
package cn.smartjavaai.face.constant;
|
||||
|
||||
/**
|
||||
* 人脸检测常量
|
||||
* @author dwj
|
||||
*/
|
||||
public class FaceConfig {
|
||||
public class FaceDetectConstant {
|
||||
|
||||
/**
|
||||
* 置信度阈值
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.face.constant;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.enums.LivenessModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 活体检测常量
|
||||
* @author dwj
|
||||
*/
|
||||
public class LivenessConstant {
|
||||
|
||||
/**
|
||||
* 默认人脸清晰度阈值
|
||||
*/
|
||||
public static final float DEFAULT_FACE_CLARITY_THRESHOLD = 0.3F;
|
||||
|
||||
/**
|
||||
* 默认活体阈值
|
||||
*/
|
||||
public static final float DEFAULT_REALITY_THRESHOLD = 0.8F;
|
||||
|
||||
/**
|
||||
* 视频默认检测帧数
|
||||
*/
|
||||
public static final int DEFAULT_FRAME_COUNT = 10;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.smartjavaai.face.context;
|
||||
|
||||
import com.seeta.sdk.*;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/5/8
|
||||
*/
|
||||
public class PredictorContext {
|
||||
|
||||
public GenderPredictor genderPredictor;
|
||||
public AgePredictor agePredictor;
|
||||
public EyeStateDetector eyeStateDetector;
|
||||
public MaskDetector maskDetector;
|
||||
public PoseEstimator poseEstimator;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* 人脸属性识别模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/10
|
||||
*/
|
||||
public enum FaceAttributeModelEnum {
|
||||
|
||||
SEETA_FACE6_MODEL("SeetaFace6Model");
|
||||
|
||||
private final String modelClassName;
|
||||
|
||||
FaceAttributeModelEnum(String modelClassName) {
|
||||
this.modelClassName = modelClassName;
|
||||
}
|
||||
|
||||
public String getModelClassName() {
|
||||
return modelClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static FaceAttributeModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (FaceAttributeModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
package cn.smartjavaai.face;
|
||||
|
||||
import cn.smartjavaai.face.model.FeatureExtractionModel;
|
||||
import cn.smartjavaai.face.model.RetinaFaceModel;
|
||||
import cn.smartjavaai.face.model.SeetaFace6Model;
|
||||
import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* 人脸模型枚举
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.smartjavaai.face.enums;
|
||||
|
||||
/**
|
||||
* 活体检测模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/10
|
||||
*/
|
||||
public enum LivenessModelEnum {
|
||||
|
||||
SEETA_FACE6_MODEL("SeetaFace6Model");
|
||||
|
||||
private final String modelClassName;
|
||||
|
||||
LivenessModelEnum(String modelClassName) {
|
||||
this.modelClassName = modelClassName;
|
||||
}
|
||||
|
||||
public String getModelClassName() {
|
||||
return modelClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static LivenessModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (LivenessModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cn.smartjavaai.face.factory;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.model.attribute.Seetaface6FaceAttributeModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import cn.smartjavaai.face.model.attribute.FaceAttributeModel;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 人脸属性检测模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class FaceAttributeModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile FaceAttributeModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, FaceAttributeModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends FaceAttributeModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static FaceAttributeModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (FaceAttributeModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new FaceAttributeModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends FaceAttributeModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public FaceAttributeModel getModel(FaceAttributeConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new FaceException("未配置活体检测模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private FaceAttributeModel createFaceModel(FaceAttributeConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new FaceException("Unsupported algorithm");
|
||||
}
|
||||
FaceAttributeModel model = null;
|
||||
try {
|
||||
model = (FaceAttributeModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("seetaface6model", Seetaface6FaceAttributeModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package cn.smartjavaai.face;
|
||||
package cn.smartjavaai.face.factory;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.model.FeatureExtractionModel;
|
||||
import cn.smartjavaai.face.model.RetinaFaceModel;
|
||||
import cn.smartjavaai.face.model.SeetaFace6Model;
|
||||
import cn.smartjavaai.face.model.UltraLightFastGenericFaceModel;
|
||||
import cn.smartjavaai.face.model.facerec.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -13,7 +13,7 @@ import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 人脸算法工厂
|
||||
* 人脸检测识别模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
@@ -76,8 +76,8 @@ public class FaceModelFactory {
|
||||
// 初始化默认配置
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.RETINA_FACE);
|
||||
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
|
||||
config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||
config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
|
||||
return getModel(config);
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ public class FaceModelFactory {
|
||||
// 初始化默认配置
|
||||
FaceModelConfig config = new FaceModelConfig();
|
||||
config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
|
||||
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
|
||||
config.setConfidenceThreshold(FaceDetectConstant.DEFAULT_CONFIDENCE_THRESHOLD);
|
||||
config.setNmsThresh(FaceDetectConstant.NMS_THRESHOLD);
|
||||
return getModel(config);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package cn.smartjavaai.face.factory;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.config.LivenessConfig;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.enums.FaceModelEnum;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.model.facerec.*;
|
||||
import cn.smartjavaai.face.model.liveness.LivenessDetModel;
|
||||
import cn.smartjavaai.face.model.liveness.Seetaface6LivenessModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 活体检测模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class LivenessModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile LivenessModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, LivenessDetModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends LivenessDetModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static LivenessModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (LivenessModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new LivenessModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends LivenessDetModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public LivenessDetModel getModel(LivenessConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new FaceException("未配置活体检测模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private LivenessDetModel createFaceModel(LivenessConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new FaceException("Unsupported algorithm");
|
||||
}
|
||||
LivenessDetModel model = null;
|
||||
try {
|
||||
model = (LivenessDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("seetaface6model", Seetaface6LivenessModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package cn.smartjavaai.face.model.attribute;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.FaceAttribute;
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||
import cn.smartjavaai.common.enums.GenderType;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸属性识别模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface FaceAttributeModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(FaceAttributeConfig config); // 加载模型
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param imageData 图片字节流
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<FaceAttribute> detect(String imagePath, DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(单人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param imageData 图片数据
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<FaceAttribute> detect(byte[] imageData,DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(单人脸)
|
||||
* @param imageData
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 人脸属性识别(多人脸)
|
||||
* @param image BufferedImage
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<FaceAttribute> detect(BufferedImage image,DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(单人脸)
|
||||
* @param image BufferedImage
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 人脸属性识别(分数最高人脸)
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectTopFace(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 人脸属性识别(分数最高人脸)
|
||||
* @param imagePath
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectTopFace(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(分数最高人脸)
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectTopFace(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(裁剪后的人脸)
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectCropedFace(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(裁剪后的人脸)
|
||||
* @param imagePath
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectCropedFace(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 人脸属性识别(裁剪后的人脸)
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
default FaceAttribute detectCropedFace(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
package cn.smartjavaai.face.model.attribute;
|
||||
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.enums.EyeStatus;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.PoolUtils;
|
||||
import cn.smartjavaai.face.config.FaceAttributeConfig;
|
||||
import cn.smartjavaai.common.enums.GenderType;
|
||||
import cn.smartjavaai.face.context.PredictorContext;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.seetaface.NativeLoader;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import com.seeta.pool.*;
|
||||
import com.seeta.sdk.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* seetaface6 人脸属性识别模型
|
||||
* @author dwj
|
||||
* @date 2025/4/30
|
||||
*/
|
||||
@Slf4j
|
||||
public class Seetaface6FaceAttributeModel implements FaceAttributeModel {
|
||||
|
||||
|
||||
private FaceDetectorPool faceDetectorPool;
|
||||
private GenderPredictorPool genderPredictorPool;
|
||||
private FaceLandmarkerPool faceLandmarkerPool;
|
||||
private AgePredictorPool agePredictorPool;
|
||||
private EyeStateDetectorPool eyeStateDetectorPool;
|
||||
private MaskDetectorPool maskDetectorPool;
|
||||
private PoseEstimatorPool poseEstimatorPool;
|
||||
|
||||
private FaceAttributeConfig config;
|
||||
|
||||
|
||||
@Override
|
||||
public void loadModel(FaceAttributeConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new FaceException("modelPath is null");
|
||||
}
|
||||
this.config = config;
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("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"};
|
||||
String[] agePredictorModelPath = {config.getModelPath() + File.separator + "age_predictor.csta"};
|
||||
String[] eyeStateDetectorModelPath = {config.getModelPath() + File.separator + "eye_state.csta"};
|
||||
String[] maskDetectorModelPath = {config.getModelPath() + File.separator + "mask_detector.csta"};
|
||||
String[] poseEstimatorModelPath = {config.getModelPath() + File.separator + "pose_estimation.csta"};
|
||||
SeetaDevice device = SeetaDevice.SEETA_DEVICE_AUTO;
|
||||
int gpuId = 0;
|
||||
if(Objects.nonNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? SeetaDevice.SEETA_DEVICE_CPU : SeetaDevice.SEETA_DEVICE_GPU;
|
||||
if(config.getGpuId() >= 0 && device == SeetaDevice.SEETA_DEVICE_GPU){
|
||||
gpuId = config.getGpuId();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SeetaModelSetting faceDetectorPoolSetting = new SeetaModelSetting(gpuId, faceDetectorModelPath, device);
|
||||
SeetaConfSetting faceDetectorPoolConfSetting = new SeetaConfSetting(faceDetectorPoolSetting);
|
||||
|
||||
SeetaModelSetting faceLandmarkerPoolSetting = new SeetaModelSetting(gpuId, faceLandmarkerModelPath, device);
|
||||
SeetaConfSetting faceLandmarkerPoolConfSetting = new SeetaConfSetting(faceLandmarkerPoolSetting);
|
||||
|
||||
SeetaModelSetting genderPredictorPoolSetting = new SeetaModelSetting(gpuId, genderPredictorModelPath, device);
|
||||
SeetaConfSetting genderPredictorPoolConfSetting = new SeetaConfSetting(genderPredictorPoolSetting);
|
||||
|
||||
SeetaModelSetting agePredictorPoolSetting = new SeetaModelSetting(gpuId, agePredictorModelPath, device);
|
||||
SeetaConfSetting agePredictorPoolConfSetting = new SeetaConfSetting(agePredictorPoolSetting);
|
||||
|
||||
SeetaModelSetting eyeStateDetectorPoolSetting = new SeetaModelSetting(gpuId, eyeStateDetectorModelPath, device);
|
||||
SeetaConfSetting eyeStateDetectorPoolConfSetting = new SeetaConfSetting(eyeStateDetectorPoolSetting);
|
||||
|
||||
SeetaModelSetting maskDetectorPoolSetting = new SeetaModelSetting(gpuId, maskDetectorModelPath, device);
|
||||
SeetaConfSetting maskDetectorPoolConfSetting = new SeetaConfSetting(maskDetectorPoolSetting);
|
||||
|
||||
SeetaModelSetting poseEstimatorPoolSetting = new SeetaModelSetting(gpuId, poseEstimatorModelPath, device);
|
||||
SeetaConfSetting poseEstimatorPoolConfSetting = new SeetaConfSetting(poseEstimatorPoolSetting);
|
||||
|
||||
this.faceDetectorPool = new FaceDetectorPool(faceDetectorPoolConfSetting);
|
||||
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
|
||||
this.genderPredictorPool = new GenderPredictorPool(genderPredictorPoolConfSetting);
|
||||
this.agePredictorPool = new AgePredictorPool(agePredictorPoolConfSetting);
|
||||
this.eyeStateDetectorPool = new EyeStateDetectorPool(eyeStateDetectorPoolConfSetting);
|
||||
this.maskDetectorPool = new MaskDetectorPool(maskDetectorPoolConfSetting);
|
||||
this.poseEstimatorPool = new PoseEstimatorPool(poseEstimatorPoolConfSetting);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(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 detect(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
//创建推力器上下文
|
||||
PredictorContext predictorContext = new PredictorContext();
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
FaceDetector detectPredictor = null;
|
||||
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
|
||||
List<FaceAttribute> faceAttributeList = new ArrayList<FaceAttribute>();
|
||||
try {
|
||||
detectPredictor = faceDetectorPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
for(SeetaRect seetaRect : seetaResult){
|
||||
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||
seetaPointFSList.add(landmarks);
|
||||
//人脸属性检测
|
||||
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||
faceAttributeList.add(faceAttribute);
|
||||
}
|
||||
return FaceUtils.convertToFaceAttributeResponse(seetaResult, seetaPointFSList, faceAttributeList);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("人脸属性检测错误", e);
|
||||
} finally {
|
||||
// 统一归还所有 Predictor 到池
|
||||
PoolUtils.returnToPool(faceDetectorPool, detectPredictor);
|
||||
PoolUtils.returnToPool(faceLandmarkerPool, faceLandmarker);
|
||||
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单人脸属性检测
|
||||
* @param imageData
|
||||
* @param seetaRect
|
||||
* @param landmarks
|
||||
* @param predictorContext
|
||||
* @return
|
||||
*/
|
||||
private FaceAttribute detect(SeetaImageData imageData, SeetaRect seetaRect, SeetaPointF[] landmarks, PredictorContext predictorContext){
|
||||
FaceAttribute faceAttribute = new FaceAttribute();
|
||||
//性别检测
|
||||
GenderType genderType = null;
|
||||
if (config.isEnableGender()){
|
||||
GenderPredictor.GENDER[] gender = new GenderPredictor.GENDER[1];
|
||||
boolean isSuccess = predictorContext.genderPredictor.PredictGenderWithCrop(imageData, landmarks, gender);
|
||||
genderType = isSuccess ? FaceUtils.convertToGenderType(gender[0]) : GenderType.UNKNOWN;
|
||||
}
|
||||
//眼睛状态检测
|
||||
EyeStatus leftEyeStatus = null;
|
||||
EyeStatus rightEyeStatus = null;
|
||||
if (config.isEnableEyeStatus()){
|
||||
EyeStateDetector.EYE_STATE[] eyeState = predictorContext.eyeStateDetector.detect(imageData, landmarks);
|
||||
leftEyeStatus = FaceUtils.convertToEyeStatus(eyeState[0]);
|
||||
rightEyeStatus = FaceUtils.convertToEyeStatus(eyeState[1]);
|
||||
}
|
||||
//年龄检测
|
||||
Integer age = 0;
|
||||
if (config.isEnableAge()){
|
||||
age = predictorContext.agePredictor.predictAgeWithCrop(imageData, landmarks);
|
||||
}
|
||||
//口罩检测
|
||||
Boolean wearingMask = null;
|
||||
if (config.isEnableMask()){
|
||||
float[] score = new float[1];
|
||||
wearingMask = predictorContext.maskDetector.detect(imageData, seetaRect, score);
|
||||
}
|
||||
//姿态检测
|
||||
if (config.isEnableHeadPose()){
|
||||
float[] yaw = new float[1];//左右转头(水平旋转)
|
||||
float[] pitch = new float[1]; //上下抬头/低头(垂直旋转)
|
||||
float[] roll = new float[1]; //头部左右倾斜(平面旋转)
|
||||
predictorContext.poseEstimator.Estimate(imageData, seetaRect, yaw, pitch, roll);
|
||||
faceAttribute.setHeadPose(new HeadPose(yaw[0], pitch[0], roll[0]));
|
||||
}
|
||||
faceAttribute.setGenderType(genderType);
|
||||
faceAttribute.setAge(age);
|
||||
faceAttribute.setLeftEyeStatus(leftEyeStatus);
|
||||
faceAttribute.setRightEyeStatus(rightEyeStatus);
|
||||
faceAttribute.setWearingMask(wearingMask);
|
||||
return faceAttribute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaceAttribute> detect(String imagePath, DetectionResponse faceDetectionResponse) {
|
||||
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 detect(image, faceDetectionResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaceAttribute> detect(byte[] imageData, DetectionResponse faceDetectionResponse) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaceAttribute> detect(BufferedImage image, DetectionResponse faceDetectionResponse) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
//创建推力器上下文
|
||||
PredictorContext predictorContext = new PredictorContext();
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
List<FaceAttribute> faceAttributeList = new ArrayList<FaceAttribute>();
|
||||
try {
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||
for(DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()){
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(detectionInfo.getDetectionRectangle());
|
||||
SeetaPointF[] landmarks = null;
|
||||
FaceInfo faceInfo = detectionInfo.getFaceInfo();
|
||||
//如果没有人脸标识,则提取人脸标识
|
||||
if(faceInfo == null || faceInfo.getKeyPoints() == null || faceInfo.getKeyPoints().isEmpty()){
|
||||
//提取人脸的5点人脸标识
|
||||
landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||
}else{
|
||||
landmarks = FaceUtils.convertToSeetaPointF(faceInfo.getKeyPoints());
|
||||
}
|
||||
//人脸属性检测
|
||||
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||
faceAttributeList.add(faceAttribute);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||
}
|
||||
return faceAttributeList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaceAttribute detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
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 detect(image, faceDetectionRectangle, keyPoints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public FaceAttribute detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle, keyPoints);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public FaceAttribute detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(faceDetectionRectangle)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
//创建推力器上下文
|
||||
PredictorContext predictorContext = new PredictorContext();
|
||||
try {
|
||||
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(faceDetectionRectangle);
|
||||
SeetaPointF[] landmarks = null;
|
||||
if(keyPoints == null || keyPoints.isEmpty()){
|
||||
throw new FaceException("人脸关键点keyPoints为空");
|
||||
}
|
||||
landmarks = FaceUtils.convertToSeetaPointF(keyPoints);
|
||||
//人脸属性检测
|
||||
FaceAttribute faceAttribute = detect(imageData, seetaRect, landmarks, predictorContext);
|
||||
return faceAttribute;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaceAttribute detectTopFace(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 detectTopFace(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaceAttribute detectTopFace(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detectTopFace(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public FaceAttribute detectTopFace(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
FaceDetector detectPredictor = null;
|
||||
//创建推力器上下文
|
||||
PredictorContext predictorContext = new PredictorContext();
|
||||
try {
|
||||
detectPredictor = faceDetectorPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
predictorContext.genderPredictor = config.isEnableGender() ? genderPredictorPool.borrowObject() : null;
|
||||
predictorContext.agePredictor = config.isEnableAge() ? agePredictorPool.borrowObject() : null;
|
||||
predictorContext.maskDetector = config.isEnableMask() ? maskDetectorPool.borrowObject() : null;
|
||||
predictorContext.eyeStateDetector = config.isEnableEyeStatus() ? eyeStateDetectorPool.borrowObject() : null;
|
||||
predictorContext.poseEstimator = config.isEnableHeadPose() ? poseEstimatorPool.borrowObject() : null;
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaResult[0], landmarks);
|
||||
//人脸属性检测
|
||||
FaceAttribute faceAttribute = detect(imageData, seetaResult[0], landmarks, predictorContext);
|
||||
return faceAttribute;
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (detectPredictor != null) {
|
||||
try {
|
||||
faceDetectorPool.returnObject(detectPredictor);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
PoolUtils.returnToPool(genderPredictorPool, predictorContext.genderPredictor);
|
||||
PoolUtils.returnToPool(agePredictorPool, predictorContext.agePredictor);
|
||||
PoolUtils.returnToPool(maskDetectorPool, predictorContext.maskDetector);
|
||||
PoolUtils.returnToPool(eyeStateDetectorPool, predictorContext.eyeStateDetector);
|
||||
PoolUtils.returnToPool(poseEstimatorPool, predictorContext.poseEstimator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.smartjavaai.face;
|
||||
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;
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.smartjavaai.face;
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.face.model;
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
@@ -12,14 +12,18 @@ 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.*;
|
||||
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;
|
||||
@@ -28,17 +32,15 @@ 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.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
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.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -223,19 +225,21 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
|
||||
}
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
||||
DetectionResponse detectedResult = faceModel.detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getRectangleList()) || detectedResult.getRectangleList().isEmpty()){
|
||||
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 (DetectionRectangle rectangle : detectedResult.getRectangleList()){
|
||||
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.isAlign()){
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
@@ -314,18 +318,19 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
|
||||
if(config.isCropFace()){
|
||||
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
|
||||
DetectionResponse detectedResult = faceModel.detect(image);
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getRectangleList()) || detectedResult.getRectangleList().isEmpty()){
|
||||
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("未检测到人脸");
|
||||
}
|
||||
//只取第一个人脸
|
||||
DetectionRectangle rectangle = detectedResult.getRectangleList().get(0);
|
||||
DetectionInfo detectionInfo = detectedResult.getDetectionInfoList().get(0);
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
//裁剪人脸
|
||||
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
|
||||
//人脸对齐
|
||||
if(config.isAlign()){
|
||||
NDManager manager = NDManager.newBaseManager();
|
||||
//获取子图中人脸关键点坐标
|
||||
double[][] pointsArray = FaceUtils.facePoints(rectangle.getKeyPoints());
|
||||
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
|
||||
NDArray srcPoints = manager.create(pointsArray);
|
||||
NDArray dstPoints = FaceUtils.faceTemplate512x512(manager);
|
||||
// 5点仿射变换
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.face.model;
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
@@ -15,7 +15,8 @@ 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.*;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
@@ -24,7 +25,6 @@ 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.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -33,7 +33,6 @@ import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -73,7 +72,7 @@ public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
FaceDetectionTranslator translator =
|
||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
|
||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceDetectConstant.MAX_FACE_LIMIT, scales, steps);
|
||||
Criteria<Image, DetectedObjects> criteria =
|
||||
Criteria.builder()
|
||||
.setTypes(Image.class, DetectedObjects.class)
|
||||
@@ -1,17 +1,14 @@
|
||||
package cn.smartjavaai.face.model;
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.face.AbstractFaceModel;
|
||||
import cn.smartjavaai.face.FaceExtractConfig;
|
||||
import cn.smartjavaai.face.FaceModelConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.dao.FaceDao;
|
||||
import cn.smartjavaai.face.entity.FaceData;
|
||||
import cn.smartjavaai.face.entity.FaceResult;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.utils.FaceAlignUtils;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import com.seeta.pool.*;
|
||||
import com.seeta.sdk.*;
|
||||
@@ -63,7 +60,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
config.setSimilarityThreshold(SEETAFACE_DEFAULT_SIMILARITY_THRESHOLD);
|
||||
}
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config);
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("Loading seetaFace6 library successfully.");
|
||||
String[] faceDetectorModelPath = {config.getModelPath() + File.separator + "face_detector.csta"};
|
||||
String[] faceRecognizerModelPath = {config.getModelPath() + File.separator + "face_recognizer.csta"};
|
||||
@@ -209,7 +206,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
throw new FaceException("无效图片路径", e);
|
||||
}
|
||||
DetectionResponse result = detect(image);
|
||||
if(Objects.isNull(result) || Objects.isNull(result.getRectangleList()) || result.getRectangleList().isEmpty()){
|
||||
if(Objects.isNull(result) || Objects.isNull(result.getDetectionInfoList()) || result.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("未识别到人脸");
|
||||
}
|
||||
//绘制人脸框
|
||||
@@ -225,7 +222,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
DetectionResponse detectedObjects = detect(sourceImage);
|
||||
if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getRectangleList()) || detectedObjects.getRectangleList().isEmpty()){
|
||||
if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getDetectionInfoList()) || detectedObjects.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("未识别到人脸");
|
||||
}
|
||||
//绘制人脸框
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.face.model;
|
||||
package cn.smartjavaai.face.model.facerec;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
@@ -15,7 +15,8 @@ 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.*;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.constant.FaceDetectConstant;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
@@ -24,13 +25,11 @@ 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.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -72,7 +71,7 @@ public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
FaceDetectionTranslator translator =
|
||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
|
||||
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceDetectConstant.MAX_FACE_LIMIT, scales, steps);
|
||||
Criteria<Image, DetectedObjects> criteria =
|
||||
Criteria.builder()
|
||||
.setTypes(Image.class, DetectedObjects.class)
|
||||
@@ -0,0 +1,206 @@
|
||||
package cn.smartjavaai.face.model.liveness;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.face.config.LivenessConfig;
|
||||
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活体检测模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface LivenessDetModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(LivenessConfig config); // 加载模型
|
||||
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param imageData 图片字节流
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<LivenessStatus> detect(String imagePath, DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(单人脸)
|
||||
* @param imagePath 图片路径
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param imageData 图片数据
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<LivenessStatus> detect(byte[] imageData,DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(单人脸)
|
||||
* @param imageData
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活体检测(多人脸)
|
||||
* @param image BufferedImage
|
||||
* @param faceDetectionResponse 人脸检测结果
|
||||
* @return
|
||||
*/
|
||||
default List<LivenessStatus> detect(BufferedImage image,DetectionResponse faceDetectionResponse){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(单人脸)
|
||||
* @param image BufferedImage
|
||||
* @param faceDetectionRectangle 人脸检测结果-人脸框
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活体检测(分数最高人脸)
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectTopFace(BufferedImage image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活体检测(分数最高人脸)
|
||||
* @param imagePath
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectTopFace(String imagePath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 活体检测(分数最高人脸)
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectTopFace(byte[] imageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 视频活体检测(逐帧检测)
|
||||
* @param frameImage
|
||||
* @param faceDetectionRectangle
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideoByFrame(BufferedImage frameImage, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频活体检测(逐帧检测)
|
||||
* @param frameData
|
||||
* @param faceDetectionRectangle
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频活体检测(逐帧检测)
|
||||
* @param frameImageData
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideoByFrame(byte[] frameImageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频活体检测(逐帧检测)
|
||||
* @param frameImageData
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideoByFrame(BufferedImage frameImageData){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频活体检测
|
||||
* @param videoInputStream
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideo(InputStream videoInputStream){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频活体检测
|
||||
* @param videoPath
|
||||
* @return
|
||||
*/
|
||||
default LivenessStatus detectVideo(String videoPath){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package cn.smartjavaai.face.model.liveness;
|
||||
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.face.config.LivenessConfig;
|
||||
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import cn.smartjavaai.face.seetaface.NativeLoader;
|
||||
import cn.smartjavaai.face.utils.FaceUtils;
|
||||
import com.seeta.pool.*;
|
||||
import com.seeta.sdk.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* seetaface6 活体检测模型
|
||||
* @author dwj
|
||||
* @date 2025/4/30
|
||||
*/
|
||||
@Slf4j
|
||||
public class Seetaface6LivenessModel implements LivenessDetModel{
|
||||
|
||||
|
||||
private FaceDetectorPool faceDetectorPool;
|
||||
private FaceAntiSpoofingPool faceAntiSpoofingPool;
|
||||
private FaceLandmarkerPool faceLandmarkerPool;
|
||||
|
||||
|
||||
@Override
|
||||
public void loadModel(LivenessConfig config) {
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new FaceException("modelPath is null");
|
||||
}
|
||||
//加载依赖库
|
||||
NativeLoader.loadNativeLibraries(config.getDevice());
|
||||
log.info("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"};
|
||||
SeetaDevice device = SeetaDevice.SEETA_DEVICE_AUTO;
|
||||
int gpuId = 0;
|
||||
if(Objects.nonNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? SeetaDevice.SEETA_DEVICE_CPU : SeetaDevice.SEETA_DEVICE_GPU;
|
||||
if(config.getGpuId() >= 0 && device == SeetaDevice.SEETA_DEVICE_GPU){
|
||||
gpuId = config.getGpuId();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SeetaModelSetting faceDetectorPoolSetting = new SeetaModelSetting(gpuId, faceDetectorModelPath, device);
|
||||
SeetaConfSetting faceDetectorPoolConfSetting = new SeetaConfSetting(faceDetectorPoolSetting);
|
||||
|
||||
SeetaModelSetting faceLandmarkerPoolSetting = new SeetaModelSetting(gpuId, faceLandmarkerModelPath, device);
|
||||
SeetaConfSetting faceLandmarkerPoolConfSetting = new SeetaConfSetting(faceLandmarkerPoolSetting);
|
||||
|
||||
SeetaModelSetting faceAntiSpoofingSetting = new SeetaModelSetting(gpuId, faceAntiSpoofingModelPath, device);
|
||||
SeetaConfSetting faceAntiSpoofingPoolConfSetting = new SeetaConfSetting(faceAntiSpoofingSetting);
|
||||
|
||||
this.faceDetectorPool = new FaceDetectorPool(faceDetectorPoolConfSetting);
|
||||
this.faceAntiSpoofingPool = new FaceAntiSpoofingPool(faceAntiSpoofingPoolConfSetting);
|
||||
this.faceLandmarkerPool = new FaceLandmarkerPool(faceLandmarkerPoolConfSetting);
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
//设置参数
|
||||
try {
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
if(config.getFaceClarityThreshold() > 0 && config.getRealityThreshold() > 0){
|
||||
faceAntiSpoofing.SetThreshold(config.getFaceClarityThreshold(), config.getRealityThreshold());
|
||||
}
|
||||
if(config.getFrameCount() > 0){
|
||||
faceAntiSpoofing.SetVideoFrameCount(config.getFrameCount());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
} finally {
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new FaceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(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 detect(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
FaceAntiSpoofing.Status status = null;
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
FaceDetector detectPredictor = null;
|
||||
List<SeetaPointF[]> seetaPointFSList = new ArrayList<SeetaPointF[]>();
|
||||
List<LivenessStatus> livenessStatusList = new ArrayList<LivenessStatus>();
|
||||
try {
|
||||
detectPredictor = faceDetectorPool.borrowObject();
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
for(SeetaRect seetaRect : seetaResult){
|
||||
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||
seetaPointFSList.add(landmarks);
|
||||
//检测图片
|
||||
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||
livenessStatusList.add(FaceUtils.convertToLivenessStatus(status));
|
||||
}
|
||||
return FaceUtils.convertToDetectionResponse(seetaResult, seetaPointFSList, livenessStatusList);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (detectPredictor != null) {
|
||||
try {
|
||||
faceDetectorPool.returnObject(detectPredictor);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LivenessStatus> detect(String imagePath, DetectionResponse faceDetectionResponse) {
|
||||
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 detect(image, faceDetectionResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detect(String imagePath, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
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 detect(image, faceDetectionRectangle, keyPoints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private List<LivenessStatus> detect(BufferedImage image, DetectionResponse faceDetectionResponse,boolean isImage) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
FaceAntiSpoofing.Status status = null;
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
List<LivenessStatus> livenessStatusList = new ArrayList<LivenessStatus>();
|
||||
try {
|
||||
for(DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()){
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(detectionInfo.getDetectionRectangle());
|
||||
SeetaPointF[] landmarks = null;
|
||||
FaceInfo faceInfo = detectionInfo.getFaceInfo();
|
||||
//如果没有人脸标识,则提取人脸标识
|
||||
if(faceInfo == null || faceInfo.getKeyPoints() == null || faceInfo.getKeyPoints().isEmpty()){
|
||||
//提取人脸的5点人脸标识
|
||||
landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaRect, landmarks);
|
||||
}else{
|
||||
landmarks = FaceUtils.convertToSeetaPointF(faceInfo.getKeyPoints());
|
||||
}
|
||||
//检测图片
|
||||
if(isImage){
|
||||
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||
}else{
|
||||
//检测视频
|
||||
status = faceAntiSpoofing.PredictVideo(imageData, seetaRect, landmarks);
|
||||
}
|
||||
livenessStatusList.add(FaceUtils.convertToLivenessStatus(status));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return livenessStatusList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LivenessStatus> detect(BufferedImage image, DetectionResponse faceDetectionResponse) {
|
||||
return detect(image, faceDetectionResponse, true);
|
||||
}
|
||||
|
||||
private LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints, boolean isImage) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(faceDetectionRectangle)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
FaceAntiSpoofing.Status status = null;
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
try {
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
SeetaRect seetaRect = FaceUtils.convertToSeetaRect(faceDetectionRectangle);
|
||||
SeetaPointF[] landmarks = null;
|
||||
if(keyPoints == null || keyPoints.isEmpty()){
|
||||
throw new FaceException("人脸关键点keyPoints为空");
|
||||
}
|
||||
landmarks = FaceUtils.convertToSeetaPointF(keyPoints);
|
||||
//检测图片
|
||||
if(isImage){
|
||||
status = faceAntiSpoofing.Predict(imageData, seetaRect, landmarks);
|
||||
}else{
|
||||
//检测视频
|
||||
status = faceAntiSpoofing.PredictVideo(imageData, seetaRect, landmarks);
|
||||
}
|
||||
return FaceUtils.convertToLivenessStatus(status);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detect(BufferedImage image, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
return detect(image, faceDetectionRectangle, keyPoints, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<LivenessStatus> detect(byte[] imageData, DetectionResponse faceDetectionResponse) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detect(byte[] imageData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle, keyPoints);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectTopFace(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 detectTopFace(image);
|
||||
}
|
||||
|
||||
|
||||
private LivenessStatus detectTopFace(BufferedImage image, boolean isImage) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
FaceAntiSpoofing.Status status = null;
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
FaceLandmarker faceLandmarker = null;
|
||||
FaceDetector detectPredictor = null;
|
||||
try {
|
||||
detectPredictor = faceDetectorPool.borrowObject();
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
faceLandmarker = faceLandmarkerPool.borrowObject();
|
||||
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
|
||||
imageData.data = ImageUtils.getMatrixBGR(image);
|
||||
//检测人脸
|
||||
SeetaRect[] seetaResult = detectPredictor.Detect(imageData);
|
||||
if(Objects.isNull(seetaResult)){
|
||||
throw new FaceException("无人脸数据");
|
||||
}
|
||||
SeetaPointF[] landmarks = new SeetaPointF[faceLandmarker.number()];
|
||||
faceLandmarker.mark(imageData, seetaResult[0], landmarks);
|
||||
//检测图片
|
||||
if(isImage){
|
||||
status = faceAntiSpoofing.Predict(imageData, seetaResult[0], landmarks);
|
||||
}else{
|
||||
status = faceAntiSpoofing.PredictVideo(imageData, seetaResult[0], landmarks);
|
||||
}
|
||||
return FaceUtils.convertToLivenessStatus(status);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException("活体检测错误", e);
|
||||
} finally {
|
||||
if (detectPredictor != null) {
|
||||
try {
|
||||
faceDetectorPool.returnObject(detectPredictor);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
if (faceLandmarker != null) {
|
||||
try {
|
||||
faceLandmarkerPool.returnObject(faceLandmarker);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectTopFace(BufferedImage image) {
|
||||
return detectTopFace(image, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectTopFace(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detectTopFace(ImageIO.read(new ByteArrayInputStream(imageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideoByFrame(BufferedImage frameImage, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
if(!ImageUtils.isImageValid(frameImage)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
return detect(frameImage,faceDetectionRectangle, keyPoints,false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideoByFrame(byte[] frameData, DetectionRectangle faceDetectionRectangle, List<Point> keyPoints) {
|
||||
if(Objects.isNull(frameData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detect(ImageIO.read(new ByteArrayInputStream(frameData)), faceDetectionRectangle, keyPoints, false);
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideoByFrame(byte[] frameImageData) {
|
||||
if(Objects.isNull(frameImageData)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
try {
|
||||
return detectVideoByFrame(ImageIO.read(new ByteArrayInputStream(frameImageData)));
|
||||
} catch (IOException e) {
|
||||
throw new FaceException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideoByFrame(BufferedImage frameImageData) {
|
||||
return detectTopFace(frameImageData, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideo(InputStream videoInputStream) {
|
||||
if(Objects.isNull(videoInputStream)){
|
||||
throw new FaceException("视频无效");
|
||||
}
|
||||
return detectVideo(new FFmpegFrameGrabber(videoInputStream));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivenessStatus detectVideo(String videoPath) {
|
||||
if(!FileUtils.isFileExists(videoPath)){
|
||||
throw new FaceException("视频文件不存在");
|
||||
}
|
||||
return detectVideo(new FFmpegFrameGrabber(videoPath));
|
||||
}
|
||||
|
||||
private LivenessStatus detectVideo(FFmpegFrameGrabber grabber) {
|
||||
FaceAntiSpoofing faceAntiSpoofing = null;
|
||||
try {
|
||||
faceAntiSpoofing = faceAntiSpoofingPool.borrowObject();
|
||||
grabber.start();
|
||||
// 获取视频总帧数
|
||||
int totalFrames = grabber.getLengthInFrames();
|
||||
int videoFrameCountConfig = faceAntiSpoofing.GetVideoFrameCount();
|
||||
log.info("视频总帧数:{},检测帧数:{}", totalFrames, videoFrameCountConfig);
|
||||
if(totalFrames < videoFrameCountConfig){
|
||||
throw new FaceException("视频帧数低于检测帧数");
|
||||
}
|
||||
// 逐帧处理视频
|
||||
for (int frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
||||
// 获取当前帧
|
||||
Frame frame = grabber.grabImage();
|
||||
if (frame != null) {
|
||||
BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame);
|
||||
LivenessStatus livenessStatus = detectVideoByFrame(bufferedImage);
|
||||
//满足检测帧数之后停止检测
|
||||
if(livenessStatus != LivenessStatus.DETECTING){
|
||||
return livenessStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
grabber.stop();
|
||||
} catch (FFmpegFrameGrabber.Exception e) {
|
||||
throw new FaceException(e);
|
||||
} catch (Exception e) {
|
||||
throw new FaceException(e);
|
||||
} finally {
|
||||
if (faceAntiSpoofing != null) {
|
||||
try {
|
||||
faceAntiSpoofingPool.returnObject(faceAntiSpoofing);
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return LivenessStatus.UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import cn.hutool.system.OsInfo;
|
||||
import cn.hutool.system.SystemUtil;
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.face.FaceModelConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import com.seeta.sdk.util.DllItem;
|
||||
import com.seeta.sdk.util.LoadNativeCore;
|
||||
@@ -39,35 +39,48 @@ public class NativeLoader {
|
||||
*/
|
||||
private static final String PROPERTIES_FILE_NAME = "dll.properties";
|
||||
|
||||
// 使用 volatile 保证内存可见性
|
||||
private static volatile boolean isDllLoaded = false;
|
||||
|
||||
|
||||
public static void loadNativeLibraries(FaceModelConfig config) {
|
||||
|
||||
public static void loadNativeLibraries(DeviceEnum device) {
|
||||
try {
|
||||
OsInfo osInfo = SystemUtil.getOsInfo();
|
||||
//检查当前系统是否支持
|
||||
if(!osInfo.isWindows() && !osInfo.isLinux()){
|
||||
throw new FaceException("当前系统不支持:" + osInfo.getName());
|
||||
}
|
||||
//判断硬件架构是否支持GPU
|
||||
if(config.getDevice() != null && config.getDevice().equals(DeviceEnum.GPU)){
|
||||
//GPU仅支持amd64
|
||||
if(!osInfo.getArch().contains("amd64") && !osInfo.getArch().contains("x86_64")){
|
||||
throw new FaceException("seetaface6 GPU模型不支持当前arch:" + osInfo.getArch());
|
||||
if (!isDllLoaded) {
|
||||
synchronized (NativeLoader.class) {
|
||||
if (!isDllLoaded) { // 双重检查
|
||||
OsInfo osInfo = SystemUtil.getOsInfo();
|
||||
//检查当前系统是否支持
|
||||
if(!osInfo.isWindows() && !osInfo.isLinux()){
|
||||
throw new FaceException("当前系统不支持:" + osInfo.getName());
|
||||
}
|
||||
//判断硬件架构是否支持GPU
|
||||
if(device != null && device.equals(DeviceEnum.GPU)){
|
||||
//GPU仅支持amd64
|
||||
if(!osInfo.getArch().contains("amd64") && !osInfo.getArch().contains("x86_64")){
|
||||
throw new FaceException("seetaface6 GPU模型不支持当前arch:" + osInfo.getArch());
|
||||
}
|
||||
}
|
||||
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
|
||||
//创建目录
|
||||
FileUtil.mkdir(seetaface6NativePath);
|
||||
log.info("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()));
|
||||
});
|
||||
}
|
||||
isDllLoaded = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("SeetaFace DLL is already loaded.");
|
||||
}
|
||||
seetaface6NativePath = Paths.get(Config.getCachePath(), SEETAFACE_LIB_DIR);
|
||||
//创建目录
|
||||
FileUtil.mkdir(seetaface6NativePath);
|
||||
log.info("seetaface6依赖库路径: " + seetaface6NativePath.toAbsolutePath().toString());
|
||||
//拷贝依赖库到缓存目录
|
||||
List<File> fileList = getLibFiles(osInfo, config.getDevice());
|
||||
if(fileList != null && !fileList.isEmpty()){
|
||||
// 加载依赖库文件
|
||||
fileList.forEach(file -> {
|
||||
System.load(file.getAbsolutePath());
|
||||
log.info(String.format("load %s finish", file.getAbsolutePath()));
|
||||
});
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Native library loading failed", e);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import ai.djl.translate.Translator;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
|
||||
/**
|
||||
* facenet人脸特征提取Translator
|
||||
* @author dwj
|
||||
* @date 2025/3/31
|
||||
*/
|
||||
|
||||
@@ -5,15 +5,15 @@ import ai.djl.modality.cv.output.BoundingBox;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.common.enums.EyeStatus;
|
||||
import cn.smartjavaai.common.enums.GenderType;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.face.FaceModelConfig;
|
||||
import cn.smartjavaai.face.config.FaceModelConfig;
|
||||
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||
import cn.smartjavaai.face.exception.FaceException;
|
||||
import com.seeta.sdk.SeetaImageData;
|
||||
import com.seeta.sdk.SeetaPointF;
|
||||
import com.seeta.sdk.SeetaRect;
|
||||
import com.seeta.sdk.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
@@ -45,7 +45,7 @@ public class FaceUtils {
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
Iterator iterator = detectedObjectList.iterator();
|
||||
int index = 0;
|
||||
while(iterator.hasNext()) {
|
||||
@@ -64,12 +64,13 @@ public class FaceUtils {
|
||||
if (y < 0) y = 0;
|
||||
if (x + width > img.getWidth()) width = img.getWidth() - x;
|
||||
if (y + height > img.getHeight()) height = img.getHeight() - y;
|
||||
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue());
|
||||
rectangle.setKeyPoints(keyPoints);
|
||||
rectangleList.add(rectangle);
|
||||
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height);
|
||||
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||
DetectionInfo detectionInfo = new DetectionInfo(rectangle, detection.getProbabilities().get(index).floatValue(),faceInfo);
|
||||
detectionInfoList.add(detectionInfo);
|
||||
index++;
|
||||
}
|
||||
detectionResponse.setRectangleList(rectangleList);
|
||||
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ public class FaceUtils {
|
||||
return null;
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
for(int i = 0; i < seetaResult.length; i++){
|
||||
SeetaRect rect = seetaResult[i];
|
||||
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||
@@ -91,14 +92,15 @@ public class FaceUtils {
|
||||
/*if(config.getConfidenceThreshold() > 0){
|
||||
continue;
|
||||
}*/
|
||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height, 0);
|
||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||
.map(p -> new Point(p.x, p.y))
|
||||
.collect(Collectors.toList());
|
||||
rectangle.setKeyPoints(keyPoints);
|
||||
rectangleList.add(rectangle);
|
||||
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||
DetectionInfo detectionInfo = new DetectionInfo(rectangle, 0, faceInfo);
|
||||
detectionInfoList.add(detectionInfo);
|
||||
}
|
||||
detectionResponse.setRectangleList(rectangleList);
|
||||
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ public class FaceUtils {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
@@ -122,13 +124,15 @@ public class FaceUtils {
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
//绘制人脸关键点
|
||||
if(rectangle.getKeyPoints() != null){
|
||||
drawLandmarks(graphics, rectangle.getKeyPoints());
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
@@ -145,7 +149,7 @@ public class FaceUtils {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
@@ -154,13 +158,15 @@ public class FaceUtils {
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
for(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
//绘制人脸关键点
|
||||
if(rectangle.getKeyPoints() != null){
|
||||
drawLandmarks(graphics, rectangle.getKeyPoints());
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
@@ -215,7 +221,7 @@ public class FaceUtils {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null; // 无效区域
|
||||
}
|
||||
return new DetectionRectangle(x, y, width, height, rectangle.score);
|
||||
return new DetectionRectangle(x, y, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,4 +315,263 @@ public class FaceUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将DetectionRectangle转换为SeetaRect
|
||||
* @param detectionRectangle
|
||||
* @return
|
||||
*/
|
||||
public static SeetaRect convertToSeetaRect(DetectionRectangle detectionRectangle){
|
||||
SeetaRect seetaRect = new SeetaRect();
|
||||
seetaRect.x = detectionRectangle.getX();
|
||||
seetaRect.y = detectionRectangle.getY();
|
||||
seetaRect.width = detectionRectangle.getWidth();
|
||||
seetaRect.height = detectionRectangle.getHeight();
|
||||
return seetaRect;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将PointList转换为SeetaPointF[]
|
||||
* @param pointList
|
||||
* @return
|
||||
*/
|
||||
public static SeetaPointF[] convertToSeetaPointF(List<Point> pointList){
|
||||
return pointList.stream()
|
||||
.map(p -> {
|
||||
SeetaPointF sp = new SeetaPointF();
|
||||
sp.x = p.getX();
|
||||
sp.y = p.getY();
|
||||
return sp;
|
||||
})
|
||||
.toArray(SeetaPointF[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将SeetaAntiSpoofing.Status转换为LivenessStatus
|
||||
* @param status
|
||||
* @return
|
||||
*/
|
||||
public static LivenessStatus convertToLivenessStatus(FaceAntiSpoofing.Status status){
|
||||
if(status == null){
|
||||
return LivenessStatus.UNKNOWN;
|
||||
}
|
||||
switch (status) {
|
||||
case REAL:
|
||||
return LivenessStatus.LIVE;
|
||||
case SPOOF:
|
||||
return LivenessStatus.NON_LIVE;
|
||||
case FUZZY:
|
||||
return LivenessStatus.UNKNOWN;
|
||||
case DETECTING:
|
||||
return LivenessStatus.DETECTING;
|
||||
default:
|
||||
return LivenessStatus.UNKNOWN; // 默认返回未知
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为GenderType
|
||||
* @param gender
|
||||
* @return
|
||||
*/
|
||||
public static GenderType convertToGenderType(GenderPredictor.GENDER gender){
|
||||
if(gender == null){
|
||||
return GenderType.UNKNOWN;
|
||||
}
|
||||
switch (gender) {
|
||||
case MALE:
|
||||
return GenderType.MALE;
|
||||
case FEMALE:
|
||||
return GenderType.FEMALE;
|
||||
default:
|
||||
return GenderType.UNKNOWN; // 默认返回未知
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为EyeStatus
|
||||
* @param eyeState
|
||||
* @return
|
||||
*/
|
||||
public static EyeStatus convertToEyeStatus(EyeStateDetector.EYE_STATE eyeState){
|
||||
if(eyeState == null){
|
||||
return EyeStatus.UNKNOWN;
|
||||
}
|
||||
switch (eyeState) {
|
||||
case EYE_OPEN:
|
||||
return EyeStatus.OPEN;
|
||||
case EYE_CLOSE:
|
||||
return EyeStatus.CLOSED;
|
||||
case EYE_RANDOM:
|
||||
return EyeStatus.NON_EYE_REGION;
|
||||
default:
|
||||
return EyeStatus.UNKNOWN; // 默认返回未知
|
||||
}
|
||||
}
|
||||
|
||||
public static DetectionResponse convertToFaceAttributeResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<FaceAttribute> faceAttributeList){
|
||||
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(faceAttributeList != null && faceAttributeList.size() > 0){
|
||||
faceInfo.setFaceAttribute(faceAttributeList.get(i));
|
||||
}
|
||||
detectionInfoList.add(new DetectionInfo(rectangle, 0, faceInfo));
|
||||
}
|
||||
return new DetectionResponse(detectionInfoList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为FaceDetectedResult
|
||||
* @param seetaResult
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, List<SeetaPointF[]> seetaPointFSList, List<LivenessStatus> livenessStatusList){
|
||||
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
|
||||
return null;
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
for(int i = 0; i < seetaResult.length; i++){
|
||||
SeetaRect rect = seetaResult[i];
|
||||
SeetaPointF[] seetaPointFS = seetaPointFSList.get(i);
|
||||
//过滤置信度
|
||||
/*if(config.getConfidenceThreshold() > 0){
|
||||
continue;
|
||||
}*/
|
||||
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height);
|
||||
List<Point> keyPoints = Arrays.stream(seetaPointFS)
|
||||
.map(p -> new Point(p.x, p.y))
|
||||
.collect(Collectors.toList());
|
||||
FaceInfo faceInfo = new FaceInfo(keyPoints);
|
||||
faceInfo.setLivenessStatus(livenessStatusList.get(i));
|
||||
DetectionInfo detectionInfo = new DetectionInfo(rectangle, 0, faceInfo);
|
||||
detectionInfoList.add(detectionInfo);
|
||||
}
|
||||
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制人脸属性
|
||||
* @param sourceImage
|
||||
* @param detectionResponse
|
||||
* @param savePath
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void drawBoxesWithFaceAttribute(BufferedImage sourceImage, DetectionResponse detectionResponse, String savePath) throws IOException {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new FaceException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new FaceException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
//drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
//绘制人脸关键点
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
}
|
||||
// 判断人脸框是否足够大
|
||||
if (rectangle.getHeight() > 60 && detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getFaceAttribute() != null) {
|
||||
StringBuilder attrText = new StringBuilder();
|
||||
FaceAttribute faceAttribute = detectionInfo.getFaceInfo().getFaceAttribute();
|
||||
if (faceAttribute.getGenderType() != null) {
|
||||
attrText.append(faceAttribute.getGenderType().name()).append(" ");
|
||||
}
|
||||
|
||||
if (faceAttribute.getAge() != null) {
|
||||
attrText.append(faceAttribute.getAge()).append("岁").append(" ");
|
||||
}
|
||||
|
||||
if (faceAttribute.getWearingMask() != null) {
|
||||
attrText.append(faceAttribute.getWearingMask() ? "戴口罩" : "未戴口罩").append(" ");
|
||||
}
|
||||
|
||||
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||
attrText.append("眼睛:")
|
||||
.append(faceAttribute.getLeftEyeStatus().name())
|
||||
.append("/")
|
||||
.append(faceAttribute.getRightEyeStatus().name())
|
||||
.append(" ");
|
||||
}
|
||||
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
if (faceAttribute.getGenderType() != null) {
|
||||
lines.add("性别: " + faceAttribute.getGenderType().name());
|
||||
}
|
||||
if (faceAttribute.getAge() != null) {
|
||||
lines.add("年龄: " + faceAttribute.getAge());
|
||||
}
|
||||
if (faceAttribute.getWearingMask() != null) {
|
||||
lines.add("口罩: " + (faceAttribute.getWearingMask() ? "是" : "否"));
|
||||
}
|
||||
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||
lines.add("眼睛: " + faceAttribute.getLeftEyeStatus().name() + "/" + faceAttribute.getRightEyeStatus().name());
|
||||
}
|
||||
if (faceAttribute.getHeadPose() != null) {
|
||||
//attrText.append("姿态:").append(faceAttribute.getHeadPose().toString());
|
||||
HeadPose pose = faceAttribute.getHeadPose();
|
||||
String pitch = pose.getPitch() != null ? String.valueOf(pose.getPitch().intValue()) : "-";
|
||||
String yaw = pose.getYaw() != null ? String.valueOf(pose.getYaw().intValue()) : "-";
|
||||
String roll = pose.getRoll() != null ? String.valueOf(pose.getRoll().intValue()) : "-";
|
||||
lines.add("姿态: P=" + pitch + " Y=" + yaw + " R=" + roll);
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
drawMultilineTextWithBackground(graphics, lines, rectangle.getX(), rectangle.getY()); // 适当偏移
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
ImageIO.write(sourceImage, "jpg", new File(savePath));
|
||||
}
|
||||
|
||||
private static void drawMultilineTextWithBackground(Graphics2D g, List<String> lines, int x, int y) {
|
||||
Font font = new Font("SansSerif", Font.PLAIN, 14);
|
||||
g.setFont(font);
|
||||
FontMetrics fm = g.getFontMetrics();
|
||||
int lineHeight = fm.getHeight();
|
||||
int maxWidth = lines.stream().mapToInt(fm::stringWidth).max().orElse(0);
|
||||
|
||||
int padding = 4;
|
||||
int boxWidth = maxWidth + padding * 2;
|
||||
int boxHeight = lineHeight * lines.size() + padding * 2;
|
||||
|
||||
// 背景矩形
|
||||
g.setColor(new Color(0, 0, 0, 128));
|
||||
g.fillRoundRect(x, y, boxWidth, boxHeight, 8, 8);
|
||||
|
||||
// 绘制每一行文字
|
||||
g.setColor(Color.WHITE);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
g.drawString(lines.get(i), x + padding, y + padding + (i + 1) * lineHeight - 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user