1、FaceNet 特征提取新增人脸对齐

2、人脸检测新5点人脸关键点定位
3、特征提取接口支持多人脸和最佳人脸提取
4、修复人脸框边界精度问题
5、更新 Maven 发布的 groupId
This commit is contained in:
dengwenjie
2025-04-28 16:13:34 +08:00
parent 2fdc20f610
commit 42d2943a94
50 changed files with 3413 additions and 554 deletions

149
smartjavaai-ocr/pom.xml Normal file
View File

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

View File

@@ -0,0 +1,24 @@
package cn.smartjavaai.ocr;
import cn.smartjavaai.common.entity.DetectionResponse;
/**
* 人脸识别算法
* @author dwj
*/
public abstract class AbstractOcrModel implements OcrModel {
@Override
public void loadModel(OcrModelConfig config) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public DetectionResponse detect(String imagePath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
@Override
public void detectAndDraw(String imagePath, String outputPath) {
throw new UnsupportedOperationException("默认不支持该功能");
}
}

View File

@@ -0,0 +1,35 @@
package cn.smartjavaai.ocr;
import cn.smartjavaai.common.entity.DetectionResponse;
import java.awt.image.BufferedImage;
import java.io.InputStream;
/**
* 人脸识别算法
* @author dwj
*/
public interface OcrModel {
/**
* 加载模型
* @param config
*/
void loadModel(OcrModelConfig config); // 加载模型
/**
* 人脸检测
* @param imagePath 图片路径
* @return
*/
DetectionResponse detect(String imagePath);
/**
* 检测并绘制结果
* @param imagePath 图片输入路径(包含文件名称)
* @param outputPath 图片输出路径(包含文件名称)
*/
void detectAndDraw(String imagePath, String outputPath);
}

View File

@@ -0,0 +1,28 @@
package cn.smartjavaai.ocr;
import cn.smartjavaai.common.enums.DeviceEnum;
import lombok.Data;
/**
* @author dwj
* @date 2025/4/22
*/
@Data
public class OcrModelConfig {
/**
* 模型名称
*/
private OcrModelEnum modelEnum;
/**
* 设备类型
*/
private DeviceEnum device;
/**
* 模型路径
*/
private String modelPath;
}

View File

@@ -0,0 +1,27 @@
package cn.smartjavaai.ocr;
/**
* OCR模型枚举
* @author dwj
* @date 2025/4/4
*/
public enum OcrModelEnum {
PADDLEOCR_V4_DET_MODEL;
/**
* 根据名称获取枚举 (忽略大小写和下划线变体)
*/
public static OcrModelEnum fromName(String name) {
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
for (OcrModelEnum model : values()) {
if (model.name().replaceAll("_", "").equals(formatted)) {
return model;
}
}
throw new IllegalArgumentException("未知模型名称: " + name);
}
}

View File

@@ -0,0 +1,95 @@
package cn.smartjavaai.ocr;
import cn.smartjavaai.common.config.Config;
import cn.smartjavaai.ocr.exception.OcrException;
import cn.smartjavaai.ocr.model.PaddleOCRV4DetectModel;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 人脸算法工厂
* @author dwj
*/
@Slf4j
public class OcrModelFactory {
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
private static volatile OcrModelFactory instance;
private static final ConcurrentHashMap<String, OcrModel> modelMap = new ConcurrentHashMap<>();
/**
* 算法注册表
*/
private static final Map<String, Class<? extends OcrModel>> registry =
new ConcurrentHashMap<>();
public static OcrModelFactory getInstance() {
if (instance == null) {
synchronized (OcrModelFactory.class) {
if (instance == null) {
instance = new OcrModelFactory();
}
}
}
return instance;
}
/**
* 注册算法
* @param name
* @param clazz
*/
private static void registerModel(String name, Class<? extends OcrModel> clazz) {
registry.put(name.toLowerCase(), clazz);
}
/**
* 获取模型(通过配置)
* @param config
* @return
*/
public OcrModel getModel(OcrModelConfig config) {
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
throw new OcrException("未配置OCR模型");
}
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
return createFaceModel(config);
});
}
/**
* 使用ModelConfig创建算法
* @param config
* @return
*/
private OcrModel createFaceModel(OcrModelConfig config) {
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
if(clazz == null){
throw new OcrException("Unsupported model");
}
OcrModel algorithm = null;
try {
algorithm = (OcrModel) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new OcrException(e);
}
algorithm.loadModel(config);
return algorithm;
}
// 初始化默认算法
static {
registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetectModel.class);
log.info("缓存目录:{}", Config.getCachePath());
}
}

View File

@@ -0,0 +1,50 @@
package cn.smartjavaai.ocr.entity;
import ai.djl.modality.cv.Image;
import ai.djl.ndarray.NDArray;
/**
* 图像信息
*/
public class ImageInfo {
private String name;
private Double prob;
private Image image;
private NDArray box;
public ImageInfo(Image image, NDArray box) {
this.image = image;
this.box = box;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getProb() {
return prob;
}
public void setProb(Double prob) {
this.prob = prob;
}
public Image getImage() {
return image;
}
public void setImage(Image image) {
this.image = image;
}
public NDArray getBox() {
return box;
}
public void setBox(NDArray box) {
this.box = box;
}
}

View File

@@ -0,0 +1,46 @@
package cn.smartjavaai.ocr.entity;
import ai.djl.ndarray.NDArray;
/**
* 旋转检测框
*/
public class RotatedBox implements Comparable<RotatedBox> {
private NDArray box;
private String text;
public RotatedBox(NDArray box, String text) {
this.box = box;
this.text = text;
}
/**
* 将左上角 Y 坐标升序排序
*
* @param o
* @return
*/
@Override
public int compareTo(RotatedBox o) {
NDArray lowBox = this.getBox();
NDArray highBox = o.getBox();
float lowY = lowBox.toFloatArray()[1];
float highY = highBox.toFloatArray()[1];
return (lowY < highY) ? -1 : 1;
}
public NDArray getBox() {
return box;
}
public void setBox(NDArray box) {
this.box = box;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

View File

@@ -0,0 +1,46 @@
package cn.smartjavaai.ocr.entity;
import ai.djl.ndarray.NDArray;
/**
* 旋转检测框 - 支持左上角 X 坐标升序排序
*/
public class RotatedBoxCompX implements Comparable<RotatedBoxCompX> {
private NDArray box;
private String text;
public RotatedBoxCompX(NDArray box, String text) {
this.box = box;
this.text = text;
}
/**
* 将左上角 X 坐标升序排序
*
* @param o
* @return
*/
@Override
public int compareTo(RotatedBoxCompX o) {
NDArray leftBox = this.getBox();
NDArray rightBox = o.getBox();
float leftX = leftBox.toFloatArray()[0];
float rightX = rightBox.toFloatArray()[0];
return (leftX < rightX) ? -1 : 1;
}
public NDArray getBox() {
return box;
}
public void setBox(NDArray box) {
this.box = box;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

View File

@@ -0,0 +1,30 @@
package cn.smartjavaai.ocr.exception;
/**
* 人脸检测异常
* @author dwj
* @date 2025/4/4
*/
public class OcrException extends RuntimeException{
public OcrException() {
super();
}
public OcrException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public OcrException(String message, Throwable cause) {
super(message, cause);
}
public OcrException(String message) {
super(message);
}
public OcrException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,134 @@
package cn.smartjavaai.ocr.model;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ModelZoo;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;
import ai.djl.translate.TranslateException;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.pool.PredictorFactory;
import cn.smartjavaai.common.utils.FileUtils;
import cn.smartjavaai.ocr.AbstractOcrModel;
import cn.smartjavaai.ocr.OcrModelConfig;
import cn.smartjavaai.ocr.exception.OcrException;
import cn.smartjavaai.ocr.translator.PaddleOCRV4DetectionTranslator;
import cn.smartjavaai.ocr.utils.ImageUtils;
import cn.smartjavaai.ocr.utils.OcrUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.opencv.core.Mat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dwj
* @date 2025/4/21
*/
@Slf4j
public class PaddleOCRV4DetectModel extends AbstractOcrModel {
private ZooModel detectionModel;
private ObjectPool<Predictor<Image, NDList>> predictorPool;
@Override
public void loadModel(OcrModelConfig config){
if(StringUtils.isBlank(config.getModelPath())){
throw new OcrException("modelPath is null");
}
Criteria<Image, NDList> criteria =
Criteria.builder()
.optEngine("OnnxRuntime")
.setTypes(Image.class, NDList.class)
.optModelPath(Paths.get(config.getModelPath()))
.optTranslator(new PaddleOCRV4DetectionTranslator(new ConcurrentHashMap<String, String>()))
.optProgress(new ProgressBar())
.build();
try{
detectionModel = ModelZoo.loadModel(criteria);
// 创建池子:每个线程独享 Predictor
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
throw new OcrException("模型加载失败", e);
}
}
@Override
public DetectionResponse detect(String imagePath){
if(!FileUtils.isFileExists(imagePath)){
throw new OcrException("图像文件不存在");
}
Image img = null;
try {
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
} catch (IOException e) {
throw new OcrException("无效的图片", e);
}
return detect(img);
}
/**
* 人脸检测
* @param image
* @return
*/
private DetectionResponse detect(Image image){
Predictor<Image, NDList> predictor = null;
try {
predictor = predictorPool.borrowObject();
NDList result = predictor.predict(image);
return OcrUtils.convertToDetectionResponse(result, image);
} catch (Exception e) {
throw new OcrException("OCR检测错误", e);
}finally {
if (predictor != null) {
try {
predictorPool.returnObject(predictor); //归还
} catch (Exception e) {
log.warn("归还Predictor失败", e);
try {
predictor.close(); // 归还失败才销毁
} catch (Exception ex) {
log.error("关闭Predictor失败", ex);
}
}
}
}
}
@Override
public void detectAndDraw(String imagePath, String outputPath) {
if(!FileUtils.isFileExists(imagePath)){
throw new OcrException("图像文件不存在");
}
try {
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
DetectionResponse detectionResponse = detect(img);
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getRectangleList()) || detectionResponse.getRectangleList().isEmpty()){
throw new OcrException("未识别到文字");
}
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
Path output = Paths.get(outputPath);
log.info("Saving to {}", output.toAbsolutePath().toString());
img.save(Files.newOutputStream(output), "png");
} catch (IOException e) {
throw new OcrException(e);
}
}
}

View File

@@ -0,0 +1,27 @@
package cn.smartjavaai.ocr.model;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelZoo;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;
import cn.smartjavaai.ocr.translator.PaddleOCRV4DetectionTranslator;
import org.opencv.core.Mat;
import java.nio.file.Paths;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dwj
* @date 2025/4/21
*/
public class PaddleOCRV4Model {
public void loadModel(){
}
}

View File

@@ -0,0 +1,228 @@
package cn.smartjavaai.ocr.opencv;
import ai.djl.ndarray.NDArray;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import java.util.ArrayList;
import java.util.List;
/**
* NDArray Utils
*
*/
public class OcrNDArrayUtils {
/**
* 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;
}
/**
* 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;
}
/**
* 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 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;
}
/**
* 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;
}
/**
* List To Mat
* @param points
* @return
*/
public static Mat toMat(List<ai.djl.modality.cv.output.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;
}
}

View File

@@ -0,0 +1,64 @@
package cn.smartjavaai.ocr.opencv;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
/**
* OpenCV Utils
*
*/
public class OcrOpenCVUtils {
/**
* 透视变换
*
* @param src
* @param srcPoints
* @param dstPoints
* @return
*/
public static Mat perspectiveTransform(Mat src, Mat srcPoints, Mat dstPoints) {
Mat dst = src.clone();
Mat warp_mat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
Imgproc.warpPerspective(src, dst, warp_mat, dst.size());
warp_mat.release();
return dst;
}
/**
* Mat to BufferedImage
*
* @param mat
* @return
*/
public static BufferedImage mat2Image(Mat mat) {
int width = mat.width();
int height = mat.height();
byte[] data = new byte[width * height * (int) mat.elemSize()];
Imgproc.cvtColor(mat, mat, 4);
mat.get(0, 0, data);
BufferedImage ret = new BufferedImage(width, height, 5);
ret.getRaster().setDataElements(0, 0, width, height, data);
return ret;
}
/**
* BufferedImage to Mat
*
* @param img
* @return
*/
public static Mat image2Mat(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
byte[] data = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
Mat mat = new Mat(height, width, CvType.CV_8UC3);
mat.put(0, 0, data);
return mat;
}
}

View File

@@ -0,0 +1,521 @@
package cn.smartjavaai.ocr.translator;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDArrays;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.index.NDIndex;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import ai.djl.translate.Batchifier;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 文字检测前后处理
*
* @author Calvin
* @mail 179209347@qq.com
* @website www.aias.top
*/
public class PaddleOCRV4DetectionTranslator implements Translator<Image, NDList> {
// det_algorithm == "DB"
private final float thresh = 0.3f;
private final boolean use_dilation = false;
private final String score_mode = "fast";
private final String box_type = "quad";
private final int limit_side_len;
private final int max_candidates;
private final int min_size;
private final float box_thresh;
private final float unclip_ratio;
private float ratio_h;
private float ratio_w;
private int img_height;
private int img_width;
public PaddleOCRV4DetectionTranslator(Map<String, ?> arguments) {
limit_side_len =
arguments.containsKey("limit_side_len")
? Integer.parseInt(arguments.get("limit_side_len").toString())
: 960;
max_candidates =
arguments.containsKey("max_candidates")
? Integer.parseInt(arguments.get("max_candidates").toString())
: 1000;
min_size =
arguments.containsKey("min_size")
? Integer.parseInt(arguments.get("min_size").toString())
: 3;
box_thresh =
arguments.containsKey("box_thresh")
? Float.parseFloat(arguments.get("box_thresh").toString())
: 0.6f; // 0.5f
unclip_ratio =
arguments.containsKey("unclip_ratio")
? Float.parseFloat(arguments.get("unclip_ratio").toString())
: 1.6f;
}
@Override
public NDList processOutput(TranslatorContext ctx, NDList list) {
NDManager manager = ctx.getNDManager();
NDArray pred = list.get(0);
pred = pred.squeeze();
NDArray segmentation = pred.gt(thresh); // thresh=0.3 .mul(255f)
segmentation = segmentation.toType(DataType.UINT8, true);
Shape shape = segmentation.getShape();
int rows = (int) shape.get(0);
int cols = (int) shape.get(1);
Mat newMask = new Mat();
if (this.use_dilation) {
Mat mask = new Mat();
//convert from NDArray to Mat
Mat srcMat = OcrNDArrayUtils.uint8NDArrayToMat(segmentation);
// size 越小,腐蚀的单位越小,图片越接近原图
// Mat dilation_kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(2, 2));
Mat dilation_kernel = OcrNDArrayUtils.uint8ArrayToMat(new byte[][]{{1, 1}, {1, 1}});
/**
* 膨胀说明: 图像的一部分区域与指定的核进行卷积, 求核的最`大`值并赋值给指定区域。 膨胀可以理解为图像中`高亮区域`的'领域扩大'。
* 意思是高亮部分会侵蚀不是高亮的部分,使高亮部分越来越多。
*/
Imgproc.dilate(srcMat, mask, dilation_kernel);
//destination Matrix
Scalar scalar = new Scalar(255);
Core.multiply(mask, scalar, newMask);
// release Mat
mask.release();
srcMat.release();
dilation_kernel.release();
} else {
Mat srcMat = OcrNDArrayUtils.uint8NDArrayToMat(segmentation);
//destination Matrix
Scalar scalar = new Scalar(255);
Core.multiply(srcMat, scalar, newMask);
// release Mat
srcMat.release();
}
NDArray boxes = boxes_from_bitmap(manager, pred, newMask);
//boxes[:, :, 0] = boxes[:, :, 0] / ratio_w
NDArray boxes1 = boxes.get(":, :, 0").div(ratio_w);
boxes.set(new NDIndex(":, :, 0"), boxes1);
//boxes[:, :, 1] = boxes[:, :, 1] / ratio_h
NDArray boxes2 = boxes.get(":, :, 1").div(ratio_h);
boxes.set(new NDIndex(":, :, 1"), boxes2);
NDList dt_boxes = this.filter_tag_det_res(boxes);
dt_boxes.detach();
// release Mat
newMask.release();
return dt_boxes;
}
private NDList filter_tag_det_res(NDArray dt_boxes) {
NDList boxesList = new NDList();
int num = (int) dt_boxes.getShape().get(0);
for (int i = 0; i < num; i++) {
NDArray box = dt_boxes.get(i);
box = order_points_clockwise(box);
box = clip_det_res(box);
float[] box0 = box.get(0).toFloatArray();
float[] box1 = box.get(1).toFloatArray();
float[] box3 = box.get(3).toFloatArray();
int rect_width = (int) Math.sqrt(Math.pow(box1[0] - box0[0], 2) + Math.pow(box1[1] - box0[1], 2));
int rect_height = (int) Math.sqrt(Math.pow(box3[0] - box0[0], 2) + Math.pow(box3[1] - box0[1], 2));
if (rect_width <= 3 || rect_height <= 3)
continue;
boxesList.add(box);
}
return boxesList;
}
private NDArray clip_det_res(NDArray points) {
for (int i = 0; i < points.getShape().get(0); i++) {
int value = Math.max((int) points.get(i, 0).toFloatArray()[0], 0);
value = Math.min(value, img_width - 1);
points.set(new NDIndex(i + ",0"), value);
value = Math.max((int) points.get(i, 1).toFloatArray()[0], 0);
value = Math.min(value, img_height - 1);
points.set(new NDIndex(i + ",1"), value);
}
return points;
}
/**
* sort the points based on their x-coordinates
* 顺时针
*
* @param pts
* @return
*/
private NDArray order_points_clockwise(NDArray pts) {
NDList list = new NDList();
long[] indexes = pts.get(":, 0").argSort().toLongArray();
// grab the left-most and right-most points from the sorted
// x-roodinate points
Shape s1 = pts.getShape();
NDArray leftMost1 = pts.get(indexes[0] + ",:");
NDArray leftMost2 = pts.get(indexes[1] + ",:");
NDArray leftMost = leftMost1.concat(leftMost2).reshape(2, 2);
NDArray rightMost1 = pts.get(indexes[2] + ",:");
NDArray rightMost2 = pts.get(indexes[3] + ",:");
NDArray rightMost = rightMost1.concat(rightMost2).reshape(2, 2);
// now, sort the left-most coordinates according to their
// y-coordinates so we can grab the top-left and bottom-left
// points, respectively
indexes = leftMost.get(":, 1").argSort().toLongArray();
NDArray lt = leftMost.get(indexes[0] + ",:");
NDArray lb = leftMost.get(indexes[1] + ",:");
indexes = rightMost.get(":, 1").argSort().toLongArray();
NDArray rt = rightMost.get(indexes[0] + ",:");
NDArray rb = rightMost.get(indexes[1] + ",:");
list.add(lt);
list.add(rt);
list.add(rb);
list.add(lb);
NDArray rect = NDArrays.concat(list).reshape(4, 2);
return rect;
}
/**
* Get boxes from the binarized image predicted by DB
*
* @param manager
* @param pred the binarized image predicted by DB.
* @param bitmap new 'pred' after threshold filtering.
*/
private NDArray boxes_from_bitmap(NDManager manager, NDArray pred, Mat bitmap) {
int dest_height = (int) pred.getShape().get(0);
int dest_width = (int) pred.getShape().get(1);
int height = bitmap.rows();
int width = bitmap.cols();
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
// 寻找轮廓
Imgproc.findContours(
bitmap,
contours,
hierarchy,
Imgproc.RETR_LIST,
Imgproc.CHAIN_APPROX_SIMPLE);
int num_contours = Math.min(contours.size(), max_candidates);
NDList boxList = new NDList();
float[] scores = new float[num_contours];
for (int index = 0; index < num_contours; index++) {
MatOfPoint contour = contours.get(index);
MatOfPoint2f newContour = new MatOfPoint2f(contour.toArray());
float[][] pointsArr = new float[4][2];
int sside = get_mini_boxes(newContour, pointsArr);
if (sside < this.min_size)
continue;
NDArray points = manager.create(pointsArr);
float score = box_score_fast(manager, pred, points);
if (score < this.box_thresh)
continue;
NDArray box = unclip(manager, points); // TODO get_mini_boxes(box)
// box[:, 0] = np.clip(np.round(box[:, 0] / width * dest_width), 0, dest_width)
NDArray boxes1 = box.get(":,0").div(width).mul(dest_width).round().clip(0, dest_width);
box.set(new NDIndex(":, 0"), boxes1);
// box[:, 1] = np.clip(np.round(box[:, 1] / height * dest_height), 0, dest_height)
NDArray boxes2 = box.get(":,1").div(height).mul(dest_height).round().clip(0, dest_height);
box.set(new NDIndex(":, 1"), boxes2);
boxList.add(box);
scores[index] = score;
// release memory
contour.release();
newContour.release();
}
NDArray boxes = NDArrays.stack(boxList);
// release
hierarchy.release();
return boxes;
}
/**
* Shrink or expand the boxaccording to 'unclip_ratio'
*
* @param points The predicted box.
* @return uncliped box
*/
private NDArray unclip(NDManager manager, NDArray points) {
points = order_points_clockwise(points);
float[] pointsArr = points.toFloatArray();
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
float width = distance(lt, rt);
float height = distance(lt, lb);
if (width > height) {
float k = (lt[1] - rt[1]) / (lt[0] - rt[0]); // y = k * x + b
float delta_dis = height;
float delta_x = (float) Math.sqrt((delta_dis * delta_dis) / (k * k + 1));
float delta_y = Math.abs(k * delta_x);
if (k > 0) {
pointsArr[0] = lt[0] - delta_x + delta_y;
pointsArr[1] = lt[1] - delta_y - delta_x;
pointsArr[2] = rt[0] + delta_x + delta_y;
pointsArr[3] = rt[1] + delta_y - delta_x;
pointsArr[4] = rb[0] + delta_x - delta_y;
pointsArr[5] = rb[1] + delta_y + delta_x;
pointsArr[6] = lb[0] - delta_x - delta_y;
pointsArr[7] = lb[1] - delta_y + delta_x;
} else {
pointsArr[0] = lt[0] - delta_x - delta_y;
pointsArr[1] = lt[1] + delta_y - delta_x;
pointsArr[2] = rt[0] + delta_x - delta_y;
pointsArr[3] = rt[1] - delta_y - delta_x;
pointsArr[4] = rb[0] + delta_x + delta_y;
pointsArr[5] = rb[1] - delta_y + delta_x;
pointsArr[6] = lb[0] - delta_x + delta_y;
pointsArr[7] = lb[1] + delta_y + delta_x;
}
} else {
float k = (lt[1] - rt[1]) / (lt[0] - rt[0]); // y = k * x + b
float delta_dis = width;
float delta_y = (float) Math.sqrt((delta_dis * delta_dis) / (k * k + 1));
float delta_x = Math.abs(k * delta_y);
if (k > 0) {
pointsArr[0] = lt[0] + delta_x - delta_y;
pointsArr[1] = lt[1] - delta_y - delta_x;
pointsArr[2] = rt[0] + delta_x + delta_y;
pointsArr[3] = rt[1] - delta_y + delta_x;
pointsArr[4] = rb[0] - delta_x + delta_y;
pointsArr[5] = rb[1] + delta_y + delta_x;
pointsArr[6] = lb[0] - delta_x - delta_y;
pointsArr[7] = lb[1] + delta_y - delta_x;
} else {
pointsArr[0] = lt[0] - delta_x - delta_y;
pointsArr[1] = lt[1] - delta_y + delta_x;
pointsArr[2] = rt[0] - delta_x + delta_y;
pointsArr[3] = rt[1] - delta_y - delta_x;
pointsArr[4] = rb[0] + delta_x + delta_y;
pointsArr[5] = rb[1] + delta_y - delta_x;
pointsArr[6] = lb[0] + delta_x - delta_y;
pointsArr[7] = lb[1] + delta_y + delta_x;
}
}
points = manager.create(pointsArr).reshape(4, 2);
return points;
}
private float distance(float[] point1, float[] point2) {
float disX = point1[0] - point2[0];
float disY = point1[1] - point2[1];
float dis = (float) Math.sqrt(disX * disX + disY * disY);
return dis;
}
/**
* Get boxes from the contour or box.
*
* @param contour The predicted contour.
* @param pointsArr The predicted box.
* @return smaller side of box
*/
private int get_mini_boxes(MatOfPoint2f contour, float[][] pointsArr) {
// https://blog.csdn.net/qq_37385726/article/details/82313558
// bounding_box[1] - rect 返回矩形的长和宽
RotatedRect rect = Imgproc.minAreaRect(contour);
Mat points = new Mat();
Imgproc.boxPoints(rect, points);
float[][] fourPoints = new float[4][2];
for (int row = 0; row < 4; row++) {
fourPoints[row][0] = (float) points.get(row, 0)[0];
fourPoints[row][1] = (float) points.get(row, 1)[0];
}
float[] tmpPoint = new float[2];
for (int i = 0; i < 4; i++) {
for (int j = i + 1; j < 4; j++) {
if (fourPoints[j][0] < fourPoints[i][0]) {
tmpPoint[0] = fourPoints[i][0];
tmpPoint[1] = fourPoints[i][1];
fourPoints[i][0] = fourPoints[j][0];
fourPoints[i][1] = fourPoints[j][1];
fourPoints[j][0] = tmpPoint[0];
fourPoints[j][1] = tmpPoint[1];
}
}
}
int index_1 = 0;
int index_2 = 1;
int index_3 = 2;
int index_4 = 3;
if (fourPoints[1][1] > fourPoints[0][1]) {
index_1 = 0;
index_4 = 1;
} else {
index_1 = 1;
index_4 = 0;
}
if (fourPoints[3][1] > fourPoints[2][1]) {
index_2 = 2;
index_3 = 3;
} else {
index_2 = 3;
index_3 = 2;
}
pointsArr[0] = fourPoints[index_1];
pointsArr[1] = fourPoints[index_2];
pointsArr[2] = fourPoints[index_3];
pointsArr[3] = fourPoints[index_4];
int height = rect.boundingRect().height;
int width = rect.boundingRect().width;
int sside = Math.min(height, width);
// release
points.release();
return sside;
}
/**
* Calculate the score of box.
*
* @param bitmap The binarized image predicted by DB.
* @param points The predicted box
* @return
*/
private float box_score_fast(NDManager manager, NDArray bitmap, NDArray points) {
NDArray box = points.get(":");
long h = bitmap.getShape().get(0);
long w = bitmap.getShape().get(1);
// xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1)
int xmin = box.get(":, 0").min().floor().clip(0, w - 1).toType(DataType.INT32, true).toIntArray()[0];
int xmax = box.get(":, 0").max().ceil().clip(0, w - 1).toType(DataType.INT32, true).toIntArray()[0];
int ymin = box.get(":, 1").min().floor().clip(0, h - 1).toType(DataType.INT32, true).toIntArray()[0];
int ymax = box.get(":, 1").max().ceil().clip(0, h - 1).toType(DataType.INT32, true).toIntArray()[0];
NDArray mask = manager.zeros(new Shape(ymax - ymin + 1, xmax - xmin + 1), DataType.UINT8);
box.set(new NDIndex(":, 0"), box.get(":, 0").sub(xmin));
box.set(new NDIndex(":, 1"), box.get(":, 1").sub(ymin));
//mask - convert from NDArray to Mat
Mat maskMat = OcrNDArrayUtils.uint8NDArrayToMat(mask);
//mask - convert from NDArray to Mat - 4 rows, 2 cols
Mat boxMat = OcrNDArrayUtils.floatNDArrayToMat(box, CvType.CV_32S);
// boxMat.reshape(1, new int[]{1, 4, 2});
List<MatOfPoint> pts = new ArrayList<>();
MatOfPoint matOfPoint = OcrNDArrayUtils.matToMatOfPoint(boxMat); // new MatOfPoint(boxMat);
pts.add(matOfPoint);
Imgproc.fillPoly(maskMat, pts, new Scalar(1));
NDArray subBitMap = bitmap.get(ymin + ":" + (ymax + 1) + "," + xmin + ":" + (xmax + 1));
Mat bitMapMat = OcrNDArrayUtils.floatNDArrayToMat(subBitMap);
Scalar score = Core.mean(bitMapMat, maskMat);
float scoreValue = (float) score.val[0];
// release
maskMat.release();
boxMat.release();
bitMapMat.release();
return scoreValue;
}
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
NDArray img = input.toNDArray(ctx.getNDManager());
int h = input.getHeight();
int w = input.getWidth();
img_height = h;
img_width = w;
// limit the max side
float ratio = 1.0f;
if (Math.max(h, w) > limit_side_len) {
if (h > w) {
ratio = (float) limit_side_len / (float) h;
} else {
ratio = (float) limit_side_len / (float) w;
}
}
int resize_h = (int) (h * ratio);
int resize_w = (int) (w * ratio);
resize_h = Math.round((float) resize_h / 32f) * 32;
resize_w = Math.round((float) resize_w / 32f) * 32;
ratio_h = resize_h / (float) h;
ratio_w = resize_w / (float) w;
img = NDImageUtils.resize(img, resize_w, resize_h);
img = NDImageUtils.toTensor(img);
img =
NDImageUtils.normalize(
img,
new float[]{0.485f, 0.456f, 0.406f},
new float[]{0.229f, 0.224f, 0.225f});
img = img.expandDims(0);
return new NDList(img);
}
@Override
public Batchifier getBatchifier() {
return null;
}
}

View File

@@ -0,0 +1,123 @@
package cn.smartjavaai.ocr.translator;
import ai.djl.Model;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.index.NDIndex;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import ai.djl.translate.Batchifier;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import ai.djl.util.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 文字识别前后处理
*
*/
public class PaddleOCRV4WordRecTranslator implements Translator<Image, String> {
private List<String> table;
private final boolean use_space_char;
public PaddleOCRV4WordRecTranslator(Map<String, ?> arguments) {
use_space_char =
arguments.containsKey("use_space_char")
? Boolean.parseBoolean(arguments.get("use_space_char").toString())
: true;
}
@Override
public void prepare(TranslatorContext ctx) throws IOException {
Model model = ctx.getModel();
try (InputStream is = model.getArtifact("dict.txt").openStream()) {
table = Utils.readLines(is, true);
table.add(0, "blank");
if(use_space_char){
table.add(" ");
table.add(" ");
}
else{
table.add("");
table.add("");
}
}
}
@Override
public String processOutput(TranslatorContext ctx, NDList list) throws IOException {
StringBuilder sb = new StringBuilder();
NDArray tokens = list.singletonOrThrow();
long[] indices = tokens.get(0).argMax(1).toLongArray();
boolean[] selection = new boolean[indices.length];
Arrays.fill(selection, true);
for (int i = 1; i < indices.length; i++) {
if (indices[i] == indices[i - 1]) {
selection[i] = false;
}
}
// 字符置信度
// float[] probs = new float[indices.length];
// for (int row = 0; row < indices.length; row++) {
// NDArray value = tokens.get(0).get(new NDIndex(""+ row +":" + (row + 1) +"," + indices[row] +":" + ( indices[row] + 1)));
// probs[row] = value.toFloatArray()[0];
// }
int lastIdx = 0;
for (int i = 0; i < indices.length; i++) {
if (selection[i] == true && indices[i] > 0 && !(i > 0 && indices[i] == lastIdx)) {
sb.append(table.get((int) indices[i]));
}
}
return sb.toString();
}
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
NDArray img = input.toNDArray(ctx.getNDManager(), Image.Flag.COLOR);
int imgC = 3;
int imgH = 48;
int imgW = 320;
float max_wh_ratio = (float) imgW / (float) imgH;
int h = input.getHeight();
int w = input.getWidth();
float wh_ratio = (float) w / (float) h;
max_wh_ratio = Math.max(max_wh_ratio,wh_ratio);
imgW = (int)(imgH * max_wh_ratio);
int resized_w;
if (Math.ceil(imgH * wh_ratio) > imgW) {
resized_w = imgW;
} else {
resized_w = (int) (Math.ceil(imgH * wh_ratio));
}
NDArray resized_image = NDImageUtils.resize(img, resized_w, imgH);
resized_image = resized_image.transpose(2, 0, 1).toType(DataType.FLOAT32,false);
resized_image.divi(255f).subi(0.5f).divi(0.5f);
NDArray padding_im = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW), DataType.FLOAT32);
padding_im.set(new NDIndex(":,:,0:" + resized_w), resized_image);
padding_im = padding_im.flip(0);
padding_im = padding_im.expandDims(0);
return new NDList(padding_im);
}
@Override
public Batchifier getBatchifier() {
return null;
}
}

View File

@@ -0,0 +1,219 @@
package cn.smartjavaai.ocr.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import cn.smartjavaai.common.utils.OpenCVUtils;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 图像工具类
*/
public class ImageUtils {
/**
* 保存BufferedImage图片
*
* @param img
* @param name
* @param path
*/
public static void saveImage(BufferedImage img, String name, String path) {
Image djlImg = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(img)); // 支持多种图片格式,自动适配
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();
}
}
/**
* 保存DJL图片
*
* @param img
* @param name
* @param path
*/
public static void saveImage(Image img, String name, String path) {
Path outputDir = Paths.get(path);
Path imagePath = outputDir.resolve(name);
// OpenJDK 不能保存 jpg 图片的 alpha channel
try {
img.save(Files.newOutputStream(imagePath), "png");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 保存图片,含检测框
*
* @param img
* @param detection
* @param name
* @param path
* @throws IOException
*/
public static void saveBoundingBoxImage(
Image img, DetectedObjects detection, String name, String path) throws IOException {
// Make image copy with alpha channel because original image was jpg
img.drawBoundingBoxes(detection);
Path outputDir = Paths.get(path);
Files.createDirectories(outputDir);
Path imagePath = outputDir.resolve(name);
// OpenJDK can't save jpg with alpha channel
img.save(Files.newOutputStream(imagePath), "png");
}
/**
* 画检测框(有倾斜角)
*
* @param image
* @param box
*/
public static void drawImageRect(BufferedImage image, NDArray box) {
float[] points = box.toFloatArray();
int[] xPoints = new int[5];
int[] yPoints = new int[5];
for (int i = 0; i < 4; i++) {
xPoints[i] = (int) points[2 * i];
yPoints[i] = (int) points[2 * i + 1];
}
xPoints[4] = xPoints[0];
yPoints[4] = yPoints[0];
// 将绘制图像转换为Graphics2D
Graphics2D g = (Graphics2D) image.getGraphics();
try {
g.setColor(new Color(0, 255, 0));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
} finally {
g.dispose();
}
}
/**
* 画检测框(有倾斜角)和文本
*
* @param image
* @param box
* @param text
*/
public static void drawImageRectWithText(BufferedImage image, NDArray box, String text) {
float[] points = box.toFloatArray();
int[] xPoints = new int[5];
int[] yPoints = new int[5];
for (int i = 0; i < 4; i++) {
xPoints[i] = (int) points[2 * i];
yPoints[i] = (int) points[2 * i + 1];
}
xPoints[4] = xPoints[0];
yPoints[4] = yPoints[0];
// 将绘制图像转换为Graphics2D
Graphics2D g = (Graphics2D) image.getGraphics();
try {
int fontSize = 32;
Font font = new Font("楷体", Font.PLAIN, fontSize);
g.setFont(font);
g.setColor(new Color(0, 0, 255));
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
g.setStroke(bStroke);
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
g.drawString(text, xPoints[0], yPoints[0]);
} finally {
g.dispose();
}
}
/**
* 画检测框
*
* @param image
* @param 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 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 mat
* @param box
*/
public static void drawRect(Mat mat, DetectionResponse detectionResponse) {
for(DetectionRectangle detectionRectangle : detectionResponse.getRectangleList()){
// 左上角点
Point topLeft = new Point(detectionRectangle.getX(), detectionRectangle.getY());
// 右下角点
Point bottomRight = new Point(detectionRectangle.getX() + detectionRectangle.getWidth(), detectionRectangle.getY() + detectionRectangle.getHeight());
// 绘制矩形图像、左上角、右下角、颜色BGR、线宽
Imgproc.rectangle(mat, topLeft, bottomRight, new Scalar(0, 255, 0), 1);
}
}
}

View File

@@ -0,0 +1,64 @@
package cn.smartjavaai.ocr.utils;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* @author dwj
* @date 2025/4/22
*/
@Slf4j
public class OcrUtils {
/**
* 转换为FaceDetectedResult
* @param dt_boxes
* @param img
* @return
*/
public static DetectionResponse convertToDetectionResponse(NDList dt_boxes, Image img){
if(Objects.isNull(dt_boxes) || dt_boxes.size() == 0){
return null;
}
DetectionResponse detectionResponse = new DetectionResponse();
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
for(NDArray box : dt_boxes){
DetectionRectangle rectangle = new DetectionRectangle();
float[] points = box.toFloatArray();
log.info("points: {}", points);
int x = (int)points[0];
int y = (int)points[1];
int width = new BigDecimal(points[4]).subtract(new BigDecimal(points[6])).intValue();
int height = new BigDecimal(points[7]).subtract(new BigDecimal(points[1])).intValue();
// 修正边界,防止越界
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x + width > img.getWidth()) width = img.getWidth() - x;
if (y + height > img.getHeight()) height = img.getHeight() - y;
rectangle.setX(x);
rectangle.setY(y);
rectangle.setHeight(height);
rectangle.setWidth(width);
rectangleList.add(rectangle);
}
detectionResponse.setRectangleList(rectangleList);
return detectionResponse;
}
}