1、目标检测:支持自己训练的模型推理

2、目标检测:支持yolo12模型
3、支持JDK8使用
4、引入离线依赖库,支持完全离线使用
5、优化FaceNet人脸比对速度
6、支持4通道图片检测
This commit is contained in:
dengwenjie
2025-05-17 11:19:46 +08:00
parent ab669d58e3
commit a7a118c5aa
27 changed files with 401 additions and 237 deletions

View File

@@ -6,11 +6,11 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.12</version>
<version>1.0.13</version>
</parent>
<artifactId>smartjavaai-face</artifactId>
<version>1.0.12</version>
<version>1.0.13</version>
<name>smartjavaai-face</name>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>
@@ -22,8 +22,8 @@
</licenses>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<!-- <maven.compiler.source>11</maven.compiler.source>-->
<!-- <maven.compiler.target>11</maven.compiler.target>-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.test.skip>true</maven.test.skip>
<javacv.version>1.5.8</javacv.version>
@@ -87,7 +87,7 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
<!-- <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>-->
<doclint>none</doclint>
<additionalJOptions>
<additionalJOption>-Xdoclint:none</additionalJOption>

View File

@@ -1,5 +1,6 @@
package cn.smartjavaai.face.config;
import cn.smartjavaai.face.model.facerec.FaceModel;
import lombok.Data;
/**
@@ -21,17 +22,17 @@ public class FaceExtractConfig {
private boolean align = true;
/**
* 人脸检测模型配置
* 人脸检测模型
*/
private FaceModelConfig detectModelConfig;
private FaceModel detectModel;
public FaceExtractConfig() {
}
public FaceExtractConfig(boolean cropFace, boolean align, FaceModelConfig detectModelConfig) {
public FaceExtractConfig(boolean cropFace, boolean align, FaceModel detectModel) {
this.cropFace = cropFace;
this.align = align;
this.detectModelConfig = detectModelConfig;
this.detectModel = detectModel;
}

View File

@@ -52,6 +52,11 @@ public class FaceModelConfig {
*/
private int gpuId = 0;
/**
* 人脸特征提取配置
*/
private FaceExtractConfig extractConfig;
public FaceModelConfig() {
}

View File

@@ -140,21 +140,6 @@ public abstract class AbstractFaceModel implements FaceModel {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(BufferedImage image, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(String imagePath, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public List<float[]> extractFeatures(byte[] imageData, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(BufferedImage image) {
throw new UnsupportedOperationException("默认不支持该功能");
@@ -170,18 +155,5 @@ public abstract class AbstractFaceModel implements FaceModel {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(BufferedImage image, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(String imagePath, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public float[] extractTopFaceFeature(byte[] imageData, FaceExtractConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
}

View File

@@ -199,30 +199,6 @@ public interface FaceModel {
*/
List<float[]> extractFeatures(BufferedImage image);
/**
* 特征提取(使用自定义配置)
* 强制裁剪操作
* @param image BufferedImage
* @param config
* @return
*/
List<float[]> extractFeatures(BufferedImage image, FaceExtractConfig config);
/**
* 特征提取(使用自定义配置)
* @param imagePath 图片路径
* @param config
* @return
*/
List<float[]> extractFeatures(String imagePath, FaceExtractConfig config);
/**
* 特征提取(使用自定义配置)
* @param imageData 图片字节流
* @param config
* @return
*/
List<float[]> extractFeatures(byte[] imageData, FaceExtractConfig config);
/**
* 提取分数最高人脸特征(使用默认配置)
@@ -245,27 +221,6 @@ public interface FaceModel {
*/
float[] extractTopFaceFeature(byte[] imageData);
/**
* 提取分数最高人脸特征(使用自定义配置)
* @param image BufferedImage
* @return
*/
float[] extractTopFaceFeature(BufferedImage image, FaceExtractConfig config);
/**
* 提取分数最高人脸特征(使用自定义配置)
* @param imagePath 图片路径
* @param config
* @return
*/
float[] extractTopFaceFeature(String imagePath, FaceExtractConfig config);
/**
* 提取分数最高人脸特征(使用自定义配置)
* @param imageData 图片字节流
* @param config
* @return
*/
float[] extractTopFaceFeature(byte[] imageData, FaceExtractConfig config);
}

View File

@@ -59,6 +59,8 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
private ZooModel<Image, float[]> model;
private FaceModelConfig config;
public static final List<Float> mean =
Arrays.asList(
127.5f / 255.0f,
@@ -75,10 +77,21 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
*/
@Override
public void loadModel(FaceModelConfig config) {
if(Objects.isNull(config)){
throw new FaceException("config为null");
}
if(Objects.isNull(config.getExtractConfig())){
config.setExtractConfig(getDefaultConfig());
}else{
if(Objects.isNull(config.getExtractConfig().getDetectModel())){
throw new FaceException("请设置人脸检测模型");
}
}
Device device = null;
if(!Objects.isNull(config.getDevice())){
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
}
this.config = config;
String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
Criteria<Image, float[]> faceFeatureCriteria =
Criteria.builder()
@@ -162,7 +175,8 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
}
float[] feature1 = extractTopFaceFeature(imagePath1);
float[] feature2 = extractTopFaceFeature(imagePath2);
return calculSimilar(feature1, feature2);
float ret = calculSimilar(feature1, feature2);
return ret;
}
@@ -194,37 +208,18 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
FaceExtractConfig config = new FaceExtractConfig();
FaceModelConfig detectModelConfig = new FaceModelConfig();
detectModelConfig.setModelEnum(FaceModelEnum.ULTRA_LIGHT_FAST_GENERIC_FACE);
config.setDetectModelConfig(detectModelConfig);
log.debug("创建默认检测模型ULTRA_LIGHT_FAST_GENERIC_FACE");
FaceModel detectModel = FaceModelFactory.getInstance().getModel(detectModelConfig);
log.debug("创建检测模型完毕");
config.setDetectModel(detectModel);
return config;
}
@Override
public List<float[]> extractFeatures(String imagePath) {
return extractFeatures(imagePath, getDefaultConfig());
}
@Override
public List<float[]> extractFeatures(byte[] imageData) {
return extractFeatures(imageData, getDefaultConfig());
}
@Override
public List<float[]> extractFeatures(BufferedImage image) {
return extractFeatures(image, getDefaultConfig());
}
@Override
public List<float[]> extractFeatures(BufferedImage image, FaceExtractConfig config) {
if(Objects.isNull(config)){
throw new FaceException("config为null");
}
List<float[]> featureList = new ArrayList<float[]>();
if(Objects.isNull(config.getDetectModelConfig())){
throw new FaceException("config.detectModelConfig为null");
}
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
DetectionResponse detectedResult = faceModel.detect(image);
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
throw new FaceException("未检测到人脸");
}
@@ -237,7 +232,7 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.isAlign()){
if(config.getExtractConfig().isAlign()){
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
NDArray srcPoints = manager.create(pointsArray);
@@ -263,8 +258,21 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
return featureList;
}
@Override
public List<float[]> extractFeatures(String imagePath, FaceExtractConfig config) {
public List<float[]> extractFeatures(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
}
@Override
public List<float[]> extractFeatures(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
@@ -275,49 +283,15 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractFeatures(image, config);
}
@Override
public List<float[]> extractFeatures(byte[] imageData, FaceExtractConfig config) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return extractFeatures(ImageIO.read(new ByteArrayInputStream(imageData)), config);
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}
return extractFeatures(image);
}
@Override
public float[] extractTopFaceFeature(BufferedImage image) {
return extractTopFaceFeature(image, getDefaultConfig());
}
@Override
public float[] extractTopFaceFeature(String imagePath) {
return extractTopFaceFeature(imagePath, getDefaultConfig());
}
@Override
public float[] extractTopFaceFeature(byte[] imageData) {
return extractTopFaceFeature(imageData, getDefaultConfig());
}
@Override
public float[] extractTopFaceFeature(BufferedImage image, FaceExtractConfig config) {
if(Objects.isNull(config)){
throw new FaceException("config为null");
}
if(Objects.isNull(config.getDetectModelConfig())){
throw new FaceException("config.detectModelConfig为null");
}
Image djlImage = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
float[] features = null;
if(config.isCropFace()){
FaceModel faceModel = FaceModelFactory.getInstance().getModel(config.getDetectModelConfig());
DetectionResponse detectedResult = faceModel.detect(image);
if(config.getExtractConfig().isCropFace()){
DetectionResponse detectedResult = config.getExtractConfig().getDetectModel().detect(image);
if(Objects.isNull(detectedResult) || Objects.isNull(detectedResult.getDetectionInfoList()) || detectedResult.getDetectionInfoList().isEmpty()){
throw new FaceException("未检测到人脸");
}
@@ -327,7 +301,7 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
//裁剪人脸
Image subImage = djlImage.getSubImage(rectangle.getX(), rectangle.getY() , rectangle.getWidth() , rectangle.getHeight());
//人脸对齐
if(config.isAlign()){
if(config.getExtractConfig().isAlign()){
NDManager manager = NDManager.newBaseManager();
//获取子图中人脸关键点坐标
double[][] pointsArray = FaceUtils.facePoints(detectionInfo.getFaceInfo().getKeyPoints());
@@ -355,7 +329,7 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
}
@Override
public float[] extractTopFaceFeature(String imagePath, FaceExtractConfig config) {
public float[] extractTopFaceFeature(String imagePath) {
if(!FileUtils.isFileExists(imagePath)){
throw new FaceException("图像文件不存在");
}
@@ -366,16 +340,16 @@ public class FeatureExtractionModel extends AbstractFaceModel implements AutoClo
} catch (IOException e) {
throw new FaceException("无效图片路径", e);
}
return extractTopFaceFeature(image, config);
return extractTopFaceFeature(image);
}
@Override
public float[] extractTopFaceFeature(byte[] imageData, FaceExtractConfig config) {
public float[] extractTopFaceFeature(byte[] imageData) {
if(Objects.isNull(imageData)){
throw new FaceException("图像无效");
}
try {
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)), config);
return extractTopFaceFeature(ImageIO.read(new ByteArrayInputStream(imageData)));
} catch (IOException e) {
throw new FaceException("错误的图像", e);
}

View File

@@ -629,6 +629,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
@Override
public FaceResult search(BufferedImage image) {
long time1 = System.currentTimeMillis();
if(!ImageUtils.isImageValid(image)){
throw new FaceException("图像无效");
}
@@ -648,7 +649,12 @@ public class SeetaFace6Model extends AbstractFaceModel {
if(similarity[0] < config.getSimilarityThreshold()){
return null;
}
return searchFaceDb(index[0], similarity[0]);
long time2 = System.currentTimeMillis();
System.out.println("总耗时1" + (time2 - time1) + " ms");
FaceResult faceResult = searchFaceDb(index[0], similarity[0]);
long time3 = System.currentTimeMillis();
System.out.println("总耗时2" + (time3 - time2) + " ms");
return faceResult;
} catch (FaceException e) {
throw e;
} catch (Exception e) {

View File

@@ -111,8 +111,26 @@ public class OpenCVUtils {
public static Mat image2Mat(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
int channels;
// 获取图像类型
int imageType = img.getType();
// 判断是3通道还是4通道
if (imageType == BufferedImage.TYPE_3BYTE_BGR) {
channels = 3;
} else if (imageType == BufferedImage.TYPE_4BYTE_ABGR || imageType == BufferedImage.TYPE_4BYTE_ABGR_PRE) {
channels = 4;
} else {
// 如果不是已知格式,强制转换为 3 通道 BGR
BufferedImage convertedImg = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
convertedImg.getGraphics().drawImage(img, 0, 0, null);
img = convertedImg;
channels = 3;
}
byte[] data = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
Mat mat = new Mat(height, width, CvType.CV_8UC3);
Mat mat = new Mat(height, width, CvType.CV_8UC(channels));
mat.put(0, 0, data);
return mat;
}