commit 28f55e85ccccc730836d1b280970c825f4a6bcf5
Author: dengwenjie <775747758@qq.com>
Date: Fri Feb 21 11:26:58 2025 +0800
初始提交
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..45c3a01
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea
+.idea/
+target
+log
+*.iml
+/.settings/
+/logging.file_IS_UNDEFINED/
+
diff --git a/output.jpg b/output.jpg
new file mode 100644
index 0000000..974fd32
Binary files /dev/null and b/output.jpg differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a4606bf
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,110 @@
+
+
+ 4.0.0
+
+ ink.numberone
+ smartjavaai-parent
+ 1.0.0
+ pom
+
+ smartjavaai-face
+ smartjavaai-common
+
+
+
+ 11
+ 11
+
+ UTF-8
+ 0.32.0-SNAPSHOT
+
+
+
+
+
+ ai.djl
+ bom
+ ${djl.version}
+ pom
+ import
+
+
+
+
+
+ commons-cli
+ commons-cli
+ 1.9.0
+
+
+ commons-io
+ commons-io
+ 2.17.0
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ 2.24.1
+
+
+ ai.djl
+ api
+
+
+ ai.djl
+ basicdataset
+
+
+ ai.djl
+ model-zoo
+
+
+ ai.djl.timeseries
+ timeseries
+
+
+ ai.djl.huggingface
+ tokenizers
+
+
+ ai.djl.audio
+ audio
+
+
+
+ ai.djl.mxnet
+ mxnet-model-zoo
+
+
+
+ ai.djl.pytorch
+ pytorch-model-zoo
+
+
+
+ ai.djl.tensorflow
+ tensorflow-model-zoo
+
+
+
+ ai.djl.onnxruntime
+ onnxruntime-engine
+
+
+ org.testng
+ testng
+ 7.10.2
+ test
+
+
+
+ commons-beanutils
+ commons-beanutils
+ 1.9.4
+
+
+
+
diff --git a/smartjavaai-common/pom.xml b/smartjavaai-common/pom.xml
new file mode 100644
index 0000000..7abd336
--- /dev/null
+++ b/smartjavaai-common/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+ ink.numberone
+ smartjavaai-parent
+ 1.0.0
+
+
+ smartjavaai-common
+
+
+ 8
+ 8
+ UTF-8
+
+
+
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java
new file mode 100644
index 0000000..0dbe960
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java
@@ -0,0 +1,42 @@
+package cn.smartjavaai.common.entity;
+
+import ai.djl.util.JsonUtils;
+
+import java.io.Serializable;
+
+/**
+ * 点
+ * @author dwj
+ * @date 2025/2/19
+ */
+public class Point implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private int x;
+ private int y;
+
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public void setX(int x) {
+ this.x = x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public void setY(int y) {
+ this.y = y;
+ }
+
+ @Override
+ public String toString() {
+ return JsonUtils.GSON_COMPACT.toJson(this);
+ }
+}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java
new file mode 100644
index 0000000..f5a8655
--- /dev/null
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Rectangle.java
@@ -0,0 +1,50 @@
+package cn.smartjavaai.common.entity;
+
+import java.util.List;
+
+/**
+ * 矩形区域
+ * @author dwj
+ * @date 2025/2/19
+ */
+public class Rectangle {
+
+ /**
+ * 矩形区域点集合
+ */
+ List pointList;
+
+ /**
+ * 矩形区域宽度
+ */
+ int width;
+
+ /**
+ * 矩形区域高度
+ */
+ int height;
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public List getPointList() {
+ return pointList;
+ }
+
+ public void setPointList(List pointList) {
+ this.pointList = pointList;
+ }
+}
diff --git a/smartjavaai-face/pom.xml b/smartjavaai-face/pom.xml
new file mode 100644
index 0000000..5abae2d
--- /dev/null
+++ b/smartjavaai-face/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ ink.numberone
+ smartjavaai-parent
+ 1.0.0
+
+
+ smartjavaai-face
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+
+ ink.numberone
+ smartjavaai-common
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java
new file mode 100644
index 0000000..366cfe4
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/AbstractFaceAlgorithm.java
@@ -0,0 +1,50 @@
+package cn.smartjavaai.face;
+
+import java.io.InputStream;
+
+/**
+ * 人脸识别算法
+ * @author dwj
+ * @date 2025/2/20
+ */
+public abstract class AbstractFaceAlgorithm implements FaceAlgorithm{
+ @Override
+ public void loadModel(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("默认不支持该功能");
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java
new file mode 100644
index 0000000..7bff051
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithm.java
@@ -0,0 +1,83 @@
+package cn.smartjavaai.face;
+
+import ai.djl.MalformedModelException;
+import ai.djl.repository.zoo.ModelNotFoundException;
+import ai.djl.translate.TranslateException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 人脸识别算法
+ * @author dwj
+ * @date 2025/2/19
+ */
+public interface FaceAlgorithm {
+
+ /**
+ * 加载模型
+ * @param config
+ * @throws Exception
+ */
+ void loadModel(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;
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java
new file mode 100644
index 0000000..0051830
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceAlgorithmFactory.java
@@ -0,0 +1,95 @@
+package cn.smartjavaai.face;
+
+import cn.smartjavaai.face.algo.RetinaFace;
+import cn.smartjavaai.face.algo.UltraLightFastGenericFace;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 人脸算法工厂
+ * @author dwj
+ * @date 2025/2/19
+ */
+public class FaceAlgorithmFactory {
+
+ /**
+ * 算法注册表
+ */
+ private static final Map> 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){
+ 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);
+ 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;
+ }
+
+ /**
+ * 创建轻量级算法
+ * @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;
+ }
+
+ // 初始化默认算法
+ static {
+ registerAlgorithm("retinaface", RetinaFace.class);
+ registerAlgorithm("ultralightfastgenericface", UltraLightFastGenericFace.class);
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java
new file mode 100644
index 0000000..eabcc6c
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceConfig.java
@@ -0,0 +1,26 @@
+package cn.smartjavaai.face;
+
+/**
+ * @author dwj
+ * @date 2025/2/20
+ */
+public class FaceConfig {
+
+ /**
+ * 置信度阈值
+ */
+ public static final float DEFAULT_CONFIDENCE_THRESHOLD = 0.85F;
+
+ /**
+ * 每张特征图保留的最大候选框数量
+ */
+ public static final int MAX_FACE_LIMIT = 5000;
+
+ /**
+ * nms阈值:控制重叠框的合并程度
+ */
+ public static final float NMS_THRESHOLD = 0.45F;
+
+
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java
new file mode 100644
index 0000000..24f80ad
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectedResult.java
@@ -0,0 +1,41 @@
+package cn.smartjavaai.face;
+
+
+import cn.smartjavaai.common.entity.Rectangle;
+
+import java.util.List;
+
+/**
+ * 人脸检测结果
+ * @author dwj
+ * @date 2025/2/19
+ */
+public class FaceDetectedResult {
+
+
+ /**
+ * 置信度
+ */
+ private List probabilities;
+
+ /**
+ * 人脸框
+ */
+ private List rectangles;
+
+ public List getProbabilities() {
+ return probabilities;
+ }
+
+ public void setProbabilities(List probabilities) {
+ this.probabilities = probabilities;
+ }
+
+ public List getRectangles() {
+ return rectangles;
+ }
+
+ public void setRectangles(List rectangles) {
+ this.rectangles = rectangles;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java
new file mode 100644
index 0000000..f70b567
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/FaceDetectionTranslator.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * 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;
+
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.output.*;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDArrays;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.ndarray.types.DataType;
+import ai.djl.ndarray.types.Shape;
+import ai.djl.translate.Translator;
+import ai.djl.translate.TranslatorContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Face Detection Translator
+ */
+public class FaceDetectionTranslator implements Translator {
+
+ private double confThresh;
+ private double nmsThresh;
+ private int topK;
+ private double[] variance;
+ private int[][] scales;
+ private int[] steps;
+
+ public FaceDetectionTranslator(
+ double confThresh,
+ double nmsThresh,
+ double[] variance,
+ int topK,
+ int[][] scales,
+ int[] steps) {
+ this.confThresh = confThresh;
+ this.nmsThresh = nmsThresh;
+ this.variance = variance;
+ this.topK = topK;
+ this.scales = scales;
+ this.steps = steps;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public NDList processInput(TranslatorContext ctx, Image input) {
+
+ ctx.setAttachment("width", input.getWidth());
+ ctx.setAttachment("height", input.getHeight());
+
+ NDArray array = input.toNDArray(ctx.getNDManager(), Image.Flag.COLOR);
+ array = array.transpose(2, 0, 1).flip(0); // HWC -> CHW RGB -> BGR
+ // The network by default takes float32
+ if (!array.getDataType().equals(DataType.FLOAT32)) {
+ array = array.toType(DataType.FLOAT32, false);
+ }
+ NDArray mean =
+ ctx.getNDManager().create(new float[] {104f, 117f, 123f}, new Shape(3, 1, 1));
+ array = array.sub(mean);
+ return new NDList(array);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public DetectedObjects processOutput(TranslatorContext ctx, NDList list) {
+
+ int width = (int) ctx.getAttachment("width");
+ int height = (int) ctx.getAttachment("height");
+
+ NDManager manager = ctx.getNDManager();
+ double scaleXY = variance[0];
+ double scaleWH = variance[1];
+
+ NDArray prob = list.get(1).get(":, 1:");
+ prob =
+ NDArrays.stack(
+ new NDList(
+ prob.argMax(1).toType(DataType.FLOAT32, false),
+ prob.max(new int[] {1})));
+
+ NDArray boxRecover = boxRecover(manager, width, height, scales, steps);
+ NDArray boundingBoxes = list.get(0);
+ NDArray bbWH = boundingBoxes.get(":, 2:").mul(scaleWH).exp().mul(boxRecover.get(":, 2:"));
+ NDArray bbXY =
+ boundingBoxes
+ .get(":, :2")
+ .mul(scaleXY)
+ .mul(boxRecover.get(":, 2:"))
+ .add(boxRecover.get(":, :2"))
+ .sub(bbWH.mul(0.5f));
+
+ boundingBoxes = NDArrays.concat(new NDList(bbXY, bbWH), 1);
+
+ NDArray landms = list.get(2);
+ landms = decodeLandm(landms, boxRecover, scaleXY);
+
+ // filter the result below the threshold
+ NDArray cutOff = prob.get(1).gt(confThresh);
+ boundingBoxes = boundingBoxes.transpose().booleanMask(cutOff, 1).transpose();
+ landms = landms.transpose().booleanMask(cutOff, 1).transpose();
+ prob = prob.booleanMask(cutOff, 1);
+
+ // start categorical filtering
+ long[] order = prob.get(1).argSort().get(":" + topK).toLongArray();
+ prob = prob.transpose();
+ List retNames = new ArrayList<>();
+ List retProbs = new ArrayList<>();
+ List retBB = new ArrayList<>();
+
+ Map> recorder = new ConcurrentHashMap<>();
+
+ for (int i = order.length - 1; i >= 0; i--) {
+ long currMaxLoc = order[i];
+ float[] classProb = prob.get(currMaxLoc).toFloatArray();
+ int classId = (int) classProb[0];
+ double probability = classProb[1];
+
+ double[] boxArr = boundingBoxes.get(currMaxLoc).toDoubleArray();
+ double[] landmsArr = landms.get(currMaxLoc).toDoubleArray();
+ Rectangle rect = new Rectangle(boxArr[0], boxArr[1], boxArr[2], boxArr[3]);
+ List boxes = recorder.getOrDefault(classId, new ArrayList<>());
+ boolean belowIoU = true;
+ for (BoundingBox box : boxes) {
+ if (box.getIoU(rect) > nmsThresh) {
+ belowIoU = false;
+ break;
+ }
+ }
+ if (belowIoU) {
+ List keyPoints = new ArrayList<>();
+ for (int j = 0; j < 5; j++) { // 5 face landmarks
+ double x = landmsArr[j * 2];
+ double y = landmsArr[j * 2 + 1];
+ keyPoints.add(new Point(x * width, y * height));
+ }
+ Landmark landmark =
+ new Landmark(boxArr[0], boxArr[1], boxArr[2], boxArr[3], keyPoints);
+
+ boxes.add(landmark);
+ recorder.put(classId, boxes);
+ String className = "Face"; // classes.get(classId)
+ retNames.add(className);
+ retProbs.add(probability);
+ retBB.add(landmark);
+ }
+ }
+
+ return new DetectedObjects(retNames, retProbs, retBB);
+ }
+
+ private NDArray boxRecover(
+ NDManager manager, int width, int height, int[][] scales, int[] steps) {
+ int[][] aspectRatio = new int[steps.length][2];
+ for (int i = 0; i < steps.length; i++) {
+ int wRatio = (int) Math.ceil((float) width / steps[i]);
+ int hRatio = (int) Math.ceil((float) height / steps[i]);
+ aspectRatio[i] = new int[] {hRatio, wRatio};
+ }
+
+ List defaultBoxes = new ArrayList<>();
+
+ for (int idx = 0; idx < steps.length; idx++) {
+ int[] scale = scales[idx];
+ for (int h = 0; h < aspectRatio[idx][0]; h++) {
+ for (int w = 0; w < aspectRatio[idx][1]; w++) {
+ for (int i : scale) {
+ double skx = i * 1.0 / width;
+ double sky = i * 1.0 / height;
+ double cx = (w + 0.5) * steps[idx] / width;
+ double cy = (h + 0.5) * steps[idx] / height;
+ defaultBoxes.add(new double[] {cx, cy, skx, sky});
+ }
+ }
+ }
+ }
+
+ double[][] boxes = new double[defaultBoxes.size()][defaultBoxes.get(0).length];
+ for (int i = 0; i < defaultBoxes.size(); i++) {
+ boxes[i] = defaultBoxes.get(i);
+ }
+ return manager.create(boxes).clip(0.0, 1.0);
+ }
+
+ // decode face landmarks, 5 points per face
+ private NDArray decodeLandm(NDArray pre, NDArray priors, double scaleXY) {
+ NDArray point1 =
+ pre.get(":, :2").mul(scaleXY).mul(priors.get(":, 2:")).add(priors.get(":, :2"));
+ NDArray point2 =
+ pre.get(":, 2:4").mul(scaleXY).mul(priors.get(":, 2:")).add(priors.get(":, :2"));
+ NDArray point3 =
+ pre.get(":, 4:6").mul(scaleXY).mul(priors.get(":, 2:")).add(priors.get(":, :2"));
+ NDArray point4 =
+ pre.get(":, 6:8").mul(scaleXY).mul(priors.get(":, 2:")).add(priors.get(":, :2"));
+ NDArray point5 =
+ pre.get(":, 8:10").mul(scaleXY).mul(priors.get(":, 2:")).add(priors.get(":, :2"));
+ return NDArrays.concat(new NDList(point1, point2, point3, point4, point5), 1);
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java
new file mode 100644
index 0000000..c378987
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/ModelConfig.java
@@ -0,0 +1,62 @@
+package cn.smartjavaai.face;
+
+/**
+ * 模型配置
+ * @author dwj
+ * @date 2025/2/19
+ */
+
+public class ModelConfig {
+
+ /**
+ * 人脸算法名称
+ */
+ private String algorithmName;
+
+ /**
+ * 置信度阈值
+ */
+ private double confidenceThreshold;
+
+ /**
+ * 非极大抑制阈值 作用:消除重叠检测框,保留最优结果
+ */
+ private double nmsThresh;
+
+ /**
+ * 最大检测人脸数量
+ */
+ private int maxFaceCount;
+
+ 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;
+ }
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/Test.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/Test.java
new file mode 100644
index 0000000..286cfcb
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/Test.java
@@ -0,0 +1,62 @@
+package cn.smartjavaai.face;
+
+import cn.smartjavaai.common.entity.Rectangle;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileInputStream;
+
+/**
+ * @author dwj
+ * @date 2025/2/19
+ */
+public class Test {
+
+ public static void main(String[] args) throws Exception {
+ // 初始化配置
+ ModelConfig config = new ModelConfig();
+ config.setAlgorithmName("retinaface");
+ //config.setAlgorithmName("ultralightfastgenericface");
+ //config.setModelPath("/Users/wenjie/Documents/idea_workplace/SmartJavaAI/models/retinaface.pt");
+ config.setConfidenceThreshold(0.85f);
+ config.setMaxFaceCount(100);
+ config.setNmsThresh(0.45f);
+
+ FaceAlgorithm currentAlgorithm = FaceAlgorithmFactory.createFaceAlgorithm(config);
+ File imageFile = new File("/Users/wenjie/Downloads/djl-master/examples/src/test/resources/largest_selfie.jpg");
+ FaceDetectedResult result = currentAlgorithm.detect("/Users/wenjie/Downloads/djl-master/examples/src/test/resources/largest_selfie.jpg");
+ //FaceDetectedResult result = currentAlgorithm.detect(new FileInputStream(imageFile));
+
+
+ // 1. 加载原始图片
+ File input = new File("/Users/wenjie/Downloads/djl-master/examples/src/test/resources/largest_selfie.jpg");
+ BufferedImage image = ImageIO.read(input);
+
+ // 2. 创建绘图上下文
+ Graphics2D graphics = image.createGraphics();
+
+ // 3. 配置绘制参数
+ graphics.setColor(Color.RED); // 边框颜色
+ graphics.setStroke(new BasicStroke(2)); // 线宽2像素
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
+
+ for(Rectangle rectangle : result.getRectangles()){
+ // 4. 绘制矩形(左上角坐标x=50,y=100,宽200,高150)
+ graphics.drawRect(rectangle.getPointList().get(0).getX(),
+ rectangle.getPointList().get(0).getY(), rectangle.getWidth(), rectangle.getHeight());
+ }
+
+
+
+ // 5. 释放资源并保存
+ graphics.dispose();
+ ImageIO.write(image, "jpg", new File("output.jpg"));
+
+
+ System.out.println("111111");
+ }
+
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java
new file mode 100644
index 0000000..951214f
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/RetinaFace.java
@@ -0,0 +1,278 @@
+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.beanutils.BeanUtils;
+import org.apache.commons.compress.utils.Lists;
+
+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
+ * @date 2025/2/19
+ */
+public class RetinaFace extends AbstractFaceAlgorithm {
+
+ private Criteria criteria;
+
+ /**
+ * 特征图层的基础缩放比例
+ */
+ 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) {
+ 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/retinaface.zip")
+ //.optModelPath(modelPath)
+ // Load model from local file, e.g:
+ .optModelName("retinaface") // specify model file prefix
+ .optTranslator(translator)
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+ }
+
+ /**
+ * 检测人脸
+ * @param imagePath 图片路径
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public FaceDetectedResult detect(String imagePath) throws Exception{
+ Path facePath = Paths.get(imagePath);
+ Image img = ImageFactory.getInstance().fromFile(facePath);
+ try (ZooModel model = criteria.loadModel();
+ Predictor predictor = model.newPredictor()) {
+ 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);
+ try (ZooModel model = criteria.loadModel();
+ Predictor predictor = model.newPredictor()) {
+ DetectedObjects detection = predictor.predict(img);
+ return convertToFaceDetectedResult(detection,img);
+ /*saveBoundingBoxImage(img, detection);
+ return detection;*/
+ }
+ }
+
+ /**
+ * 转换为FaceDetectedResult
+ * @param detection
+ * @param img
+ * @return
+ */
+ private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
+ FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
+ List probabilities = new ArrayList<>(detection.getProbabilities());
+ List detectedObjectList = detection.items();
+ List RectangleList = detectedObjectList.parallelStream()
+ .map(obj -> {
+ Rectangle rectangle = new Rectangle();
+ List 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;
+ }
+
+ /**
+ * 特征提取
+ * @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();
+ List 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);
+ String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
+
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, float[].class)
+ .optModelUrls(
+ "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
+ .optModelName("face_feature") // specify model file prefix
+ .optArgument("normalize", normalize)
+ .optTranslatorFactory(new ImageFeatureExtractorFactory())
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+
+ try (ZooModel model = criteria.loadModel()) {
+ Predictor predictor = model.newPredictor();
+ 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();
+ List 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);
+ String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
+
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, float[].class)
+ .optModelUrls(
+ "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
+ .optModelName("face_feature") // specify model file prefix
+ .optArgument("normalize", normalize)
+ .optTranslatorFactory(new ImageFeatureExtractorFactory())
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+
+ try (ZooModel model = criteria.loadModel()) {
+ Predictor predictor = model.newPredictor();
+ 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 {
+
+ }*/
+}
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java
new file mode 100644
index 0000000..169c291
--- /dev/null
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/algo/UltraLightFastGenericFace.java
@@ -0,0 +1,270 @@
+package cn.smartjavaai.face.algo;
+
+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.*;
+
+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
+ * @date 2025/2/19
+ */
+public class UltraLightFastGenericFace extends AbstractFaceAlgorithm {
+
+
+ private Criteria 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};
+
+
+
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ @Override
+ public void loadModel(ModelConfig config) {
+ 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();
+ }
+
+ /**
+ * 检测人脸
+ * @param imagePath 图片路径
+ * @return
+ * @throws Exception
+ */
+ @Override
+ public FaceDetectedResult detect(String imagePath) throws Exception{
+ Path facePath = Paths.get(imagePath);
+ Image img = ImageFactory.getInstance().fromFile(facePath);
+ try (ZooModel model = criteria.loadModel();
+ Predictor predictor = model.newPredictor()) {
+ 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);
+ try (ZooModel model = criteria.loadModel();
+ Predictor predictor = model.newPredictor()) {
+ DetectedObjects detection = predictor.predict(img);
+ return convertToFaceDetectedResult(detection,img);
+ /*saveBoundingBoxImage(img, detection);
+ return detection;*/
+ }
+ }
+
+ /**
+ * 转换检测结果
+ * @param detection
+ * @param img
+ * @return
+ */
+ private FaceDetectedResult convertToFaceDetectedResult(DetectedObjects detection, Image img){
+ FaceDetectedResult faceDetectedResult = new FaceDetectedResult();
+ List probabilities = new ArrayList<>(detection.getProbabilities());
+ List detectedObjectList = detection.items();
+ List RectangleList = detectedObjectList.parallelStream()
+ .map(obj -> {
+ Rectangle rectangle = new Rectangle();
+ List 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;
+ }
+
+ /**
+ * 特征提取
+ * @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();
+ List 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);
+ String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
+
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, float[].class)
+ .optModelUrls(
+ "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
+ .optModelName("face_feature") // specify model file prefix
+ .optArgument("normalize", normalize)
+ .optTranslatorFactory(new ImageFeatureExtractorFactory())
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+
+ try (ZooModel model = criteria.loadModel()) {
+ Predictor predictor = model.newPredictor();
+ 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();
+ List 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);
+ String normalize = mean.stream().map(Object::toString).collect(Collectors.joining(","));
+
+ Criteria criteria =
+ Criteria.builder()
+ .setTypes(Image.class, float[].class)
+ .optModelUrls(
+ "https://resources.djl.ai/test-models/pytorch/face_feature.zip")
+ .optModelName("face_feature") // specify model file prefix
+ .optArgument("normalize", normalize)
+ .optTranslatorFactory(new ImageFeatureExtractorFactory())
+ .optProgress(new ProgressBar())
+ .optEngine("PyTorch") // Use PyTorch engine
+ .build();
+
+ try (ZooModel model = criteria.loadModel()) {
+ Predictor predictor = model.newPredictor();
+ 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 {
+
+ }*/
+}