mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-19 00:49:17 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import cn.smartjavaai.common.entity.face.FaceInfo;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import cn.smartjavaai.common.enums.LivenessStatus;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
package cn.smartjavaai.common.entity.face;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
package cn.smartjavaai.common.entity.face;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
package cn.smartjavaai.common.enums.face;
|
||||
|
||||
/**
|
||||
* 眼睛状态
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
package cn.smartjavaai.common.enums.face;
|
||||
|
||||
/**
|
||||
* 性别枚举
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
package cn.smartjavaai.common.enums.face;
|
||||
|
||||
/**
|
||||
* 活体检测结果
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user