临时提交

This commit is contained in:
dengwenjie
2025-08-29 10:30:35 +08:00
parent 8bf620a330
commit 86ea7eb03e
364 changed files with 8572 additions and 540 deletions

124
common/pom.xml Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.smartjavaai</groupId>
<artifactId>smartjavaai-parent</artifactId>
<version>1.0.24</version>
</parent>
<name>common</name>
<artifactId>common</artifactId>
<description>SmartJavaAI</description>
<url>https://github.com/geekwenjie/SmartJavaAI</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.4.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>dengwenjie</publishingServerId>
<tokenAuth>true</tokenAuth>
<deploymentName>${project.groupId}:${project.artifactId}:${project.version}</deploymentName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>-->
<doclint>none</doclint>
<additionalJOptions>
<additionalJOption>-Xdoclint:none</additionalJOption>
</additionalJOptions>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- 必须添加SCM信息 -->
<scm>
<connection>scm:git:git://github.com/geekwenjie/SmartJavaAI.git</connection>
<developerConnection>scm:git:ssh://github.com/geekwenjie/SmartJavaAI.git</developerConnection>
<url>http://github.com/geekwenjie/SmartJavaAI/tree/master</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>dengwenjie</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>dengwenjie</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<developers>
<developer>
<name>dengwenjie</name>
<email>775747758@qq.com</email>
<roles>
<role>Project Manager</role>
<role>Architect</role>
</roles>
</developer>
</developers>
</project>

View File

@@ -0,0 +1,97 @@
package cn.smartjavaai.common.config;
import cn.hutool.core.io.FileUtil;
import cn.hutool.system.SystemUtil;
import cn.hutool.system.UserInfo;
import cn.smartjavaai.common.utils.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.nio.file.Paths;
/**
* 全局配置
* @author dwj
* @date 2025/4/15
*/
@Slf4j
public class Config {
/**
* 默认缓存文件夹
*/
private static final String CACHE_DIR = "smartjavaai_cache";
private static String cachePath;
static{
createCachePath();
if(StringUtils.isNotBlank(cachePath)){
System.setProperty("DJL_CACHE_DIR", cachePath);
}
System.setProperty("ai.djl.default_engine", "PyTorch");
log.info("设置默认引擎:{}", "PyTorch");
}
// 设置缓存路径的方法
public static void setCachePath(String customeCachePath) {
if (StringUtils.isNotBlank(customeCachePath)) {
/*if(!FileUtils.isValidDirectory(customeCachePath)){
throw new IllegalArgumentException("无效的缓存路径");
}*/
cachePath = customeCachePath;
FileUtil.mkdir(cachePath);
// 如果需要在此时直接设置系统属性
System.setProperty("DJL_CACHE_DIR", cachePath);
} else {
throw new IllegalArgumentException("缓存路径不允许为空");
}
}
// 获取缓存路径的方法
public static String getCachePath() {
if(StringUtils.isBlank(cachePath)){
createCachePath();
}
if(StringUtils.isNotBlank(cachePath)){
System.setProperty("DJL_CACHE_DIR", cachePath);
}
return cachePath;
}
// 获取当前缓存路径的系统属性(如果需要在其他地方使用)
public static String getCachePathFromSystem() {
return System.getProperty("DJL_CACHE_DIR");
}
private static void createCachePath(){
String osName = SystemUtil.getOsInfo().getName();
log.info("当前操作系统:{}", osName);
if(osName.toLowerCase().contains("windows")){
cachePath = Paths.get(
SystemUtil.getUserInfo().getHomeDir(),
"smartjavaai_cache"
).toString();
FileUtil.mkdir(cachePath);
}else if(osName.toLowerCase().contains("linux")){
cachePath = "/root/" + CACHE_DIR;
FileUtil.mkdir(cachePath);
}else if(osName.toLowerCase().contains("mac")){
cachePath = Paths.get(
SystemUtil.getUserInfo().getHomeDir(),
"smartjavaai_cache"
).toString();
FileUtil.mkdir(cachePath);
}else{
cachePath = Paths.get(
SystemUtil.getUserInfo().getHomeDir(),
"smartjavaai_cache"
).toString();
FileUtil.mkdir(cachePath);
}
}
}

View File

@@ -0,0 +1,61 @@
package cn.smartjavaai.common.config;
import cn.smartjavaai.common.enums.DeviceEnum;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模型配置
* @author dwj
*/
@Data
public class ModelConfig {
/**
* 设备类型
*/
private DeviceEnum device;
/**
* gpu设备ID 当device为GPU时生效
*/
private int gpuId = 0;
/**
* 批量数据打包方式stackpadding
*/
private String batchifier;
/**
* 模型预测器池大小(默认为cpu核心数)
*/
private int predictorPoolSize;
/**
* 个性化配置(按模型类型动态解析)
*/
private ConcurrentHashMap<String, Object> customParams = new ConcurrentHashMap<>();
public <T> T getCustomParam(String key, Class<T> clazz) {
Object value = customParams.get(key);
if (value == null) return null;
return clazz.cast(value);
}
/**
* 添加个性化配置项
*/
public void putCustomParam(String key, Object value) {
if (customParams == null) {
customParams = new ConcurrentHashMap<>();
}
customParams.put(key, value);
}
}

View File

@@ -0,0 +1,65 @@
package cn.smartjavaai.common.cv;
import ai.djl.modality.cv.BufferedImageFactory;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import ai.djl.opencv.OpenCVImageFactory;
import ai.djl.util.Utils;
import cn.smartjavaai.common.utils.Base64ImageUtils;
import cn.smartjavaai.common.utils.OpenCVUtils;
import nu.pattern.OpenCV;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.file.Path;
/**
* 图片处理工厂类
* @author dwj
*/
public class SmartImageFactory extends BufferedImageFactory {
private static volatile SmartImageFactory instance;
public static SmartImageFactory newInstance() {
if (instance == null) {
synchronized (SmartImageFactory.class) {
if (instance == null) {
instance = new SmartImageFactory();
}
}
}
return instance;
}
public static SmartImageFactory getInstance(){
return newInstance();
}
public Image fromBufferedImage(BufferedImage sourceImage){
return fromImage(OpenCVUtils.image2Mat(sourceImage));
}
public Image fromBase64(String base64Image) throws IOException {
return fromUrl(base64Image);
}
public Image fromBytes(byte[] imageData){
return fromImage(new ByteArrayInputStream(imageData));
}
}

View File

@@ -0,0 +1,66 @@
package cn.smartjavaai.common.entity;
import cn.smartjavaai.common.entity.face.FaceInfo;
import lombok.Data;
/**
* 检测结果信息
* @author dwj
* @date 2025/5/7
*/
@Data
public class DetectionInfo {
/**
* 检测位置信息
*/
private DetectionRectangle detectionRectangle;
/**
* 检测得分
*/
private float score;
/**
* 人脸信息
*/
private FaceInfo faceInfo;
/**
* 目标检测信息
*/
private ObjectDetInfo objectDetInfo;
/**
* 目标分割信息
*/
private InstanceSegInfo instanceSegInfo;
/**
* 旋转框信息
*/
private ObbDetInfo obbDetInfo;
public DetectionInfo() {
}
public DetectionInfo(DetectionRectangle detectionRectangle) {
this.detectionRectangle = detectionRectangle;
}
public DetectionInfo(DetectionRectangle detectionRectangle, float score) {
this.detectionRectangle = detectionRectangle;
this.score = score;
}
public DetectionInfo(DetectionRectangle detectionRectangle, float score, FaceInfo faceInfo) {
this.detectionRectangle = detectionRectangle;
this.score = score;
this.faceInfo = faceInfo;
}
}

View File

@@ -0,0 +1,27 @@
package cn.smartjavaai.common.entity;
import lombok.Data;
/**
* 检测结果-矩形区域
* @author dwj
*/
@Data
public class DetectionRectangle {
public int x;
public int y;
public int width;
public int height;
public DetectionRectangle() {
}
public DetectionRectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}

View File

@@ -0,0 +1,27 @@
package cn.smartjavaai.common.entity;
import ai.djl.modality.cv.Image;
import lombok.Data;
import java.util.List;
/**
* 检测结果
* @author dwj
* @date 2025/4/12
*/
@Data
public class DetectionResponse {
private List<DetectionInfo> detectionInfoList;
private Image drawnImage;
public DetectionResponse() {
}
public DetectionResponse(List<DetectionInfo> detectionInfoList) {
this.detectionInfoList = detectionInfoList;
}
}

View File

@@ -0,0 +1,29 @@
package cn.smartjavaai.common.entity;
import lombok.Data;
/**
* 目标分割信息
* @author dwj
*/
@Data
public class InstanceSegInfo {
/**
* 类别名称
*/
private String className;
/**
* 遮罩
*/
private float[][] mask;
public InstanceSegInfo() {
}
public InstanceSegInfo(String className, float[][] mask) {
this.className = className;
this.mask = mask;
}
}

View File

@@ -0,0 +1,138 @@
package cn.smartjavaai.common.entity;
/**
* 语言枚举
* @author dwj
*/
public enum Language {
EN("en", "English", "英语"),
ZH("zh", "Chinese", "中文"),
DE("de", "German", "德语"),
ES("es", "Spanish", "西班牙语"),
RU("ru", "Russian", "俄语"),
KO("ko", "Korean", "韩语"),
FR("fr", "French", "法语"),
JA("ja", "Japanese", "日语"),
PT("pt", "Portuguese", "葡萄牙语"),
TR("tr", "Turkish", "土耳其语"),
PL("pl", "Polish", "波兰语"),
CA("ca", "Catalan", "加泰罗尼亚语"),
NL("nl", "Dutch", "荷兰语"),
AR("ar", "Arabic", "阿拉伯语"),
SV("sv", "Swedish", "瑞典语"),
IT("it", "Italian", "意大利语"),
ID("id", "Indonesian", "印尼语"),
HI("hi", "Hindi", "印地语"),
FI("fi", "Finnish", "芬兰语"),
VI("vi", "Vietnamese", "越南语"),
HE("he", "Hebrew", "希伯来语"),
UK("uk", "Ukrainian", "乌克兰语"),
EL("el", "Greek", "希腊语"),
MS("ms", "Malay", "马来语"),
CS("cs", "Czech", "捷克语"),
RO("ro", "Romanian", "罗马尼亚语"),
DA("da", "Danish", "丹麦语"),
HU("hu", "Hungarian", "匈牙利语"),
TA("ta", "Tamil", "泰米尔语"),
NO("no", "Norwegian", "挪威语"),
TH("th", "Thai", "泰语"),
UR("ur", "Urdu", "乌尔都语"),
HR("hr", "Croatian", "克罗地亚语"),
BG("bg", "Bulgarian", "保加利亚语"),
LT("lt", "Lithuanian", "立陶宛语"),
LA("la", "Latin", "拉丁语"),
MI("mi", "Maori", "毛利语"),
ML("ml", "Malayalam", "马拉雅拉姆语"),
CY("cy", "Welsh", "威尔士语"),
SK("sk", "Slovak", "斯洛伐克语"),
TE("te", "Telugu", "泰卢固语"),
FA("fa", "Persian", "波斯语"),
LV("lv", "Latvian", "拉脱维亚语"),
BN("bn", "Bengali", "孟加拉语"),
SR("sr", "Serbian", "塞尔维亚语"),
AZ("az", "Azerbaijani", "阿塞拜疆语"),
SL("sl", "Slovenian", "斯洛文尼亚语"),
KN("kn", "Kannada", "卡纳达语"),
ET("et", "Estonian", "爱沙尼亚语"),
MK("mk", "Macedonian", "马其顿语"),
BR("br", "Breton", "布列塔尼语"),
EU("eu", "Basque", "巴斯克语"),
IS("is", "Icelandic", "冰岛语"),
HY("hy", "Armenian", "亚美尼亚语"),
NE("ne", "Nepali", "尼泊尔语"),
MN("mn", "Mongolian", "蒙古语"),
BS("bs", "Bosnian", "波斯尼亚语"),
KK("kk", "Kazakh", "哈萨克语"),
SQ("sq", "Albanian", "阿尔巴尼亚语"),
SW("sw", "Swahili", "斯瓦希里语"),
GL("gl", "Galician", "加利西亚语"),
MR("mr", "Marathi", "马拉地语"),
PA("pa", "Punjabi", "旁遮普语"),
SI("si", "Sinhala", "僧伽罗语"),
KM("km", "Khmer", "高棉语"),
SN("sn", "Shona", "修纳语"),
YO("yo", "Yoruba", "约鲁巴语"),
SO("so", "Somali", "索马里语"),
AF("af", "Afrikaans", "南非荷兰语"),
OC("oc", "Occitan", "奥克语"),
KA("ka", "Georgian", "格鲁吉亚语"),
BE("be", "Belarusian", "白俄罗斯语"),
TG("tg", "Tajik", "塔吉克语"),
SD("sd", "Sindhi", "信德语"),
GU("gu", "Gujarati", "古吉拉特语"),
AM("am", "Amharic", "阿姆哈拉语"),
YI("yi", "Yiddish", "意第绪语"),
LO("lo", "Lao", "老挝语"),
UZ("uz", "Uzbek", "乌兹别克语"),
FO("fo", "Faroese", "法罗语"),
HT("ht", "Haitian Creole", "海地克里奥尔语"),
PS("ps", "Pashto", "普什图语"),
TK("tk", "Turkmen", "土库曼语"),
NN("nn", "Nynorsk", "新挪威语"),
MT("mt", "Maltese", "马耳他语"),
SA("sa", "Sanskrit", "梵语"),
LB("lb", "Luxembourgish", "卢森堡语"),
MY("my", "Myanmar", "缅甸语"),
BO("bo", "Tibetan", "藏语"),
TL("tl", "Tagalog", "他加禄语"),
MG("mg", "Malagasy", "马尔加什语"),
AS("as", "Assamese", "阿萨姆语"),
TT("tt", "Tatar", "鞑靼语"),
HAW("haw", "Hawaiian", "夏威夷语"),
LN("ln", "Lingala", "林加拉语"),
HA("ha", "Hausa", "豪萨语"),
BA("ba", "Bashkir", "巴什基尔语"),
JW("jw", "Javanese", "爪哇语"),
SU("su", "Sundanese", "巽他语"),
YUE("yue", "Cantonese", "粤语");
private final String code; // Whisper语言代码
private final String englishName; // 英文名称
private final String chineseName; // 中文名称
Language(String code, String englishName, String chineseName) {
this.code = code;
this.englishName = englishName;
this.chineseName = chineseName;
}
public String getCode() {
return code;
}
public String getEnglishName() {
return englishName;
}
public String getChineseName() {
return chineseName;
}
@Override
public String toString() {
return code;
}
}

View File

@@ -0,0 +1,29 @@
package cn.smartjavaai.common.entity;
import java.util.List;
/**
* 定向边界框 检测结果
* @author dwj
*/
public class ObbDetInfo {
/**
* 类别名称
*/
private String className;
/**
* 检测框坐标
*/
private RotatedBox rotatedBox;
public ObbDetInfo() {
}
public ObbDetInfo(String className, RotatedBox rotatedBox) {
this.className = className;
this.rotatedBox = rotatedBox;
}
}

View File

@@ -0,0 +1,21 @@
package cn.smartjavaai.common.entity;
import lombok.Data;
/**
* 目标检测信息
* @author dwj
* @date 2025/5/7
*/
@Data
public class ObjectDetInfo {
private String className;
public ObjectDetInfo() {
}
public ObjectDetInfo(String className) {
this.className = className;
}
}

View File

@@ -0,0 +1,47 @@
package cn.smartjavaai.common.entity;
import ai.djl.util.JsonUtils;
import java.io.Serializable;
/**
* 点
* @author dwj
*/
public class Point implements Serializable {
private static final long serialVersionUID = 1L;
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
@Override
public String toString() {
return JsonUtils.GSON_COMPACT.toJson(this);
}
public org.opencv.core.Point toCvPoint() {
return new org.opencv.core.Point(x, y);
}
}

View File

@@ -0,0 +1,93 @@
package cn.smartjavaai.common.entity;
import lombok.Data;
import java.util.Arrays;
import java.util.Optional;
/**
* 通用响应封装类,用于统一接口返回结构
* @author dwj
* @date 2025/6/4
*/
@Data
public class R<T> {
private Integer code;
private String message;
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<>();
r.code = 0;
r.message = "成功";
r.data = data;
return r;
}
public static <T> R<T> fail(Integer code, String message) {
R<T> r = new R<>();
r.code = code;
r.message = message;
r.data = null;
return r;
}
public static <T> R<T> fail(Status status) {
R<T> r = new R<>();
r.code = status.code;
r.message = status.message;
r.data = null;
return r;
}
public enum Status {
SUCCESS(0, "成功"),
INVALID_IMAGE(1, "图像无效"),
FILE_NOT_FOUND(2, "文件不存在"),
NO_FACE_DETECTED(3, "未检测到人脸"),
PARAM_ERROR(4, "参数错误"),
INVALID_VIDEO(5, "视频无效"),
NO_OBJECT_DETECTED(6, "未检测到目标"),
Unknown(-1, "未知错误");
private final int code;
private final String message;
Status(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public static Status valueOf(int val) {
Optional<Status> search = Arrays.stream(values()).filter((status) -> {
return status.code == val;
}).findFirst();
return (Status)search.orElse(Unknown);
}
}
public boolean isSuccess() {
return code != null && code.equals(Status.SUCCESS.code);
}
}

View File

@@ -0,0 +1,38 @@
package cn.smartjavaai.common.entity;
/**
* 旋转框
* @author dwj
*/
public class RotatedBox {
/**
* 左上角
*/
private Point topLeft;
/**
* 右上角
*/
private Point topRight;
/**
* 右下角
*/
private Point bottomRight;
/**
* 左下角
*/
private Point bottomLeft;
public RotatedBox(Point topLeft, Point topRight, Point bottomRight, Point bottomLeft) {
this.topLeft = topLeft;
this.topRight = topRight;
this.bottomRight = bottomRight;
this.bottomLeft = bottomLeft;
}
public RotatedBox() {
}
}

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

@@ -0,0 +1,50 @@
package cn.smartjavaai.common.entity.face;
import cn.smartjavaai.common.enums.face.EyeStatus;
import cn.smartjavaai.common.enums.face.GenderType;
import lombok.Data;
/**
* 人脸属性
* @author dwj
* @date 2025/5/7
*/
@Data
public class FaceAttribute {
/**
* 性别
*/
private GenderType genderType;
/**
* 年龄
*/
private Integer age;
/**
* 左眼状态
*/
private EyeStatus leftEyeStatus;
/**
* 右眼状态
*/
private EyeStatus rightEyeStatus;
/**
* 是否带口罩
*/
private Boolean wearingMask;
/**
* 姿态
*/
private HeadPose headPose;
public FaceAttribute() {
}
}

View File

@@ -0,0 +1,65 @@
package cn.smartjavaai.common.entity.face;
import cn.smartjavaai.common.entity.Point;
import lombok.Data;
import java.util.List;
/**
* 人脸信息
* @author dwj
* @date 2025/5/7
*/
@Data
public class FaceInfo {
/**
* 人脸关键点
*/
private List<Point> keyPoints;
/**
* 人脸属性
*/
private FaceAttribute faceAttribute;
/**
* 活体检测结果
*/
private LivenessResult livenessStatus;
/**
* 人脸查询结果
*/
private List<FaceSearchResult> faceSearchResults;
/**
* 人脸特征
*/
private float[] feature;
/**
* 表情检测结果
*/
private ExpressionResult expressionResult;
public FaceInfo() {
}
public FaceInfo(List<Point> keyPoints) {
this.keyPoints = keyPoints;
}
public FaceInfo(List<Point> keyPoints, FaceAttribute faceAttribute, LivenessResult livenessStatus) {
this.keyPoints = keyPoints;
this.faceAttribute = faceAttribute;
this.livenessStatus = livenessStatus;
}
public FaceInfo(FaceAttribute faceAttribute, LivenessResult livenessStatus) {
this.faceAttribute = faceAttribute;
this.livenessStatus = livenessStatus;
}
}

View File

@@ -0,0 +1,37 @@
package cn.smartjavaai.common.entity.face;
import lombok.Data;
/**
* 人脸向量搜索结果
* @author dwj
*/
@Data
public class FaceSearchResult {
/**
* 向量ID
*/
private String id;
/**
* 相似度分数
*/
private float similarity;
/**
* 元数据
*/
private String metadata;
/**
* 构造函数
* @param id 向量ID
* @param similarity 相似度分数
* @param metadata 元数据
*/
public FaceSearchResult(String id, float similarity, String metadata) {
this.id = id;
this.similarity = similarity;
this.metadata = metadata;
}
}

View File

@@ -0,0 +1,32 @@
package cn.smartjavaai.common.entity.face;
import lombok.Data;
/**
* 姿态检测结果(单位:度)
* pitch上下俯仰角正值抬头负值低头
* yaw左右偏航角正值右偏负值左偏
* roll倾斜翻滚角正值右倾负值左倾
*/
@Data
public class HeadPose {
/** 俯仰角:头上下抬(-90°~+90° */
private Float pitch;
/** 偏航角:头左右转(-90°~+90° */
private Float yaw;
/** 翻滚角:头部倾斜(-90°~+90° */
private Float roll;
public HeadPose() {
}
public HeadPose(Float pitch, Float yaw, Float roll) {
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
}
}

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

@@ -0,0 +1,8 @@
package cn.smartjavaai.common.entity.ocr;
/**
* 表格结构
* @author dwj
*/
public class TableStructure {
}

View File

@@ -0,0 +1,14 @@
package cn.smartjavaai.common.enums;
/**
* 目标检测模型枚举
* @author dwj
* @date 2025/4/4
*/
public enum DeviceEnum {
// resnet50 系列
CPU,
GPU;
}

View File

@@ -0,0 +1,12 @@
package cn.smartjavaai.common.enums;
/**
* 视频源类型枚举
* @author dwj
* @date 2025/8/27
*/
public enum VideoSourceType {
STREAM, // RTSP 或 HTTP 流
FILE, // 本地视频文件
CAMERA; // 本地摄像头
}

View File

@@ -0,0 +1,42 @@
package cn.smartjavaai.common.enums.face;
/**
* 眼睛状态
* @author dwj
* @date 2025/5/7
*/
public enum EyeStatus {
OPEN(0, "睁眼"),
CLOSED(1, "闭眼"),
NON_EYE_REGION(2, "非眼部区域"),
UNKNOWN(3, "未知状态");
private final int code;
private final String description;
EyeStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static EyeStatus fromCode(int code) {
for (EyeStatus status : EyeStatus.values()) {
if (status.getCode() == code) {
return status;
}
}
return UNKNOWN; // 默认返回未知
}
}

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

@@ -0,0 +1,40 @@
package cn.smartjavaai.common.enums.face;
/**
* 性别枚举
* @author dwj
* @date 2025/5/6
*/
public enum GenderType {
MALE(0, ""),
FEMALE(1, ""),
UNKNOWN(2, "未知");
private final int code;
private final String description;
GenderType(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static GenderType fromCode(int code) {
for (GenderType status : GenderType.values()) {
if (status.getCode() == code) {
return status;
}
}
return UNKNOWN; // 默认返回未知
}
}

View File

@@ -0,0 +1,40 @@
package cn.smartjavaai.common.enums.face;
/**
* 活体检测结果
* @author dwj
* @date 2025/4/29
*/
public enum LivenessStatus {
LIVE(0, "活体"),
NON_LIVE(1, "非活体"),
UNKNOWN(2, "未知"),
DETECTING(3, "正在检测");
private final int code;
private final String description;
LivenessStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static LivenessStatus fromCode(int code) {
for (LivenessStatus status : LivenessStatus.values()) {
if (status.getCode() == code) {
return status;
}
}
return UNKNOWN; // 默认返回未知
}
}

View File

@@ -0,0 +1,38 @@
package cn.smartjavaai.common.pool;
import ai.djl.inference.Predictor;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.NoBatchifyTranslator;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
/**
* @author dwj
* @date 2025/6/14
*/
public class CommonPredictorFactory extends BasePooledObjectFactory<Predictor<?, ?>> {
private final ZooModel<?, ?> model;
private final NoBatchifyTranslator<?, ?> translator;
public CommonPredictorFactory(ZooModel<?, ?> model, NoBatchifyTranslator<?, ?> translator) {
this.model = model;
this.translator = translator;
}
@Override
public Predictor<?, ?> create() {
return model.newPredictor(translator);
}
@Override
public PooledObject<Predictor<?, ?>> wrap(Predictor<?, ?> predictor) {
return new DefaultPooledObject<>(predictor);
}
@Override
public void destroyObject(PooledObject<Predictor<?, ?>> p) {
p.getObject().close();
}
}

View File

@@ -0,0 +1,71 @@
package cn.smartjavaai.common.pool;
import ai.djl.inference.Predictor;
import ai.djl.repository.zoo.ZooModel;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模型共享池管理器
* @author dwj
* @date 2025/4/8
*/
public class ModelPredictorPoolManager {
// 每个模型的唯一key -> 对应Predictor池
private final Map<String, GenericObjectPool<? extends Predictor<?, ?>>> poolMap = new ConcurrentHashMap<>();
/**
* 注册模型池
* @param key 模型标识符(自定义,如模型路径、模型名等)
* @param model 模型本体
* @param config 池配置(可选)
*/
public <I, O> void registerModel(String key, ZooModel<I, O> model, GenericObjectPoolConfig<Predictor<I, O>> config) {
PredictorFactory<I, O> factory = new PredictorFactory<>(model);
GenericObjectPool<Predictor<I, O>> pool = new GenericObjectPool<>(factory, config);
poolMap.put(key, pool);
}
/**
* 借出一个 Predictor
*/
@SuppressWarnings("unchecked")
public <I, O> Predictor<I, O> borrowPredictor(String key) throws Exception {
GenericObjectPool<Predictor<I, O>> pool = (GenericObjectPool<Predictor<I, O>>) poolMap.get(key);
if (pool == null) {
throw new IllegalArgumentException("模型未注册: " + key);
}
return pool.borrowObject();
}
/**
* 归还一个 Predictor
*/
@SuppressWarnings("unchecked")
public <I, O> void returnPredictor(String key, Predictor<I, O> predictor) {
GenericObjectPool<Predictor<I, O>> pool = (GenericObjectPool<Predictor<I, O>>) poolMap.get(key);
if (pool != null) {
pool.returnObject(predictor);
}
}
/**
* 销毁全部池
*/
public void closeAll() {
for (GenericObjectPool<? extends Predictor<?, ?>> pool : poolMap.values()) {
pool.close();
}
poolMap.clear();
}
}

View File

@@ -0,0 +1,40 @@
package cn.smartjavaai.common.pool;
import ai.djl.inference.Predictor;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.Translator;
import lombok.extern.slf4j.Slf4j;
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
*/
@Slf4j
public class PredictorFactory<I, O> extends BasePooledObjectFactory<Predictor<I, O>> {
private final ZooModel<I, O> model;
public PredictorFactory(ZooModel<I, O> model) {
this.model = model;
}
@Override
public Predictor<I, O> create() {
log.debug("create predictor");
return model.newPredictor();
}
@Override
public PooledObject<Predictor<I, O>> wrap(Predictor<I, O> predictor) {
return new DefaultPooledObject<>(predictor);
}
@Override
public void destroyObject(PooledObject<Predictor<I, O>> p) {
log.debug("close predictor");
p.getObject().close();
}
}

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

@@ -0,0 +1,32 @@
package cn.smartjavaai.common.utils;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* @author dwj
*/
public class DJLCommonUtils {
/**
* 检查模型目录中是否存在 "serving.properties" 文件
*
* @param modelPath 模型目录路径
* @return true 表示存在false 表示不存在
*/
public static boolean isServingPropertiesExists(Path modelPath) {
if (modelPath == null || !Files.exists(modelPath)) {
return false;
}
// 确定目录路径
Path dirPath = Files.isDirectory(modelPath) ? modelPath : modelPath.getParent();
if (dirPath == null) {
return false; // 可能是根目录的文件
}
// 判断目录下的 serving.properties 是否存在
Path servingFile = dirPath.resolve("serving.properties");
return Files.exists(servingFile);
}
}

View File

@@ -0,0 +1,31 @@
package cn.smartjavaai.common.utils;
import java.io.File;
/**
* 文件操作工具类
* @author dwj
* @date 2025/4/4
*/
public class FileUtils {
/**
* 检查文件是否存在
* @param filePath
* @return
*/
public static boolean isFileExists(String filePath) {
File file = new File(filePath);
return file.exists() && !file.isDirectory(); // 确保是文件且存在
}
/**
* 检查目录是否存在
* @param path
* @return
*/
public static boolean isValidDirectory(String path) {
File file = new File(path);
return file.exists() && file.isDirectory();
}
}

View File

@@ -0,0 +1,66 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.CvMat;
import org.bytedeco.opencv.opencv_core.Mat;
import org.opencv.core.CvType;
import java.awt.image.BufferedImage;
/**
* @author dwj
* @date 2025/8/27
*/
public class FrameConverterUtil {
/**
* 将 Bytedeco Mat 转为 DJL Image
* 支持 1/3/4 通道
*/
public static Image matToDJLImage(Mat cvMat) {
if (cvMat == null || cvMat.empty()) {
return null;
}
int width = cvMat.cols();
int height = cvMat.rows();
int channels = cvMat.channels();
int[] pixels = new int[width * height];
if (channels == 1) { // 灰度图
byte[] data = new byte[width * height];
cvMat.data().get(data);
for (int i = 0; i < width * height; i++) {
int gray = data[i] & 0xFF;
pixels[i] = (0xFF << 24) | (gray << 16) | (gray << 8) | gray;
}
} else if (channels == 3) { // BGR
byte[] data = new byte[width * height * 3];
cvMat.data().get(data);
for (int i = 0; i < width * height; i++) {
int b = data[i * 3] & 0xFF;
int g = data[i * 3 + 1] & 0xFF;
int r = data[i * 3 + 2] & 0xFF;
pixels[i] = (0xFF << 24) | (r << 16) | (g << 8) | b;
}
} else if (channels == 4) { // BGRA
byte[] data = new byte[width * height * 4];
cvMat.data().get(data);
for (int i = 0; i < width * height; i++) {
int b = data[i * 4] & 0xFF;
int g = data[i * 4 + 1] & 0xFF;
int r = data[i * 4 + 2] & 0xFF;
int a = data[i * 4 + 3] & 0xFF;
pixels[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
} else {
throw new IllegalArgumentException("只支持 1/3/4 通道图像");
}
return ImageFactory.getInstance().fromPixels(pixels, width, height);
}
}

View File

@@ -0,0 +1,507 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.BufferedImageFactory;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
//import java.awt.image.ColorConvertOp;
import java.awt.image.ComponentSampleModel;
import java.awt.image.ImageObserver;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
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.Objects;
/**
* 图片处理工具类
*/
public class ImageUtils {
/**
* @param image
* @param bandOffset 用于推断通道顺序
* @return
*/
private static boolean equalBandOffsetWith3Byte(BufferedImage image, int[] bandOffset) {
if (image.getType() == BufferedImage.TYPE_3BYTE_BGR) {
if (image.getData().getSampleModel() instanceof ComponentSampleModel) {
ComponentSampleModel sampleModel = (ComponentSampleModel) image.getData().getSampleModel();
if (Arrays.equals(sampleModel.getBandOffsets(), bandOffset)) {
return true;
}
}
}
return false;
}
/**
* 推断图像是否为BGR格式
*
* @return
*/
public static boolean isBGR3Byte(BufferedImage image) {
return equalBandOffsetWith3Byte(image, new int[]{0, 1, 2});
}
/**
* 对图像解码返回BGR格式矩阵数据
*
* @param image
* @return
*/
public static byte[] getMatrixBGR(BufferedImage image) {
byte[] matrixBGR;
if (isBGR3Byte(image)) {
matrixBGR = (byte[]) image.getData().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
} else {
// ARGB格式图像数据
int intrgb[] = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());
matrixBGR = new byte[image.getWidth() * image.getHeight() * 3];
// ARGB转BGR格式
for (int i = 0, j = 0; i < intrgb.length; ++i, j += 3) {
matrixBGR[j] = (byte) (intrgb[i] & 0xff);
matrixBGR[j + 1] = (byte) ((intrgb[i] >> 8) & 0xff);
matrixBGR[j + 2] = (byte) ((intrgb[i] >> 16) & 0xff);
}
}
return matrixBGR;
}
public static BufferedImage bgrToBufferedImage(byte[] data, int width, int height) {
int type = BufferedImage.TYPE_3BYTE_BGR;
// bgr to rgb
byte b;
for (int i = 0; i < data.length; i = i + 3) {
b = data[i];
data[i] = data[i + 2];
data[i + 2] = b;
}
BufferedImage image = new BufferedImage(width, height, type);
image.getRaster().setDataElements(0, 0, width, height, data);
return image;
}
/**
* 检查图像是否有效
* @param image
* @return
*/
public static boolean isImageValid(BufferedImage image) {
// 检查是否为 null 或尺寸异常如宽高为0
return image != null && image.getWidth() > 0 && image.getHeight() > 0;
}
/**
* 画检测框
*
* @param image
* @param x
* @param y
* @param width
* @param height
*/
public static void drawImageRect(BufferedImage image, int x, int y, int width, int height) {
// 将绘制图像转换为Graphics2D
Graphics2D g = (Graphics2D) image.getGraphics();
try {
g.setColor(new Color(0, 255, 0));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawRect(x, y, width, height);
} finally {
g.dispose();
}
}
/**
* 画检测框
*
* @param image
* @param x
* @param y
* @param width
* @param height
*/
public static void drawBufferedImageRect(Image image, int x, int y, int width, int height) {
// 将绘制图像转换为Graphics2D
BufferedImage bufferedImage = (BufferedImage)image.getWrappedImage();
Graphics2D g = (Graphics2D) bufferedImage.getGraphics();
try {
g.setColor(new Color(0, 255, 0));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawRect(x, y, width, height);
} finally {
g.dispose();
}
}
/**
* 保存BufferedImage图片
*
* @param img
* @param name
* @param path
*/
public static void saveImage(BufferedImage img, String name, String path) {
Mat mat = OpenCVUtils.image2Mat(img);
Image djlImg = ImageFactory.getInstance().fromImage(mat); // 支持多种图片格式,自动适配
Path outputDir = Paths.get(path);
Path imagePath = outputDir.resolve(name);
// OpenJDK 不能保存 jpg 图片的 alpha channel
try {
djlImg.save(Files.newOutputStream(imagePath), "png");
} catch (IOException e) {
e.printStackTrace();
}
mat.release();
}
/**
* 保存BufferedImage图片
*
* @param img
* @param path
*/
public static void saveImage(BufferedImage img, String path) {
Mat mat = OpenCVUtils.image2Mat(img);
Image djlImg = ImageFactory.getInstance().fromImage(mat); // 支持多种图片格式,自动适配
Path outputDir = Paths.get(path);
// OpenJDK 不能保存 jpg 图片的 alpha channel
try {
djlImg.save(Files.newOutputStream(outputDir), "png");
} catch (IOException e) {
e.printStackTrace();
}
mat.release();
}
/**
* 保存DJL图片
*
* @param img
* @param name
* @param path
*/
public static void saveImage(Image img, String name, String path) {
Path outputDir = Paths.get(path);
Path imagePath = outputDir.resolve(name);
// OpenJDK 不能保存 jpg 图片的 alpha channel
try {
img.save(Files.newOutputStream(imagePath), "png");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 保存图片,含检测框
*
* @param img
* @param detection
* @param name
* @param path
* @throws IOException
*/
public static void saveBoundingBoxImage(
Image img, DetectedObjects detection, String name, String path) throws IOException {
// Make image copy with alpha channel because original image was jpg
img.drawBoundingBoxes(detection);
Path outputDir = Paths.get(path);
Files.createDirectories(outputDir);
Path imagePath = outputDir.resolve(name);
// OpenJDK can't save jpg with alpha channel
img.save(Files.newOutputStream(imagePath), "png");
}
/**
* 画检测框(有倾斜角)
*
* @param image
* @param box
*/
public static void drawImageRect(BufferedImage image, NDArray box) {
float[] points = box.toFloatArray();
int[] xPoints = new int[5];
int[] yPoints = new int[5];
for (int i = 0; i < 4; i++) {
xPoints[i] = (int) points[2 * i];
yPoints[i] = (int) points[2 * i + 1];
}
xPoints[4] = xPoints[0];
yPoints[4] = yPoints[0];
// 将绘制图像转换为Graphics2D
Graphics2D g = (Graphics2D) image.getGraphics();
try {
g.setColor(new Color(0, 255, 0));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
} finally {
g.dispose();
}
}
/**
* 画检测框(有倾斜角)和文本
*
* @param image
* @param box
* @param text
*/
public static void drawImageRectWithText(BufferedImage image, NDArray box, String text) {
float[] points = box.toFloatArray();
int[] xPoints = new int[5];
int[] yPoints = new int[5];
for (int i = 0; i < 4; i++) {
xPoints[i] = (int) points[2 * i];
yPoints[i] = (int) points[2 * i + 1];
}
xPoints[4] = xPoints[0];
yPoints[4] = yPoints[0];
// 将绘制图像转换为Graphics2D
Graphics2D g = (Graphics2D) image.getGraphics();
try {
int fontSize = 32;
Font font = new Font("楷体", Font.PLAIN, fontSize);
g.setFont(font);
g.setColor(new Color(0, 0, 255));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
g.drawString(text, xPoints[0], yPoints[0]);
} finally {
g.dispose();
}
}
/**
* 显示文字
*
* @param image
* @param text
* @param x
* @param y
*/
public static void drawImageText(BufferedImage image, String text, int x, int y) {
Graphics graphics = image.getGraphics();
int fontSize = 32;
Font font = new Font("楷体", Font.PLAIN, fontSize);
try {
graphics.setFont(font);
graphics.setColor(new Color(0, 0, 255));
int strWidth = graphics.getFontMetrics().stringWidth(text);
graphics.drawString(text, x, y);
} finally {
graphics.dispose();
}
}
/**
* 画检测框(有倾斜角)和文本
*
* @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();
y = Math.max(0, y - height);
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);
}
/**
* 计算左上角,右下角坐标 x0,y0,x1,y1
* Get absolute coordinations
*
* @param rect
* @param width
* @param height
* @return
*/
public static int[] rectXYXY(ai.djl.modality.cv.output.Rectangle rect, int width, int height) {
int left = Math.max((int) (width * rect.getX()), 0);
int top = Math.max((int) (height * rect.getY()), 0);
int right = Math.min((int) (width * (rect.getX() + rect.getWidth())), width - 1);
int bottom = Math.min((int) (height * (rect.getY() + rect.getHeight())), height - 1);
return new int[] {left, top, right, bottom};
}
/**
* 列出文件夹下的所有图片文件
* List all image files under the folder
*
* @param folderPath
* @return
*/
public static List<File> listImageFiles(String folderPath) {
File folder = new File(folderPath);
List<File> imageFiles = new ArrayList<>();
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles();
if (files == null) {
return imageFiles;
}
for (File file : files) {
if (file.isFile()) {
String name = file.getName().toLowerCase();
if (name.endsWith(".jpg") || name.endsWith(".jpeg") ||
name.endsWith(".png") || name.endsWith(".bmp") ||
name.endsWith(".gif") || name.endsWith(".tiff") ||
name.endsWith(".webp")) {
imageFiles.add(file);
}
}
}
}
return imageFiles;
}
/**
* 读取指定目录下所有图片,返回 List<Image>DJL 格式)
*
* @param folderPath 图片文件夹路径
* @return List<Image>
* @throws IOException
*/
public static List<Image> readImagesFromFolder(String folderPath) throws IOException {
File folder = new File(folderPath);
List<Image> imageList = new ArrayList<>();
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles();
if (files == null) {
return imageList;
}
for (File file : files) {
if (file.isFile()) {
String name = file.getName().toLowerCase();
if (name.endsWith(".jpg") || name.endsWith(".jpeg") ||
name.endsWith(".png") || name.endsWith(".bmp") ||
name.endsWith(".gif") || name.endsWith(".tiff") ||
name.endsWith(".webp")) {
Image img = ImageFactory.getInstance().fromInputStream(Files.newInputStream(file.toPath()));
imageList.add(img);
}
}
}
}
return imageList;
}
/**
* 判断所有图片尺寸是否一致
*
* @param images 图片列表
*/
public static boolean isAllImageSizeEqual(List<Image> images) {
if (images == null || images.isEmpty()) {
return true; // 空集合视为一致
}
int width = images.get(0).getWidth();
int height = images.get(0).getHeight();
for (Image img : images) {
if (img.getWidth() != width || img.getHeight() != height) {
return false;
}
}
return true;
}
/**
* 在图像上绘制带白色背景、黑色文字的文本
*/
public static void putTextWithBackground(Mat image, String text, org.opencv.core.Point origin, Scalar textColor, Scalar backgroundColor, int padding) {
// 默认字体
int font = Imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX;
// 默认字体缩放大小
double fontScale = 1.0;
//线条粗细
int thickness = 2;
//获取文字大小
int[] baseLine = new int[1];
Size textSize = Imgproc.getTextSize(text, font, fontScale, thickness, baseLine);
int textWidth = (int) textSize.width;
int textHeight = (int) textSize.height;
//计算带padding的背景框
org.opencv.core.Point bgTopLeft = new org.opencv.core.Point(origin.x - padding, origin.y - textHeight - padding);
org.opencv.core.Point bgBottomRight = new org.opencv.core.Point(origin.x + textWidth + padding, origin.y + baseLine[0] + padding);
//绘制背景矩形
Imgproc.rectangle(image, bgTopLeft, bgBottomRight, backgroundColor, Imgproc.FILLED);
//绘制文字(黑色)
Imgproc.putText(image, text, origin, font, fontScale, textColor, thickness);
}
}

View File

@@ -0,0 +1,148 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.output.Rectangle;
import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.index.NDIndex;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import java.util.Arrays;
/**
* 按比例缩放,剩余空间用指定颜色填充
* @author dwj
*/
public class LetterBoxUtils {
public enum PaddingPosition {
CENTER, LEFT_TOP, RIGHT_BOTTOM
}
public static class ResizeResult {
public NDArray image;
public float r;
public int left;
public int top;
}
public static ResizeResult letterboxWithMeta(NDArray paddingImg, float r, int left, int top) {
// ... letterbox 逻辑不变
ResizeResult result = new ResizeResult();
result.image = paddingImg;
result.r = r;
result.left = left;
result.top = top;
return result;
}
/**
* 按比例缩放 + padding
*
* @param img 原图 NDArray HWC
* @param targetW 目标宽度
* @param targetH 目标高度
* @param padColor padding 填充颜色RGB 归一化 0-1
* @param position padding 位置CENTER / LEFT_TOP / RIGHT_BOTTOM
* @return 处理后的 NDArray
*/
public static ResizeResult letterbox(NDManager manager, NDArray img, int targetW, int targetH, float padColor, PaddingPosition position) {
long origH = img.getShape().get(0);
long origW = img.getShape().get(1);
float r = Math.min(targetW / (float) origW, targetH / (float) origH);
int newW = Math.round(origW * r);
int newH = Math.round(origH * r);
img = NDImageUtils.resize(img, newW, newH); // HWC 0~1
// NDArray paddingImg = manager
// .full(new Shape(targetW, targetH, 3), padColor, DataType.UINT8);
NDArray paddingImg = manager.zeros(new Shape(targetW, targetH, 3), DataType.FLOAT32);
paddingImg = paddingImg.add(114);
int padW = targetW - newW;
int padH = targetH - newH;
int top = 0, left = 0;
switch (position) {
case CENTER:
left = padW / 2;
top = padH / 2;
break;
case LEFT_TOP:
left = 0;
top = 0;
break;
case RIGHT_BOTTOM:
left = padW;
top = padH;
break;
}
paddingImg.set(new NDIndex(String.format("%d:%d,%d:%d", top, top + newH, left, left + newW)), img);
return letterboxWithMeta(paddingImg, r, left, top);
}
/**
* 恢复缩放后的 box
* @param boxes
* @param scaleRatio
* @param left
* @param top
* @param keypointStart
* @param keypointDim
* @return
*/
public static NDArray restoreBox(NDArray boxes, float scaleRatio, float left, float top, int keypointStart, int keypointDim) {
// 处理 bbox
NDArray x1 = boxes.get(":, 0").sub(left).div(scaleRatio);
NDArray y1 = boxes.get(":, 1").sub(top).div(scaleRatio);
NDArray x2 = boxes.get(":, 2").sub(left).div(scaleRatio);
NDArray y2 = boxes.get(":, 3").sub(top).div(scaleRatio);
boxes.set(new NDIndex(":, 0"), x1);
boxes.set(new NDIndex(":, 1"), y1);
boxes.set(new NDIndex(":, 2"), x2);
boxes.set(new NDIndex(":, 3"), y2);
if (keypointDim > 0) {
for (int i = 0; i < keypointDim; i += 2) {
int xIdx = keypointStart + i;
int yIdx = keypointStart + i + 1;
NDArray keyX = boxes.get(":, " + xIdx).sub(left).div(scaleRatio);
NDArray keyY = boxes.get(":, " + yIdx).sub(top).div(scaleRatio);
boxes.set(new NDIndex(":, " + xIdx), keyX);
boxes.set(new NDIndex(":, " + yIdx), keyY);
}
}
return boxes;
}
/**
* 恢复缩放后的 box(左上角坐标)
* @param rectangle
* @param scale
* @param origImageWidth
* @param origImageHeight
*/
public static Rectangle restoreBox(Rectangle rectangle, float scale, int origImageWidth, int origImageHeight, int inputWidth, int inputHeight){
double paddingWidth = (inputWidth - origImageWidth * scale) / 2;
double paddingHeight = (inputHeight - origImageHeight * scale) / 2;
// 去掉 padding
double x_noPad = rectangle.getX() - paddingWidth;
double y_noPad = rectangle.getY() - paddingHeight;
//模型输出就是原图坐标
double x1 = x_noPad / scale / origImageWidth;
double y1 = y_noPad / scale / origImageHeight;
double boxW = rectangle.getWidth() / scale / origImageWidth ;
double boxH = rectangle.getHeight() / scale / origImageHeight;
return new Rectangle(x1, y1, boxW, boxH);
}
}

View File

@@ -0,0 +1,110 @@
package cn.smartjavaai.common.utils;
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 java.util.ArrayList;
import java.util.List;
/**
* @author dwj
* @date 2025/7/23
*/
public class NMSUtils {
/**
* 通用 NMS 方法,输入 NDArray 形式的 boxes 和 scores返回保留的索引列表
*
* @param boxes NDArray 形状为 (N, 4),格式为 [x1, y1, x2, y2]
* @param scores NDArray 形状为 (N,) 或 (N,1),每个 box 的置信度
* @param iouThreshold IOU 阈值,超过该阈值则认为有重叠
* @return 保留框的索引列表
*/
public static int[] nms(NDArray boxes, NDArray scores, float iouThreshold) {
if (boxes.isEmpty()) {
return new int[0];
}
NDArray x1 = boxes.get(":, 0");
NDArray y1 = boxes.get(":, 1");
NDArray x2 = boxes.get(":, 2");
NDArray y2 = boxes.get(":, 3");
NDArray areas = x2.sub(x1).add(1).mul(y2.sub(y1).add(1));
// 按照置信度降序排序
NDArray order = scores.argSort().flip(0);
List<Integer> keep = new ArrayList<>();
while (order.size() > 0) {
int idx = (int)order.getLong(0);
keep.add(idx);
if (order.size() == 1) break;
NDArray currentBox = boxes.get(idx);
NDArray others = boxes.get(order);
NDArray xx1 = x1.get(order).maximum(x1.get(idx));
NDArray yy1 = y1.get(order).maximum(y1.get(idx));
NDArray xx2 = x2.get(order).minimum(x2.get(idx));
NDArray yy2 = y2.get(order).minimum(y2.get(idx));
NDArray w = xx2.sub(xx1).add(1).maximum(0);
NDArray h = yy2.sub(yy1).add(1).maximum(0);
NDArray inter = w.mul(h);
NDArray remAreas = areas.get(order);
NDArray union = remAreas.add(areas.get(idx)).sub(inter);
NDArray iou = inter.div(union);
NDArray mask = iou.lte(iouThreshold);
order = order.get(mask);
}
return keep.stream().mapToInt(i -> i).toArray();
}
/**
* 批量执行 NMS输入 NDArray 形式的 boxes、scores 和 idxs返回保留的索引列表
*
* @param boxes NDArray 形状为 (N, 4),格式为 [x1, y1, x2, y2]
* @param scores NDArray 形状为 (N,) 或 (N,1),每个 box 的置信度
* @param idxs NDArray 形状为 (N,),每个 box 对应的 batch id
* @param iouThreshold IOU 阈值,超过该阈值则认为有
* @return 批量保留框的索引列表
*
*/
public static NDArray batchedNms(NDArray boxes, NDArray scores, NDArray idxs, float iouThreshold, NDManager manager) {
List<NDArray> keepList = new ArrayList<>();
// 获取唯一 batch id
NDArray uniqueIdxs = idxs.unique().get(0);
for (long batchId : uniqueIdxs.toLongArray()) {
// 找出当前 batch 的框
NDArray mask = idxs.eq(batchId);
NDArray batchBoxes = boxes.get(mask);
NDArray batchScores = scores.get(mask);
// 执行单 batch NMS
int[] keepIndices = nms(batchBoxes, batchScores, iouThreshold);
if (keepIndices.length > 0) {
// 将局部索引映射回全局索引
NDArray globalIndices = manager.arange(boxes.getShape().get(0))
.get(mask)
.toType(DataType.INT64, false)
.get(manager.create(keepIndices));
keepList.add(globalIndices);
}
}
if (keepList.isEmpty()) {
return manager.create(new long[0]);
}
return NDArrays.concat(new NDList(keepList));
}
}

View File

@@ -0,0 +1,265 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
import ai.djl.util.RandomUtils;
import cn.smartjavaai.common.entity.DetectionInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.opencv.core.*;
import org.opencv.core.Point;
import org.opencv.imgproc.Imgproc;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.util.List;
import java.util.Objects;
/**
* OpenCV 工具类
*/
public class OpenCVUtils {
/**
* canny算法边缘检测
*
* @param src
* @return
*/
public static Mat canny(Mat src) {
Mat mat = src.clone();
Imgproc.Canny(src, mat, 100, 200);
return mat;
}
/**
* 画线
*
* @param mat
* @param point1
* @param point2
*/
public static void line(Mat mat, Point point1, Point point2) {
Imgproc.line(mat, point1, point2, new Scalar(255, 255, 255), 1);
}
/**
* NDArray to opencv_core.Mat
*
* @param manager
* @param srcPoints
* @param dstPoints
* @return
*/
public static Mat toOpenCVMat(NDManager manager, NDArray srcPoints, NDArray dstPoints) {
NDArray svdMat = SVDUtils.transformationFromPoints(manager, srcPoints, dstPoints);
double[] doubleArray = svdMat.toDoubleArray();
Mat newSvdMat = new Mat(2, 3, CvType.CV_64F);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
newSvdMat.put(i, j, doubleArray[i * 3 + j]);
}
}
return newSvdMat;
}
/**
* double[][] points array to Mat
* @param points
* @return
*/
public static Mat toOpenCVMat(double[][] points) {
Mat mat = new Mat(5, 2, CvType.CV_64F);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 2; j++) {
mat.put(i, j, points[i * 5 + j]);
}
}
return mat;
}
/**
* 变换矩阵的逆矩阵
*
* @param src
* @return
*/
public static Mat invertAffineTransform(Mat src) {
Mat dst = src.clone();
Imgproc.invertAffineTransform(src, dst);
return dst;
}
/**
* Mat to BufferedImage
*
* @param mat
* @return
*/
public static BufferedImage mat2Image(Mat mat) {
int width = mat.width();
int height = mat.height();
byte[] data = new byte[width * height * (int) mat.elemSize()];
Imgproc.cvtColor(mat, mat, 4);
mat.get(0, 0, data);
BufferedImage ret = new BufferedImage(width, height, 5);
ret.getRaster().setDataElements(0, 0, width, height, data);
return ret;
}
/**
* BufferedImage to Mat
*
* @param img
* @return
*/
public static Mat image2Mat(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
// 强制转换为 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;
}
/**
* 透视变换
*
* @param src
* @param srcPoints
* @param dstPoints
* @return
*/
public static Mat perspectiveTransform(Mat src, Mat srcPoints, Mat dstPoints) {
Mat dst = src.clone();
Mat warp_mat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
Imgproc.warpPerspective(src, dst, warp_mat, dst.size());
warp_mat.release();
return dst;
}
/**
* 绘制矩形框和文字
*
* @param image
* @param detectionInfoList
*/
public static void drawRectAndText(Image image, List<DetectionInfo> detectionInfoList) {
if(CollectionUtils.isEmpty(detectionInfoList))
return;
for(DetectionInfo detectionInfo : detectionInfoList){
drawRectAndText(image, detectionInfo);
}
}
/**
* 绘制矩形框和文字
*
* @param image
* @param detectionInfo
*/
public static void drawRectAndText(Image image, DetectionInfo detectionInfo) {
Mat mat = (Mat)image.getWrappedImage();
if (image == null) return;
int x = detectionInfo.getDetectionRectangle().getX();
int y = detectionInfo.getDetectionRectangle().getY();
int width = detectionInfo.getDetectionRectangle().getWidth();
int height = detectionInfo.getDetectionRectangle().getHeight();
Scalar rectangleColor = new Scalar((double)RandomUtils.nextInt(178), (double)RandomUtils.nextInt(178), (double)RandomUtils.nextInt(178));
// 绘制矩形框
Point pt1 = new Point(x, y);
Point pt2 = new Point(x + width, y + height);
Imgproc.rectangle(mat, pt1, pt2, rectangleColor, 2);
// 绘制文字
if (Objects.nonNull(detectionInfo.getObjectDetInfo()) && StringUtils.isNotBlank(detectionInfo.getObjectDetInfo().getClassName())) {
String className = detectionInfo.getObjectDetInfo().getClassName();
Size size = Imgproc.getTextSize(className, 1, 1.3, 1, (int[])null);
Point br = new Point((double)x + size.width + 4.0, (double)y + size.height + 4.0);
Imgproc.rectangle(mat, pt1, br, rectangleColor, -1);
Point point = new Point((double)x, (double)y + size.height + 2.0);
Scalar color = new Scalar(255.0, 255.0, 255.0);
Imgproc.putText(mat, className, point, 1, 1.3, color, 1);
}
image = ImageFactory.getInstance().fromImage(mat);
}
/**
* 在Mat上绘制矩形框和文字
*
* @param mat 待绘制的Mat
* @param x 矩形左上角X
* @param y 矩形左上角Y
* @param width 矩形宽度
* @param height 矩形高度
* @param color 框的颜色,例如 new Scalar(0, 255, 0) 绿色
* @param thickness 框线宽度
* @param text 需要绘制的文字可以为null或空
* @param fontScale 文字缩放比例
* @param textColor 文字颜色
*/
public static void drawRectAndText(Mat mat,
int x, int y, int width, int height,
Scalar color, int thickness,
String text, double fontScale, Scalar textColor) {
if (mat == null || mat.empty()) return;
// 绘制矩形框
Point pt1 = new Point(x, y);
Point pt2 = new Point(x + width, y + height);
Imgproc.rectangle(mat, pt1, pt2, color, thickness);
// 绘制文字
if (text != null && !text.isEmpty()) {
int baseline[] = new int[1];
Size textSize = Imgproc.getTextSize(text, Imgproc.FONT_HERSHEY_SIMPLEX, fontScale, thickness, baseline);
// 保证文字不超出矩形
Point textOrg = new Point(x, y - 5 < 0 ? y + textSize.height + 5 : y - 5);
Imgproc.putText(mat, text, textOrg, Imgproc.FONT_HERSHEY_SIMPLEX, fontScale, textColor, thickness);
}
}
/**
* 将 Bytedeco 的 Mat 转换为 OpenCV 官方的 Mat
* @param src Bytedeco Mat (BGR 或 BGRA)
* @return OpenCV Mat (BGR 或 BGRA)
*/
public static org.opencv.core.Mat convertToOpenCVMat(org.bytedeco.opencv.opencv_core.Mat bMat) {
try {
int width = bMat.cols();
int height = bMat.rows();
int channels = bMat.channels();
// 创建 OpenCV Mat
org.opencv.core.Mat cvMat = new org.opencv.core.Mat(height, width, channels == 3 ? CvType.CV_8UC3 : CvType.CV_8UC1);
// 从 bytedeco Mat 获取像素数据
byte[] data = new byte[width * height * channels];
bMat.data().get(data);
// 填充到 OpenCV Mat
cvMat.put(0, 0, data);
return cvMat;
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,70 @@
package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.output.Point;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* @author dwj
*/
public class PointUtils {
/**
* 对 4 个关键点进行排序,顺序为:
* 左上、右上、右下、左下
*/
public static List<Point> orderPoints(List<Point> points) {
if (points == null || points.size() != 4) {
throw new IllegalArgumentException("必须提供 4 个点");
}
// 按 X 坐标升序排列
points.sort(Comparator.comparingDouble(Point::getX));
List<Point> left = points.subList(0, 2);
List<Point> right = points.subList(2, 4);
// 左侧两点按 Y 排序:上为 tl下为 bl
Point tl = left.get(0).getY() < left.get(1).getY() ? left.get(0) : left.get(1);
Point bl = left.get(0).getY() >= left.get(1).getY() ? left.get(0) : left.get(1);
// 右侧两点按 Y 排序:上为 tr下为 br
Point tr = right.get(0).getY() < right.get(1).getY() ? right.get(0) : right.get(1);
Point br = right.get(0).getY() >= right.get(1).getY() ? right.get(0) : right.get(1);
return Arrays.asList(tl, tr, br, bl);
}
/**
* 欧式距离计算
*
* @param point1
* @param point2
* @return
*/
public static float distance(float[] point1, float[] point2) {
float disX = point1[0] - point2[0];
float disY = point1[1] - point2[1];
float dis = (float) Math.sqrt(disX * disX + disY * disY);
return dis;
}
/**
* 欧式距离计算
*
* @param point1
* @param point2
* @return
*/
public static float distance(Point point1, Point point2) {
double disX = point1.getX() - point2.getX();
double disY = point1.getY() - point2.getY();
float dis = (float) Math.sqrt(disX * disX + disY * disY);
return dis;
}
}

View File

@@ -0,0 +1,25 @@
package cn.smartjavaai.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.ObjectPool;
/**
* @author dwj
* @date 2025/5/7
*/
@Slf4j
public class PoolUtils {
// 泛型方法,支持任意类型的 Predictor 和对象池
public static <T> void returnToPool(ObjectPool<T> pool, T predictor) {
if (pool == null || predictor == null) {
return;
}
try {
pool.returnObject(predictor);
} catch (Exception e) {
log.warn("归还Predictor到池失败", e);
}
}
}

View File

@@ -0,0 +1,119 @@
package cn.smartjavaai.common.utils;
import Jama.Matrix;
import Jama.SingularValueDecomposition;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDManager;
/**
* 仿射变换处理工具
*/
public class SVDUtils {
/**
* 计算仿射变换矩阵
* Calculate affine transformation matrix
*
* @param manager
* @param points1
* @param points2
* @return
*/
public static NDArray transformationFromPoints(
NDManager manager, NDArray points1, NDArray points2) {
// 按列计算均值
// Calculate column-wise mean
NDArray c1 = points1.mean(new int[]{0}); // axis=0 列操作 - axis=0 column operation
NDArray c2 = points2.mean(new int[]{0}); // axis=0 列操作 - axis=0 column operation
// 按列减去均值
// Subtract column-wise mean
points1 = points1.sub(c1);
points2 = points2.sub(c2);
// 计算全局标准差
// Calculate global standard deviation
double s1 = std(points1);
double s2 = std(points2);
// 矩阵除以全局标准差
// Matrix divided by global standard deviation
NDArray djl_s1 = manager.create(s1);
NDArray djl_s2 = manager.create(s2);
points1 = points1.div(djl_s1);
points2 = points2.div(djl_s2);
double[] points1D = points1.toDoubleArray();
double[] points2D = points2.toDoubleArray();
// DJL 格式转换成Jamma格式
// Convert DJL format to Jama format
double[][] m1 = new double[5][2];
double[][] m2 = new double[5][2];
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 2; j++) {
m1[i][j] = points1D[i * 2 + j];
}
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 2; j++) {
m2[i][j] = points2D[i * 2 + j];
}
}
Matrix p1 = new Matrix(m1);
Matrix p2 = new Matrix(m2);
// 进行奇异值分解
// Perform singular value decomposition
Matrix p3 = p1.transpose().times(p2);
SingularValueDecomposition s = p3.svd();
Matrix U = s.getU();
Matrix S = s.getS();
Matrix V = s.getV();
// TODO 为什么第2列的符号是反的
// Why is the sign of the second column opposite?
m1 = U.getArray();
m1[0][1] = -m1[0][1];
m1[1][1] = -m1[1][1];
m2 = V.getArray();
m2[0][1] = -m2[0][1];
m2[1][1] = -m2[1][1];
Matrix R = (U.times(V)).transpose();
double[][] rArray = R.getArray();
NDArray newR = manager.create(rArray);
// np.vstack([np.hstack(((s2 / s1) * R, c2.T - (s2 / s1) * R * c1.T)), np.matrix([0.,0., 1.])])
// (s2 / s1) * R
NDArray leftPart = djl_s2.div(djl_s1).mul(newR);
// c2.T - (s2 / s1) * R * c1.T)
NDArray rightPart = c2.reshape(2, 1).sub(leftPart.matMul(c1.reshape(2, 1)));
// numpy.hstack(((s2 / s1) * R, c2.T - (s2 / s1) * R * c1.T))
NDArray upPart = leftPart.concat(rightPart, 1);
// np.matrix([0.,0., 1.])
double[] downArray = {0d, 0d, 1d};
NDArray downPart = manager.create(downArray).reshape(1, 3);
NDArray all = upPart.concat(downPart, 0);
// System.out.println("all: " + all);
return upPart;
}
/**
* 计算全局标准差
* Calculate global standard deviation
*
* @param points
* @return
*/
public static double std(NDArray points) {
points = points.square();
double[] doubleResult = points.toDoubleArray();
double std = 0;
for (int i = 0; i < doubleResult.length; i++) {
std = std + doubleResult[i];
}
std = (float) Math.sqrt(std / doubleResult.length);
return std;
}
}

View File

@@ -0,0 +1,77 @@
package cn.smartjavaai.common.utils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.opencv_core.Mat;
/**
* 视频工具类
* @author dwj
* @date 2025/7/17
*/
public class VideoUtils {
/**
* 视频旋转
* @param inputPath 输入视频路径
* @param outputPath 输出视频路径
* @param angle 旋转角度
* @param format 视频格式
* @param videoCodec 视频编码器
* @throws FFmpegFrameRecorder.Exception
* @throws FFmpegFrameGrabber.Exception
*/
public static void rotateVideo(String inputPath, String outputPath, int angle, String format, int videoCodec) throws FFmpegFrameRecorder.Exception, FFmpegFrameGrabber.Exception {
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputPath);
grabber.start();
int inputWidth = grabber.getImageWidth();
int inputHeight = grabber.getImageHeight();
int outputWidth = inputWidth;
int outputHeight = inputHeight;
if (angle == 90 || angle == 270) {
outputWidth = inputHeight;
outputHeight = inputWidth;
}
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputPath,
outputWidth, outputHeight, grabber.getAudioChannels());
recorder.setVideoCodec(videoCodec);
recorder.setFormat(format);
recorder.start();
Frame frame;
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
while ((frame = grabber.grab()) != null) {
if (frame.image != null) {
Mat mat = converter.convert(frame);
Mat rotated = new Mat();
switch (angle) {
case 90:
opencv_core.transpose(mat, rotated);
opencv_core.flip(rotated, rotated, 1);
break;
case 180:
opencv_core.flip(mat, rotated, -1);
break;
case 270:
opencv_core.transpose(mat, rotated);
opencv_core.flip(rotated, rotated, 0);
break;
default:
rotated = mat.clone();
break;
}
frame = converter.convert(rotated);
recorder.record(frame);
}
}
recorder.stop();
recorder.release();
grabber.stop();
grabber.release();
}
}