diff --git a/examples/face-example/src/main/java/smartai/examples/face/headpose/HeadPoseDetDemo.java b/examples/face-example/src/main/java/smartai/examples/face/headpose/HeadPoseDetDemo.java new file mode 100644 index 0000000..48d7fc1 --- /dev/null +++ b/examples/face-example/src/main/java/smartai/examples/face/headpose/HeadPoseDetDemo.java @@ -0,0 +1,208 @@ +package smartai.examples.face.headpose; + +import cn.smartjavaai.common.cv.SmartImageFactory; +import cn.smartjavaai.common.entity.DetectionInfo; +import cn.smartjavaai.common.entity.DetectionResponse; +import cn.smartjavaai.common.entity.R; +import cn.smartjavaai.common.entity.face.HeadPose; +import cn.smartjavaai.face.config.FaceDetConfig; +import cn.smartjavaai.face.config.HeadPoseConfig; +import cn.smartjavaai.face.enums.FaceDetModelEnum; +import cn.smartjavaai.face.enums.HeadPoseModelEnum; +import cn.smartjavaai.face.factory.FaceDetModelFactory; +import cn.smartjavaai.face.factory.HeadPoseModelFactory; +import cn.smartjavaai.face.model.facedect.FaceDetModel; +import cn.smartjavaai.face.model.headpose.HeadPoseModel; +import ai.djl.modality.cv.Image; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +/** + * 人脸姿态检测 demo + *

+ * 演示两种后端的使用方式: + * 1. SeetaFace6 PoseEstimator + * 2. SixDRepNet ONNX 模型 + *

+ * + * @author hyw + */ +@Slf4j +public class HeadPoseDetDemo { + + @BeforeClass + public static void beforeAll() throws IOException { + SmartImageFactory.setEngine(SmartImageFactory.Engine.OPENCV); + } + + /** + * 使用 SeetaFace6 进行人脸姿态检测(结合人脸检测) + */ + @Test + public void testSeetaFace6HeadPose() { + try { + // 需替换为实际模型存储路径 + String seetaModelPath = "C:/Users/DengWenJie/Downloads/sf3.0_models/sf3.0_models"; + + // 1. 创建人脸姿态检测模型(SeetaFace6) + HeadPoseConfig headPoseConfig = new HeadPoseConfig(); + headPoseConfig.setModelEnum(HeadPoseModelEnum.SEETA_FACE6_MODEL); + headPoseConfig.setModelPath(seetaModelPath); + HeadPoseModel headPoseModel = HeadPoseModelFactory.getInstance().getModel(headPoseConfig); + + // 2. 创建人脸检测模型 + FaceDetConfig faceDetConfig = new FaceDetConfig(); + faceDetConfig.setModelEnum(FaceDetModelEnum.SEETA_FACE6_MODEL); + faceDetConfig.setModelPath(seetaModelPath); + FaceDetModel faceDetModel = FaceDetModelFactory.getInstance().getModel(faceDetConfig); + + // 3. 检测人脸 + Image image = SmartImageFactory.getInstance().fromFile("src/main/resources/iu_1.jpg"); + R detectionResult = faceDetModel.detect(image); + if (!detectionResult.isSuccess() || detectionResult.getData() == null) { + log.info("人脸检测失败:{}", detectionResult.getMessage()); + return; + } + + DetectionResponse detectionResponse = detectionResult.getData(); + if (detectionResponse.getDetectionInfoList() == null || detectionResponse.getDetectionInfoList().isEmpty()) { + log.info("未检测到人脸"); + return; + } + + // 4. 对每张人脸进行姿态检测 + for (DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()) { + HeadPose headPose = headPoseModel.predict(image, detectionInfo.getDetectionRectangle()); + log.info("SeetaFace6 姿态检测结果:pitch={}, yaw={}, roll={}", + headPose.getPitch(), headPose.getYaw(), headPose.getRoll()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 使用 SixDRepNet ONNX 模型进行人脸姿态检测(结合人脸检测) + */ + @Test + public void testSixDRepNetHeadPose() { + try { + // SeetaFace6 模型路径(用于人脸检测) + String seetaModelPath = "C:/Users/DengWenJie/Downloads/sf3.0_models/sf3.0_models"; + // SixDRepNet ONNX 模型路径 + String sixDRepNetOnnxPath = "F:/人脸检测/未转换模型/人脸倾斜角度检测/6DRepNet/6drepnet.onnx"; + + // 1. 创建人脸姿态检测模型(SixDRepNet) + HeadPoseConfig headPoseConfig = new HeadPoseConfig(); + headPoseConfig.setModelEnum(HeadPoseModelEnum.SIX_D_REP_NET_MODEL); + headPoseConfig.setModelPath(sixDRepNetOnnxPath); + HeadPoseModel headPoseModel = HeadPoseModelFactory.getInstance().getModel(headPoseConfig); + + // 2. 创建人脸检测模型(使用 SeetaFace6 做人脸检测) + FaceDetConfig faceDetConfig = new FaceDetConfig(); + faceDetConfig.setModelEnum(FaceDetModelEnum.SEETA_FACE6_MODEL); + faceDetConfig.setModelPath(seetaModelPath); + FaceDetModel faceDetModel = FaceDetModelFactory.getInstance().getModel(faceDetConfig); + + // 3. 检测人脸 + Image image = SmartImageFactory.getInstance().fromFile("src/main/resources/iu_1.jpg"); + R detectionResult = faceDetModel.detect(image); + if (!detectionResult.isSuccess() || detectionResult.getData() == null) { + log.info("人脸检测失败:{}", detectionResult.getMessage()); + return; + } + + DetectionResponse detectionResponse = detectionResult.getData(); + if (detectionResponse.getDetectionInfoList() == null || detectionResponse.getDetectionInfoList().isEmpty()) { + log.info("未检测到人脸"); + return; + } + + // 4. 对每张人脸进行姿态检测 + for (DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()) { + HeadPose headPose = headPoseModel.predict(image, detectionInfo.getDetectionRectangle()); + log.info("SixDRepNet 姿态检测结果:pitch={}, yaw={}, roll={}", + headPose.getPitch(), headPose.getYaw(), headPose.getRoll()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 使用 SixDRepNet 对裁剪后的人脸进行姿态检测 + */ + @Test + public void testSixDRepNetCropedFace() { + try { + // SixDRepNet ONNX 模型路径 + String sixDRepNetOnnxPath = "F:/人脸检测/未转换模型/人脸倾斜角度检测/6DRepNet/6drepnet.onnx"; + + HeadPoseConfig headPoseConfig = new HeadPoseConfig(); + headPoseConfig.setModelEnum(HeadPoseModelEnum.SIX_D_REP_NET_MODEL); + headPoseConfig.setModelPath(sixDRepNetOnnxPath); + HeadPoseModel headPoseModel = HeadPoseModelFactory.getInstance().getModel(headPoseConfig); + + // 从裁剪后的人脸图片检测姿态 + HeadPose headPose = headPoseModel.predictCropedFace("src/main/resources/cropped_face.jpg"); + log.info("SixDRepNet 裁剪人脸姿态检测结果:{}", JSONObject.toJSONString(headPose)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 对比 SeetaFace6 和 SixDRepNet 两种模型的检测结果 + */ + @Test + public void testCompareModels() { + try { + String seetaModelPath = "C:/Users/DengWenJie/Downloads/sf3.0_models/sf3.0_models"; + String sixDRepNetOnnxPath = "F:/人脸检测/未转换模型/人脸倾斜角度检测/6DRepNet/6drepnet.onnx"; + + // 人脸检测 + FaceDetConfig faceDetConfig = new FaceDetConfig(); + faceDetConfig.setModelEnum(FaceDetModelEnum.SEETA_FACE6_MODEL); + faceDetConfig.setModelPath(seetaModelPath); + FaceDetModel faceDetModel = FaceDetModelFactory.getInstance().getModel(faceDetConfig); + + Image image = SmartImageFactory.getInstance().fromFile("src/main/resources/iu_1.jpg"); + R detectionResult = faceDetModel.detect(image); + if (!detectionResult.isSuccess() || detectionResult.getData() == null) { + log.info("人脸检测失败"); + return; + } + + // SeetaFace6 姿态检测 + HeadPoseConfig seetaConfig = new HeadPoseConfig(); + seetaConfig.setModelEnum(HeadPoseModelEnum.SEETA_FACE6_MODEL); + seetaConfig.setModelPath(seetaModelPath); + HeadPoseModel seetaModel = HeadPoseModelFactory.getInstance().getModel(seetaConfig); + + // SixDRepNet 姿态检测 + HeadPoseConfig sixDConfig = new HeadPoseConfig(); + sixDConfig.setModelEnum(HeadPoseModelEnum.SIX_DREP_NET_MODEL); + sixDConfig.setModelPath(sixDRepNetOnnxPath); + HeadPoseModel sixDModel = HeadPoseModelFactory.getInstance().getModel(sixDConfig); + + DetectionResponse detectionResponse = detectionResult.getData(); + if (detectionResponse.getDetectionInfoList() != null) { + for (DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()) { + HeadPose seetaPose = seetaModel.predict(image, detectionInfo.getDetectionRectangle()); + HeadPose sixDPose = sixDModel.predict(image, detectionInfo.getDetectionRectangle()); + log.info("===== 模型对比 ====="); + log.info("SeetaFace6: pitch={}, yaw={}, roll={}", seetaPose.getPitch(), seetaPose.getYaw(), seetaPose.getRoll()); + log.info("SixDRepNet: pitch={}, yaw={}, roll={}", sixDPose.getPitch(), sixDPose.getYaw(), sixDPose.getRoll()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/face/src/main/java/cn/smartjavaai/face/config/HeadPoseConfig.java b/face/src/main/java/cn/smartjavaai/face/config/HeadPoseConfig.java new file mode 100644 index 0000000..9a04d2a --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/config/HeadPoseConfig.java @@ -0,0 +1,45 @@ +package cn.smartjavaai.face.config; + +import cn.smartjavaai.common.config.ModelConfig; +import cn.smartjavaai.face.enums.HeadPoseModelEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 人脸姿态检测模型配置 + * @author hyw + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class HeadPoseConfig extends ModelConfig { + + /** + * 人脸姿态检测模型枚举 + */ + private HeadPoseModelEnum modelEnum = HeadPoseModelEnum.SEETA_FACE6_MODEL; + + /** + * 模型路径 + * SeetaFace6: 模型目录路径(包含 pose_estimation.csta) + * SixDRepNet: ONNX 模型文件路径(如 6drepnet.onnx) + */ + private String modelPath; + + + public HeadPoseConfig() { + } + + public HeadPoseConfig(HeadPoseModelEnum modelEnum) { + this.modelEnum = modelEnum; + } + + public HeadPoseConfig(HeadPoseModelEnum modelEnum, String modelPath) { + this.modelEnum = modelEnum; + this.modelPath = modelPath; + } + + public HeadPoseConfig(String modelPath) { + this.modelPath = modelPath; + } + +} diff --git a/face/src/main/java/cn/smartjavaai/face/enums/HeadPoseModelEnum.java b/face/src/main/java/cn/smartjavaai/face/enums/HeadPoseModelEnum.java new file mode 100644 index 0000000..0af7eca --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/enums/HeadPoseModelEnum.java @@ -0,0 +1,37 @@ +package cn.smartjavaai.face.enums; + +/** + * 人脸姿态检测模型枚举 + * @author hyw + * @date 2026/8/25 + */ +public enum HeadPoseModelEnum { + + SEETA_FACE6_MODEL("SeetaFace6Model"), + + SIX_DREP_NET_MODEL("SixDRepNetModel"); + + private final String modelClassName; + + HeadPoseModelEnum(String modelClassName) { + this.modelClassName = modelClassName; + } + + public String getModelClassName() { + return modelClassName; + } + + /** + * 根据名称获取枚举 (忽略大小写和下划线变体) + */ + public static HeadPoseModelEnum fromName(String name) { + String formatted = name.trim().toUpperCase().replaceAll("[-_]", ""); + for (HeadPoseModelEnum model : values()) { + if (model.name().replaceAll("_", "").equals(formatted)) { + return model; + } + } + throw new IllegalArgumentException("未知模型名称: " + name); + } + +} diff --git a/face/src/main/java/cn/smartjavaai/face/factory/HeadPoseModelFactory.java b/face/src/main/java/cn/smartjavaai/face/factory/HeadPoseModelFactory.java new file mode 100644 index 0000000..ef480b3 --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/factory/HeadPoseModelFactory.java @@ -0,0 +1,125 @@ +package cn.smartjavaai.face.factory; + +import cn.smartjavaai.common.config.Config; +import cn.smartjavaai.face.config.HeadPoseConfig; +import cn.smartjavaai.face.enums.HeadPoseModelEnum; +import cn.smartjavaai.face.exception.FaceException; +import cn.smartjavaai.face.model.headpose.HeadPoseModel; +import cn.smartjavaai.face.model.headpose.Seetaface6HeadPoseModel; +import cn.smartjavaai.face.model.headpose.SixDrepNetHeadPoseModel; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 人脸姿态检测模型工厂 + *

+ * 支持 SeetaFace6 和 SixDRepNet 两种后端实现,通过配置切换。 + *

+ * + * @author hyw + */ +@Slf4j +public class HeadPoseModelFactory { + + // 使用 volatile 和双重检查锁定来确保线程安全的单例模式 + private static volatile HeadPoseModelFactory instance; + + private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>(); + + /** + * 模型注册表 + */ + private static final Map> registry = new ConcurrentHashMap<>(); + + + public static HeadPoseModelFactory getInstance() { + if (instance == null) { + synchronized (HeadPoseModelFactory.class) { + if (instance == null) { + instance = new HeadPoseModelFactory(); + } + } + } + return instance; + } + + + /** + * 注册模型 + * @param headPoseModelEnum 模型枚举 + * @param clazz 模型实现类 + */ + private static void registerModel(HeadPoseModelEnum headPoseModelEnum, Class clazz) { + registry.put(headPoseModelEnum, clazz); + } + + + /** + * 获取模型(通过配置) + * @param config 模型配置 + * @return 人脸姿态检测模型 + */ + public HeadPoseModel getModel(HeadPoseConfig config) { + if (Objects.isNull(config) || Objects.isNull(config.getModelEnum())) { + throw new FaceException("未配置人脸姿态检测模型"); + } + return modelMap.computeIfAbsent(config.getModelEnum(), k -> { + return createHeadPoseModel(config); + }); + } + + /** + * 使用 HeadPoseConfig 创建算法 + * @param config 模型配置 + * @return 人脸姿态检测模型 + */ + private HeadPoseModel createHeadPoseModel(HeadPoseConfig config) { + Class clazz = registry.get(config.getModelEnum()); + if (clazz == null) { + throw new FaceException("Unsupported head pose model: " + config.getModelEnum()); + } + HeadPoseModel model = null; + try { + model = (HeadPoseModel) clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new FaceException(e); + } + model.loadModel(config); + model.setFromFactory(true); + return model; + } + + + // 初始化默认算法 + static { + registerModel(HeadPoseModelEnum.SEETA_FACE6_MODEL, Seetaface6HeadPoseModel.class); + registerModel(HeadPoseModelEnum.SIX_DREP_NET_MODEL, SixDrepNetHeadPoseModel.class); + log.debug("缓存目录:{}", Config.getCachePath()); + } + + /** + * 关闭所有已加载的模型 + */ + public void closeAll() { + modelMap.values().forEach(model -> { + try { + model.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + modelMap.clear(); + } + + /** + * 移除缓存的模型 + * @param modelEnum 模型枚举 + */ + public static void removeFromCache(HeadPoseModelEnum modelEnum) { + modelMap.remove(modelEnum); + } + +} diff --git a/face/src/main/java/cn/smartjavaai/face/model/headpose/HeadPoseModel.java b/face/src/main/java/cn/smartjavaai/face/model/headpose/HeadPoseModel.java new file mode 100644 index 0000000..5ec1771 --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/model/headpose/HeadPoseModel.java @@ -0,0 +1,154 @@ +package cn.smartjavaai.face.model.headpose; + +import ai.djl.modality.cv.Image; +import cn.smartjavaai.common.entity.DetectionRectangle; +import cn.smartjavaai.common.entity.DetectionResponse; +import cn.smartjavaai.common.entity.face.HeadPose; +import cn.smartjavaai.face.config.HeadPoseConfig; + +import java.awt.image.BufferedImage; +import java.util.List; + +/** + * 人脸姿态检测模型接口 + *

+ * 支持多种后端实现(SeetaFace6、SixDRepNet等),检测人脸的 pitch/yaw/roll 三个欧拉角。 + *

+ * + * @author hyw + */ +public interface HeadPoseModel extends AutoCloseable { + + /** + * 加载模型 + * @param config 模型配置 + */ + void loadModel(HeadPoseConfig config); + + /** + * 单人脸姿态检测(基于人脸框) + * @param image 原始图片 BufferedImage + * @param faceDetectionRectangle 人脸检测结果-人脸框 + * @return 姿态结果(pitch/yaw/roll,单位:度) + */ + default HeadPose predict(BufferedImage image, DetectionRectangle faceDetectionRectangle) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(基于人脸框) + * @param imagePath 图片路径 + * @param faceDetectionRectangle 人脸检测结果-人脸框 + * @return 姿态结果 + */ + default HeadPose predict(String imagePath, DetectionRectangle faceDetectionRectangle) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(基于人脸框) + * @param imageData 图片字节流 + * @param faceDetectionRectangle 人脸检测结果-人脸框 + * @return 姿态结果 + */ + default HeadPose predict(byte[] imageData, DetectionRectangle faceDetectionRectangle) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(裁剪后的人脸) + * @param croppedFace 已裁剪的人脸图片 BufferedImage + * @return 姿态结果 + */ + default HeadPose predictCropedFace(BufferedImage croppedFace) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(裁剪后的人脸) + * @param imagePath 已裁剪的人脸图片路径 + * @return 姿态结果 + */ + default HeadPose predictCropedFace(String imagePath) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(裁剪后的人脸) + * @param imageData 已裁剪的人脸图片字节流 + * @return 姿态结果 + */ + default HeadPose predictCropedFace(byte[] imageData) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 多人脸姿态检测(基于已检测结果) + * @param image 原始图片 BufferedImage + * @param faceDetectionResponse 人脸检测结果 + * @return 每张人脸的姿态结果列表 + */ + default List predict(BufferedImage image, DetectionResponse faceDetectionResponse) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 多人脸姿态检测(基于已检测结果) + * @param imagePath 图片路径 + * @param faceDetectionResponse 人脸检测结果 + * @return 每张人脸的姿态结果列表 + */ + default List predict(String imagePath, DetectionResponse faceDetectionResponse) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 多人脸姿态检测(基于已检测结果) + * @param imageData 图片字节流 + * @param faceDetectionResponse 人脸检测结果 + * @return 每张人脸的姿态结果列表 + */ + default List predict(byte[] imageData, DetectionResponse faceDetectionResponse) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + // ==================== DJL Image 变体 ==================== + + /** + * 单人脸姿态检测(基于人脸框)- DJL Image + * @param image DJL Image + * @param faceDetectionRectangle 人脸检测结果-人脸框 + * @return 姿态结果 + */ + default HeadPose predict(Image image, DetectionRectangle faceDetectionRectangle) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 单人脸姿态检测(裁剪后的人脸)- DJL Image + * @param croppedFace 已裁剪的人脸 DJL Image + * @return 姿态结果 + */ + default HeadPose predictCropedFace(Image croppedFace) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 多人脸姿态检测(基于已检测结果)- DJL Image + * @param image DJL Image + * @param faceDetectionResponse 人脸检测结果 + * @return 每张人脸的姿态结果列表 + */ + default List predict(Image image, DetectionResponse faceDetectionResponse) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + + /** + * 设置是否由工厂管理 + * @param fromFactory true 表示由工厂创建 + */ + default void setFromFactory(boolean fromFactory) { + throw new UnsupportedOperationException("默认不支持该功能"); + } + +} diff --git a/face/src/main/java/cn/smartjavaai/face/model/headpose/Seetaface6HeadPoseModel.java b/face/src/main/java/cn/smartjavaai/face/model/headpose/Seetaface6HeadPoseModel.java new file mode 100644 index 0000000..1ea1078 --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/model/headpose/Seetaface6HeadPoseModel.java @@ -0,0 +1,337 @@ +package cn.smartjavaai.face.model.headpose; + +import ai.djl.modality.cv.Image; +import cn.smartjavaai.common.entity.DetectionInfo; +import cn.smartjavaai.common.entity.DetectionRectangle; +import cn.smartjavaai.common.entity.DetectionResponse; +import cn.smartjavaai.common.entity.face.HeadPose; +import cn.smartjavaai.common.enums.DeviceEnum; +import cn.smartjavaai.common.utils.BufferedImageUtils; +import cn.smartjavaai.common.utils.FileUtils; +import cn.smartjavaai.common.utils.ImageUtils; +import cn.smartjavaai.common.utils.PoolUtils; +import cn.smartjavaai.face.config.HeadPoseConfig; +import cn.smartjavaai.face.exception.FaceException; +import cn.smartjavaai.face.factory.HeadPoseModelFactory; +import cn.smartjavaai.face.seetaface.NativeLoader; +import cn.smartjavaai.face.utils.Seetaface6Utils; +import com.seeta.pool.PoseEstimatorPool; +import com.seeta.pool.SeetaConfSetting; +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 人脸姿态检测模型 + *

+ * 基于 SeetaFace6 的 PoseEstimator 实现人脸姿态(pitch/yaw/roll)检测。 + *

+ * + * @author hyw + * @date 2026/8/25 + */ +@Slf4j +public class Seetaface6HeadPoseModel implements HeadPoseModel { + + private PoseEstimatorPool poseEstimatorPool; + + private HeadPoseConfig config; + + private boolean fromFactory = false; + + @Override + public void loadModel(HeadPoseConfig config) { + if (StringUtils.isBlank(config.getModelPath())) { + throw new FaceException("modelPath is null"); + } + this.config = config; + // 加载 SeetaFace6 依赖库 + NativeLoader.loadNativeLibraries(config.getDevice()); + log.debug("Loading seetaFace6 library successfully."); + + 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 poseEstimatorPoolSetting = new SeetaModelSetting(gpuId, poseEstimatorModelPath, device); + SeetaConfSetting poseEstimatorPoolConfSetting = new SeetaConfSetting(poseEstimatorPoolSetting); + this.poseEstimatorPool = new PoseEstimatorPool(poseEstimatorPoolConfSetting); + + int predictorPoolSize = config.getPredictorPoolSize(); + if (predictorPoolSize <= 0) { + predictorPoolSize = Runtime.getRuntime().availableProcessors(); + } + poseEstimatorPool.setMaxTotal(predictorPoolSize); + log.debug("SeetaFace6 HeadPose 模型推理器线程池最大数量: {}", predictorPoolSize); + } catch (FileNotFoundException e) { + throw new FaceException(e); + } + } + + @Override + public HeadPose predict(BufferedImage image, DetectionRectangle faceDetectionRectangle) { + if (!BufferedImageUtils.isImageValid(image)) { + throw new FaceException("图像无效"); + } + if (Objects.isNull(faceDetectionRectangle)) { + throw new FaceException("无人脸数据"); + } + PoseEstimator poseEstimator = null; + try { + poseEstimator = poseEstimatorPool.borrowObject(); + SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3); + imageData.data = BufferedImageUtils.getMatrixBGR(image); + SeetaRect seetaRect = Seetaface6Utils.convertToSeetaRect(faceDetectionRectangle); + return estimatePose(poseEstimator, imageData, seetaRect); + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + } + + @Override + public HeadPose predict(String imagePath, DetectionRectangle faceDetectionRectangle) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predict(image, faceDetectionRectangle); + } + + @Override + public HeadPose predict(byte[] imageData, DetectionRectangle faceDetectionRectangle) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predict(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + @Override + public HeadPose predictCropedFace(BufferedImage croppedFace) { + if (!BufferedImageUtils.isImageValid(croppedFace)) { + throw new FaceException("图像无效"); + } + PoseEstimator poseEstimator = null; + try { + poseEstimator = poseEstimatorPool.borrowObject(); + SeetaImageData imageData = new SeetaImageData(croppedFace.getWidth(), croppedFace.getHeight(), 3); + imageData.data = BufferedImageUtils.getMatrixBGR(croppedFace); + // 裁剪后的人脸使用全图区域作为人脸框 + SeetaRect seetaRect = new SeetaRect(); + seetaRect.x = 0; + seetaRect.y = 0; + seetaRect.width = croppedFace.getWidth(); + seetaRect.height = croppedFace.getHeight(); + return estimatePose(poseEstimator, imageData, seetaRect); + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + } + + @Override + public HeadPose predictCropedFace(String imagePath) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predictCropedFace(image); + } + + @Override + public HeadPose predictCropedFace(byte[] imageData) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predictCropedFace(ImageIO.read(new ByteArrayInputStream(imageData))); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + @Override + public List predict(BufferedImage image, DetectionResponse faceDetectionResponse) { + if (!BufferedImageUtils.isImageValid(image)) { + throw new FaceException("图像无效"); + } + if (Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()) { + throw new FaceException("无人脸数据"); + } + PoseEstimator poseEstimator = null; + List headPoseList = new ArrayList<>(); + try { + poseEstimator = poseEstimatorPool.borrowObject(); + for (DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()) { + SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3); + imageData.data = BufferedImageUtils.getMatrixBGR(image); + SeetaRect seetaRect = Seetaface6Utils.convertToSeetaRect(detectionInfo.getDetectionRectangle()); + HeadPose headPose = estimatePose(poseEstimator, imageData, seetaRect); + headPoseList.add(headPose); + } + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + return headPoseList; + } + + @Override + public List predict(String imagePath, DetectionResponse faceDetectionResponse) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predict(image, faceDetectionResponse); + } + + @Override + public List predict(byte[] imageData, DetectionResponse faceDetectionResponse) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predict(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + // ==================== DJL Image 变体 ==================== + + @Override + public HeadPose predict(Image image, DetectionRectangle faceDetectionRectangle) { + if (Objects.isNull(faceDetectionRectangle)) { + throw new FaceException("无人脸数据"); + } + PoseEstimator poseEstimator = null; + try { + poseEstimator = poseEstimatorPool.borrowObject(); + SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3); + imageData.data = ImageUtils.getMatrixBGR(image); + SeetaRect seetaRect = Seetaface6Utils.convertToSeetaRect(faceDetectionRectangle); + return estimatePose(poseEstimator, imageData, seetaRect); + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + } + + @Override + public HeadPose predictCropedFace(Image croppedFace) { + PoseEstimator poseEstimator = null; + try { + poseEstimator = poseEstimatorPool.borrowObject(); + SeetaImageData imageData = new SeetaImageData(croppedFace.getWidth(), croppedFace.getHeight(), 3); + imageData.data = ImageUtils.getMatrixBGR(croppedFace); + SeetaRect seetaRect = new SeetaRect(); + seetaRect.x = 0; + seetaRect.y = 0; + seetaRect.width = croppedFace.getWidth(); + seetaRect.height = croppedFace.getHeight(); + return estimatePose(poseEstimator, imageData, seetaRect); + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + } + + @Override + public List predict(Image image, DetectionResponse faceDetectionResponse) { + if (Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()) { + throw new FaceException("无人脸数据"); + } + PoseEstimator poseEstimator = null; + List headPoseList = new ArrayList<>(); + try { + poseEstimator = poseEstimatorPool.borrowObject(); + for (DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()) { + SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3); + imageData.data = ImageUtils.getMatrixBGR(image); + SeetaRect seetaRect = Seetaface6Utils.convertToSeetaRect(detectionInfo.getDetectionRectangle()); + HeadPose headPose = estimatePose(poseEstimator, imageData, seetaRect); + headPoseList.add(headPose); + } + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } finally { + PoolUtils.returnToPool(poseEstimatorPool, poseEstimator); + } + return headPoseList; + } + + // ==================== 内部方法 ==================== + + /** + * 执行姿态估计 + */ + private HeadPose estimatePose(PoseEstimator poseEstimator, SeetaImageData imageData, SeetaRect seetaRect) { + float[] yaw = new float[1]; // 左右转头(水平旋转) + float[] pitch = new float[1]; // 上下抬头/低头(垂直旋转) + float[] roll = new float[1]; // 头部左右倾斜(平面旋转) + poseEstimator.Estimate(imageData, seetaRect, yaw, pitch, roll); + return new HeadPose(pitch[0], yaw[0], roll[0]); + } + + @Override + public void setFromFactory(boolean fromFactory) { + this.fromFactory = fromFactory; + } + + public boolean isFromFactory() { + return fromFactory; + } + + public PoseEstimatorPool getPoseEstimatorPool() { + return poseEstimatorPool; + } + + @Override + public void close() throws Exception { + if (fromFactory) { + HeadPoseModelFactory.removeFromCache(config.getModelEnum()); + } + if (Objects.nonNull(poseEstimatorPool)) { + poseEstimatorPool.close(); + } + } +} diff --git a/face/src/main/java/cn/smartjavaai/face/model/headpose/SixDrepNetHeadPoseModel.java b/face/src/main/java/cn/smartjavaai/face/model/headpose/SixDrepNetHeadPoseModel.java new file mode 100644 index 0000000..fe2bbdb --- /dev/null +++ b/face/src/main/java/cn/smartjavaai/face/model/headpose/SixDrepNetHeadPoseModel.java @@ -0,0 +1,429 @@ +package cn.smartjavaai.face.model.headpose; + +import ai.djl.modality.cv.Image; +import ai.onnxruntime.*; +import cn.smartjavaai.common.entity.DetectionInfo; +import cn.smartjavaai.common.entity.DetectionRectangle; +import cn.smartjavaai.common.entity.DetectionResponse; +import cn.smartjavaai.common.entity.face.HeadPose; +import cn.smartjavaai.common.utils.BufferedImageUtils; +import cn.smartjavaai.common.utils.FileUtils; +import cn.smartjavaai.common.utils.ImageUtils; +import cn.smartjavaai.face.config.HeadPoseConfig; +import cn.smartjavaai.face.exception.FaceException; +import cn.smartjavaai.face.factory.HeadPoseModelFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.FloatBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * SixDRepNet ONNX 人脸姿态检测模型 + *

+ * 基于 6DRepNet 的 ONNX 模型实现人脸姿态(pitch/yaw/roll)检测。 + * 模型输出 3x3 旋转矩阵,通过后处理转换为欧拉角。 + *

+ *

+ * 预处理流程对应 Python 端的 torchvision transforms: + * Resize(224) → CenterCrop(224) → ToTensor → Normalize(ImageNet) + *

+ * + * @author hyw + * @date 2026/8/25 + */ +@Slf4j +public class SixDrepNetHeadPoseModel implements HeadPoseModel { + + // ImageNet 归一化参数 + private static final float[] MEAN = {0.485f, 0.456f, 0.406f}; + private static final float[] STD = {0.229f, 0.224f, 0.225f}; + private static final int INPUT_SIZE = 224; + + private OrtSession session; + private OrtEnvironment env; + + private HeadPoseConfig config; + private boolean fromFactory = false; + + @Override + public void loadModel(HeadPoseConfig config) { + if (StringUtils.isBlank(config.getModelPath())) { + throw new FaceException("modelPath is null"); + } + this.config = config; + + try { + env = OrtEnvironment.getEnvironment(); + OrtSession.SessionOptions opts = new OrtSession.SessionOptions(); + + // GPU 支持需要 onnxruntime-gpu 依赖 + if (Objects.nonNull(config.getDevice()) && config.getDevice() == cn.smartjavaai.common.enums.DeviceEnum.GPU) { + opts.addCUDA(config.getGpuId() >= 0 ? config.getGpuId() : 0); + log.debug("SixDRepNet 使用 GPU 模式, gpuId={}", config.getGpuId()); + } else { + log.debug("SixDRepNet 使用 CPU 模式"); + } + + session = env.createSession(config.getModelPath(), opts); + log.info("SixDRepNet ONNX 模型已加载: {}", config.getModelPath()); + } catch (OrtException e) { + throw new FaceException("加载 SixDRepNet ONNX 模型失败: " + config.getModelPath(), e); + } + } + + @Override + public HeadPose predict(BufferedImage image, DetectionRectangle faceDetectionRectangle) { + if (!BufferedImageUtils.isImageValid(image)) { + throw new FaceException("图像无效"); + } + if (Objects.isNull(faceDetectionRectangle)) { + throw new FaceException("无人脸数据"); + } + try { + // 根据人脸框裁剪人脸区域 + BufferedImage croppedFace = cropFace(image, faceDetectionRectangle); + return predictInternal(croppedFace); + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + } + + @Override + public HeadPose predict(String imagePath, DetectionRectangle faceDetectionRectangle) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predict(image, faceDetectionRectangle); + } + + @Override + public HeadPose predict(byte[] imageData, DetectionRectangle faceDetectionRectangle) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predict(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionRectangle); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + @Override + public HeadPose predictCropedFace(BufferedImage croppedFace) { + if (!BufferedImageUtils.isImageValid(croppedFace)) { + throw new FaceException("图像无效"); + } + try { + return predictInternal(croppedFace); + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + } + + @Override + public HeadPose predictCropedFace(String imagePath) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predictCropedFace(image); + } + + @Override + public HeadPose predictCropedFace(byte[] imageData) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predictCropedFace(ImageIO.read(new ByteArrayInputStream(imageData))); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + @Override + public List predict(BufferedImage image, DetectionResponse faceDetectionResponse) { + if (!BufferedImageUtils.isImageValid(image)) { + throw new FaceException("图像无效"); + } + if (Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()) { + throw new FaceException("无人脸数据"); + } + List headPoseList = new ArrayList<>(); + try { + for (DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()) { + HeadPose headPose = predict(image, detectionInfo.getDetectionRectangle()); + headPoseList.add(headPose); + } + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + return headPoseList; + } + + @Override + public List predict(String imagePath, DetectionResponse faceDetectionResponse) { + if (!FileUtils.isFileExists(imagePath)) { + throw new FaceException("图像文件不存在"); + } + BufferedImage image; + try { + image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString())); + } catch (IOException e) { + throw new FaceException("无效图片路径", e); + } + return predict(image, faceDetectionResponse); + } + + @Override + public List predict(byte[] imageData, DetectionResponse faceDetectionResponse) { + if (Objects.isNull(imageData)) { + throw new FaceException("图像无效"); + } + try { + return predict(ImageIO.read(new ByteArrayInputStream(imageData)), faceDetectionResponse); + } catch (IOException e) { + throw new FaceException("错误的图像", e); + } + } + + // ==================== DJL Image 变体 ==================== + + @Override + public HeadPose predict(Image image, DetectionRectangle faceDetectionRectangle) { + if (Objects.isNull(faceDetectionRectangle)) { + throw new FaceException("无人脸数据"); + } + try { + BufferedImage bufferedImage = ImageUtils.toBufferedImage(image); + BufferedImage croppedFace = cropFace(bufferedImage, faceDetectionRectangle); + return predictInternal(croppedFace); + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + } + + @Override + public HeadPose predictCropedFace(Image croppedFace) { + try { + BufferedImage bufferedImage = ImageUtils.toBufferedImage(croppedFace); + return predictInternal(bufferedImage); + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + } + + @Override + public List predict(Image image, DetectionResponse faceDetectionResponse) { + if (Objects.isNull(faceDetectionResponse) || Objects.isNull(faceDetectionResponse.getDetectionInfoList()) || faceDetectionResponse.getDetectionInfoList().isEmpty()) { + throw new FaceException("无人脸数据"); + } + BufferedImage bufferedImage = ImageUtils.toBufferedImage(image); + List headPoseList = new ArrayList<>(); + try { + for (DetectionInfo detectionInfo : faceDetectionResponse.getDetectionInfoList()) { + HeadPose headPose = predict(bufferedImage, detectionInfo.getDetectionRectangle()); + headPoseList.add(headPose); + } + } catch (FaceException e) { + throw e; + } catch (Exception e) { + throw new FaceException("人脸姿态检测错误", e); + } + return headPoseList; + } + + // ==================== 内部方法 ==================== + + /** + * 核心推理:对已裁剪的人脸图片进行姿态预测 + * + * @param faceImage 人脸图片(RGB 格式) + * @return float[3],分别为 pitch, yaw, roll(单位:度) + */ + private HeadPose predictInternal(BufferedImage faceImage) throws OrtException { + // 预处理 + float[] input = preprocess(faceImage); + + // 创建 ONNX 输入 tensor,形状 [1, 3, 224, 224] + long[] shape = {1, 3, INPUT_SIZE, INPUT_SIZE}; + OnnxTensor tensor = OnnxTensor.createTensor(env, FloatBuffer.wrap(input), shape); + + // 获取输入输出名称 + String inputName = session.getInputNames().iterator().next(); + String outputName = session.getOutputNames().iterator().next(); + + // 执行推理 + OrtSession.Result result = session.run(Collections.singletonMap(inputName, tensor)); + + // 获取输出:旋转矩阵 [1, 3, 3] + float[][][] rotationMatrix = (float[][][]) result.get(0).getValue(); + + // 旋转矩阵 → 欧拉角 + float[] eulerRad = rotationMatrixToEuler(rotationMatrix[0]); + + // 弧度 → 度 + float pitch = (float) Math.toDegrees(eulerRad[0]); + float yaw = (float) Math.toDegrees(eulerRad[1]); + float roll = (float) Math.toDegrees(eulerRad[2]); + + tensor.close(); + result.close(); + + return new HeadPose(pitch, yaw, roll); + } + + /** + * 预处理图像,对应 Python 的 torchvision transforms: + * Resize(224) → CenterCrop(224) → ToTensor → Normalize + * + * @param img 原始 BufferedImage(RGB 格式) + * @return float[1][3][224][224],NCHW 格式(一维展开) + */ + private float[] preprocess(BufferedImage img) { + // 1. Resize: 短边缩放到 224,保持宽高比(对应 torchvision.Resize(224)) + int origW = img.getWidth(); + int origH = img.getHeight(); + int newW, newH; + if (origW < origH) { + newW = INPUT_SIZE; + newH = (int) Math.round((double) origH / origW * INPUT_SIZE); + } else { + newH = INPUT_SIZE; + newW = (int) Math.round((double) origW / origH * INPUT_SIZE); + } + + // 使用多步缩放逼近 torchvision 的抗锯齿 Resize + java.awt.Image tmp = img.getScaledInstance(newW, newH, java.awt.Image.SCALE_AREA_AVERAGING); + BufferedImage resized = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB); + Graphics2D g = resized.createGraphics(); + g.drawImage(tmp, 0, 0, null); + g.dispose(); + + // 2. CenterCrop: 从中心裁剪 224x224 + int cropX = (newW - INPUT_SIZE) / 2; + int cropY = (newH - INPUT_SIZE) / 2; + BufferedImage cropped = resized.getSubimage(cropX, cropY, INPUT_SIZE, INPUT_SIZE); + + // 3. ToTensor + Normalize: HWC [0,255] → CHW [0,1] → 归一化 + float[] tensor = new float[3 * INPUT_SIZE * INPUT_SIZE]; + int idx = 0; + + for (int c = 0; c < 3; c++) { + for (int y = 0; y < INPUT_SIZE; y++) { + for (int x = 0; x < INPUT_SIZE; x++) { + int rgb = cropped.getRGB(x, y); + float pixel; + switch (c) { + case 0: pixel = ((rgb >> 16) & 0xFF) / 255.0f; break; // R + case 1: pixel = ((rgb >> 8) & 0xFF) / 255.0f; break; // G + case 2: pixel = ( rgb & 0xFF) / 255.0f; break; // B + default: pixel = 0; + } + // ImageNet 归一化 + tensor[idx++] = (pixel - MEAN[c]) / STD[c]; + } + } + } + + return tensor; + } + + /** + * 3x3 旋转矩阵 → 欧拉角(弧度) + * 对应 Python utils.compute_euler_angles_from_rotation_matrices + *

+ * 旋转顺序: X(pitch) → Y(yaw) → Z(roll) + * + * @param R 3x3 旋转矩阵 + * @return float[3],分别为 pitch, yaw, roll(弧度) + */ + private float[] rotationMatrixToEuler(float[][] R) { + float sy = (float) Math.sqrt(R[0][0] * R[0][0] + R[1][0] * R[1][0]); + boolean singular = sy < 1e-6f; + + float x, y, z; + if (!singular) { + x = (float) Math.atan2(R[2][1], R[2][2]); // pitch + y = (float) Math.atan2(-R[2][0], sy); // yaw + z = (float) Math.atan2(R[1][0], R[0][0]); // roll + } else { + x = (float) Math.atan2(-R[1][2], R[1][1]); // pitch + y = (float) Math.atan2(-R[2][0], sy); // yaw + z = 0; // roll + } + + return new float[]{x, y, z}; + } + + /** + * 根据人脸框从原图裁剪人脸区域 + * + * @param image 原始图片 + * @param rect 人脸检测框 + * @return 裁剪后的人脸图片 + */ + private BufferedImage cropFace(BufferedImage image, DetectionRectangle rect) { + int x = Math.max(0, rect.getX()); + int y = Math.max(0, rect.getY()); + int w = Math.min(rect.getWidth(), image.getWidth() - x); + int h = Math.min(rect.getHeight(), image.getHeight() - y); + if (w <= 0 || h <= 0) { + throw new FaceException("人脸框区域无效"); + } + return image.getSubimage(x, y, w, h); + } + + @Override + public void setFromFactory(boolean fromFactory) { + this.fromFactory = fromFactory; + } + + public boolean isFromFactory() { + return fromFactory; + } + + @Override + public void close() throws Exception { + if (fromFactory) { + HeadPoseModelFactory.removeFromCache(config.getModelEnum()); + } + if (Objects.nonNull(session)) { + session.close(); + } + if (Objects.nonNull(env)) { + env.close(); + } + } + +}