新增目标检测功能

This commit is contained in:
dengwenjie
2025-04-13 20:33:15 +08:00
parent 4eb02c6d87
commit 241b816e7f
56 changed files with 3300 additions and 1993 deletions

View File

@@ -1,87 +0,0 @@
package cn.smartjavaai.face;
import cn.smartjavaai.face.entity.FaceResult;
import java.io.IOException;
import java.io.InputStream;
/**
* 人脸识别算法
* @author dwj
*/
public abstract class AbstractFaceAlgorithm implements FaceAlgorithm{
@Override
public void loadModel(ModelConfig config) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public void loadFaceFeatureModel(ModelConfig config) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceDetectedResult detect(String imagePath) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
return null;
}
@Override
public float[] featureExtraction(String imagePath) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] featureExtraction(InputStream inputStream) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(String imagePath1, String imagePath2) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, String imagePath) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public boolean register(String key, InputStream inputStream) throws Exception {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(String imagePath) throws Exception{
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public FaceResult search(InputStream inputStream) throws Exception{
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public long removeRegister(String... keys) throws Exception{
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public long clearFace() throws Exception{
throw new UnsupportedOperationException("默认不支持该功能");
}
}

View File

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

View File

@@ -1,133 +0,0 @@
package cn.smartjavaai.face;
import ai.djl.MalformedModelException;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.translate.TranslateException;
import cn.smartjavaai.face.entity.FaceResult;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
/**
* 人脸识别算法
* @author dwj
*/
public interface FaceAlgorithm {
/**
* 加载模型
* @param config
* @throws Exception
*/
void loadModel(ModelConfig config) throws Exception; // 加载模型
/**
* 加载人脸特征提取模型
* @param config
* @throws Exception
*/
void loadFaceFeatureModel(ModelConfig config) throws Exception; // 加载模型
/**
* 人脸检测
* @param imagePath 图片路径
* @return
* @throws Exception
*/
FaceDetectedResult detect(String imagePath) throws Exception;
/**
* 人脸检测
* @param imageInputStream 图片输入流
* @return
* @throws Exception
*/
FaceDetectedResult detect(InputStream imageInputStream) throws Exception;
/**
* 特征提取
* @param imagePath 图片路径
* @return
* @throws Exception
*/
float[] featureExtraction(String imagePath) throws Exception;
/**
* 特征提取
* @param inputStream 输入流
* @return
* @throws Exception
*/
float[] featureExtraction(InputStream inputStream) throws Exception;
/**
* 计算相似度
* @param feature1 图1特征
* @param feature2 图2特征
* @return
* @throws Exception
*/
float calculSimilar(float[] feature1, float[] feature2) throws Exception;
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
* @throws Exception
*/
float featureComparison(String imagePath1, String imagePath2) throws Exception;
/**
* 特征比较
* @param inputStream1 图1输入流
* @param inputStream2 图2输入流
* @return
* @throws Exception
*/
float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception;
/**
* 注册人脸
* @param key
* @param imagePath
* @return
*/
boolean register(String key, String imagePath) throws Exception;
/**
* 注册人脸
* @param key
* @param inputStream
* @return
*/
boolean register(String key, InputStream inputStream) throws Exception;
/**
* 查询人脸
* @param imagePath
* @return
*/
FaceResult search(String imagePath) throws Exception;
/**
* 查询人脸
* @param inputStream
* @return
*/
FaceResult search(InputStream inputStream) throws Exception;
/**
* 删除已标记人脸
* @param keys
* @return
*/
long removeRegister(String... keys) throws Exception;
/**
* 清空人脸库数据
*/
long clearFace() throws Exception;
}

View File

@@ -1,122 +0,0 @@
package cn.smartjavaai.face;
import cn.smartjavaai.face.algo.FeatureExtractionAlgo;
import cn.smartjavaai.face.algo.RetinaFace;
import cn.smartjavaai.face.algo.SeetaFace6Algo;
import cn.smartjavaai.face.algo.UltraLightFastGenericFace;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 人脸算法工厂
* @author dwj
*/
public class FaceAlgorithmFactory {
/**
* 算法注册表
*/
private static final Map<String, Class<? extends FaceAlgorithm>> registry =
new ConcurrentHashMap<>();
/**
* 注册算法
* @param name
* @param clazz
*/
public static void registerAlgorithm(String name, Class<? extends FaceAlgorithm> clazz) {
registry.put(name.toLowerCase(), clazz);
}
/**
* 使用ModelConfig创建算法
* @param config
* @return
* @throws Exception
*/
public static FaceAlgorithm createFaceAlgorithm(ModelConfig config) throws Exception {
Class<?> clazz = registry.get(config.getAlgorithmName().toLowerCase());
if(clazz == null){
System.out.println("No such algorithm: " + config.getAlgorithmName().toLowerCase());
throw new IllegalArgumentException("Unsupported algorithm");
}
FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
algorithm.loadModel(config);
return algorithm;
}
/**
* 创建默认算法
* @return
* @throws Exception
*/
public static FaceAlgorithm createFaceAlgorithm() throws Exception {
// 初始化配置
ModelConfig config = new ModelConfig();
config.setAlgorithmName("retinaface");
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
return createFaceAlgorithm(config);
}
/**
* 创建轻量级算法
* @return
* @throws Exception
*/
public static FaceAlgorithm createLightFaceAlgorithm() throws Exception {
// 初始化配置
ModelConfig config = new ModelConfig();
config.setAlgorithmName("ultralightfastgenericface");
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
config.setMaxFaceCount(FaceConfig.MAX_FACE_LIMIT);
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
Class<?> clazz = registry.get(config.getAlgorithmName().toLowerCase());
if(clazz == null){
throw new IllegalArgumentException("Unsupported algorithm");
}
FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
algorithm.loadModel(config);
return algorithm;
}
/**
* 使用ModelConfig创建人脸特征提取算法
* @param config
* @return
* @throws Exception
*/
public static FaceAlgorithm createFaceFeatureAlgorithm(ModelConfig config) throws Exception {
Class<?> clazz = registry.get(config.getAlgorithmName().toLowerCase());
if(clazz == null){
throw new IllegalArgumentException("Unsupported algorithm");
}
FaceAlgorithm algorithm = (FaceAlgorithm) clazz.newInstance();
algorithm.loadFaceFeatureModel(config);
return algorithm;
}
/**
* 创建人脸特征提取算法
* @return
* @throws Exception
*/
public static FaceAlgorithm createFaceFeatureAlgorithm() throws Exception {
// 初始化配置
ModelConfig config = new ModelConfig();
config.setAlgorithmName("featureExtraction");
return createFaceFeatureAlgorithm(config);
}
// 初始化默认算法
static {
registerAlgorithm("retinaface", RetinaFace.class);
registerAlgorithm("ultralightfastgenericface", UltraLightFastGenericFace.class);
//人脸特征提取
registerAlgorithm("featureExtraction", FeatureExtractionAlgo.class);
registerAlgorithm("seetaface6", SeetaFace6Algo.class);
}
}

View File

@@ -1,40 +0,0 @@
package cn.smartjavaai.face;
import cn.smartjavaai.common.entity.Rectangle;
import java.util.List;
/**
* 人脸检测结果
* @author dwj
*/
public class FaceDetectedResult {
/**
* 置信度
*/
private List<Double> probabilities;
/**
* 人脸框
*/
private List<Rectangle> rectangles;
public List<Double> getProbabilities() {
return probabilities;
}
public void setProbabilities(List<Double> probabilities) {
this.probabilities = probabilities;
}
public List<Rectangle> getRectangles() {
return rectangles;
}
public void setRectangles(List<Rectangle> rectangles) {
this.rectangles = rectangles;
}
}

View File

@@ -0,0 +1,206 @@
package cn.smartjavaai.face;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.face.entity.FaceResult;
import java.awt.image.BufferedImage;
import java.io.InputStream;
/**
* 人脸识别算法
* @author dwj
*/
public interface FaceModel {
/**
* 加载模型
* @param config
*/
void loadModel(FaceModelConfig config); // 加载模型
/**
* 人脸检测
* @param imagePath 图片路径
* @return
*/
DetectionResponse detect(String imagePath);
/**
* 人脸检测
* @param imageInputStream 图片输入流
* @return
*/
DetectionResponse detect(InputStream imageInputStream);
/**
* 人脸检测
* @param image BufferedImage
* @return
*/
DetectionResponse detect(BufferedImage image);
/**
* 人脸检测
* @param imageData
* @return
*/
DetectionResponse detect(byte[] imageData);
/**
* 检测并绘制人脸
* @param imagePath 图片输入路径(包含文件名称)
* @param outputPath 图片输出路径(包含文件名称)
*/
void detectAndDraw(String imagePath, String outputPath);
/**
* 检测并绘制人脸
* @param sourceImage
* @return
*/
BufferedImage detectAndDraw(BufferedImage sourceImage);
/**
* 特征提取
* @param imagePath 图片路径
* @return
*/
float[] featureExtraction(String imagePath);
/**
* 特征提取
* @param inputStream 输入流
* @return
*/
float[] featureExtraction(InputStream inputStream);
/**
* 特征提取
* @param sourceImage BufferedImage图片数据
* @return
*/
float[] featureExtraction(BufferedImage sourceImage);
/**
* 特征提取
* @param imageData 图片字节流
* @return
*/
float[] featureExtraction(byte[] imageData);
/**
* 计算相似度
* @param feature1 图1特征
* @param feature2 图2特征
* @return
*/
float calculSimilar(float[] feature1, float[] feature2);
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
*/
float featureComparison(String imagePath1, String imagePath2);
/**
* 特征比较
* @param sourceImage1 图1BufferedImage
* @param sourceImag2 图2BufferedImage
* @return
*/
float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2);
/**
* 特征比较
* @param inputStream1 图1输入流
* @param inputStream2 图2输入流
* @return
*/
float featureComparison(InputStream inputStream1, InputStream inputStream2);
/**
* 特征比较
* @param imageData1
* @param imageData2
* @return
*/
float featureComparison(byte[] imageData1, byte[] imageData2);
/**
* 注册人脸
* @param key
* @param imagePath
* @return
*/
boolean register(String key, String imagePath);
/**
* 注册人脸
* @param key
* @param inputStream
* @return
*/
boolean register(String key, InputStream inputStream);
/**
* 注册人脸
* @param key
* @param sourceImage
* @return
*/
boolean register(String key, BufferedImage sourceImage);
/**
* 注册人脸
* @param key
* @param imageData
* @return
*/
boolean register(String key, byte[] imageData);
/**
* 查询人脸
* @param imagePath
* @return
*/
FaceResult search(String imagePath);
/**
* 查询人脸
* @param inputStream
* @return
*/
FaceResult search(InputStream inputStream);
/**
* 查询人脸
* @param sourceImage
* @return
*/
FaceResult search(BufferedImage sourceImage);
/**
* 查询人脸
* @param imageData
* @return
*/
FaceResult search(byte[] imageData);
/**
* 删除已标记人脸
* @param keys
* @return
*/
long removeRegister(String... keys);
/**
* 清空人脸库数据
*/
long clearFace();
}

View File

@@ -0,0 +1,45 @@
package cn.smartjavaai.face;
import cn.smartjavaai.common.enums.DeviceEnum;
import lombok.Data;
/**
* 模型配置
* @author dwj
*/
@Data
public class FaceModelConfig {
/**
* 人脸算法名称
*/
private FaceModelEnum modelEnum;
/**
* 置信度阈值
*/
private double confidenceThreshold = FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD;
/**
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
*/
private double nmsThresh = FaceConfig.NMS_THRESHOLD;
/**
* 模型路径
*/
private String modelPath;
/**
* 人脸库路径
*/
private String faceDbPath;
/**
* 设备类型
*/
private DeviceEnum device;
}

View File

@@ -0,0 +1,44 @@
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;
/**
* 人脸模型枚举
* @author dwj
* @date 2025/4/10
*/
public enum FaceModelEnum {
RETINA_FACE("RetinaFaceModel"),
ULTRA_LIGHT_FAST_GENERIC_FACE("UltraLightFastGenericFaceModel"),
FACENET_FEATURE_EXTRACTION("FeatureExtractionModel"),
SEETA_FACE6_MODEL("SeetaFace6Model");
private final String modelClassName;
FaceModelEnum(String modelClassName) {
this.modelClassName = modelClassName;
}
public String getModelClassName() {
return modelClassName;
}
/**
* 根据名称获取枚举 (忽略大小写和下划线变体)
*/
public static FaceModelEnum fromName(String name) {
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
for (FaceModelEnum model : values()) {
if (model.name().replaceAll("_", "").equals(formatted)) {
return model;
}
}
throw new IllegalArgumentException("未知模型名称: " + name);
}
}

View File

@@ -0,0 +1,127 @@
package cn.smartjavaai.face;
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 lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 人脸算法工厂
* @author dwj
*/
@Slf4j
public class FaceModelFactory {
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
private static volatile FaceModelFactory instance;
private static final ConcurrentHashMap<String, FaceModel> modelMap = new ConcurrentHashMap<>();
/**
* 算法注册表
*/
private static final Map<String, Class<? extends FaceModel>> registry =
new ConcurrentHashMap<>();
public static FaceModelFactory getInstance() {
if (instance == null) {
synchronized (FaceModelFactory.class) {
if (instance == null) {
instance = new FaceModelFactory();
}
}
}
return instance;
}
/**
* 注册算法
* @param name
* @param clazz
*/
private static void registerAlgorithm(String name, Class<? extends FaceModel> clazz) {
registry.put(name.toLowerCase(), clazz);
}
/**
* 获取模型(通过配置)
* @param config
* @return
*/
public FaceModel getModel(FaceModelConfig config) {
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
throw new FaceException("未配置人脸模型");
}
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
return createFaceModel(config);
});
}
/**
* 获取默认模型
* @return
*/
public FaceModel getModel() {
// 初始化默认配置
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.RETINA_FACE);
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
return getModel(config);
}
/**
* 使用ModelConfig创建算法
* @param config
* @return
*/
private FaceModel createFaceModel(FaceModelConfig config) {
Class<?> clazz = registry.get(config.getModelEnum().getModelClassName().toLowerCase());
if(clazz == null){
throw new FaceException("Unsupported algorithm");
}
FaceModel algorithm = null;
try {
algorithm = (FaceModel) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new FaceException(e);
}
algorithm.loadModel(config);
return algorithm;
}
/**
* 获取轻量级人脸模型
* @return
*/
public FaceModel getLightFaceModel() {
// 初始化默认配置
FaceModelConfig config = new FaceModelConfig();
config.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
config.setConfidenceThreshold(FaceConfig.DEFAULT_CONFIDENCE_THRESHOLD);
config.setNmsThresh(FaceConfig.NMS_THRESHOLD);
return getModel(config);
}
// 初始化默认算法
static {
registerAlgorithm("retinafacemodel", RetinaFaceModel.class);
registerAlgorithm("ultralightfastgenericfacemodel", UltraLightFastGenericFaceModel.class);
//人脸特征提取
registerAlgorithm("featureextractionmodel", FeatureExtractionModel.class);
registerAlgorithm("seetaface6model", SeetaFace6Model.class);
}
}

View File

@@ -1,88 +0,0 @@
package cn.smartjavaai.face;
/**
* 模型配置
* @author dwj
*/
public class ModelConfig {
/**
* 人脸算法名称
*/
private String algorithmName;
/**
* 置信度阈值
*/
private double confidenceThreshold;
/**
* 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
*/
private double nmsThresh;
/**
* 最大检测人脸数量
*/
private int maxFaceCount;
/**
* 模型路径
*/
private String modelPath;
/**
* 人脸库路径
*/
private String faceDbPath;
public String getAlgorithmName() {
return algorithmName;
}
public void setAlgorithmName(String algorithmName) {
this.algorithmName = algorithmName;
}
public double getConfidenceThreshold() {
return confidenceThreshold;
}
public void setConfidenceThreshold(double confidenceThreshold) {
this.confidenceThreshold = confidenceThreshold;
}
public double getNmsThresh() {
return nmsThresh;
}
public void setNmsThresh(double nmsThresh) {
this.nmsThresh = nmsThresh;
}
public int getMaxFaceCount() {
return maxFaceCount;
}
public void setMaxFaceCount(int maxFaceCount) {
this.maxFaceCount = maxFaceCount;
}
public String getModelPath() {
return modelPath;
}
public void setModelPath(String modelPath) {
this.modelPath = modelPath;
}
public String getFaceDbPath() {
return faceDbPath;
}
public void setFaceDbPath(String faceDbPath) {
this.faceDbPath = faceDbPath;
}
}

View File

@@ -1,162 +0,0 @@
package cn.smartjavaai.face.algo;
import ai.djl.Device;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.translator.ImageFeatureExtractorFactory;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;
import cn.smartjavaai.common.entity.Point;
import cn.smartjavaai.common.entity.Rectangle;
import cn.smartjavaai.face.AbstractFaceAlgorithm;
import cn.smartjavaai.face.FaceDetectedResult;
import cn.smartjavaai.face.FaceDetectionTranslator;
import cn.smartjavaai.face.ModelConfig;
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
import org.apache.commons.lang3.StringUtils;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author dwj
*/
public class FeatureExtractionAlgo extends AbstractFaceAlgorithm {
private Criteria<Image, float[]> faceFeatureCriteria;
private Predictor<Image, float[]> predictor;
private ZooModel<Image, float[]> model;
public static final List<Float> mean =
Arrays.asList(
127.5f / 255.0f,
127.5f / 255.0f,
127.5f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f);
/**
* 加载人脸特征提取模型
* @param config
* @throws Exception
*/
@Override
public void loadFaceFeatureModel(ModelConfig config) throws Exception {
String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
faceFeatureCriteria =
Criteria.builder()
.setTypes(Image.class, float[].class)
.optModelName("face_feature") // specify model file prefix
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
"https://resources.djl.ai/test-models/pytorch/face_feature.zip")
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optTranslator(new FaceFeatureTranslator())
.optArgument("normalize", normalize)
.optEngine("PyTorch") // Use PyTorch engine
.optProgress(new ProgressBar())
.build();
model = faceFeatureCriteria.loadModel();
predictor = model.newPredictor();
}
/**
* 特征提取
* @param imagePath 图片路径
* @return
* @throws Exception
*/
@Override
public float[] featureExtraction(String imagePath) throws Exception {
Path imageFile = Paths.get(imagePath);
Image img = ImageFactory.getInstance().fromFile(imageFile);
img.getWrappedImage();
return predictor.predict(img);
}
/**
* 特征提取
* @param inputStream 输入流
* @return
* @throws Exception
*/
@Override
public float[] featureExtraction(InputStream inputStream) throws Exception {
Image img = ImageFactory.getInstance().fromInputStream(inputStream);
img.getWrappedImage();
return predictor.predict(img);
}
/**
* 计算相似度
* @param feature1 图1特征
* @param feature2 图2特征
* @return
* @throws Exception
*/
@Override
public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
float ret = 0.0f;
float mod1 = 0.0f;
float mod2 = 0.0f;
int length = feature1.length;
for (int i = 0; i < length; ++i) {
ret += feature1[i] * feature2[i];
mod1 += feature1[i] * feature1[i];
mod2 += feature2[i] * feature2[i];
}
return (float) ((ret / Math.sqrt(mod1) / Math.sqrt(mod2) + 1) / 2.0f);
}
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
* @throws Exception
*/
@Override
public float featureComparison(String imagePath1, String imagePath2) throws Exception {
float[] feature1 = featureExtraction(imagePath1);
float[] feature2 = featureExtraction(imagePath2);
return calculSimilar(feature1, feature2);
}
/**
* 特征比较
* @param inputStream1 图1输入流
* @param inputStream2 图2输入流
* @return
* @throws Exception
*/
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
float[] feature1 = featureExtraction(inputStream1);
float[] feature2 = featureExtraction(inputStream2);
return calculSimilar(feature1, feature2);
}
/*@Override
public float[] recognize(FaceRegion region) {
return new float[0];
}*/
/*@Override
public void loadModel(ModelConfig config) throws Exception {
}*/
}

View File

@@ -1,144 +0,0 @@
package cn.smartjavaai.face.algo;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.translator.ImageFeatureExtractorFactory;
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 ai.djl.translate.TranslateException;
import cn.smartjavaai.common.entity.Point;
import cn.smartjavaai.common.entity.Rectangle;
import cn.smartjavaai.face.*;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* RetinaFace实现
* @author dwj
*/
public class RetinaFace extends AbstractFaceAlgorithm {
private Criteria<Image, DetectedObjects> criteria;
private Criteria<Image, float[]> faceFeatureCriteria;
private Predictor<Image, DetectedObjects> predictor;
private ZooModel<Image, DetectedObjects> model;
/**
* 特征图层的基础缩放比例
*/
public static final int[][] scales = {{16, 32}, {64, 128}, {256, 512}};
/**
* 特征图相对于原图的采样步长
*/
public static final int[] steps = {8, 16, 32};
/**
* 缩放系数
*/
public static final double[] variance = {0.1f, 0.2f};
/**
* 加载模型
* @param config
*/
@Override
public void loadModel(ModelConfig config) throws ModelNotFoundException, MalformedModelException, IOException {
FaceDetectionTranslator translator =
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, config.getMaxFaceCount(), scales, steps);
criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/retinaface.zip")
// Load model from local file, e.g:
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optModelName(StringUtils.isNotBlank(config.getAlgorithmName()) ? config.getAlgorithmName() : "retinaface") // specify model file prefix
.optTranslator(translator)
.optProgress(new ProgressBar())
.optEngine("PyTorch") // Use PyTorch engine
.build();
model = criteria.loadModel();
predictor = model.newPredictor();
}
/**
* 检测人脸
* @param imagePath 图片路径
* @return
* @throws Exception
*/
@Override
public FaceDetectedResult detect(String imagePath) throws Exception{
Path facePath = Paths.get(imagePath);
Image img = ImageFactory.getInstance().fromFile(facePath);
DetectedObjects detection = predictor.predict(img);
return convertToFaceDetectedResult(detection,img);
}
/**
* 检测人脸
* @param imageInputStream 图片流
* @return
* @throws Exception
*/
@Override
public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
DetectedObjects detection = predictor.predict(img);
return convertToFaceDetectedResult(detection,img);
}
/**
* 转换为FaceDetectedResult
* @param detection
* @param img
* @return
*/
private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
List<Double> probabilities = new ArrayList<>(detection.getProbabilities());
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
List<Rectangle> RectangleList = detectedObjectList.parallelStream()
.map(obj -> {
Rectangle rectangle = new Rectangle();
List<Point> pointList = new ArrayList<>();
ai.djl.modality.cv.output.Rectangle rectangleDjl = obj.getBoundingBox().getBounds();
int x = (int)(rectangleDjl.getX() * (double)img.getWidth());
int y = (int)(rectangleDjl.getY() * (double)img.getHeight());
int width = (int)(rectangleDjl.getWidth() * (double)img.getWidth());
int height = (int)(rectangleDjl.getHeight() * (double)img.getHeight());
pointList.add(new Point(x,y));
pointList.add(new Point(x + width,y));
pointList.add(new Point(x,y + height));
pointList.add(new Point(x + width,y + height));
rectangle.setPointList(pointList);
rectangle.setHeight(height);
rectangle.setWidth(width);
return rectangle;
})
.collect(Collectors.toList());
faceDetectedResult.setProbabilities(probabilities);
faceDetectedResult.setRectangles(RectangleList);
return faceDetectedResult;
}
}

View File

@@ -1,319 +0,0 @@
package cn.smartjavaai.face.algo;
import cn.smartjavaai.common.entity.Point;
import cn.smartjavaai.common.entity.Rectangle;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.AbstractFaceAlgorithm;
import cn.smartjavaai.face.FaceDetectedResult;
import cn.smartjavaai.face.ModelConfig;
import cn.smartjavaai.face.dao.FaceDao;
import cn.smartjavaai.face.entity.FaceData;
import cn.smartjavaai.face.entity.FaceResult;
import com.seetaface.NativeLoader;
import com.seetaface.SeetaFace6JNI;
import com.seetaface.model.RecognizeResult;
import com.seetaface.model.SeetaImageData;
import com.seetaface.model.SeetaRect;
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.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* SeetaFace6 人脸算法
* @author dwj
*/
@Slf4j
public class SeetaFace6Algo extends AbstractFaceAlgorithm {
private ModelConfig config;
@Override
public void loadModel(ModelConfig config) throws Exception {
this.config = config;
if (NativeLoader.seetaFace6SDK == null) {
synchronized (SeetaFace6JNI.class) {
if(StringUtils.isBlank(config.getModelPath())){
throw new Exception("modelPath is null");
}
//加载依赖库
NativeLoader.loadNativeLibraries(config.getModelPath());
log.info("Loading seetaFace6 library successfully.");
NativeLoader.seetaFace6SDK = new SeetaFace6JNI();
//加载模型
boolean isSuccess = NativeLoader.seetaFace6SDK.initModel(config.getModelPath());
if(!isSuccess){
throw new Exception("seetaFace6模型初始化失败," + config.getModelPath());
}
log.info("Load seetaFace6 model success!");
new Thread(new Runnable() {
public void run() {
try {
log.info("start load faceDb...");
loadFaceDb();
log.info("Load faceDb success!");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
@Override
public FaceDetectedResult detect(String imagePath) throws Exception {
// 将图片路径转换为 BufferedImage
BufferedImage image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
return convertToFaceDetectedResult(seetaResult);
}
@Override
public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
BufferedImage image = ImageIO.read(imageInputStream);
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
return convertToFaceDetectedResult(seetaResult);
}
@Override
public float[] featureExtraction(String imagePath) throws Exception {
BufferedImage image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
}
@Override
public float[] featureExtraction(InputStream inputStream) throws Exception {
BufferedImage image = ImageIO.read(inputStream);
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
}
@Override
public float calculSimilar(float[] feature1, float[] feature2) throws Exception {
return NativeLoader.seetaFace6SDK.calculateSimilarity(feature1, feature2);
}
@Override
public float featureComparison(String imagePath1, String imagePath2) throws Exception {
return featureComparison(new FileInputStream(Paths.get(imagePath1).toAbsolutePath().toString()),
new FileInputStream(Paths.get(imagePath2).toAbsolutePath().toString()));
}
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) throws Exception {
BufferedImage image1 = ImageIO.read(inputStream1);
BufferedImage image2 = ImageIO.read(inputStream2);
SeetaImageData imageData1 = new SeetaImageData(image1.getWidth(), image1.getHeight(), 3);
imageData1.data = ImageUtils.getMatrixBGR(image1);
SeetaImageData imageData2 = new SeetaImageData(image2.getWidth(), image2.getHeight(), 3);
imageData2.data = ImageUtils.getMatrixBGR(image2);
//裁剪
byte[][] cropImg1 = NativeLoader.seetaFace6SDK.crop(imageData1);
byte[][] cropImg2 = NativeLoader.seetaFace6SDK.crop(imageData2);
if(cropImg1 == null || cropImg1.length == 0){
throw new Exception("未发现人脸");
}
if(cropImg2 == null || cropImg2.length == 0){
throw new Exception("未发现人脸");
}
BufferedImage cropImage1 = ImageUtils.bgrToBufferedImage(cropImg1[0], 256, 256);
BufferedImage cropImage2 = ImageUtils.bgrToBufferedImage(cropImg2[0], 256, 256);
SeetaImageData cropImageData1 = new SeetaImageData(cropImage1.getWidth(), cropImage1.getHeight(), 3);
cropImageData1.data = ImageUtils.getMatrixBGR(cropImage1);
SeetaImageData cropImageData2 = new SeetaImageData(cropImage2.getWidth(), cropImage2.getHeight(), 3);
cropImageData2.data = ImageUtils.getMatrixBGR(cropImage2);
return NativeLoader.seetaFace6SDK.compare(cropImageData1, cropImageData2);
}
/**
* 转换为FaceDetectedResult
* @param seetaResult
* @return
*/
private FaceDetectedResult convertToFaceDetectedResult(SeetaRect[] seetaResult){
FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
List<Rectangle> RectangleList = new ArrayList<Rectangle>();
List<Double> probabilities = new ArrayList<Double>();
if(seetaResult != null && seetaResult.length > 0){
for(SeetaRect rect : seetaResult){
Rectangle rectangle = new Rectangle();
List<Point> pointList = new ArrayList<>();
pointList.add(new Point(rect.x,rect.y));
pointList.add(new Point(rect.x + rect.width,rect.y));
pointList.add(new Point(rect.x,rect.y + rect.height));
pointList.add(new Point(rect.x + rect.width,rect.y + rect.height));
rectangle.setPointList(pointList);
rectangle.setHeight(rect.height);
rectangle.setWidth(rect.width);
RectangleList.add(rectangle);
probabilities.add(new Double(rect.score));
}
}
faceDetectedResult.setProbabilities(probabilities);
faceDetectedResult.setRectangles(RectangleList);
return faceDetectedResult;
}
@Override
public boolean register(String key, String imagePath) throws Exception {
return register(key, new FileInputStream(Paths.get(imagePath).toAbsolutePath().toString()));
}
@Override
public boolean register(String key, InputStream inputStream) throws Exception {
if(!checkFaceDb()){
throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
//裁剪人脸
BufferedImage image = ImageIO.read(inputStream);
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
byte[][] bytes = NativeLoader.seetaFace6SDK.crop(imageData);
if (bytes == null || bytes.length == 0) {
log.info("register face fail: key={}, error=no valid face", key);
return false;
}
long index = NativeLoader.seetaFace6SDK.registerCroppedFace(bytes[0]);
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
//持久化到sqlite数据库
FaceData face = new FaceData();
face.setKey(key);
face.setIndex(index);
face.setImgData(bytes[0]);
new FaceDao(config.getFaceDbPath()).save(face);
return true;
}
public boolean register(String key, FaceData faceData) throws Exception {
long index = NativeLoader.seetaFace6SDK.registerCroppedFace(faceData.getImgData());
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
int rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
return rows > 0;
}
@Override
public FaceResult search(String imagePath) throws Exception {
return search(new FileInputStream(Paths.get(imagePath).toAbsolutePath().toString()));
}
@Override
public FaceResult search(InputStream inputStream) throws Exception{
if(!checkFaceDb()){
throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
BufferedImage image = ImageIO.read(inputStream);
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
RecognizeResult recognizeResult = NativeLoader.seetaFace6SDK.query(imageData);
return searchFaceDb(recognizeResult);
}
@Override
public long removeRegister(String... keys) throws Exception {
if(!checkFaceDb()){
throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
List<Long> list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
if (list == null) {
return 0;
}
long[] array = list.stream().mapToLong(Long::longValue).toArray();
long rows = NativeLoader.seetaFace6SDK.delete(array);
new FaceDao(config.getFaceDbPath()).deleteFace(keys);
return rows;
}
@Override
public long clearFace() throws Exception{
if(!checkFaceDb()){
throw new Exception("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
long rows = NativeLoader.seetaFace6SDK.delete(new long[]{-1});
new FaceDao(config.getFaceDbPath()).deleteAll();
return rows;
}
/**
* 检查是否存在人脸库
* @return
*/
private boolean checkFaceDb(){
if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
File file = new File(config.getFaceDbPath());
return file.exists() && file.isFile();
}
return false;
}
private FaceResult searchFaceDb(RecognizeResult recognizeResult) throws SQLException, ClassNotFoundException {
if(recognizeResult != null && recognizeResult.index >= 0){
String key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(recognizeResult.index);
return new FaceResult(key, recognizeResult.similar);
}
return null;
}
/**
* 加载人脸库
* @throws SQLException
* @throws ClassNotFoundException
*/
private void loadFaceDb() throws SQLException, ClassNotFoundException {
if(!checkFaceDb()){
log.info("未配置人脸库");
return;
}
//分页查询人脸库
int pageNo = 0, pageSize = 100;
while (true) {
List<FaceData> list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
if (list == null) {
break;
}
list.forEach(face -> {
try {
register(face.getKey(), face);
} catch (Exception e) {
e.printStackTrace();
}
});
if (list.size() < pageSize) {
break;
}
pageNo++;
}
}
}

View File

@@ -1,135 +0,0 @@
package cn.smartjavaai.face.algo;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.translator.ImageFeatureExtractorFactory;
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.Point;
import cn.smartjavaai.common.entity.Rectangle;
import cn.smartjavaai.face.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author dwj
*/
public class UltraLightFastGenericFace extends AbstractFaceAlgorithm {
private Criteria<Image, DetectedObjects> criteria;
/**
* 特征图层的基础缩放比例
*/
private static final int[][] scales = {{10, 16, 24}, {32, 48}, {64, 96}, {128, 192, 256}};
/**
* 特征图相对于原图的采样步长
*/
private static final int[] steps = {8, 16, 32, 64};
/**
* 缩放系数
*/
private static final double[] variance = {0.1f, 0.2f};
private Predictor<Image, DetectedObjects> predictor;
private ZooModel<Image, DetectedObjects> model;
/**
* 加载模型
* @param config
*/
@Override
public void loadModel(ModelConfig config) throws ModelNotFoundException, MalformedModelException, IOException {
FaceDetectionTranslator translator =
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, config.getMaxFaceCount(), scales, steps);
criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls("https://resources.djl.ai/test-models/pytorch/ultranet.zip")
.optTranslator(translator)
.optProgress(new ProgressBar())
.optEngine("PyTorch") // Use PyTorch engine
.build();
model = criteria.loadModel();
predictor = model.newPredictor();
}
/**
* 检测人脸
* @param imagePath 图片路径
* @return
* @throws Exception
*/
@Override
public FaceDetectedResult detect(String imagePath) throws Exception{
Path facePath = Paths.get(imagePath);
Image img = ImageFactory.getInstance().fromFile(facePath);
DetectedObjects detection = predictor.predict(img);
return convertToFaceDetectedResult(detection,img);
}
/**
* 检测人脸
* @param imageInputStream 图片输入流
* @return
* @throws Exception
*/
@Override
public FaceDetectedResult detect(InputStream imageInputStream) throws Exception {
Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
DetectedObjects detection = predictor.predict(img);
return convertToFaceDetectedResult(detection,img);
}
/**
* 转换检测结果
* @param detection
* @param img
* @return
*/
private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
List<Double> probabilities = new ArrayList<>(detection.getProbabilities());
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
List<Rectangle> RectangleList = detectedObjectList.parallelStream()
.map(obj -> {
Rectangle rectangle = new Rectangle();
List<Point> pointList = new ArrayList<>();
ai.djl.modality.cv.output.Rectangle rectangleDjl = obj.getBoundingBox().getBounds();
int x = (int)(rectangleDjl.getX() * (double)img.getWidth());
int y = (int)(rectangleDjl.getY() * (double)img.getHeight());
int width = (int)(rectangleDjl.getWidth() * (double)img.getWidth());
int height = (int)(rectangleDjl.getHeight() * (double)img.getHeight());
pointList.add(new Point(x,y));
pointList.add(new Point(x + width,y));
pointList.add(new Point(x,y + height));
pointList.add(new Point(x + width,y + height));
rectangle.setPointList(pointList);
rectangle.setHeight(height);
rectangle.setWidth(width);
return rectangle;
})
.collect(Collectors.toList());
faceDetectedResult.setProbabilities(probabilities);
faceDetectedResult.setRectangles(RectangleList);
return faceDetectedResult;
}
}

View File

@@ -0,0 +1,30 @@
package cn.smartjavaai.face.exception;
/**
* 人脸检测异常
* @author dwj
* @date 2025/4/4
*/
public class FaceException extends RuntimeException{
public FaceException() {
super();
}
public FaceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public FaceException(String message, Throwable cause) {
super(message, cause);
}
public FaceException(String message) {
super(message);
}
public FaceException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,34 @@
package cn.smartjavaai.face.factory;
import ai.djl.inference.Predictor;
import ai.djl.repository.zoo.ZooModel;
import cn.smartjavaai.face.model.SeetaFace6Model;
import com.seetaface.SeetaFace6JNI;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
/**
* Predictor 工厂类
* @author dwj
* @date 2025/4/8
*/
public class SeetaFace6Factory extends BasePooledObjectFactory<SeetaFace6JNI> {
@Override
public SeetaFace6JNI create() {
return new SeetaFace6JNI();
}
@Override
public PooledObject<SeetaFace6JNI> wrap(SeetaFace6JNI obj) {
return new DefaultPooledObject<>(obj);
}
@Override
public void destroyObject(PooledObject<SeetaFace6JNI> p) {
//p.getObject().dispose(); // 如果需要释放 native 资源
SeetaFace6JNI object = p.getObject();
object = null;
}
}

View File

@@ -0,0 +1,259 @@
package cn.smartjavaai.face.model;
import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.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.enums.DeviceEnum;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.AbstractFaceModel;
import cn.smartjavaai.face.FaceModelConfig;
import cn.smartjavaai.face.exception.FaceException;
import cn.smartjavaai.face.translator.FaceFeatureTranslator;
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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author dwj
*/
@Slf4j
public class FeatureExtractionModel extends AbstractFaceModel implements AutoCloseable{
private ObjectPool<Predictor<Image, float[]>> predictorPool;
private ZooModel<Image, float[]> model;
public static final List<Float> mean =
Arrays.asList(
127.5f / 255.0f,
127.5f / 255.0f,
127.5f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f,
128.0f / 255.0f);
/**
* 加载人脸特征提取模型
* @param config
*/
@Override
public void loadModel(FaceModelConfig config) {
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
Criteria<Image, float[]> faceFeatureCriteria =
Criteria.builder()
.setTypes(Image.class, float[].class)
.optModelName("face_feature") // specify model file prefix
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null :
"https://resources.djl.ai/test-models/pytorch/face_feature.zip")
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optTranslator(new FaceFeatureTranslator())
.optArgument("normalize", normalize)
.optDevice(device)
.optEngine("PyTorch") // Use PyTorch engine
.optProgress(new ProgressBar())
.build();
try {
model = faceFeatureCriteria.loadModel();
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
log.info("当前设备: " + model.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new FaceException("模型加载失败", e);
}
}
private float[] featureExtraction(Image image){
image.getWrappedImage();
Predictor<Image, float[]> predictor = null;
try {
predictor = predictorPool.borrowObject();
return predictor.predict(image);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
log.info("释放资源");
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
/**
* 特征提取
* @param imagePath 图片路径
* @return
*/
@Override
public float[] featureExtraction(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
Image img = null;
try {
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
} catch (IOException e) {
throw new FaceException("无效图片", e);
}
return featureExtraction(img);
}
/**
* 特征提取
* @param inputStream 输入流
* @return
*/
@Override
public float[] featureExtraction(InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
Image img = null;
try {
img = ImageFactory.getInstance().fromInputStream(inputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return featureExtraction(img);
}
@Override
public float[] featureExtraction(BufferedImage sourceImage) {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
Image img = ImageFactory.getInstance().fromImage(sourceImage);
return featureExtraction(img);
}
@Override
public float[] featureExtraction(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return featureExtraction(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("无效图片字节流", e);
}
}
/**
* 计算相似度
* @param feature1 图1特征
* @param feature2 图2特征
* @return
*/
@Override
public float calculSimilar(float[] feature1, float[] feature2) {
float ret = 0.0f;
float mod1 = 0.0f;
float mod2 = 0.0f;
int length = feature1.length;
for (int i = 0; i < length; ++i) {
ret += feature1[i] * feature2[i];
mod1 += feature1[i] * feature1[i];
mod2 += feature2[i] * feature2[i];
}
return (float) ((ret / Math.sqrt(mod1) / Math.sqrt(mod2) + 1) / 2.0f);
}
/**
* 特征比较
* @param imagePath1 图1路径
* @param imagePath2 图2路径
* @return
*/
@Override
public float featureComparison(String imagePath1, String imagePath2) {
if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
throw new FaceException("图像文件不存在");
}
float[] feature1 = featureExtraction(imagePath1);
float[] feature2 = featureExtraction(imagePath2);
return calculSimilar(feature1, feature2);
}
/**
* 特征比较
* @param inputStream1 图1输入流
* @param inputStream2 图2输入流
* @return
*/
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
if(Objects.isNull(inputStream1) || Objects.isNull(inputStream2)){
throw new FaceException("图像输入流无效");
}
float[] feature1 = featureExtraction(inputStream1);
float[] feature2 = featureExtraction(inputStream2);
return calculSimilar(feature1, feature2);
}
@Override
public float featureComparison(BufferedImage sourceImage1, BufferedImage sourceImag2) {
if(!ImageUtils.isImageValid(sourceImage1) || !ImageUtils.isImageValid(sourceImag2)){
throw new FaceException("图像无效");
}
float[] feature1 = featureExtraction(sourceImage1);
float[] feature2 = featureExtraction(sourceImag2);
return calculSimilar(feature1, feature2);
}
@Override
public float featureComparison(byte[] imageData1, byte[] imageData2) {
if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
throw new FaceException("图像无效");
}
float[] feature1 = featureExtraction(imageData1);
float[] feature2 = featureExtraction(imageData2);
return calculSimilar(feature1, feature2);
}
@Override
public void close() {
if (predictorPool != null) {
predictorPool.close();
}
}
}

View File

@@ -0,0 +1,243 @@
package cn.smartjavaai.face.model;
import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
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.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.exception.FaceException;
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
import cn.smartjavaai.face.utils.FaceUtils;
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.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Objects;
/**
* RetinaFace实现
* @author dwj
*/
@Slf4j
public class RetinaFaceModel extends AbstractFaceModel implements AutoCloseable{
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
private ZooModel<Image, DetectedObjects> model;
/**
* 特征图层的基础缩放比例
*/
public static final int[][] scales = {{16, 32}, {64, 128}, {256, 512}};
/**
* 特征图相对于原图的采样步长
*/
public static final int[] steps = {8, 16, 32};
/**
* 缩放系数
*/
public static final double[] variance = {0.1f, 0.2f};
/**
* 加载模型
* @param config
*/
@Override
public void loadModel(FaceModelConfig config){
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
FaceDetectionTranslator translator =
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/retinaface.zip")
// Load model from local file, e.g:
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optModelName("retinaface") // specify model file prefix
.optTranslator(translator)
.optDevice(device)
.optProgress(new ProgressBar())
.optEngine("PyTorch") // Use PyTorch engine
.build();
try {
model = criteria.loadModel();
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
log.info("当前设备: " + model.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new FaceException("模型加载失败", e);
}
}
/**
* 检测人脸
* @param imagePath 图片路径
* @return
* @throws Exception
*/
@Override
public DetectionResponse detect(String imagePath){
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
Image img = null;
try {
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
} catch (IOException e) {
throw new FaceException("无效的图片", e);
}
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
}
/**
* 检测人脸
* @param imageInputStream 图片流
* @return
* @throws Exception
*/
@Override
public DetectionResponse detect(InputStream imageInputStream){
if(Objects.isNull(imageInputStream)){
throw new FaceException("图像输入流无效");
}
try {
Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
}
@Override
public DetectionResponse detect(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
Image img = ImageFactory.getInstance().fromImage(image);
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
}
@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 void detectAndDraw(String imagePath, String outputPath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
try {
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
DetectedObjects detectedObjects = detect(img);
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
throw new FaceException("未识别到人脸");
}
img.drawBoundingBoxes(detectedObjects);
Path output = Paths.get(outputPath);
log.info("Saving to {}", output.toAbsolutePath().toString());
img.save(Files.newOutputStream(output), "png");
} catch (IOException e) {
throw new FaceException(e);
}
}
@Override
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
Image img = ImageFactory.getInstance().fromImage(sourceImage);
DetectedObjects detectedObjects = detect(img);
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
throw new FaceException("未识别到人脸");
}
img.drawBoundingBoxes(detectedObjects);
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 调用 save 方法将 Image 写入字节流
img.save(outputStream, "png");
// 将字节流转换为 BufferedImage
byte[] imageBytes = outputStream.toByteArray();
return ImageIO.read(new ByteArrayInputStream(imageBytes));
} catch (IOException e) {
throw new FaceException("导出图片失败", e);
}
}
/**
* 人脸检测
* @param image
* @return
*/
private DetectedObjects detect(Image image){
Predictor<Image, DetectedObjects> predictor = null;
try {
predictor = predictorPool.borrowObject();
return predictor.predict(image);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
log.info("释放资源");
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
@Override
public void close() {
if (predictorPool != null) {
predictorPool.close();
}
}
}

View File

@@ -0,0 +1,585 @@
package cn.smartjavaai.face.model;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.AbstractFaceModel;
import cn.smartjavaai.face.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.FaceUtils;
import com.seetaface.NativeLoader;
import com.seetaface.SeetaFace6JNI;
import com.seetaface.model.RecognizeResult;
import com.seetaface.model.SeetaImageData;
import com.seetaface.model.SeetaRect;
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.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
/**
* SeetaFace6 人脸算法
* @author dwj
*/
@SuppressWarnings("AliMissingOverrideAnnotation")
@Slf4j
public class SeetaFace6Model extends AbstractFaceModel {
private FaceModelConfig config;
private static final Object lock = new Object(); // 全局锁
@Override
public void loadModel(FaceModelConfig config) {
this.config = config;
if (NativeLoader.seetaFace6SDK == null) {
synchronized (lock) {
if(StringUtils.isBlank(config.getModelPath())){
throw new FaceException("modelPath is null");
}
//加载依赖库
NativeLoader.loadNativeLibraries(config.getModelPath());
log.info("Loading seetaFace6 library successfully.");
NativeLoader.seetaFace6SDK = new SeetaFace6JNI();
//加载模型
boolean isSuccess = NativeLoader.seetaFace6SDK.initModel(config.getModelPath());
if(!isSuccess){
throw new FaceException("seetaFace6模型初始化失败," + config.getModelPath());
}
log.info("Load seetaFace6 model success!");
new Thread(new Runnable() {
public void run() {
try {
log.info("start load faceDb...");
loadFaceDb();
log.info("Load faceDb success!");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
@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(InputStream imageInputStream) {
if(Objects.isNull(imageInputStream)){
throw new FaceException("图像输入流无效");
}
BufferedImage image = null;
try {
image = ImageIO.read(imageInputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return detect(image);
}
@Override
public DetectionResponse detect(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
synchronized (lock) {
SeetaRect[] seetaResult = NativeLoader.seetaFace6SDK.detect(imageData);
return FaceUtils.convertToDetectionResponse(seetaResult, config);
}
}
@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 void detectAndDraw(String imagePath, String outputPath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
try {
//创建保存路径
Path imageOutputPath = Paths.get(outputPath);
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
DetectionResponse result = detect(image);
if(Objects.isNull(result) || Objects.isNull(result.getRectangleList()) || result.getRectangleList().isEmpty()){
throw new FaceException("未识别到人脸");
}
//绘制人脸框
FaceUtils.drawBoundingBoxes(image, result, imageOutputPath.toAbsolutePath().toString());
} catch (IOException e) {
throw new FaceException(e);
}
}
@Override
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
DetectionResponse detectedObjects = detect(sourceImage);
if(Objects.isNull(detectedObjects) || Objects.isNull(detectedObjects.getRectangleList()) || detectedObjects.getRectangleList().isEmpty()){
throw new FaceException("未识别到人脸");
}
//绘制人脸框
try {
return FaceUtils.drawBoundingBoxes(sourceImage, detectedObjects);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public float[] featureExtraction(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
synchronized (lock) {
return NativeLoader.seetaFace6SDK.extractMaxFace(imageData);
}
}
@Override
public float[] featureExtraction(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
BufferedImage image = null;
try {
image = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return featureExtraction(image);
}
@Override
public float[] featureExtraction(InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
BufferedImage image = null;
try {
image = ImageIO.read(inputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return featureExtraction(image);
}
@Override
public float[] featureExtraction(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return featureExtraction(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public float calculSimilar(float[] feature1, float[] feature2) {
if(Objects.isNull(feature1) || Objects.isNull(feature2)){
throw new FaceException("特征向量无效");
}
synchronized (lock) {
return NativeLoader.seetaFace6SDK.calculateSimilarity(feature1, feature2);
}
}
@Override
public float featureComparison(String imagePath1, String imagePath2) {
if(!FileUtils.isFileExists(imagePath1) || !FileUtils.isFileExists(imagePath2)){
throw new FaceException("图像文件不存在");
}
BufferedImage image1 = null;
BufferedImage image2 = null;
try {
image1 = ImageIO.read(new File(Paths.get(imagePath1).toAbsolutePath().toString()));
image2 = ImageIO.read(new File(Paths.get(imagePath2).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return featureComparison(image1, image2);
}
@Override
public float featureComparison(InputStream inputStream1, InputStream inputStream2) {
if(Objects.isNull(inputStream1) || Objects.isNull(inputStream2)){
throw new FaceException("图像输入流无效");
}
BufferedImage image1 = null;
BufferedImage image2 = null;
try {
image1 = ImageIO.read(inputStream1);
image2 = ImageIO.read(inputStream2);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return featureComparison(image1, image2);
}
@Override
public float featureComparison(BufferedImage image1, BufferedImage image2) {
if(!ImageUtils.isImageValid(image1) || !ImageUtils.isImageValid(image2)){
throw new FaceException("图像无效");
}
SeetaImageData imageData1 = new SeetaImageData(image1.getWidth(), image1.getHeight(), 3);
imageData1.data = ImageUtils.getMatrixBGR(image1);
SeetaImageData imageData2 = new SeetaImageData(image2.getWidth(), image2.getHeight(), 3);
imageData2.data = ImageUtils.getMatrixBGR(image2);
synchronized (lock) {
//裁剪
byte[][] cropImg1 = NativeLoader.seetaFace6SDK.crop(imageData1);
byte[][] cropImg2 = NativeLoader.seetaFace6SDK.crop(imageData2);
if(cropImg1 == null || cropImg1.length == 0){
throw new FaceException("未发现人脸");
}
if(cropImg2 == null || cropImg2.length == 0){
throw new FaceException("未发现人脸");
}
BufferedImage cropImage1 = ImageUtils.bgrToBufferedImage(cropImg1[0], 256, 256);
BufferedImage cropImage2 = ImageUtils.bgrToBufferedImage(cropImg2[0], 256, 256);
SeetaImageData cropImageData1 = new SeetaImageData(cropImage1.getWidth(), cropImage1.getHeight(), 3);
cropImageData1.data = ImageUtils.getMatrixBGR(cropImage1);
SeetaImageData cropImageData2 = new SeetaImageData(cropImage2.getWidth(), cropImage2.getHeight(), 3);
cropImageData2.data = ImageUtils.getMatrixBGR(cropImage2);
return NativeLoader.seetaFace6SDK.compare(cropImageData1, cropImageData2);
}
}
@Override
public float featureComparison(byte[] imageData1, byte[] imageData2) {
if(Objects.isNull(imageData1) || Objects.isNull(imageData2)){
throw new FaceException("图像无效");
}
BufferedImage image1 = null;
BufferedImage image2 = null;
try {
image1 = ImageIO.read(new ByteArrayInputStream(imageData1));
image2 = ImageIO.read(new ByteArrayInputStream(imageData2));
} catch (IOException e) {
throw new FaceException("无效图片", e);
}
return featureComparison(image1, image2);
}
@Override
public boolean register(String key, String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return register(key, bufferedImage);
}
@Override
public boolean register(String key, BufferedImage image) {
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
synchronized (lock) {
byte[][] bytes = NativeLoader.seetaFace6SDK.crop(imageData);
if (bytes == null || bytes.length == 0) {
log.info("register face fail: key={}, error=no valid face", key);
return false;
}
long index = NativeLoader.seetaFace6SDK.registerCroppedFace(bytes[0]);
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
//持久化到sqlite数据库
FaceData face = new FaceData();
face.setKey(key);
face.setIndex(index);
face.setImgData(bytes[0]);
try {
new FaceDao(config.getFaceDbPath()).save(face);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("保存人脸库失败", e);
}
return true;
}
}
@Override
public boolean register(String key, byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
} catch (IOException e) {
throw new FaceException(e);
}
return register(key, bufferedImage);
}
@Override
public boolean register(String key, InputStream inputStream) {
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
BufferedImage image = null;
try {
image = ImageIO.read(inputStream);
} catch (IOException e) {
throw new FaceException("无效的图片输入流", e);
}
return register(key, image);
}
/**
* 注册已裁剪后人脸
* @param key
* @param faceData
* @return
*/
private boolean register(String key, FaceData faceData) {
synchronized (lock) {
long index = NativeLoader.seetaFace6SDK.registerCroppedFace(faceData.getImgData());
if (index < 0) {
log.info("register face fail: key={}, index={}", key, index);
return false;
}
int rows = 0;
try {
rows = new FaceDao(config.getFaceDbPath()).updateIndex(index, faceData);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException(e);
}
return rows > 0;
}
}
@Override
public FaceResult search(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new File(Paths.get(imagePath).toAbsolutePath().toString()));
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return search(bufferedImage);
}
@Override
public FaceResult search(InputStream inputStream) {
if(Objects.isNull(inputStream)){
throw new FaceException("图像输入流无效");
}
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
BufferedImage image = null;
try {
image = ImageIO.read(inputStream);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
return search(image);
}
@Override
public FaceResult search(BufferedImage image) {
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
SeetaImageData imageData = new SeetaImageData(image.getWidth(), image.getHeight(), 3);
imageData.data = ImageUtils.getMatrixBGR(image);
synchronized (lock) {
RecognizeResult recognizeResult = NativeLoader.seetaFace6SDK.query(imageData);
return searchFaceDb(recognizeResult);
}
}
@Override
public FaceResult search(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
} catch (IOException e) {
throw new FaceException(e);
}
return search(bufferedImage);
}
@Override
public long removeRegister(String... keys) {
if(keys == null || keys.length == 0){
throw new FaceException("keys不允许为空");
}
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
synchronized (lock) {
try {
List<Long> list = new FaceDao(config.getFaceDbPath()).findIndexList(keys);
if (list == null) {
return 0;
}
long[] array = list.stream().mapToLong(Long::longValue).toArray();
long rows = NativeLoader.seetaFace6SDK.delete(array);
new FaceDao(config.getFaceDbPath()).deleteFace(keys);
return rows;
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException(e);
}
}
}
@Override
public long clearFace(){
if(!checkFaceDb()){
throw new FaceException("未找到人脸库,无法使用此功能(请检查是否配置人脸库路径)");
}
synchronized (lock) {
long rows = NativeLoader.seetaFace6SDK.delete(new long[]{-1});
try {
new FaceDao(config.getFaceDbPath()).deleteAll();
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("删除人脸库失败", e);
}
return rows;
}
}
/**
* 检查是否存在人脸库
* @return
*/
private boolean checkFaceDb(){
if(Objects.nonNull(config) && StringUtils.isNotBlank(config.getFaceDbPath())){
File file = new File(config.getFaceDbPath());
return file.exists() && file.isFile();
}
return false;
}
private FaceResult searchFaceDb(RecognizeResult recognizeResult) {
if(recognizeResult != null && recognizeResult.index >= 0){
String key = null;
synchronized (lock) {
try {
key = new FaceDao(config.getFaceDbPath()).findKeyByIndex(recognizeResult.index);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("查询人脸库失败", e);
}
return new FaceResult(key, recognizeResult.similar);
}
}
return null;
}
/**
* 加载人脸库
* @throws SQLException
* @throws ClassNotFoundException
*/
private void loadFaceDb() {
if(!checkFaceDb()){
log.info("未配置人脸库");
return;
}
//分页查询人脸库
int pageNo = 0, pageSize = 100;
while (true) {
List<FaceData> list = null;
try {
list = new FaceDao(config.getFaceDbPath()).findFace(pageNo, pageSize);
} catch (SQLException | ClassNotFoundException e) {
throw new FaceException("查询人脸库失败", e);
}
if (list == null) {
break;
}
list.forEach(face -> {
try {
register(face.getKey(), face);
} catch (Exception e) {
e.printStackTrace();
}
});
if (list.size() < pageSize) {
break;
}
pageNo++;
}
}
}

View File

@@ -0,0 +1,232 @@
package cn.smartjavaai.face.model;
import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
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.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.exception.FaceException;
import cn.smartjavaai.face.translator.FaceDetectionTranslator;
import cn.smartjavaai.face.utils.FaceUtils;
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;
/**
* @author dwj
*/
@Slf4j
public class UltraLightFastGenericFaceModel extends AbstractFaceModel implements AutoCloseable{
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
/**
* 特征图层的基础缩放比例
*/
private static final int[][] scales = {{10, 16, 24}, {32, 48}, {64, 96}, {128, 192, 256}};
/**
* 特征图相对于原图的采样步长
*/
private static final int[] steps = {8, 16, 32, 64};
/**
* 缩放系数
*/
private static final double[] variance = {0.1f, 0.2f};
private ZooModel<Image, DetectedObjects> model;
/**
* 加载模型
* @param config
*/
@Override
public void loadModel(FaceModelConfig config) {
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
FaceDetectionTranslator translator =
new FaceDetectionTranslator(config.getConfidenceThreshold(), config.getNmsThresh(), variance, FaceConfig.MAX_FACE_LIMIT, scales, steps);
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls(StringUtils.isNotBlank(config.getModelPath()) ? null : "https://resources.djl.ai/test-models/pytorch/ultranet.zip")
.optModelPath(StringUtils.isNotBlank(config.getModelPath()) ? Paths.get(config.getModelPath()) : null)
.optTranslator(translator)
.optProgress(new ProgressBar())
.optDevice(device)
.optEngine("PyTorch") // Use PyTorch engine
.build();
try {
model = criteria.loadModel();
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
log.info("当前设备: " + model.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new FaceException("模型加载失败", e);
}
}
/**
* 检测人脸
* @param imagePath 图片路径
* @return
* @throws Exception
*/
@Override
public DetectionResponse detect(String imagePath){
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
Image img = null;
try {
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
} catch (IOException e) {
throw new FaceException("无效的图片", e);
}
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
}
/**
* 检测人脸
* @param imageInputStream 图片流
* @return
* @throws Exception
*/
@Override
public DetectionResponse detect(InputStream imageInputStream){
try {
Image img = ImageFactory.getInstance().fromInputStream(imageInputStream);
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
} catch (IOException e) {
throw new FaceException("无效图片输入流", e);
}
}
@Override
public DetectionResponse detect(BufferedImage image) {
Image img = ImageFactory.getInstance().fromImage(image);
DetectedObjects detection = detect(img);
return FaceUtils.convertToDetectionResponse(detection,img);
}
@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 void detectAndDraw(String imagePath, String outputPath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
try {
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
DetectedObjects detectedObjects = detect(img);
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
throw new FaceException("未识别到人脸");
}
img.drawBoundingBoxes(detectedObjects);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 调用 save 方法将 Image 写入字节流
img.save(new FileOutputStream(Paths.get(outputPath).toAbsolutePath().toString()), "png");
} catch (IOException e) {
throw new FaceException(e);
}
}
@Override
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
Image img = ImageFactory.getInstance().fromImage(sourceImage);
DetectedObjects detectedObjects = detect(img);
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
throw new FaceException("未识别到人脸");
}
img.drawBoundingBoxes(detectedObjects);
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 调用 save 方法将 Image 写入字节流
img.save(outputStream, "png");
// 将字节流转换为 BufferedImage
byte[] imageBytes = outputStream.toByteArray();
return ImageIO.read(new ByteArrayInputStream(imageBytes));
} catch (IOException e) {
throw new FaceException("导出图片失败", e);
}
}
/**
* 人脸检测
* @param image
* @return
*/
private DetectedObjects detect(Image image){
Predictor<Image, DetectedObjects> predictor = null;
try {
predictor = predictorPool.borrowObject();
return predictor.predict(image);
} catch (Exception e) {
throw new FaceException("目标检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
log.info("释放资源");
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
@Override
public void close() {
if (predictorPool != null) {
predictorPool.close();
}
}
}

View File

@@ -10,7 +10,7 @@
* OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
package cn.smartjavaai.face;
package cn.smartjavaai.face.translator;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.*;

View File

@@ -12,7 +12,7 @@ import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
/**
* @author 邓文杰
* @author dwj
* @date 2025/3/31
*/
public final class FaceFeatureTranslator implements Translator<Image, float[]> {

View File

@@ -0,0 +1,167 @@
package cn.smartjavaai.face.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.entity.Point;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.utils.ImageUtils;
import cn.smartjavaai.face.FaceModelConfig;
import cn.smartjavaai.face.exception.FaceException;
import com.seetaface.model.SeetaRect;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 人脸检测相关工具类
* @author dwj
* @date 2025/4/9
*/
public class FaceUtils {
/**
* 转换为FaceDetectedResult
* @param detection
* @param img
* @return
*/
public static DetectionResponse convertToDetectionResponse(DetectedObjects detection, Image img){
if(Objects.isNull(detection) || Objects.isNull(detection.getProbabilities())
|| detection.getProbabilities().isEmpty() || Objects.isNull(detection.items()) || detection.items().isEmpty()){
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
Iterator iterator = detectedObjectList.iterator();
int index = 0;
while(iterator.hasNext()) {
DetectedObjects.DetectedObject result = (DetectedObjects.DetectedObject)iterator.next();
BoundingBox box = result.getBoundingBox();
int x = (int)(box.getBounds().getX() * (double)img.getWidth());
int y = (int)(box.getBounds().getY() * (double)img.getHeight());
int width = (int)(box.getBounds().getWidth() * (double)img.getWidth());
int height = (int)(box.getBounds().getHeight() * (double)img.getHeight());
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue());
rectangleList.add(rectangle);
index++;
}
detectionResponse.setRectangleList(rectangleList);
return detectionResponse;
}
/**
* 转换为FaceDetectedResult
* @param seetaResult
* @return
*/
public static DetectionResponse convertToDetectionResponse(SeetaRect[] seetaResult, FaceModelConfig config){
if(Objects.isNull(seetaResult) || seetaResult.length == 0){
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
for(SeetaRect rect : seetaResult){
//过滤置信度
if(config.getConfidenceThreshold() > 0 && rect.score < config.getConfidenceThreshold()){
continue;
}
DetectionRectangle rectangle = new DetectionRectangle(rect.x, rect.y, rect.width, rect.height, rect.score);
rectangleList.add(rectangle);
}
detectionResponse.setRectangleList(rectangleList);
return detectionResponse;
}
/**
* 绘制人脸框
* @param sourceImage
* @param detectionResponse
* @param savePath
* @throws IOException
*/
public static void drawBoundingBoxes(BufferedImage sourceImage, DetectionResponse detectionResponse, String savePath) throws IOException {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().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(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
graphics.setColor(Color.RED);// 边框颜色
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
}
graphics.dispose();
ImageIO.write(sourceImage, "jpg", new File(savePath));
}
/**
* 绘制人脸框
* @param sourceImage
* @param detectionResponse
* @throws IOException
*/
public static BufferedImage drawBoundingBoxes(BufferedImage sourceImage, DetectionResponse detectionResponse) throws IOException {
if(!ImageUtils.isImageValid(sourceImage)){
throw new FaceException("图像无效");
}
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().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(DetectionRectangle rectangle : detectionResponse.getRectangleList()){
graphics.setColor(Color.RED);// 边框颜色
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
}
graphics.dispose();
return sourceImage;
}
/**
* 绘制文字
* @param g
* @param text
* @param x
* @param y
* @param stroke
* @param padding
*/
private static void drawText(Graphics2D g, String text, int x, int y, int stroke, int padding) {
FontMetrics metrics = g.getFontMetrics();
x += stroke / 2;
y += stroke / 2;
int width = metrics.stringWidth(text) + padding * 2 - stroke / 2;
int height = metrics.getHeight() + metrics.getDescent();
int ascent = metrics.getAscent();
java.awt.Rectangle background = new java.awt.Rectangle(x, y, width, height);
g.fill(background);
g.setPaint(Color.WHITE);
g.drawString(text, x + padding, y + ascent);
}
}

View File

@@ -124,4 +124,12 @@ public class SeetaFace6JNI {
*/
public native int predictImage(SeetaImageData img);
public native void dispose();
@Override
protected void finalize() throws Throwable {
super.finalize();
this.dispose();
}
}