【人脸识别】 新增多种人脸识别模型

【底层优化】 支持自由选择 OpenCV 或 BufferedImage 作为图像引擎

【通用图像】 全部模型启用 Image 输入,支持各类图片格式与 Image 的互转

【模型管理】 优化模型生命周期,关闭后可重新创建

【人脸识别】 支持在人脸查询结果中绘制姓名标注

【人脸检测】 新增人脸裁剪功能

【修复】 修复若干已知问题,提升系统稳定性
This commit is contained in:
dengwenjie
2025-10-02 16:26:42 +08:00
parent 1b50e2b943
commit dfa8cf9bb4
133 changed files with 6635 additions and 3532 deletions

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
//当图片BufferedImageDJL的duplicate会有问题
//当图片BufferedImageDJL的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);
}
}
}

View File

@@ -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;
}
}