mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
临时提交
This commit is contained in:
124
common/pom.xml
Normal file
124
common/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 批量数据打包方式:stack,padding
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
138
common/src/main/java/cn/smartjavaai/common/entity/Language.java
Normal file
138
common/src/main/java/cn/smartjavaai/common/entity/Language.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
47
common/src/main/java/cn/smartjavaai/common/entity/Point.java
Normal file
47
common/src/main/java/cn/smartjavaai/common/entity/Point.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
93
common/src/main/java/cn/smartjavaai/common/entity/R.java
Normal file
93
common/src/main/java/cn/smartjavaai/common/entity/R.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package cn.smartjavaai.common.entity.ocr;
|
||||
|
||||
/**
|
||||
* 表格结构
|
||||
* @author dwj
|
||||
*/
|
||||
public class TableStructure {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
|
||||
/**
|
||||
* 目标检测模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum DeviceEnum {
|
||||
|
||||
// resnet50 系列
|
||||
CPU,
|
||||
GPU;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.smartjavaai.common.enums;
|
||||
|
||||
/**
|
||||
* 视频源类型枚举
|
||||
* @author dwj
|
||||
* @date 2025/8/27
|
||||
*/
|
||||
public enum VideoSourceType {
|
||||
STREAM, // RTSP 或 HTTP 流
|
||||
FILE, // 本地视频文件
|
||||
CAMERA; // 本地摄像头
|
||||
}
|
||||
@@ -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; // 默认返回未知
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; // 默认返回未知
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; // 默认返回未知
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
507
common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
Normal file
507
common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
110
common/src/main/java/cn/smartjavaai/common/utils/NMSUtils.java
Normal file
110
common/src/main/java/cn/smartjavaai/common/utils/NMSUtils.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
119
common/src/main/java/cn/smartjavaai/common/utils/SVDUtils.java
Normal file
119
common/src/main/java/cn/smartjavaai/common/utils/SVDUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user