mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
【人脸识别】 新增多种人脸识别模型
【底层优化】 支持自由选择 OpenCV 或 BufferedImage 作为图像引擎 【通用图像】 全部模型启用 Image 输入,支持各类图片格式与 Image 的互转 【模型管理】 优化模型生命周期,关闭后可重新创建 【人脸识别】 支持在人脸查询结果中绘制姓名标注 【人脸检测】 新增人脸裁剪功能 【修复】 修复若干已知问题,提升系统稳定性
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.24</version>
|
||||
<version>1.0.25</version>
|
||||
</parent>
|
||||
|
||||
<name>common</name>
|
||||
|
||||
@@ -21,45 +21,132 @@ import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 图片处理工厂类
|
||||
* @author dwj
|
||||
*/
|
||||
public class SmartImageFactory extends BufferedImageFactory {
|
||||
public class SmartImageFactory {
|
||||
|
||||
public enum Engine {
|
||||
BUFFEREDIMAGE,
|
||||
OPENCV
|
||||
}
|
||||
|
||||
private static volatile Engine currentEngine = Engine.BUFFEREDIMAGE;
|
||||
private static volatile SmartImageFactory instance;
|
||||
|
||||
public static SmartImageFactory newInstance() {
|
||||
public static synchronized void setEngine(Engine engine) {
|
||||
if (engine == null || engine == currentEngine) {
|
||||
return;
|
||||
}
|
||||
currentEngine = engine;
|
||||
// 只在切换时注册全局
|
||||
switch (currentEngine) {
|
||||
case OPENCV:
|
||||
ImageFactory.setImageFactory(new OpenCVImageFactory());
|
||||
break;
|
||||
case BUFFEREDIMAGE:
|
||||
default:
|
||||
ImageFactory.setImageFactory(new BufferedImageFactory());
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized SmartImageFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SmartImageFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new SmartImageFactory();
|
||||
}
|
||||
instance = new SmartImageFactory();
|
||||
// 初始化全局 Engine
|
||||
switch (currentEngine) {
|
||||
case OPENCV:
|
||||
ImageFactory.setImageFactory(new OpenCVImageFactory());
|
||||
break;
|
||||
case BUFFEREDIMAGE:
|
||||
default:
|
||||
ImageFactory.setImageFactory(new BufferedImageFactory());
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static SmartImageFactory getInstance(){
|
||||
return newInstance();
|
||||
}
|
||||
|
||||
public Image fromBufferedImage(BufferedImage sourceImage){
|
||||
return fromImage(OpenCVUtils.image2Mat(sourceImage));
|
||||
if (sourceImage == null) {
|
||||
throw new IllegalArgumentException("BufferedImage 不能为空");
|
||||
}
|
||||
Image image = null;
|
||||
switch (currentEngine) {
|
||||
case BUFFEREDIMAGE:
|
||||
image = ImageFactory.getInstance().fromImage(sourceImage);
|
||||
break;
|
||||
case OPENCV:
|
||||
// 先转 Mat
|
||||
Mat mat = OpenCVUtils.image2Mat(sourceImage);
|
||||
image = ImageFactory.getInstance().fromImage(mat);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("未知 Engine: " + currentEngine);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
public Image fromMat(Mat mat){
|
||||
if (mat == null) {
|
||||
throw new IllegalArgumentException("mat 不能为空");
|
||||
}
|
||||
Image image = null;
|
||||
switch (currentEngine) {
|
||||
case OPENCV:
|
||||
image = ImageFactory.getInstance().fromImage(mat);
|
||||
break;
|
||||
case BUFFEREDIMAGE:
|
||||
// 先转 Mat
|
||||
BufferedImage sourceImage = OpenCVUtils.mat2Image(mat);
|
||||
image = ImageFactory.getInstance().fromImage(sourceImage);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("未知 Engine: " + currentEngine);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
public Image fromBase64(String base64Image) throws IOException {
|
||||
return fromUrl(base64Image);
|
||||
return ImageFactory.getInstance().fromUrl(base64Image);
|
||||
}
|
||||
|
||||
public Image fromBytes(byte[] imageData){
|
||||
return fromImage(new ByteArrayInputStream(imageData));
|
||||
public Image fromBytes(byte[] imageData) throws IOException {
|
||||
return ImageFactory.getInstance().fromInputStream(new ByteArrayInputStream(imageData));
|
||||
}
|
||||
|
||||
public Image fromFile(File file) throws IOException {
|
||||
return ImageFactory.getInstance().fromFile(file.toPath());
|
||||
}
|
||||
|
||||
public Image fromFile(Path path) throws IOException {
|
||||
return ImageFactory.getInstance().fromFile(path);
|
||||
}
|
||||
|
||||
public Image fromFile(String filePath) throws IOException {
|
||||
if (filePath == null || filePath.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("filePath 不能为空");
|
||||
}
|
||||
return fromFile(Paths.get(filePath));
|
||||
}
|
||||
|
||||
public Image fromPixels(int[] pixels, int width, int height){
|
||||
return ImageFactory.getInstance().fromPixels(pixels, width, height);
|
||||
}
|
||||
|
||||
public Image fromInputStream(InputStream inputStream) throws IOException {
|
||||
return ImageFactory.getInstance().fromInputStream(inputStream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.smartjavaai.common.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多边形
|
||||
* @author dwj
|
||||
*/
|
||||
@Data
|
||||
public class PolygonLabel {
|
||||
|
||||
private List<Point> points;
|
||||
private String text;
|
||||
|
||||
public PolygonLabel(List<Point> points, String text) {
|
||||
this.points = points;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public PolygonLabel() {
|
||||
}
|
||||
|
||||
public PolygonLabel(List<Point> points) {
|
||||
this.points = points;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import java.awt.image.BufferedImage;
|
||||
* @author dwj
|
||||
* @date 2025/6/27
|
||||
*/
|
||||
public class BufferedImagePreprocessor {
|
||||
public class BufferedImagePreprocessor implements ImagePreprocessor<BufferedImage>{
|
||||
|
||||
private BufferedImage image;
|
||||
private DetectionRectangle rect;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.smartjavaai.common.preprocess;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class DJLImagePreprocessor implements ImagePreprocessor<Image>{
|
||||
|
||||
|
||||
private final ImagePreprocessor<?> delegate;
|
||||
private Image input = null;
|
||||
|
||||
public DJLImagePreprocessor(Image image, DetectionRectangle rect) {
|
||||
this.input = image;
|
||||
if (input.getWrappedImage() instanceof BufferedImage) {
|
||||
this.delegate = new BufferedImagePreprocessor((BufferedImage) input.getWrappedImage(), rect);
|
||||
} else if (input.getWrappedImage() instanceof Mat) {
|
||||
this.delegate = new OpenCVPreprocessor((Mat) input.getWrappedImage(), rect);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported input type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor setExtendRatio(float ratio) {
|
||||
delegate.setExtendRatio(ratio);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor setTargetSize(int size) {
|
||||
delegate.setTargetSize(size);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor setCenterCropSize(int size) {
|
||||
delegate.setCenterCropSize(size);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor enableSquarePadding(boolean enable) {
|
||||
delegate.enableSquarePadding(enable);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor enableScaling(boolean enable) {
|
||||
delegate.enableScaling(enable);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DJLImagePreprocessor enableCenterCrop(boolean enable) {
|
||||
delegate.enableCenterCrop(enable);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImagePreprocessor setPaddingColor(Color color) {
|
||||
return delegate.setPaddingColor(color);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Image process() {
|
||||
Object result = delegate.process();
|
||||
if (result instanceof BufferedImage) {
|
||||
return SmartImageFactory.getInstance().fromBufferedImage((BufferedImage) result);
|
||||
} else if (result instanceof Mat) {
|
||||
return SmartImageFactory.getInstance().fromMat((Mat) result);
|
||||
}
|
||||
throw new IllegalStateException("Unsupported process result: " + result.getClass());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.smartjavaai.common.preprocess;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 图片预处理
|
||||
* @author dwj
|
||||
*/
|
||||
public interface ImagePreprocessor<T> {
|
||||
|
||||
|
||||
ImagePreprocessor<T> setExtendRatio(float ratio);
|
||||
|
||||
ImagePreprocessor<T> setTargetSize(int size);
|
||||
|
||||
ImagePreprocessor<T> setCenterCropSize(int size);
|
||||
|
||||
ImagePreprocessor<T> enableSquarePadding(boolean enable);
|
||||
|
||||
ImagePreprocessor<T> enableScaling(boolean enable);
|
||||
|
||||
ImagePreprocessor<T> enableCenterCrop(boolean enable);
|
||||
|
||||
ImagePreprocessor<T> setPaddingColor(Color color);
|
||||
|
||||
T process();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package cn.smartjavaai.common.preprocess;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.*;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class OpenCVPreprocessor implements ImagePreprocessor<Mat> {
|
||||
|
||||
private Mat image;
|
||||
private DetectionRectangle rect;
|
||||
private float extendRatio = 1;
|
||||
private int targetSize = 128;
|
||||
private int centerCropSize = 80;
|
||||
|
||||
private Scalar paddingColor = new Scalar(127, 127, 127); // 默认灰色
|
||||
private boolean enableSquarePadding = true;
|
||||
private boolean enableScaling = true;
|
||||
private boolean enableCenterCrop = false;
|
||||
|
||||
public OpenCVPreprocessor(Mat image, DetectionRectangle rect) {
|
||||
this.image = image;
|
||||
this.rect = rect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor setExtendRatio(float ratio) {
|
||||
this.extendRatio = ratio;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor setTargetSize(int size) {
|
||||
this.targetSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor setCenterCropSize(int size) {
|
||||
this.centerCropSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor enableSquarePadding(boolean enable) {
|
||||
this.enableSquarePadding = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor enableScaling(boolean enable) {
|
||||
this.enableScaling = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor enableCenterCrop(boolean enable) {
|
||||
this.enableCenterCrop = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenCVPreprocessor setPaddingColor(java.awt.Color color) {
|
||||
this.paddingColor = new Scalar(color.getBlue(), color.getGreen(), color.getRed());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mat process() {
|
||||
// Step 1: 裁剪 + 扩展
|
||||
Mat cropped = cropAndExtend();
|
||||
|
||||
// Step 2: 填充正方形
|
||||
Mat squared = enableSquarePadding ? squarePadding(cropped) : cropped;
|
||||
|
||||
// Step 3: 缩放
|
||||
Mat scaled = enableScaling ? scaleToTarget(squared) : squared;
|
||||
|
||||
// Step 4: CenterCrop
|
||||
Mat finalResult = enableCenterCrop ? centerCrop(scaled) : scaled;
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测框扩展及裁剪
|
||||
*/
|
||||
private Mat 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.width(), x + width + extendX);
|
||||
int top = Math.max(0, y - extendY);
|
||||
int bottom = Math.min(image.height(), 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.width() - right, (longSide - origRoiWidth + 1) / 2);
|
||||
int extendTop = Math.min(top, (longSide - origRoiHeight) / 2);
|
||||
int extendBottom = Math.min(image.height() - bottom, (longSide - origRoiHeight + 1) / 2);
|
||||
|
||||
int expandedLeft = left - extendLeft;
|
||||
int expandedRight = right + extendRight;
|
||||
int expandedTop = top - extendTop;
|
||||
int expandedBottom = bottom + extendBottom;
|
||||
|
||||
Rect roi = new Rect(expandedLeft, expandedTop, expandedRight - expandedLeft, expandedBottom - expandedTop);
|
||||
return new Mat(image, roi).clone(); // clone 避免与原图共享内存
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充为正方形
|
||||
*/
|
||||
private Mat squarePadding(Mat src) {
|
||||
int longSide = Math.max(src.width(), src.height());
|
||||
Mat squared = new Mat(new Size(longSide, longSide), src.type(), paddingColor);
|
||||
int xOffset = (longSide - src.width()) / 2;
|
||||
int yOffset = (longSide - src.height()) / 2;
|
||||
src.copyTo(squared.submat(yOffset, yOffset + src.height(), xOffset, xOffset + src.width()));
|
||||
return squared;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放到目标大小
|
||||
*/
|
||||
private Mat scaleToTarget(Mat src) {
|
||||
Mat result = new Mat();
|
||||
Imgproc.resize(src, result, new Size(targetSize, targetSize), 0, 0, Imgproc.INTER_AREA);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* CenterCrop
|
||||
*/
|
||||
private Mat centerCrop(Mat src) {
|
||||
int startX = (src.width() - centerCropSize) / 2;
|
||||
int startY = (src.height() - centerCropSize) / 2;
|
||||
Rect roi = new Rect(startX, startY, centerCropSize, centerCropSize);
|
||||
return new Mat(src, roi).clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
package cn.smartjavaai.common.utils;
|
||||
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.util.RandomUtils;
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.common.entity.face.FaceAttribute;
|
||||
import cn.smartjavaai.common.entity.face.FaceSearchResult;
|
||||
import cn.smartjavaai.common.entity.face.HeadPose;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ComponentSampleModel;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class BufferedImageUtils {
|
||||
|
||||
/**
|
||||
* 拷贝图片
|
||||
* @param src
|
||||
* @return
|
||||
*/
|
||||
public static BufferedImage copyBufferedImage(BufferedImage src) {
|
||||
BufferedImage copy = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
|
||||
Graphics2D g = copy.createGraphics();
|
||||
g.drawImage(src, 0, 0, null);
|
||||
g.dispose();
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对图像解码返回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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断图像是否为BGR格式
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static boolean isBGR3Byte(BufferedImage image) {
|
||||
return equalBandOffsetWith3Byte(image, new int[]{0, 1, 2});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
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 drawRect(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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存BufferedImage图片
|
||||
* @param image
|
||||
* @param outputPath
|
||||
* @param formatName
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void saveBufferedImage(BufferedImage image, String outputPath, String formatName) throws IOException {
|
||||
if (image == null) {
|
||||
throw new IllegalArgumentException("BufferedImage 不能为空");
|
||||
}
|
||||
if (outputPath == null || outputPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("输出路径不能为空");
|
||||
}
|
||||
if (formatName == null || formatName.isEmpty()) {
|
||||
throw new IllegalArgumentException("格式不能为空");
|
||||
}
|
||||
|
||||
Path path = Paths.get(outputPath);
|
||||
Path parent = path.getParent();
|
||||
if (parent != null && !Files.exists(parent)) {
|
||||
Files.createDirectories(parent); // 自动创建父目录
|
||||
}
|
||||
File outFile = path.toFile();
|
||||
boolean result = ImageIO.write(image, formatName, outFile);
|
||||
if (!result) {
|
||||
throw new IOException("保存图片失败,不支持的格式: " + formatName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 默认保存图片格式为png
|
||||
* @param image
|
||||
* @param outputPath
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void saveImage(BufferedImage image, String outputPath) throws IOException {
|
||||
saveBufferedImage(image, outputPath, "png");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)
|
||||
*
|
||||
* @param image
|
||||
* @param box
|
||||
*/
|
||||
public static void drawRect(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 drawRectAndText(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 drawRectAndText(BufferedImage image, DetectionRectangle box, String text, Color color) {
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D graphics = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
drawRectAndText(graphics, box, text, color);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)和文本
|
||||
*
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(BufferedImage image, DetectionRectangle box, String text, int fontSize) {
|
||||
Color color = new Color(255, 0, 0);
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D graphics = (Graphics2D) image.getGraphics();
|
||||
graphics.setFont(new Font("楷体", Font.PLAIN, fontSize));
|
||||
try {
|
||||
drawRectAndText(graphics, box, text, color);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)和文本
|
||||
*
|
||||
* @param graphics
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(Graphics2D graphics, DetectionRectangle box, String text, Color color) {
|
||||
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());
|
||||
Graphics2DUtils.drawText(graphics, text, box.getX(), box.getY(), stroke, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制检测框
|
||||
* @param sourceImage
|
||||
* @param detectionResponse
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void drawFaceSearchResult(BufferedImage sourceImage, DetectionResponse detectionResponse, String displayField) {
|
||||
if(!BufferedImageUtils.isImageValid(sourceImage)){
|
||||
throw new IllegalArgumentException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new IllegalArgumentException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
//绘制人脸关键点
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
Graphics2DUtils.drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
//人脸查询结果
|
||||
if(detectionInfo.getFaceInfo().getFaceSearchResults() != null){
|
||||
for (FaceSearchResult faceSearchResult : detectionInfo.getFaceInfo().getFaceSearchResults()){
|
||||
if(StringUtils.isNotBlank(faceSearchResult.getMetadata())){
|
||||
JsonObject metadata = GsonUtils.parseToJsonObject(faceSearchResult.getMetadata());
|
||||
JsonElement nameElement = metadata.get("name");
|
||||
if(metadata.has("name")){
|
||||
Graphics2DUtils.drawText(graphics, nameElement.getAsString(), rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 绘制检测框
|
||||
* @param sourceImage
|
||||
* @param detectionResponse
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void drawBoundingBoxes(BufferedImage sourceImage, DetectionResponse detectionResponse) {
|
||||
if(!BufferedImageUtils.isImageValid(sourceImage)){
|
||||
throw new IllegalArgumentException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new IllegalArgumentException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
//绘制人脸关键点
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
Graphics2DUtils.drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
// Graphics2DUtils.drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
//人脸属性
|
||||
if(detectionInfo.getFaceInfo().getFaceAttribute() != null){
|
||||
FaceAttribute faceAttribute = detectionInfo.getFaceInfo().getFaceAttribute();
|
||||
drawFaceAttribute(faceAttribute, rectangle, graphics);
|
||||
}
|
||||
}
|
||||
//绘制目标检测信息
|
||||
if(detectionInfo.getObjectDetInfo() != null){
|
||||
String className = detectionInfo.getObjectDetInfo().getClassName();
|
||||
Graphics2DUtils.drawText(graphics, className, rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
}
|
||||
}
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制矩形框和文字
|
||||
*
|
||||
* @param sourceImage
|
||||
* @param detectionInfo
|
||||
*/
|
||||
public static void drawRectAndText(BufferedImage sourceImage, DetectionInfo detectionInfo) {
|
||||
if(!BufferedImageUtils.isImageValid(sourceImage)){
|
||||
throw new IllegalArgumentException("图像无效");
|
||||
}
|
||||
if(Objects.isNull(detectionInfo)){
|
||||
throw new IllegalArgumentException("无目标数据");
|
||||
}
|
||||
Graphics2D graphics = sourceImage.createGraphics();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.setStroke(new BasicStroke(2)); // 线宽2像素
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
|
||||
int stroke = 2;
|
||||
DetectionRectangle rectangle = detectionInfo.getDetectionRectangle();
|
||||
graphics.setColor(Color.RED);// 边框颜色
|
||||
graphics.drawRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
|
||||
//绘制人脸关键点
|
||||
if(detectionInfo.getFaceInfo() != null && detectionInfo.getFaceInfo().getKeyPoints() != null &&
|
||||
!detectionInfo.getFaceInfo().getKeyPoints().isEmpty()){
|
||||
Graphics2DUtils.drawLandmarks(graphics, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
// Graphics2DUtils.drawText(graphics, "face", rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
//人脸属性
|
||||
if(detectionInfo.getFaceInfo().getFaceAttribute() != null){
|
||||
FaceAttribute faceAttribute = detectionInfo.getFaceInfo().getFaceAttribute();
|
||||
drawFaceAttribute(faceAttribute, rectangle, graphics);
|
||||
}
|
||||
}
|
||||
//绘制目标检测信息
|
||||
if(detectionInfo.getObjectDetInfo() != null){
|
||||
String className = detectionInfo.getObjectDetInfo().getClassName();
|
||||
Graphics2DUtils.drawText(graphics, className, rectangle.getX(), rectangle.getY(), stroke, 4);
|
||||
}
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制人脸属性
|
||||
* @param faceAttribute
|
||||
* @param rectangle
|
||||
* @param graphics
|
||||
*/
|
||||
public static void drawFaceAttribute(FaceAttribute faceAttribute, DetectionRectangle rectangle, Graphics2D graphics){
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (faceAttribute.getGenderType() != null) {
|
||||
lines.add("性别: " + faceAttribute.getGenderType().name());
|
||||
}
|
||||
if (faceAttribute.getAge() != null) {
|
||||
lines.add("年龄: " + faceAttribute.getAge());
|
||||
}
|
||||
if (faceAttribute.getWearingMask() != null) {
|
||||
lines.add("口罩: " + (faceAttribute.getWearingMask() ? "是" : "否"));
|
||||
}
|
||||
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||
lines.add("眼睛: " + faceAttribute.getLeftEyeStatus().name() + "/" + faceAttribute.getRightEyeStatus().name());
|
||||
}
|
||||
if (faceAttribute.getHeadPose() != null) {
|
||||
HeadPose pose = faceAttribute.getHeadPose();
|
||||
String pitch = pose.getPitch() != null ? String.valueOf(pose.getPitch().intValue()) : "-";
|
||||
String yaw = pose.getYaw() != null ? String.valueOf(pose.getYaw().intValue()) : "-";
|
||||
String roll = pose.getRoll() != null ? String.valueOf(pose.getRoll().intValue()) : "-";
|
||||
lines.add("姿态: P=" + pitch + " Y=" + yaw + " R=" + roll);
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
Graphics2DUtils.drawMultilineTextWithBackground(graphics, lines, rectangle.getX(), rectangle.getY()); // 适当偏移
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void drawPolygonWithText(BufferedImage image,List<PolygonLabel> polygonLabelList, int fontSize) {
|
||||
Font font = new Font("楷体", Font.PLAIN, fontSize);
|
||||
Stroke stroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
Color color = new Color(0, 0, 255);
|
||||
Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
for (PolygonLabel polygonLabel : polygonLabelList){
|
||||
drawPolygonWithText(g, polygonLabel.getPoints(), polygonLabel.getText(), font, color, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制多边形及文字
|
||||
* @param g Graphics2D
|
||||
* @param points 多边形顶点
|
||||
* @param text 绘制的文字(可为空)
|
||||
* @param font 字体
|
||||
* @param color 颜色
|
||||
* @param stroke 画笔样式
|
||||
*/
|
||||
public static void drawPolygonWithText(Graphics2D g, List<Point> points,
|
||||
String text, int fontSize) {
|
||||
Font font = new Font("楷体", Font.PLAIN, fontSize);
|
||||
Stroke stroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
Color color = new Color(0, 0, 255);
|
||||
drawPolygonWithText(g, points, text, font, color, stroke);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制多边形及文字
|
||||
* @param g Graphics2D
|
||||
* @param points 多边形顶点
|
||||
* @param text 绘制的文字(可为空)
|
||||
* @param font 字体
|
||||
* @param color 颜色
|
||||
* @param stroke 画笔样式
|
||||
*/
|
||||
public static void drawPolygonWithText(Graphics2D g, List<Point> points,
|
||||
String text, Font font,
|
||||
Color color, Stroke stroke) {
|
||||
if (points == null || points.size() < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
int[] xPoints = points.stream().mapToInt(p -> (int) p.getX()).toArray();
|
||||
int[] yPoints = points.stream().mapToInt(p -> (int) p.getY()).toArray();
|
||||
|
||||
g.setFont(font);
|
||||
g.setColor(color);
|
||||
g.setStroke(stroke);
|
||||
// 绘制多边形
|
||||
g.drawPolygon(xPoints, yPoints, points.size());
|
||||
// 绘制文字(默认放在第一个点)
|
||||
if (text != null && !text.isEmpty()) {
|
||||
g.drawString(text, xPoints[0], yPoints[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制关键点
|
||||
* @param graphics
|
||||
* @param points
|
||||
* @param color
|
||||
*/
|
||||
public static void drawKeyPoints(Graphics2D graphics, List<Point> points, Color color){
|
||||
if(points == null || points.isEmpty()){
|
||||
return;
|
||||
}
|
||||
for (Point point : points){
|
||||
//绘制关键点
|
||||
graphics.setColor(color);
|
||||
graphics.drawRect((int)point.getX(), (int)point.getY(), 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
package cn.smartjavaai.common.utils;
|
||||
|
||||
import ai.djl.modality.cv.output.BoundingBox;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.modality.cv.output.Point;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import org.opencv.core.CvType;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -43,4 +50,113 @@ public class DJLCommonUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* float NDArray To float[][] Array
|
||||
* @param ndArray
|
||||
* @return
|
||||
*/
|
||||
public static float[][] floatNDArrayToArray(NDArray ndArray) {
|
||||
int rows = (int) (ndArray.getShape().get(0));
|
||||
int cols = (int) (ndArray.getShape().get(1));
|
||||
float[][] arr = new float[rows][cols];
|
||||
|
||||
float[] arrs = ndArray.toFloatArray();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
arr[i][j] = arrs[i * cols + j];
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* float NDArray To float[][] Array
|
||||
* @param ndArray
|
||||
* @param cvType
|
||||
* @return
|
||||
*/
|
||||
public static Mat floatNDArrayToMat(NDArray ndArray, int cvType) {
|
||||
int rows = (int) (ndArray.getShape().get(0));
|
||||
int cols = (int) (ndArray.getShape().get(1));
|
||||
Mat mat = new Mat(rows, cols, cvType);
|
||||
|
||||
float[] arrs = ndArray.toFloatArray();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
mat.put(i, j, arrs[i * cols + j]);
|
||||
}
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
/**
|
||||
* float NDArray To Mat
|
||||
* @param ndArray
|
||||
* @return
|
||||
*/
|
||||
public static Mat floatNDArrayToMat(NDArray ndArray) {
|
||||
int rows = (int) (ndArray.getShape().get(0));
|
||||
int cols = (int) (ndArray.getShape().get(1));
|
||||
Mat mat = new Mat(rows, cols, CvType.CV_32F);
|
||||
|
||||
float[] arrs = ndArray.toFloatArray();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
mat.put(i, j, arrs[i * cols + j]);
|
||||
}
|
||||
}
|
||||
|
||||
return mat;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* uint8 NDArray To Mat
|
||||
* @param ndArray
|
||||
* @return
|
||||
*/
|
||||
public static Mat uint8NDArrayToMat(NDArray ndArray) {
|
||||
int rows = (int) (ndArray.getShape().get(0));
|
||||
int cols = (int) (ndArray.getShape().get(1));
|
||||
Mat mat = new Mat(rows, cols, CvType.CV_8U);
|
||||
|
||||
byte[] arrs = ndArray.toByteArray();
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
mat.put(i, j, arrs[i * cols + j]);
|
||||
}
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List To Mat
|
||||
* @param points
|
||||
* @return
|
||||
*/
|
||||
public static Mat toMat(List<Point> points) {
|
||||
Mat mat = new Mat(points.size(), 2, CvType.CV_32F);
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
ai.djl.modality.cv.output.Point point = points.get(i);
|
||||
mat.put(i, 0, (float) point.getX());
|
||||
mat.put(i, 1, (float) point.getY());
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个空的 DetectedObjects 对象
|
||||
* @return
|
||||
*/
|
||||
public static DetectedObjects buildEmptyDetectedObjects(){
|
||||
List<String> classNames = new ArrayList<>();
|
||||
List<Double> probabilities = new ArrayList<>();
|
||||
List<BoundingBox> boxes = new ArrayList<>();
|
||||
return new DetectedObjects(classNames, probabilities, boxes);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.smartjavaai.common.utils;
|
||||
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class Graphics2DUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 绘制文本
|
||||
* @param g
|
||||
* @param text
|
||||
* @param x
|
||||
* @param y
|
||||
* @param stroke
|
||||
* @param padding
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制人脸关键点
|
||||
* @param g
|
||||
* @param keyPoints
|
||||
*/
|
||||
public static void drawLandmarks(Graphics2D g, List<cn.smartjavaai.common.entity.Point> keyPoints) {
|
||||
g.setColor(new Color(246, 96, 0));
|
||||
BasicStroke bStroke = new BasicStroke(4.0F, 0, 0);
|
||||
g.setStroke(bStroke);
|
||||
for (Point point : keyPoints){
|
||||
g.drawRect((int)point.getX(), (int)point.getY(), 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void drawMultilineTextWithBackground(Graphics2D g, List<String> lines, int x, int y) {
|
||||
Font font = new Font("SansSerif", Font.PLAIN, 14);
|
||||
g.setFont(font);
|
||||
FontMetrics fm = g.getFontMetrics();
|
||||
int lineHeight = fm.getHeight();
|
||||
int maxWidth = lines.stream().mapToInt(fm::stringWidth).max().orElse(0);
|
||||
|
||||
int padding = 4;
|
||||
int boxWidth = maxWidth + padding * 2;
|
||||
int boxHeight = lineHeight * lines.size() + padding * 2;
|
||||
|
||||
// 背景矩形
|
||||
g.setColor(new Color(0, 0, 0, 128));
|
||||
g.fillRoundRect(x, y, boxWidth, boxHeight, 8, 8);
|
||||
|
||||
// 绘制每一行文字
|
||||
g.setColor(Color.WHITE);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
g.drawString(lines.get(i), x + padding, y + padding + (i + 1) * lineHeight - 4);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.smartjavaai.common.utils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
*/
|
||||
public class GsonUtils {
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private GsonUtils() {
|
||||
// 私有构造,防止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串安全转换为 JsonObject
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @return JsonObject,如果解析失败返回 null
|
||||
*/
|
||||
public static JsonObject parseToJsonObject(String jsonStr) {
|
||||
if (jsonStr == null || jsonStr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JsonParser.parseString(jsonStr).getAsJsonObject();
|
||||
} catch (JsonSyntaxException | IllegalStateException e) {
|
||||
// 解析失败返回 null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为指定类型对象
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @param clazz 目标类型
|
||||
* @param <T> 类型参数
|
||||
* @return 对象实例,如果解析失败返回 null
|
||||
*/
|
||||
public static <T> T fromJson(String jsonStr, Class<T> clazz) {
|
||||
if (jsonStr == null || jsonStr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return GSON.fromJson(jsonStr, clazz);
|
||||
} catch (JsonSyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为 JSON 字符串
|
||||
*
|
||||
* @param obj 对象
|
||||
* @return JSON 字符串,如果对象为 null 返回 null
|
||||
*/
|
||||
public static String toJson(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return GSON.toJson(obj);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,209 +1,38 @@
|
||||
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.CategoryMask;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.modality.cv.util.NDImageUtils;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import ai.djl.util.RandomUtils;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.PolygonLabel;
|
||||
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.DataBufferByte;
|
||||
import java.awt.image.ImageObserver;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -213,7 +42,7 @@ public class ImageUtils {
|
||||
* @param name
|
||||
* @param path
|
||||
*/
|
||||
public static void saveImage(Image img, String name, String path) {
|
||||
public static void save(Image img, String name, String path) {
|
||||
Path outputDir = Paths.get(path);
|
||||
Path imagePath = outputDir.resolve(name);
|
||||
// OpenJDK 不能保存 jpg 图片的 alpha channel
|
||||
@@ -224,6 +53,23 @@ public class ImageUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片矩阵BGR
|
||||
*
|
||||
* @param img
|
||||
* @return
|
||||
*/
|
||||
public static byte[] getMatrixBGR(Image img) {
|
||||
if (img.getWrappedImage() instanceof BufferedImage){
|
||||
return BufferedImageUtils.getMatrixBGR((BufferedImage)img.getWrappedImage());
|
||||
}else if (img.getWrappedImage() instanceof Mat){
|
||||
return OpenCVUtils.getMatrixBGR((Mat)img.getWrappedImage());
|
||||
}else {
|
||||
throw new RuntimeException("不支持的图片类型");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存图片,含检测框
|
||||
*
|
||||
@@ -246,138 +92,6 @@ public class ImageUtils {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)
|
||||
*
|
||||
* @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
|
||||
@@ -448,7 +162,7 @@ public class ImageUtils {
|
||||
name.endsWith(".gif") || name.endsWith(".tiff") ||
|
||||
name.endsWith(".webp")) {
|
||||
|
||||
Image img = ImageFactory.getInstance().fromInputStream(Files.newInputStream(file.toPath()));
|
||||
Image img = SmartImageFactory.getInstance().fromInputStream(Files.newInputStream(file.toPath()));
|
||||
imageList.add(img);
|
||||
}
|
||||
}
|
||||
@@ -476,46 +190,6 @@ public class ImageUtils {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 拷贝图片
|
||||
* @param src
|
||||
* @return
|
||||
*/
|
||||
public static BufferedImage copyBufferedImage(BufferedImage src) {
|
||||
BufferedImage copy = new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
|
||||
Graphics2D g = copy.createGraphics();
|
||||
g.drawImage(src, 0, 0, null);
|
||||
g.dispose();
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -525,9 +199,9 @@ public class ImageUtils {
|
||||
*/
|
||||
public static Image copy(Image src) {
|
||||
Object srcData = src.getWrappedImage();
|
||||
//当图片未BufferedImage,DJL的duplicate会有问题
|
||||
//当图片是BufferedImage,DJL的duplicate会有问题
|
||||
if (srcData instanceof BufferedImage) {
|
||||
return SmartImageFactory.getInstance().fromImage(copyBufferedImage((BufferedImage) srcData));
|
||||
return SmartImageFactory.getInstance().fromBufferedImage(BufferedImageUtils.copyBufferedImage((BufferedImage) srcData));
|
||||
}else{
|
||||
return src.duplicate();
|
||||
}
|
||||
@@ -584,8 +258,241 @@ public class ImageUtils {
|
||||
image.drawImage(maskImage, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Image 到指定路径,格式根据后缀自动推断
|
||||
*/
|
||||
public static void save(Image image, Path path) throws IOException {
|
||||
String fileName = path.getFileName().toString().toLowerCase();
|
||||
String format = "png"; // 默认 png
|
||||
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
|
||||
format = "jpg";
|
||||
} else if (fileName.endsWith(".bmp")) {
|
||||
format = "bmp";
|
||||
} else if (fileName.endsWith(".webp")) {
|
||||
format = "webp";
|
||||
}
|
||||
Files.createDirectories(path.getParent());
|
||||
try (OutputStream os = Files.newOutputStream(path)) {
|
||||
image.save(os, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Image 到指定路径,格式根据后缀自动推断
|
||||
*/
|
||||
public static void save(Image image, Path path, String format) throws IOException {
|
||||
Files.createDirectories(path.getParent());
|
||||
try (OutputStream os = Files.newOutputStream(path)) {
|
||||
image.save(os, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Image 到指定路径
|
||||
*/
|
||||
public static void save(Image image, String imagePath) throws IOException {
|
||||
Path path = Paths.get(imagePath);
|
||||
Files.createDirectories(path.getParent());
|
||||
try (OutputStream os = Files.newOutputStream(path)) {
|
||||
image.save(os, "png");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 BufferedImage
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(Image image) {
|
||||
Object wrapped = image.getWrappedImage();
|
||||
if (wrapped instanceof BufferedImage) {
|
||||
return (BufferedImage) wrapped;
|
||||
} else if (wrapped instanceof Mat) {
|
||||
Mat mat = (Mat) wrapped;
|
||||
return OpenCVUtils.mat2Image(mat);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported wrapped image type: " + wrapped.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 Mat
|
||||
*/
|
||||
public static Mat toMat(Image image) {
|
||||
Object wrapped = image.getWrappedImage();
|
||||
if (wrapped instanceof BufferedImage) {
|
||||
return OpenCVUtils.image2Mat((BufferedImage) wrapped);
|
||||
} else if (wrapped instanceof Mat) {
|
||||
return (Mat) wrapped;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported wrapped image type: " + wrapped.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Image 转 byte[] (默认 png 格式)
|
||||
*/
|
||||
public static byte[] toBytes(Image image, String format) {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
image.save(baos, format);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to convert Image to byte[]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Image 到 OutputStream
|
||||
*
|
||||
* @param image 图像对象
|
||||
* @param os 输出流(需要调用方负责关闭)
|
||||
* @param format 保存格式(png/jpg/webp)
|
||||
*/
|
||||
public static void toOutputStream(Image image, OutputStream os, String format) {
|
||||
try {
|
||||
image.save(os, format);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write image to OutputStream", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 Image 为 Base64 字符串
|
||||
*
|
||||
* @param image 图像对象
|
||||
* @param format 输出格式(png/jpg/webp)
|
||||
* @return Base64 编码的字符串
|
||||
*/
|
||||
public static String toBase64(Image image, String format) {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
image.save(baos, format);
|
||||
return Base64.encode(baos.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to convert image to Base64", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放 OpenCV Mat
|
||||
* @param image
|
||||
*/
|
||||
public static void releaseOpenCVMat(Image image){
|
||||
if (image != null && image.getWrappedImage() instanceof Mat){
|
||||
((Mat)image.getWrappedImage()).release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制检测结果
|
||||
* @param sourceImage
|
||||
* @param detectionResponse
|
||||
* @return
|
||||
*/
|
||||
public static Image drawBoundingBoxes(Image sourceImage, DetectionResponse detectionResponse){
|
||||
Object srcData = sourceImage.getWrappedImage();
|
||||
if (srcData instanceof BufferedImage) {
|
||||
BufferedImage copyBufferedImage = BufferedImageUtils.copyBufferedImage((BufferedImage) srcData);
|
||||
BufferedImageUtils.drawBoundingBoxes(copyBufferedImage, detectionResponse);
|
||||
return SmartImageFactory.getInstance().fromBufferedImage(copyBufferedImage);
|
||||
}else if (srcData instanceof Mat) {
|
||||
Mat srcMat = ((Mat) srcData).clone();
|
||||
OpenCVUtils.drawBoundingBoxes(srcMat, detectionResponse);
|
||||
return SmartImageFactory.getInstance().fromMat(srcMat);
|
||||
}else {
|
||||
throw new IllegalArgumentException("Unsupported wrapped image type: " + srcData.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆时针旋转图片
|
||||
*
|
||||
* @param image
|
||||
* @param times
|
||||
* @return
|
||||
*/
|
||||
public static Image rotateImg(Image image, int times) {
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), times);
|
||||
return OpenCVImageFactory.getInstance().fromNDArray(rotated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片旋转
|
||||
*
|
||||
* @param manager
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
public static Image rotateImg(NDManager manager, Image image) {
|
||||
NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1);
|
||||
return ImageFactory.getInstance().fromNDArray(rotated);
|
||||
}
|
||||
|
||||
|
||||
public static void drawPolygonWithText(Image image, List<PolygonLabel> polygonLabelList, int fontSize) {
|
||||
Object srcData = image.getWrappedImage();
|
||||
if (srcData instanceof BufferedImage) {
|
||||
BufferedImageUtils.drawPolygonWithText((BufferedImage) srcData, polygonLabelList, fontSize);
|
||||
}else if (srcData instanceof Mat) {
|
||||
Mat srcMat = (Mat) srcData;
|
||||
OpenCVUtils.drawPolygonWithText(srcMat, polygonLabelList, fontSize);
|
||||
}else {
|
||||
throw new IllegalArgumentException("Unsupported wrapped image type: " + srcData.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 绘制矩形框
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(Image image, DetectionRectangle box, String text) {
|
||||
Object srcData = image.getWrappedImage();
|
||||
if (srcData instanceof BufferedImage) {
|
||||
BufferedImageUtils.drawRectAndText((BufferedImage) srcData, box, text,12);
|
||||
}else if (srcData instanceof Mat) {
|
||||
Mat srcMat = (Mat) srcData;
|
||||
OpenCVUtils.drawRectAndText(srcMat, box, text, 0.5);
|
||||
}else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制矩形框
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(Image image, DetectionRectangle box, String text, double fontSize) {
|
||||
Object srcData = image.getWrappedImage();
|
||||
if (srcData instanceof BufferedImage) {
|
||||
BufferedImageUtils.drawRectAndText((BufferedImage) srcData, box, text, (int)fontSize);
|
||||
}else if (srcData instanceof Mat) {
|
||||
Mat srcMat = (Mat) srcData;
|
||||
OpenCVUtils.drawRectAndText(srcMat, box, text, fontSize);
|
||||
}else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawRectAndText(Image image, DetectionInfo detectionInfo){
|
||||
Object srcData = image.getWrappedImage();
|
||||
if (srcData instanceof BufferedImage) {
|
||||
BufferedImageUtils.drawRectAndText((BufferedImage) srcData, detectionInfo);
|
||||
}else if (srcData instanceof Mat) {
|
||||
Mat srcMat = (Mat) srcData;
|
||||
OpenCVUtils.drawRectAndText(srcMat, detectionInfo);
|
||||
}else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void drawRectAndText(Image image, List<DetectionInfo> detectionInfoList){
|
||||
for(DetectionInfo detectionInfo : detectionInfoList){
|
||||
drawRectAndText(image, detectionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,36 @@ 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.opencv.OpenCVImageFactory;
|
||||
import ai.djl.util.RandomUtils;
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.PolygonLabel;
|
||||
import cn.smartjavaai.common.entity.face.FaceAttribute;
|
||||
import cn.smartjavaai.common.entity.face.HeadPose;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.opencv.core.*;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* OpenCV 工具类
|
||||
*/
|
||||
public class OpenCVUtils {
|
||||
|
||||
|
||||
/**
|
||||
* canny算法,边缘检测
|
||||
*
|
||||
@@ -153,7 +165,7 @@ public class OpenCVUtils {
|
||||
* @param image
|
||||
* @param detectionInfoList
|
||||
*/
|
||||
public static void drawRectAndText(Image image, List<DetectionInfo> detectionInfoList) {
|
||||
public static void drawRectAndText(Mat image, List<DetectionInfo> detectionInfoList) {
|
||||
if(CollectionUtils.isEmpty(detectionInfoList))
|
||||
return;
|
||||
for(DetectionInfo detectionInfo : detectionInfoList){
|
||||
@@ -162,42 +174,90 @@ public class OpenCVUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制矩形框和文字
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(Mat image, DetectionRectangle box, String text) {
|
||||
if(image.empty())
|
||||
return;
|
||||
int x = box.getX();
|
||||
int y = box.getY();
|
||||
int width = box.getWidth();
|
||||
int height = box.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(image, pt1, pt2, rectangleColor, 2);
|
||||
Scalar textColor = new Scalar(255.0, 255.0, 255.0);
|
||||
putTextWithBackground(image, text, pt1, textColor, rectangleColor, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制矩形框和文字
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawRectAndText(Mat image, DetectionRectangle box, String text, double fontSize) {
|
||||
if(image.empty())
|
||||
return;
|
||||
int x = box.getX();
|
||||
int y = box.getY();
|
||||
int width = box.getWidth();
|
||||
int height = box.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(image, pt1, pt2, rectangleColor, 2);
|
||||
Scalar textColor = new Scalar(255.0, 255.0, 255.0);
|
||||
putTextWithBackground(image, text, pt1, textColor, rectangleColor, 1, fontSize);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制矩形框和文字
|
||||
*
|
||||
* @param image
|
||||
* @param detectionInfo
|
||||
*/
|
||||
public static void drawRectAndText(Image image, DetectionInfo detectionInfo) {
|
||||
|
||||
|
||||
Mat mat = (Mat)image.getWrappedImage();
|
||||
public static void drawRectAndText(Mat image, DetectionInfo detectionInfo) {
|
||||
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);
|
||||
|
||||
Imgproc.rectangle(image, 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);
|
||||
String className = null;
|
||||
Scalar textColor = new Scalar(255.0, 255.0, 255.0);
|
||||
//目标检测信息
|
||||
if(detectionInfo.getObjectDetInfo() != null){
|
||||
className = detectionInfo.getObjectDetInfo().getClassName();
|
||||
putTextWithBackground(image, className, pt1, textColor, rectangleColor, 1);
|
||||
}
|
||||
//人脸
|
||||
if(detectionInfo.getFaceInfo() != null){
|
||||
className = "face";
|
||||
putTextWithBackground(image, className, pt1, textColor, rectangleColor, 1);
|
||||
//绘制关键点
|
||||
drawLandmarks(image, detectionInfo.getFaceInfo().getKeyPoints());
|
||||
//绘制人脸属性
|
||||
if(detectionInfo.getFaceInfo().getFaceAttribute() != null){
|
||||
drawFaceAttribute(detectionInfo.getFaceInfo().getFaceAttribute(), detectionInfo.getDetectionRectangle(), image);
|
||||
}
|
||||
}
|
||||
image = ImageFactory.getInstance().fromImage(mat);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在Mat上绘制矩形框和文字
|
||||
*
|
||||
@@ -262,4 +322,430 @@ public class OpenCVUtils {
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* 从 OpenCV Mat 中获取 BGR 格式矩阵数据
|
||||
*
|
||||
* @param mat OpenCV Mat,需为 CV_8UC3 或可转换为 BGR 格式
|
||||
* @return BGR 格式字节数组,按行连续存储
|
||||
*/
|
||||
public static byte[] getMatrixBGR(Mat mat) {
|
||||
if (mat == null || mat.empty()) {
|
||||
throw new IllegalArgumentException("Mat 不能为空");
|
||||
}
|
||||
|
||||
// 确保是三通道 BGR 格式
|
||||
Mat bgrMat = new Mat();
|
||||
if (mat.channels() == 3) {
|
||||
mat.copyTo(bgrMat);
|
||||
} else if (mat.channels() == 4) {
|
||||
// RGBA 转 BGR
|
||||
Imgproc.cvtColor(mat, bgrMat, Imgproc.COLOR_RGBA2BGR);
|
||||
} else if (mat.channels() == 1) {
|
||||
// 灰度转 BGR
|
||||
Imgproc.cvtColor(mat, bgrMat, Imgproc.COLOR_GRAY2BGR);
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的通道数: " + mat.channels());
|
||||
}
|
||||
|
||||
int size = (int) (bgrMat.total() * bgrMat.channels());
|
||||
byte[] data = new byte[size];
|
||||
bgrMat.get(0, 0, data);
|
||||
bgrMat.release(); // 释放临时 Mat
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图像上绘制带白色背景、黑色文字的文本
|
||||
*/
|
||||
public static void putTextWithBackground(Mat image, String text, org.opencv.core.Point origin, Scalar textColor, Scalar backgroundColor, int padding) {
|
||||
// 默认字体
|
||||
int font = Imgproc.FONT_HERSHEY_SIMPLEX;
|
||||
// 默认字体缩放大小
|
||||
double fontScale = 0.6;
|
||||
//线条粗细
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图像上绘制带白色背景、黑色文字的文本
|
||||
*/
|
||||
public static void putTextWithBackground(Mat image, String text, org.opencv.core.Point origin, Scalar textColor, Scalar backgroundColor, int padding, double fontScale) {
|
||||
// 默认字体
|
||||
int font = Imgproc.FONT_HERSHEY_SIMPLEX;
|
||||
//线条粗细
|
||||
int thickness = 1;
|
||||
//获取文字大小
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制检测结果
|
||||
* @param image 待绘制的图片
|
||||
* @param detectionResponse 检测结果
|
||||
* @return 绘制后的图片
|
||||
*/
|
||||
public static void drawBoundingBoxes(Mat image, DetectionResponse detectionResponse){
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new IllegalArgumentException("无目标数据");
|
||||
}
|
||||
drawRectAndText(image, detectionResponse.getDetectionInfoList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Mat 上绘制关键点
|
||||
* @param mat OpenCV 图像
|
||||
* @param keyPoints 人脸关键点列表
|
||||
*/
|
||||
public static void drawLandmarks(Mat mat, List<cn.smartjavaai.common.entity.Point> keyPoints) {
|
||||
// 设置颜色 (BGR 格式),这里是橙色 (0,96,246)
|
||||
Scalar color = new Scalar(0, 96, 246);
|
||||
// 遍历关键点,用圆来表示 (比矩形更自然)
|
||||
for (cn.smartjavaai.common.entity.Point p : keyPoints) {
|
||||
Point cvPoint = new Point(p.getX(), p.getY());
|
||||
Imgproc.circle(mat, cvPoint, 2, color, 2, Imgproc.LINE_AA, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Mat 上绘制多行文字,并带背景
|
||||
* @param mat OpenCV Mat
|
||||
* @param lines 文字行
|
||||
* @param x 起始 x
|
||||
* @param y 起始 y
|
||||
*/
|
||||
public static void drawMultilineTextWithBackground(Mat mat, List<String> lines, int x, int y) {
|
||||
int fontFace = Imgproc.FONT_HERSHEY_SIMPLEX;
|
||||
double fontScale = 0.5; // 字体大小
|
||||
int thickness = 1;
|
||||
int baseline[] = {0};
|
||||
|
||||
// 逐行计算最大宽度 & 总高度
|
||||
int maxWidth = 0;
|
||||
int lineHeight = 0;
|
||||
for (String line : lines) {
|
||||
Size textSize = Imgproc.getTextSize(line, fontFace, fontScale, thickness, baseline);
|
||||
maxWidth = Math.max(maxWidth, (int) textSize.width);
|
||||
lineHeight = Math.max(lineHeight, (int) (textSize.height + baseline[0]));
|
||||
}
|
||||
|
||||
int padding = 4;
|
||||
int boxWidth = maxWidth + padding * 2;
|
||||
int boxHeight = lineHeight * lines.size() + padding * 2;
|
||||
|
||||
// 绘制背景矩形 (半透明黑色在 OpenCV 里不好直接实现,只能画实色或用 addWeighted 合成)
|
||||
Scalar bgColor = new Scalar(0, 0, 0); // BGR = 黑色
|
||||
Point topLeft = new Point(x, y);
|
||||
Point bottomRight = new Point(x + boxWidth, y + boxHeight);
|
||||
Imgproc.rectangle(mat, topLeft, bottomRight, bgColor, -1); // -1 表示填充
|
||||
|
||||
// 逐行绘制文字 (白色)
|
||||
Scalar textColor = new Scalar(255, 255, 255);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
int textY = y + padding + (i + 1) * lineHeight;
|
||||
Imgproc.putText(mat, lines.get(i),
|
||||
new Point(x + padding, textY),
|
||||
fontFace, fontScale, textColor, thickness, Imgproc.LINE_AA, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制人脸属性
|
||||
* @param faceAttribute
|
||||
* @param rectangle
|
||||
* @param mat
|
||||
*/
|
||||
public static void drawFaceAttribute(FaceAttribute faceAttribute, DetectionRectangle rectangle, Mat mat){
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (faceAttribute.getGenderType() != null) {
|
||||
lines.add("gender: " + faceAttribute.getGenderType());
|
||||
}
|
||||
if (faceAttribute.getAge() != null) {
|
||||
lines.add("age: " + faceAttribute.getAge());
|
||||
}
|
||||
if (faceAttribute.getWearingMask() != null) {
|
||||
lines.add("mask: " + (faceAttribute.getWearingMask() ? "yes" : "no"));
|
||||
}
|
||||
if (faceAttribute.getLeftEyeStatus() != null && faceAttribute.getRightEyeStatus() != null) {
|
||||
lines.add("eyes: " + faceAttribute.getLeftEyeStatus() + "/" + faceAttribute.getRightEyeStatus());
|
||||
}
|
||||
if (faceAttribute.getHeadPose() != null) {
|
||||
HeadPose pose = faceAttribute.getHeadPose();
|
||||
String pitch = pose.getPitch() != null ? String.valueOf(pose.getPitch().intValue()) : "-";
|
||||
String yaw = pose.getYaw() != null ? String.valueOf(pose.getYaw().intValue()) : "-";
|
||||
String roll = pose.getRoll() != null ? String.valueOf(pose.getRoll().intValue()) : "-";
|
||||
lines.add("head pose: P=" + pitch + " Y=" + yaw + " R=" + roll);
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
drawMultilineTextWithBackground(mat, lines, rectangle.getX(), rectangle.getY()); // 适当偏移
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 透视变换 + 裁剪
|
||||
* @param srcMat
|
||||
* @param landMarks
|
||||
* @return
|
||||
*/
|
||||
public static Image transformAndCrop(Mat srcMat, List<ai.djl.modality.cv.output.Point> landMarks){
|
||||
if (landMarks == null || landMarks.size() != 4) {
|
||||
throw new IllegalArgumentException("必须提供4个关键点");
|
||||
}
|
||||
|
||||
// 步骤 1:排序为 左上、右上、右下、左下
|
||||
List<ai.djl.modality.cv.output.Point> ordered = PointUtils.orderPoints(landMarks);
|
||||
|
||||
ai.djl.modality.cv.output.Point lt = ordered.get(0);
|
||||
ai.djl.modality.cv.output.Point rt = ordered.get(1);
|
||||
ai.djl.modality.cv.output.Point rb = ordered.get(2);
|
||||
ai.djl.modality.cv.output.Point lb = ordered.get(3);
|
||||
|
||||
// 步骤 2:计算目标图像尺寸(宽、高)
|
||||
int img_crop_width = (int) Math.max(
|
||||
PointUtils.distance(lt, rt),
|
||||
PointUtils.distance(rb, lb)
|
||||
);
|
||||
int img_crop_height = (int) Math.max(
|
||||
PointUtils.distance(lt, lb),
|
||||
PointUtils.distance(rt, rb)
|
||||
);
|
||||
|
||||
// 步骤 3:构造目标坐标点
|
||||
List<ai.djl.modality.cv.output.Point> dstPoints = Arrays.asList(
|
||||
new ai.djl.modality.cv.output.Point(0, 0),
|
||||
new ai.djl.modality.cv.output.Point(img_crop_width, 0),
|
||||
new ai.djl.modality.cv.output.Point(img_crop_width, img_crop_height),
|
||||
new ai.djl.modality.cv.output.Point(0, img_crop_height)
|
||||
);
|
||||
|
||||
// 步骤 4:透视变换
|
||||
Mat srcPoint2f = DJLCommonUtils.toMat(ordered);
|
||||
Mat dstPoint2f = DJLCommonUtils.toMat(dstPoints);
|
||||
Mat cvMat = OpenCVUtils.perspectiveTransform(srcMat, srcPoint2f, dstPoint2f);
|
||||
|
||||
// 步骤 5:转为 DJL Image + 裁剪
|
||||
Image subImg = OpenCVImageFactory.getInstance().fromImage(cvMat);
|
||||
subImg = subImg.getSubImage(0, 0, img_crop_width, img_crop_height);
|
||||
|
||||
// 释放资源
|
||||
cvMat.release();
|
||||
srcPoint2f.release();
|
||||
dstPoint2f.release();
|
||||
return subImg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mat To MatOfPoint
|
||||
* @param mat
|
||||
* @return
|
||||
*/
|
||||
public static MatOfPoint matToMatOfPoint(Mat mat) {
|
||||
int rows = mat.rows();
|
||||
MatOfPoint matOfPoint = new MatOfPoint();
|
||||
|
||||
List<Point> list = new ArrayList<>();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
Point point = new Point((float) mat.get(i, 0)[0], (float) mat.get(i, 1)[0]);
|
||||
list.add(point);
|
||||
}
|
||||
matOfPoint.fromList(list);
|
||||
|
||||
return matOfPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mat To double[][] Array
|
||||
* @param mat
|
||||
* @return
|
||||
*/
|
||||
public static double[][] matToDoubleArray(Mat mat) {
|
||||
int rows = mat.rows();
|
||||
int cols = mat.cols();
|
||||
double[][] doubles = new double[rows][cols];
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
doubles[i][j] = mat.get(i, j)[0];
|
||||
}
|
||||
}
|
||||
return doubles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mat To float[][] Array
|
||||
* @param mat
|
||||
* @return
|
||||
*/
|
||||
public static float[][] matToFloatArray(Mat mat) {
|
||||
int rows = mat.rows();
|
||||
int cols = mat.cols();
|
||||
|
||||
float[][] floats = new float[rows][cols];
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
floats[i][j] = (float) mat.get(i, j)[0];
|
||||
}
|
||||
}
|
||||
|
||||
return floats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mat To byte[][] Array
|
||||
* @param mat
|
||||
* @return
|
||||
*/
|
||||
public static byte[][] matToUint8Array(Mat mat) {
|
||||
int rows = mat.rows();
|
||||
int cols = mat.cols();
|
||||
|
||||
byte[][] bytes = new byte[rows][cols];
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
bytes[i][j] = (byte) mat.get(i, j)[0];
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* float[][] Array To Mat
|
||||
* @param arr
|
||||
* @return
|
||||
*/
|
||||
public static Mat floatArrayToMat(float[][] arr) {
|
||||
int rows = arr.length;
|
||||
int cols = arr[0].length;
|
||||
Mat mat = new Mat(rows, cols, CvType.CV_32F);
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
mat.put(i, j, arr[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
/**
|
||||
* byte[][] Array To Mat
|
||||
* @param arr
|
||||
* @return
|
||||
*/
|
||||
public static Mat uint8ArrayToMat(byte[][] arr) {
|
||||
int rows = arr.length;
|
||||
int cols = arr[0].length;
|
||||
Mat mat = new Mat(rows, cols, CvType.CV_8U);
|
||||
|
||||
for (int i = 0; i < rows; i++) {
|
||||
for (int j = 0; j < cols; j++) {
|
||||
mat.put(i, j, arr[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将自定义 Point 列表转换为 OpenCV Point 列表
|
||||
* @param pointList 自定义 Point 列表
|
||||
* @return OpenCV Point 列表
|
||||
*/
|
||||
public static List<Point> toCvPointList(List<cn.smartjavaai.common.entity.Point> pointList) {
|
||||
if (pointList == null) {
|
||||
return null;
|
||||
}
|
||||
return pointList.stream()
|
||||
.map(p -> new Point(p.getX(), p.getY()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static void drawPolygonWithText(Mat mat, List<PolygonLabel> polygonLabelList, int fontSize) {
|
||||
for (PolygonLabel polygonLabel : polygonLabelList){
|
||||
List<Point> cvPointList = toCvPointList(polygonLabel.getPoints());
|
||||
drawPolygonWithText(mat, cvPointList, polygonLabel.getText(), new Scalar(0, 255, 0), 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在图像上绘制多边形(任意边数)
|
||||
*
|
||||
* @param mat 图像
|
||||
* @param points 点的列表(至少2个点)
|
||||
* @param color 颜色
|
||||
* @param thickness 线宽
|
||||
*/
|
||||
public static void drawPolygonWithText(Mat mat, List<Point> points, String text, Scalar color, int thickness) {
|
||||
if (points == null || points.size() < 2) {
|
||||
return;
|
||||
}
|
||||
// 连线
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
Point p1 = points.get(i);
|
||||
Point p2 = points.get((i + 1) % points.size()); // 最后一个点连回第一个
|
||||
Imgproc.line(mat, p1, p2, color, thickness);
|
||||
}
|
||||
if(StringUtils.isNotBlank(text)){
|
||||
Scalar textScalar = new Scalar(0,0,0);
|
||||
// 保证文字不超出矩形
|
||||
Imgproc.putText(mat, text, points.get(0), Imgproc.FONT_HERSHEY_SIMPLEX, 1, textScalar, thickness);
|
||||
}
|
||||
}
|
||||
|
||||
public static Mat getSubImage(Mat image, int x, int y, int w, int h) {
|
||||
return image.submat(new Rect(x, y, w, h));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地路径读取图片并转为 Mat
|
||||
*
|
||||
* @param path 图片路径
|
||||
* @return Mat 对象
|
||||
*/
|
||||
public static Mat loadImage(String path) {
|
||||
// 使用 imread 读取
|
||||
Mat mat = Imgcodecs.imread(path);
|
||||
// 判空,避免后续处理时报错
|
||||
if (mat.empty()) {
|
||||
throw new IllegalArgumentException("无法加载图片: " + path);
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user