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 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 clazz) { + commonDetRegistry.put(name.toLowerCase(), clazz); + } + + /** + * 注册通用识别模型 + * @param name + * @param clazz + */ + private static void registerCommonRecModel(String name, Class clazz) { + commonRecRegistry.put(name.toLowerCase(), clazz); + } + + /** + * 注册通用方向分类模型 + * @param name + * @param clazz + */ + private static void registerDirectionModel(String name, Class 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 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; }