1、人脸模块:新增小视科技(MiniVision)活体检测模型

2、人脸模块:新增阿里通义工作室活体检测模型
3、人脸模块:新增2个表情识别模型
4、人脸模块:新增InsightFace、ElasticFace人脸识别模型
5、人脸模块:新增Seetaface6质量评估模型
6、目标检测模块:开放更多自定义模型参数
7、人脸模块:支持base64图片
8、实现接口 AutoCloseable,支持资源的自动释放
9、OCR模块:解决加方向矫正后无法连续识别bug
10、人脸模块:解决人脸更新后缓存问题
11、优化部分功能
This commit is contained in:
dengwenjie
2025-07-07 08:45:08 +08:00
parent 3e631a060b
commit 07a8a18835
168 changed files with 8562 additions and 2850 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.17</version>
<version>1.0.19</version>
</parent>
<name>smartjavaai-common</name>

View File

@@ -1,5 +1,6 @@
package cn.smartjavaai.common.entity;
import cn.smartjavaai.common.entity.face.FaceInfo;
import lombok.Data;
/**

View File

@@ -1,11 +1,7 @@
package cn.smartjavaai.common.entity;
import cn.smartjavaai.common.enums.GenderType;
import cn.smartjavaai.common.enums.LivenessStatus;
import lombok.Data;
import java.util.List;
/**
* 检测结果-矩形区域
* @author dwj

View File

@@ -1,6 +1,5 @@
package cn.smartjavaai.common.entity;
import cn.smartjavaai.common.enums.LivenessStatus;
import lombok.Data;
/**

View File

@@ -18,6 +18,12 @@ public class R<T> {
private T data;
public static <T> R<T> ok() {
R<T> r = new R<>();
r.code = 0;
r.message = "成功";
return r;
}
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
@@ -50,6 +56,7 @@ public class R<T> {
FILE_NOT_FOUND(2, "图像文件不存在"),
NO_FACE_DETECTED(3, "未检测到人脸"),
PARAM_ERROR(4, "参数错误"),
INVALID_VIDEO(5, "视频无效"),
Unknown(-1, "未知错误");
private final int code;

View File

@@ -0,0 +1,44 @@
package cn.smartjavaai.common.entity.face;
import ai.djl.modality.Classifications;
import cn.smartjavaai.common.enums.face.FacialExpression;
import cn.smartjavaai.common.enums.face.LivenessStatus;
import lombok.Data;
/**
* 人脸表情识别结果
* @author dwj
*/
@Data
public class ExpressionResult {
/**
* 表情
*/
private FacialExpression expression;
/**
* 分数
*/
private float score;
/**
* 完整结果
*/
private Classifications classifications;
public ExpressionResult() {
}
public ExpressionResult(FacialExpression expression, float score) {
this.expression = expression;
this.score = score;
}
public ExpressionResult(FacialExpression expression, float score, Classifications classifications) {
this.expression = expression;
this.score = score;
this.classifications = classifications;
}
}

View File

@@ -1,7 +1,7 @@
package cn.smartjavaai.common.entity;
package cn.smartjavaai.common.entity.face;
import cn.smartjavaai.common.enums.EyeStatus;
import cn.smartjavaai.common.enums.GenderType;
import cn.smartjavaai.common.enums.face.EyeStatus;
import cn.smartjavaai.common.enums.face.GenderType;
import lombok.Data;
/**

View File

@@ -1,6 +1,6 @@
package cn.smartjavaai.common.entity;
package cn.smartjavaai.common.entity.face;
import cn.smartjavaai.common.enums.LivenessStatus;
import cn.smartjavaai.common.entity.Point;
import lombok.Data;
import java.util.List;
@@ -26,7 +26,7 @@ public class FaceInfo {
/**
* 活体检测结果
*/
private LivenessStatus livenessStatus;
private LivenessResult livenessStatus;
/**
* 人脸查询结果
@@ -38,6 +38,11 @@ public class FaceInfo {
*/
private float[] feature;
/**
* 表情检测结果
*/
private ExpressionResult expressionResult;
public FaceInfo() {
}
@@ -45,14 +50,16 @@ public class FaceInfo {
this.keyPoints = keyPoints;
}
public FaceInfo(List<Point> keyPoints, FaceAttribute faceAttribute, LivenessStatus livenessStatus) {
public FaceInfo(List<Point> keyPoints, FaceAttribute faceAttribute, LivenessResult livenessStatus) {
this.keyPoints = keyPoints;
this.faceAttribute = faceAttribute;
this.livenessStatus = livenessStatus;
}
public FaceInfo(FaceAttribute faceAttribute, LivenessStatus livenessStatus) {
public FaceInfo(FaceAttribute faceAttribute, LivenessResult livenessStatus) {
this.faceAttribute = faceAttribute;
this.livenessStatus = livenessStatus;
}
}

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.common.entity;
package cn.smartjavaai.common.entity.face;
import lombok.Data;
/**

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.common.entity;
package cn.smartjavaai.common.entity.face;
import lombok.Data;

View File

@@ -0,0 +1,29 @@
package cn.smartjavaai.common.entity.face;
import cn.smartjavaai.common.enums.face.LivenessStatus;
import lombok.Data;
/**
* 活体检测结果
* @author dwj
* @date 2025/6/27
*/
@Data
public class LivenessResult {
private LivenessStatus status;
private float score;
public LivenessResult() {
}
public LivenessResult(LivenessStatus status, float score) {
this.status = status;
this.score = score;
}
public LivenessResult(LivenessStatus status) {
this.status = status;
}
}

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.common.enums;
package cn.smartjavaai.common.enums.face;
/**
* 眼睛状态

View File

@@ -0,0 +1,42 @@
package cn.smartjavaai.common.enums.face;
/**
* 人脸表情枚举
* @author dwj
*/
public enum FacialExpression {
ANGRY("angry", "愤怒"),
DISGUST("disgust", "厌恶"),
FEAR("fear", "害怕"),
HAPPY("happy", "高兴"),
SAD("sad", "伤心"),
SURPRISE("surprise", "惊讶"),
NEUTRAL("neutral", "中性");
private final String label;
private final String description;
FacialExpression(String label, String description) {
this.label = label;
this.description = description;
}
public String getLabel() {
return label;
}
public String getDescription() {
return description;
}
public static FacialExpression fromLabel(String label) {
for (FacialExpression facialExpression : FacialExpression.values()) {
if (facialExpression.getLabel().equals(label)) {
return facialExpression;
}
}
throw new IllegalArgumentException("Invalid facial expression label: " + label);
}
}

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.common.enums;
package cn.smartjavaai.common.enums.face;
/**
* 性别枚举

View File

@@ -1,4 +1,4 @@
package cn.smartjavaai.common.enums;
package cn.smartjavaai.common.enums.face;
/**
* 活体检测结果

View File

@@ -0,0 +1,160 @@
package cn.smartjavaai.common.preprocess;
import cn.smartjavaai.common.entity.DetectionRectangle;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* 图片预处理
* @author dwj
* @date 2025/6/27
*/
public class BufferedImagePreprocessor {
private BufferedImage image;
private DetectionRectangle rect;
private float extendRatio = 1;
private int targetSize = 128;
private int centerCropSize = 80;
private Color paddingColor = new Color(127, 127, 127); // 默认灰色
private boolean enableSquarePadding = true;
private boolean enableScaling = true;
private boolean enableCenterCrop = false;
public BufferedImagePreprocessor(BufferedImage image, DetectionRectangle rect) {
this.image = image;
this.rect = rect;
}
public BufferedImagePreprocessor setExtendRatio(float ratio) {
this.extendRatio = ratio;
return this;
}
public BufferedImagePreprocessor setTargetSize(int size) {
this.targetSize = size;
return this;
}
public BufferedImagePreprocessor setCenterCropSize(int size) {
this.centerCropSize = size;
return this;
}
public BufferedImagePreprocessor enableSquarePadding(boolean enable) {
this.enableSquarePadding = enable;
return this;
}
public BufferedImagePreprocessor enableScaling(boolean enable) {
this.enableScaling = enable;
return this;
}
public BufferedImagePreprocessor enableCenterCrop(boolean enable) {
this.enableCenterCrop = enable;
return this;
}
public BufferedImagePreprocessor setPaddingColor(Color color) {
this.paddingColor = color;
return this;
}
public BufferedImage process() {
// Step 1: 基于检测框扩展
BufferedImage cropped = cropAndExtend();
// Step 2: 补正方形 + 背景填充
BufferedImage squared = enableSquarePadding ? squarePadding(cropped) : cropped;
// Step 3: 缩放
BufferedImage scaled = enableScaling ? scaleToTarget(squared) : squared;
// Step 4: CenterCrop
BufferedImage finalResult = enableCenterCrop ? centerCrop(scaled) : scaled;
return finalResult;
}
/**
* 检测框扩展及裁剪
* @return
*/
private BufferedImage cropAndExtend() {
int x = rect.x;
int y = rect.y;
int width = rect.width;
int height = rect.height;
int extendX = Math.round(width * extendRatio);
int extendY = Math.round(height * extendRatio);
// 计算扩展后的边界 (确保不超出图像范围)
int left = Math.max(0, x - extendX);
int right = Math.min(image.getWidth(), x + width + extendX);
int top = Math.max(0, y - extendY);
int bottom = Math.min(image.getHeight(), y + height + extendY);
// 动态计算最大可用扩展区域
int origRoiWidth = right - left;
int origRoiHeight = bottom - top;
int longSide = Math.max(origRoiWidth, origRoiHeight);
// 计算可扩展空间(不超出原图边界)
int extendLeft = Math.min(left, (longSide - origRoiWidth) / 2);
int extendRight = Math.min(image.getWidth() - right, (longSide - origRoiWidth + 1) / 2);
int extendTop = Math.min(top, (longSide - origRoiHeight) / 2);
int extendBottom = Math.min(image.getHeight() - bottom, (longSide - origRoiHeight + 1) / 2);
// 计算实际扩展后的区域
int expandedLeft = left - extendLeft;
int expandedRight = right + extendRight;
int expandedTop = top - extendTop;
int expandedBottom = bottom + extendBottom;
int expandedWidth = expandedRight - expandedLeft;
int expandedHeight = expandedBottom - expandedTop;
return image.getSubimage(expandedLeft, expandedTop, expandedWidth, expandedHeight);
}
/**
* 填充正方形
* @param src
* @return
*/
private BufferedImage squarePadding(BufferedImage src) {
int longSide = Math.max(src.getWidth(), src.getHeight());
BufferedImage squared = new BufferedImage(longSide, longSide, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g = squared.createGraphics();
g.setColor(paddingColor);
g.fillRect(0, 0, longSide, longSide);
int xOffset = (longSide - src.getWidth()) / 2;
int yOffset = (longSide - src.getHeight()) / 2;
g.drawImage(src, xOffset, yOffset, null);
g.dispose();
return squared;
}
private BufferedImage scaleToTarget(BufferedImage src) {
Image scaled = src.getScaledInstance(targetSize, targetSize, Image.SCALE_SMOOTH);
BufferedImage result = new BufferedImage(targetSize, targetSize, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g = result.createGraphics();
g.drawImage(scaled, 0, 0, null);
g.dispose();
return result;
}
private BufferedImage centerCrop(BufferedImage src) {
int startX = (src.getWidth() - centerCropSize) / 2;
int startY = (src.getHeight() - centerCropSize) / 2;
return src.getSubimage(startX, startY, centerCropSize, centerCropSize);
}
}

View File

@@ -0,0 +1,39 @@
package cn.smartjavaai.common.utils;
/**
* 数组工具类
* @author dwj
* @date 2025/6/27
*/
public class ArrayUtils {
/**
* 求和并找到最大值的索引
* @param arr1
* @param arr2
* @return
*/
public static int sumAndFindMaxIndex(float[] arr1, float[] arr2, int length) {
float[] sum = new float[length];
// 处理可能为null的情况null当作全0数组处理
for (int i = 0; i < length; i++) {
float v1 = (arr1 != null && arr1.length > i) ? arr1[i] : 0f;
float v2 = (arr2 != null && arr2.length > i) ? arr2[i] : 0f;
sum[i] = v1 + v2;
}
// 找最大值索引
int maxIndex = 0;
float maxValue = sum[0];
for (int i = 1; i < length; i++) {
if (sum[i] > maxValue) {
maxValue = sum[i];
maxIndex = i;
}
}
// 返回最大值的索引
return maxIndex;
}
}

View File

@@ -0,0 +1,38 @@
package cn.smartjavaai.common.utils;
import cn.hutool.core.codec.Base64;
/**
*
* @author dwj
* @date 2025/6/28
*/
public class Base64ImageUtils {
/**
* 将 Base64 字符串(可带头部)转图片
*/
public static byte[] base64ToImage(String base64Str){
String cleanBase64 = stripBase64Header(base64Str);
return Base64.decode(cleanBase64);
}
/**
* 检查 Base64 字符串是否带有 Data URI 头部
*/
public static boolean hasBase64Header(String base64Str) {
return base64Str != null && base64Str.startsWith("data:") && base64Str.contains(";base64,");
}
/**
* 去除 Base64 字符串的 Data URI 头部
*/
public static String stripBase64Header(String base64Str) {
if (hasBase64Header(base64Str)) {
return base64Str.substring(base64Str.indexOf(",") + 1);
}
return base64Str;
}
}

View File

@@ -332,5 +332,44 @@ public class ImageUtils {
}
/**
* 画检测框(有倾斜角)和文本
*
* @param image
* @param box
* @param text
*/
public static void drawImageRectWithText(BufferedImage image, DetectionRectangle box, String text, Color color) {
// 将绘制图像转换为Graphics2D
Graphics2D graphics = (Graphics2D) image.getGraphics();
try {
graphics.setColor(Color.RED);// 边框颜色
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
int stroke = 2;
graphics.setColor(color);// 边框颜色
graphics.drawRect(box.getX(), box.getY(), box.getWidth(), box.getHeight());
drawText(graphics, text, box.getX(), box.getY(), stroke, 4);
graphics.dispose();
} finally {
graphics.dispose();
}
}
public 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

@@ -8,6 +8,7 @@ import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
@@ -111,7 +112,14 @@ public class OpenCVUtils {
public static Mat image2Mat(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
byte[] data = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
// 强制转换为 TYPE_3BYTE_BGR自动去除透明通道
BufferedImage convertedImg = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g2d = convertedImg.createGraphics();
g2d.drawImage(img, 0, 0, null);
g2d.dispose();
byte[] data = ((DataBufferByte) convertedImg.getRaster().getDataBuffer()).getData();
Mat mat = new Mat(height, width, CvType.CV_8UC3);
mat.put(0, 0, data);
return mat;