From 1914e2c316c3e9c89d10b13cd606bf26b82e94d6 Mon Sep 17 00:00:00 2001
From: dengwenjie <775747758@qq.com>
Date: Mon, 26 May 2025 16:24:24 +0800
Subject: [PATCH] =?UTF-8?q?-=20=E6=96=B0=E5=A2=9EOCR=E6=96=87=E5=AD=97?=
=?UTF-8?q?=E8=AF=86=E5=88=AB=E6=A8=A1=E5=9D=97=EF=BC=9A=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E6=9C=80=E6=96=B0=20PP-OCRv5=20-=20OCR=E6=96=87=E6=9C=AC?=
=?UTF-8?q?=E8=AF=86=E5=88=AB=EF=BC=9A=E6=94=AF=E6=8C=81=E6=96=87=E5=AD=97?=
=?UTF-8?q?=E6=96=B9=E5=90=91=E6=A3=80=E6=B5=8B=E4=B8=8E=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E6=A0=A1=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 40 ++-
examples/README.md | 8 +-
examples/pom.xml | 8 +-
.../examples/ocr/OcrDetectionDemo.java | 66 ++++
.../examples/ocr/OcrDirectionDetDemo.java | 74 ++++
.../examples/ocr/OcrRecognizeDemo.java | 132 +++++++
examples/src/main/resources/logback.xml | 2 +-
pom.xml | 14 +-
smartjavaai-all/pom.xml | 4 +-
smartjavaai-bom/pom.xml | 9 +-
smartjavaai-common/pom.xml | 2 +-
.../cn/smartjavaai/common/config/Config.java | 2 +
.../common/entity/DetectionInfo.java | 2 +
.../cn/smartjavaai/common/entity/Point.java | 6 +
.../smartjavaai/common/utils/ImageUtils.java | 186 ++++++++++
smartjavaai-face/pom.xml | 4 +-
.../face/config/FaceModelConfig.java | 2 +-
.../factory/FaceAttributeModelFactory.java | 2 +-
.../face/factory/FaceModelFactory.java | 6 +-
.../face/factory/LivenessModelFactory.java | 4 +-
.../face/model/facerec/AbstractFaceModel.java | 2 +-
.../face/model/facerec/FaceModel.java | 2 +-
.../face/model/facerec/SeetaFace6Model.java | 6 +-
smartjavaai-objectdetection/pom.xml | 4 +-
.../config/DetectorModelConfig.java | 2 +-
smartjavaai-ocr/pom.xml | 25 +-
.../ocr/config/DirectionModelConfig.java | 45 +++
.../OcrDetModelConfig.java | 13 +-
.../ocr/config/OcrRecModelConfig.java | 52 +++
.../ocr/detection/OcrDetModel.java | 37 --
.../ocr/detection/OcrDetModelFactory.java | 95 -----
.../smartjavaai/ocr/entity/DirectionInfo.java | 41 +++
.../cn/smartjavaai/ocr/entity/IdCardInfo.java | 12 +
.../cn/smartjavaai/ocr/entity/OcrBox.java | 52 +++
.../cn/smartjavaai/ocr/entity/OcrInfo.java | 27 ++
.../cn/smartjavaai/ocr/entity/OcrItem.java | 55 +++
.../ocr/entity/RotatedBoxCompX.java | 1 +
.../cn/smartjavaai/ocr/enums/AngleEnum.java | 38 ++
.../CommonDetModelEnum.java} | 12 +-
.../CommonRecModelEnum.java} | 10 +-
.../ocr/enums/DirectionModelEnum.java | 27 ++
.../ocr/exception/OcrException.java | 2 +-
.../ocr/factory/OcrModelFactory.java | 211 ++++++++++++
.../common/detect/OcrCommonDetModel.java | 80 +++++
.../model/common/detect/PpOCRV5DetModel.java | 201 +++++++++++
.../translator/PPOCRV5DetTranslator.java} | 6 +-
.../common/direction/OcrDirectionModel.java | 96 ++++++
.../common/direction/PPOCRMobileV2Model.java | 290 ++++++++++++++++
.../translator/PpWordRotateTranslator.java | 86 +++++
.../common/recognize/OcrCommonRecModel.java | 72 ++++
.../common/recognize/PpOCRV5RecModel.java | 324 ++++++++++++++++++
.../translator/PPOCRV5RecTranslator.java} | 6 +-
.../ocr/ppv4/model/PaddleOCRV4DetModel.java | 126 -------
.../ocr/ppv4/model/PaddleOCRV4RecModel.java | 129 -------
.../ocr/recognition/OcrRecModel.java | 37 --
.../ocr/recognition/OcrRecModelConfig.java | 28 --
.../ocr/recognition/OcrRecModelFactory.java | 95 -----
.../cn/smartjavaai/ocr/utils/ImageUtils.java | 221 ------------
.../cn/smartjavaai/ocr/utils/OcrUtils.java | 274 +++++++++++++--
59 files changed, 2523 insertions(+), 892 deletions(-)
create mode 100644 examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
create mode 100644 examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
create mode 100644 examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/DirectionModelConfig.java
rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{detection => config}/OcrDetModelConfig.java (52%)
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrRecModelConfig.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/DirectionInfo.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/IdCardInfo.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrBox.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrInfo.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrItem.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/AngleEnum.java
rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{detection/OcrDetModelEnum.java => enums/CommonDetModelEnum.java} (64%)
rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{recognition/OcrRecModelEnum.java => enums/CommonRecModelEnum.java} (67%)
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/DirectionModelEnum.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/OcrCommonDetModel.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java
rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{ppv4/translator/PaddleOCRV4DetectTranslator.java => model/common/detect/translator/PPOCRV5DetTranslator.java} (98%)
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/OcrDirectionModel.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/translator/PpWordRotateTranslator.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModel.java
create mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java
rename smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/{ppv4/translator/PaddleOCRV4WordRecTranslator.java => model/common/recognize/translator/PPOCRV5RecTranslator.java} (95%)
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java
delete mode 100644 smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
diff --git a/README.md b/README.md
index 6fab33c..9261205 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,22 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
-
+
+
+
+
+ OCR文字识别
+ - 支持任意角度文字识别
+ - 支持印刷体识别
+ - 支持手写字识别
+
+ |
+
+
+ 
+
+ |
+
@@ -174,11 +189,13 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
- **目标检测**
- 支持多种主流模型:兼容 YOLOv3、YOLOv5、YOLOv8、YOLOv11、YOLOv12、SSD 等目标检测算法
- 支持自定义模型加载:可无缝加载并部署用户自行训练的目标检测模型
+- **OCR文字识别**
+ - 支持PaddleOCR 3.0模型:集成最新PP-OCRv5模型
+ - 支持任意角度识别,方向校准
+ - 支持通用文字识别,通用手写字识别
### ⌛ 规划中功能
-- 文字识别(OCR)
-
- 图像分类(Image classification)
- 万物分割 (Segment Anything)
@@ -215,11 +232,12 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
## 🛠️包含组件
-| 模块 | 介绍 |
-|-----------------------------|---------------------------------------------------------------------------------- |
-| smartjavaai-common | 基础通用模块,封装了公共功能,供各算法模块共享使用 |
-| smartjavaai-face | 人脸功能模块 |
-| smartjavaai-objectdetection | 目标检测模块 |
+| 模块 | 介绍 |
+|-----------------------------|---------------------------|
+| smartjavaai-common | 基础通用模块,封装了公共功能,供各算法模块共享使用 |
+| smartjavaai-face | 人脸功能模块 |
+| smartjavaai-objectdetection | 目标检测模块 |
+| smartjavaai-ocr | OCR文字识别模块 |
可以根据需求对每个模块单独引入,也可以通过引入`smartjavaai-all`方式引入所有模块。
@@ -242,7 +260,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
cn.smartjavaai
smartjavaai-all
- 1.0.13
+ 1.0.14
```
### 3、完整示例代码
@@ -277,6 +295,10 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
## 近期更新日志
+## [v1.0.14] - 2025-05-17
+- 新增OCR文字识别模块:支持最新 PP-OCRv5
+- OCR文本识别:支持文字方向检测与自动校正
+
## [v1.0.13] - 2025-05-17
- 支持 JDK8 环境运行
- 引入离线依赖,支持完全离线使用
diff --git a/examples/README.md b/examples/README.md
index ab81666..43e1718 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -17,8 +17,12 @@ src/main/java/smartai/examples/
│ │ └── SeetaFace6Demo.java 示例:集成 SeetaFace6 的人脸识别
│ └── liveness/ 活体检测模块
│ ├── LivenessDetDemo.java 示例:基于图像进行活体检测
-└── objectdetection/ 目标检测模块
- └── ObjectDetection.java 示例:使用目标检测模型识别图像中的目标
+├── objectdetection/ 目标检测模块
+│ └── ObjectDetection.java 示例:使用目标检测模型识别图像中的目标
+└── ocr/ OCR文字识别模块
+ ├── OcrDetectionDemo.java 示例:OCR通用文字检测示例
+ ├── OcrDirectionDetDemo.java 示例:OCR方向检测示例
+ └── OcrRecognizeDemo.java 示例:OCR通用文字识别示例
```
## 快速开始
diff --git a/examples/pom.xml b/examples/pom.xml
index f9c4ab6..04d1f09 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -12,7 +12,7 @@
11
11
UTF-8
- 1.0.13
+ 1.0.14
smartai.examples.face.facerec.RetinaFaceDemo
@@ -103,6 +103,12 @@
smartjavaai-objectdetection
+
+
+ cn.smartjavaai
+ smartjavaai-ocr
+
+
ai.djl.pytorch
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
new file mode 100644
index 0000000..ed2d7ed
--- /dev/null
+++ b/examples/src/main/java/smartai/examples/ocr/OcrDetectionDemo.java
@@ -0,0 +1,66 @@
+package smartai.examples.ocr;
+
+import cn.smartjavaai.common.entity.DetectionResponse;
+import cn.smartjavaai.objectdetection.model.DetectorModel;
+import cn.smartjavaai.objectdetection.model.ObjectDetectionModelFactory;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
+import cn.smartjavaai.ocr.factory.OcrModelFactory;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * OCR 文本检测 示例
+ * @author dwj
+ * @date 2025/5/25
+ */
+@Slf4j
+public class OcrDetectionDemo {
+
+
+ /**
+ * 文本检测
+ * 检测图像中的文本区域,仅返回文本框位置,不识别文字内容
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void detect(){
+ OcrDetModelConfig config = new OcrDetModelConfig();
+ //指定检测模型
+ config.setModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ config.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ OcrCommonDetModel model = OcrModelFactory.getInstance().getDetModel(config);
+ List boxes = model.detect("src/main/resources/ocr_1.jpg");
+ log.info("OCR检测结果:{}", JSONObject.toJSONString(boxes));
+ }
+
+ /**
+ * 文本检测并绘制结果
+ * 检测图像中的文本区域,仅检测文本框位置,不识别文字内容
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void detectAndDraw(){
+ OcrDetModelConfig config = new OcrDetModelConfig();
+ //指定检测模型
+ config.setModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ config.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ OcrCommonDetModel model = OcrModelFactory.getInstance().getDetModel(config);
+ model.detectAndDraw("src/main/resources/ocr_1.jpg", "output/ocr_1_detected.jpg");
+ }
+
+
+
+
+}
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
new file mode 100644
index 0000000..7cd68bc
--- /dev/null
+++ b/examples/src/main/java/smartai/examples/ocr/OcrDirectionDetDemo.java
@@ -0,0 +1,74 @@
+package smartai.examples.ocr;
+
+import cn.smartjavaai.ocr.config.DirectionModelConfig;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.entity.OcrItem;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
+import cn.smartjavaai.ocr.enums.DirectionModelEnum;
+import cn.smartjavaai.ocr.factory.OcrModelFactory;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * OCR 文本方向检测 示例
+ * @author dwj
+ * @date 2025/5/25
+ */
+@Slf4j
+public class OcrDirectionDetDemo {
+
+
+ /**
+ * 文本方向检测
+ * 流程:文本检测 -> 方向分类
+ * 检测图像中文字的整体方向
+ * 支持返回四种可能的方向角度:0°, 90°, 180°, 270°
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void detect(){
+ DirectionModelConfig directionModelConfig = new DirectionModelConfig();
+ //指定检测模型
+ directionModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ directionModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定文本方向检测模型
+ directionModelConfig.setModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
+ //指定文本方向检测模型路径,需要更改为自己的模型路径(下载地址请查看文档)
+ directionModelConfig.setModelPath("/cls/ch_ppocr_mobile_v2.0_cls.onnx");
+ OcrDirectionModel directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
+ List itemList = directionModel.detect("src/main/resources/ocr_3.jpg");
+ log.info("OCR方向检测结果:{}", JSONObject.toJSONString(itemList));
+ }
+
+ /**
+ * 文本检测并绘制结果
+ * 流程:文本检测 -> 方向分类
+ * 检测图像中的文本区域,仅检测文本框位置,不识别文字内容
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void detectAndDraw(){
+ DirectionModelConfig directionModelConfig = new DirectionModelConfig();
+ //指定检测模型
+ directionModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ directionModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定文本方向检测模型
+ directionModelConfig.setModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
+ //指定文本方向检测模型路径,需要更改为自己的模型路径(下载地址请查看文档)
+ directionModelConfig.setModelPath("/cls/ch_ppocr_mobile_v2.0_cls.onnx");
+ OcrDirectionModel directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
+ directionModel.detectAndDraw("src/main/resources/ocr_3.jpg", "output/ocr_3_detected.png");
+ }
+
+
+
+
+}
diff --git a/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java b/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
new file mode 100644
index 0000000..3901083
--- /dev/null
+++ b/examples/src/main/java/smartai/examples/ocr/OcrRecognizeDemo.java
@@ -0,0 +1,132 @@
+package smartai.examples.ocr;
+
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.config.OcrRecModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.entity.OcrInfo;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
+import cn.smartjavaai.ocr.enums.CommonRecModelEnum;
+import cn.smartjavaai.ocr.enums.DirectionModelEnum;
+import cn.smartjavaai.ocr.factory.OcrModelFactory;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * OCR 文本识别 示例
+ * @author dwj
+ * @date 2025/5/25
+ */
+@Slf4j
+public class OcrRecognizeDemo {
+
+
+ /**
+ * 文本识别
+ * 本方法支持旋转角度范围为 -90 到 90 度的文字
+ * 同时兼容印刷体和手写体文字。
+ * 流程:文本检测 -> 文本识别
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void recognize(){
+ OcrRecModelConfig recModelConfig = new OcrRecModelConfig();
+ //指定检测模型
+ recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定识别模型
+ recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
+ //指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
+ OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
+ OcrInfo ocrInfo = recModel.recognize("src/main/resources/general_ocr_002.png");
+ log.info("OCR识别结果:{}", JSONObject.toJSONString(ocrInfo));
+ }
+
+
+ /**
+ * 文本识别(手写字)
+ * 本方法支持旋转角度范围为 -90 到 90 度的文字
+ * 同时兼容印刷体和手写体文字。
+ * 流程:文本检测 -> 文本识别
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void recognizeHandWriting(){
+ OcrRecModelConfig recModelConfig = new OcrRecModelConfig();
+ //指定检测模型
+ recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定识别模型
+ recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
+ //指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
+ OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
+ OcrInfo ocrInfo = recModel.recognize("src/main/resources/handwriting_1.jpg");
+ log.info("OCR识别结果:{}", JSONObject.toJSONString(ocrInfo));
+ }
+
+ /**
+ * 文本识别(带方向矫正)
+ * 本方法支持任意角度文字识别
+ * 同时兼容印刷体和手写体文字。
+ * 流程:文本检测 -> 方向检测 -> 方向矫正 -> 文本识别
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void recognize2(){
+ OcrRecModelConfig recModelConfig = new OcrRecModelConfig();
+ //指定检测模型
+ recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定识别模型
+ recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
+ //指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
+ //指定方向检测模型
+ recModelConfig.setDirectionModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
+ //指定方向模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setDirectionModelPath("/cls/ch_ppocr_mobile_v2.0_cls.onnx");
+ OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
+ OcrInfo ocrInfo = recModel.recognize("src/main/resources/ocr_4.jpg");
+ log.info("OCR识别结果:{}", JSONObject.toJSONString(ocrInfo));
+ }
+
+
+
+ /**
+ * 文本识别并绘制结果
+ * 本方法支持旋转角度范围为 -90 到 90 度的文字
+ * 同时兼容印刷体和手写体文字。
+ * 流程:文本检测 -> 文本识别
+ * 模型需要放在单独文件夹
+ */
+ @Test
+ public void recognizeAndDraw(){
+ OcrRecModelConfig recModelConfig = new OcrRecModelConfig();
+ //指定检测模型
+ recModelConfig.setDetModelEnum(CommonDetModelEnum.PADDLEOCR_V5_DET_MODEL);
+ //指定检测模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setDetModelPath("/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx");
+ //指定识别模型
+ recModelConfig.setRecModelEnum(CommonRecModelEnum.PADDLEOCR_V5_REC_MODEL);
+ //directionModelConfig.setDirectionModelEnum(DirectionModelEnum.CH_PPOCR_MOBILE_V2_CLS);
+ //指定识别模型位置,需要更改为自己的模型路径(下载地址请查看文档)
+ recModelConfig.setRecModelPath("/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx");
+ //directionModelConfig.setDirectionModelPath("/Users/wenjie/Documents/develop/ocr模型/ch_ppocr_mobile_v2.0_cls.onnx");
+ OcrCommonRecModel recModel = OcrModelFactory.getInstance().getRecModel(recModelConfig);
+ int fontSize = 20;
+ recModel.recognizeAndDraw("src/main/resources/general_ocr_002.png", "output/general_ocr_002_recognized.png", fontSize);
+ }
+
+
+
+
+}
diff --git a/examples/src/main/resources/logback.xml b/examples/src/main/resources/logback.xml
index 8f006d9..809ebab 100644
--- a/examples/src/main/resources/logback.xml
+++ b/examples/src/main/resources/logback.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/pom.xml b/pom.xml
index 16d9dd9..c5aeed1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
pom
SmartJavaAI
@@ -35,18 +35,6 @@
pom
import
-
-
- cn.smartjavaai
- smartjavaai-common
- 1.0.13
-
-
-
- cn.smartjavaai
- smartjavaai-face
- 1.0.13
-
diff --git a/smartjavaai-all/pom.xml b/smartjavaai-all/pom.xml
index 4f53fb8..da2f8c4 100644
--- a/smartjavaai-all/pom.xml
+++ b/smartjavaai-all/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
smartjavaai-all
- 1.0.13
+ 1.0.14
${project.artifactId}
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-bom/pom.xml b/smartjavaai-bom/pom.xml
index d83389d..62207ec 100644
--- a/smartjavaai-bom/pom.xml
+++ b/smartjavaai-bom/pom.xml
@@ -6,10 +6,10 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
- 1.0.13
+ 1.0.14
smartjavaai-bom
smartjavaai-bom
统一版本管理的 BOM 包,同时支持 import 和全量依赖
@@ -33,6 +33,11 @@
smartjavaai-objectdetection
${project.parent.version}
+
+ cn.smartjavaai
+ smartjavaai-ocr
+ ${project.parent.version}
+
diff --git a/smartjavaai-common/pom.xml b/smartjavaai-common/pom.xml
index 89f8293..858ec49 100644
--- a/smartjavaai-common/pom.xml
+++ b/smartjavaai-common/pom.xml
@@ -6,7 +6,7 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
smartjavaai-common
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/config/Config.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/config/Config.java
index 5a3cad0..bf537f1 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/config/Config.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/config/Config.java
@@ -29,6 +29,8 @@ public class Config {
if(StringUtils.isNotBlank(cachePath)){
System.setProperty("DJL_CACHE_DIR", cachePath);
}
+ System.setProperty("ai.djl.default_engine", "PyTorch");
+ log.info("设置默认引擎:{}", "PyTorch");
}
// 设置缓存路径的方法
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java
index 17e2d46..7fbfcf7 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/DetectionInfo.java
@@ -31,6 +31,8 @@ public class DetectionInfo {
*/
private ObjectDetInfo objectDetInfo;
+
+
public DetectionInfo() {
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java
index 00dfd7a..9027f20 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/entity/Point.java
@@ -38,4 +38,10 @@ public class Point implements Serializable {
public String toString() {
return JsonUtils.GSON_COMPACT.toJson(this);
}
+
+ public org.opencv.core.Point toCvPoint() {
+ return new org.opencv.core.Point(x, y);
+ }
+
+
}
diff --git a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
index 162bac5..3ac5485 100644
--- a/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
+++ b/smartjavaai-common/src/main/java/cn/smartjavaai/common/utils/ImageUtils.java
@@ -2,9 +2,12 @@ package cn.smartjavaai.common.utils;
import ai.djl.modality.cv.BufferedImageFactory;
import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.ndarray.NDArray;
import cn.smartjavaai.common.entity.DetectionRectangle;
import cn.smartjavaai.common.entity.DetectionResponse;
+import org.opencv.core.Mat;
import javax.imageio.ImageIO;
import java.awt.*;
@@ -14,6 +17,9 @@ import java.awt.image.ComponentSampleModel;
import java.awt.image.ImageObserver;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -146,5 +152,185 @@ public class ImageUtils {
}
+ /**
+ * 保存BufferedImage图片
+ *
+ * @param img
+ * @param name
+ * @param path
+ */
+ public static void saveImage(BufferedImage img, String name, String path) {
+ Mat mat = OpenCVUtils.image2Mat(img);
+ Image djlImg = ImageFactory.getInstance().fromImage(mat); // 支持多种图片格式,自动适配
+ Path outputDir = Paths.get(path);
+ Path imagePath = outputDir.resolve(name);
+ // OpenJDK 不能保存 jpg 图片的 alpha channel
+ try {
+ djlImg.save(Files.newOutputStream(imagePath), "png");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ mat.release();
+ }
+
+
+ /**
+ * 保存BufferedImage图片
+ *
+ * @param img
+ * @param path
+ */
+ public static void saveImage(BufferedImage img, String path) {
+ Mat mat = OpenCVUtils.image2Mat(img);
+ Image djlImg = ImageFactory.getInstance().fromImage(mat); // 支持多种图片格式,自动适配
+ Path outputDir = Paths.get(path);
+ // OpenJDK 不能保存 jpg 图片的 alpha channel
+ try {
+ djlImg.save(Files.newOutputStream(outputDir), "png");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ mat.release();
+ }
+
+
+
+
+
+
+
+ /**
+ * 保存DJL图片
+ *
+ * @param img
+ * @param name
+ * @param path
+ */
+ public static void saveImage(Image img, String name, String path) {
+ Path outputDir = Paths.get(path);
+ Path imagePath = outputDir.resolve(name);
+ // OpenJDK 不能保存 jpg 图片的 alpha channel
+ try {
+ img.save(Files.newOutputStream(imagePath), "png");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 保存图片,含检测框
+ *
+ * @param img
+ * @param detection
+ * @param name
+ * @param path
+ * @throws IOException
+ */
+ public static void saveBoundingBoxImage(
+ Image img, DetectedObjects detection, String name, String path) throws IOException {
+ // Make image copy with alpha channel because original image was jpg
+ img.drawBoundingBoxes(detection);
+ Path outputDir = Paths.get(path);
+ Files.createDirectories(outputDir);
+ Path imagePath = outputDir.resolve(name);
+ // OpenJDK can't save jpg with alpha channel
+ img.save(Files.newOutputStream(imagePath), "png");
+ }
+
+
+
+
+ /**
+ * 画检测框(有倾斜角)
+ *
+ * @param image
+ * @param box
+ */
+ public static void drawImageRect(BufferedImage image, NDArray box) {
+ float[] points = box.toFloatArray();
+ int[] xPoints = new int[5];
+ int[] yPoints = new int[5];
+
+ for (int i = 0; i < 4; i++) {
+ xPoints[i] = (int) points[2 * i];
+ yPoints[i] = (int) points[2 * i + 1];
+ }
+ xPoints[4] = xPoints[0];
+ yPoints[4] = yPoints[0];
+
+ // 将绘制图像转换为Graphics2D
+ Graphics2D g = (Graphics2D) image.getGraphics();
+ try {
+ g.setColor(new Color(0, 255, 0));
+ // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
+ BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
+ g.setStroke(bStroke);
+ g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
+ } finally {
+ g.dispose();
+ }
+ }
+
+ /**
+ * 画检测框(有倾斜角)和文本
+ *
+ * @param image
+ * @param box
+ * @param text
+ */
+ public static void drawImageRectWithText(BufferedImage image, NDArray box, String text) {
+ float[] points = box.toFloatArray();
+ int[] xPoints = new int[5];
+ int[] yPoints = new int[5];
+
+ for (int i = 0; i < 4; i++) {
+ xPoints[i] = (int) points[2 * i];
+ yPoints[i] = (int) points[2 * i + 1];
+ }
+ xPoints[4] = xPoints[0];
+ yPoints[4] = yPoints[0];
+
+ // 将绘制图像转换为Graphics2D
+ Graphics2D g = (Graphics2D) image.getGraphics();
+ try {
+ int fontSize = 32;
+ Font font = new Font("楷体", Font.PLAIN, fontSize);
+ g.setFont(font);
+ g.setColor(new Color(0, 0, 255));
+ // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
+ BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
+ g.setStroke(bStroke);
+ g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
+ g.drawString(text, xPoints[0], yPoints[0]);
+ } finally {
+ g.dispose();
+ }
+ }
+
+
+
+ /**
+ * 显示文字
+ *
+ * @param image
+ * @param text
+ * @param x
+ * @param y
+ */
+ public static void drawImageText(BufferedImage image, String text, int x, int y) {
+ Graphics graphics = image.getGraphics();
+ int fontSize = 32;
+ Font font = new Font("楷体", Font.PLAIN, fontSize);
+ try {
+ graphics.setFont(font);
+ graphics.setColor(new Color(0, 0, 255));
+ int strWidth = graphics.getFontMetrics().stringWidth(text);
+ graphics.drawString(text, x, y);
+ } finally {
+ graphics.dispose();
+ }
+ }
+
+
}
diff --git a/smartjavaai-face/pom.xml b/smartjavaai-face/pom.xml
index 31189d9..fba8b96 100644
--- a/smartjavaai-face/pom.xml
+++ b/smartjavaai-face/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
smartjavaai-face
- 1.0.13
+ 1.0.14
smartjavaai-face
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
index 74fd113..970e466 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/config/FaceModelConfig.java
@@ -13,7 +13,7 @@ import lombok.Data;
public class FaceModelConfig {
/**
- * 人脸算法名称
+ * 人脸模型枚举
*/
private FaceModelEnum modelEnum;
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
index 573558d..116b9d8 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceAttributeModelFactory.java
@@ -24,7 +24,7 @@ public class FaceAttributeModelFactory {
private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
/**
- * 算法注册表
+ * 模型注册表
*/
private static final Map> registry =
new ConcurrentHashMap<>();
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
index 176de1b..7b1b549 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/FaceModelFactory.java
@@ -25,7 +25,7 @@ public class FaceModelFactory {
private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
/**
- * 算法注册表
+ * 模型注册表
*/
private static final Map> registry =
new ConcurrentHashMap<>();
@@ -45,7 +45,7 @@ public class FaceModelFactory {
/**
- * 注册算法
+ * 注册模型
* @param name
* @param clazz
*/
@@ -82,7 +82,7 @@ public class FaceModelFactory {
}
/**
- * 使用ModelConfig创建算法
+ * 使用ModelConfig创建模型
* @param config
* @return
*/
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
index 972854f..d6447c3 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/factory/LivenessModelFactory.java
@@ -28,7 +28,7 @@ public class LivenessModelFactory {
private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
/**
- * 算法注册表
+ * 模型注册表
*/
private static final Map> registry =
new ConcurrentHashMap<>();
@@ -72,7 +72,7 @@ public class LivenessModelFactory {
}
/**
- * 使用ModelConfig创建算法
+ * 使用ModelConfig创建模型
* @param config
* @return
*/
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
index 07ea934..53b5d88 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/AbstractFaceModel.java
@@ -10,7 +10,7 @@ import java.io.InputStream;
import java.util.List;
/**
- * 人脸识别算法
+ * 人脸识别模型
* @author dwj
*/
public abstract class AbstractFaceModel implements FaceModel {
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
index bf07399..01eae36 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/FaceModel.java
@@ -10,7 +10,7 @@ import java.io.InputStream;
import java.util.List;
/**
- * 人脸识别算法
+ * 人脸识别模型
* @author dwj
*/
public interface FaceModel {
diff --git a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
index 0ae0013..c4274a9 100644
--- a/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
+++ b/smartjavaai-face/src/main/java/cn/smartjavaai/face/model/facerec/SeetaFace6Model.java
@@ -27,7 +27,7 @@ import java.util.List;
import java.util.Objects;
/**
- * SeetaFace6 人脸算法
+ * SeetaFace6 人脸模型
* @author dwj
*/
@SuppressWarnings("AliMissingOverrideAnnotation")
@@ -649,11 +649,7 @@ public class SeetaFace6Model extends AbstractFaceModel {
if(similarity[0] < config.getSimilarityThreshold()){
return null;
}
- long time2 = System.currentTimeMillis();
- System.out.println("总耗时1:" + (time2 - time1) + " ms");
FaceResult faceResult = searchFaceDb(index[0], similarity[0]);
- long time3 = System.currentTimeMillis();
- System.out.println("总耗时2:" + (time3 - time2) + " ms");
return faceResult;
} catch (FaceException e) {
throw e;
diff --git a/smartjavaai-objectdetection/pom.xml b/smartjavaai-objectdetection/pom.xml
index 2c60614..53cdcaf 100644
--- a/smartjavaai-objectdetection/pom.xml
+++ b/smartjavaai-objectdetection/pom.xml
@@ -6,11 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
smartjavaai-objectdetection
- 1.0.13
+ 1.0.14
smartjavaai-objectdetection
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java
index f1440d0..2a68a4f 100644
--- a/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java
+++ b/smartjavaai-objectdetection/src/main/java/cn/smartjavaai/objectdetection/config/DetectorModelConfig.java
@@ -15,7 +15,7 @@ import lombok.Data;
public class DetectorModelConfig {
/**
- * 模型名称
+ * 模型
*/
private DetectorModelEnum modelEnum;
diff --git a/smartjavaai-ocr/pom.xml b/smartjavaai-ocr/pom.xml
index 0d11804..9ac0d67 100644
--- a/smartjavaai-ocr/pom.xml
+++ b/smartjavaai-ocr/pom.xml
@@ -6,18 +6,11 @@
cn.smartjavaai
smartjavaai-parent
- 1.0.13
+ 1.0.14
smartjavaai-ocr
-
-
-
- UTF-8
- 1.5.8
- 5.1.2-1.5.8
-
@@ -25,23 +18,9 @@
smartjavaai-common
${project.version}
-
- org.bytedeco
- javacpp
- ${javacv.version}
-
-
- org.bytedeco
- ffmpeg
- ${javacv.ffmpeg.version}
-
-
- ai.djl.opencv
- opencv
-
- 1.0.13
+ 1.0.14
smartjavaai-ocr
SmartJavaAI
https://github.com/geekwenjie/SmartJavaAI
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/DirectionModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/DirectionModelConfig.java
new file mode 100644
index 0000000..4b90ba1
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/DirectionModelConfig.java
@@ -0,0 +1,45 @@
+package cn.smartjavaai.ocr.config;
+
+import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
+import cn.smartjavaai.ocr.enums.DirectionModelEnum;
+import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
+import lombok.Data;
+
+/**
+ * 文本方向分类模型配置
+ * @author dwj
+ * @date 2025/4/22
+ */
+@Data
+public class DirectionModelConfig {
+
+ /**
+ * 模型
+ */
+ private DirectionModelEnum modelEnum;
+
+ /**
+ * 设备类型
+ */
+ private DeviceEnum device;
+
+ /**
+ * 检测模型路径
+ */
+ private String modelPath;
+
+ /**
+ * 检测模型
+ */
+ private CommonDetModelEnum detModelEnum;
+
+ /**
+ * 检测模型路径
+ */
+ private String detModelPath;
+
+
+
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrDetModelConfig.java
similarity index 52%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrDetModelConfig.java
index 42889d2..f03bbf4 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelConfig.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrDetModelConfig.java
@@ -1,9 +1,11 @@
-package cn.smartjavaai.ocr.detection;
+package cn.smartjavaai.ocr.config;
import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
import lombok.Data;
/**
+ * OCR检测模型配置
* @author dwj
* @date 2025/4/22
*/
@@ -11,9 +13,9 @@ import lombok.Data;
public class OcrDetModelConfig {
/**
- * 模型名称
+ * 模型
*/
- private OcrDetModelEnum modelEnum;
+ private CommonDetModelEnum modelEnum;
/**
* 设备类型
@@ -21,8 +23,9 @@ public class OcrDetModelConfig {
private DeviceEnum device;
/**
- * 模型路径
+ * 检测模型路径
*/
- private String modelPath;
+ private String detModelPath;
+
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrRecModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrRecModelConfig.java
new file mode 100644
index 0000000..0aa9b35
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/config/OcrRecModelConfig.java
@@ -0,0 +1,52 @@
+package cn.smartjavaai.ocr.config;
+
+import cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
+import cn.smartjavaai.ocr.enums.CommonRecModelEnum;
+import cn.smartjavaai.ocr.enums.DirectionModelEnum;
+import lombok.Data;
+
+/**
+ * OCR识别模型配置
+ * @author dwj
+ * @date 2025/4/22
+ */
+@Data
+public class OcrRecModelConfig {
+
+ /**
+ * 检测模型
+ */
+ private CommonDetModelEnum detModelEnum;
+
+ /**
+ * 识别模型
+ */
+ private CommonRecModelEnum recModelEnum;
+
+ /**
+ * 设备类型
+ */
+ private DeviceEnum device;
+
+ /**
+ * 检测模型路径
+ */
+ private String detModelPath;
+
+ /**
+ * 识别模型路径
+ */
+ private String recModelPath;
+
+ /**
+ * 方向检测模型
+ */
+ private DirectionModelEnum directionModelEnum;
+
+ /**
+ * 方向检测模型路径
+ */
+ private String directionModelPath;
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
deleted file mode 100644
index 5b98d22..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModel.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package cn.smartjavaai.ocr.detection;
-
-import cn.smartjavaai.common.entity.DetectionResponse;
-
-/**
- * 人脸识别算法
- * @author dwj
- */
-public interface OcrDetModel {
-
- /**
- * 加载模型
- * @param config
- */
- void loadModel(OcrDetModelConfig config); // 加载模型
-
- /**
- * 人脸检测
- * @param imagePath 图片路径
- * @return
- */
- default DetectionResponse detect(String imagePath) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- /**
- * 检测并绘制结果
- * @param imagePath 图片输入路径(包含文件名称)
- * @param outputPath 图片输出路径(包含文件名称)
- */
- default void detectAndDraw(String imagePath, String outputPath) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
-
-
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
deleted file mode 100644
index a8413bd..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelFactory.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package cn.smartjavaai.ocr.detection;
-
-import cn.smartjavaai.common.config.Config;
-import cn.smartjavaai.ocr.exception.OcrException;
-import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * 人脸算法工厂
- * @author dwj
- */
-@Slf4j
-public class OcrDetModelFactory {
-
- // 使用 volatile 和双重检查锁定来确保线程安全的单例模式
- private static volatile OcrDetModelFactory instance;
-
- private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
-
- /**
- * 算法注册表
- */
- private static final Map> registry =
- new ConcurrentHashMap<>();
-
-
- public static OcrDetModelFactory getInstance() {
- if (instance == null) {
- synchronized (OcrDetModelFactory.class) {
- if (instance == null) {
- instance = new OcrDetModelFactory();
- }
- }
- }
- return instance;
- }
-
-
-
- /**
- * 注册算法
- * @param name
- * @param clazz
- */
- private static void registerModel(String name, Class extends OcrDetModel> clazz) {
- registry.put(name.toLowerCase(), clazz);
- }
-
-
- /**
- * 获取模型(通过配置)
- * @param config
- * @return
- */
- public OcrDetModel getModel(OcrDetModelConfig 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 OcrDetModel createFaceModel(OcrDetModelConfig config) {
- Class> clazz = registry.get(config.getModelEnum().name().toLowerCase());
- if(clazz == null){
- throw new OcrException("Unsupported model");
- }
- OcrDetModel algorithm = null;
- try {
- algorithm = (OcrDetModel) clazz.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new OcrException(e);
- }
- algorithm.loadModel(config);
- return algorithm;
- }
-
-
- // 初始化默认算法
- static {
- registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetModel.class);
- log.info("缓存目录:{}", Config.getCachePath());
- }
-
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/DirectionInfo.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/DirectionInfo.java
new file mode 100644
index 0000000..6fd58de
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/DirectionInfo.java
@@ -0,0 +1,41 @@
+package cn.smartjavaai.ocr.entity;
+
+/**
+ * 方向检测结果
+ * @author Calvin
+ * @mail 179209347@qq.com
+ * @website www.aias.top
+ */
+public class DirectionInfo {
+
+ /**
+ * 方向 0 90 180 270
+ */
+ private String name;
+
+ /**
+ * 置信度
+ */
+ private Double prob;
+
+ public DirectionInfo(String name, Double prob) {
+ this.name = name;
+ this.prob = prob;
+ }
+
+ 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;
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/IdCardInfo.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/IdCardInfo.java
new file mode 100644
index 0000000..608d006
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/IdCardInfo.java
@@ -0,0 +1,12 @@
+package cn.smartjavaai.ocr.entity;
+
+/**
+ * 身份证信息
+ * @author dwj
+ * @date 2025/5/22
+ */
+public class IdCardInfo {
+
+
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrBox.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrBox.java
new file mode 100644
index 0000000..fc2b6df
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrBox.java
@@ -0,0 +1,52 @@
+package cn.smartjavaai.ocr.entity;
+
+import cn.smartjavaai.common.entity.Point;
+import lombok.Data;
+
+/**
+ * OCR 检测框
+ * @author dwj
+ * @date 2025/5/20
+ */
+@Data
+public class OcrBox {
+
+ /**
+ * 左上角
+ */
+ private Point topLeft;
+
+ /**
+ * 右上角
+ */
+ private Point topRight;
+
+ /**
+ * 右下角
+ */
+ private Point bottomRight;
+
+ /**
+ * 左下角
+ */
+ private Point bottomLeft;
+
+ public OcrBox(Point topLeft, Point topRight, Point bottomRight, Point bottomLeft) {
+ this.topLeft = topLeft;
+ this.topRight = topRight;
+ this.bottomRight = bottomRight;
+ this.bottomLeft = bottomLeft;
+ }
+
+ public OcrBox() {
+ }
+
+ public float[] toFloatArray() {
+ return new float[]{
+ (float)topLeft.getX(), (float)topLeft.getY(),
+ (float)topRight.getX(), (float)topRight.getY(),
+ (float)bottomRight.getX(), (float)bottomRight.getY(),
+ (float)bottomLeft.getX(), (float)bottomLeft.getY()
+ };
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrInfo.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrInfo.java
new file mode 100644
index 0000000..f287033
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrInfo.java
@@ -0,0 +1,27 @@
+package cn.smartjavaai.ocr.entity;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * OCR信息
+ * @author dwj
+ * @date 2025/5/20
+ */
+@Data
+public class OcrInfo {
+
+ private List> lineList;
+
+ private String fullText;
+
+
+ public OcrInfo(List> lineList, String fullText) {
+ this.lineList = lineList;
+ this.fullText = fullText;
+ }
+ public OcrInfo() {
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrItem.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrItem.java
new file mode 100644
index 0000000..9c74752
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/OcrItem.java
@@ -0,0 +1,55 @@
+package cn.smartjavaai.ocr.entity;
+
+import cn.smartjavaai.ocr.enums.AngleEnum;
+import lombok.Data;
+
+/**
+ * @author dwj
+ * @date 2025/5/20
+ */
+@Data
+public class OcrItem {
+
+ /**
+ * 识别框
+ */
+ private OcrBox ocrBox;
+
+ /**
+ * 文本
+ */
+ private String text;
+
+ /**
+ * 方向
+ */
+ private AngleEnum angle;
+
+ /**
+ * 检测得分
+ */
+ private float score;
+
+
+
+ public OcrItem(OcrBox ocrBox, String text) {
+ this.ocrBox = ocrBox;
+ this.text = text;
+ }
+
+ public OcrItem() {
+ }
+
+ public OcrItem(OcrBox ocrBox, String text, AngleEnum angle) {
+ this.ocrBox = ocrBox;
+ this.text = text;
+ this.angle = angle;
+ }
+
+ public OcrItem(OcrBox ocrBox, AngleEnum angle, float score) {
+ this.ocrBox = ocrBox;
+ this.angle = angle;
+ this.score = score;
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/RotatedBoxCompX.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/RotatedBoxCompX.java
index 763f9cb..00ab52f 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/RotatedBoxCompX.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/entity/RotatedBoxCompX.java
@@ -1,6 +1,7 @@
package cn.smartjavaai.ocr.entity;
import ai.djl.ndarray.NDArray;
+
/**
* 旋转检测框 - 支持左上角 X 坐标升序排序
*/
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/AngleEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/AngleEnum.java
new file mode 100644
index 0000000..47b3ff2
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/AngleEnum.java
@@ -0,0 +1,38 @@
+package cn.smartjavaai.ocr.enums;
+
+/**
+ * 文本方向
+ * @author dwj
+ * @date 2025/5/23
+ */
+public enum AngleEnum {
+
+ ANGLE_0("0"),
+ ANGLE_90("90"),
+ ANGLE_180("180"),
+ ANGLE_270("270");
+
+ private final String value;
+
+ AngleEnum(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static AngleEnum fromValue(String value) {
+ for (AngleEnum angle : values()) {
+ if (angle.value.equals(value)) {
+ return angle;
+ }
+ }
+ throw new IllegalArgumentException("Invalid angle value: " + value);
+ }
+
+ @Override
+ public String toString() {
+ return value + "°";
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonDetModelEnum.java
similarity index 64%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonDetModelEnum.java
index ee35871..d3aaae3 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/detection/OcrDetModelEnum.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonDetModelEnum.java
@@ -1,21 +1,21 @@
-package cn.smartjavaai.ocr.detection;
+package cn.smartjavaai.ocr.enums;
/**
- * OCR模型枚举
+ * OCR检测模型枚举
* @author dwj
* @date 2025/4/4
*/
-public enum OcrDetModelEnum {
+public enum CommonDetModelEnum {
- PADDLEOCR_V4_DET_MODEL;
+ PADDLEOCR_V5_DET_MODEL;
/**
* 根据名称获取枚举 (忽略大小写和下划线变体)
*/
- public static OcrDetModelEnum fromName(String name) {
+ public static CommonDetModelEnum fromName(String name) {
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
- for (OcrDetModelEnum model : values()) {
+ for (CommonDetModelEnum model : values()) {
if (model.name().replaceAll("_", "").equals(formatted)) {
return model;
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonRecModelEnum.java
similarity index 67%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonRecModelEnum.java
index 007c8f6..2348dcf 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelEnum.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/CommonRecModelEnum.java
@@ -1,21 +1,21 @@
-package cn.smartjavaai.ocr.recognition;
+package cn.smartjavaai.ocr.enums;
/**
* OCR识别模型枚举
* @author dwj
* @date 2025/4/4
*/
-public enum OcrRecModelEnum {
+public enum CommonRecModelEnum {
- PADDLEOCR_V4_REC_MODEL;
+ PADDLEOCR_V5_REC_MODEL;
/**
* 根据名称获取枚举 (忽略大小写和下划线变体)
*/
- public static OcrRecModelEnum fromName(String name) {
+ public static CommonRecModelEnum fromName(String name) {
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
- for (OcrRecModelEnum model : values()) {
+ for (CommonRecModelEnum model : values()) {
if (model.name().replaceAll("_", "").equals(formatted)) {
return model;
}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/DirectionModelEnum.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/DirectionModelEnum.java
new file mode 100644
index 0000000..a7f50c5
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/enums/DirectionModelEnum.java
@@ -0,0 +1,27 @@
+package cn.smartjavaai.ocr.enums;
+
+/**
+ * OCR文本方向分类模型枚举
+ * @author dwj
+ * @date 2025/4/4
+ */
+public enum DirectionModelEnum {
+
+ CH_PPOCR_MOBILE_V2_CLS;
+
+
+ /**
+ * 根据名称获取枚举 (忽略大小写和下划线变体)
+ */
+ public static DirectionModelEnum fromName(String name) {
+ String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
+ for (DirectionModelEnum model : values()) {
+ if (model.name().replaceAll("_", "").equals(formatted)) {
+ return model;
+ }
+ }
+ throw new IllegalArgumentException("未知模型名称: " + name);
+ }
+
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/exception/OcrException.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/exception/OcrException.java
index c0f6b1c..0253265 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/exception/OcrException.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/exception/OcrException.java
@@ -1,7 +1,7 @@
package cn.smartjavaai.ocr.exception;
/**
- * 人脸检测异常
+ * OCR异常
* @author dwj
* @date 2025/4/4
*/
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java
new file mode 100644
index 0000000..223f0d7
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/factory/OcrModelFactory.java
@@ -0,0 +1,211 @@
+package cn.smartjavaai.ocr.factory;
+
+import cn.smartjavaai.common.config.Config;
+import cn.smartjavaai.ocr.config.DirectionModelConfig;
+import cn.smartjavaai.ocr.config.OcrRecModelConfig;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import cn.smartjavaai.ocr.model.common.detect.PpOCRV5DetModel;
+import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.exception.OcrException;
+import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
+import cn.smartjavaai.ocr.model.common.direction.PPOCRMobileV2Model;
+import cn.smartjavaai.ocr.model.common.recognize.PpOCRV5RecModel;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * OCR模型工厂
+ * @author dwj
+ */
+@Slf4j
+public class OcrModelFactory {
+
+ // 使用 volatile 和双重检查锁定来确保线程安全的单例模式
+ private static volatile OcrModelFactory instance;
+
+ private static final ConcurrentHashMap commonDetModelMap = new ConcurrentHashMap<>();
+
+
+ private static final ConcurrentHashMap commonRecModelMap = new ConcurrentHashMap<>();
+
+ private static final ConcurrentHashMap directionModelMap = new ConcurrentHashMap<>();
+
+ /**
+ * 检测模型注册表
+ */
+ private static final Map> commonDetRegistry =
+ new ConcurrentHashMap<>();
+
+ /**
+ * 识别模型注册表
+ */
+ private static final Map> commonRecRegistry =
+ new ConcurrentHashMap<>();
+
+ /**
+ * 方向分类模型注册表
+ */
+ private static final Map> directionRegistry =
+ 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 registerCommonDetModel(String name, Class extends OcrCommonDetModel> clazz) {
+ commonDetRegistry.put(name.toLowerCase(), clazz);
+ }
+
+ /**
+ * 注册通用识别模型
+ * @param name
+ * @param clazz
+ */
+ private static void registerCommonRecModel(String name, Class extends OcrCommonRecModel> clazz) {
+ commonRecRegistry.put(name.toLowerCase(), clazz);
+ }
+
+ /**
+ * 注册通用方向分类模型
+ * @param name
+ * @param clazz
+ */
+ private static void registerDirectionModel(String name, Class extends OcrDirectionModel> clazz) {
+ directionRegistry.put(name.toLowerCase(), clazz);
+ }
+
+
+ /**
+ * 获取检测模型(通过配置)
+ * @param config
+ * @return
+ */
+ public OcrCommonDetModel getDetModel(OcrDetModelConfig config) {
+ if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
+ throw new OcrException("未配置OCR模型");
+ }
+ return commonDetModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
+ return createCommonDetModel(config);
+ });
+ }
+
+ /**
+ * 获取识别模型(通过配置)
+ * @param config
+ * @return
+ */
+ public OcrCommonRecModel getRecModel(OcrRecModelConfig config) {
+ if(Objects.isNull(config) || Objects.isNull(config.getRecModelEnum())){
+ throw new OcrException("未配置OCR模型");
+ }
+ return commonRecModelMap.computeIfAbsent(config.getRecModelEnum().name(), k -> {
+ return createCommonRecModel(config);
+ });
+ }
+
+ /**
+ * 获取模型(通过配置)
+ * @param config
+ * @return
+ */
+ public OcrDirectionModel getDirectionModel(DirectionModelConfig config) {
+ if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
+ throw new OcrException("未配置OCR模型");
+ }
+ return directionModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
+ return createDirectionModel(config);
+ });
+ }
+
+
+
+ /**
+ * 创建OCR通用检测模型
+ * @param config
+ * @return
+ */
+ private OcrCommonDetModel createCommonDetModel(OcrDetModelConfig config) {
+ Class> clazz = commonDetRegistry.get(config.getModelEnum().name().toLowerCase());
+ if(clazz == null){
+ throw new OcrException("Unsupported model");
+ }
+ OcrCommonDetModel model = null;
+ try {
+ model = (OcrCommonDetModel) clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new OcrException(e);
+ }
+ model.loadModel(config);
+ return model;
+ }
+
+
+ /**
+ * 创建OCR通用识别模型
+ * @param config
+ * @return
+ */
+ private OcrCommonRecModel createCommonRecModel(OcrRecModelConfig config) {
+ Class> clazz = commonRecRegistry.get(config.getRecModelEnum().name().toLowerCase());
+ if(clazz == null){
+ throw new OcrException("Unsupported model");
+ }
+ OcrCommonRecModel model = null;
+ try {
+ model = (OcrCommonRecModel) clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new OcrException(e);
+ }
+ model.loadModel(config);
+ return model;
+ }
+
+ /**
+ * 创建OCR方向分类模型
+ * @param config
+ * @return
+ */
+ private OcrDirectionModel createDirectionModel(DirectionModelConfig config) {
+ Class> clazz = directionRegistry.get(config.getModelEnum().name().toLowerCase());
+ if(clazz == null){
+ throw new OcrException("Unsupported model");
+ }
+ OcrDirectionModel model = null;
+ try {
+ model = (OcrDirectionModel) clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new OcrException(e);
+ }
+ model.loadModel(config);
+ return model;
+ }
+
+
+ // 初始化默认算法
+ static {
+ registerCommonDetModel("PADDLEOCR_V5_DET_MODEL", PpOCRV5DetModel.class);
+ registerCommonRecModel("PADDLEOCR_V5_REC_MODEL", PpOCRV5RecModel.class);
+ registerDirectionModel("CH_PPOCR_MOBILE_V2_CLS", PPOCRMobileV2Model.class);
+ log.info("缓存目录:{}", Config.getCachePath());
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/OcrCommonDetModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/OcrCommonDetModel.java
new file mode 100644
index 0000000..124ce3d
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/OcrCommonDetModel.java
@@ -0,0 +1,80 @@
+package cn.smartjavaai.ocr.model.common.detect;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+
+import java.awt.image.BufferedImage;
+import java.util.List;
+
+/**
+ * OCR 通用检测模型
+ * @author dwj
+ */
+public interface OcrCommonDetModel {
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ void loadModel(OcrDetModelConfig config); // 加载模型
+
+ /**
+ * 文本检测
+ * @param imagePath 图片路径
+ * @return
+ */
+ default List detect(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+
+
+ /**
+ * 文本检测
+ * @param image BufferedImage
+ * @return
+ */
+ default List detect(BufferedImage image) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本检测
+ * @param imageData 图片字节数组
+ * @return
+ */
+ default List detect(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 文本检测
+ * @param image DJL Image
+ * @return
+ */
+ default List detect(Image image){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 检测并绘制结果
+ * @param imagePath 图片输入路径(包含文件名称)
+ * @param outputPath 图片输出路径(包含文件名称)
+ */
+ default void detectAndDraw(String imagePath, String outputPath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 检测并绘制结果
+ * @param sourceImage
+ * @return
+ */
+ default BufferedImage detectAndDraw(BufferedImage sourceImage){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java
new file mode 100644
index 0000000..cd3592d
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/PpOCRV5DetModel.java
@@ -0,0 +1,201 @@
+package cn.smartjavaai.ocr.model.common.detect;
+
+import ai.djl.Device;
+import ai.djl.MalformedModelException;
+import ai.djl.engine.Engine;
+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 cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.common.pool.PredictorFactory;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.common.utils.OpenCVUtils;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.exception.OcrException;
+import cn.smartjavaai.ocr.model.common.detect.translator.PPOCRV5DetTranslator;
+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 javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * PPOCRV5 检测模型
+ * @author dwj
+ * @date 2025/4/21
+ */
+@Slf4j
+public class PpOCRV5DetModel implements OcrCommonDetModel {
+
+
+ private ObjectPool> detPredictorPool;
+
+ private OcrDetModelConfig config;
+
+ @Override
+ public void loadModel(OcrDetModelConfig config){
+ if(StringUtils.isBlank(config.getDetModelPath())){
+ throw new OcrException("modelPath is null");
+ }
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ this.config = config;
+ //初始化 检测Criteria
+ Criteria detCriteria =
+ Criteria.builder()
+ .optEngine("OnnxRuntime")
+ .setTypes(Image.class, NDList.class)
+ .optModelPath(Paths.get(config.getDetModelPath()))
+ .optTranslator(new PPOCRV5DetTranslator(new ConcurrentHashMap()))
+ .optDevice(device)
+ .optProgress(new ProgressBar())
+ .build();
+
+ try{
+ ZooModel detectionModel = ModelZoo.loadModel(detCriteria);
+ // 创建池子:每个线程独享 Predictor
+ this.detPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
+ log.info("当前设备: " + detectionModel.getNDManager().getDevice());
+ log.info("当前引擎: " + Engine.getInstance().getEngineName());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new OcrException("检测模型加载失败", e);
+ }
+ }
+
+ @Override
+ public List 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);
+ }
+ List ocrBoxList = detect(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrBoxList;
+ }
+
+ @Override
+ public List detect(Image image){
+ Predictor predictor = null;
+ try (NDManager manager = NDManager.newBaseManager()) {
+ predictor = detPredictorPool.borrowObject();
+ NDList result = predictor.predict(image);
+ result.attach(manager);
+ return OcrUtils.convertToOcrBox(result, image);
+ } catch (Exception e) {
+ throw new OcrException("OCR检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ detPredictorPool.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 (NDManager manager = NDManager.newBaseManager()) {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ List boxList = detect(img);
+ if(Objects.isNull(boxList) || boxList.isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ OcrUtils.drawRect((Mat)img.getWrappedImage(), boxList);
+ Path output = Paths.get(outputPath);
+ log.info("Saving to {}", output.toAbsolutePath().toString());
+ img.save(Files.newOutputStream(output), "png");
+ ((Mat) img.getWrappedImage()).release();
+ } catch (IOException e) {
+ throw new OcrException(e);
+ }
+ }
+
+
+ @Override
+ public List detect(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
+ List ocrBoxList = detect(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrBoxList;
+ }
+
+ @Override
+ public List detect(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new OcrException("图像无效");
+ }
+ try {
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
+ return detect(image);
+ } catch (IOException e) {
+ throw new OcrException("错误的图像", e);
+ }
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
+ List ocrBoxList = detect(img);
+ if(Objects.isNull(ocrBoxList) || ocrBoxList.isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ OcrUtils.drawRect((Mat)img.getWrappedImage(), ocrBoxList);
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ ((Mat) img.getWrappedImage()).release();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new OcrException("导出图片失败", e);
+ }
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/translator/PPOCRV5DetTranslator.java
similarity index 98%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/translator/PPOCRV5DetTranslator.java
index 4396d1a..9725561 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4DetectTranslator.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/detect/translator/PPOCRV5DetTranslator.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.ocr.ppv4.translator;
+package cn.smartjavaai.ocr.model.common.detect.translator;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.util.NDImageUtils;
@@ -27,7 +27,7 @@ import java.util.Map;
* @mail 179209347@qq.com
* @website www.aias.top
*/
-public class PaddleOCRV4DetectTranslator implements Translator {
+public class PPOCRV5DetTranslator implements Translator {
// det_algorithm == "DB"
private final float thresh = 0.3f;
private final boolean use_dilation = false;
@@ -44,7 +44,7 @@ public class PaddleOCRV4DetectTranslator implements Translator {
private int img_height;
private int img_width;
- public PaddleOCRV4DetectTranslator(Map arguments) {
+ public PPOCRV5DetTranslator(Map arguments) {
limit_side_len =
arguments.containsKey("limit_side_len")
? Integer.parseInt(arguments.get("limit_side_len").toString())
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/OcrDirectionModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/OcrDirectionModel.java
new file mode 100644
index 0000000..a1d2620
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/OcrDirectionModel.java
@@ -0,0 +1,96 @@
+package cn.smartjavaai.ocr.model.common.direction;
+
+import ai.djl.inference.Predictor;
+import ai.djl.modality.cv.Image;
+import ai.djl.ndarray.NDManager;
+import cn.smartjavaai.ocr.config.DirectionModelConfig;
+import cn.smartjavaai.ocr.entity.DirectionInfo;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.entity.OcrInfo;
+import cn.smartjavaai.ocr.entity.OcrItem;
+import org.opencv.core.Mat;
+
+import java.awt.image.BufferedImage;
+import java.util.List;
+
+/**
+ * OCR 文本方向分类模型
+ * @author dwj
+ */
+public interface OcrDirectionModel {
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ void loadModel(DirectionModelConfig config); // 加载模型
+
+ /**
+ * 文本方向检测
+ * @param imagePath 图片路径
+ * @return
+ */
+ default List detect(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本方向检测
+ * @param image BufferedImage
+ * @return
+ */
+ default List detect(BufferedImage image) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本方向检测
+ * @param imageData 图片字节数组
+ * @return
+ */
+ default List detect(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 文本方向检测
+ * @param image
+ * @return
+ */
+ default List detect(Image image) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本方向检测(基于检测结果)
+ * @param boxList
+ * @param srcMat
+ * @param manager
+ * @return
+ */
+ default List detect(List boxList, Mat srcMat, NDManager manager) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 检测并绘制结果
+ * @param imagePath 图片输入路径(包含文件名称)
+ * @param outputPath 图片输出路径(包含文件名称)
+ */
+ default void detectAndDraw(String imagePath, String outputPath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 检测并绘制结果
+ * @param sourceImage
+ * @return
+ */
+ default BufferedImage detectAndDraw(BufferedImage sourceImage){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java
new file mode 100644
index 0000000..05ace1c
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/PPOCRMobileV2Model.java
@@ -0,0 +1,290 @@
+package cn.smartjavaai.ocr.model.common.direction;
+
+import ai.djl.Device;
+import ai.djl.MalformedModelException;
+import ai.djl.engine.Engine;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.output.Point;
+import ai.djl.ndarray.NDManager;
+import ai.djl.opencv.OpenCVImageFactory;
+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 cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.common.pool.PredictorFactory;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.common.utils.OpenCVUtils;
+import cn.smartjavaai.ocr.config.DirectionModelConfig;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.entity.*;
+import cn.smartjavaai.ocr.enums.AngleEnum;
+import cn.smartjavaai.ocr.exception.OcrException;
+import cn.smartjavaai.ocr.factory.OcrModelFactory;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import cn.smartjavaai.ocr.model.common.direction.translator.PpWordRotateTranslator;
+import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
+import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
+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 javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+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;
+import java.util.Objects;
+
+/**
+ * PPOCRMobileV2Model 方向分类模型
+ * @author dwj
+ * @date 2025/4/21
+ */
+@Slf4j
+public class PPOCRMobileV2Model implements OcrDirectionModel {
+
+
+ private ObjectPool> predictorPool;
+
+ private DirectionModelConfig config;
+
+ private OcrCommonDetModel detModel;
+
+
+ @Override
+ public void loadModel(DirectionModelConfig config){
+ if(StringUtils.isBlank(config.getModelPath())){
+ throw new OcrException("modelPath is null");
+ }
+
+ this.config = config;
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ Criteria criteria =
+ Criteria.builder()
+ .optEngine("OnnxRuntime")
+ .setTypes(Image.class, DirectionInfo.class)
+ .optModelPath(Paths.get(config.getModelPath()))
+ .optDevice(device)
+ .optTranslator(new PpWordRotateTranslator())
+ .optProgress(new ProgressBar())
+ .build();
+ try{
+ ZooModel model = ModelZoo.loadModel(criteria);
+ // 创建池子:每个线程独享 Predictor
+ this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
+ log.info("当前设备: " + model.getNDManager().getDevice());
+ log.info("当前引擎: " + Engine.getInstance().getEngineName());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new OcrException("模型加载失败", e);
+ }
+
+ //获取检测模型
+ if(StringUtils.isNotBlank(config.getDetModelPath()) && Objects.nonNull(config.getDetModelEnum())){
+ OcrDetModelConfig detModelConfig = new OcrDetModelConfig();
+ detModelConfig.setModelEnum(config.getDetModelEnum());
+ detModelConfig.setDetModelPath(config.getDetModelPath());
+ detModel = OcrModelFactory.getInstance().getDetModel(detModelConfig);
+ }
+ }
+
+ @Override
+ public List 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);
+ }
+ List ocrItemList = detect(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrItemList;
+ }
+
+
+ @Override
+ public List detect(Image image){
+ //检测文本
+ List boxeList = detModel.detect(image);
+ if(Objects.isNull(boxeList) || boxeList.isEmpty()){
+ throw new OcrException("未检测到文本");
+ }
+ Predictor predictor = null;
+ List ocrItemList = new ArrayList<>();
+ try (NDManager manager = NDManager.newBaseManager()) {
+ Mat srcMat = (Mat) image.getWrappedImage();
+ predictor = predictorPool.borrowObject();
+ for (OcrBox box : boxeList){
+ OcrItem ocrItem = detect(box, srcMat, predictor, manager);
+ ocrItemList.add(ocrItem);
+ }
+ return ocrItemList;
+ } 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);
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 基于文本框检测方向
+ * @param box
+ * @param srcMat
+ * @param predictor
+ * @param manager
+ * @return
+ */
+ private OcrItem detect(OcrBox box, Mat srcMat, Predictor predictor, NDManager manager){
+ if(Objects.isNull(box)){
+ throw new OcrException("box参数为空");
+ }
+ try {
+ //透视变换及裁剪
+ Image subImg = OcrUtils.transformAndCrop(srcMat, box);
+ DirectionInfo directionInfo = null;
+ String angle;
+ //高宽比 > 1.5 纵向
+ if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) {
+ //旋转图片90度
+ subImg = OcrUtils.rotateImg(manager, subImg);
+ //ImageUtils.saveImage(subImg, i + "rotate.png", "build/output");
+ //检测方向
+ directionInfo = predictor.predict(subImg);
+ if (directionInfo.getName().equalsIgnoreCase("Rotate")) {
+ angle = "90";
+ } else {
+ angle = "270";
+ }
+ }else{ //横向
+ directionInfo = predictor.predict(subImg);
+ if (directionInfo.getName().equalsIgnoreCase("No Rotate")) {
+ angle = "0";
+ } else {
+ angle = "180";
+ }
+ }
+ ((Mat)subImg.getWrappedImage()).release();
+ return new OcrItem(box, AngleEnum.fromValue(angle), directionInfo.getProb().floatValue());
+ } catch (Exception e) {
+ throw new OcrException("OCR检测错误", e);
+ }
+ }
+
+ @Override
+ public List detect(List boxList,Mat srcMat,NDManager manager){
+ if(Objects.isNull(boxList) || boxList.isEmpty()){
+ throw new OcrException("boxList为空");
+ }
+ Predictor predictor = null;
+ List ocrItemList = new ArrayList<>();
+ try {
+ predictor = predictorPool.borrowObject();
+ for (OcrBox box : boxList){
+ OcrItem ocrItem = detect(box, srcMat, predictor, manager);
+ ocrItemList.add(ocrItem);
+ }
+ } catch (Exception e) {
+ throw new OcrException("OCR检测错误", e);
+ }
+ return ocrItemList;
+ }
+
+ @Override
+ public void detectAndDraw(String imagePath, String outputPath) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new OcrException("图像文件不存在");
+ }
+ try (NDManager manager = NDManager.newBaseManager()) {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ List itemList = detect(img);
+ if(Objects.isNull(itemList) || itemList.isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), itemList);
+ Path output = Paths.get(outputPath);
+ log.info("Saving to {}", output.toAbsolutePath().toString());
+ img.save(Files.newOutputStream(output), "png");
+ ((Mat) img.getWrappedImage()).release();
+ } catch (IOException e) {
+ throw new OcrException(e);
+ }
+ }
+
+ @Override
+ public List detect(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
+ List ocrItemList = detect(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrItemList;
+ }
+
+ @Override
+ public List detect(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new OcrException("图像无效");
+ }
+ try {
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
+ return detect(image);
+ } catch (IOException e) {
+ throw new OcrException("错误的图像", e);
+ }
+ }
+
+ @Override
+ public BufferedImage detectAndDraw(BufferedImage sourceImage) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
+ List ocrItemList = detect(img);
+ if(Objects.isNull(ocrItemList) || ocrItemList.isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), ocrItemList);
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ ((Mat) img.getWrappedImage()).release();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new OcrException("导出图片失败", e);
+ }
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/translator/PpWordRotateTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/translator/PpWordRotateTranslator.java
new file mode 100644
index 0000000..c0e4e3a
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/direction/translator/PpWordRotateTranslator.java
@@ -0,0 +1,86 @@
+package cn.smartjavaai.ocr.model.common.direction.translator;
+
+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.Shape;
+import ai.djl.translate.Batchifier;
+import ai.djl.translate.Translator;
+import ai.djl.translate.TranslatorContext;
+import cn.smartjavaai.ocr.entity.DirectionInfo;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 方向检测
+ *
+ * @author Calvin
+ * @mail 179209347@qq.com
+ * @website www.aias.top
+ */
+public class PpWordRotateTranslator implements Translator {
+ List classes = Arrays.asList("No Rotate", "Rotate");
+
+ public PpWordRotateTranslator() {
+ }
+
+ @Override
+ public DirectionInfo processOutput(TranslatorContext ctx, NDList list) {
+ NDArray prob = list.singletonOrThrow();
+ float[] res = prob.toFloatArray();
+ int maxIndex = 0;
+ if (res[1] > res[0]) {
+ maxIndex = 1;
+ }
+
+ return new DirectionInfo(classes.get(maxIndex), Double.valueOf(res[maxIndex]));
+ }
+
+// public NDList processInput2(TranslatorContext ctx, Image input){
+// NDArray img = input.toNDArray(ctx.getNDManager());
+// img = NDImageUtils.resize(img, 192, 48);
+// img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F);
+// img = img.expandDims(0);
+// return new NDList(new NDArray[]{img});
+// }
+
+ @Override
+ public NDList processInput(TranslatorContext ctx, Image input) {
+ NDArray img = input.toNDArray(ctx.getNDManager());
+ int imgC = 3;
+ int imgH = 48;
+ int imgW = 192;
+
+ NDArray array = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW));
+
+ int h = input.getHeight();
+ int w = input.getWidth();
+ int resized_w = 0;
+
+ float ratio = (float) w / (float) h;
+ if (Math.ceil(imgH * ratio) > imgW) {
+ resized_w = imgW;
+ } else {
+ resized_w = (int) (Math.ceil(imgH * ratio));
+ }
+
+ img = NDImageUtils.resize(img, resized_w, imgH);
+
+ img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F);
+ // img = img.transpose(2, 0, 1);
+
+ array.set(new NDIndex(":,:,0:" + resized_w), img);
+
+ array = array.expandDims(0);
+
+ return new NDList(new NDArray[]{array});
+ }
+
+ @Override
+ public Batchifier getBatchifier() {
+ return null;
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModel.java
new file mode 100644
index 0000000..03b023b
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/OcrCommonRecModel.java
@@ -0,0 +1,72 @@
+package cn.smartjavaai.ocr.model.common.recognize;
+
+import ai.djl.modality.cv.Image;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.config.OcrRecModelConfig;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.entity.OcrInfo;
+
+import java.awt.image.BufferedImage;
+import java.util.List;
+
+/**
+ * OCR 通用识别模型
+ * @author dwj
+ */
+public interface OcrCommonRecModel {
+
+ /**
+ * 加载模型
+ * @param config
+ */
+ void loadModel(OcrRecModelConfig config); // 加载模型
+
+ /**
+ * 文本识别
+ * @param imagePath 图片路径
+ * @return
+ */
+ default OcrInfo recognize(String imagePath) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本检测
+ * @param image BufferedImage
+ * @return
+ */
+ default OcrInfo recognize(BufferedImage image) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 文本检测
+ * @param imageData 图片字节数组
+ * @return
+ */
+ default OcrInfo recognize(byte[] imageData) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+
+ /**
+ * 识别并绘制结果
+ * @param imagePath
+ * @param outputPath
+ */
+ default void recognizeAndDraw(String imagePath, String outputPath, int fontSize) {
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+ /**
+ * 识别并绘制结果
+ * @param sourceImage
+ * @return
+ */
+ default BufferedImage recognizeAndDraw(BufferedImage sourceImage, int fontSize){
+ throw new UnsupportedOperationException("默认不支持该功能");
+ }
+
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java
new file mode 100644
index 0000000..4712051
--- /dev/null
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/PpOCRV5RecModel.java
@@ -0,0 +1,324 @@
+package cn.smartjavaai.ocr.model.common.recognize;
+
+import ai.djl.Device;
+import ai.djl.MalformedModelException;
+import ai.djl.engine.Engine;
+import ai.djl.inference.Predictor;
+import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
+import ai.djl.modality.cv.output.Point;
+import ai.djl.ndarray.NDArray;
+import ai.djl.ndarray.NDList;
+import ai.djl.ndarray.NDManager;
+import ai.djl.opencv.OpenCVImageFactory;
+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 cn.smartjavaai.common.enums.DeviceEnum;
+import cn.smartjavaai.common.pool.PredictorFactory;
+import cn.smartjavaai.common.utils.FileUtils;
+import cn.smartjavaai.common.utils.ImageUtils;
+import cn.smartjavaai.common.utils.OpenCVUtils;
+import cn.smartjavaai.ocr.config.DirectionModelConfig;
+import cn.smartjavaai.ocr.config.OcrDetModelConfig;
+import cn.smartjavaai.ocr.config.OcrRecModelConfig;
+import cn.smartjavaai.ocr.entity.*;
+import cn.smartjavaai.ocr.exception.OcrException;
+import cn.smartjavaai.ocr.factory.OcrModelFactory;
+import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
+import cn.smartjavaai.ocr.model.common.detect.translator.PPOCRV5DetTranslator;
+import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
+import cn.smartjavaai.ocr.model.common.recognize.translator.PPOCRV5RecTranslator;
+import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
+import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
+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 javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+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.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * PPOCRV5 识别模型
+ * @author dwj
+ * @date 2025/4/21
+ */
+@Slf4j
+public class PpOCRV5RecModel implements OcrCommonRecModel {
+
+ private ObjectPool> recPredictorPool;
+
+ private OcrRecModelConfig config;
+
+ private OcrCommonDetModel detModel;
+
+ private OcrDirectionModel directionModel;
+
+ @Override
+ public void loadModel(OcrRecModelConfig config){
+ if(StringUtils.isBlank(config.getRecModelPath())){
+ throw new OcrException("recModelPath is null");
+ }
+ Device device = null;
+ if(!Objects.isNull(config.getDevice())){
+ device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
+ }
+ this.config = config;
+ //初始化 识别Criteria
+ Criteria recCriteria =
+ Criteria.builder()
+ .optEngine("OnnxRuntime")
+ .setTypes(Image.class, String.class)
+ .optModelPath(Paths.get(config.getRecModelPath()))
+ .optTranslator(new PPOCRV5RecTranslator(new ConcurrentHashMap()))
+ .optProgress(new ProgressBar())
+ .optDevice(device)
+ .build();
+ try{
+ ZooModel recognitionModel = ModelZoo.loadModel(recCriteria);
+ this.recPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(recognitionModel));
+ log.info("当前设备: " + recognitionModel.getNDManager().getDevice());
+ log.info("当前引擎: " + Engine.getInstance().getEngineName());
+ } catch (IOException | ModelNotFoundException | MalformedModelException e) {
+ throw new OcrException("识别模型加载失败", e);
+ }
+
+
+ //获取检测模型
+ if(StringUtils.isNotBlank(config.getDetModelPath()) && Objects.nonNull(config.getDetModelEnum())){
+ OcrDetModelConfig detModelConfig = new OcrDetModelConfig();
+ detModelConfig.setModelEnum(config.getDetModelEnum());
+ detModelConfig.setDetModelPath(config.getDetModelPath());
+ detModel = OcrModelFactory.getInstance().getDetModel(detModelConfig);
+ }
+
+ //获取方向检测模型
+ if(StringUtils.isNotBlank(config.getDirectionModelPath()) && Objects.nonNull(config.getDirectionModelEnum())){
+ DirectionModelConfig directionModelConfig = new DirectionModelConfig();
+ directionModelConfig.setModelEnum(config.getDirectionModelEnum());
+ directionModelConfig.setModelPath(config.getDirectionModelPath());
+ directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
+ }
+ }
+
+
+ @Override
+ public OcrInfo recognize(String imagePath) {
+ if(StringUtils.isBlank(config.getRecModelPath())){
+ throw new OcrException("recModelPath为空,无法识别");
+ }
+ 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);
+ }
+ OcrInfo ocrInfo = recognize(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrInfo;
+ }
+
+ private OcrInfo recognize(Image image) {
+ //检测文本
+ List boxeList = detModel.detect(image);
+ if(Objects.isNull(boxeList) || boxeList.isEmpty()){
+ throw new OcrException("未检测到文本");
+ }
+ Predictor predictor = null;
+ List rotatedBoxes = new ArrayList<>();
+ List ocrItemList = new ArrayList<>();
+ try (NDManager manager = NDManager.newBaseManager()) {
+ Mat srcMat = (Mat) image.getWrappedImage();
+ predictor = recPredictorPool.borrowObject();
+ //检测方向
+ if(directionModel != null){
+ ocrItemList = directionModel.detect(boxeList, srcMat, manager);
+ if(Objects.isNull(ocrItemList) || ocrItemList.isEmpty()){
+ throw new OcrException("方向检测失败");
+ }
+ for (OcrItem ocrItem : ocrItemList){
+ //放射变换+裁剪
+ Image subImage = OcrUtils.transformAndCrop(srcMat, ocrItem.getOcrBox());
+ //纠正文本框
+ subImage = OcrUtils.rotateImg(subImage, ocrItem.getAngle());
+ //识别
+ String name = predictor.predict(subImage);
+ ocrItem.setText(name);
+ NDArray ndArray = manager.create(ocrItem.getOcrBox().toFloatArray());
+ rotatedBoxes.add(new RotatedBox(ndArray, ocrItem.getText()));
+ ((Mat)subImage.getWrappedImage()).release();
+ }
+ }else{
+ for (OcrBox box : boxeList){
+ RotatedBox rotatedBox = recognize(box, srcMat, predictor, manager);
+ rotatedBoxes.add(rotatedBox);
+ }
+ }
+ //后处理
+ return postProcessOcrResult(rotatedBoxes);
+ } catch (Exception e) {
+ throw new OcrException("OCR检测错误", e);
+ }finally {
+ if (predictor != null) {
+ try {
+ recPredictorPool.returnObject(predictor); //归还
+ } catch (Exception e) {
+ log.warn("归还Predictor失败", e);
+ try {
+ predictor.close(); // 归还失败才销毁
+ } catch (Exception ex) {
+ log.error("关闭Predictor失败", ex);
+ }
+ }
+ }
+ }
+ }
+
+
+
+ private RotatedBox recognize(OcrBox box,Mat srcMat,Predictor recPredictor,NDManager manager){
+ try {
+ //透视变换 + 裁剪
+ Image subImg = OcrUtils.transformAndCrop(srcMat, box);
+ //ImageUtils.saveImage(subImg, i + "crop.png", "build/output");
+ //高宽比 > 1.5
+ if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) {
+ //旋转图片90度
+ subImg = OcrUtils.rotateImg(manager, subImg);
+ //ImageUtils.saveImage(subImg, i + "rotate.png", "build/output");
+ }
+ String name = recPredictor.predict(subImg);
+ ((Mat)subImg.getWrappedImage()).release();
+ NDArray pointsArray = manager.create(box.toFloatArray());
+ return new RotatedBox(pointsArray, name);
+ } catch (Exception e) {
+ throw new OcrException("OCR检测错误", e);
+ }
+ }
+
+
+ /**
+ * 后处理:排序,分行
+ * @param rotatedBoxes
+ */
+ private OcrInfo postProcessOcrResult(List rotatedBoxes){
+ //Y坐标升序排序
+ List initList = new ArrayList<>();
+ for (RotatedBox result : rotatedBoxes) {
+ initList.add(result);
+ }
+ Collections.sort(initList);
+ //多行文本框的集合
+ List> lines = new ArrayList<>();
+ List line = new ArrayList<>();
+ RotatedBoxCompX firstBox = new RotatedBoxCompX(initList.get(0).getBox(), initList.get(0).getText());
+ line.add(firstBox);
+ lines.add((ArrayList) line);
+ //分行判断
+ for (int i = 1; i < initList.size(); i++) {
+ RotatedBoxCompX tmpBox = new RotatedBoxCompX(initList.get(i).getBox(), initList.get(i).getText());
+ float y1 = firstBox.getBox().toFloatArray()[1];
+ float y2 = tmpBox.getBox().toFloatArray()[1];
+ float dis = Math.abs(y2 - y1);
+ if (dis < 20) { // 认为是同 1 行 - Considered to be in the same line
+ line.add(tmpBox);
+ } else { // 换行 - Line break
+ firstBox = tmpBox;
+ Collections.sort(line);
+ line = new ArrayList<>();
+ line.add(firstBox);
+ lines.add((ArrayList) line);
+ }
+ }
+ return OcrUtils.convertToOcrInfo(lines);
+ }
+
+
+ @Override
+ public void recognizeAndDraw(String imagePath, String outputPath, int fontSize) {
+ if(!FileUtils.isFileExists(imagePath)){
+ throw new OcrException("图像文件不存在");
+ }
+ try {
+ Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
+ OcrInfo ocrInfo = recognize(img);
+ if(Objects.isNull(ocrInfo) || Objects.isNull(ocrInfo.getLineList()) || ocrInfo.getLineList().isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ Mat wrappedImage = (Mat) img.getWrappedImage();
+ BufferedImage bufferedImage = OcrOpenCVUtils.mat2Image(wrappedImage);
+ OcrUtils.drawRectWithText(bufferedImage, ocrInfo, fontSize);
+ ImageUtils.saveImage(bufferedImage, outputPath);
+ wrappedImage.release();
+ } catch (IOException e) {
+ throw new OcrException(e);
+ }
+ }
+
+ @Override
+ public OcrInfo recognize(BufferedImage image) {
+ if(!ImageUtils.isImageValid(image)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
+ OcrInfo ocrInfo = recognize(img);
+ ((Mat)img.getWrappedImage()).release();
+ return ocrInfo;
+ }
+
+ @Override
+ public OcrInfo recognize(byte[] imageData) {
+ if(Objects.isNull(imageData)){
+ throw new OcrException("图像无效");
+ }
+ try {
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
+ return recognize(image);
+ } catch (IOException e) {
+ throw new OcrException("错误的图像", e);
+ }
+ }
+
+ @Override
+ public BufferedImage recognizeAndDraw(BufferedImage sourceImage, int fontSize) {
+ if(!ImageUtils.isImageValid(sourceImage)){
+ throw new OcrException("图像无效");
+ }
+ Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
+ OcrInfo ocrInfo = recognize(img);
+ if(Objects.isNull(ocrInfo) || Objects.isNull(ocrInfo.getLineList()) || ocrInfo.getLineList().isEmpty()){
+ throw new OcrException("未检测到文字");
+ }
+ try {
+ OcrUtils.drawRectWithText(sourceImage, ocrInfo, fontSize);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 调用 save 方法将 Image 写入字节流
+ img.save(outputStream, "png");
+ // 将字节流转换为 BufferedImage
+ byte[] imageBytes = outputStream.toByteArray();
+ ((Mat) img.getWrappedImage()).release();
+ return ImageIO.read(new ByteArrayInputStream(imageBytes));
+ } catch (IOException e) {
+ throw new OcrException("导出图片失败", e);
+ }
+ }
+}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/translator/PPOCRV5RecTranslator.java
similarity index 95%
rename from smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java
rename to smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/translator/PPOCRV5RecTranslator.java
index 4cc0a0e..dab81d9 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/translator/PaddleOCRV4WordRecTranslator.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/model/common/recognize/translator/PPOCRV5RecTranslator.java
@@ -1,4 +1,4 @@
-package cn.smartjavaai.ocr.ppv4.translator;
+package cn.smartjavaai.ocr.model.common.recognize.translator;
import ai.djl.Model;
import ai.djl.modality.cv.Image;
@@ -23,11 +23,11 @@ import java.util.Map;
* 文字识别前后处理
*
*/
-public class PaddleOCRV4WordRecTranslator implements Translator {
+public class PPOCRV5RecTranslator implements Translator {
private List table;
private final boolean use_space_char;
- public PaddleOCRV4WordRecTranslator(Map arguments) {
+ public PPOCRV5RecTranslator(Map arguments) {
use_space_char =
arguments.containsKey("use_space_char")
? Boolean.parseBoolean(arguments.get("use_space_char").toString())
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
deleted file mode 100644
index 46e4800..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4DetModel.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package cn.smartjavaai.ocr.ppv4.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.ndarray.NDList;
-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 cn.smartjavaai.common.entity.DetectionResponse;
-import cn.smartjavaai.common.pool.PredictorFactory;
-import cn.smartjavaai.common.utils.FileUtils;
-import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
-import cn.smartjavaai.ocr.exception.OcrException;
-import cn.smartjavaai.ocr.detection.OcrDetModel;
-import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
-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 PaddleOCRV4DetModel implements OcrDetModel {
-
- private ZooModel detectionModel;
-
- private ObjectPool> predictorPool;
-
- @Override
- public void loadModel(OcrDetModelConfig config){
- if(StringUtils.isBlank(config.getModelPath())){
- throw new OcrException("modelPath is null");
- }
- Criteria criteria =
- Criteria.builder()
- .optEngine("OnnxRuntime")
- .setTypes(Image.class, NDList.class)
- .optModelPath(Paths.get(config.getModelPath()))
- .optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap()))
- .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);
- }
-
- private DetectionResponse detect(Image image){
- Predictor 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.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().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);
- }
- }
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java
deleted file mode 100644
index cdd3678..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/ppv4/model/PaddleOCRV4RecModel.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package cn.smartjavaai.ocr.ppv4.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.ndarray.NDList;
-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 cn.smartjavaai.common.entity.DetectionResponse;
-import cn.smartjavaai.common.pool.PredictorFactory;
-import cn.smartjavaai.common.utils.FileUtils;
-import cn.smartjavaai.ocr.detection.OcrDetModel;
-import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
-import cn.smartjavaai.ocr.exception.OcrException;
-import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
-import cn.smartjavaai.ocr.recognition.OcrRecModel;
-import cn.smartjavaai.ocr.recognition.OcrRecModelConfig;
-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;
-
-/**
- * PaddleOCRV4 识别模型实现
- * @author dwj
- * @date 2025/4/21
- */
-@Slf4j
-public class PaddleOCRV4RecModel implements OcrRecModel {
-
- private ZooModel detectionModel;
-
- private ObjectPool> predictorPool;
-
- @Override
- public void loadModel(OcrRecModelConfig config){
- if(StringUtils.isBlank(config.getModelPath())){
- throw new OcrException("modelPath is null");
- }
- Criteria criteria =
- Criteria.builder()
- .optEngine("OnnxRuntime")
- .setTypes(Image.class, NDList.class)
- .optModelPath(Paths.get(config.getModelPath()))
- .optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap()))
- .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);
- }
-
- private DetectionResponse detect(Image image){
- Predictor 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.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().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);
- }
- }
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java
deleted file mode 100644
index 09da861..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModel.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package cn.smartjavaai.ocr.recognition;
-
-import cn.smartjavaai.common.entity.DetectionResponse;
-
-/**
- * OCR模型
- * @author dwj
- */
-public interface OcrRecModel {
-
- /**
- * 加载模型
- * @param config
- */
- void loadModel(OcrRecModelConfig config); // 加载模型
-
- /**
- * 人脸检测
- * @param imagePath 图片路径
- * @return
- */
- default DetectionResponse detect(String imagePath) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
- /**
- * 检测并绘制结果
- * @param imagePath 图片输入路径(包含文件名称)
- * @param outputPath 图片输出路径(包含文件名称)
- */
- default void detectAndDraw(String imagePath, String outputPath) {
- throw new UnsupportedOperationException("默认不支持该功能");
- }
-
-
-
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java
deleted file mode 100644
index 1a1616f..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelConfig.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package cn.smartjavaai.ocr.recognition;
-
-import cn.smartjavaai.common.enums.DeviceEnum;
-import lombok.Data;
-
-/**
- * @author dwj
- * @date 2025/4/22
- */
-@Data
-public class OcrRecModelConfig {
-
- /**
- * 模型名称
- */
- private OcrRecModelEnum modelEnum;
-
- /**
- * 设备类型
- */
- private DeviceEnum device;
-
- /**
- * 模型路径
- */
- private String modelPath;
-
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java
deleted file mode 100644
index 4083693..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/recognition/OcrRecModelFactory.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package cn.smartjavaai.ocr.recognition;
-
-import cn.smartjavaai.common.config.Config;
-import cn.smartjavaai.ocr.exception.OcrException;
-import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * OCR模型工厂
- * @author dwj
- */
-@Slf4j
-public class OcrRecModelFactory {
-
- // 使用 volatile 和双重检查锁定来确保线程安全的单例模式
- private static volatile OcrRecModelFactory instance;
-
- private static final ConcurrentHashMap modelMap = new ConcurrentHashMap<>();
-
- /**
- * 算法注册表
- */
- private static final Map> registry =
- new ConcurrentHashMap<>();
-
-
- public static OcrRecModelFactory getInstance() {
- if (instance == null) {
- synchronized (OcrRecModelFactory.class) {
- if (instance == null) {
- instance = new OcrRecModelFactory();
- }
- }
- }
- return instance;
- }
-
-
-
- /**
- * 注册算法
- * @param name
- * @param clazz
- */
- private static void registerModel(String name, Class extends OcrRecModel> clazz) {
- registry.put(name.toLowerCase(), clazz);
- }
-
-
- /**
- * 获取模型(通过配置)
- * @param config
- * @return
- */
- public OcrRecModel getModel(OcrRecModelConfig 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 OcrRecModel createFaceModel(OcrRecModelConfig config) {
- Class> clazz = registry.get(config.getModelEnum().name().toLowerCase());
- if(clazz == null){
- throw new OcrException("Unsupported model");
- }
- OcrRecModel algorithm = null;
- try {
- algorithm = (OcrRecModel) clazz.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new OcrException(e);
- }
- algorithm.loadModel(config);
- return algorithm;
- }
-
-
- // 初始化默认算法
- static {
- //registerModel("PADDLEOCR_V4_REC_MODEL", PaddleOCRV4DetModel.class);
- log.info("缓存目录:{}", Config.getCachePath());
- }
-
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
deleted file mode 100644
index 2bc9f86..0000000
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/ImageUtils.java
+++ /dev/null
@@ -1,221 +0,0 @@
-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.DetectionInfo;
-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(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
- DetectionRectangle detectionRectangle = detectionInfo.getDetectionRectangle();
- // 左上角点
- 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);
- }
- }
-}
diff --git a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
index 8fe4ca0..9f16d13 100644
--- a/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
+++ b/smartjavaai-ocr/src/main/java/cn/smartjavaai/ocr/utils/OcrUtils.java
@@ -1,15 +1,30 @@
package cn.smartjavaai.ocr.utils;
import ai.djl.modality.cv.Image;
+import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
+import ai.djl.modality.cv.util.NDImageUtils;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
-import cn.smartjavaai.common.entity.DetectionInfo;
-import cn.smartjavaai.common.entity.DetectionRectangle;
-import cn.smartjavaai.common.entity.DetectionResponse;
+import ai.djl.ndarray.NDManager;
+import ai.djl.opencv.OpenCVImageFactory;
+import cn.smartjavaai.common.entity.*;
+import cn.smartjavaai.common.entity.Point;
+import cn.smartjavaai.ocr.entity.OcrBox;
+import cn.smartjavaai.ocr.entity.OcrInfo;
+import cn.smartjavaai.ocr.entity.OcrItem;
+import cn.smartjavaai.ocr.entity.RotatedBoxCompX;
+import cn.smartjavaai.ocr.enums.AngleEnum;
+import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
+import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
import lombok.extern.slf4j.Slf4j;
+import org.opencv.core.Mat;
+import org.opencv.core.Scalar;
+import org.opencv.imgproc.Imgproc;
+import java.awt.*;
+import java.awt.image.BufferedImage;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
@@ -25,40 +40,243 @@ public class OcrUtils {
/**
- * 转换为FaceDetectedResult
+ * 转换为OcrBox
* @param dt_boxes
* @param img
* @return
*/
- public static DetectionResponse convertToDetectionResponse(NDList dt_boxes, Image img){
+ public static List convertToOcrBox(NDList dt_boxes, Image img){
if(Objects.isNull(dt_boxes) || dt_boxes.size() == 0){
return null;
}
- DetectionResponse detectionResponse = new DetectionResponse();
- List detectionInfoList = new ArrayList();
+ List boxList = new ArrayList();
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);
- detectionInfoList.add(new DetectionInfo(rectangle));
+ float[] pointsArr = box.toFloatArray();
+ //log.info("points: {}", pointsArr);
+ float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
+ float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
+ float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
+ float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
+ OcrBox ocrBox = new OcrBox(new Point(lt[0], lt[1]), new Point(rt[0], rt[1]), new Point(rb[0], rb[1]), new Point(lb[0], lb[1]));
+ boxList.add(ocrBox);
+ }
+ return boxList;
+ }
+
+ /**
+ * 欧式距离计算
+ *
+ * @param point1
+ * @param point2
+ * @return
+ */
+ public static float distance(float[] point1, float[] point2) {
+ float disX = point1[0] - point2[0];
+ float disY = point1[1] - point2[1];
+ float dis = (float) Math.sqrt(disX * disX + disY * disY);
+ return dis;
+ }
+
+ /**
+ * 图片旋转
+ *
+ * @param 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);
+ }
+
+ /**
+ * 逆时针旋转图片
+ *
+ * @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 image
+ * @param angleEnum
+ * @return
+ */
+ public static Image rotateImg(Image image, AngleEnum angleEnum) {
+ try (NDManager manager = NDManager.newBaseManager()) {
+ int times = 0;
+ switch (angleEnum) {
+ case ANGLE_90:
+ times = 1;
+ break;
+ case ANGLE_180:
+ times = 2;
+ break;
+ case ANGLE_270:
+ times = 3;
+ break;
+ }
+ NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), times);
+ return OpenCVImageFactory.getInstance().fromNDArray(rotated);
+ }
+ }
+
+
+ /**
+ * 转换为OcrInfo
+ * @param lines
+ * @return
+ */
+ public static OcrInfo convertToOcrInfo(List> lines){
+ if(Objects.isNull(lines) || lines.size() == 0){
+ return null;
+ }
+ List detectionInfoList = new ArrayList();
+ List> lineList = new ArrayList>();
+ String fullText = "";
+ for(ArrayList boxList : lines){
+ List line = new ArrayList();
+ for(RotatedBoxCompX box : boxList){
+ float[] pointsArr = box.getBox().toFloatArray();
+ float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
+ float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
+ float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
+ float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
+ OcrBox ocrBox = new OcrBox(new Point(lt[0], lt[1]), new Point(rt[0], rt[1]), new Point(rb[0], rb[1]), new Point(lb[0], lb[1]));
+ OcrItem ocrItem = new OcrItem(ocrBox, box.getText());
+ line.add(ocrItem);
+ String text = box.getText();
+ if(text.trim().equals(""))
+ continue;
+ fullText += text + " ";
+ }
+ lineList.add(line);
+ fullText += '\n';
+ }
+ return new OcrInfo(lineList, fullText);
+ }
+
+
+ /**
+ * 放射变换+裁剪
+ * @param srcMat
+ * @param box
+ * @return
+ */
+ public static Image transformAndCrop(Mat srcMat, OcrBox box){
+ float[] pointsArr = box.toFloatArray();
+ float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
+ float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
+ float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
+ float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
+ int img_crop_width = (int) Math.max(OcrUtils.distance(lt, rt), OcrUtils.distance(rb, lb));
+ int img_crop_height = (int) Math.max(OcrUtils.distance(lt, lb), OcrUtils.distance(rt, rb));
+ List srcPoints = new ArrayList<>();
+ srcPoints.add(new ai.djl.modality.cv.output.Point(lt[0], lt[1]));
+ srcPoints.add(new ai.djl.modality.cv.output.Point(rt[0], rt[1]));
+ srcPoints.add(new ai.djl.modality.cv.output.Point(rb[0], rb[1]));
+ srcPoints.add(new ai.djl.modality.cv.output.Point(lb[0], lb[1]));
+ List dstPoints = new ArrayList<>();
+ dstPoints.add(new ai.djl.modality.cv.output.Point(0, 0));
+ dstPoints.add(new ai.djl.modality.cv.output.Point(img_crop_width, 0));
+ dstPoints.add(new ai.djl.modality.cv.output.Point(img_crop_width, img_crop_height));
+ dstPoints.add(new ai.djl.modality.cv.output.Point(0, img_crop_height));
+ Mat srcPoint2f = OcrNDArrayUtils.toMat(srcPoints);
+ Mat dstPoint2f = OcrNDArrayUtils.toMat(dstPoints);
+ //透视变换
+ Mat cvMat = OcrOpenCVUtils.perspectiveTransform(srcMat, srcPoint2f, dstPoint2f);
+ Image subImg = OpenCVImageFactory.getInstance().fromImage(cvMat);
+ //ImageUtils.saveImage(subImg, i + ".png", "build/output");
+ //变换后裁剪
+ subImg = subImg.getSubImage(0, 0, img_crop_width, img_crop_height);
+ cvMat.release();
+ srcPoint2f.release();
+ dstPoint2f.release();
+ return subImg;
+ }
+
+ /**
+ * 绘制文本框
+ *
+ * @param mat
+ * @param boxList
+ */
+ public static void drawRect(Mat mat, List boxList) {
+ for(OcrBox ocrBox : boxList){
+ Imgproc.line(mat, ocrBox.getTopLeft().toCvPoint(), ocrBox.getTopRight().toCvPoint(), new Scalar(0, 255, 0), 1);
+ Imgproc.line(mat, ocrBox.getTopRight().toCvPoint(), ocrBox.getBottomRight().toCvPoint(), new Scalar(0, 255, 0),1);
+ Imgproc.line(mat, ocrBox.getBottomRight().toCvPoint(), ocrBox.getBottomLeft().toCvPoint(), new Scalar(0, 255, 0),1);
+ Imgproc.line(mat, ocrBox.getBottomLeft().toCvPoint(), ocrBox.getTopLeft().toCvPoint(), new Scalar(0, 255, 0), 1);
+ }
+ }
+
+
+ /**
+ * 绘制文本框及文本
+ * @param image
+ * @param ocrInfo
+ */
+ public static void drawRectWithText(BufferedImage image, OcrInfo ocrInfo, int fontSize) {
+ // 将绘制图像转换为Graphics2D
+ Graphics2D g = (Graphics2D) image.getGraphics();
+ try {
+ 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);
+ for(List ocrItemList : ocrInfo.getLineList()){
+ for(OcrItem item : ocrItemList){
+ OcrBox box = item.getOcrBox();
+ int[] xPoints = {
+ (int)box.getTopLeft().getX(),
+ (int)box.getTopRight().getX(),
+ (int)box.getBottomRight().getX(),
+ (int)box.getBottomLeft().getX(),
+ (int)box.getTopLeft().getX()
+ };
+ int[] yPoints = {
+ (int)box.getTopLeft().getY(),
+ (int)box.getTopRight().getY(),
+ (int)box.getBottomRight().getY(),
+ (int)box.getBottomLeft().getY(),
+ (int)box.getTopLeft().getY()
+ };
+ g.drawPolyline(xPoints, yPoints, 5);
+ g.drawString(item.getText(), xPoints[0], yPoints[0]);
+ }
+ }
+ } finally {
+ g.dispose();
+ }
+ }
+
+
+ /**
+ * 绘制文本框及文本
+ * @param srcMat
+ * @param itemList
+ */
+ public static void drawRectWithText(Mat srcMat, List itemList) {
+ for(OcrItem item : itemList){
+ OcrBox ocrBox = item.getOcrBox();
+ Imgproc.line(srcMat, ocrBox.getTopLeft().toCvPoint(), ocrBox.getTopRight().toCvPoint(), new Scalar(0, 255, 0), 1);
+ Imgproc.line(srcMat, ocrBox.getTopRight().toCvPoint(), ocrBox.getBottomRight().toCvPoint(), new Scalar(0, 255, 0),1);
+ Imgproc.line(srcMat, ocrBox.getBottomRight().toCvPoint(), ocrBox.getBottomLeft().toCvPoint(), new Scalar(0, 255, 0),1);
+ Imgproc.line(srcMat, ocrBox.getBottomLeft().toCvPoint(), ocrBox.getTopLeft().toCvPoint(), new Scalar(0, 255, 0), 1);
+ // 中文乱码
+ Imgproc.putText(srcMat, item.getAngle().getValue(), ocrBox.getTopLeft().toCvPoint(), Imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX, 1.0, new Scalar(0, 255, 0), 1);
}
- detectionResponse.setDetectionInfoList(detectionInfoList);
- return detectionResponse;
}